@magclaw/cli-core 0.1.28 → 0.1.29
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/README.md +6 -0
- package/package.json +1 -1
- package/src/cli.js +573 -12
- package/src/list-renderer.js +2 -1
package/README.md
CHANGED
|
@@ -5,3 +5,9 @@ Shared local MagClaw CLI implementation.
|
|
|
5
5
|
This package owns the reusable command implementation used by
|
|
6
6
|
`@magclaw/daemon` and `@magclaw/computer`. Install the public entry packages
|
|
7
7
|
instead of using this package directly.
|
|
8
|
+
|
|
9
|
+
Public entry commands:
|
|
10
|
+
|
|
11
|
+
- `magclaw`: daemon/profile operations.
|
|
12
|
+
- `magclaw-computer`: browser-approved Computer setup and control-plane
|
|
13
|
+
operations.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -385,6 +385,10 @@ function rootLockPaths(env = process.env) {
|
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
function computerChannelPath(env = process.env) {
|
|
389
|
+
return path.join(daemonRoot(env), 'channel');
|
|
390
|
+
}
|
|
391
|
+
|
|
388
392
|
export function profilePaths(profile = DEFAULT_PROFILE, env = process.env) {
|
|
389
393
|
const profileName = safeProfileName(profile);
|
|
390
394
|
const dir = path.join(daemonRoot(env), 'profiles', profileName);
|
|
@@ -541,6 +545,10 @@ export function parseCli(argv = process.argv) {
|
|
|
541
545
|
flags.help = true;
|
|
542
546
|
continue;
|
|
543
547
|
}
|
|
548
|
+
if (item === '-V') {
|
|
549
|
+
flags.version = true;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
544
552
|
if (!item.startsWith('--')) {
|
|
545
553
|
positionals.push(item);
|
|
546
554
|
continue;
|
|
@@ -612,6 +620,150 @@ function renderHelp() {
|
|
|
612
620
|
].join('\n');
|
|
613
621
|
}
|
|
614
622
|
|
|
623
|
+
function renderComputerHelp(subcommand = '') {
|
|
624
|
+
const command = String(subcommand || '').trim();
|
|
625
|
+
const usage = {
|
|
626
|
+
login: [
|
|
627
|
+
'Usage: magclaw-computer login [options] <serverSlug>',
|
|
628
|
+
'',
|
|
629
|
+
'Start MagClaw browser approval for one server. This is an alias for setup.',
|
|
630
|
+
'',
|
|
631
|
+
'Options:',
|
|
632
|
+
' --server-url <url> MagClaw Cloud URL',
|
|
633
|
+
' --name <name> Computer display name',
|
|
634
|
+
' --no-start Save the approved profile without starting the daemon',
|
|
635
|
+
' -h, --help Show this help',
|
|
636
|
+
],
|
|
637
|
+
attach: [
|
|
638
|
+
'Usage: magclaw-computer attach [options] <serverSlug>',
|
|
639
|
+
'',
|
|
640
|
+
'Attach this Computer to one MagClaw server using browser approval.',
|
|
641
|
+
'',
|
|
642
|
+
'Options:',
|
|
643
|
+
' --server-url <url> MagClaw Cloud URL',
|
|
644
|
+
' --name <name> Computer display name',
|
|
645
|
+
' --no-run Save the approved profile without starting the daemon',
|
|
646
|
+
' --foreground Run foreground if background service cannot start',
|
|
647
|
+
' -h, --help Show this help',
|
|
648
|
+
],
|
|
649
|
+
setup: [
|
|
650
|
+
'Usage: magclaw-computer setup [options] <serverSlug>',
|
|
651
|
+
'',
|
|
652
|
+
'Set up this Computer for one server, then start its daemon unless --no-start is set.',
|
|
653
|
+
'',
|
|
654
|
+
'Options:',
|
|
655
|
+
' --server-url <url> MagClaw Cloud URL',
|
|
656
|
+
' --name <name> Computer display name',
|
|
657
|
+
' --no-start Save the approved profile without starting the daemon',
|
|
658
|
+
' --foreground Run foreground if background service cannot start',
|
|
659
|
+
' -h, --help Show this help',
|
|
660
|
+
],
|
|
661
|
+
detach: [
|
|
662
|
+
'Usage: magclaw-computer detach <serverSlug>',
|
|
663
|
+
'',
|
|
664
|
+
'Stop one local profile and remove its local attachment state.',
|
|
665
|
+
],
|
|
666
|
+
status: [
|
|
667
|
+
'Usage: magclaw-computer status [options] [serverSlug]',
|
|
668
|
+
'',
|
|
669
|
+
'Show aggregate Computer state, or one server profile when a slug is provided.',
|
|
670
|
+
'',
|
|
671
|
+
'Options:',
|
|
672
|
+
' --json Emit the machine-readable report',
|
|
673
|
+
' -h, --help Show this help',
|
|
674
|
+
],
|
|
675
|
+
start: [
|
|
676
|
+
'Usage: magclaw-computer start [options] [serverSlug]',
|
|
677
|
+
'',
|
|
678
|
+
'Start one saved background daemon profile, or all saved profiles when no slug is provided.',
|
|
679
|
+
'',
|
|
680
|
+
'Options:',
|
|
681
|
+
' --foreground Run in this terminal for one selected profile',
|
|
682
|
+
],
|
|
683
|
+
stop: [
|
|
684
|
+
'Usage: magclaw-computer stop [options] [serverSlug]',
|
|
685
|
+
'',
|
|
686
|
+
'Stop one daemon profile, or all saved profiles when no slug is provided.',
|
|
687
|
+
'',
|
|
688
|
+
'Options:',
|
|
689
|
+
' --disable Suppress background relaunch until the next start',
|
|
690
|
+
],
|
|
691
|
+
doctor: [
|
|
692
|
+
'Usage: magclaw-computer doctor [options] [serverSlug]',
|
|
693
|
+
'',
|
|
694
|
+
'Diagnose local profiles, service state, runtime availability, and stale pidfiles.',
|
|
695
|
+
'',
|
|
696
|
+
'Options:',
|
|
697
|
+
' --json Emit the machine-readable report',
|
|
698
|
+
' --cleanup Clear stale local locks while diagnosing',
|
|
699
|
+
' --fix Alias for --cleanup',
|
|
700
|
+
],
|
|
701
|
+
logs: [
|
|
702
|
+
'Usage: magclaw-computer logs [options] [serverSlug]',
|
|
703
|
+
'',
|
|
704
|
+
'Print recent daemon logs for one attached server profile.',
|
|
705
|
+
'',
|
|
706
|
+
'Options:',
|
|
707
|
+
' --lines <n> Number of trailing lines to print (default 120)',
|
|
708
|
+
' --server <slug> Select a server profile',
|
|
709
|
+
],
|
|
710
|
+
runners: [
|
|
711
|
+
'Usage: magclaw-computer runners <command> [options]',
|
|
712
|
+
'',
|
|
713
|
+
'Computer runner control plane.',
|
|
714
|
+
'',
|
|
715
|
+
'Commands:',
|
|
716
|
+
' list List local daemon profiles and known Computer bindings',
|
|
717
|
+
' stop <agentId> Not available locally; stop Agents from the MagClaw web console',
|
|
718
|
+
],
|
|
719
|
+
channel: [
|
|
720
|
+
'Usage: magclaw-computer channel [set <channel>]',
|
|
721
|
+
'',
|
|
722
|
+
'Show or set the local Computer release channel (latest | alpha | pinned:<semver>).',
|
|
723
|
+
],
|
|
724
|
+
upgrade: [
|
|
725
|
+
'Usage: magclaw-computer upgrade [options]',
|
|
726
|
+
'',
|
|
727
|
+
'Upgrade the background Computer package for a saved profile.',
|
|
728
|
+
'',
|
|
729
|
+
'Options:',
|
|
730
|
+
' --dry-run Preview upgrade actions',
|
|
731
|
+
' --channel <name> latest | alpha | pinned:<semver>',
|
|
732
|
+
' --target-version <semver> Explicit target version',
|
|
733
|
+
' --force Accepted for Slock parity; currently maps to the normal upgrade path',
|
|
734
|
+
],
|
|
735
|
+
};
|
|
736
|
+
if (command && usage[command]) return `${usage[command].join('\n')}\n`;
|
|
737
|
+
return [
|
|
738
|
+
`MagClaw Computer CLI ${DAEMON_VERSION}`,
|
|
739
|
+
'',
|
|
740
|
+
'Usage: magclaw-computer [options] [command]',
|
|
741
|
+
'',
|
|
742
|
+
'MagClaw Computer - local-machine control plane (browser approval + per-server profiles).',
|
|
743
|
+
'',
|
|
744
|
+
'Options:',
|
|
745
|
+
' -V, --version output the version number',
|
|
746
|
+
' -h, --help show help for command',
|
|
747
|
+
'',
|
|
748
|
+
'Commands:',
|
|
749
|
+
' login [options] <serverSlug> Browser-approved login for one server (alias for setup)',
|
|
750
|
+
' attach [options] <serverSlug> Attach this Computer to one server',
|
|
751
|
+
' setup [options] <serverSlug> Login/attach if needed, then start',
|
|
752
|
+
' adopt-legacy [options] <serverSlug> Migrate from legacy pair-token style setup when possible',
|
|
753
|
+
' detach <serverSlug> Remove one local server attachment',
|
|
754
|
+
' status [options] [serverSlug] Show aggregate or per-profile state',
|
|
755
|
+
' start [options] [serverSlug] Start one or all saved profiles',
|
|
756
|
+
' stop [options] [serverSlug] Stop one or all saved profiles',
|
|
757
|
+
' doctor [options] [serverSlug] Diagnose local profiles and runtime state',
|
|
758
|
+
' logs [options] [serverSlug] Print one profile daemon log',
|
|
759
|
+
' runners Computer runner control plane',
|
|
760
|
+
' channel Show or set release channel',
|
|
761
|
+
' upgrade [options] Upgrade the Computer package',
|
|
762
|
+
' help [command] show help for command',
|
|
763
|
+
'',
|
|
764
|
+
].join('\n');
|
|
765
|
+
}
|
|
766
|
+
|
|
615
767
|
async function readJsonFile(file, fallback = {}) {
|
|
616
768
|
if (!existsSync(file)) return fallback;
|
|
617
769
|
try {
|
|
@@ -710,6 +862,27 @@ async function clearRemoteClosedServiceState(profile = DEFAULT_PROFILE, env = pr
|
|
|
710
862
|
}, env);
|
|
711
863
|
}
|
|
712
864
|
|
|
865
|
+
export function daemonRunLaunchedByBackgroundService(env = process.env) {
|
|
866
|
+
return String(env.MAGCLAW_DAEMON_BACKGROUND_SERVICE || '').trim() === '1';
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function backgroundServiceModeForPlatform(platform = process.platform) {
|
|
870
|
+
if (platform === 'darwin') return 'launchd';
|
|
871
|
+
if (platform === 'linux') return 'systemd';
|
|
872
|
+
if (platform === 'win32') return 'schtasks';
|
|
873
|
+
return 'foreground';
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function serviceStatePatchForDaemonRun(service = {}, env = process.env, platform = process.platform) {
|
|
877
|
+
if (daemonRunLaunchedByBackgroundService(env)) {
|
|
878
|
+
return {
|
|
879
|
+
mode: service.mode || backgroundServiceModeForPlatform(platform),
|
|
880
|
+
background: true,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
return { mode: 'foreground', background: false };
|
|
884
|
+
}
|
|
885
|
+
|
|
713
886
|
async function markForegroundServiceState(profile = DEFAULT_PROFILE, env = process.env) {
|
|
714
887
|
const packageInfo = runtimePackageInfo(env);
|
|
715
888
|
return writeServiceState(profile, {
|
|
@@ -726,6 +899,22 @@ async function markForegroundServiceState(profile = DEFAULT_PROFILE, env = proce
|
|
|
726
899
|
}, env);
|
|
727
900
|
}
|
|
728
901
|
|
|
902
|
+
async function markDaemonRunServiceState(profile = DEFAULT_PROFILE, env = process.env) {
|
|
903
|
+
if (!daemonRunLaunchedByBackgroundService(env)) return markForegroundServiceState(profile, env);
|
|
904
|
+
const service = await readServiceState(profile, env);
|
|
905
|
+
const packageInfo = runtimePackageInfo(env, service);
|
|
906
|
+
return writeServiceState(profile, {
|
|
907
|
+
...serviceStatePatchForDaemonRun(service, env),
|
|
908
|
+
packageSpec: packageInfo.spec,
|
|
909
|
+
packageName: packageInfo.name,
|
|
910
|
+
packageVersion: packageInfo.version,
|
|
911
|
+
packageKind: packageInfo.kind,
|
|
912
|
+
packageBin: packageInfo.bin,
|
|
913
|
+
installedDaemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
914
|
+
installedPackageVersion: packageInfo.version || DAEMON_VERSION,
|
|
915
|
+
}, env);
|
|
916
|
+
}
|
|
917
|
+
|
|
729
918
|
async function readUpgradeHandoff(profile = DEFAULT_PROFILE, env = process.env) {
|
|
730
919
|
const paths = profilePaths(profile, env);
|
|
731
920
|
const handoff = await readJsonFile(paths.upgradeHandoff, null);
|
|
@@ -3249,6 +3438,8 @@ class MagClawDaemon {
|
|
|
3249
3438
|
this.pendingUpgradeRequest = null;
|
|
3250
3439
|
this.upgradeIdleTimer = null;
|
|
3251
3440
|
this.upgradeWorkerStarting = false;
|
|
3441
|
+
this.runtimeStatusTimer = null;
|
|
3442
|
+
this.runtimeStatusInFlight = false;
|
|
3252
3443
|
}
|
|
3253
3444
|
|
|
3254
3445
|
send(payload) {
|
|
@@ -3619,7 +3810,6 @@ class MagClawDaemon {
|
|
|
3619
3810
|
}
|
|
3620
3811
|
|
|
3621
3812
|
async readyPayload() {
|
|
3622
|
-
const runtimes = await detectRuntimes(this.env);
|
|
3623
3813
|
const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
|
|
3624
3814
|
const service = await readServiceState(this.paths.profile, this.env);
|
|
3625
3815
|
const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
|
|
@@ -3658,8 +3848,7 @@ class MagClawDaemon {
|
|
|
3658
3848
|
cliCoreVersion: CLI_CORE_VERSION,
|
|
3659
3849
|
},
|
|
3660
3850
|
upgrade: upgrade || null,
|
|
3661
|
-
|
|
3662
|
-
runtimeDetails: runtimes,
|
|
3851
|
+
runtimeScanPending: true,
|
|
3663
3852
|
runningAgents: [...this.sessions.keys()],
|
|
3664
3853
|
capabilities: CAPABILITIES,
|
|
3665
3854
|
};
|
|
@@ -3670,10 +3859,68 @@ class MagClawDaemon {
|
|
|
3670
3859
|
const sent = this.send(payload);
|
|
3671
3860
|
logInfo(
|
|
3672
3861
|
'daemon',
|
|
3673
|
-
`Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes
|
|
3862
|
+
`Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes=deferred, runningAgents=${payload.runningAgents.length}, sent=${sent}).`,
|
|
3674
3863
|
);
|
|
3675
3864
|
}
|
|
3676
3865
|
|
|
3866
|
+
runtimeStatusDelayMs() {
|
|
3867
|
+
return envInteger(this.env, 'MAGCLAW_DAEMON_RUNTIME_STATUS_DELAY_MS', 1000, { min: 0, max: 60_000 });
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
clearRuntimeStatusTimer() {
|
|
3871
|
+
if (!this.runtimeStatusTimer) return;
|
|
3872
|
+
clearTimeout(this.runtimeStatusTimer);
|
|
3873
|
+
this.runtimeStatusTimer = null;
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
scheduleRuntimeStatus(reason = 'ready_ack') {
|
|
3877
|
+
if (this.closed || this.runtimeStatusTimer || this.runtimeStatusInFlight) return;
|
|
3878
|
+
this.runtimeStatusTimer = setTimeout(() => {
|
|
3879
|
+
this.runtimeStatusTimer = null;
|
|
3880
|
+
this.sendRuntimeStatus(reason).catch((error) => {
|
|
3881
|
+
logWarning('daemon', `Failed to send runtime status: ${error.message}`);
|
|
3882
|
+
});
|
|
3883
|
+
}, this.runtimeStatusDelayMs());
|
|
3884
|
+
this.runtimeStatusTimer.unref?.();
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
async runtimeStatusPayload(reason = 'ready_ack') {
|
|
3888
|
+
const runtimes = await detectRuntimes(this.env);
|
|
3889
|
+
const packageInfo = runtimePackageInfo(this.env);
|
|
3890
|
+
return {
|
|
3891
|
+
type: 'daemon:runtime_status',
|
|
3892
|
+
time: now(),
|
|
3893
|
+
reason,
|
|
3894
|
+
computerId: this.config.computerId || null,
|
|
3895
|
+
daemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
3896
|
+
packageName: packageInfo.name,
|
|
3897
|
+
packageVersion: packageInfo.version,
|
|
3898
|
+
packageKind: packageInfo.kind,
|
|
3899
|
+
packageSpec: packageInfo.spec,
|
|
3900
|
+
packageBin: packageInfo.bin,
|
|
3901
|
+
cliCoreVersion: CLI_CORE_VERSION,
|
|
3902
|
+
runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
|
|
3903
|
+
runtimeDetails: runtimes,
|
|
3904
|
+
runningAgents: [...this.sessions.keys()],
|
|
3905
|
+
};
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
async sendRuntimeStatus(reason = 'ready_ack') {
|
|
3909
|
+
if (this.closed || this.runtimeStatusInFlight) return false;
|
|
3910
|
+
this.runtimeStatusInFlight = true;
|
|
3911
|
+
try {
|
|
3912
|
+
const payload = await this.runtimeStatusPayload(reason);
|
|
3913
|
+
const sent = this.send(payload);
|
|
3914
|
+
logInfo(
|
|
3915
|
+
'daemon',
|
|
3916
|
+
`Sent runtime status for computer ${payload.computerId || 'unpaired'} (runtimes=${payload.runtimes.join(', ') || 'none'}, sent=${sent}).`,
|
|
3917
|
+
);
|
|
3918
|
+
return sent;
|
|
3919
|
+
} finally {
|
|
3920
|
+
this.runtimeStatusInFlight = false;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3677
3924
|
sendHeartbeat() {
|
|
3678
3925
|
const packageInfo = runtimePackageInfo(this.env);
|
|
3679
3926
|
const sent = this.send({
|
|
@@ -3801,6 +4048,7 @@ class MagClawDaemon {
|
|
|
3801
4048
|
break;
|
|
3802
4049
|
case 'ready:ack':
|
|
3803
4050
|
logInfo('daemon', `MagClaw daemon ready for computer ${message.computerId || this.config.computerId}.`);
|
|
4051
|
+
this.scheduleRuntimeStatus('ready_ack');
|
|
3804
4052
|
break;
|
|
3805
4053
|
case 'ping':
|
|
3806
4054
|
this.send({ type: 'pong', time: now() });
|
|
@@ -4162,6 +4410,7 @@ class MagClawDaemon {
|
|
|
4162
4410
|
this.sessions.clear();
|
|
4163
4411
|
this.stopHeartbeat();
|
|
4164
4412
|
this.clearInboundWatchdog();
|
|
4413
|
+
this.clearRuntimeStatusTimer();
|
|
4165
4414
|
if (this.agentStartPumpTimer) {
|
|
4166
4415
|
clearTimeout(this.agentStartPumpTimer);
|
|
4167
4416
|
this.agentStartPumpTimer = null;
|
|
@@ -4214,6 +4463,7 @@ class MagClawDaemon {
|
|
|
4214
4463
|
settled = true;
|
|
4215
4464
|
this.stopHeartbeat();
|
|
4216
4465
|
this.clearInboundWatchdog();
|
|
4466
|
+
this.clearRuntimeStatusTimer();
|
|
4217
4467
|
if (this.socket && this.socket.destroyed) this.socket = null;
|
|
4218
4468
|
if (this.request === req) this.request = null;
|
|
4219
4469
|
callback(value);
|
|
@@ -4437,6 +4687,7 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4437
4687
|
" MAGCLAW_DAEMON_PACKAGE_SPEC: packageSpec,",
|
|
4438
4688
|
" MAGCLAW_DAEMON_PACKAGE_KIND: packageKind,",
|
|
4439
4689
|
" MAGCLAW_DAEMON_PACKAGE_BIN: packageBin,",
|
|
4690
|
+
" MAGCLAW_DAEMON_BACKGROUND_SERVICE: '1',",
|
|
4440
4691
|
" PATH: launchPath,",
|
|
4441
4692
|
"};",
|
|
4442
4693
|
"if (packageKind === 'computer') childEnv.MAGCLAW_COMPUTER_DAEMON = '1';",
|
|
@@ -4591,17 +4842,40 @@ async function startBackground(profile, env = process.env) {
|
|
|
4591
4842
|
return { ok: false, mode: 'foreground', message: 'Background daemon is only automated on macOS launchd, Linux user systemd, and Windows schtasks.' };
|
|
4592
4843
|
}
|
|
4593
4844
|
|
|
4845
|
+
export function parseLaunchdPrintStatus(result = {}) {
|
|
4846
|
+
const stdout = String(result.stdout || '');
|
|
4847
|
+
const stderr = String(result.stderr || '');
|
|
4848
|
+
const state = (stdout.match(/^\s*state\s*=\s*(.+?)\s*$/m)?.[1] || '').trim();
|
|
4849
|
+
const activeCountValue = Number(stdout.match(/^\s*active count\s*=\s*(\d+)\s*$/m)?.[1] || NaN);
|
|
4850
|
+
const stateStatus = state || (result.status === 0 ? 'loaded' : 'inactive');
|
|
4851
|
+
const active = result.status === 0 && (
|
|
4852
|
+
stateStatus.toLowerCase() === 'running'
|
|
4853
|
+
|| (!state && Number.isFinite(activeCountValue) && activeCountValue > 0)
|
|
4854
|
+
);
|
|
4855
|
+
return {
|
|
4856
|
+
active,
|
|
4857
|
+
status: active ? 'running' : stateStatus,
|
|
4858
|
+
state,
|
|
4859
|
+
activeCount: Number.isFinite(activeCountValue) ? activeCountValue : null,
|
|
4860
|
+
error: result.status === 0 ? '' : String(stderr || stdout || '').trim(),
|
|
4861
|
+
};
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4594
4864
|
function backgroundServiceStatus(profile, env = process.env) {
|
|
4595
4865
|
const paths = profilePaths(profile, env);
|
|
4596
4866
|
if (process.platform === 'darwin') {
|
|
4597
4867
|
const label = launchAgentLabel(paths.profile);
|
|
4598
4868
|
const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
|
|
4869
|
+
const parsed = parseLaunchdPrintStatus(result);
|
|
4599
4870
|
return {
|
|
4600
4871
|
mode: 'launchd',
|
|
4601
|
-
active:
|
|
4872
|
+
active: parsed.active,
|
|
4602
4873
|
label,
|
|
4603
4874
|
file: path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`),
|
|
4604
|
-
|
|
4875
|
+
status: parsed.status,
|
|
4876
|
+
state: parsed.state,
|
|
4877
|
+
activeCount: parsed.activeCount,
|
|
4878
|
+
error: parsed.error,
|
|
4605
4879
|
};
|
|
4606
4880
|
}
|
|
4607
4881
|
if (process.platform === 'linux') {
|
|
@@ -4811,14 +5085,15 @@ async function status(profile) {
|
|
|
4811
5085
|
};
|
|
4812
5086
|
}
|
|
4813
5087
|
|
|
4814
|
-
async function logs(profile) {
|
|
5088
|
+
async function logs(profile, options = {}) {
|
|
4815
5089
|
const paths = profilePaths(profile);
|
|
5090
|
+
const lines = Math.max(1, Math.min(5000, Number(options.lines || 120) || 120));
|
|
4816
5091
|
const files = [path.join(paths.logDir, 'daemon.log'), path.join(paths.logDir, 'daemon.err.log')];
|
|
4817
5092
|
for (const file of files) {
|
|
4818
5093
|
if (!existsSync(file)) continue;
|
|
4819
5094
|
process.stdout.write(`\n==> ${file} <==\n`);
|
|
4820
5095
|
const text = await readFile(file, 'utf8').catch(() => '');
|
|
4821
|
-
process.stdout.write(text.split(/\r?\n/).slice(-
|
|
5096
|
+
process.stdout.write(text.split(/\r?\n/).slice(-lines).join('\n'));
|
|
4822
5097
|
process.stdout.write('\n');
|
|
4823
5098
|
}
|
|
4824
5099
|
}
|
|
@@ -4857,6 +5132,9 @@ async function listProfiles(env = process.env) {
|
|
|
4857
5132
|
label: service.label || '',
|
|
4858
5133
|
serviceName: service.serviceName || '',
|
|
4859
5134
|
taskName: service.taskName || '',
|
|
5135
|
+
status: service.status || '',
|
|
5136
|
+
state: service.state || '',
|
|
5137
|
+
activeCount: service.activeCount ?? null,
|
|
4860
5138
|
},
|
|
4861
5139
|
createdAt: config.createdAt || '',
|
|
4862
5140
|
updatedAt: config.updatedAt || '',
|
|
@@ -5303,10 +5581,22 @@ async function postSetupJson(serverUrl, pathname, body = {}) {
|
|
|
5303
5581
|
return data;
|
|
5304
5582
|
}
|
|
5305
5583
|
|
|
5584
|
+
function hasComputerTarget(flags = {}) {
|
|
5585
|
+
return Boolean(flags.profileExplicit || flags.server || flags.serverSlug || flags.slug || flags._?.[1]);
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
function profileFromComputerTarget(value = '') {
|
|
5589
|
+
return safeProfileName(normalizeSetupServerSlug(value) || value || DEFAULT_PROFILE);
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
function computerTargetProfile(flags = {}, fallback = DEFAULT_PROFILE) {
|
|
5593
|
+
return profileFromComputerTarget(flags.server || flags.serverSlug || flags.slug || flags._?.[1] || flags.profile || fallback);
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5306
5596
|
async function runComputerSetup(flags, env = process.env) {
|
|
5307
5597
|
const subcommand = String(flags._?.[0] || '').trim();
|
|
5308
|
-
if (
|
|
5309
|
-
throw new Error('Usage: magclaw
|
|
5598
|
+
if (!['setup', 'attach', 'login'].includes(subcommand)) {
|
|
5599
|
+
throw new Error('Usage: magclaw-computer setup /<server-slug> --server-url <url>');
|
|
5310
5600
|
}
|
|
5311
5601
|
const serverSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
|
|
5312
5602
|
if (!serverSlug) throw new Error('Run computer setup with a server slug, for example: magclaw computer setup /my-server');
|
|
@@ -5367,6 +5657,20 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5367
5657
|
await saveProfile(config.profile, config, env);
|
|
5368
5658
|
await clearRemoteClosedServiceState(config.profile, env);
|
|
5369
5659
|
const cli = await tryInstallCliShim(flags, env);
|
|
5660
|
+
if (flags.noStart || flags.noRun) {
|
|
5661
|
+
printJson({
|
|
5662
|
+
ok: true,
|
|
5663
|
+
started: false,
|
|
5664
|
+
cli,
|
|
5665
|
+
computerId: config.computerId,
|
|
5666
|
+
computerName: config.computerName || config.name || displayName,
|
|
5667
|
+
profile: config.profile,
|
|
5668
|
+
serverName: config.serverName,
|
|
5669
|
+
serverSlug: approved.serverSlug || serverSlug,
|
|
5670
|
+
next: `Run magclaw-computer start ${config.profile} when ready.`,
|
|
5671
|
+
});
|
|
5672
|
+
return;
|
|
5673
|
+
}
|
|
5370
5674
|
const result = await startBackground(config.profile, env);
|
|
5371
5675
|
printJson({
|
|
5372
5676
|
...result,
|
|
@@ -5383,6 +5687,255 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5383
5687
|
}
|
|
5384
5688
|
}
|
|
5385
5689
|
|
|
5690
|
+
async function renderComputerAggregateStatus(env = process.env) {
|
|
5691
|
+
const report = await listProfiles(env);
|
|
5692
|
+
return {
|
|
5693
|
+
ok: true,
|
|
5694
|
+
root: report.root,
|
|
5695
|
+
loggedIn: report.profiles.some((profile) => profile.configured),
|
|
5696
|
+
supervisor: {
|
|
5697
|
+
model: 'per-profile-service',
|
|
5698
|
+
running: report.profiles.some((profile) => profile.running || profile.service?.active),
|
|
5699
|
+
managedProfiles: report.profiles.length,
|
|
5700
|
+
},
|
|
5701
|
+
profiles: report.profiles,
|
|
5702
|
+
};
|
|
5703
|
+
}
|
|
5704
|
+
|
|
5705
|
+
function formatComputerStatus(report = {}) {
|
|
5706
|
+
const profiles = report.profiles || [];
|
|
5707
|
+
if (report.profile) {
|
|
5708
|
+
return [
|
|
5709
|
+
`Profile: ${report.profile}`,
|
|
5710
|
+
`Configured: ${report.configured ? 'yes' : 'no'}`,
|
|
5711
|
+
`Daemon: ${report.running ? `running (pid ${report.pid})` : 'stopped'}`,
|
|
5712
|
+
`Service: ${report.service?.mode || 'foreground'}${report.service?.active ? ' active' : ''}`,
|
|
5713
|
+
`Server URL: ${report.serverUrl || '-'}`,
|
|
5714
|
+
`Computer ID: ${report.computerId || '-'}`,
|
|
5715
|
+
`Config: ${report.configPath}`,
|
|
5716
|
+
'',
|
|
5717
|
+
].join('\n');
|
|
5718
|
+
}
|
|
5719
|
+
return [
|
|
5720
|
+
'MagClaw Computers',
|
|
5721
|
+
`Profiles: ${profiles.length} Running: ${profiles.filter((profile) => profile.running || profile.service?.active).length}`,
|
|
5722
|
+
`Root: ${report.root || '-'}`,
|
|
5723
|
+
'',
|
|
5724
|
+
...profiles.map((profile) => [
|
|
5725
|
+
`${profile.running || profile.service?.active ? 'online ' : 'offline'} ${profile.profile}`,
|
|
5726
|
+
` server=${profile.serverSlug || profile.serverName || '-'} computer=${profile.computerId || '-'} service=${profile.service?.mode || 'foreground'}`,
|
|
5727
|
+
].join('\n')),
|
|
5728
|
+
profiles.length ? '' : 'No profiles. Run `magclaw-computer setup /<serverSlug>` first.',
|
|
5729
|
+
'',
|
|
5730
|
+
].join('\n');
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5733
|
+
async function computerStatus(flags = {}, env = process.env) {
|
|
5734
|
+
if (hasComputerTarget(flags)) return status(computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE));
|
|
5735
|
+
return renderComputerAggregateStatus(env);
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5738
|
+
async function startAllComputerProfiles(env = process.env) {
|
|
5739
|
+
const report = await listProfiles(env);
|
|
5740
|
+
const results = [];
|
|
5741
|
+
for (const profile of report.profiles) {
|
|
5742
|
+
if (!profile.configured) continue;
|
|
5743
|
+
results.push({ profile: profile.profile, ...(await startSavedBackground({ profile: profile.profile }, env)) });
|
|
5744
|
+
}
|
|
5745
|
+
return { ok: results.every((item) => item.ok), count: results.length, results };
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5748
|
+
async function stopAllComputerProfiles(flags = {}, env = process.env) {
|
|
5749
|
+
const report = await listProfiles(env);
|
|
5750
|
+
const results = [];
|
|
5751
|
+
for (const profile of report.profiles) {
|
|
5752
|
+
results.push({ profile: profile.profile, ...(await stopDaemon(profile.profile, env, { disable: Boolean(flags.disable) })) });
|
|
5753
|
+
}
|
|
5754
|
+
return { ok: results.every((item) => item.ok), count: results.length, results };
|
|
5755
|
+
}
|
|
5756
|
+
|
|
5757
|
+
async function detachComputerProfile(flags = {}, env = process.env) {
|
|
5758
|
+
const profile = computerTargetProfile(flags);
|
|
5759
|
+
if (!profile || profile === DEFAULT_PROFILE && !hasComputerTarget(flags)) {
|
|
5760
|
+
throw new Error('Usage: magclaw-computer detach <serverSlug>');
|
|
5761
|
+
}
|
|
5762
|
+
const paths = profilePaths(profile, env);
|
|
5763
|
+
const stopped = await uninstallBackground(profile, env);
|
|
5764
|
+
await rm(paths.dir, { recursive: true, force: true });
|
|
5765
|
+
return { ok: true, profile, detached: true, stopped };
|
|
5766
|
+
}
|
|
5767
|
+
|
|
5768
|
+
async function cleanupComputerResidue(env = process.env) {
|
|
5769
|
+
const report = await listProfiles(env);
|
|
5770
|
+
const cleaned = [];
|
|
5771
|
+
await activeComputerLock(env);
|
|
5772
|
+
for (const profile of report.profiles) {
|
|
5773
|
+
const paths = profilePaths(profile.profile, env);
|
|
5774
|
+
const before = existsSync(paths.lockFile);
|
|
5775
|
+
await activeDaemonLock(profile.profile, env);
|
|
5776
|
+
if (before && !existsSync(paths.lockFile)) cleaned.push(paths.lockFile);
|
|
5777
|
+
}
|
|
5778
|
+
return cleaned;
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
async function computerDoctor(flags = {}, env = process.env) {
|
|
5782
|
+
const cleanup = Boolean(flags.cleanup || flags.fix);
|
|
5783
|
+
const target = hasComputerTarget(flags) ? computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE) : '';
|
|
5784
|
+
const runtime = await doctor(env);
|
|
5785
|
+
const aggregate = await renderComputerAggregateStatus(env);
|
|
5786
|
+
const selected = target ? await status(target) : null;
|
|
5787
|
+
const cleaned = cleanup ? await cleanupComputerResidue(env) : [];
|
|
5788
|
+
const checks = [
|
|
5789
|
+
{ name: 'MAGCLAW_DAEMON_HOME', ok: true, detail: aggregate.root },
|
|
5790
|
+
{ name: 'profiles', ok: aggregate.profiles.length > 0, detail: `${aggregate.profiles.length} configured` },
|
|
5791
|
+
{ name: 'runtime', ok: runtime.runtimes.some((item) => item.available), detail: runtime.runtimes.filter((item) => item.available).map((item) => item.id).join(', ') || 'none detected' },
|
|
5792
|
+
];
|
|
5793
|
+
if (selected) {
|
|
5794
|
+
checks.push(
|
|
5795
|
+
{ name: `profile ${target}`, ok: selected.configured, detail: selected.configPath },
|
|
5796
|
+
{ name: `daemon ${target}`, ok: selected.running || selected.service?.active, detail: selected.running ? `running (pid ${selected.pid})` : (selected.service?.active ? 'service active' : 'stopped') },
|
|
5797
|
+
);
|
|
5798
|
+
}
|
|
5799
|
+
return {
|
|
5800
|
+
ok: checks.every((check) => check.ok !== false),
|
|
5801
|
+
checks,
|
|
5802
|
+
runtime,
|
|
5803
|
+
aggregate,
|
|
5804
|
+
...(selected ? { profile: selected } : {}),
|
|
5805
|
+
cleanup: { requested: cleanup, staleLocksCleared: cleaned },
|
|
5806
|
+
};
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5809
|
+
async function readComputerChannel(env = process.env) {
|
|
5810
|
+
const file = computerChannelPath(env);
|
|
5811
|
+
const value = existsSync(file) ? String(await readFile(file, 'utf8')).trim() : 'latest';
|
|
5812
|
+
return value || 'latest';
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5815
|
+
function validateComputerChannel(value = '') {
|
|
5816
|
+
const channel = String(value || '').trim();
|
|
5817
|
+
if (channel === 'latest' || channel === 'alpha' || /^pinned:[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(channel)) return channel;
|
|
5818
|
+
throw new Error('Channel must be latest, alpha, or pinned:<semver>.');
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5821
|
+
async function setComputerChannel(value, env = process.env) {
|
|
5822
|
+
const channel = validateComputerChannel(value);
|
|
5823
|
+
const file = computerChannelPath(env);
|
|
5824
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
5825
|
+
await writeFile(file, `${channel}\n`);
|
|
5826
|
+
return channel;
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
async function computerChannel(flags = {}, env = process.env) {
|
|
5830
|
+
const action = String(flags._?.[1] || flags._?.[0] || '').trim();
|
|
5831
|
+
const value = String(flags._?.[2] || flags.channel || '').trim();
|
|
5832
|
+
if (action === 'set') {
|
|
5833
|
+
const channel = await setComputerChannel(value, env);
|
|
5834
|
+
return { ok: true, channel };
|
|
5835
|
+
}
|
|
5836
|
+
return { ok: true, channel: await readComputerChannel(env), file: computerChannelPath(env) };
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
async function computerRunners(flags = {}, env = process.env) {
|
|
5840
|
+
const action = String(flags._?.[1] || 'list').trim();
|
|
5841
|
+
if (action === 'list') {
|
|
5842
|
+
const aggregate = await renderComputerAggregateStatus(env);
|
|
5843
|
+
return {
|
|
5844
|
+
ok: true,
|
|
5845
|
+
note: 'MagClaw local CLI can list Computer profiles. Per-agent runner stop/list remains a cloud console or agent-tool operation.',
|
|
5846
|
+
profiles: aggregate.profiles.map((profile) => ({
|
|
5847
|
+
profile: profile.profile,
|
|
5848
|
+
serverSlug: profile.serverSlug,
|
|
5849
|
+
computerId: profile.computerId,
|
|
5850
|
+
running: profile.running || profile.service?.active,
|
|
5851
|
+
})),
|
|
5852
|
+
};
|
|
5853
|
+
}
|
|
5854
|
+
if (action === 'stop') {
|
|
5855
|
+
throw new Error('Local runner stop is not available yet. Stop Agents from the MagClaw web console or agent runtime controls.');
|
|
5856
|
+
}
|
|
5857
|
+
throw new Error(`Unknown runners command: ${action}`);
|
|
5858
|
+
}
|
|
5859
|
+
|
|
5860
|
+
async function computerUpgrade(flags = {}, env = process.env) {
|
|
5861
|
+
const channel = flags.channel ? validateComputerChannel(flags.channel) : await readComputerChannel(env);
|
|
5862
|
+
const targetVersion = flags.targetVersion || flags.to || flags.version || (String(channel).startsWith('pinned:') ? String(channel).slice('pinned:'.length) : channel);
|
|
5863
|
+
await runManualUpgrade({
|
|
5864
|
+
...flags,
|
|
5865
|
+
to: targetVersion,
|
|
5866
|
+
targetVersion,
|
|
5867
|
+
packageName: COMPUTER_PACKAGE_NAME,
|
|
5868
|
+
packageBin: 'magclaw-computer',
|
|
5869
|
+
}, {
|
|
5870
|
+
...env,
|
|
5871
|
+
MAGCLAW_ENTRY_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
|
|
5872
|
+
MAGCLAW_DAEMON_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
|
|
5873
|
+
MAGCLAW_DAEMON_PACKAGE_KIND: 'computer',
|
|
5874
|
+
MAGCLAW_DAEMON_PACKAGE_BIN: 'magclaw-computer',
|
|
5875
|
+
});
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
async function runComputerCommand(flags, env = process.env) {
|
|
5879
|
+
const subcommand = String(flags._?.[0] || 'help').trim();
|
|
5880
|
+
if (subcommand === 'help' || flags.help) {
|
|
5881
|
+
process.stdout.write(renderComputerHelp(subcommand === 'help' ? flags._?.[1] : subcommand));
|
|
5882
|
+
return;
|
|
5883
|
+
}
|
|
5884
|
+
switch (subcommand) {
|
|
5885
|
+
case 'login':
|
|
5886
|
+
case 'attach':
|
|
5887
|
+
case 'setup':
|
|
5888
|
+
await runComputerSetup(flags, env);
|
|
5889
|
+
break;
|
|
5890
|
+
case 'adopt-legacy':
|
|
5891
|
+
throw new Error('MagClaw legacy adoption is handled by `magclaw-computer setup /<serverSlug>` or `magclaw connect --pair-token <token>`.');
|
|
5892
|
+
case 'detach':
|
|
5893
|
+
printJson(await detachComputerProfile(flags, env));
|
|
5894
|
+
break;
|
|
5895
|
+
case 'status': {
|
|
5896
|
+
const report = await computerStatus(flags, env);
|
|
5897
|
+
if (flags.json) printJson(report);
|
|
5898
|
+
else process.stdout.write(formatComputerStatus(report));
|
|
5899
|
+
break;
|
|
5900
|
+
}
|
|
5901
|
+
case 'start':
|
|
5902
|
+
if (hasComputerTarget(flags)) {
|
|
5903
|
+
if (flags.foreground) {
|
|
5904
|
+
await runForegroundDaemon(await buildConfig({ ...flags, profile: computerTargetProfile(flags) }, env), env);
|
|
5905
|
+
} else {
|
|
5906
|
+
printJson(await startSavedBackground({ ...flags, profile: computerTargetProfile(flags) }, env));
|
|
5907
|
+
}
|
|
5908
|
+
} else {
|
|
5909
|
+
printJson(await startAllComputerProfiles(env));
|
|
5910
|
+
}
|
|
5911
|
+
break;
|
|
5912
|
+
case 'stop':
|
|
5913
|
+
if (hasComputerTarget(flags)) printJson(await stopDaemon(computerTargetProfile(flags), env, { disable: Boolean(flags.disable) }));
|
|
5914
|
+
else printJson(await stopAllComputerProfiles(flags, env));
|
|
5915
|
+
break;
|
|
5916
|
+
case 'doctor': {
|
|
5917
|
+
const report = await computerDoctor(flags, env);
|
|
5918
|
+
if (flags.json) printJson(report);
|
|
5919
|
+
else process.stdout.write(`${report.checks.map((check) => `${check.ok ? 'ok' : 'fail'} ${check.name}: ${check.detail}`).join('\n')}\n`);
|
|
5920
|
+
break;
|
|
5921
|
+
}
|
|
5922
|
+
case 'logs':
|
|
5923
|
+
await logs(computerTargetProfile(flags), { lines: flags.lines || flags.lineCount });
|
|
5924
|
+
break;
|
|
5925
|
+
case 'runners':
|
|
5926
|
+
printJson(await computerRunners(flags, env));
|
|
5927
|
+
break;
|
|
5928
|
+
case 'channel':
|
|
5929
|
+
printJson(await computerChannel(flags, env));
|
|
5930
|
+
break;
|
|
5931
|
+
case 'upgrade':
|
|
5932
|
+
await computerUpgrade(flags, env);
|
|
5933
|
+
break;
|
|
5934
|
+
default:
|
|
5935
|
+
throw new Error(`Unknown computer command: ${subcommand}`);
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5386
5939
|
async function buildConfig(flags, env = process.env) {
|
|
5387
5940
|
const diskConfig = await readProfile(flags.profile, env);
|
|
5388
5941
|
const profile = flags.profile || diskConfig.profile || DEFAULT_PROFILE;
|
|
@@ -5405,7 +5958,7 @@ async function buildConfig(flags, env = process.env) {
|
|
|
5405
5958
|
|
|
5406
5959
|
async function runForegroundDaemon(config, env = process.env) {
|
|
5407
5960
|
const releaseLock = await acquireDaemonLock(config.profile, config, env);
|
|
5408
|
-
await
|
|
5961
|
+
await markDaemonRunServiceState(config.profile, env);
|
|
5409
5962
|
const daemon = new MagClawDaemon(config, env);
|
|
5410
5963
|
let forceExitTimer = null;
|
|
5411
5964
|
const shutdown = (signal) => {
|
|
@@ -5482,6 +6035,14 @@ function requireExplicitProfile(command, flags = {}) {
|
|
|
5482
6035
|
|
|
5483
6036
|
export async function main(argv = process.argv, env = process.env) {
|
|
5484
6037
|
const { command, flags } = parseCli(argv);
|
|
6038
|
+
if (flags.version) {
|
|
6039
|
+
process.stdout.write(`${DAEMON_VERSION}\n`);
|
|
6040
|
+
return;
|
|
6041
|
+
}
|
|
6042
|
+
if (command === 'computer' && flags.help) {
|
|
6043
|
+
process.stdout.write(renderComputerHelp(flags._?.[0] || ''));
|
|
6044
|
+
return;
|
|
6045
|
+
}
|
|
5485
6046
|
if (command === 'help' || flags.help) {
|
|
5486
6047
|
process.stdout.write(renderHelp());
|
|
5487
6048
|
return;
|
|
@@ -5491,7 +6052,7 @@ export async function main(argv = process.argv, env = process.env) {
|
|
|
5491
6052
|
await runConnect(flags, env);
|
|
5492
6053
|
break;
|
|
5493
6054
|
case 'computer':
|
|
5494
|
-
await
|
|
6055
|
+
await runComputerCommand(flags, env);
|
|
5495
6056
|
break;
|
|
5496
6057
|
case 'start': {
|
|
5497
6058
|
printJson(await startSavedBackground(flags, env));
|
package/src/list-renderer.js
CHANGED
|
@@ -57,7 +57,8 @@ function statusLabel(profile, color) {
|
|
|
57
57
|
|
|
58
58
|
function serviceLabel(profile) {
|
|
59
59
|
const mode = fallback(profile.service?.mode, 'foreground');
|
|
60
|
-
|
|
60
|
+
const status = fallback(profile.service?.status || (profile.service?.active ? 'active' : 'inactive'), 'inactive');
|
|
61
|
+
return `${mode}: ${status}`;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function shouldUseColor({ env = process.env, stream = process.stdout, flags = {} } = {}) {
|