@magclaw/cli-core 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +197 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magclaw/cli-core",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Shared local MagClaw CLI implementation used by daemon and computer packages.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -52,6 +52,7 @@ export const CAPABILITIES = [
52
52
  'agent:stop',
53
53
  'agent:skills:list',
54
54
  'daemon:upgrade',
55
+ 'daemon:close',
55
56
  'daemon:release_notice',
56
57
  'machine:runtime_models:detect',
57
58
  ];
@@ -403,6 +404,12 @@ export function profilePaths(profile = DEFAULT_PROFILE, env = process.env) {
403
404
  };
404
405
  }
405
406
 
407
+ function sleepSync(ms) {
408
+ const timeout = Math.max(0, Number(ms) || 0);
409
+ if (!timeout) return;
410
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, timeout);
411
+ }
412
+
406
413
  function machineFingerprintValue() {
407
414
  return `mfp_${crypto.createHash('sha256')
408
415
  .update([
@@ -585,6 +592,7 @@ function renderHelp() {
585
592
  ' --server-url <url> MagClaw Cloud URL',
586
593
  ' --api-key <key> Machine API key for connect',
587
594
  ' --background Install and run as a background service',
595
+ ' --disable With stop: suppress background relaunch until next start',
588
596
  ' --bin-dir <path> install-cli target directory',
589
597
  ' --to <version> Upgrade target version (default: latest)',
590
598
  ' --wait-cloud Wait for Cloud heartbeat during manual upgrade',
@@ -671,6 +679,10 @@ async function readServiceState(profile = DEFAULT_PROFILE, env = process.env) {
671
679
  previousPackageSpec: state.previousPackageSpec || '',
672
680
  installedDaemonVersion: state.installedDaemonVersion || DAEMON_VERSION,
673
681
  installedPackageVersion: state.installedPackageVersion || packageVersion || state.installedDaemonVersion || DAEMON_VERSION,
682
+ remoteClosed: Boolean(state.remoteClosed),
683
+ remoteClosedAt: state.remoteClosedAt || '',
684
+ remoteCloseReason: state.remoteCloseReason || '',
685
+ remoteCloseCommandId: state.remoteCloseCommandId || '',
674
686
  updatedAt: state.updatedAt || '',
675
687
  };
676
688
  }
@@ -689,6 +701,31 @@ async function writeServiceState(profile = DEFAULT_PROFILE, patch = {}, env = pr
689
701
  return next;
690
702
  }
691
703
 
704
+ async function clearRemoteClosedServiceState(profile = DEFAULT_PROFILE, env = process.env) {
705
+ return writeServiceState(profile, {
706
+ remoteClosed: false,
707
+ remoteClosedAt: '',
708
+ remoteCloseReason: '',
709
+ remoteCloseCommandId: '',
710
+ }, env);
711
+ }
712
+
713
+ async function markForegroundServiceState(profile = DEFAULT_PROFILE, env = process.env) {
714
+ const packageInfo = runtimePackageInfo(env);
715
+ return writeServiceState(profile, {
716
+ mode: 'foreground',
717
+ background: false,
718
+ launcher: 'foreground',
719
+ packageSpec: packageInfo.spec,
720
+ packageName: packageInfo.name,
721
+ packageVersion: packageInfo.version,
722
+ packageKind: packageInfo.kind,
723
+ packageBin: packageInfo.bin,
724
+ installedDaemonVersion: packageInfo.version || DAEMON_VERSION,
725
+ installedPackageVersion: packageInfo.version || DAEMON_VERSION,
726
+ }, env);
727
+ }
728
+
692
729
  async function readUpgradeHandoff(profile = DEFAULT_PROFILE, env = process.env) {
693
730
  const paths = profilePaths(profile, env);
694
731
  const handoff = await readJsonFile(paths.upgradeHandoff, null);
@@ -1185,6 +1222,27 @@ export function toWebSocketUrl(serverUrl, config = {}) {
1185
1222
  if (/^mfp_[a-f0-9]{64}$/i.test(machineFingerprint)) {
1186
1223
  url.searchParams.set('machine_fingerprint', machineFingerprint.toLowerCase());
1187
1224
  }
1225
+ const packageName = String(config.packageName || '').trim();
1226
+ const packageVersion = String(config.packageVersion || config.daemonVersion || '').trim();
1227
+ const packageKind = String(config.packageKind || '').trim();
1228
+ const packageSpec = String(config.packageSpec || '').trim();
1229
+ const packageBin = String(config.packageBin || '').trim();
1230
+ const cliCoreVersion = String(config.cliCoreVersion || '').trim();
1231
+ const serviceMode = String(config.serviceMode || '').trim();
1232
+ const serviceBackground = Boolean(config.serviceBackground);
1233
+ const serviceActive = Boolean(config.serviceActive);
1234
+ if (packageName) url.searchParams.set('package_name', packageName);
1235
+ if (packageVersion) {
1236
+ url.searchParams.set('package_version', packageVersion);
1237
+ url.searchParams.set('daemon_version', packageVersion);
1238
+ }
1239
+ if (packageKind) url.searchParams.set('package_kind', packageKind);
1240
+ if (packageSpec) url.searchParams.set('package_spec', packageSpec);
1241
+ if (packageBin) url.searchParams.set('package_bin', packageBin);
1242
+ if (cliCoreVersion) url.searchParams.set('cli_core_version', cliCoreVersion);
1243
+ if (serviceMode) url.searchParams.set('service_mode', serviceMode);
1244
+ url.searchParams.set('service_background', String(serviceBackground));
1245
+ url.searchParams.set('service_active', String(serviceActive));
1188
1246
  return url;
1189
1247
  }
1190
1248
 
@@ -3096,6 +3154,12 @@ class ClaudeAgentSession {
3096
3154
  }
3097
3155
  }
3098
3156
 
3157
+ function readOnlySessionAckStatus(session) {
3158
+ const status = String(session?.status || '').toLowerCase();
3159
+ if ((!status || status === 'offline') && !session?.started) return 'idle';
3160
+ return session?.status || 'idle';
3161
+ }
3162
+
3099
3163
  class MagClawDaemon {
3100
3164
  constructor(config, env = process.env) {
3101
3165
  this.env = env;
@@ -3452,6 +3516,47 @@ class MagClawDaemon {
3452
3516
  await this.startUpgradeWorker(message);
3453
3517
  }
3454
3518
 
3519
+ async handleDaemonClose(message) {
3520
+ const commandId = String(message.commandId || '').trim();
3521
+ const service = await readServiceState(this.paths.profile, this.env);
3522
+ const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
3523
+ const packageInfo = runtimePackageInfo(this.env, service);
3524
+ const runMode = {
3525
+ mode: service.mode || serviceStatus.mode || 'foreground',
3526
+ background: Boolean(service.background),
3527
+ active: Boolean(serviceStatus.active),
3528
+ label: serviceStatus.label || '',
3529
+ serviceName: serviceStatus.serviceName || '',
3530
+ taskName: serviceStatus.taskName || '',
3531
+ packageName: packageInfo.name,
3532
+ packageVersion: packageInfo.version,
3533
+ packageKind: packageInfo.kind,
3534
+ packageSpec: packageInfo.spec,
3535
+ packageBin: packageInfo.bin,
3536
+ };
3537
+ logWarning('daemon', `Remote close requested (${message.reason || 'closed_from_cloud'}).`);
3538
+ await writeServiceState(this.paths.profile, {
3539
+ remoteClosed: true,
3540
+ remoteClosedAt: now(),
3541
+ remoteCloseReason: message.reason || 'closed_from_cloud',
3542
+ remoteCloseCommandId: commandId,
3543
+ }, this.env);
3544
+ for (const session of this.sessions.values()) session.stop();
3545
+ this.sessions.clear();
3546
+ this.send({ type: 'daemon:close:ack',
3547
+ commandId,
3548
+ status: 'stopping',
3549
+ reason: message.reason || 'closed_from_cloud',
3550
+ service: runMode,
3551
+ at: now(),
3552
+ });
3553
+ const background = stopBackground(this.paths.profile, this.env, { disable: message.disableBackground !== false });
3554
+ logInfo('daemon', `Close request stopped background service mode=${background.mode || 'foreground'} ok=${Boolean(background.ok)}.`);
3555
+ this.close();
3556
+ process.exitCode = 0;
3557
+ setTimeout(() => process.exit(0), 50).unref?.();
3558
+ }
3559
+
3455
3560
  async readyPayload() {
3456
3561
  const runtimes = await detectRuntimes(this.env);
3457
3562
  const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
@@ -3657,6 +3762,9 @@ class MagClawDaemon {
3657
3762
  case 'daemon:upgrade':
3658
3763
  await this.handleDaemonUpgrade(message);
3659
3764
  break;
3765
+ case 'daemon:close':
3766
+ await this.handleDaemonClose(message);
3767
+ break;
3660
3768
  case 'agent:start':
3661
3769
  await this.handleAgentStart(message);
3662
3770
  break;
@@ -3706,7 +3814,7 @@ class MagClawDaemon {
3706
3814
  ? await session.listSkills()
3707
3815
  : { agent: { id: agent.id, name: agent.name || agent.id }, global: [], workspace: [], plugin: [], tools: [] };
3708
3816
  this.send({ type: 'agent:skills:list_result', commandId: message.commandId, agentId: agent.id, skills });
3709
- this.send({ type: 'agent:ack', commandId: message.commandId, agentId: agent.id, status: session.status || 'idle' });
3817
+ this.send({ type: 'agent:ack', commandId: message.commandId, agentId: agent.id, status: readOnlySessionAckStatus(session) });
3710
3818
  } catch (error) {
3711
3819
  this.send({ type: 'agent:skills:list_result', commandId: message.commandId, agentId: agent.id, skills: { agent: { id: agent.id, name: agent.name || agent.id }, global: [], workspace: [], plugin: [], tools: [], error: error.message } });
3712
3820
  this.send({ type: 'agent:error', commandId: message.commandId, agentId: agent.id, error: error.message });
@@ -3731,7 +3839,7 @@ class MagClawDaemon {
3731
3839
  const session = await this.sessionForWorkspaceRequest(message);
3732
3840
  const tree = await session.listWorkspace(message.path || message.payload?.path || '');
3733
3841
  this.send({ type: 'agent:workspace:list_result', commandId: message.commandId, agentId: session.agent.id, tree });
3734
- this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: session.status || 'idle' });
3842
+ this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: readOnlySessionAckStatus(session) });
3735
3843
  } catch (error) {
3736
3844
  this.send({ type: 'agent:error', commandId: message.commandId, agentId: message.agentId || null, error: error.message });
3737
3845
  }
@@ -3742,7 +3850,7 @@ class MagClawDaemon {
3742
3850
  const session = await this.sessionForWorkspaceRequest(message);
3743
3851
  const file = await session.readWorkspaceFile(message.path || message.payload?.path || 'MEMORY.md');
3744
3852
  this.send({ type: 'agent:workspace:file_result', commandId: message.commandId, agentId: session.agent.id, file: file.file });
3745
- this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: session.status || 'idle' });
3853
+ this.send({ type: 'agent:ack', commandId: message.commandId, agentId: session.agent.id, status: readOnlySessionAckStatus(session) });
3746
3854
  } catch (error) {
3747
3855
  this.send({ type: 'agent:error', commandId: message.commandId, agentId: message.agentId || null, error: error.message });
3748
3856
  }
@@ -3992,11 +4100,26 @@ class MagClawDaemon {
3992
4100
  async connectOnce() {
3993
4101
  if (this.closed) return;
3994
4102
  await this.refreshConfigFromDisk();
3995
- const url = toWebSocketUrl(this.config.serverUrl, this.config);
4103
+ const service = await readServiceState(this.paths.profile, this.env);
4104
+ const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
4105
+ const packageInfo = runtimePackageInfo(this.env, service);
4106
+ const url = toWebSocketUrl(this.config.serverUrl, {
4107
+ ...this.config,
4108
+ packageName: packageInfo.name,
4109
+ packageVersion: packageInfo.version,
4110
+ packageKind: packageInfo.kind,
4111
+ packageSpec: packageInfo.spec,
4112
+ packageBin: packageInfo.bin,
4113
+ cliCoreVersion: CLI_CORE_VERSION,
4114
+ serviceMode: service.mode || serviceStatus.mode || 'foreground',
4115
+ serviceBackground: Boolean(service.background),
4116
+ serviceActive: Boolean(serviceStatus.active),
4117
+ });
3996
4118
  const requestModule = url.protocol === 'wss:' ? https : http;
3997
4119
  const requestUrl = new URL(url.href.replace(/^ws/, 'http'));
3998
4120
  const key = crypto.randomBytes(16).toString('base64');
3999
- logInfo('daemon', `Connecting MagClaw daemon profile "${this.paths.profile}" to ${this.config.serverUrl}...`);
4121
+ logInfo('daemon', `Connecting MagClaw daemon v${packageInfo.version || DAEMON_VERSION} profile "${this.paths.profile}" to ${this.config.serverUrl}...`);
4122
+ if (this.closed) return;
4000
4123
  return new Promise((resolve, reject) => {
4001
4124
  let settled = false;
4002
4125
  const finish = (callback, value) => {
@@ -4176,6 +4299,10 @@ async function writeLauncher(profile, env = process.env) {
4176
4299
  installedDaemonVersion: packageVersion || DAEMON_VERSION,
4177
4300
  installedPackageVersion: packageVersion || DAEMON_VERSION,
4178
4301
  commandMode: useNpmLauncher ? 'npm' : 'local',
4302
+ remoteClosed: false,
4303
+ remoteClosedAt: '',
4304
+ remoteCloseReason: '',
4305
+ remoteCloseCommandId: '',
4179
4306
  }, env);
4180
4307
  const code = [
4181
4308
  '#!/usr/bin/env node',
@@ -4192,6 +4319,11 @@ async function writeLauncher(profile, env = process.env) {
4192
4319
  `const defaultPackageSpec = ${JSON.stringify(service.packageSpec)};`,
4193
4320
  "let service = {};",
4194
4321
  "try { service = JSON.parse(fs.readFileSync(serviceFile, 'utf8')); } catch {}",
4322
+ "if (service.remoteClosed) {",
4323
+ " const stamp = new Date().toISOString().replace('T', ' ').slice(0, 19);",
4324
+ " console.error(`${stamp} INFO DAEMON profile ${profile} is closed from MagClaw Cloud; run magclaw start/restart/connect to reconnect.`);",
4325
+ " process.exit(0);",
4326
+ "}",
4195
4327
  "const packageSpec = String(service.packageSpec || defaultPackageSpec || '@magclaw/daemon@latest');",
4196
4328
  "const packageName = String(service.packageName || (packageSpec.startsWith('@magclaw/computer@') || packageSpec === '@magclaw/computer' ? '@magclaw/computer' : '@magclaw/daemon'));",
4197
4329
  "const packageKind = String(service.packageKind || (packageName === '@magclaw/computer' ? 'computer' : 'daemon'));",
@@ -4285,6 +4417,7 @@ async function startMacBackground(profile, env = process.env) {
4285
4417
  ].join('\n');
4286
4418
  await writeFile(plist, xml);
4287
4419
  spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4420
+ spawnSync('launchctl', ['enable', `gui/${process.getuid()}/${label}`], { stdio: 'ignore' });
4288
4421
  const boot = spawnSync('launchctl', ['bootstrap', `gui/${process.getuid()}`, plist], { encoding: 'utf8' });
4289
4422
  if (boot.status !== 0) {
4290
4423
  const load = spawnSync('launchctl', ['load', plist], { encoding: 'utf8' });
@@ -4403,6 +4536,29 @@ function backgroundServiceStatus(profile, env = process.env) {
4403
4536
  return { mode: 'foreground', active: false };
4404
4537
  }
4405
4538
 
4539
+ function launchctlResultIsNotLoaded(result) {
4540
+ const detail = String(result?.stderr || result?.stdout || '').toLowerCase();
4541
+ return detail.includes('no such process') || detail.includes('could not find service') || detail.includes('not found');
4542
+ }
4543
+
4544
+ function launchctlServiceIsStopped(serviceTarget) {
4545
+ const result = spawnSync('launchctl', ['print', serviceTarget], { encoding: 'utf8' });
4546
+ if (result.status !== 0) return launchctlResultIsNotLoaded(result);
4547
+ const output = String(result.stdout || result.stderr || '');
4548
+ if (/\bstate\s*=\s*running\b/i.test(output)) return false;
4549
+ if (/\bpid\s*=\s*[1-9][0-9]*\b/i.test(output)) return false;
4550
+ return true;
4551
+ }
4552
+
4553
+ function waitForBackgroundServiceStopped(serviceTarget, timeoutMs = 2000) {
4554
+ const deadline = Date.now() + Math.max(250, Number(timeoutMs) || 2000);
4555
+ while (Date.now() < deadline) {
4556
+ if (launchctlServiceIsStopped(serviceTarget)) return true;
4557
+ sleepSync(100);
4558
+ }
4559
+ return launchctlServiceIsStopped(serviceTarget);
4560
+ }
4561
+
4406
4562
  async function waitForPidExit(pid, timeoutMs = 2000) {
4407
4563
  const deadline = Date.now() + timeoutMs;
4408
4564
  while (Date.now() < deadline) {
@@ -4451,32 +4607,58 @@ async function stopActiveDaemon(profile, env = process.env) {
4451
4607
  return { ok: stopped, running: !stopped, pid, signal };
4452
4608
  }
4453
4609
 
4454
- function stopBackground(profile, env = process.env) {
4610
+ function stopBackground(profile, env = process.env, options = {}) {
4455
4611
  if (process.platform === 'darwin') {
4456
4612
  const paths = profilePaths(profile, env);
4457
4613
  const label = launchAgentLabel(paths.profile);
4614
+ const serviceTarget = `gui/${process.getuid()}/${label}`;
4458
4615
  const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
4459
- spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4460
- return { ok: true, mode: 'launchd', label, file: plist };
4616
+ if (options.disable) {
4617
+ const disabled = spawnSync('launchctl', ['disable', serviceTarget], { encoding: 'utf8' });
4618
+ const stopped = spawnSync('launchctl', ['stop', label], { encoding: 'utf8' });
4619
+ const stoppedConfirmed = stopped.status === 0 && waitForBackgroundServiceStopped(serviceTarget);
4620
+ const needsBootout = disabled.status !== 0 || !stoppedConfirmed;
4621
+ const bootout = needsBootout
4622
+ ? spawnSync('launchctl', ['bootout', serviceTarget], { encoding: 'utf8' })
4623
+ : null;
4624
+ const bootoutOk = bootout ? (bootout.status === 0 || launchctlResultIsNotLoaded(bootout)) : false;
4625
+ if (stopped.status !== 0) spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
4626
+ return {
4627
+ ok: disabled.status === 0 && (stoppedConfirmed || bootoutOk),
4628
+ mode: 'launchd',
4629
+ label,
4630
+ serviceTarget,
4631
+ file: plist,
4632
+ disabled: disabled.status === 0,
4633
+ stopped: stoppedConfirmed || bootoutOk,
4634
+ bootout: bootout ? bootout.status === 0 : false,
4635
+ error: disabled.status === 0 && (stoppedConfirmed || bootoutOk) ? '' : String(bootout?.stderr || stopped.stderr || disabled.stderr || bootout?.stdout || stopped.stdout || disabled.stdout || '').trim(),
4636
+ };
4637
+ }
4638
+ const bootout = spawnSync('launchctl', ['bootout', serviceTarget], { encoding: 'utf8' });
4639
+ return { ok: bootout.status === 0 || launchctlResultIsNotLoaded(bootout), mode: 'launchd', label, serviceTarget, file: plist, error: bootout.stderr || bootout.stdout || '' };
4461
4640
  }
4462
4641
  if (process.platform === 'linux') {
4463
4642
  const paths = profilePaths(profile, env);
4464
4643
  const serviceName = systemdServiceName(paths.profile);
4465
- const result = spawnSync('systemctl', ['--user', 'disable', '--now', serviceName], { encoding: 'utf8' });
4644
+ const result = options.disable
4645
+ ? spawnSync('systemctl', ['--user', 'disable', '--now', serviceName], { encoding: 'utf8' })
4646
+ : spawnSync('systemctl', ['--user', 'stop', serviceName], { encoding: 'utf8' });
4466
4647
  return { ok: result.status === 0, mode: 'systemd', serviceName, error: result.stderr || '' };
4467
4648
  }
4468
4649
  if (process.platform === 'win32') {
4469
4650
  const paths = profilePaths(profile, env);
4470
4651
  const taskName = windowsTaskName(paths.profile);
4471
4652
  spawnSync('schtasks.exe', ['/End', '/TN', taskName], { stdio: 'ignore' });
4653
+ if (!options.disable) return { ok: true, mode: 'schtasks', taskName };
4472
4654
  const result = spawnSync('schtasks.exe', ['/Change', '/TN', taskName, '/DISABLE'], { encoding: 'utf8' });
4473
4655
  return { ok: result.status === 0, mode: 'schtasks', taskName, error: result.stderr || result.stdout || '' };
4474
4656
  }
4475
4657
  return { ok: false, mode: 'foreground' };
4476
4658
  }
4477
4659
 
4478
- async function stopDaemon(profile, env = process.env) {
4479
- const background = stopBackground(profile, env);
4660
+ async function stopDaemon(profile, env = process.env, options = {}) {
4661
+ const background = stopBackground(profile, env, options);
4480
4662
  const processResult = await stopActiveDaemon(profile, env);
4481
4663
  const backgroundRequired = background.mode !== 'foreground';
4482
4664
  return {
@@ -5081,6 +5263,7 @@ async function runComputerSetup(flags, env = process.env) {
5081
5263
  fingerprint: owner.fingerprint,
5082
5264
  }, env);
5083
5265
  await saveProfile(config.profile, config, env);
5266
+ await clearRemoteClosedServiceState(config.profile, env);
5084
5267
  const cli = await tryInstallCliShim(flags, env);
5085
5268
  const result = await startBackground(config.profile, env);
5086
5269
  printJson({
@@ -5119,6 +5302,7 @@ async function buildConfig(flags, env = process.env) {
5119
5302
 
5120
5303
  async function runForegroundDaemon(config, env = process.env) {
5121
5304
  const releaseLock = await acquireDaemonLock(config.profile, config, env);
5305
+ await markForegroundServiceState(config.profile, env);
5122
5306
  const daemon = new MagClawDaemon(config, env);
5123
5307
  let forceExitTimer = null;
5124
5308
  const shutdown = (signal) => {
@@ -5145,6 +5329,7 @@ async function runConnect(flags, env = process.env) {
5145
5329
  throw new Error('Run connect with --api-key, --pair-token for legacy pairing, or use a saved profile with a machine token.');
5146
5330
  }
5147
5331
  await saveProfile(config.profile, config, env);
5332
+ await clearRemoteClosedServiceState(config.profile, env);
5148
5333
  const cli = await tryInstallCliShim(flags, env);
5149
5334
  if (flags.background) {
5150
5335
  const result = await startBackground(config.profile, env);
@@ -5211,7 +5396,7 @@ export async function main(argv = process.argv, env = process.env) {
5211
5396
  }
5212
5397
  case 'stop':
5213
5398
  requireExplicitProfile('stop', flags);
5214
- printJson(await stopDaemon(flags.profile, env));
5399
+ printJson(await stopDaemon(flags.profile, env, { disable: Boolean(flags.disable) }));
5215
5400
  break;
5216
5401
  case 'restart':
5217
5402
  printJson(await restartSavedBackground(flags, env));