@magclaw/cli-core 0.1.29 → 0.1.30

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 +207 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magclaw/cli-core",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
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
@@ -588,7 +588,7 @@ function renderHelp() {
588
588
  ' status Show daemon status for one profile',
589
589
  ' list List local daemon profiles and connected Computers',
590
590
  ' logs Print recent daemon logs for one profile',
591
- ' install-cli Install or repair the durable magclaw command shim',
591
+ ' install-cli Install or repair durable magclaw command shims',
592
592
  ' upgrade Upgrade the background daemon package',
593
593
  ' doctor Show runtime and environment diagnostics',
594
594
  ' uninstall Stop and remove the background daemon service',
@@ -1110,64 +1110,88 @@ function defaultCliPackageSpec(env = process.env) {
1110
1110
  return String(env.MAGCLAW_CLI_PACKAGE_SPEC || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest';
1111
1111
  }
1112
1112
 
1113
+ function defaultComputerCliPackageSpec(env = process.env) {
1114
+ return String(env.MAGCLAW_COMPUTER_CLI_PACKAGE_SPEC || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest';
1115
+ }
1116
+
1113
1117
  function defaultCliNpmPath(env = process.env) {
1114
1118
  return commandExists('npm', env) || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
1115
1119
  }
1116
1120
 
1117
- export function renderCliShimFiles({ platform = process.platform, npmPath = '', packageSpec = '@magclaw/cli-core@latest' } = {}) {
1121
+ function cliShimTargets({ packageSpec = '@magclaw/cli-core@latest', computerPackageSpec = '@magclaw/computer@latest' } = {}) {
1122
+ return [
1123
+ {
1124
+ command: 'magclaw',
1125
+ packageSpec: String(packageSpec || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest',
1126
+ },
1127
+ {
1128
+ command: 'magclaw-computer',
1129
+ packageSpec: String(computerPackageSpec || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest',
1130
+ },
1131
+ ];
1132
+ }
1133
+
1134
+ export function renderCliShimFiles({
1135
+ platform = process.platform,
1136
+ npmPath = '',
1137
+ packageSpec = '@magclaw/cli-core@latest',
1138
+ computerPackageSpec = '@magclaw/computer@latest',
1139
+ } = {}) {
1118
1140
  const targetPackage = String(packageSpec || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest';
1141
+ const targetComputerPackage = String(computerPackageSpec || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest';
1119
1142
  const targetNpm = String(npmPath || (platform === 'win32' ? 'npm.cmd' : 'npm')).trim() || (platform === 'win32' ? 'npm.cmd' : 'npm');
1143
+ const targets = cliShimTargets({ packageSpec: targetPackage, computerPackageSpec: targetComputerPackage });
1120
1144
  if (platform === 'win32') {
1121
1145
  const fallback = path.win32.basename(targetNpm) || 'npm.cmd';
1122
- return [
1146
+ return targets.flatMap((target) => [
1123
1147
  {
1124
- name: 'magclaw.cmd',
1148
+ name: `${target.command}.cmd`,
1125
1149
  executable: true,
1126
1150
  content: [
1127
1151
  '@echo off',
1128
1152
  'setlocal',
1129
1153
  `set "NPM_BIN=${cmdEnvValue(targetNpm)}"`,
1130
1154
  `if not exist "%NPM_BIN%" set "NPM_BIN=${cmdEnvValue(fallback)}"`,
1131
- `set "PACKAGE_SPEC=${cmdEnvValue(targetPackage)}"`,
1155
+ `set "PACKAGE_SPEC=${cmdEnvValue(target.packageSpec)}"`,
1132
1156
  'set "ARGS=%*"',
1133
- '"%NPM_BIN%" exec --yes --package "%PACKAGE_SPEC%" -- magclaw %ARGS%',
1157
+ `"%NPM_BIN%" exec --yes --package "%PACKAGE_SPEC%" -- ${target.command} %ARGS%`,
1134
1158
  'exit /b %ERRORLEVEL%',
1135
1159
  '',
1136
1160
  ].join('\r\n'),
1137
1161
  },
1138
1162
  {
1139
- name: 'magclaw.ps1',
1163
+ name: `${target.command}.ps1`,
1140
1164
  executable: true,
1141
1165
  content: [
1142
1166
  `$npmBin = ${psSingleQuote(targetNpm)}`,
1143
1167
  `if (-not (Test-Path -LiteralPath $npmBin)) { $npmBin = ${psSingleQuote(fallback)} }`,
1144
- `$packageSpec = ${psSingleQuote(targetPackage)}`,
1145
- '& $npmBin exec --yes --package $packageSpec -- magclaw @args',
1168
+ `$packageSpec = ${psSingleQuote(target.packageSpec)}`,
1169
+ `& $npmBin exec --yes --package $packageSpec -- ${target.command} @args`,
1146
1170
  'exit $LASTEXITCODE',
1147
1171
  '',
1148
1172
  ].join('\n'),
1149
1173
  },
1150
- ];
1174
+ ]);
1151
1175
  }
1152
1176
  const fallback = path.basename(targetNpm) || 'npm';
1153
- return [
1177
+ return targets.map((target) => (
1154
1178
  {
1155
- name: 'magclaw',
1179
+ name: target.command,
1156
1180
  executable: true,
1157
1181
  content: [
1158
1182
  '#!/bin/sh',
1159
1183
  'set -eu',
1160
1184
  '# MagClaw CLI shim generated by @magclaw/cli-core.',
1161
1185
  `NPM_BIN=${shSingleQuote(targetNpm)}`,
1162
- `PACKAGE_SPEC=${shSingleQuote(targetPackage)}`,
1186
+ `PACKAGE_SPEC=${shSingleQuote(target.packageSpec)}`,
1163
1187
  'if [ ! -x "$NPM_BIN" ]; then',
1164
1188
  ` NPM_BIN=${shSingleQuote(fallback)}`,
1165
1189
  'fi',
1166
- 'exec "$NPM_BIN" exec --yes --package "$PACKAGE_SPEC" -- magclaw "$@"',
1190
+ `exec "$NPM_BIN" exec --yes --package "$PACKAGE_SPEC" -- ${target.command} "$@"`,
1167
1191
  '',
1168
1192
  ].join('\n'),
1169
- },
1170
- ];
1193
+ }
1194
+ ));
1171
1195
  }
1172
1196
 
1173
1197
  async function chooseCliShimBinDir(options = {}, env = process.env) {
@@ -1203,17 +1227,21 @@ async function chooseCliShimBinDir(options = {}, env = process.env) {
1203
1227
  return { dir: fallback, explicit: false, pathReady: directoryIsInPath(fallback, env) };
1204
1228
  }
1205
1229
 
1206
- async function existingDurableMagclawCommand(env = process.env) {
1207
- const existing = commandExists('magclaw', env);
1230
+ function isGeneratedMagClawShim(content = '') {
1231
+ return Boolean(
1232
+ content.includes('MagClaw CLI shim generated by @magclaw/cli-core')
1233
+ || content.includes('MagClaw CLI shim generated by @magclaw/daemon')
1234
+ || content.includes('@magclaw/cli-core@')
1235
+ || content.includes('@magclaw/daemon@')
1236
+ || content.includes('@magclaw/computer@')
1237
+ );
1238
+ }
1239
+
1240
+ async function existingDurableMagclawCommand(command = 'magclaw', env = process.env) {
1241
+ const existing = commandExists(command, env);
1208
1242
  if (!existing || pathLooksEphemeralCli(existing)) return '';
1209
1243
  const content = await readFile(existing, 'utf8').catch(() => '');
1210
- if (
1211
- content
1212
- && !content.includes('MagClaw CLI shim generated by @magclaw/cli-core')
1213
- && !content.includes('MagClaw CLI shim generated by @magclaw/daemon')
1214
- && !content.includes('@magclaw/cli-core@')
1215
- && !content.includes('@magclaw/daemon@')
1216
- ) {
1244
+ if (content && !isGeneratedMagClawShim(content)) {
1217
1245
  return '';
1218
1246
  }
1219
1247
  return existing;
@@ -1222,12 +1250,7 @@ async function existingDurableMagclawCommand(env = process.env) {
1222
1250
  async function writeCliShimFile(file, content, { force = false } = {}) {
1223
1251
  if (existsSync(file) && !force) {
1224
1252
  const existing = await readFile(file, 'utf8').catch(() => '');
1225
- if (
1226
- !existing.includes('MagClaw CLI shim generated by @magclaw/cli-core')
1227
- && !existing.includes('MagClaw CLI shim generated by @magclaw/daemon')
1228
- && !existing.includes('@magclaw/cli-core@')
1229
- && !existing.includes('@magclaw/daemon@')
1230
- ) {
1253
+ if (!isGeneratedMagClawShim(existing)) {
1231
1254
  const error = new Error(`Refusing to overwrite existing non-MagClaw command: ${file}`);
1232
1255
  error.code = 'EEXIST';
1233
1256
  throw error;
@@ -1241,16 +1264,39 @@ async function installCliShim(options = {}, env = process.env) {
1241
1264
  if (env.MAGCLAW_INSTALL_CLI === '0' || options.installCli === false || options.noInstallCli) {
1242
1265
  return { ok: true, command: 'magclaw', installed: false, skipped: true, reason: 'disabled' };
1243
1266
  }
1244
- const existing = await existingDurableMagclawCommand(env);
1245
- if (existing && !options.binDir && !options.cliBinDir && !options.force) {
1246
- return { ok: true, command: 'magclaw', installed: false, path: existing, files: [existing], pathReady: true, reason: 'already_available' };
1267
+ const existing = await existingDurableMagclawCommand('magclaw', env);
1268
+ const existingComputer = await existingDurableMagclawCommand('magclaw-computer', env);
1269
+ const existingCommandsShareDir = Boolean(
1270
+ existing
1271
+ && existingComputer
1272
+ && path.dirname(existing) === path.dirname(existingComputer)
1273
+ );
1274
+ if (existingCommandsShareDir && !options.binDir && !options.cliBinDir && !options.force) {
1275
+ return {
1276
+ ok: true,
1277
+ command: 'magclaw',
1278
+ commands: ['magclaw', 'magclaw-computer'],
1279
+ installed: false,
1280
+ path: existing,
1281
+ files: [existing, existingComputer],
1282
+ pathReady: true,
1283
+ reason: 'already_available',
1284
+ };
1247
1285
  }
1248
1286
 
1249
- const target = await chooseCliShimBinDir(options, env);
1287
+ const target = (existing || existingComputer) && !options.binDir && !options.cliBinDir && !options.force
1288
+ ? { dir: path.dirname(existing || existingComputer), explicit: false, pathReady: true }
1289
+ : await chooseCliShimBinDir(options, env);
1250
1290
  await mkdir(target.dir, { recursive: true });
1251
1291
  const npmPath = String(options.npmPath || defaultCliNpmPath(env)).trim() || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
1252
1292
  const packageSpec = String(options.packageSpec || options.cliPackageSpec || defaultCliPackageSpec(env)).trim() || '@magclaw/cli-core@latest';
1253
- const shimFiles = renderCliShimFiles({ platform: process.platform, npmPath, packageSpec });
1293
+ const computerPackageSpec = String(options.computerPackageSpec || options.computerCliPackageSpec || defaultComputerCliPackageSpec(env)).trim() || '@magclaw/computer@latest';
1294
+ const shimFiles = renderCliShimFiles({
1295
+ platform: process.platform,
1296
+ npmPath,
1297
+ packageSpec,
1298
+ computerPackageSpec,
1299
+ });
1254
1300
  const written = [];
1255
1301
  for (const shim of shimFiles) {
1256
1302
  const file = path.join(target.dir, shim.name);
@@ -1260,12 +1306,14 @@ async function installCliShim(options = {}, env = process.env) {
1260
1306
  return {
1261
1307
  ok: true,
1262
1308
  command: 'magclaw',
1309
+ commands: ['magclaw', 'magclaw-computer'],
1263
1310
  installed: true,
1264
1311
  binDir: target.dir,
1265
1312
  files: written,
1266
1313
  path: written[0] || '',
1267
1314
  pathReady: Boolean(target.pathReady),
1268
1315
  packageSpec,
1316
+ computerPackageSpec,
1269
1317
  npmPath,
1270
1318
  };
1271
1319
  }
@@ -5567,8 +5615,12 @@ function normalizeSetupServerSlug(value = '') {
5567
5615
  return String(value || '').trim().replace(/^\/+/, '').replace(/\/+$/, '');
5568
5616
  }
5569
5617
 
5618
+ function normalizeSetupServerUrl(value = '') {
5619
+ return String(value || DEFAULT_SERVER_URL).replace(/\/+$/, '');
5620
+ }
5621
+
5570
5622
  async function postSetupJson(serverUrl, pathname, body = {}) {
5571
- const url = `${String(serverUrl || DEFAULT_SERVER_URL).replace(/\/+$/, '')}${pathname}`;
5623
+ const url = `${normalizeSetupServerUrl(serverUrl)}${pathname}`;
5572
5624
  const response = await fetch(url, {
5573
5625
  method: 'POST',
5574
5626
  headers: { 'content-type': 'application/json' },
@@ -5593,6 +5645,113 @@ function computerTargetProfile(flags = {}, fallback = DEFAULT_PROFILE) {
5593
5645
  return profileFromComputerTarget(flags.server || flags.serverSlug || flags.slug || flags._?.[1] || flags.profile || fallback);
5594
5646
  }
5595
5647
 
5648
+ function savedComputerSetupMatches(config = {}, target = {}) {
5649
+ const token = String(config.token || config.machineToken || config.apiKey || '').trim();
5650
+ const computerId = String(config.computerId || '').trim();
5651
+ if (!token || !computerId) return false;
5652
+ if (normalizeSetupServerUrl(config.serverUrl) !== normalizeSetupServerUrl(target.serverUrl)) return false;
5653
+
5654
+ const targetProfile = safeProfileName(target.profile || target.serverSlug || DEFAULT_PROFILE);
5655
+ const configProfile = safeProfileName(config.profile || targetProfile);
5656
+ if (configProfile !== targetProfile) return false;
5657
+
5658
+ const targetSlug = normalizeSetupServerSlug(target.serverSlug);
5659
+ const configSlug = normalizeSetupServerSlug(config.serverSlug || config.slug || '');
5660
+ if (configSlug && targetSlug && configSlug !== targetSlug) return false;
5661
+ return true;
5662
+ }
5663
+
5664
+ async function reusableComputerSetupProfile(target = {}, env = process.env) {
5665
+ if (target.force || target.relogin || target.reauthorize) return null;
5666
+ const profile = safeProfileName(target.profile || target.serverSlug || DEFAULT_PROFILE);
5667
+ const config = await readProfile(profile, env);
5668
+ if (!savedComputerSetupMatches(config, { ...target, profile })) return null;
5669
+ const service = await readServiceState(profile, env);
5670
+ if (service.remoteClosed) return null;
5671
+ return {
5672
+ config: {
5673
+ ...config,
5674
+ profile,
5675
+ serverUrl: normalizeSetupServerUrl(config.serverUrl || target.serverUrl),
5676
+ token: String(config.token || config.machineToken || config.apiKey || '').trim(),
5677
+ pairToken: '',
5678
+ },
5679
+ service,
5680
+ serviceStatus: backgroundServiceStatus(profile, env),
5681
+ };
5682
+ }
5683
+
5684
+ async function finishReusableComputerSetup(existing, flags = {}, env = process.env) {
5685
+ const requestedSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
5686
+ const serverSlug = existing.config.serverSlug || requestedSlug;
5687
+ const config = await buildConfig({
5688
+ ...flags,
5689
+ profile: existing.config.profile,
5690
+ serverUrl: existing.config.serverUrl,
5691
+ apiKey: existing.config.token,
5692
+ computerId: existing.config.computerId,
5693
+ workspaceId: existing.config.workspaceId || existing.config.workspace,
5694
+ name: existing.config.name,
5695
+ serverName: existing.config.serverName || serverSlug,
5696
+ serverSlug,
5697
+ fingerprint: existing.config.fingerprint,
5698
+ }, env);
5699
+ await saveProfile(config.profile, config, env);
5700
+ const cli = await tryInstallCliShim(flags, env);
5701
+ const computerName = config.computerName || config.name || os.hostname();
5702
+ const basePayload = {
5703
+ cli,
5704
+ computerId: config.computerId,
5705
+ computerName,
5706
+ profile: config.profile,
5707
+ serverName: config.serverName,
5708
+ serverSlug: config.serverSlug,
5709
+ reused: true,
5710
+ reason: 'already_configured',
5711
+ };
5712
+ if (flags.noStart || flags.noRun) {
5713
+ printJson({
5714
+ ok: true,
5715
+ started: false,
5716
+ ...basePayload,
5717
+ next: `Run magclaw-computer start ${config.profile} when ready.`,
5718
+ });
5719
+ return;
5720
+ }
5721
+
5722
+ const serviceStatus = existing.serviceStatus || backgroundServiceStatus(config.profile, env);
5723
+ let result;
5724
+ if (serviceStatus.active) {
5725
+ result = {
5726
+ ok: true,
5727
+ mode: serviceStatus.mode,
5728
+ active: true,
5729
+ alreadyRunning: true,
5730
+ started: false,
5731
+ label: serviceStatus.label,
5732
+ serviceName: serviceStatus.serviceName,
5733
+ taskName: serviceStatus.taskName,
5734
+ file: serviceStatus.file,
5735
+ status: serviceStatus.status,
5736
+ state: serviceStatus.state,
5737
+ };
5738
+ } else {
5739
+ const started = await startBackground(config.profile, env);
5740
+ result = {
5741
+ ...started,
5742
+ started: Boolean(started.ok),
5743
+ };
5744
+ }
5745
+ printJson({
5746
+ ...result,
5747
+ ...basePayload,
5748
+ });
5749
+ if (!result.ok) {
5750
+ logWarning('daemon', 'Falling back to foreground mode.');
5751
+ await runForegroundDaemon(config, env);
5752
+ }
5753
+ }
5754
+
5596
5755
  async function runComputerSetup(flags, env = process.env) {
5597
5756
  const subcommand = String(flags._?.[0] || '').trim();
5598
5757
  if (!['setup', 'attach', 'login'].includes(subcommand)) {
@@ -5600,8 +5759,18 @@ async function runComputerSetup(flags, env = process.env) {
5600
5759
  }
5601
5760
  const serverSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
5602
5761
  if (!serverSlug) throw new Error('Run computer setup with a server slug, for example: magclaw computer setup /my-server');
5603
- const serverUrl = String(flags.serverUrl || env.MAGCLAW_PUBLIC_URL || DEFAULT_SERVER_URL).replace(/\/+$/, '');
5762
+ const serverUrl = normalizeSetupServerUrl(flags.serverUrl || env.MAGCLAW_PUBLIC_URL || DEFAULT_SERVER_URL);
5604
5763
  const profile = safeProfileName(flags.profile && flags.profile !== DEFAULT_PROFILE ? flags.profile : serverSlug);
5764
+ const existing = await reusableComputerSetupProfile({
5765
+ ...flags,
5766
+ profile,
5767
+ serverSlug,
5768
+ serverUrl,
5769
+ }, env);
5770
+ if (existing) {
5771
+ await finishReusableComputerSetup(existing, flags, env);
5772
+ return;
5773
+ }
5605
5774
  const owner = await ensureMachineFingerprint(profile, env);
5606
5775
  const displayName = String(flags.displayName || flags.name || os.hostname()).trim();
5607
5776
  const packageInfo = runtimePackageInfo(env);