@magclaw/cli-core 0.1.25 → 0.1.27

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 +113 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magclaw/cli-core",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
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
@@ -785,17 +785,66 @@ export function selectRuntimeCommandPath(output, fallback = '', platform = proce
785
785
  .sort((left, right) => left.score - right.score || left.index - right.index)[0]?.file || paths[0];
786
786
  }
787
787
 
788
+ function homeDirForEnv(env = process.env) {
789
+ return String(env.HOME || env.USERPROFILE || os.homedir() || '').trim();
790
+ }
791
+
792
+ function uniquePathEntries(entries = []) {
793
+ const seen = new Set();
794
+ const result = [];
795
+ for (const entry of entries) {
796
+ const value = String(entry || '').trim();
797
+ if (!value) continue;
798
+ const key = durablePathKey(value);
799
+ if (seen.has(key)) continue;
800
+ seen.add(key);
801
+ result.push(value);
802
+ }
803
+ return result;
804
+ }
805
+
806
+ export function runtimeSearchPathEntries(env = process.env) {
807
+ const home = homeDirForEnv(env);
808
+ const userEntries = home ? [
809
+ path.join(home, '.local', 'bin'),
810
+ path.join(home, 'bin'),
811
+ path.join(home, '.npm-global', 'bin'),
812
+ path.join(home, '.volta', 'bin'),
813
+ path.join(home, '.bun', 'bin'),
814
+ process.platform === 'win32' ? path.join(home, 'AppData', 'Roaming', 'npm') : '',
815
+ ] : [];
816
+ const platformEntries = process.platform === 'darwin'
817
+ ? ['/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin', '/usr/local/sbin']
818
+ : process.platform === 'win32'
819
+ ? []
820
+ : ['/usr/local/bin', '/usr/local/sbin'];
821
+ return uniquePathEntries([
822
+ ...pathDirs(env),
823
+ env.NVM_BIN,
824
+ env.VOLTA_HOME ? path.join(env.VOLTA_HOME, 'bin') : '',
825
+ env.BUN_INSTALL ? path.join(env.BUN_INSTALL, 'bin') : '',
826
+ path.dirname(process.execPath),
827
+ ...userEntries,
828
+ ...platformEntries,
829
+ ]);
830
+ }
831
+
832
+ function runtimeDetectionEnv(env = process.env) {
833
+ return {
834
+ ...env,
835
+ PATH: runtimeSearchPathEntries(env).join(path.delimiter),
836
+ };
837
+ }
838
+
788
839
  function commandExists(command, env = process.env) {
840
+ const runtimeEnv = runtimeDetectionEnv(env);
789
841
  const checker = process.platform === 'win32' ? 'where' : 'command';
790
842
  const args = process.platform === 'win32' ? [command] : ['-v', command];
791
843
  const result = process.platform === 'win32'
792
- ? commandOutput(checker, args, { env, timeoutMs: 1500 })
793
- : commandOutput('/bin/sh', ['-lc', `command -v ${JSON.stringify(command)}`], { env, timeoutMs: 1500 });
844
+ ? commandOutput(checker, args, { env: runtimeEnv, timeoutMs: 1500 })
845
+ : commandOutput('/bin/sh', ['-lc', `command -v ${JSON.stringify(command)}`], { env: runtimeEnv, timeoutMs: 1500 });
794
846
  if (result.ok) return selectRuntimeCommandPath(result.stdout, command);
795
- for (const candidate of [
796
- path.join(path.dirname(process.execPath), command),
797
- path.join(os.homedir(), '.local', 'bin', command),
798
- ]) {
847
+ for (const candidate of runtimeSearchPathEntries(runtimeEnv).map((dir) => path.join(dir, command))) {
799
848
  if (existsSync(candidate)) return candidate;
800
849
  }
801
850
  return '';
@@ -1046,13 +1095,15 @@ async function tryInstallCliShim(options = {}, env = process.env) {
1046
1095
  }
1047
1096
 
1048
1097
  function runtimeVersion(command, env = process.env) {
1049
- const result = commandOutput(command, ['--version'], { env, timeoutMs: 3000 });
1098
+ const runtimeEnv = runtimeDetectionEnv(env);
1099
+ const result = commandOutput(command, ['--version'], { env: runtimeEnv, timeoutMs: 3000 });
1050
1100
  if (!result.ok && result.error?.code === 'ENOENT') return '';
1051
1101
  return result.stdout || result.stderr || '';
1052
1102
  }
1053
1103
 
1054
1104
  function codexAppServerCapable(command, env = process.env) {
1055
- const result = commandOutput(command, ['app-server', '--help'], { env, timeoutMs: 3000 });
1105
+ const runtimeEnv = runtimeDetectionEnv(env);
1106
+ const result = commandOutput(command, ['app-server', '--help'], { env: runtimeEnv, timeoutMs: 3000 });
1056
1107
  if (result.error?.code === 'ENOENT') return false;
1057
1108
  if (result.ok) return true;
1058
1109
  const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
@@ -1060,6 +1111,7 @@ function codexAppServerCapable(command, env = process.env) {
1060
1111
  }
1061
1112
 
1062
1113
  function defaultCodexCommand(env = process.env) {
1114
+ const runtimeEnv = runtimeDetectionEnv(env);
1063
1115
  const macAppBinary = '/Applications/Codex.app/Contents/Resources/codex';
1064
1116
  const candidates = [env.CODEX_PATH, env.MAGCLAW_CODEX_PATH, macAppBinary, 'codex']
1065
1117
  .map((item) => String(item || '').trim())
@@ -1067,9 +1119,9 @@ function defaultCodexCommand(env = process.env) {
1067
1119
  for (const candidate of [...new Set(candidates)]) {
1068
1120
  const command = runtimeCommandHasPathSeparator(candidate)
1069
1121
  ? candidate
1070
- : commandExists(candidate, env) || candidate;
1122
+ : commandExists(candidate, runtimeEnv) || candidate;
1071
1123
  if (runtimeCommandHasPathSeparator(command) && !existsSync(command)) continue;
1072
- const result = commandOutput(command, ['--version'], { env, timeoutMs: 3000 });
1124
+ const result = commandOutput(command, ['--version'], { env: runtimeEnv, timeoutMs: 3000 });
1073
1125
  if (result.ok) return command;
1074
1126
  }
1075
1127
  return candidates[0] || 'codex';
@@ -1088,7 +1140,7 @@ function codexRuntimeModels(command, env = process.env) {
1088
1140
  reasoningEffort: ['low', 'medium', 'high', 'xhigh'],
1089
1141
  defaultReasoningEffort: 'medium',
1090
1142
  };
1091
- const result = commandOutput(command, ['debug', 'models'], { env, timeoutMs: 5000 });
1143
+ const result = commandOutput(command, ['debug', 'models'], { env: runtimeDetectionEnv(env), timeoutMs: 5000 });
1092
1144
  if (!result.ok) return fallback;
1093
1145
  try {
1094
1146
  const data = JSON.parse(result.stdout || '{}');
@@ -1121,14 +1173,15 @@ function codexRuntimeModels(command, env = process.env) {
1121
1173
  }
1122
1174
 
1123
1175
  export async function detectRuntimes(env = process.env) {
1124
- const codexCommand = defaultCodexCommand(env);
1176
+ const runtimeEnv = runtimeDetectionEnv(env);
1177
+ const codexCommand = defaultCodexCommand(runtimeEnv);
1125
1178
  const candidates = [
1126
1179
  {
1127
1180
  id: 'codex',
1128
1181
  name: 'Codex CLI',
1129
1182
  command: codexCommand,
1130
1183
  createSupported: true,
1131
- modelsFor: (command) => codexRuntimeModels(command, env),
1184
+ modelsFor: (command) => codexRuntimeModels(command, runtimeEnv),
1132
1185
  },
1133
1186
  {
1134
1187
  id: 'claude-code',
@@ -1180,10 +1233,10 @@ export async function detectRuntimes(env = process.env) {
1180
1233
  },
1181
1234
  ];
1182
1235
  return candidates.map((item) => {
1183
- const pathValue = runtimeCommandHasPathSeparator(item.command) ? (existsSync(item.command) ? item.command : '') : commandExists(item.command, env);
1236
+ const pathValue = runtimeCommandHasPathSeparator(item.command) ? (existsSync(item.command) ? item.command : '') : commandExists(item.command, runtimeEnv);
1184
1237
  const runtimeCommand = pathValue || item.command;
1185
1238
  const installed = Boolean(pathValue);
1186
- const version = installed ? runtimeVersion(runtimeCommand, env) : '';
1239
+ const version = installed ? runtimeVersion(runtimeCommand, runtimeEnv) : '';
1187
1240
  const modelInfo = installed && item.modelsFor ? item.modelsFor(runtimeCommand) : {
1188
1241
  models: item.models || [],
1189
1242
  modelNames: (item.models || []).map((model) => ({ slug: model, name: model })),
@@ -1199,7 +1252,7 @@ export async function detectRuntimes(env = process.env) {
1199
1252
  installed,
1200
1253
  version,
1201
1254
  createSupported: item.createSupported !== false,
1202
- appServer: item.id === 'codex' && installed ? codexAppServerCapable(runtimeCommand, env) : false,
1255
+ appServer: item.id === 'codex' && installed ? codexAppServerCapable(runtimeCommand, runtimeEnv) : false,
1203
1256
  ...modelInfo,
1204
1257
  };
1205
1258
  });
@@ -3520,14 +3573,15 @@ class MagClawDaemon {
3520
3573
  const commandId = String(message.commandId || '').trim();
3521
3574
  const service = await readServiceState(this.paths.profile, this.env);
3522
3575
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
3576
+ const serviceRunMode = daemonServiceRunMode(service, serviceStatus);
3523
3577
  const packageInfo = runtimePackageInfo(this.env, service);
3524
3578
  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 || '',
3579
+ mode: serviceRunMode.mode,
3580
+ background: serviceRunMode.background,
3581
+ active: serviceRunMode.active,
3582
+ label: serviceRunMode.background ? serviceStatus.label || '' : '',
3583
+ serviceName: serviceRunMode.background ? serviceStatus.serviceName || '' : '',
3584
+ taskName: serviceRunMode.background ? serviceStatus.taskName || '' : '',
3531
3585
  packageName: packageInfo.name,
3532
3586
  packageVersion: packageInfo.version,
3533
3587
  packageKind: packageInfo.kind,
@@ -3550,8 +3604,15 @@ class MagClawDaemon {
3550
3604
  service: runMode,
3551
3605
  at: now(),
3552
3606
  });
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)}.`);
3607
+ const shouldStopBackground = Boolean(runMode.background);
3608
+ const background = shouldStopBackground
3609
+ ? stopBackground(this.paths.profile, this.env, { disable: message.disableBackground !== false })
3610
+ : { ok: true, mode: runMode.mode || 'foreground', skipped: true };
3611
+ if (shouldStopBackground) {
3612
+ logInfo('daemon', `Close request stopped background service mode=${background.mode || 'foreground'} ok=${Boolean(background.ok)}.`);
3613
+ } else {
3614
+ logInfo('daemon', 'Foreground close request did not stop background service.');
3615
+ }
3555
3616
  this.close();
3556
3617
  process.exitCode = 0;
3557
3618
  setTimeout(() => process.exit(0), 50).unref?.();
@@ -3562,6 +3623,7 @@ class MagClawDaemon {
3562
3623
  const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
3563
3624
  const service = await readServiceState(this.paths.profile, this.env);
3564
3625
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
3626
+ const serviceRunMode = daemonServiceRunMode(service, serviceStatus);
3565
3627
  const upgrade = await readUpgradeHandoff(this.paths.profile, this.env);
3566
3628
  const packageInfo = runtimePackageInfo(this.env, service);
3567
3629
  return {
@@ -3581,12 +3643,12 @@ class MagClawDaemon {
3581
3643
  packageBin: packageInfo.bin,
3582
3644
  cliCoreVersion: CLI_CORE_VERSION,
3583
3645
  service: {
3584
- mode: service.mode || serviceStatus.mode || 'foreground',
3585
- background: Boolean(service.background),
3586
- active: Boolean(serviceStatus.active),
3587
- label: serviceStatus.label || '',
3588
- serviceName: serviceStatus.serviceName || '',
3589
- taskName: serviceStatus.taskName || '',
3646
+ mode: serviceRunMode.mode,
3647
+ background: serviceRunMode.background,
3648
+ active: serviceRunMode.active,
3649
+ label: serviceRunMode.background ? serviceStatus.label || '' : '',
3650
+ serviceName: serviceRunMode.background ? serviceStatus.serviceName || '' : '',
3651
+ taskName: serviceRunMode.background ? serviceStatus.taskName || '' : '',
3590
3652
  launcher: service.launcher || '',
3591
3653
  packageSpec: service.packageSpec || packageInfo.spec || '',
3592
3654
  packageName: service.packageName || packageInfo.name,
@@ -4102,6 +4164,7 @@ class MagClawDaemon {
4102
4164
  await this.refreshConfigFromDisk();
4103
4165
  const service = await readServiceState(this.paths.profile, this.env);
4104
4166
  const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
4167
+ const serviceRunMode = daemonServiceRunMode(service, serviceStatus);
4105
4168
  const packageInfo = runtimePackageInfo(this.env, service);
4106
4169
  const url = toWebSocketUrl(this.config.serverUrl, {
4107
4170
  ...this.config,
@@ -4111,9 +4174,9 @@ class MagClawDaemon {
4111
4174
  packageSpec: packageInfo.spec,
4112
4175
  packageBin: packageInfo.bin,
4113
4176
  cliCoreVersion: CLI_CORE_VERSION,
4114
- serviceMode: service.mode || serviceStatus.mode || 'foreground',
4115
- serviceBackground: Boolean(service.background),
4116
- serviceActive: Boolean(serviceStatus.active),
4177
+ serviceMode: serviceRunMode.mode,
4178
+ serviceBackground: serviceRunMode.background,
4179
+ serviceActive: serviceRunMode.active,
4117
4180
  });
4118
4181
  const requestModule = url.protocol === 'wss:' ? https : http;
4119
4182
  const requestUrl = new URL(url.href.replace(/^ws/, 'http'));
@@ -4261,6 +4324,10 @@ async function writeLauncher(profile, env = process.env) {
4261
4324
  const npmPath = commandExists('npm', env);
4262
4325
  const nodeDir = path.dirname(process.execPath);
4263
4326
  const npmDir = npmPath ? path.dirname(npmPath) : '';
4327
+ const launchPathEntries = runtimeSearchPathEntries({
4328
+ ...env,
4329
+ PATH: [nodeDir, npmDir, env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(path.delimiter),
4330
+ });
4264
4331
  const commandMode = String(env.MAGCLAW_DAEMON_COMMAND_MODE || '').trim().toLowerCase();
4265
4332
  const useNpmLauncher = Boolean(npmPath) && !['local', 'local-repo', 'repo', 'source'].includes(commandMode);
4266
4333
  const launcher = path.join(paths.runDir, 'launcher.js');
@@ -4312,6 +4379,8 @@ async function writeLauncher(profile, env = process.env) {
4312
4379
  `const useNpmLauncher = ${JSON.stringify(useNpmLauncher)};`,
4313
4380
  `const nodeDir = ${JSON.stringify(nodeDir)};`,
4314
4381
  `const npmDir = ${JSON.stringify(npmDir)};`,
4382
+ `const pathDelimiter = ${JSON.stringify(path.delimiter)};`,
4383
+ `const launchPathEntries = ${JSON.stringify(launchPathEntries)};`,
4315
4384
  `const fallbackBin = ${JSON.stringify(fallbackBin)};`,
4316
4385
  `const profile = ${JSON.stringify(paths.profile)};`,
4317
4386
  `const daemonHome = ${JSON.stringify(daemonRoot(env))};`,
@@ -4334,7 +4403,7 @@ async function writeLauncher(profile, env = process.env) {
4334
4403
  "const args = useNpmLauncher",
4335
4404
  " ? ['exec', '--yes', '--package', packageSpec, '--', packageBin, 'connect', '--profile', profile]",
4336
4405
  " : [fallbackBin, 'connect', '--profile', profile];",
4337
- "const launchPath = [nodeDir, npmDir, process.env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(':');",
4406
+ "const launchPath = [...launchPathEntries, nodeDir, npmDir, process.env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(pathDelimiter);",
4338
4407
  "const childEnv = {",
4339
4408
  " ...process.env,",
4340
4409
  " MAGCLAW_DAEMON_HOME: daemonHome,",
@@ -4536,6 +4605,15 @@ function backgroundServiceStatus(profile, env = process.env) {
4536
4605
  return { mode: 'foreground', active: false };
4537
4606
  }
4538
4607
 
4608
+ function daemonServiceRunMode(service = {}, serviceStatus = {}) {
4609
+ const background = service.background === true;
4610
+ return {
4611
+ mode: background ? (service.mode || serviceStatus.mode || 'foreground') : 'foreground',
4612
+ background,
4613
+ active: background ? Boolean(serviceStatus.active) : false,
4614
+ };
4615
+ }
4616
+
4539
4617
  function launchctlResultIsNotLoaded(result) {
4540
4618
  const detail = String(result?.stderr || result?.stdout || '').toLowerCase();
4541
4619
  return detail.includes('no such process') || detail.includes('could not find service') || detail.includes('not found');
@@ -5270,6 +5348,7 @@ async function runComputerSetup(flags, env = process.env) {
5270
5348
  ...result,
5271
5349
  cli,
5272
5350
  computerId: config.computerId,
5351
+ computerName: config.computerName || config.name || displayName,
5273
5352
  profile: config.profile,
5274
5353
  serverName: config.serverName,
5275
5354
  serverSlug: approved.serverSlug || serverSlug,