diff --git a/src/api/ic.api.ts b/src/api/ic.api.ts new file mode 100644 index 0000000..2db8937 --- /dev/null +++ b/src/api/ic.api.ts @@ -0,0 +1,23 @@ +import {ICManagementCanister} from '@dfinity/ic-management'; +import type {Principal} from '@dfinity/principal'; +import {initAgent} from '../utils/actor.utils'; + +export const canisterStop = async ({canisterId}: {canisterId: Principal}): Promise => { + const agent = await initAgent(); + + const {stopCanister} = ICManagementCanister.create({ + agent + }); + + await stopCanister(canisterId); +}; + +export const canisterStart = async ({canisterId}: {canisterId: Principal}): Promise => { + const agent = await initAgent(); + + const {startCanister} = ICManagementCanister.create({ + agent + }); + + await startCanister(canisterId); +}; diff --git a/src/commands/start-stop.ts b/src/commands/start-stop.ts new file mode 100644 index 0000000..72e624d --- /dev/null +++ b/src/commands/start-stop.ts @@ -0,0 +1,38 @@ +import {nextArg} from '@junobuild/cli-tools'; +import {red} from 'kleur'; +import {logHelpStart} from '../help/start.help'; +import {logHelpStop} from '../help/stop.help'; +import { + startStopMissionControl, + startStopOrbiter, + startStopSatellite +} from '../services/start-stop.services'; +import type {StartStopAction} from '../types/start-stop'; + +export const startStop = async ({args, action}: {args?: string[]; action: StartStopAction}) => { + const target = nextArg({args, option: '-t'}) ?? nextArg({args, option: '--target'}); + + switch (target) { + case 's': + case 'satellite': + await startStopSatellite({args, action}); + break; + case 'm': + case 'mission-control': + await startStopMissionControl({args, action}); + break; + case 'o': + case 'orbiter': + await startStopOrbiter({args, action}); + break; + default: + console.log(`${red('Unknown target.')}`); + + if (action === 'stop') { + logHelpStop(args); + return; + } + + logHelpStart(args); + } +}; diff --git a/src/help/start.help.ts b/src/help/start.help.ts new file mode 100644 index 0000000..81e33fc --- /dev/null +++ b/src/help/start.help.ts @@ -0,0 +1,34 @@ +import {cyan, green, magenta, yellow} from 'kleur'; +import {helpMode, helpOutput} from './common.help'; +import {TITLE} from './help'; + +export const START_DESCRIPTION = 'Start a module.'; + +const usage = `Usage: ${green('juno')} ${cyan('start')} ${yellow('[options]')} + +Options: + ${yellow('-t, --target')} Which module type should be started? Valid targets are ${magenta('satellite')}, ${magenta('mission-control')} or ${magenta('orbiter')}. + ${helpMode} + ${yellow('-h, --help')} Output usage information. + +Notes: + +- Targets can be shortened to ${magenta('s')} for satellite, ${magenta('m')} for mission-control and ${magenta('o')} for orbiter.`; + +const doc = `${START_DESCRIPTION} + +\`\`\`bash +${usage} +\`\`\` +`; + +const help = `${TITLE} + +${START_DESCRIPTION} + +${usage} +`; + +export const logHelpStart = (args?: string[]) => { + console.log(helpOutput(args) === 'doc' ? doc : help); +}; diff --git a/src/help/stop.help.ts b/src/help/stop.help.ts new file mode 100644 index 0000000..835b55d --- /dev/null +++ b/src/help/stop.help.ts @@ -0,0 +1,34 @@ +import {cyan, green, magenta, yellow} from 'kleur'; +import {helpMode, helpOutput} from './common.help'; +import {TITLE} from './help'; + +export const STOP_DESCRIPTION = 'Stop a module.'; + +const usage = `Usage: ${green('juno')} ${cyan('stop')} ${yellow('[options]')} + +Options: + ${yellow('-t, --target')} Which module type should be stopped? Valid targets are ${magenta('satellite')}, ${magenta('mission-control')} or ${magenta('orbiter')}. + ${helpMode} + ${yellow('-h, --help')} Output usage information. + +Notes: + +- Targets can be shortened to ${magenta('s')} for satellite, ${magenta('m')} for mission-control and ${magenta('o')} for orbiter.`; + +const doc = `${STOP_DESCRIPTION} + +\`\`\`bash +${usage} +\`\`\` +`; + +const help = `${TITLE} + +${STOP_DESCRIPTION} + +${usage} +`; + +export const logHelpStop = (args?: string[]) => { + console.log(helpOutput(args) === 'doc' ? doc : help); +}; diff --git a/src/index.ts b/src/index.ts index f018dc8..31b9b3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import {deploy} from './commands/deploy'; import {dev} from './commands/dev'; import {init} from './commands/init'; import {open} from './commands/open'; +import {startStop} from './commands/start-stop'; import {upgrade} from './commands/upgrade'; import {use} from './commands/use'; import {version as versionCommand} from './commands/version'; @@ -20,6 +21,8 @@ import {logHelpInit} from './help/init.help'; import {logHelpLogin} from './help/login.help'; import {logHelpLogout} from './help/logout.help'; import {logHelpOpen} from './help/open.help'; +import {logHelpStart} from './help/start.help'; +import {logHelpStop} from './help/stop.help'; import {logHelpUpgrade} from './help/upgrade.help'; import {logHelpUse} from './help/use.help'; import {logHelpVersion} from './help/version.help'; @@ -90,6 +93,12 @@ export const run = async () => { case 'whoami': logHelpWhoAmI(args); break; + case 'stop': + logHelpStop(args); + break; + case 'start': + logHelpStart(args); + break; default: console.log(`${red('Unknown command.')}`); console.log(help); @@ -131,6 +140,12 @@ export const run = async () => { case 'use': await use(args); break; + case 'stop': + await startStop({args, action: 'stop'}); + break; + case 'start': + await startStop({args, action: 'start'}); + break; case 'dev': await dev(args); break; diff --git a/src/services/start-stop.services.ts b/src/services/start-stop.services.ts new file mode 100644 index 0000000..af4c388 --- /dev/null +++ b/src/services/start-stop.services.ts @@ -0,0 +1,106 @@ +import {Principal} from '@dfinity/principal'; +import {assertNonNullish, isNullish} from '@junobuild/utils'; +import {cyan, red} from 'kleur'; +import ora from 'ora'; +import {canisterStart, canisterStop} from '../api/ic.api'; +import {getCliMissionControl, getCliOrbiters} from '../configs/cli.config'; +import {junoConfigExist, readJunoConfig} from '../configs/juno.config'; +import type {AssetKey} from '../types/asset-key'; +import type {StartStopAction} from '../types/start-stop'; +import {configEnv} from '../utils/config.utils'; +import {consoleNoConfigFound} from '../utils/msg.utils'; +import {satelliteParameters} from '../utils/satellite.utils'; + +export const startStopMissionControl = async ({ + action +}: { + args?: string[]; + action: StartStopAction; +}) => { + const missionControl = await getCliMissionControl(); + + if (isNullish(missionControl)) { + console.log(`${red(`A mission control must be set in your configuration.`)}`); + process.exit(1); + } + + await startStop({ + action, + segment: 'mission_control', + canisterId: missionControl + }); +}; + +export const startStopOrbiter = async ({action}: {args?: string[]; action: StartStopAction}) => { + const authOrbiters = await getCliOrbiters(); + + if (authOrbiters === undefined || authOrbiters.length === 0) { + return; + } + + if (authOrbiters.length > 0) { + console.log(`${red(`The CLI supports only one orbiter per project. Reach out to Juno.`)}`); + process.exit(1); + } + + const [orbiter] = authOrbiters; + + await startStop({ + action, + segment: 'orbiter', + canisterId: orbiter.p + }); +}; + +export const startStopSatellite = async ({ + args, + action +}: { + args?: string[]; + action: StartStopAction; +}) => { + if (!(await junoConfigExist())) { + consoleNoConfigFound(); + return; + } + + const env = configEnv(args); + const {satellite: satelliteConfig} = await readJunoConfig(env); + + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); + const {satelliteId} = satellite; + + // TS guard. satelliteParameters exit if satelliteId is undefined. + assertNonNullish(satelliteId); + + await startStop({ + action, + segment: 'satellite', + canisterId: satelliteId + }); +}; + +const startStop = async ({ + action, + segment, + canisterId +}: { + action: StartStopAction; + canisterId: string; + segment: AssetKey; +}) => { + const spinner = ora(`${action === 'stop' ? 'Stopping' : 'Starting'} satellite...`).start(); + + try { + const fn = action === 'stop' ? canisterStop : canisterStart; + await fn({canisterId: Principal.fromText(canisterId)}); + } finally { + spinner.stop(); + } + + const capitalize = (str: string): string => str[0].toUpperCase() + str.slice(1); + + console.log( + `${capitalize(segment)} ${canisterId} ${cyan(action === 'stop' ? 'stopped' : 'started')}.` + ); +}; diff --git a/src/types/start-stop.ts b/src/types/start-stop.ts new file mode 100644 index 0000000..6d0a842 --- /dev/null +++ b/src/types/start-stop.ts @@ -0,0 +1 @@ +export type StartStopAction = 'start' | 'stop';