@magclaw/cli-core 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/cli.js +780 -50
- 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;
|
|
@@ -580,7 +588,7 @@ function renderHelp() {
|
|
|
580
588
|
' status Show daemon status for one profile',
|
|
581
589
|
' list List local daemon profiles and connected Computers',
|
|
582
590
|
' logs Print recent daemon logs for one profile',
|
|
583
|
-
' install-cli Install or repair
|
|
591
|
+
' install-cli Install or repair durable magclaw command shims',
|
|
584
592
|
' upgrade Upgrade the background daemon package',
|
|
585
593
|
' doctor Show runtime and environment diagnostics',
|
|
586
594
|
' uninstall Stop and remove the background daemon service',
|
|
@@ -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);
|
|
@@ -921,64 +1110,88 @@ function defaultCliPackageSpec(env = process.env) {
|
|
|
921
1110
|
return String(env.MAGCLAW_CLI_PACKAGE_SPEC || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest';
|
|
922
1111
|
}
|
|
923
1112
|
|
|
1113
|
+
function defaultComputerCliPackageSpec(env = process.env) {
|
|
1114
|
+
return String(env.MAGCLAW_COMPUTER_CLI_PACKAGE_SPEC || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest';
|
|
1115
|
+
}
|
|
1116
|
+
|
|
924
1117
|
function defaultCliNpmPath(env = process.env) {
|
|
925
1118
|
return commandExists('npm', env) || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
926
1119
|
}
|
|
927
1120
|
|
|
928
|
-
|
|
1121
|
+
function cliShimTargets({ packageSpec = '@magclaw/cli-core@latest', computerPackageSpec = '@magclaw/computer@latest' } = {}) {
|
|
1122
|
+
return [
|
|
1123
|
+
{
|
|
1124
|
+
command: 'magclaw',
|
|
1125
|
+
packageSpec: String(packageSpec || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest',
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
command: 'magclaw-computer',
|
|
1129
|
+
packageSpec: String(computerPackageSpec || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest',
|
|
1130
|
+
},
|
|
1131
|
+
];
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
export function renderCliShimFiles({
|
|
1135
|
+
platform = process.platform,
|
|
1136
|
+
npmPath = '',
|
|
1137
|
+
packageSpec = '@magclaw/cli-core@latest',
|
|
1138
|
+
computerPackageSpec = '@magclaw/computer@latest',
|
|
1139
|
+
} = {}) {
|
|
929
1140
|
const targetPackage = String(packageSpec || '@magclaw/cli-core@latest').trim() || '@magclaw/cli-core@latest';
|
|
1141
|
+
const targetComputerPackage = String(computerPackageSpec || '@magclaw/computer@latest').trim() || '@magclaw/computer@latest';
|
|
930
1142
|
const targetNpm = String(npmPath || (platform === 'win32' ? 'npm.cmd' : 'npm')).trim() || (platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
1143
|
+
const targets = cliShimTargets({ packageSpec: targetPackage, computerPackageSpec: targetComputerPackage });
|
|
931
1144
|
if (platform === 'win32') {
|
|
932
1145
|
const fallback = path.win32.basename(targetNpm) || 'npm.cmd';
|
|
933
|
-
return [
|
|
1146
|
+
return targets.flatMap((target) => [
|
|
934
1147
|
{
|
|
935
|
-
name:
|
|
1148
|
+
name: `${target.command}.cmd`,
|
|
936
1149
|
executable: true,
|
|
937
1150
|
content: [
|
|
938
1151
|
'@echo off',
|
|
939
1152
|
'setlocal',
|
|
940
1153
|
`set "NPM_BIN=${cmdEnvValue(targetNpm)}"`,
|
|
941
1154
|
`if not exist "%NPM_BIN%" set "NPM_BIN=${cmdEnvValue(fallback)}"`,
|
|
942
|
-
`set "PACKAGE_SPEC=${cmdEnvValue(
|
|
1155
|
+
`set "PACKAGE_SPEC=${cmdEnvValue(target.packageSpec)}"`,
|
|
943
1156
|
'set "ARGS=%*"',
|
|
944
|
-
|
|
1157
|
+
`"%NPM_BIN%" exec --yes --package "%PACKAGE_SPEC%" -- ${target.command} %ARGS%`,
|
|
945
1158
|
'exit /b %ERRORLEVEL%',
|
|
946
1159
|
'',
|
|
947
1160
|
].join('\r\n'),
|
|
948
1161
|
},
|
|
949
1162
|
{
|
|
950
|
-
name:
|
|
1163
|
+
name: `${target.command}.ps1`,
|
|
951
1164
|
executable: true,
|
|
952
1165
|
content: [
|
|
953
1166
|
`$npmBin = ${psSingleQuote(targetNpm)}`,
|
|
954
1167
|
`if (-not (Test-Path -LiteralPath $npmBin)) { $npmBin = ${psSingleQuote(fallback)} }`,
|
|
955
|
-
`$packageSpec = ${psSingleQuote(
|
|
956
|
-
|
|
1168
|
+
`$packageSpec = ${psSingleQuote(target.packageSpec)}`,
|
|
1169
|
+
`& $npmBin exec --yes --package $packageSpec -- ${target.command} @args`,
|
|
957
1170
|
'exit $LASTEXITCODE',
|
|
958
1171
|
'',
|
|
959
1172
|
].join('\n'),
|
|
960
1173
|
},
|
|
961
|
-
];
|
|
1174
|
+
]);
|
|
962
1175
|
}
|
|
963
1176
|
const fallback = path.basename(targetNpm) || 'npm';
|
|
964
|
-
return
|
|
1177
|
+
return targets.map((target) => (
|
|
965
1178
|
{
|
|
966
|
-
name:
|
|
1179
|
+
name: target.command,
|
|
967
1180
|
executable: true,
|
|
968
1181
|
content: [
|
|
969
1182
|
'#!/bin/sh',
|
|
970
1183
|
'set -eu',
|
|
971
1184
|
'# MagClaw CLI shim generated by @magclaw/cli-core.',
|
|
972
1185
|
`NPM_BIN=${shSingleQuote(targetNpm)}`,
|
|
973
|
-
`PACKAGE_SPEC=${shSingleQuote(
|
|
1186
|
+
`PACKAGE_SPEC=${shSingleQuote(target.packageSpec)}`,
|
|
974
1187
|
'if [ ! -x "$NPM_BIN" ]; then',
|
|
975
1188
|
` NPM_BIN=${shSingleQuote(fallback)}`,
|
|
976
1189
|
'fi',
|
|
977
|
-
|
|
1190
|
+
`exec "$NPM_BIN" exec --yes --package "$PACKAGE_SPEC" -- ${target.command} "$@"`,
|
|
978
1191
|
'',
|
|
979
1192
|
].join('\n'),
|
|
980
|
-
}
|
|
981
|
-
|
|
1193
|
+
}
|
|
1194
|
+
));
|
|
982
1195
|
}
|
|
983
1196
|
|
|
984
1197
|
async function chooseCliShimBinDir(options = {}, env = process.env) {
|
|
@@ -1014,17 +1227,21 @@ async function chooseCliShimBinDir(options = {}, env = process.env) {
|
|
|
1014
1227
|
return { dir: fallback, explicit: false, pathReady: directoryIsInPath(fallback, env) };
|
|
1015
1228
|
}
|
|
1016
1229
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1230
|
+
function isGeneratedMagClawShim(content = '') {
|
|
1231
|
+
return Boolean(
|
|
1232
|
+
content.includes('MagClaw CLI shim generated by @magclaw/cli-core')
|
|
1233
|
+
|| content.includes('MagClaw CLI shim generated by @magclaw/daemon')
|
|
1234
|
+
|| content.includes('@magclaw/cli-core@')
|
|
1235
|
+
|| content.includes('@magclaw/daemon@')
|
|
1236
|
+
|| content.includes('@magclaw/computer@')
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function existingDurableMagclawCommand(command = 'magclaw', env = process.env) {
|
|
1241
|
+
const existing = commandExists(command, env);
|
|
1019
1242
|
if (!existing || pathLooksEphemeralCli(existing)) return '';
|
|
1020
1243
|
const content = await readFile(existing, 'utf8').catch(() => '');
|
|
1021
|
-
if (
|
|
1022
|
-
content
|
|
1023
|
-
&& !content.includes('MagClaw CLI shim generated by @magclaw/cli-core')
|
|
1024
|
-
&& !content.includes('MagClaw CLI shim generated by @magclaw/daemon')
|
|
1025
|
-
&& !content.includes('@magclaw/cli-core@')
|
|
1026
|
-
&& !content.includes('@magclaw/daemon@')
|
|
1027
|
-
) {
|
|
1244
|
+
if (content && !isGeneratedMagClawShim(content)) {
|
|
1028
1245
|
return '';
|
|
1029
1246
|
}
|
|
1030
1247
|
return existing;
|
|
@@ -1033,12 +1250,7 @@ async function existingDurableMagclawCommand(env = process.env) {
|
|
|
1033
1250
|
async function writeCliShimFile(file, content, { force = false } = {}) {
|
|
1034
1251
|
if (existsSync(file) && !force) {
|
|
1035
1252
|
const existing = await readFile(file, 'utf8').catch(() => '');
|
|
1036
|
-
if (
|
|
1037
|
-
!existing.includes('MagClaw CLI shim generated by @magclaw/cli-core')
|
|
1038
|
-
&& !existing.includes('MagClaw CLI shim generated by @magclaw/daemon')
|
|
1039
|
-
&& !existing.includes('@magclaw/cli-core@')
|
|
1040
|
-
&& !existing.includes('@magclaw/daemon@')
|
|
1041
|
-
) {
|
|
1253
|
+
if (!isGeneratedMagClawShim(existing)) {
|
|
1042
1254
|
const error = new Error(`Refusing to overwrite existing non-MagClaw command: ${file}`);
|
|
1043
1255
|
error.code = 'EEXIST';
|
|
1044
1256
|
throw error;
|
|
@@ -1052,16 +1264,39 @@ async function installCliShim(options = {}, env = process.env) {
|
|
|
1052
1264
|
if (env.MAGCLAW_INSTALL_CLI === '0' || options.installCli === false || options.noInstallCli) {
|
|
1053
1265
|
return { ok: true, command: 'magclaw', installed: false, skipped: true, reason: 'disabled' };
|
|
1054
1266
|
}
|
|
1055
|
-
const existing = await existingDurableMagclawCommand(env);
|
|
1056
|
-
|
|
1057
|
-
|
|
1267
|
+
const existing = await existingDurableMagclawCommand('magclaw', env);
|
|
1268
|
+
const existingComputer = await existingDurableMagclawCommand('magclaw-computer', env);
|
|
1269
|
+
const existingCommandsShareDir = Boolean(
|
|
1270
|
+
existing
|
|
1271
|
+
&& existingComputer
|
|
1272
|
+
&& path.dirname(existing) === path.dirname(existingComputer)
|
|
1273
|
+
);
|
|
1274
|
+
if (existingCommandsShareDir && !options.binDir && !options.cliBinDir && !options.force) {
|
|
1275
|
+
return {
|
|
1276
|
+
ok: true,
|
|
1277
|
+
command: 'magclaw',
|
|
1278
|
+
commands: ['magclaw', 'magclaw-computer'],
|
|
1279
|
+
installed: false,
|
|
1280
|
+
path: existing,
|
|
1281
|
+
files: [existing, existingComputer],
|
|
1282
|
+
pathReady: true,
|
|
1283
|
+
reason: 'already_available',
|
|
1284
|
+
};
|
|
1058
1285
|
}
|
|
1059
1286
|
|
|
1060
|
-
const target =
|
|
1287
|
+
const target = (existing || existingComputer) && !options.binDir && !options.cliBinDir && !options.force
|
|
1288
|
+
? { dir: path.dirname(existing || existingComputer), explicit: false, pathReady: true }
|
|
1289
|
+
: await chooseCliShimBinDir(options, env);
|
|
1061
1290
|
await mkdir(target.dir, { recursive: true });
|
|
1062
1291
|
const npmPath = String(options.npmPath || defaultCliNpmPath(env)).trim() || (process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
1063
1292
|
const packageSpec = String(options.packageSpec || options.cliPackageSpec || defaultCliPackageSpec(env)).trim() || '@magclaw/cli-core@latest';
|
|
1064
|
-
const
|
|
1293
|
+
const computerPackageSpec = String(options.computerPackageSpec || options.computerCliPackageSpec || defaultComputerCliPackageSpec(env)).trim() || '@magclaw/computer@latest';
|
|
1294
|
+
const shimFiles = renderCliShimFiles({
|
|
1295
|
+
platform: process.platform,
|
|
1296
|
+
npmPath,
|
|
1297
|
+
packageSpec,
|
|
1298
|
+
computerPackageSpec,
|
|
1299
|
+
});
|
|
1065
1300
|
const written = [];
|
|
1066
1301
|
for (const shim of shimFiles) {
|
|
1067
1302
|
const file = path.join(target.dir, shim.name);
|
|
@@ -1071,12 +1306,14 @@ async function installCliShim(options = {}, env = process.env) {
|
|
|
1071
1306
|
return {
|
|
1072
1307
|
ok: true,
|
|
1073
1308
|
command: 'magclaw',
|
|
1309
|
+
commands: ['magclaw', 'magclaw-computer'],
|
|
1074
1310
|
installed: true,
|
|
1075
1311
|
binDir: target.dir,
|
|
1076
1312
|
files: written,
|
|
1077
1313
|
path: written[0] || '',
|
|
1078
1314
|
pathReady: Boolean(target.pathReady),
|
|
1079
1315
|
packageSpec,
|
|
1316
|
+
computerPackageSpec,
|
|
1080
1317
|
npmPath,
|
|
1081
1318
|
};
|
|
1082
1319
|
}
|
|
@@ -3249,6 +3486,8 @@ class MagClawDaemon {
|
|
|
3249
3486
|
this.pendingUpgradeRequest = null;
|
|
3250
3487
|
this.upgradeIdleTimer = null;
|
|
3251
3488
|
this.upgradeWorkerStarting = false;
|
|
3489
|
+
this.runtimeStatusTimer = null;
|
|
3490
|
+
this.runtimeStatusInFlight = false;
|
|
3252
3491
|
}
|
|
3253
3492
|
|
|
3254
3493
|
send(payload) {
|
|
@@ -3619,7 +3858,6 @@ class MagClawDaemon {
|
|
|
3619
3858
|
}
|
|
3620
3859
|
|
|
3621
3860
|
async readyPayload() {
|
|
3622
|
-
const runtimes = await detectRuntimes(this.env);
|
|
3623
3861
|
const owner = await ensureMachineFingerprint(this.paths.profile, this.env);
|
|
3624
3862
|
const service = await readServiceState(this.paths.profile, this.env);
|
|
3625
3863
|
const serviceStatus = backgroundServiceStatus(this.paths.profile, this.env);
|
|
@@ -3658,8 +3896,7 @@ class MagClawDaemon {
|
|
|
3658
3896
|
cliCoreVersion: CLI_CORE_VERSION,
|
|
3659
3897
|
},
|
|
3660
3898
|
upgrade: upgrade || null,
|
|
3661
|
-
|
|
3662
|
-
runtimeDetails: runtimes,
|
|
3899
|
+
runtimeScanPending: true,
|
|
3663
3900
|
runningAgents: [...this.sessions.keys()],
|
|
3664
3901
|
capabilities: CAPABILITIES,
|
|
3665
3902
|
};
|
|
@@ -3670,10 +3907,68 @@ class MagClawDaemon {
|
|
|
3670
3907
|
const sent = this.send(payload);
|
|
3671
3908
|
logInfo(
|
|
3672
3909
|
'daemon',
|
|
3673
|
-
`Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes
|
|
3910
|
+
`Sent ready payload for computer ${payload.computerId || 'unpaired'} (runtimes=deferred, runningAgents=${payload.runningAgents.length}, sent=${sent}).`,
|
|
3674
3911
|
);
|
|
3675
3912
|
}
|
|
3676
3913
|
|
|
3914
|
+
runtimeStatusDelayMs() {
|
|
3915
|
+
return envInteger(this.env, 'MAGCLAW_DAEMON_RUNTIME_STATUS_DELAY_MS', 1000, { min: 0, max: 60_000 });
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
clearRuntimeStatusTimer() {
|
|
3919
|
+
if (!this.runtimeStatusTimer) return;
|
|
3920
|
+
clearTimeout(this.runtimeStatusTimer);
|
|
3921
|
+
this.runtimeStatusTimer = null;
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
scheduleRuntimeStatus(reason = 'ready_ack') {
|
|
3925
|
+
if (this.closed || this.runtimeStatusTimer || this.runtimeStatusInFlight) return;
|
|
3926
|
+
this.runtimeStatusTimer = setTimeout(() => {
|
|
3927
|
+
this.runtimeStatusTimer = null;
|
|
3928
|
+
this.sendRuntimeStatus(reason).catch((error) => {
|
|
3929
|
+
logWarning('daemon', `Failed to send runtime status: ${error.message}`);
|
|
3930
|
+
});
|
|
3931
|
+
}, this.runtimeStatusDelayMs());
|
|
3932
|
+
this.runtimeStatusTimer.unref?.();
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
async runtimeStatusPayload(reason = 'ready_ack') {
|
|
3936
|
+
const runtimes = await detectRuntimes(this.env);
|
|
3937
|
+
const packageInfo = runtimePackageInfo(this.env);
|
|
3938
|
+
return {
|
|
3939
|
+
type: 'daemon:runtime_status',
|
|
3940
|
+
time: now(),
|
|
3941
|
+
reason,
|
|
3942
|
+
computerId: this.config.computerId || null,
|
|
3943
|
+
daemonVersion: packageInfo.version || DAEMON_VERSION,
|
|
3944
|
+
packageName: packageInfo.name,
|
|
3945
|
+
packageVersion: packageInfo.version,
|
|
3946
|
+
packageKind: packageInfo.kind,
|
|
3947
|
+
packageSpec: packageInfo.spec,
|
|
3948
|
+
packageBin: packageInfo.bin,
|
|
3949
|
+
cliCoreVersion: CLI_CORE_VERSION,
|
|
3950
|
+
runtimes: runtimes.filter((runtime) => runtime.installed).map((runtime) => runtime.id),
|
|
3951
|
+
runtimeDetails: runtimes,
|
|
3952
|
+
runningAgents: [...this.sessions.keys()],
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
async sendRuntimeStatus(reason = 'ready_ack') {
|
|
3957
|
+
if (this.closed || this.runtimeStatusInFlight) return false;
|
|
3958
|
+
this.runtimeStatusInFlight = true;
|
|
3959
|
+
try {
|
|
3960
|
+
const payload = await this.runtimeStatusPayload(reason);
|
|
3961
|
+
const sent = this.send(payload);
|
|
3962
|
+
logInfo(
|
|
3963
|
+
'daemon',
|
|
3964
|
+
`Sent runtime status for computer ${payload.computerId || 'unpaired'} (runtimes=${payload.runtimes.join(', ') || 'none'}, sent=${sent}).`,
|
|
3965
|
+
);
|
|
3966
|
+
return sent;
|
|
3967
|
+
} finally {
|
|
3968
|
+
this.runtimeStatusInFlight = false;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3677
3972
|
sendHeartbeat() {
|
|
3678
3973
|
const packageInfo = runtimePackageInfo(this.env);
|
|
3679
3974
|
const sent = this.send({
|
|
@@ -3801,6 +4096,7 @@ class MagClawDaemon {
|
|
|
3801
4096
|
break;
|
|
3802
4097
|
case 'ready:ack':
|
|
3803
4098
|
logInfo('daemon', `MagClaw daemon ready for computer ${message.computerId || this.config.computerId}.`);
|
|
4099
|
+
this.scheduleRuntimeStatus('ready_ack');
|
|
3804
4100
|
break;
|
|
3805
4101
|
case 'ping':
|
|
3806
4102
|
this.send({ type: 'pong', time: now() });
|
|
@@ -4162,6 +4458,7 @@ class MagClawDaemon {
|
|
|
4162
4458
|
this.sessions.clear();
|
|
4163
4459
|
this.stopHeartbeat();
|
|
4164
4460
|
this.clearInboundWatchdog();
|
|
4461
|
+
this.clearRuntimeStatusTimer();
|
|
4165
4462
|
if (this.agentStartPumpTimer) {
|
|
4166
4463
|
clearTimeout(this.agentStartPumpTimer);
|
|
4167
4464
|
this.agentStartPumpTimer = null;
|
|
@@ -4214,6 +4511,7 @@ class MagClawDaemon {
|
|
|
4214
4511
|
settled = true;
|
|
4215
4512
|
this.stopHeartbeat();
|
|
4216
4513
|
this.clearInboundWatchdog();
|
|
4514
|
+
this.clearRuntimeStatusTimer();
|
|
4217
4515
|
if (this.socket && this.socket.destroyed) this.socket = null;
|
|
4218
4516
|
if (this.request === req) this.request = null;
|
|
4219
4517
|
callback(value);
|
|
@@ -4437,6 +4735,7 @@ async function writeLauncher(profile, env = process.env) {
|
|
|
4437
4735
|
" MAGCLAW_DAEMON_PACKAGE_SPEC: packageSpec,",
|
|
4438
4736
|
" MAGCLAW_DAEMON_PACKAGE_KIND: packageKind,",
|
|
4439
4737
|
" MAGCLAW_DAEMON_PACKAGE_BIN: packageBin,",
|
|
4738
|
+
" MAGCLAW_DAEMON_BACKGROUND_SERVICE: '1',",
|
|
4440
4739
|
" PATH: launchPath,",
|
|
4441
4740
|
"};",
|
|
4442
4741
|
"if (packageKind === 'computer') childEnv.MAGCLAW_COMPUTER_DAEMON = '1';",
|
|
@@ -4591,17 +4890,40 @@ async function startBackground(profile, env = process.env) {
|
|
|
4591
4890
|
return { ok: false, mode: 'foreground', message: 'Background daemon is only automated on macOS launchd, Linux user systemd, and Windows schtasks.' };
|
|
4592
4891
|
}
|
|
4593
4892
|
|
|
4893
|
+
export function parseLaunchdPrintStatus(result = {}) {
|
|
4894
|
+
const stdout = String(result.stdout || '');
|
|
4895
|
+
const stderr = String(result.stderr || '');
|
|
4896
|
+
const state = (stdout.match(/^\s*state\s*=\s*(.+?)\s*$/m)?.[1] || '').trim();
|
|
4897
|
+
const activeCountValue = Number(stdout.match(/^\s*active count\s*=\s*(\d+)\s*$/m)?.[1] || NaN);
|
|
4898
|
+
const stateStatus = state || (result.status === 0 ? 'loaded' : 'inactive');
|
|
4899
|
+
const active = result.status === 0 && (
|
|
4900
|
+
stateStatus.toLowerCase() === 'running'
|
|
4901
|
+
|| (!state && Number.isFinite(activeCountValue) && activeCountValue > 0)
|
|
4902
|
+
);
|
|
4903
|
+
return {
|
|
4904
|
+
active,
|
|
4905
|
+
status: active ? 'running' : stateStatus,
|
|
4906
|
+
state,
|
|
4907
|
+
activeCount: Number.isFinite(activeCountValue) ? activeCountValue : null,
|
|
4908
|
+
error: result.status === 0 ? '' : String(stderr || stdout || '').trim(),
|
|
4909
|
+
};
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4594
4912
|
function backgroundServiceStatus(profile, env = process.env) {
|
|
4595
4913
|
const paths = profilePaths(profile, env);
|
|
4596
4914
|
if (process.platform === 'darwin') {
|
|
4597
4915
|
const label = launchAgentLabel(paths.profile);
|
|
4598
4916
|
const result = spawnSync('launchctl', ['print', `gui/${process.getuid()}/${label}`], { encoding: 'utf8' });
|
|
4917
|
+
const parsed = parseLaunchdPrintStatus(result);
|
|
4599
4918
|
return {
|
|
4600
4919
|
mode: 'launchd',
|
|
4601
|
-
active:
|
|
4920
|
+
active: parsed.active,
|
|
4602
4921
|
label,
|
|
4603
4922
|
file: path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`),
|
|
4604
|
-
|
|
4923
|
+
status: parsed.status,
|
|
4924
|
+
state: parsed.state,
|
|
4925
|
+
activeCount: parsed.activeCount,
|
|
4926
|
+
error: parsed.error,
|
|
4605
4927
|
};
|
|
4606
4928
|
}
|
|
4607
4929
|
if (process.platform === 'linux') {
|
|
@@ -4811,14 +5133,15 @@ async function status(profile) {
|
|
|
4811
5133
|
};
|
|
4812
5134
|
}
|
|
4813
5135
|
|
|
4814
|
-
async function logs(profile) {
|
|
5136
|
+
async function logs(profile, options = {}) {
|
|
4815
5137
|
const paths = profilePaths(profile);
|
|
5138
|
+
const lines = Math.max(1, Math.min(5000, Number(options.lines || 120) || 120));
|
|
4816
5139
|
const files = [path.join(paths.logDir, 'daemon.log'), path.join(paths.logDir, 'daemon.err.log')];
|
|
4817
5140
|
for (const file of files) {
|
|
4818
5141
|
if (!existsSync(file)) continue;
|
|
4819
5142
|
process.stdout.write(`\n==> ${file} <==\n`);
|
|
4820
5143
|
const text = await readFile(file, 'utf8').catch(() => '');
|
|
4821
|
-
process.stdout.write(text.split(/\r?\n/).slice(-
|
|
5144
|
+
process.stdout.write(text.split(/\r?\n/).slice(-lines).join('\n'));
|
|
4822
5145
|
process.stdout.write('\n');
|
|
4823
5146
|
}
|
|
4824
5147
|
}
|
|
@@ -4857,6 +5180,9 @@ async function listProfiles(env = process.env) {
|
|
|
4857
5180
|
label: service.label || '',
|
|
4858
5181
|
serviceName: service.serviceName || '',
|
|
4859
5182
|
taskName: service.taskName || '',
|
|
5183
|
+
status: service.status || '',
|
|
5184
|
+
state: service.state || '',
|
|
5185
|
+
activeCount: service.activeCount ?? null,
|
|
4860
5186
|
},
|
|
4861
5187
|
createdAt: config.createdAt || '',
|
|
4862
5188
|
updatedAt: config.updatedAt || '',
|
|
@@ -5289,8 +5615,12 @@ function normalizeSetupServerSlug(value = '') {
|
|
|
5289
5615
|
return String(value || '').trim().replace(/^\/+/, '').replace(/\/+$/, '');
|
|
5290
5616
|
}
|
|
5291
5617
|
|
|
5618
|
+
function normalizeSetupServerUrl(value = '') {
|
|
5619
|
+
return String(value || DEFAULT_SERVER_URL).replace(/\/+$/, '');
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5292
5622
|
async function postSetupJson(serverUrl, pathname, body = {}) {
|
|
5293
|
-
const url = `${
|
|
5623
|
+
const url = `${normalizeSetupServerUrl(serverUrl)}${pathname}`;
|
|
5294
5624
|
const response = await fetch(url, {
|
|
5295
5625
|
method: 'POST',
|
|
5296
5626
|
headers: { 'content-type': 'application/json' },
|
|
@@ -5303,15 +5633,144 @@ async function postSetupJson(serverUrl, pathname, body = {}) {
|
|
|
5303
5633
|
return data;
|
|
5304
5634
|
}
|
|
5305
5635
|
|
|
5636
|
+
function hasComputerTarget(flags = {}) {
|
|
5637
|
+
return Boolean(flags.profileExplicit || flags.server || flags.serverSlug || flags.slug || flags._?.[1]);
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5640
|
+
function profileFromComputerTarget(value = '') {
|
|
5641
|
+
return safeProfileName(normalizeSetupServerSlug(value) || value || DEFAULT_PROFILE);
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
function computerTargetProfile(flags = {}, fallback = DEFAULT_PROFILE) {
|
|
5645
|
+
return profileFromComputerTarget(flags.server || flags.serverSlug || flags.slug || flags._?.[1] || flags.profile || fallback);
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
function savedComputerSetupMatches(config = {}, target = {}) {
|
|
5649
|
+
const token = String(config.token || config.machineToken || config.apiKey || '').trim();
|
|
5650
|
+
const computerId = String(config.computerId || '').trim();
|
|
5651
|
+
if (!token || !computerId) return false;
|
|
5652
|
+
if (normalizeSetupServerUrl(config.serverUrl) !== normalizeSetupServerUrl(target.serverUrl)) return false;
|
|
5653
|
+
|
|
5654
|
+
const targetProfile = safeProfileName(target.profile || target.serverSlug || DEFAULT_PROFILE);
|
|
5655
|
+
const configProfile = safeProfileName(config.profile || targetProfile);
|
|
5656
|
+
if (configProfile !== targetProfile) return false;
|
|
5657
|
+
|
|
5658
|
+
const targetSlug = normalizeSetupServerSlug(target.serverSlug);
|
|
5659
|
+
const configSlug = normalizeSetupServerSlug(config.serverSlug || config.slug || '');
|
|
5660
|
+
if (configSlug && targetSlug && configSlug !== targetSlug) return false;
|
|
5661
|
+
return true;
|
|
5662
|
+
}
|
|
5663
|
+
|
|
5664
|
+
async function reusableComputerSetupProfile(target = {}, env = process.env) {
|
|
5665
|
+
if (target.force || target.relogin || target.reauthorize) return null;
|
|
5666
|
+
const profile = safeProfileName(target.profile || target.serverSlug || DEFAULT_PROFILE);
|
|
5667
|
+
const config = await readProfile(profile, env);
|
|
5668
|
+
if (!savedComputerSetupMatches(config, { ...target, profile })) return null;
|
|
5669
|
+
const service = await readServiceState(profile, env);
|
|
5670
|
+
if (service.remoteClosed) return null;
|
|
5671
|
+
return {
|
|
5672
|
+
config: {
|
|
5673
|
+
...config,
|
|
5674
|
+
profile,
|
|
5675
|
+
serverUrl: normalizeSetupServerUrl(config.serverUrl || target.serverUrl),
|
|
5676
|
+
token: String(config.token || config.machineToken || config.apiKey || '').trim(),
|
|
5677
|
+
pairToken: '',
|
|
5678
|
+
},
|
|
5679
|
+
service,
|
|
5680
|
+
serviceStatus: backgroundServiceStatus(profile, env),
|
|
5681
|
+
};
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
async function finishReusableComputerSetup(existing, flags = {}, env = process.env) {
|
|
5685
|
+
const requestedSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
|
|
5686
|
+
const serverSlug = existing.config.serverSlug || requestedSlug;
|
|
5687
|
+
const config = await buildConfig({
|
|
5688
|
+
...flags,
|
|
5689
|
+
profile: existing.config.profile,
|
|
5690
|
+
serverUrl: existing.config.serverUrl,
|
|
5691
|
+
apiKey: existing.config.token,
|
|
5692
|
+
computerId: existing.config.computerId,
|
|
5693
|
+
workspaceId: existing.config.workspaceId || existing.config.workspace,
|
|
5694
|
+
name: existing.config.name,
|
|
5695
|
+
serverName: existing.config.serverName || serverSlug,
|
|
5696
|
+
serverSlug,
|
|
5697
|
+
fingerprint: existing.config.fingerprint,
|
|
5698
|
+
}, env);
|
|
5699
|
+
await saveProfile(config.profile, config, env);
|
|
5700
|
+
const cli = await tryInstallCliShim(flags, env);
|
|
5701
|
+
const computerName = config.computerName || config.name || os.hostname();
|
|
5702
|
+
const basePayload = {
|
|
5703
|
+
cli,
|
|
5704
|
+
computerId: config.computerId,
|
|
5705
|
+
computerName,
|
|
5706
|
+
profile: config.profile,
|
|
5707
|
+
serverName: config.serverName,
|
|
5708
|
+
serverSlug: config.serverSlug,
|
|
5709
|
+
reused: true,
|
|
5710
|
+
reason: 'already_configured',
|
|
5711
|
+
};
|
|
5712
|
+
if (flags.noStart || flags.noRun) {
|
|
5713
|
+
printJson({
|
|
5714
|
+
ok: true,
|
|
5715
|
+
started: false,
|
|
5716
|
+
...basePayload,
|
|
5717
|
+
next: `Run magclaw-computer start ${config.profile} when ready.`,
|
|
5718
|
+
});
|
|
5719
|
+
return;
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
const serviceStatus = existing.serviceStatus || backgroundServiceStatus(config.profile, env);
|
|
5723
|
+
let result;
|
|
5724
|
+
if (serviceStatus.active) {
|
|
5725
|
+
result = {
|
|
5726
|
+
ok: true,
|
|
5727
|
+
mode: serviceStatus.mode,
|
|
5728
|
+
active: true,
|
|
5729
|
+
alreadyRunning: true,
|
|
5730
|
+
started: false,
|
|
5731
|
+
label: serviceStatus.label,
|
|
5732
|
+
serviceName: serviceStatus.serviceName,
|
|
5733
|
+
taskName: serviceStatus.taskName,
|
|
5734
|
+
file: serviceStatus.file,
|
|
5735
|
+
status: serviceStatus.status,
|
|
5736
|
+
state: serviceStatus.state,
|
|
5737
|
+
};
|
|
5738
|
+
} else {
|
|
5739
|
+
const started = await startBackground(config.profile, env);
|
|
5740
|
+
result = {
|
|
5741
|
+
...started,
|
|
5742
|
+
started: Boolean(started.ok),
|
|
5743
|
+
};
|
|
5744
|
+
}
|
|
5745
|
+
printJson({
|
|
5746
|
+
...result,
|
|
5747
|
+
...basePayload,
|
|
5748
|
+
});
|
|
5749
|
+
if (!result.ok) {
|
|
5750
|
+
logWarning('daemon', 'Falling back to foreground mode.');
|
|
5751
|
+
await runForegroundDaemon(config, env);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5306
5755
|
async function runComputerSetup(flags, env = process.env) {
|
|
5307
5756
|
const subcommand = String(flags._?.[0] || '').trim();
|
|
5308
|
-
if (
|
|
5309
|
-
throw new Error('Usage: magclaw
|
|
5757
|
+
if (!['setup', 'attach', 'login'].includes(subcommand)) {
|
|
5758
|
+
throw new Error('Usage: magclaw-computer setup /<server-slug> --server-url <url>');
|
|
5310
5759
|
}
|
|
5311
5760
|
const serverSlug = normalizeSetupServerSlug(flags._?.[1] || flags.server || flags.serverSlug || flags.slug);
|
|
5312
5761
|
if (!serverSlug) throw new Error('Run computer setup with a server slug, for example: magclaw computer setup /my-server');
|
|
5313
|
-
const serverUrl =
|
|
5762
|
+
const serverUrl = normalizeSetupServerUrl(flags.serverUrl || env.MAGCLAW_PUBLIC_URL || DEFAULT_SERVER_URL);
|
|
5314
5763
|
const profile = safeProfileName(flags.profile && flags.profile !== DEFAULT_PROFILE ? flags.profile : serverSlug);
|
|
5764
|
+
const existing = await reusableComputerSetupProfile({
|
|
5765
|
+
...flags,
|
|
5766
|
+
profile,
|
|
5767
|
+
serverSlug,
|
|
5768
|
+
serverUrl,
|
|
5769
|
+
}, env);
|
|
5770
|
+
if (existing) {
|
|
5771
|
+
await finishReusableComputerSetup(existing, flags, env);
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5315
5774
|
const owner = await ensureMachineFingerprint(profile, env);
|
|
5316
5775
|
const displayName = String(flags.displayName || flags.name || os.hostname()).trim();
|
|
5317
5776
|
const packageInfo = runtimePackageInfo(env);
|
|
@@ -5367,6 +5826,20 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5367
5826
|
await saveProfile(config.profile, config, env);
|
|
5368
5827
|
await clearRemoteClosedServiceState(config.profile, env);
|
|
5369
5828
|
const cli = await tryInstallCliShim(flags, env);
|
|
5829
|
+
if (flags.noStart || flags.noRun) {
|
|
5830
|
+
printJson({
|
|
5831
|
+
ok: true,
|
|
5832
|
+
started: false,
|
|
5833
|
+
cli,
|
|
5834
|
+
computerId: config.computerId,
|
|
5835
|
+
computerName: config.computerName || config.name || displayName,
|
|
5836
|
+
profile: config.profile,
|
|
5837
|
+
serverName: config.serverName,
|
|
5838
|
+
serverSlug: approved.serverSlug || serverSlug,
|
|
5839
|
+
next: `Run magclaw-computer start ${config.profile} when ready.`,
|
|
5840
|
+
});
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5370
5843
|
const result = await startBackground(config.profile, env);
|
|
5371
5844
|
printJson({
|
|
5372
5845
|
...result,
|
|
@@ -5383,6 +5856,255 @@ async function runComputerSetup(flags, env = process.env) {
|
|
|
5383
5856
|
}
|
|
5384
5857
|
}
|
|
5385
5858
|
|
|
5859
|
+
async function renderComputerAggregateStatus(env = process.env) {
|
|
5860
|
+
const report = await listProfiles(env);
|
|
5861
|
+
return {
|
|
5862
|
+
ok: true,
|
|
5863
|
+
root: report.root,
|
|
5864
|
+
loggedIn: report.profiles.some((profile) => profile.configured),
|
|
5865
|
+
supervisor: {
|
|
5866
|
+
model: 'per-profile-service',
|
|
5867
|
+
running: report.profiles.some((profile) => profile.running || profile.service?.active),
|
|
5868
|
+
managedProfiles: report.profiles.length,
|
|
5869
|
+
},
|
|
5870
|
+
profiles: report.profiles,
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
function formatComputerStatus(report = {}) {
|
|
5875
|
+
const profiles = report.profiles || [];
|
|
5876
|
+
if (report.profile) {
|
|
5877
|
+
return [
|
|
5878
|
+
`Profile: ${report.profile}`,
|
|
5879
|
+
`Configured: ${report.configured ? 'yes' : 'no'}`,
|
|
5880
|
+
`Daemon: ${report.running ? `running (pid ${report.pid})` : 'stopped'}`,
|
|
5881
|
+
`Service: ${report.service?.mode || 'foreground'}${report.service?.active ? ' active' : ''}`,
|
|
5882
|
+
`Server URL: ${report.serverUrl || '-'}`,
|
|
5883
|
+
`Computer ID: ${report.computerId || '-'}`,
|
|
5884
|
+
`Config: ${report.configPath}`,
|
|
5885
|
+
'',
|
|
5886
|
+
].join('\n');
|
|
5887
|
+
}
|
|
5888
|
+
return [
|
|
5889
|
+
'MagClaw Computers',
|
|
5890
|
+
`Profiles: ${profiles.length} Running: ${profiles.filter((profile) => profile.running || profile.service?.active).length}`,
|
|
5891
|
+
`Root: ${report.root || '-'}`,
|
|
5892
|
+
'',
|
|
5893
|
+
...profiles.map((profile) => [
|
|
5894
|
+
`${profile.running || profile.service?.active ? 'online ' : 'offline'} ${profile.profile}`,
|
|
5895
|
+
` server=${profile.serverSlug || profile.serverName || '-'} computer=${profile.computerId || '-'} service=${profile.service?.mode || 'foreground'}`,
|
|
5896
|
+
].join('\n')),
|
|
5897
|
+
profiles.length ? '' : 'No profiles. Run `magclaw-computer setup /<serverSlug>` first.',
|
|
5898
|
+
'',
|
|
5899
|
+
].join('\n');
|
|
5900
|
+
}
|
|
5901
|
+
|
|
5902
|
+
async function computerStatus(flags = {}, env = process.env) {
|
|
5903
|
+
if (hasComputerTarget(flags)) return status(computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE));
|
|
5904
|
+
return renderComputerAggregateStatus(env);
|
|
5905
|
+
}
|
|
5906
|
+
|
|
5907
|
+
async function startAllComputerProfiles(env = process.env) {
|
|
5908
|
+
const report = await listProfiles(env);
|
|
5909
|
+
const results = [];
|
|
5910
|
+
for (const profile of report.profiles) {
|
|
5911
|
+
if (!profile.configured) continue;
|
|
5912
|
+
results.push({ profile: profile.profile, ...(await startSavedBackground({ profile: profile.profile }, env)) });
|
|
5913
|
+
}
|
|
5914
|
+
return { ok: results.every((item) => item.ok), count: results.length, results };
|
|
5915
|
+
}
|
|
5916
|
+
|
|
5917
|
+
async function stopAllComputerProfiles(flags = {}, env = process.env) {
|
|
5918
|
+
const report = await listProfiles(env);
|
|
5919
|
+
const results = [];
|
|
5920
|
+
for (const profile of report.profiles) {
|
|
5921
|
+
results.push({ profile: profile.profile, ...(await stopDaemon(profile.profile, env, { disable: Boolean(flags.disable) })) });
|
|
5922
|
+
}
|
|
5923
|
+
return { ok: results.every((item) => item.ok), count: results.length, results };
|
|
5924
|
+
}
|
|
5925
|
+
|
|
5926
|
+
async function detachComputerProfile(flags = {}, env = process.env) {
|
|
5927
|
+
const profile = computerTargetProfile(flags);
|
|
5928
|
+
if (!profile || profile === DEFAULT_PROFILE && !hasComputerTarget(flags)) {
|
|
5929
|
+
throw new Error('Usage: magclaw-computer detach <serverSlug>');
|
|
5930
|
+
}
|
|
5931
|
+
const paths = profilePaths(profile, env);
|
|
5932
|
+
const stopped = await uninstallBackground(profile, env);
|
|
5933
|
+
await rm(paths.dir, { recursive: true, force: true });
|
|
5934
|
+
return { ok: true, profile, detached: true, stopped };
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
async function cleanupComputerResidue(env = process.env) {
|
|
5938
|
+
const report = await listProfiles(env);
|
|
5939
|
+
const cleaned = [];
|
|
5940
|
+
await activeComputerLock(env);
|
|
5941
|
+
for (const profile of report.profiles) {
|
|
5942
|
+
const paths = profilePaths(profile.profile, env);
|
|
5943
|
+
const before = existsSync(paths.lockFile);
|
|
5944
|
+
await activeDaemonLock(profile.profile, env);
|
|
5945
|
+
if (before && !existsSync(paths.lockFile)) cleaned.push(paths.lockFile);
|
|
5946
|
+
}
|
|
5947
|
+
return cleaned;
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5950
|
+
async function computerDoctor(flags = {}, env = process.env) {
|
|
5951
|
+
const cleanup = Boolean(flags.cleanup || flags.fix);
|
|
5952
|
+
const target = hasComputerTarget(flags) ? computerTargetProfile(flags, flags.profile || DEFAULT_PROFILE) : '';
|
|
5953
|
+
const runtime = await doctor(env);
|
|
5954
|
+
const aggregate = await renderComputerAggregateStatus(env);
|
|
5955
|
+
const selected = target ? await status(target) : null;
|
|
5956
|
+
const cleaned = cleanup ? await cleanupComputerResidue(env) : [];
|
|
5957
|
+
const checks = [
|
|
5958
|
+
{ name: 'MAGCLAW_DAEMON_HOME', ok: true, detail: aggregate.root },
|
|
5959
|
+
{ name: 'profiles', ok: aggregate.profiles.length > 0, detail: `${aggregate.profiles.length} configured` },
|
|
5960
|
+
{ name: 'runtime', ok: runtime.runtimes.some((item) => item.available), detail: runtime.runtimes.filter((item) => item.available).map((item) => item.id).join(', ') || 'none detected' },
|
|
5961
|
+
];
|
|
5962
|
+
if (selected) {
|
|
5963
|
+
checks.push(
|
|
5964
|
+
{ name: `profile ${target}`, ok: selected.configured, detail: selected.configPath },
|
|
5965
|
+
{ name: `daemon ${target}`, ok: selected.running || selected.service?.active, detail: selected.running ? `running (pid ${selected.pid})` : (selected.service?.active ? 'service active' : 'stopped') },
|
|
5966
|
+
);
|
|
5967
|
+
}
|
|
5968
|
+
return {
|
|
5969
|
+
ok: checks.every((check) => check.ok !== false),
|
|
5970
|
+
checks,
|
|
5971
|
+
runtime,
|
|
5972
|
+
aggregate,
|
|
5973
|
+
...(selected ? { profile: selected } : {}),
|
|
5974
|
+
cleanup: { requested: cleanup, staleLocksCleared: cleaned },
|
|
5975
|
+
};
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
async function readComputerChannel(env = process.env) {
|
|
5979
|
+
const file = computerChannelPath(env);
|
|
5980
|
+
const value = existsSync(file) ? String(await readFile(file, 'utf8')).trim() : 'latest';
|
|
5981
|
+
return value || 'latest';
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
function validateComputerChannel(value = '') {
|
|
5985
|
+
const channel = String(value || '').trim();
|
|
5986
|
+
if (channel === 'latest' || channel === 'alpha' || /^pinned:[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(channel)) return channel;
|
|
5987
|
+
throw new Error('Channel must be latest, alpha, or pinned:<semver>.');
|
|
5988
|
+
}
|
|
5989
|
+
|
|
5990
|
+
async function setComputerChannel(value, env = process.env) {
|
|
5991
|
+
const channel = validateComputerChannel(value);
|
|
5992
|
+
const file = computerChannelPath(env);
|
|
5993
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
5994
|
+
await writeFile(file, `${channel}\n`);
|
|
5995
|
+
return channel;
|
|
5996
|
+
}
|
|
5997
|
+
|
|
5998
|
+
async function computerChannel(flags = {}, env = process.env) {
|
|
5999
|
+
const action = String(flags._?.[1] || flags._?.[0] || '').trim();
|
|
6000
|
+
const value = String(flags._?.[2] || flags.channel || '').trim();
|
|
6001
|
+
if (action === 'set') {
|
|
6002
|
+
const channel = await setComputerChannel(value, env);
|
|
6003
|
+
return { ok: true, channel };
|
|
6004
|
+
}
|
|
6005
|
+
return { ok: true, channel: await readComputerChannel(env), file: computerChannelPath(env) };
|
|
6006
|
+
}
|
|
6007
|
+
|
|
6008
|
+
async function computerRunners(flags = {}, env = process.env) {
|
|
6009
|
+
const action = String(flags._?.[1] || 'list').trim();
|
|
6010
|
+
if (action === 'list') {
|
|
6011
|
+
const aggregate = await renderComputerAggregateStatus(env);
|
|
6012
|
+
return {
|
|
6013
|
+
ok: true,
|
|
6014
|
+
note: 'MagClaw local CLI can list Computer profiles. Per-agent runner stop/list remains a cloud console or agent-tool operation.',
|
|
6015
|
+
profiles: aggregate.profiles.map((profile) => ({
|
|
6016
|
+
profile: profile.profile,
|
|
6017
|
+
serverSlug: profile.serverSlug,
|
|
6018
|
+
computerId: profile.computerId,
|
|
6019
|
+
running: profile.running || profile.service?.active,
|
|
6020
|
+
})),
|
|
6021
|
+
};
|
|
6022
|
+
}
|
|
6023
|
+
if (action === 'stop') {
|
|
6024
|
+
throw new Error('Local runner stop is not available yet. Stop Agents from the MagClaw web console or agent runtime controls.');
|
|
6025
|
+
}
|
|
6026
|
+
throw new Error(`Unknown runners command: ${action}`);
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
async function computerUpgrade(flags = {}, env = process.env) {
|
|
6030
|
+
const channel = flags.channel ? validateComputerChannel(flags.channel) : await readComputerChannel(env);
|
|
6031
|
+
const targetVersion = flags.targetVersion || flags.to || flags.version || (String(channel).startsWith('pinned:') ? String(channel).slice('pinned:'.length) : channel);
|
|
6032
|
+
await runManualUpgrade({
|
|
6033
|
+
...flags,
|
|
6034
|
+
to: targetVersion,
|
|
6035
|
+
targetVersion,
|
|
6036
|
+
packageName: COMPUTER_PACKAGE_NAME,
|
|
6037
|
+
packageBin: 'magclaw-computer',
|
|
6038
|
+
}, {
|
|
6039
|
+
...env,
|
|
6040
|
+
MAGCLAW_ENTRY_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
|
|
6041
|
+
MAGCLAW_DAEMON_PACKAGE_NAME: COMPUTER_PACKAGE_NAME,
|
|
6042
|
+
MAGCLAW_DAEMON_PACKAGE_KIND: 'computer',
|
|
6043
|
+
MAGCLAW_DAEMON_PACKAGE_BIN: 'magclaw-computer',
|
|
6044
|
+
});
|
|
6045
|
+
}
|
|
6046
|
+
|
|
6047
|
+
async function runComputerCommand(flags, env = process.env) {
|
|
6048
|
+
const subcommand = String(flags._?.[0] || 'help').trim();
|
|
6049
|
+
if (subcommand === 'help' || flags.help) {
|
|
6050
|
+
process.stdout.write(renderComputerHelp(subcommand === 'help' ? flags._?.[1] : subcommand));
|
|
6051
|
+
return;
|
|
6052
|
+
}
|
|
6053
|
+
switch (subcommand) {
|
|
6054
|
+
case 'login':
|
|
6055
|
+
case 'attach':
|
|
6056
|
+
case 'setup':
|
|
6057
|
+
await runComputerSetup(flags, env);
|
|
6058
|
+
break;
|
|
6059
|
+
case 'adopt-legacy':
|
|
6060
|
+
throw new Error('MagClaw legacy adoption is handled by `magclaw-computer setup /<serverSlug>` or `magclaw connect --pair-token <token>`.');
|
|
6061
|
+
case 'detach':
|
|
6062
|
+
printJson(await detachComputerProfile(flags, env));
|
|
6063
|
+
break;
|
|
6064
|
+
case 'status': {
|
|
6065
|
+
const report = await computerStatus(flags, env);
|
|
6066
|
+
if (flags.json) printJson(report);
|
|
6067
|
+
else process.stdout.write(formatComputerStatus(report));
|
|
6068
|
+
break;
|
|
6069
|
+
}
|
|
6070
|
+
case 'start':
|
|
6071
|
+
if (hasComputerTarget(flags)) {
|
|
6072
|
+
if (flags.foreground) {
|
|
6073
|
+
await runForegroundDaemon(await buildConfig({ ...flags, profile: computerTargetProfile(flags) }, env), env);
|
|
6074
|
+
} else {
|
|
6075
|
+
printJson(await startSavedBackground({ ...flags, profile: computerTargetProfile(flags) }, env));
|
|
6076
|
+
}
|
|
6077
|
+
} else {
|
|
6078
|
+
printJson(await startAllComputerProfiles(env));
|
|
6079
|
+
}
|
|
6080
|
+
break;
|
|
6081
|
+
case 'stop':
|
|
6082
|
+
if (hasComputerTarget(flags)) printJson(await stopDaemon(computerTargetProfile(flags), env, { disable: Boolean(flags.disable) }));
|
|
6083
|
+
else printJson(await stopAllComputerProfiles(flags, env));
|
|
6084
|
+
break;
|
|
6085
|
+
case 'doctor': {
|
|
6086
|
+
const report = await computerDoctor(flags, env);
|
|
6087
|
+
if (flags.json) printJson(report);
|
|
6088
|
+
else process.stdout.write(`${report.checks.map((check) => `${check.ok ? 'ok' : 'fail'} ${check.name}: ${check.detail}`).join('\n')}\n`);
|
|
6089
|
+
break;
|
|
6090
|
+
}
|
|
6091
|
+
case 'logs':
|
|
6092
|
+
await logs(computerTargetProfile(flags), { lines: flags.lines || flags.lineCount });
|
|
6093
|
+
break;
|
|
6094
|
+
case 'runners':
|
|
6095
|
+
printJson(await computerRunners(flags, env));
|
|
6096
|
+
break;
|
|
6097
|
+
case 'channel':
|
|
6098
|
+
printJson(await computerChannel(flags, env));
|
|
6099
|
+
break;
|
|
6100
|
+
case 'upgrade':
|
|
6101
|
+
await computerUpgrade(flags, env);
|
|
6102
|
+
break;
|
|
6103
|
+
default:
|
|
6104
|
+
throw new Error(`Unknown computer command: ${subcommand}`);
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
|
|
5386
6108
|
async function buildConfig(flags, env = process.env) {
|
|
5387
6109
|
const diskConfig = await readProfile(flags.profile, env);
|
|
5388
6110
|
const profile = flags.profile || diskConfig.profile || DEFAULT_PROFILE;
|
|
@@ -5405,7 +6127,7 @@ async function buildConfig(flags, env = process.env) {
|
|
|
5405
6127
|
|
|
5406
6128
|
async function runForegroundDaemon(config, env = process.env) {
|
|
5407
6129
|
const releaseLock = await acquireDaemonLock(config.profile, config, env);
|
|
5408
|
-
await
|
|
6130
|
+
await markDaemonRunServiceState(config.profile, env);
|
|
5409
6131
|
const daemon = new MagClawDaemon(config, env);
|
|
5410
6132
|
let forceExitTimer = null;
|
|
5411
6133
|
const shutdown = (signal) => {
|
|
@@ -5482,6 +6204,14 @@ function requireExplicitProfile(command, flags = {}) {
|
|
|
5482
6204
|
|
|
5483
6205
|
export async function main(argv = process.argv, env = process.env) {
|
|
5484
6206
|
const { command, flags } = parseCli(argv);
|
|
6207
|
+
if (flags.version) {
|
|
6208
|
+
process.stdout.write(`${DAEMON_VERSION}\n`);
|
|
6209
|
+
return;
|
|
6210
|
+
}
|
|
6211
|
+
if (command === 'computer' && flags.help) {
|
|
6212
|
+
process.stdout.write(renderComputerHelp(flags._?.[0] || ''));
|
|
6213
|
+
return;
|
|
6214
|
+
}
|
|
5485
6215
|
if (command === 'help' || flags.help) {
|
|
5486
6216
|
process.stdout.write(renderHelp());
|
|
5487
6217
|
return;
|
|
@@ -5491,7 +6221,7 @@ export async function main(argv = process.argv, env = process.env) {
|
|
|
5491
6221
|
await runConnect(flags, env);
|
|
5492
6222
|
break;
|
|
5493
6223
|
case 'computer':
|
|
5494
|
-
await
|
|
6224
|
+
await runComputerCommand(flags, env);
|
|
5495
6225
|
break;
|
|
5496
6226
|
case 'start': {
|
|
5497
6227
|
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 = {} } = {}) {
|