@iexec/iapp
Version:
A CLI to guide you through the process of building an iExec iApp
209 lines (195 loc) • 8.03 kB
text/typescript
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { init } from './cmd/init.js';
import { deploy } from './cmd/deploy.js';
import { run } from './cmd/run.js';
import { test } from './cmd/test.js';
import { mockProtectedData } from './cmd/mock-protected-data.js';
import { debug } from './cmd/debug.js';
import { SUPPORTED_CHAINS } from './config/config.js';
import { checkPackageUpdate } from './cli-helpers/checkPackageUpdate.js';
import { walletImport } from './cmd/wallet-import.js';
import { walletSelect } from './cmd/wallet-select.js';
await checkPackageUpdate();
const coerceRequesterSecret = (
values: string[]
): { key: number; value: string }[] => {
// create Array<{key: number, value: string}> from the values Array<string>
const secrets = values.reduce(
(acc, curr) => {
const separatorIndex = curr.indexOf('=');
const key = Number(curr.slice(0, separatorIndex));
const value = curr.slice(separatorIndex + 1);
if (!Number.isInteger(key) || key < 1) {
throw Error(
`invalid secret index ${key} in requesterSecret \`${curr}\``
);
}
if (value === undefined) {
throw Error(
`invalid secret value ${value} in requesterSecret \`${curr}\``
);
}
return [...acc, { key, value }];
},
[] as { key: number; value: string }[]
);
return secrets;
};
const yargsInstance = yargs(hideBin(process.argv));
yargsInstance
.locale('en') // set local to American English (no i18n)
.scriptName('iapp')
.usage('$0 <cmd> [args]')
// Initialize command
.command('init', 'Initialize your app structure', () => {}, init)
// Test command
.command({
command: 'test',
describe: 'Test your app',
builder: (y) => {
return y
.option('args', {
describe: `Arguments that will be accessible into the iApp. Spaces separates arguments, use quotes to group arguments (Ex: \`--args '"foo bar" baz'\` will interpret "foo bar" as first arg, "bar" as second arg)`,
type: 'string',
})
.option('protectedData', {
describe:
'Specify the protected data mock name (use "default" protected data mock or create custom mocks with `iapp mock protectedData`) ',
type: 'string',
})
.option('inputFile', {
describe:
'Specify one or multiple input files (publicly-accessible URLs). Input files are accessible to the iApp as local files using path specified in environment variables (Ex: `--inputFile https://foo.com/fileA.txt https://bar.io/fileB.json` will download the file at "https://foo.com/fileA.txt" and make it available for the iApp at `$IEXEC_IN/$IEXEC_INPUT_FILE_NAME_1`, same for "https://bar.io/fileB.json" at `$IEXEC_IN/$IEXEC_INPUT_FILE_NAME_2`)',
type: 'string',
requiresArg: true, // must be invoked with a value
array: true,
})
.option('requesterSecret', {
describe:
'Specify one or multiple key-value requester secrets. Use syntax `secretIndex=value`, `secretIndex` is a public strictly positive integer, `value` is a secret only available in the iApp (Ex: `--requesterSecret 1=foo 42=bar` will set the following environment variables in iApp `IEXEC_REQUESTER_SECRET_1=foo` and `IEXEC_REQUESTER_SECRET_42=bar`).',
type: 'string',
requiresArg: true, // must be invoked with a value
array: true,
coerce: coerceRequesterSecret,
});
},
handler: (y) => test(y),
})
// Build and publish docker image
.command({
command: 'deploy',
describe: 'Transform you app into a TEE app and deploy it on iExec',
builder: (y) => {
return y.option('chain', {
describe:
'Specify the blockchain on which the iApp will be deployed (overrides defaultChain configuration)',
type: 'string',
choices: SUPPORTED_CHAINS,
});
},
handler: (y) => deploy(y),
})
// Run a published docker image
.command({
command: 'run <iAppAddress>',
describe: 'Run your deployed iApp',
builder: (y) => {
return y
.positional('iAppAddress', {
describe: 'The iApp address to run',
type: 'string',
demandOption: true,
})
.option('args', {
describe: `Arguments that will be accessible into the iApp. Spaces separates arguments, use quotes to group arguments (Ex: \`--args '"foo bar" baz'\` will interpret "foo bar" as first arg, "bar" as second arg)`,
type: 'string',
})
.option('protectedData', {
describe: 'Specify the protected data address',
type: 'string',
})
.option('inputFile', {
describe:
'Specify one or multiple input files (publicly-accessible URLs). Input files are accessible to the iApp as local files using path specified in environment variables (Ex: `--inputFile https://foo.com/fileA.txt https://bar.io/fileB.json` will download the file at "https://foo.com/fileA.txt" and make it available for the iApp at `$IEXEC_IN/$IEXEC_INPUT_FILE_NAME_1`, same for "https://bar.io/fileB.json" at `$IEXEC_IN/$IEXEC_INPUT_FILE_NAME_2`)',
type: 'string',
requiresArg: true, // must be invoked with a value
array: true,
})
.option('requesterSecret', {
describe:
'Specify one or multiple key-value requester secrets. Use syntax `secretIndex=value`, `secretIndex` is a public strictly positive integer, `value` is a secret only available in the iApp (Ex: `--requesterSecret 1=foo 42=bar` will set the following environment variables in iApp `IEXEC_REQUESTER_SECRET_1=foo` and `IEXEC_REQUESTER_SECRET_42=bar`).',
type: 'string',
requiresArg: true, // must be invoked with a value
array: true,
coerce: coerceRequesterSecret,
})
.option('chain', {
describe:
'Specify the blockchain on which the iApp is deployed (overrides defaultChain configuration)',
type: 'string',
choices: SUPPORTED_CHAINS,
});
},
handler: (y) => run(y),
})
.command({
command: 'debug <taskId>',
describe:
'Retrieve detailed execution logs from worker nodes for a specific task',
builder: (y) => {
return y
.positional('taskId', {
describe: 'Unique identifier of the task to debug',
type: 'string',
demandOption: true,
})
.option('chain', {
describe:
'Specify the blockchain on which the task is registered (overrides defaultChain configuration)',
type: 'string',
choices: SUPPORTED_CHAINS,
});
},
handler: (y) => debug(y),
})
.command({
command: 'mock <inputType>',
describe: 'Create a mocked input for test',
builder: (y) =>
y.positional('inputType', {
describe: 'Type of input to mock',
choices: ['protectedData'],
}),
handler: (y) => {
if (y.inputType === 'protectedData') {
return mockProtectedData();
}
},
})
.command({
command: 'wallet <action>',
describe: 'Manage wallet',
builder: (y) =>
y.positional('action', {
describe: 'Import a new wallet or select one from the keystore',
choices: ['import', 'select'],
}),
handler: (y) => {
if (y.action === 'import') {
return walletImport();
}
if (y.action === 'select') {
return walletSelect();
}
},
})
.help()
.completion('completion', false) // create hidden "completion" command
.alias('help', 'h')
.alias('version', 'v')
.strict() // show help if iapp is invoked with an invalid subcommand
.demandCommand(1, 'Missing subcommand') // show help if iapp is invoked without subcommand is invoked
.wrap(yargsInstance.terminalWidth()) // use full terminal size rather than default 80
.parse();