@magclaw/cli-core 0.1.27 → 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 +603 -18
- 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
|
@@ -12,7 +12,7 @@ import { renderListProfiles, shouldUseColor } from './list-renderer.js';
|
|
|
12
12
|
export const DEFAULT_PROFILE = 'default';
|
|
13
13
|
export const DEFAULT_SERVER_URL = 'http://127.0.0.1:6543';
|
|
14
14
|
const DEFAULT_DAEMON_HEARTBEAT_MS = 25_000;
|
|
15
|
-
const DEFAULT_DAEMON_INBOUND_WATCHDOG_MS =
|
|
15
|
+
const DEFAULT_DAEMON_INBOUND_WATCHDOG_MS = 15_000;
|
|
16
16
|
const DEFAULT_DAEMON_RECONNECT_MIN_MS = 1_000;
|
|
17
17
|
const DEFAULT_DAEMON_RECONNECT_MAX_MS = 30_000;
|
|
18
18
|
const DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
|
|
@@ -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) {
|
|
@@ -3613,13 +3804,12 @@ class MagClawDaemon {
|
|
|
3613
3804
|
} else {
|
|
3614
3805
|
logInfo('daemon', 'Foreground close request did not stop background service.');
|
|
3615
3806
|
}
|
|
3616
|
-
this.close();
|
|
3807
|
+
this.close({ notify: false, reason: 'cloud_close' });
|
|
3617
3808
|
process.exitCode = 0;
|
|
3618
3809
|
setTimeout(() => process.exit(0), 50).unref?.();
|
|
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() });
|
|
@@ -3856,7 +4104,7 @@ class MagClawDaemon {
|
|
|
3856
4104
|
break;
|
|
3857
4105
|
case 'token:revoked':
|
|
3858
4106
|
logError('daemon', 'Machine token was revoked by the server.');
|
|
3859
|
-
this.close();
|
|
4107
|
+
this.close({ notify: false, reason: 'token_revoked' });
|
|
3860
4108
|
process.exitCode = 2;
|
|
3861
4109
|
break;
|
|
3862
4110
|
default:
|
|
@@ -4134,13 +4382,35 @@ class MagClawDaemon {
|
|
|
4134
4382
|
});
|
|
4135
4383
|
}
|
|
4136
4384
|
|
|
4137
|
-
|
|
4385
|
+
sendStoppingNotice(reason = 'local_stop') {
|
|
4386
|
+
const packageInfo = runtimePackageInfo(this.env);
|
|
4387
|
+
const sent = this.send({
|
|
4388
|
+
type: 'daemon:stopping',
|
|
4389
|
+
time: now(),
|
|
4390
|
+
reason,
|
|
4391
|
+
computerId: this.config.computerId || null,
|
|
4392
|
+
daemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
4393
|
+
packageName: packageInfo.name,
|
|
4394
|
+
packageVersion: packageInfo.version,
|
|
4395
|
+
packageKind: packageInfo.kind,
|
|
4396
|
+
packageSpec: packageInfo.spec,
|
|
4397
|
+
packageBin: packageInfo.bin,
|
|
4398
|
+
runningAgents: [...this.sessions.keys()],
|
|
4399
|
+
});
|
|
4400
|
+
if (sent) logInfo('daemon', `Sent stopping notice (${reason}).`);
|
|
4401
|
+
return sent;
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
close(options = {}) {
|
|
4138
4405
|
if (this.closed) return;
|
|
4406
|
+
const notify = options.notify !== false;
|
|
4407
|
+
if (notify) this.sendStoppingNotice(options.reason || 'local_stop');
|
|
4139
4408
|
this.closed = true;
|
|
4140
4409
|
for (const session of this.sessions.values()) session.stop();
|
|
4141
4410
|
this.sessions.clear();
|
|
4142
4411
|
this.stopHeartbeat();
|
|
4143
4412
|
this.clearInboundWatchdog();
|
|
4413
|
+
this.clearRuntimeStatusTimer();
|
|
4144
4414
|
if (this.agentStartPumpTimer) {
|
|
4145
4415
|
clearTimeout(this.agentStartPumpTimer);
|
|
4146
4416
|
this.agentStartPumpTimer = null;
|
|
@@ -4155,7 +4425,10 @@ class MagClawDaemon {
|
|
|
4155
4425
|
this.request.destroy(new Error('MagClaw daemon is shutting down.'));
|
|
4156
4426
|
this.request = null;
|
|
4157
4427
|
}
|
|
4158
|
-
if (this.socket && !this.socket.destroyed)
|
|
4428
|
+
if (this.socket && !this.socket.destroyed) {
|
|
4429
|
+
if (notify) this.socket.end();
|
|
4430
|
+
else this.socket.destroy();
|
|
4431
|
+
}
|
|
4159
4432
|
this.socket = null;
|
|
4160
4433
|
}
|
|
4161
4434
|
|
|
@@ -4190,6 +4463,7 @@ class MagClawDaemon {
|
|
|
4190
4463
|
settled = true;
|
|
4191
4464
|
this.stopHeartbeat();
|
|
4192
4465
|
this.clearInboundWatchdog();
|
|
4466
|
+
this.clearRuntimeStatusTimer();
|
|
4193
4467
|
if (this.socket && this.socket.destroyed) this.socket = null;
|
|
4194
4468
|
if (this.request === req) this.request = null;
|
|
4195
4469
|
callback(value);
|
|
@@ -4413,6 +4687,7 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4413
4687
|
" MAGCLAW_DAEMON_PACKAGE_SPEC: packageSpec,",
|
|
4414
4688
|
" MAGCLAW_DAEMON_PACKAGE_KIND: packageKind,",
|
|
4415
4689
|
" MAGCLAW_DAEMON_PACKAGE_BIN: packageBin,",
|
|
4690
|
+
" MAGCLAW_DAEMON_BACKGROUND_SERVICE: '1',",
|
|
4416
4691
|
" PATH: launchPath,",
|
|
4417
4692
|
"};",
|
|
4418
4693
|
"if (packageKind === 'computer') childEnv.MAGCLAW_COMPUTER_DAEMON = '1';",
|
|
@@ -4567,17 +4842,40 @@ async function startBackground(profile, env = process.env) {
|
|
|
4567
4842
|
return { ok: false, mode: 'foreground', message: 'Background daemon is only automated on macOS launchd, Linux user systemd, and Windows schtasks.' };
|
|
4568
4843
|
}
|
|
4569
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
|
+
|
|
4570
4864
|
function backgroundServiceStatus(profile, env = process.env) {
|
|
4571
4865
|
const paths = profilePaths(profile, env);
|
|
4572
4866
|
if (process.platform === 'darwin') {
|
|
4573
4867
|
const label = launchAgentLabel(paths.profile);
|
|
4574
4868
|
const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
|
|
4869
|
+
const parsed = parseLaunchdPrintStatus(result);
|
|
4575
4870
|
return {
|
|
4576
4871
|
mode: 'launchd',
|
|
4577
|
-
active:
|
|
4872
|
+
active: parsed.active,
|
|
4578
4873
|
label,
|
|
4579
4874
|
file: path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`),
|
|
4580
|
-
|
|
4875
|
+
status: parsed.status,
|
|
4876
|
+
state: parsed.state,
|
|
4877
|
+
activeCount: parsed.activeCount,
|
|
4878
|
+
error: parsed.error,
|
|
4581
4879
|
};
|
|
4582
4880
|
}
|
|
4583
4881
|
if (process.platform === 'linux') {
|
|
@@ -4787,14 +5085,15 @@ async function status(profile) {
|
|
|
4787
5085
|
};
|
|
4788
5086
|
}
|
|
4789
5087
|
|
|
4790
|
-
async function logs(profile) {
|
|
5088
|
+
async function logs(profile, options = {}) {
|
|
4791
5089
|
const paths = profilePaths(profile);
|
|
5090
|
+
const lines = Math.max(1, Math.min(5000, Number(options.lines || 120) || 120));
|
|
4792
5091
|
const files = [path.join(paths.logDir, 'daemon.log'), path.join(paths.logDir, 'daemon.err.log')];
|
|
4793
5092
|
for (const file of files) {
|
|
4794
5093
|
if (!existsSync(file)) continue;
|
|
4795
5094
|
process.stdout.write(`\n==> ${file} <==\n`);
|
|
4796
5095
|
const text = await readFile(file, 'utf8').catch(() => '');
|
|
4797
|
-
process.stdout.write(text.split(/\r?\n/).slice(-
|
|
5096
|
+
process.stdout.write(text.split(/\r?\n/).slice(-lines).join('\n'));
|
|
4798
5097
|
process.stdout.write('\n');
|
|
4799
5098
|
}
|
|
4800
5099
|
}
|
|
@@ -4833,6 +5132,9 @@ async function listProfiles(env = process.env) {
|
|
|
4833
5132
|
label: service.label || '',
|
|
4834
5133
|
serviceName: service.serviceName || '',
|
|
4835
5134
|
taskName: service.taskName || '',
|
|
5135
|
+
status: service.status || '',
|
|
5136
|
+
state: service.state || '',
|
|
5137
|
+
activeCount: service.activeCount ?? null,
|
|
4836
5138
|
},
|
|
4837
5139
|
createdAt: config.createdAt || '',
|
|
4838
5140
|
updatedAt: config.updatedAt || '',
|
|
@@ -5279,10 +5581,22 @@ async function postSetupJson(serverUrl, pathname, body = {}) {
|
|
|
5279
5581
|
return data;
|
|
5280
5582
|
}
|
|
5281
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
|
+
|
|
5282
5596
|
async function runComputerSetup(flags, env = process.env) {
|
|
5283
5597
|
const subcommand = String(flags._?.[0] || '').trim();
|
|
5284
|
-
if (
|
|
5285
|
-
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>');
|
|
5286
5600
|
}
|
|
5287
5601
|
const serverSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
|
|
5288
5602
|
if (!serverSlug) throw new Error('Run computer setup with a server slug, for example: magclaw computer setup /my-server');
|
|
@@ -5343,6 +5657,20 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5343
5657
|
await saveProfile(config.profile, config, env);
|
|
5344
5658
|
await clearRemoteClosedServiceState(config.profile, env);
|
|
5345
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
|
+
}
|
|
5346
5674
|
const result = await startBackground(config.profile, env);
|
|
5347
5675
|
printJson({
|
|
5348
5676
|
...result,
|
|
@@ -5359,6 +5687,255 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5359
5687
|
}
|
|
5360
5688
|
}
|
|
5361
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
|
+
|
|
5362
5939
|
async function buildConfig(flags, env = process.env) {
|
|
5363
5940
|
const diskConfig = await readProfile(flags.profile, env);
|
|
5364
5941
|
const profile = flags.profile || diskConfig.profile || DEFAULT_PROFILE;
|
|
@@ -5381,12 +5958,12 @@ async function buildConfig(flags, env = process.env) {
|
|
|
5381
5958
|
|
|
5382
5959
|
async function runForegroundDaemon(config, env = process.env) {
|
|
5383
5960
|
const releaseLock = await acquireDaemonLock(config.profile, config, env);
|
|
5384
|
-
await
|
|
5961
|
+
await markDaemonRunServiceState(config.profile, env);
|
|
5385
5962
|
const daemon = new MagClawDaemon(config, env);
|
|
5386
5963
|
let forceExitTimer = null;
|
|
5387
5964
|
const shutdown = (signal) => {
|
|
5388
5965
|
process.exitCode = signal === 'SIGINT' ? 130 : 143;
|
|
5389
|
-
daemon.close();
|
|
5966
|
+
daemon.close({ reason: signal === 'SIGINT' ? 'sigint' : 'sigterm' });
|
|
5390
5967
|
forceExitTimer ||= setTimeout(() => process.exit(process.exitCode || 1), 5000);
|
|
5391
5968
|
forceExitTimer.unref?.();
|
|
5392
5969
|
};
|
|
@@ -5458,6 +6035,14 @@ function requireExplicitProfile(command, flags = {}) {
|
|
|
5458
6035
|
|
|
5459
6036
|
export async function main(argv = process.argv, env = process.env) {
|
|
5460
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
|
+
}
|
|
5461
6046
|
if (command === 'help' || flags.help) {
|
|
5462
6047
|
process.stdout.write(renderHelp());
|
|
5463
6048
|
return;
|
|
@@ -5467,7 +6052,7 @@ export async function main(argv = process.argv, env = process.env) {
|
|
|
5467
6052
|
await runConnect(flags, env);
|
|
5468
6053
|
break;
|
|
5469
6054
|
case 'computer':
|
|
5470
|
-
await
|
|
6055
|
+
await runComputerCommand(flags, env);
|
|
5471
6056
|
break;
|
|
5472
6057
|
case 'start': {
|
|
5473
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 = {} } = {}) {
|