@magclaw/cli-core 0.1.22 → 0.1.24
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.
- package/package.json +1 -1
- package/src/cli.js +282 -23
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -39,6 +39,10 @@ const PACKAGE_JSON = (() => {
|
|
|
39
39
|
}
|
|
40
40
|
})();
|
|
41
41
|
export const DAEMON_VERSION = String(PACKAGE_JSON.version || '0.0.0');
|
|
42
|
+
export const CLI_CORE_VERSION = DAEMON_VERSION;
|
|
43
|
+
const DAEMON_PACKAGE_NAME = '@magclaw/daemon';
|
|
44
|
+
const COMPUTER_PACKAGE_NAME = '@magclaw/computer';
|
|
45
|
+
const KNOWN_ENTRY_PACKAGE_NAMES = new Set([DAEMON_PACKAGE_NAME, COMPUTER_PACKAGE_NAME]);
|
|
42
46
|
const SOURCE_CODEX_HOME = path.resolve(process.env.MAGCLAW_CODEX_HOME_SOURCE || process.env.CODEX_HOME || path.join(os.homedir(), '.codex'));
|
|
43
47
|
const CODEX_HOME_SHARED_ENTRIES = ['auth.json', 'plugins', 'vendor_imports'];
|
|
44
48
|
export const CAPABILITIES = [
|
|
@@ -48,6 +52,7 @@ export const CAPABILITIES = [
|
|
|
48
52
|
'agent:stop',
|
|
49
53
|
'agent:skills:list',
|
|
50
54
|
'daemon:upgrade',
|
|
55
|
+
'daemon:close',
|
|
51
56
|
'daemon:release_notice',
|
|
52
57
|
'machine:runtime_models:detect',
|
|
53
58
|
];
|
|
@@ -56,6 +61,71 @@ function now() {
|
|
|
56
61
|
return new Date().toISOString();
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
function packageInfoFromSpec(packageSpec = '') {
|
|
65
|
+
const match = String(packageSpec || '').trim().match(/^(@magclaw\/(?:daemon|computer))(?:@(.+))?$/);
|
|
66
|
+
return {
|
|
67
|
+
name: match?.[1] || '',
|
|
68
|
+
version: match?.[2] || '',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeEntryPackageName(value = '', fallback = DAEMON_PACKAGE_NAME) {
|
|
73
|
+
const clean = String(value || '').trim();
|
|
74
|
+
if (KNOWN_ENTRY_PACKAGE_NAMES.has(clean)) return clean;
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function packageKindForPackageName(packageName = '') {
|
|
79
|
+
return normalizeEntryPackageName(packageName) === COMPUTER_PACKAGE_NAME ? 'computer' : 'daemon';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function packageBinForPackageName(packageName = '') {
|
|
83
|
+
return packageKindForPackageName(packageName) === 'computer' ? 'magclaw-computer' : 'magclaw';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function packageSpecForPackageName(packageName = DAEMON_PACKAGE_NAME, version = 'latest') {
|
|
87
|
+
const name = normalizeEntryPackageName(packageName);
|
|
88
|
+
const cleanVersion = String(version || '').trim() || 'latest';
|
|
89
|
+
return cleanVersion === 'latest' ? `${name}@latest` : `${name}@${cleanVersion}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runtimePackageInfo(env = process.env, service = {}) {
|
|
93
|
+
const envSpec = String(env.MAGCLAW_DAEMON_PACKAGE_SPEC || '').trim();
|
|
94
|
+
const serviceSpec = String(service.packageSpec || '').trim();
|
|
95
|
+
const parsed = packageInfoFromSpec(envSpec || serviceSpec);
|
|
96
|
+
const packageName = normalizeEntryPackageName(
|
|
97
|
+
env.MAGCLAW_ENTRY_PACKAGE_NAME
|
|
98
|
+
|| env.MAGCLAW_DAEMON_PACKAGE_NAME
|
|
99
|
+
|| service.packageName
|
|
100
|
+
|| parsed.name,
|
|
101
|
+
);
|
|
102
|
+
const packageVersion = String(
|
|
103
|
+
env.MAGCLAW_ENTRY_PACKAGE_VERSION
|
|
104
|
+
|| env.MAGCLAW_DAEMON_PACKAGE_VERSION
|
|
105
|
+
|| service.packageVersion
|
|
106
|
+
|| parsed.version
|
|
107
|
+
|| DAEMON_VERSION,
|
|
108
|
+
).trim();
|
|
109
|
+
const packageKind = String(
|
|
110
|
+
env.MAGCLAW_DAEMON_PACKAGE_KIND
|
|
111
|
+
|| service.packageKind
|
|
112
|
+
|| packageKindForPackageName(packageName),
|
|
113
|
+
).trim().toLowerCase() === 'computer' ? 'computer' : 'daemon';
|
|
114
|
+
const packageBin = String(
|
|
115
|
+
env.MAGCLAW_DAEMON_PACKAGE_BIN
|
|
116
|
+
|| service.packageBin
|
|
117
|
+
|| packageBinForPackageName(packageName),
|
|
118
|
+
).trim() || packageBinForPackageName(packageName);
|
|
119
|
+
const packageSpec = envSpec || serviceSpec || packageSpecForPackageName(packageName, packageVersion || 'latest');
|
|
120
|
+
return {
|
|
121
|
+
name: packageName,
|
|
122
|
+
version: packageVersion,
|
|
123
|
+
kind: packageKind,
|
|
124
|
+
bin: packageBin,
|
|
125
|
+
spec: packageSpec,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
59
129
|
function localTimestamp(date = new Date()) {
|
|
60
130
|
const pad = (value) => String(value).padStart(2, '0');
|
|
61
131
|
return [
|
|
@@ -516,6 +586,7 @@ function renderHelp() {
|
|
|
516
586
|
' --server-url <url> MagClaw Cloud URL',
|
|
517
587
|
' --api-key <key> Machine API key for connect',
|
|
518
588
|
' --background Install and run as a background service',
|
|
589
|
+
' --disable With stop: suppress background relaunch until next start',
|
|
519
590
|
' --bin-dir <path> install-cli target directory',
|
|
520
591
|
' --to <version> Upgrade target version (default: latest)',
|
|
521
592
|
' --wait-cloud Wait for Cloud heartbeat during manual upgrade',
|
|
@@ -584,17 +655,25 @@ async function saveProfile(profile, config, env = process.env) {
|
|
|
584
655
|
async function readServiceState(profile = DEFAULT_PROFILE, env = process.env) {
|
|
585
656
|
const paths = profilePaths(profile, env);
|
|
586
657
|
const state = await readJsonFile(paths.service, {});
|
|
658
|
+
const parsed = packageInfoFromSpec(state.packageSpec || '');
|
|
659
|
+
const packageName = normalizeEntryPackageName(state.packageName || parsed.name || DAEMON_PACKAGE_NAME);
|
|
660
|
+
const packageVersion = String(state.packageVersion || parsed.version || state.installedPackageVersion || state.installedDaemonVersion || '').trim();
|
|
587
661
|
return {
|
|
662
|
+
...state,
|
|
588
663
|
version: 1,
|
|
589
664
|
profile: paths.profile,
|
|
590
665
|
mode: state.mode || 'foreground',
|
|
591
666
|
background: Boolean(state.background),
|
|
592
667
|
launcher: state.launcher || '',
|
|
593
668
|
packageSpec: state.packageSpec || '',
|
|
669
|
+
packageName,
|
|
670
|
+
packageVersion,
|
|
671
|
+
packageKind: String(state.packageKind || packageKindForPackageName(packageName)).toLowerCase() === 'computer' ? 'computer' : 'daemon',
|
|
672
|
+
packageBin: state.packageBin || packageBinForPackageName(packageName),
|
|
594
673
|
previousPackageSpec: state.previousPackageSpec || '',
|
|
595
674
|
installedDaemonVersion: state.installedDaemonVersion || DAEMON_VERSION,
|
|
675
|
+
installedPackageVersion: state.installedPackageVersion || packageVersion || state.installedDaemonVersion || DAEMON_VERSION,
|
|
596
676
|
updatedAt: state.updatedAt || '',
|
|
597
|
-
...state,
|
|
598
677
|
};
|
|
599
678
|
}
|
|
600
679
|
|
|
@@ -3232,6 +3311,9 @@ class MagClawDaemon {
|
|
|
3232
3311
|
this.upgradeWorkerStarting = true;
|
|
3233
3312
|
const targetVersion = String(message.targetVersion || message.version || 'latest').trim() || 'latest';
|
|
3234
3313
|
const previousVersion = String(message.previousVersion || DAEMON_VERSION).trim() || DAEMON_VERSION;
|
|
3314
|
+
const packageName = normalizeEntryPackageName(message.packageName || packageInfoFromSpec(message.packageSpec || '').name || this.env.MAGCLAW_ENTRY_PACKAGE_NAME || this.env.MAGCLAW_DAEMON_PACKAGE_NAME);
|
|
3315
|
+
const packageKind = packageKindForPackageName(packageName);
|
|
3316
|
+
const packageBin = String(message.packageBin || packageBinForPackageName(packageName)).trim() || packageBinForPackageName(packageName);
|
|
3235
3317
|
const service = await readServiceState(this.paths.profile, this.env);
|
|
3236
3318
|
const activeService = backgroundServiceStatus(this.paths.profile, this.env);
|
|
3237
3319
|
if (!service.background || !activeService.active) {
|
|
@@ -3245,6 +3327,10 @@ class MagClawDaemon {
|
|
|
3245
3327
|
message: error,
|
|
3246
3328
|
previousVersion,
|
|
3247
3329
|
targetVersion,
|
|
3330
|
+
packageName,
|
|
3331
|
+
packageKind,
|
|
3332
|
+
packageBin,
|
|
3333
|
+
packageSpec: message.packageSpec || packageSpecForPackageName(packageName, targetVersion),
|
|
3248
3334
|
error,
|
|
3249
3335
|
service: activeService,
|
|
3250
3336
|
}, this.env);
|
|
@@ -3257,6 +3343,9 @@ class MagClawDaemon {
|
|
|
3257
3343
|
message: error,
|
|
3258
3344
|
previousVersion,
|
|
3259
3345
|
targetVersion,
|
|
3346
|
+
packageName,
|
|
3347
|
+
packageKind,
|
|
3348
|
+
packageBin,
|
|
3260
3349
|
});
|
|
3261
3350
|
this.upgradeWorkerStarting = false;
|
|
3262
3351
|
return false;
|
|
@@ -3270,7 +3359,10 @@ class MagClawDaemon {
|
|
|
3270
3359
|
message: 'Upgrade worker is starting.',
|
|
3271
3360
|
previousVersion,
|
|
3272
3361
|
targetVersion,
|
|
3273
|
-
|
|
3362
|
+
packageName,
|
|
3363
|
+
packageKind,
|
|
3364
|
+
packageBin,
|
|
3365
|
+
packageSpec: message.packageSpec || packageSpecForPackageName(packageName, targetVersion),
|
|
3274
3366
|
startedAt: now(),
|
|
3275
3367
|
}, this.env);
|
|
3276
3368
|
this.send({
|
|
@@ -3281,6 +3373,9 @@ class MagClawDaemon {
|
|
|
3281
3373
|
progress: 1,
|
|
3282
3374
|
previousVersion,
|
|
3283
3375
|
targetVersion,
|
|
3376
|
+
packageName,
|
|
3377
|
+
packageKind,
|
|
3378
|
+
packageBin,
|
|
3284
3379
|
message: 'Upgrade worker is starting.',
|
|
3285
3380
|
});
|
|
3286
3381
|
const args = [
|
|
@@ -3296,6 +3391,8 @@ class MagClawDaemon {
|
|
|
3296
3391
|
previousVersion,
|
|
3297
3392
|
];
|
|
3298
3393
|
if (message.packageSpec) args.push('--package-spec', String(message.packageSpec));
|
|
3394
|
+
args.push('--package-name', packageName);
|
|
3395
|
+
args.push('--package-bin', packageBin);
|
|
3299
3396
|
const child = spawn(process.execPath, args, {
|
|
3300
3397
|
cwd: process.cwd(),
|
|
3301
3398
|
detached: true,
|
|
@@ -3315,6 +3412,9 @@ class MagClawDaemon {
|
|
|
3315
3412
|
const commandId = String(message.commandId || '').trim();
|
|
3316
3413
|
const targetVersion = String(message.targetVersion || message.version || 'latest').trim() || 'latest';
|
|
3317
3414
|
const previousVersion = String(message.previousVersion || DAEMON_VERSION).trim() || DAEMON_VERSION;
|
|
3415
|
+
const packageName = normalizeEntryPackageName(message.packageName || packageInfoFromSpec(message.packageSpec || '').name || this.env.MAGCLAW_ENTRY_PACKAGE_NAME || this.env.MAGCLAW_DAEMON_PACKAGE_NAME);
|
|
3416
|
+
const packageKind = packageKindForPackageName(packageName);
|
|
3417
|
+
const packageBin = String(message.packageBin || packageBinForPackageName(packageName)).trim() || packageBinForPackageName(packageName);
|
|
3318
3418
|
if (!commandId) {
|
|
3319
3419
|
this.send({ type: 'daemon:upgrade:ack', status: 'failed', error: 'Missing commandId.' });
|
|
3320
3420
|
return;
|
|
@@ -3327,7 +3427,10 @@ class MagClawDaemon {
|
|
|
3327
3427
|
message: this.daemonIsIdleForUpgrade() ? 'Daemon accepted upgrade command.' : 'Waiting for all Agent work to become idle.',
|
|
3328
3428
|
previousVersion,
|
|
3329
3429
|
targetVersion,
|
|
3330
|
-
|
|
3430
|
+
packageName,
|
|
3431
|
+
packageKind,
|
|
3432
|
+
packageBin,
|
|
3433
|
+
packageSpec: message.packageSpec || packageSpecForPackageName(packageName, targetVersion),
|
|
3331
3434
|
requestedAt: now(),
|
|
3332
3435
|
}, this.env);
|
|
3333
3436
|
if (!this.daemonIsIdleForUpgrade()) {
|
|
@@ -3340,6 +3443,9 @@ class MagClawDaemon {
|
|
|
3340
3443
|
progress: 0,
|
|
3341
3444
|
previousVersion,
|
|
3342
3445
|
targetVersion,
|
|
3446
|
+
packageName,
|
|
3447
|
+
packageKind,
|
|
3448
|
+
packageBin,
|
|
3343
3449
|
message: 'Waiting for all Agent work to become idle.',
|
|
3344
3450
|
});
|
|
3345
3451
|
this.scheduleUpgradeIdleCheck();
|
|
@@ -3348,12 +3454,48 @@ class MagClawDaemon {
|
|
|
3348
3454
|
await this.startUpgradeWorker(message);
|
|
3349
3455
|
}
|
|
3350
3456
|
|
|
3457
|
+
async handleDaemonClose(message) {
|
|
3458
|
+
const commandId = String(message.commandId || '').trim();
|
|
3459
|
+
const service = await readServiceState(this.paths.profile, this.env);
|
|
3460
|
+
const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
|
|
3461
|
+
const packageInfo = runtimePackageInfo(this.env, service);
|
|
3462
|
+
const runMode = {
|
|
3463
|
+
mode: service.mode || serviceStatus.mode || 'foreground',
|
|
3464
|
+
background: Boolean(service.background),
|
|
3465
|
+
active: Boolean(serviceStatus.active),
|
|
3466
|
+
label: serviceStatus.label || '',
|
|
3467
|
+
serviceName: serviceStatus.serviceName || '',
|
|
3468
|
+
taskName: serviceStatus.taskName || '',
|
|
3469
|
+
packageName: packageInfo.name,
|
|
3470
|
+
packageVersion: packageInfo.version,
|
|
3471
|
+
packageKind: packageInfo.kind,
|
|
3472
|
+
packageSpec: packageInfo.spec,
|
|
3473
|
+
packageBin: packageInfo.bin,
|
|
3474
|
+
};
|
|
3475
|
+
logWarning('daemon', `Remote close requested (${message.reason || 'closed_from_cloud'}).`);
|
|
3476
|
+
for (const session of this.sessions.values()) session.stop();
|
|
3477
|
+
this.sessions.clear();
|
|
3478
|
+
this.send({ type: 'daemon:close:ack',
|
|
3479
|
+
commandId,
|
|
3480
|
+
status: 'stopping',
|
|
3481
|
+
reason: message.reason || 'closed_from_cloud',
|
|
3482
|
+
service: runMode,
|
|
3483
|
+
at: now(),
|
|
3484
|
+
});
|
|
3485
|
+
const background = stopBackground(this.paths.profile, this.env, { disable: message.disableBackground !== false });
|
|
3486
|
+
logInfo('daemon', `Close request stopped background service mode=${background.mode || 'foreground'} ok=${Boolean(background.ok)}.`);
|
|
3487
|
+
this.close();
|
|
3488
|
+
process.exitCode = 0;
|
|
3489
|
+
setTimeout(() => process.exit(0), 50).unref?.();
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3351
3492
|
async readyPayload() {
|
|
3352
3493
|
const runtimes = await detectRuntimes(this.env);
|
|
3353
3494
|
const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
|
|
3354
3495
|
const service = await readServiceState(this.paths.profile, this.env);
|
|
3355
3496
|
const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
|
|
3356
3497
|
const upgrade = await readUpgradeHandoff(this.paths.profile, this.env);
|
|
3498
|
+
const packageInfo = runtimePackageInfo(this.env, service);
|
|
3357
3499
|
return {
|
|
3358
3500
|
type: 'ready',
|
|
3359
3501
|
computerId: this.config.computerId || null,
|
|
@@ -3363,7 +3505,13 @@ class MagClawDaemon {
|
|
|
3363
3505
|
hostname: os.hostname(),
|
|
3364
3506
|
os: `${os.platform()} ${os.release()}`,
|
|
3365
3507
|
arch: os.arch(),
|
|
3366
|
-
daemonVersion: DAEMON_VERSION,
|
|
3508
|
+
daemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
3509
|
+
packageName: packageInfo.name,
|
|
3510
|
+
packageVersion: packageInfo.version,
|
|
3511
|
+
packageKind: packageInfo.kind,
|
|
3512
|
+
packageSpec: packageInfo.spec,
|
|
3513
|
+
packageBin: packageInfo.bin,
|
|
3514
|
+
cliCoreVersion: CLI_CORE_VERSION,
|
|
3367
3515
|
service: {
|
|
3368
3516
|
mode: service.mode || serviceStatus.mode || 'foreground',
|
|
3369
3517
|
background: Boolean(service.background),
|
|
@@ -3372,7 +3520,12 @@ class MagClawDaemon {
|
|
|
3372
3520
|
serviceName: serviceStatus.serviceName || '',
|
|
3373
3521
|
taskName: serviceStatus.taskName || '',
|
|
3374
3522
|
launcher: service.launcher || '',
|
|
3375
|
-
packageSpec: service.packageSpec || '',
|
|
3523
|
+
packageSpec: service.packageSpec || packageInfo.spec || '',
|
|
3524
|
+
packageName: service.packageName || packageInfo.name,
|
|
3525
|
+
packageVersion: service.packageVersion || packageInfo.version,
|
|
3526
|
+
packageKind: service.packageKind || packageInfo.kind,
|
|
3527
|
+
packageBin: service.packageBin || packageInfo.bin,
|
|
3528
|
+
cliCoreVersion: CLI_CORE_VERSION,
|
|
3376
3529
|
},
|
|
3377
3530
|
upgrade: upgrade || null,
|
|
3378
3531
|
runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
|
|
@@ -3392,11 +3545,18 @@ class MagClawDaemon {
|
|
|
3392
3545
|
}
|
|
3393
3546
|
|
|
3394
3547
|
sendHeartbeat() {
|
|
3548
|
+
const packageInfo = runtimePackageInfo(this.env);
|
|
3395
3549
|
const sent = this.send({
|
|
3396
3550
|
type: 'heartbeat',
|
|
3397
3551
|
time: now(),
|
|
3398
3552
|
computerId: this.config.computerId || null,
|
|
3399
|
-
daemonVersion: DAEMON_VERSION,
|
|
3553
|
+
daemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
3554
|
+
packageName: packageInfo.name,
|
|
3555
|
+
packageVersion: packageInfo.version,
|
|
3556
|
+
packageKind: packageInfo.kind,
|
|
3557
|
+
packageSpec: packageInfo.spec,
|
|
3558
|
+
packageBin: packageInfo.bin,
|
|
3559
|
+
cliCoreVersion: CLI_CORE_VERSION,
|
|
3400
3560
|
runningAgents: [...this.sessions.keys()],
|
|
3401
3561
|
});
|
|
3402
3562
|
logInfo('daemon', `Sent heartbeat (runningAgents=${this.sessions.size}, sent=${sent}).`);
|
|
@@ -3534,6 +3694,9 @@ class MagClawDaemon {
|
|
|
3534
3694
|
case 'daemon:upgrade':
|
|
3535
3695
|
await this.handleDaemonUpgrade(message);
|
|
3536
3696
|
break;
|
|
3697
|
+
case 'daemon:close':
|
|
3698
|
+
await this.handleDaemonClose(message);
|
|
3699
|
+
break;
|
|
3537
3700
|
case 'agent:start':
|
|
3538
3701
|
await this.handleAgentStart(message);
|
|
3539
3702
|
break;
|
|
@@ -3873,7 +4036,9 @@ class MagClawDaemon {
|
|
|
3873
4036
|
const requestModule = url.protocol === 'wss:' ? https : http;
|
|
3874
4037
|
const requestUrl = new URL(url.href.replace(/^ws/, 'http'));
|
|
3875
4038
|
const key = crypto.randomBytes(16).toString('base64');
|
|
3876
|
-
|
|
4039
|
+
const packageInfo = runtimePackageInfo(this.env);
|
|
4040
|
+
logInfo('daemon', `Connecting MagClaw daemon v${packageInfo.version || DAEMON_VERSION} profile "${this.paths.profile}" to ${this.config.serverUrl}...`);
|
|
4041
|
+
if (this.closed) return;
|
|
3877
4042
|
return new Promise((resolve, reject) => {
|
|
3878
4043
|
let settled = false;
|
|
3879
4044
|
const finish = (callback, value) => {
|
|
@@ -4020,13 +4185,38 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4020
4185
|
const launcher = path.join(paths.runDir, 'launcher.js');
|
|
4021
4186
|
const fallbackBin = executablePath();
|
|
4022
4187
|
const previousService = await readServiceState(paths.profile, env);
|
|
4188
|
+
const preferPersistedPackage = Boolean(previousService.pendingCommandId);
|
|
4189
|
+
const envPackageInfo = runtimePackageInfo(env, {});
|
|
4190
|
+
const persistedPackageInfo = runtimePackageInfo({}, previousService);
|
|
4191
|
+
const packageInfo = preferPersistedPackage ? persistedPackageInfo : runtimePackageInfo(env, previousService);
|
|
4192
|
+
const packageSpec = preferPersistedPackage
|
|
4193
|
+
? (previousService.packageSpec || persistedPackageInfo.spec)
|
|
4194
|
+
: (env.MAGCLAW_DAEMON_PACKAGE_SPEC || previousService.packageSpec || packageInfo.spec || packageSpecForPackageName(packageInfo.name, 'latest'));
|
|
4195
|
+
const packageName = normalizeEntryPackageName(
|
|
4196
|
+
packageInfoFromSpec(packageSpec).name
|
|
4197
|
+
|| packageInfo.name
|
|
4198
|
+
|| envPackageInfo.name
|
|
4199
|
+
|| previousService.packageName,
|
|
4200
|
+
);
|
|
4201
|
+
const packageVersion = String(packageInfoFromSpec(packageSpec).version || packageInfo.version || previousService.packageVersion || '').trim();
|
|
4202
|
+
const packageKind = packageKindForPackageName(packageName);
|
|
4203
|
+
const packageBin = String(
|
|
4204
|
+
(preferPersistedPackage ? previousService.packageBin : env.MAGCLAW_DAEMON_PACKAGE_BIN)
|
|
4205
|
+
|| previousService.packageBin
|
|
4206
|
+
|| packageBinForPackageName(packageName),
|
|
4207
|
+
).trim() || packageBinForPackageName(packageName);
|
|
4023
4208
|
const service = await writeServiceState(paths.profile, {
|
|
4024
4209
|
mode: process.platform === 'darwin' ? 'launchd' : process.platform === 'linux' ? 'systemd' : process.platform === 'win32' ? 'schtasks' : 'foreground',
|
|
4025
4210
|
background: true,
|
|
4026
4211
|
launcher,
|
|
4027
|
-
packageSpec
|
|
4212
|
+
packageSpec,
|
|
4213
|
+
packageName,
|
|
4214
|
+
packageVersion,
|
|
4215
|
+
packageKind,
|
|
4216
|
+
packageBin,
|
|
4028
4217
|
previousPackageSpec: previousService.previousPackageSpec || '',
|
|
4029
|
-
installedDaemonVersion: DAEMON_VERSION,
|
|
4218
|
+
installedDaemonVersion: packageVersion || DAEMON_VERSION,
|
|
4219
|
+
installedPackageVersion: packageVersion || DAEMON_VERSION,
|
|
4030
4220
|
commandMode: useNpmLauncher ? 'npm' : 'local',
|
|
4031
4221
|
}, env);
|
|
4032
4222
|
const code = [
|
|
@@ -4045,14 +4235,31 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4045
4235
|
"let service = {};",
|
|
4046
4236
|
"try { service = JSON.parse(fs.readFileSync(serviceFile, 'utf8')); } catch {}",
|
|
4047
4237
|
"const packageSpec = String(service.packageSpec || defaultPackageSpec || '@magclaw/daemon@latest');",
|
|
4238
|
+
"const packageName = String(service.packageName || (packageSpec.startsWith('@magclaw/computer@') || packageSpec === '@magclaw/computer' ? '@magclaw/computer' : '@magclaw/daemon'));",
|
|
4239
|
+
"const packageKind = String(service.packageKind || (packageName === '@magclaw/computer' ? 'computer' : 'daemon'));",
|
|
4240
|
+
"const packageBin = String(service.packageBin || (packageKind === 'computer' ? 'magclaw-computer' : 'magclaw'));",
|
|
4241
|
+
"const packageVersionMatch = packageSpec.match(/^@magclaw\\/(?:daemon|computer)@(.+)$/);",
|
|
4242
|
+
"const packageVersion = String(service.packageVersion || (packageVersionMatch ? packageVersionMatch[1] : ''));",
|
|
4048
4243
|
'const command = useNpmLauncher ? npmPath : process.execPath;',
|
|
4049
4244
|
"const args = useNpmLauncher",
|
|
4050
|
-
" ? ['exec', '--yes', '--package', packageSpec, '--',
|
|
4245
|
+
" ? ['exec', '--yes', '--package', packageSpec, '--', packageBin, 'connect', '--profile', profile]",
|
|
4051
4246
|
" : [fallbackBin, 'connect', '--profile', profile];",
|
|
4052
4247
|
"const launchPath = [nodeDir, npmDir, process.env.PATH || '/usr/bin:/bin:/usr/sbin:/sbin'].filter(Boolean).join(':');",
|
|
4248
|
+
"const childEnv = {",
|
|
4249
|
+
" ...process.env,",
|
|
4250
|
+
" MAGCLAW_DAEMON_HOME: daemonHome,",
|
|
4251
|
+
" MAGCLAW_ENTRY_PACKAGE_NAME: packageName,",
|
|
4252
|
+
" MAGCLAW_ENTRY_PACKAGE_VERSION: packageVersion,",
|
|
4253
|
+
" MAGCLAW_DAEMON_PACKAGE_NAME: packageName,",
|
|
4254
|
+
" MAGCLAW_DAEMON_PACKAGE_SPEC: packageSpec,",
|
|
4255
|
+
" MAGCLAW_DAEMON_PACKAGE_KIND: packageKind,",
|
|
4256
|
+
" MAGCLAW_DAEMON_PACKAGE_BIN: packageBin,",
|
|
4257
|
+
" PATH: launchPath,",
|
|
4258
|
+
"};",
|
|
4259
|
+
"if (packageKind === 'computer') childEnv.MAGCLAW_COMPUTER_DAEMON = '1';",
|
|
4053
4260
|
'const child = spawn(command, args, {',
|
|
4054
4261
|
" stdio: 'inherit',",
|
|
4055
|
-
' env:
|
|
4262
|
+
' env: childEnv,',
|
|
4056
4263
|
'});',
|
|
4057
4264
|
"child.on('exit', (code, signal) => {",
|
|
4058
4265
|
' if (signal) process.kill(process.pid, signal);',
|
|
@@ -4120,6 +4327,7 @@ async function startMacBackground(profile, env = process.env) {
|
|
|
4120
4327
|
].join('\n');
|
|
4121
4328
|
await writeFile(plist, xml);
|
|
4122
4329
|
spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
|
|
4330
|
+
spawnSync('launchctl', ['enable', `gui/${process.getuid()}/${label}`], { stdio: 'ignore' });
|
|
4123
4331
|
const boot = spawnSync('launchctl', ['bootstrap', `gui/${process.getuid()}`, plist], { encoding: 'utf8' });
|
|
4124
4332
|
if (boot.status !== 0) {
|
|
4125
4333
|
const load = spawnSync('launchctl', ['load', plist], { encoding: 'utf8' });
|
|
@@ -4286,32 +4494,49 @@ async function stopActiveDaemon(profile, env = process.env) {
|
|
|
4286
4494
|
return { ok: stopped, running: !stopped, pid, signal };
|
|
4287
4495
|
}
|
|
4288
4496
|
|
|
4289
|
-
function stopBackground(profile, env = process.env) {
|
|
4497
|
+
function stopBackground(profile, env = process.env, options = {}) {
|
|
4290
4498
|
if (process.platform === 'darwin') {
|
|
4291
4499
|
const paths = profilePaths(profile, env);
|
|
4292
4500
|
const label = launchAgentLabel(paths.profile);
|
|
4293
4501
|
const plist = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
4502
|
+
if (options.disable) {
|
|
4503
|
+
const disabled = spawnSync('launchctl', ['disable', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
|
|
4504
|
+
const stopped = spawnSync('launchctl', ['stop', label], { encoding: 'utf8' });
|
|
4505
|
+
if (stopped.status !== 0) spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
|
|
4506
|
+
return {
|
|
4507
|
+
ok: disabled.status === 0 || stopped.status === 0,
|
|
4508
|
+
mode: 'launchd',
|
|
4509
|
+
label,
|
|
4510
|
+
file: plist,
|
|
4511
|
+
disabled: disabled.status === 0,
|
|
4512
|
+
stopped: stopped.status === 0,
|
|
4513
|
+
error: disabled.status === 0 && stopped.status === 0 ? '' : String(stopped.stderr || disabled.stderr || stopped.stdout || disabled.stdout || '').trim(),
|
|
4514
|
+
};
|
|
4515
|
+
}
|
|
4294
4516
|
spawnSync('launchctl', ['bootout', `gui/${process.getuid()}`, plist], { stdio: 'ignore' });
|
|
4295
4517
|
return { ok: true, mode: 'launchd', label, file: plist };
|
|
4296
4518
|
}
|
|
4297
4519
|
if (process.platform === 'linux') {
|
|
4298
4520
|
const paths = profilePaths(profile, env);
|
|
4299
4521
|
const serviceName = systemdServiceName(paths.profile);
|
|
4300
|
-
const result =
|
|
4522
|
+
const result = options.disable
|
|
4523
|
+
? spawnSync('systemctl', ['--user', 'disable', '--now', serviceName], { encoding: 'utf8' })
|
|
4524
|
+
: spawnSync('systemctl', ['--user', 'stop', serviceName], { encoding: 'utf8' });
|
|
4301
4525
|
return { ok: result.status === 0, mode: 'systemd', serviceName, error: result.stderr || '' };
|
|
4302
4526
|
}
|
|
4303
4527
|
if (process.platform === 'win32') {
|
|
4304
4528
|
const paths = profilePaths(profile, env);
|
|
4305
4529
|
const taskName = windowsTaskName(paths.profile);
|
|
4306
4530
|
spawnSync('schtasks.exe', ['/End', '/TN', taskName], { stdio: 'ignore' });
|
|
4531
|
+
if (!options.disable) return { ok: true, mode: 'schtasks', taskName };
|
|
4307
4532
|
const result = spawnSync('schtasks.exe', ['/Change', '/TN', taskName, '/DISABLE'], { encoding: 'utf8' });
|
|
4308
4533
|
return { ok: result.status === 0, mode: 'schtasks', taskName, error: result.stderr || result.stdout || '' };
|
|
4309
4534
|
}
|
|
4310
4535
|
return { ok: false, mode: 'foreground' };
|
|
4311
4536
|
}
|
|
4312
4537
|
|
|
4313
|
-
async function stopDaemon(profile, env = process.env) {
|
|
4314
|
-
const background = stopBackground(profile, env);
|
|
4538
|
+
async function stopDaemon(profile, env = process.env, options = {}) {
|
|
4539
|
+
const background = stopBackground(profile, env, options);
|
|
4315
4540
|
const processResult = await stopActiveDaemon(profile, env);
|
|
4316
4541
|
const backgroundRequired = background.mode !== 'foreground';
|
|
4317
4542
|
return {
|
|
@@ -4573,13 +4798,24 @@ async function openUpgradeProgressSocket(url) {
|
|
|
4573
4798
|
function packageSpecForUpgrade(targetVersion, flags = {}, env = process.env) {
|
|
4574
4799
|
const explicit = String(flags.packageSpec || env.MAGCLAW_DAEMON_UPGRADE_PACKAGE_SPEC || '').trim();
|
|
4575
4800
|
if (explicit) return explicit;
|
|
4801
|
+
const packageName = normalizeEntryPackageName(
|
|
4802
|
+
flags.packageName
|
|
4803
|
+
|| flags.package
|
|
4804
|
+
|| env.MAGCLAW_DAEMON_UPGRADE_PACKAGE_NAME
|
|
4805
|
+
|| env.MAGCLAW_ENTRY_PACKAGE_NAME
|
|
4806
|
+
|| env.MAGCLAW_DAEMON_PACKAGE_NAME
|
|
4807
|
+
|| packageInfoFromSpec(env.MAGCLAW_DAEMON_PACKAGE_SPEC || '').name,
|
|
4808
|
+
);
|
|
4576
4809
|
const target = String(targetVersion || '').trim();
|
|
4577
|
-
return target && target !== 'latest' ?
|
|
4810
|
+
return packageSpecForPackageName(packageName, target && target !== 'latest' ? target : 'latest');
|
|
4578
4811
|
}
|
|
4579
4812
|
|
|
4580
4813
|
function npmPackageLooksRemote(packageSpec) {
|
|
4581
4814
|
const value = String(packageSpec || '').trim();
|
|
4582
|
-
return value.startsWith('@magclaw/daemon@')
|
|
4815
|
+
return value.startsWith('@magclaw/daemon@')
|
|
4816
|
+
|| value === '@magclaw/daemon'
|
|
4817
|
+
|| value.startsWith('@magclaw/computer@')
|
|
4818
|
+
|| value === '@magclaw/computer';
|
|
4583
4819
|
}
|
|
4584
4820
|
|
|
4585
4821
|
function preflightPackage(packageSpec, env = process.env) {
|
|
@@ -4642,12 +4878,15 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
4642
4878
|
const targetVersion = String(flags.targetVersion || flags.version || flags.to || flags.tag || 'latest').trim() || 'latest';
|
|
4643
4879
|
const previousVersion = String(flags.previousVersion || DAEMON_VERSION).trim() || DAEMON_VERSION;
|
|
4644
4880
|
const packageSpec = packageSpecForUpgrade(targetVersion, flags, env);
|
|
4881
|
+
const packageName = normalizeEntryPackageName(packageInfoFromSpec(packageSpec).name || flags.packageName || env.MAGCLAW_ENTRY_PACKAGE_NAME || env.MAGCLAW_DAEMON_PACKAGE_NAME);
|
|
4882
|
+
const packageKind = packageKindForPackageName(packageName);
|
|
4883
|
+
const packageBin = String(flags.packageBin || env.MAGCLAW_DAEMON_UPGRADE_PACKAGE_BIN || packageBinForPackageName(packageName)).trim() || packageBinForPackageName(packageName);
|
|
4645
4884
|
const progressIntervalMs = Math.max(100, Math.min(5000, Number(flags.progressIntervalMs || env.MAGCLAW_DAEMON_UPGRADE_PROGRESS_MS || 500) || 500));
|
|
4646
4885
|
const readyTimeoutMs = Math.max(5000, Math.min(10 * 60_000, Number(flags.readyTimeoutMs || env.MAGCLAW_DAEMON_UPGRADE_READY_TIMEOUT_MS || 120_000) || 120_000));
|
|
4647
4886
|
const localOnly = Boolean(flags.localOnly || flags.local || flags.noWaitCloud);
|
|
4648
4887
|
const assumeReady = Boolean(flags.assumeReady || env.MAGCLAW_DAEMON_UPGRADE_ASSUME_READY === '1');
|
|
4649
4888
|
const serviceBefore = await readServiceState(profile, env);
|
|
4650
|
-
const previousPackageSpec = serviceBefore.packageSpec ||
|
|
4889
|
+
const previousPackageSpec = serviceBefore.packageSpec || packageSpecForPackageName(packageName, previousVersion);
|
|
4651
4890
|
const dryRunPlan = {
|
|
4652
4891
|
ok: true,
|
|
4653
4892
|
dryRun: Boolean(flags.dryRun),
|
|
@@ -4657,6 +4896,9 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
4657
4896
|
previousVersion,
|
|
4658
4897
|
targetVersion,
|
|
4659
4898
|
packageSpec,
|
|
4899
|
+
packageName,
|
|
4900
|
+
packageKind,
|
|
4901
|
+
packageBin,
|
|
4660
4902
|
previousPackageSpec,
|
|
4661
4903
|
service: serviceBefore,
|
|
4662
4904
|
localOnly,
|
|
@@ -4731,6 +4973,10 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
4731
4973
|
mode: serviceBefore.mode || (process.platform === 'darwin' ? 'launchd' : process.platform === 'linux' ? 'systemd' : process.platform === 'win32' ? 'schtasks' : 'foreground'),
|
|
4732
4974
|
background: true,
|
|
4733
4975
|
packageSpec,
|
|
4976
|
+
packageName,
|
|
4977
|
+
packageVersion: targetVersion === 'latest' ? '' : targetVersion,
|
|
4978
|
+
packageKind,
|
|
4979
|
+
packageBin,
|
|
4734
4980
|
previousPackageSpec,
|
|
4735
4981
|
pendingCommandId: commandId,
|
|
4736
4982
|
pendingTargetVersion: targetVersion,
|
|
@@ -4749,19 +4995,19 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
4749
4995
|
const complete = progressSocket ? await progressSocket.waitForComplete(readyTimeoutMs) : null;
|
|
4750
4996
|
if (complete?.status === 'succeeded') {
|
|
4751
4997
|
await emitProgress({ status: 'succeeded', phase: 'ready', progress: 100, message: 'Daemon upgrade completed.' });
|
|
4752
|
-
await writeServiceState(profile, { installedDaemonVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
|
|
4998
|
+
await writeServiceState(profile, { installedDaemonVersion: targetVersion, installedPackageVersion: targetVersion, packageVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
|
|
4753
4999
|
return { ok: true, commandId, targetVersion, packageSpec };
|
|
4754
5000
|
}
|
|
4755
5001
|
if (localOnly) {
|
|
4756
5002
|
const ready = await waitForLocalDaemonReady(profile, readyTimeoutMs, env);
|
|
4757
5003
|
if (!ready.ok) throw new Error(ready.error || 'Timed out waiting for the local daemon to become ready.');
|
|
4758
5004
|
await emitProgress({ status: 'succeeded', phase: 'ready', progress: 100, message: 'Daemon upgrade completed locally.' });
|
|
4759
|
-
await writeServiceState(profile, { installedDaemonVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
|
|
5005
|
+
await writeServiceState(profile, { installedDaemonVersion: targetVersion, installedPackageVersion: targetVersion, packageVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
|
|
4760
5006
|
return { ok: true, commandId, targetVersion, packageSpec, localReady: ready };
|
|
4761
5007
|
}
|
|
4762
5008
|
if (!progressSocket && assumeReady) {
|
|
4763
5009
|
await emitProgress({ status: 'succeeded', phase: 'ready', progress: 100, message: 'Daemon upgrade completed locally.' });
|
|
4764
|
-
await writeServiceState(profile, { installedDaemonVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
|
|
5010
|
+
await writeServiceState(profile, { installedDaemonVersion: targetVersion, installedPackageVersion: targetVersion, packageVersion: targetVersion, pendingCommandId: '', pendingTargetVersion: '' }, env);
|
|
4765
5011
|
return { ok: true, commandId, targetVersion, packageSpec, assumedReady: true };
|
|
4766
5012
|
}
|
|
4767
5013
|
throw new Error('Timed out waiting for upgraded daemon ready acknowledgement.');
|
|
@@ -4774,8 +5020,14 @@ async function runUpgradeWorker(flags, env = process.env) {
|
|
|
4774
5020
|
await emitProgress({ status: 'rollback', phase: 'rollback', progress: 82, message: `Rolling back: ${upgradeError}`, error: upgradeError });
|
|
4775
5021
|
let rollbackError = '';
|
|
4776
5022
|
try {
|
|
5023
|
+
const previousPackageInfo = packageInfoFromSpec(previousPackageSpec);
|
|
5024
|
+
const rollbackPackageName = normalizeEntryPackageName(previousPackageInfo.name || serviceBefore.packageName || packageName);
|
|
4777
5025
|
await writeServiceState(profile, {
|
|
4778
5026
|
packageSpec: previousPackageSpec,
|
|
5027
|
+
packageName: rollbackPackageName,
|
|
5028
|
+
packageVersion: previousPackageInfo.version || serviceBefore.packageVersion || previousVersion,
|
|
5029
|
+
packageKind: packageKindForPackageName(rollbackPackageName),
|
|
5030
|
+
packageBin: serviceBefore.packageBin || packageBinForPackageName(rollbackPackageName),
|
|
4779
5031
|
previousPackageSpec: packageSpec,
|
|
4780
5032
|
pendingCommandId: '',
|
|
4781
5033
|
pendingTargetVersion: '',
|
|
@@ -4838,6 +5090,7 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
4838
5090
|
const profile = safeProfileName(flags.profile && flags.profile !== DEFAULT_PROFILE ? flags.profile : serverSlug);
|
|
4839
5091
|
const owner = await ensureMachineFingerprint(profile, env);
|
|
4840
5092
|
const displayName = String(flags.displayName || flags.name || os.hostname()).trim();
|
|
5093
|
+
const packageInfo = runtimePackageInfo(env);
|
|
4841
5094
|
const started = await postSetupJson(serverUrl, '/api/cloud/computer/setup/start', {
|
|
4842
5095
|
serverSlug,
|
|
4843
5096
|
machineFingerprint: owner.fingerprint,
|
|
@@ -4845,7 +5098,13 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
4845
5098
|
hostname: os.hostname(),
|
|
4846
5099
|
os: os.platform(),
|
|
4847
5100
|
arch: os.arch(),
|
|
4848
|
-
daemonVersion: DAEMON_VERSION,
|
|
5101
|
+
daemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
5102
|
+
packageName: packageInfo.name,
|
|
5103
|
+
packageVersion: packageInfo.version,
|
|
5104
|
+
packageKind: packageInfo.kind,
|
|
5105
|
+
packageSpec: packageInfo.spec,
|
|
5106
|
+
packageBin: packageInfo.bin,
|
|
5107
|
+
cliCoreVersion: CLI_CORE_VERSION,
|
|
4849
5108
|
});
|
|
4850
5109
|
process.stdout.write(`To finish login, open: ${started.verificationUri}\n`);
|
|
4851
5110
|
process.stdout.write(`and enter the code: ${started.userCode}\n`);
|
|
@@ -5012,7 +5271,7 @@ export async function main(argv = process.argv, env = process.env) {
|
|
|
5012
5271
|
}
|
|
5013
5272
|
case 'stop':
|
|
5014
5273
|
requireExplicitProfile('stop', flags);
|
|
5015
|
-
printJson(await stopDaemon(flags.profile, env));
|
|
5274
|
+
printJson(await stopDaemon(flags.profile, env, { disable: Boolean(flags.disable) }));
|
|
5016
5275
|
break;
|
|
5017
5276
|
case 'restart':
|
|
5018
5277
|
printJson(await restartSavedBackground(flags, env));
|