aicomputer 0.1.18 → 0.1.20
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 +28 -0
- package/dist/{chunk-3ZUTAUUD.js → chunk-36BZXAAX.js} +129 -258
- package/dist/chunk-3ZF7JRBW.js +270 -0
- package/dist/chunk-4TE5XTYE.js +242 -0
- package/dist/chunk-5Y2NWK5I.js +14 -0
- package/dist/chunk-D3SAFNSI.js +75 -0
- package/dist/chunk-GD42GHW3.js +183 -0
- package/dist/chunk-GGBVVRLL.js +32 -0
- package/dist/chunk-LOGK7YYJ.js +255 -0
- package/dist/{chunk-F2U4SFJ4.js → chunk-TPFE3CC6.js} +24 -174
- package/dist/index.js +678 -430
- package/dist/lib/autossh-runtime.d.ts +21 -0
- package/dist/lib/autossh-runtime.js +23 -0
- package/dist/lib/mount-config.d.ts +5 -1
- package/dist/lib/mount-mutagen.d.ts +2 -0
- package/dist/lib/mount-mutagen.js +3 -1
- package/dist/lib/mount-reconcile.js +5 -2
- package/dist/lib/mutagen-runtime.d.ts +20 -0
- package/dist/lib/mutagen-runtime.js +19 -0
- package/dist/lib/ssh-access.d.ts +74 -0
- package/dist/lib/ssh-access.js +25 -0
- package/dist/lib/ssh-config.d.ts +14 -0
- package/dist/lib/ssh-config.js +10 -0
- package/dist/lib/upgrade-version.d.ts +10 -0
- package/dist/lib/upgrade-version.js +8 -0
- package/package.json +3 -3
- package/scripts/postinstall.mjs +36 -23
package/dist/index.js
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureSSHAliasConfig
|
|
4
|
+
} from "./chunk-D3SAFNSI.js";
|
|
5
|
+
import {
|
|
6
|
+
compareVersions,
|
|
7
|
+
resolveLatestPublishedVersion
|
|
8
|
+
} from "./chunk-GGBVVRLL.js";
|
|
2
9
|
import {
|
|
3
10
|
formatMountHostInstallGuidance,
|
|
4
11
|
getMountHostValidationIssues
|
|
5
12
|
} from "./chunk-JMRAYXUO.js";
|
|
13
|
+
import {
|
|
14
|
+
formatStatus,
|
|
15
|
+
padEnd,
|
|
16
|
+
promptForSSHComputer,
|
|
17
|
+
reconcileMounts,
|
|
18
|
+
teardownManagedSessions,
|
|
19
|
+
timeAgo
|
|
20
|
+
} from "./chunk-36BZXAAX.js";
|
|
21
|
+
import {
|
|
22
|
+
isAbortError
|
|
23
|
+
} from "./chunk-TPFE3CC6.js";
|
|
24
|
+
import {
|
|
25
|
+
defaultMountServiceConfig,
|
|
26
|
+
ensureMountDirectories,
|
|
27
|
+
getMountPaths,
|
|
28
|
+
readMountConfig,
|
|
29
|
+
readMountControllerLock,
|
|
30
|
+
readMountStatusSnapshot,
|
|
31
|
+
removeMountControllerLock,
|
|
32
|
+
writeMountConfig,
|
|
33
|
+
writeMountControllerLock,
|
|
34
|
+
writeMountStatusSnapshot
|
|
35
|
+
} from "./chunk-KXLTHWW3.js";
|
|
36
|
+
import {
|
|
37
|
+
AGENTCOMPUTER_MUTAGEN_PATH_ENV,
|
|
38
|
+
ensureBundledMutagenInstalled,
|
|
39
|
+
ensureMutagenCommandPath
|
|
40
|
+
} from "./chunk-GD42GHW3.js";
|
|
41
|
+
import {
|
|
42
|
+
ensureDefaultSSHKeyRegistered,
|
|
43
|
+
openSSHConnection,
|
|
44
|
+
prepareSSHConnection,
|
|
45
|
+
prepareSSHConnectionByIdentifier
|
|
46
|
+
} from "./chunk-4TE5XTYE.js";
|
|
47
|
+
import {
|
|
48
|
+
ensureBundledAutosshInstalled
|
|
49
|
+
} from "./chunk-3ZF7JRBW.js";
|
|
6
50
|
import {
|
|
7
51
|
ApiError,
|
|
8
52
|
api,
|
|
@@ -12,7 +56,6 @@ import {
|
|
|
12
56
|
createComputer,
|
|
13
57
|
deleteComputer,
|
|
14
58
|
deletePublishedPort,
|
|
15
|
-
formatStatus,
|
|
16
59
|
getAPIKey,
|
|
17
60
|
getBaseURL,
|
|
18
61
|
getComputerByID,
|
|
@@ -23,168 +66,27 @@ import {
|
|
|
23
66
|
hasEnvAPIKey,
|
|
24
67
|
listComputers,
|
|
25
68
|
listPublishedPorts,
|
|
26
|
-
|
|
27
|
-
|
|
69
|
+
powerOffComputer,
|
|
70
|
+
powerOnComputer,
|
|
28
71
|
publishPort,
|
|
29
|
-
reconcileMounts,
|
|
30
72
|
resolveComputer,
|
|
31
73
|
setAPIKey,
|
|
32
|
-
teardownManagedSessions,
|
|
33
|
-
timeAgo,
|
|
34
74
|
vncURL,
|
|
35
75
|
webURL
|
|
36
|
-
} from "./chunk-
|
|
37
|
-
import
|
|
38
|
-
AGENTCOMPUTER_MUTAGEN_PATH_ENV,
|
|
39
|
-
ensureBundledMutagenInstalled,
|
|
40
|
-
ensureMutagenCommandPath,
|
|
41
|
-
isAbortError
|
|
42
|
-
} from "./chunk-F2U4SFJ4.js";
|
|
43
|
-
import {
|
|
44
|
-
defaultMountServiceConfig,
|
|
45
|
-
ensureMountDirectories,
|
|
46
|
-
getMountPaths,
|
|
47
|
-
readMountConfig,
|
|
48
|
-
readMountControllerLock,
|
|
49
|
-
readMountStatusSnapshot,
|
|
50
|
-
removeMountControllerLock,
|
|
51
|
-
writeMountConfig,
|
|
52
|
-
writeMountControllerLock,
|
|
53
|
-
writeMountStatusSnapshot
|
|
54
|
-
} from "./chunk-KXLTHWW3.js";
|
|
76
|
+
} from "./chunk-LOGK7YYJ.js";
|
|
77
|
+
import "./chunk-5Y2NWK5I.js";
|
|
55
78
|
|
|
56
79
|
// src/index.ts
|
|
57
|
-
import { Command as
|
|
58
|
-
import
|
|
80
|
+
import { Command as Command16 } from "commander";
|
|
81
|
+
import chalk14 from "chalk";
|
|
59
82
|
import { readFileSync as readFileSync3 } from "fs";
|
|
60
|
-
import { basename
|
|
83
|
+
import { basename } from "path";
|
|
61
84
|
|
|
62
85
|
// src/commands/access.ts
|
|
63
86
|
import { Command } from "commander";
|
|
64
87
|
import chalk from "chalk";
|
|
65
88
|
import ora from "ora";
|
|
66
89
|
|
|
67
|
-
// src/lib/ssh-access.ts
|
|
68
|
-
import { spawn } from "child_process";
|
|
69
|
-
|
|
70
|
-
// src/lib/ssh-keys.ts
|
|
71
|
-
import { basename } from "path";
|
|
72
|
-
import { homedir } from "os";
|
|
73
|
-
import { readFile, mkdir } from "fs/promises";
|
|
74
|
-
import { execFileSync } from "child_process";
|
|
75
|
-
import { existsSync } from "fs";
|
|
76
|
-
var DEFAULT_PUBLIC_KEY_PATHS = [
|
|
77
|
-
`${homedir()}/.ssh/id_ed25519.pub`,
|
|
78
|
-
`${homedir()}/.ssh/id_ecdsa.pub`,
|
|
79
|
-
`${homedir()}/.ssh/id_rsa.pub`
|
|
80
|
-
];
|
|
81
|
-
async function ensureDefaultSSHKeyRegistered() {
|
|
82
|
-
for (const path of DEFAULT_PUBLIC_KEY_PATHS) {
|
|
83
|
-
try {
|
|
84
|
-
const publicKey2 = (await readFile(path, "utf8")).trim();
|
|
85
|
-
if (!publicKey2) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const key2 = await api("/v1/ssh-keys", {
|
|
89
|
-
method: "POST",
|
|
90
|
-
body: JSON.stringify({
|
|
91
|
-
name: basename(path),
|
|
92
|
-
public_key: publicKey2
|
|
93
|
-
})
|
|
94
|
-
});
|
|
95
|
-
return {
|
|
96
|
-
key: key2,
|
|
97
|
-
publicKeyPath: path,
|
|
98
|
-
privateKeyPath: path.replace(/\.pub$/, "")
|
|
99
|
-
};
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error?.code === "ENOENT") {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const generated = await generateSSHKey();
|
|
108
|
-
const publicKey = (await readFile(generated.publicKeyPath, "utf8")).trim();
|
|
109
|
-
const key = await api("/v1/ssh-keys", {
|
|
110
|
-
method: "POST",
|
|
111
|
-
body: JSON.stringify({
|
|
112
|
-
name: basename(generated.publicKeyPath),
|
|
113
|
-
public_key: publicKey
|
|
114
|
-
})
|
|
115
|
-
});
|
|
116
|
-
return {
|
|
117
|
-
key,
|
|
118
|
-
publicKeyPath: generated.publicKeyPath,
|
|
119
|
-
privateKeyPath: generated.privateKeyPath
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
async function generateSSHKey() {
|
|
123
|
-
const sshDir = `${homedir()}/.ssh`;
|
|
124
|
-
if (!existsSync(sshDir)) {
|
|
125
|
-
await mkdir(sshDir, { mode: 448 });
|
|
126
|
-
}
|
|
127
|
-
const privateKeyPath = `${sshDir}/id_ed25519`;
|
|
128
|
-
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
129
|
-
console.log("No SSH key found \u2014 generating one at", publicKeyPath);
|
|
130
|
-
execFileSync("ssh-keygen", ["-t", "ed25519", "-f", privateKeyPath, "-N", ""], {
|
|
131
|
-
stdio: "inherit"
|
|
132
|
-
});
|
|
133
|
-
return { publicKeyPath, privateKeyPath };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// src/lib/ssh-access.ts
|
|
137
|
-
async function prepareSSHConnection(computer) {
|
|
138
|
-
const registered = await ensureDefaultSSHKeyRegistered();
|
|
139
|
-
const info = await getConnectionInfo(computer.id);
|
|
140
|
-
if (!info.connection.ssh_available) {
|
|
141
|
-
throw new Error("SSH is not available for this computer");
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
computer,
|
|
145
|
-
command: formatSSHCommand(
|
|
146
|
-
info.connection.ssh_user,
|
|
147
|
-
info.connection.ssh_host,
|
|
148
|
-
info.connection.ssh_port
|
|
149
|
-
),
|
|
150
|
-
args: [
|
|
151
|
-
"-i",
|
|
152
|
-
registered.privateKeyPath,
|
|
153
|
-
"-p",
|
|
154
|
-
String(info.connection.ssh_port),
|
|
155
|
-
`${info.connection.ssh_user}@${info.connection.ssh_host}`
|
|
156
|
-
]
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
async function prepareSSHConnectionByIdentifier(identifier) {
|
|
160
|
-
const computer = await resolveComputer(identifier);
|
|
161
|
-
return prepareSSHConnection(computer);
|
|
162
|
-
}
|
|
163
|
-
async function openSSHConnection(connection) {
|
|
164
|
-
await new Promise((resolve, reject) => {
|
|
165
|
-
const child = spawn("ssh", connection.args, {
|
|
166
|
-
stdio: "inherit"
|
|
167
|
-
});
|
|
168
|
-
child.on("error", reject);
|
|
169
|
-
child.on("exit", (code) => {
|
|
170
|
-
if (code === 0) {
|
|
171
|
-
resolve();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
reject(new Error(`ssh exited with code ${code ?? 1}`));
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
function formatSSHCommand(user, host, port) {
|
|
179
|
-
if (!user.trim() || !host.trim()) {
|
|
180
|
-
return "ssh unavailable";
|
|
181
|
-
}
|
|
182
|
-
if (port <= 0 || port === 22) {
|
|
183
|
-
return `ssh ${user}@${host}`;
|
|
184
|
-
}
|
|
185
|
-
return `ssh -p ${port} ${user}@${host}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
90
|
// src/lib/open-browser.ts
|
|
189
91
|
import { constants } from "fs";
|
|
190
92
|
import { access } from "fs/promises";
|
|
@@ -242,74 +144,6 @@ async function openBrowserURL(url) {
|
|
|
242
144
|
await open(url);
|
|
243
145
|
}
|
|
244
146
|
|
|
245
|
-
// src/lib/ssh-config.ts
|
|
246
|
-
import { homedir as homedir2 } from "os";
|
|
247
|
-
import { join } from "path";
|
|
248
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
249
|
-
var MANAGED_BLOCK_START = "# >>> agentcomputer ssh setup >>>";
|
|
250
|
-
var MANAGED_BLOCK_END = "# <<< agentcomputer ssh setup <<<";
|
|
251
|
-
async function ensureSSHAliasConfig(options) {
|
|
252
|
-
const sshDir = join(homedir2(), ".ssh");
|
|
253
|
-
const configPath = join(sshDir, "config");
|
|
254
|
-
await mkdir2(sshDir, { recursive: true, mode: 448 });
|
|
255
|
-
let existing = "";
|
|
256
|
-
try {
|
|
257
|
-
existing = await readFile2(configPath, "utf8");
|
|
258
|
-
} catch (error) {
|
|
259
|
-
if (error.code !== "ENOENT") {
|
|
260
|
-
throw error;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const nextBlock = renderManagedBlock(options);
|
|
264
|
-
const managedBlockPattern = new RegExp(
|
|
265
|
-
`${escapeRegex(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegex(MANAGED_BLOCK_END)}\\n?`,
|
|
266
|
-
"m"
|
|
267
|
-
);
|
|
268
|
-
let nextContents;
|
|
269
|
-
if (managedBlockPattern.test(existing)) {
|
|
270
|
-
nextContents = existing.replace(managedBlockPattern, nextBlock);
|
|
271
|
-
} else {
|
|
272
|
-
const normalized = existing.length === 0 ? "" : existing.endsWith("\n") ? existing : `${existing}
|
|
273
|
-
`;
|
|
274
|
-
nextContents = normalized.length === 0 ? nextBlock : `${normalized}
|
|
275
|
-
${nextBlock}`;
|
|
276
|
-
}
|
|
277
|
-
const changed = nextContents !== existing;
|
|
278
|
-
if (changed) {
|
|
279
|
-
await writeFile(configPath, nextContents, { mode: 384 });
|
|
280
|
-
}
|
|
281
|
-
return {
|
|
282
|
-
configPath,
|
|
283
|
-
changed
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
function renderManagedBlock(options) {
|
|
287
|
-
const user = options.user?.trim() || "agentcomputer";
|
|
288
|
-
const identityFile = formatIdentityFilePath(options.identityFilePath);
|
|
289
|
-
return `${MANAGED_BLOCK_START}
|
|
290
|
-
Host ${options.alias}
|
|
291
|
-
HostName ${options.host}
|
|
292
|
-
Port ${options.port}
|
|
293
|
-
User ${user}
|
|
294
|
-
IdentityFile ${identityFile}
|
|
295
|
-
IdentitiesOnly yes
|
|
296
|
-
ServerAliveInterval 30
|
|
297
|
-
ServerAliveCountMax 4
|
|
298
|
-
${MANAGED_BLOCK_END}
|
|
299
|
-
`;
|
|
300
|
-
}
|
|
301
|
-
function formatIdentityFilePath(path) {
|
|
302
|
-
const normalized = path.trim();
|
|
303
|
-
const homePath = `${homedir2()}/`;
|
|
304
|
-
if (normalized.startsWith(homePath)) {
|
|
305
|
-
return `~/${normalized.slice(homePath.length)}`;
|
|
306
|
-
}
|
|
307
|
-
return normalized.replaceAll(" ", "\\ ");
|
|
308
|
-
}
|
|
309
|
-
function escapeRegex(value) {
|
|
310
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
147
|
// src/lib/ssh-setup.ts
|
|
314
148
|
async function ensureSSHAccessConfigured(options = {}) {
|
|
315
149
|
const alias = normalizeSSHAlias(options.alias ?? "agentcomputer.ai");
|
|
@@ -389,8 +223,8 @@ var openCommand = new Command("open").description("Open a computer in your brows
|
|
|
389
223
|
process.exit(1);
|
|
390
224
|
}
|
|
391
225
|
});
|
|
392
|
-
var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").option("--setup", "Register key and configure a global SSH alias").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").action(
|
|
393
|
-
async (identifier, options) => {
|
|
226
|
+
var sshCommand = new Command("ssh").description("Open an SSH session to a computer").allowUnknownOption(true).argument("[id-or-handle]", "Computer id or handle").argument("[ssh-args...]", "SSH arguments passed through after the machine handle").option("--setup", "Register key and configure a global SSH alias").option("--tmux", "Attach or create a persistent tmux session on connect").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").action(
|
|
227
|
+
async (identifier, sshArgs, options) => {
|
|
394
228
|
if (options.setup) {
|
|
395
229
|
await setupSSHAlias(options);
|
|
396
230
|
return;
|
|
@@ -400,7 +234,10 @@ var sshCommand = new Command("ssh").description("Open an SSH session to a comput
|
|
|
400
234
|
).start();
|
|
401
235
|
try {
|
|
402
236
|
const computer = await resolveSSHComputer(identifier, spinner);
|
|
403
|
-
const connection = await prepareSSHConnection(computer
|
|
237
|
+
const connection = await prepareSSHConnection(computer, {
|
|
238
|
+
extraArgs: sshArgs,
|
|
239
|
+
tmux: options.tmux
|
|
240
|
+
});
|
|
404
241
|
spinner.succeed(`Connecting to ${chalk.bold(computer.handle)}`);
|
|
405
242
|
console.log(chalk.dim(` ${connection.command}`));
|
|
406
243
|
console.log();
|
|
@@ -1311,7 +1148,7 @@ import ora4 from "ora";
|
|
|
1311
1148
|
|
|
1312
1149
|
// src/lib/remote-auth.ts
|
|
1313
1150
|
import { randomBytes } from "crypto";
|
|
1314
|
-
import { spawn
|
|
1151
|
+
import { spawn } from "child_process";
|
|
1315
1152
|
import ora3 from "ora";
|
|
1316
1153
|
var readyPollIntervalMs = 2e3;
|
|
1317
1154
|
var readyPollTimeoutMs = 18e4;
|
|
@@ -1447,7 +1284,7 @@ async function runRemoteCommand(target, remoteArgs, script) {
|
|
|
1447
1284
|
...remoteArgs
|
|
1448
1285
|
];
|
|
1449
1286
|
return new Promise((resolve, reject) => {
|
|
1450
|
-
const child =
|
|
1287
|
+
const child = spawn("ssh", args, {
|
|
1451
1288
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1452
1289
|
});
|
|
1453
1290
|
let stdout = "";
|
|
@@ -2007,13 +1844,14 @@ function isInternalCondition(value) {
|
|
|
2007
1844
|
}
|
|
2008
1845
|
function printComputer(computer) {
|
|
2009
1846
|
const vnc = vncURL(computer);
|
|
2010
|
-
const ssh = computer.ssh_enabled ?
|
|
1847
|
+
const ssh = computer.ssh_enabled ? formatSSHCommand(computer.handle, computer.ssh_host, computer.ssh_port) : "disabled";
|
|
2011
1848
|
const isCustom = computer.runtime_family === "custom-machine";
|
|
2012
1849
|
console.log();
|
|
2013
1850
|
console.log(` ${chalk4.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
|
|
2014
1851
|
console.log();
|
|
2015
1852
|
console.log(` ${chalk4.dim("ID")} ${computer.id}`);
|
|
2016
1853
|
console.log(` ${chalk4.dim("Tier")} ${computer.tier}`);
|
|
1854
|
+
console.log(` ${chalk4.dim("Power")} ${formatDesiredPowerState(computer.desired_power_state)}`);
|
|
2017
1855
|
if (isCustom) {
|
|
2018
1856
|
console.log(` ${chalk4.dim("Runtime")} ${computer.runtime_family}`);
|
|
2019
1857
|
console.log(` ${chalk4.dim("Source")} ${computer.source_kind}`);
|
|
@@ -2043,7 +1881,7 @@ function printComputer(computer) {
|
|
|
2043
1881
|
console.log(` ${chalk4.dim("Created")} ${timeAgo(computer.created_at)}`);
|
|
2044
1882
|
console.log();
|
|
2045
1883
|
}
|
|
2046
|
-
function
|
|
1884
|
+
function formatSSHCommand(user, host, port) {
|
|
2047
1885
|
if (!user.trim() || !host.trim()) {
|
|
2048
1886
|
return "ssh unavailable";
|
|
2049
1887
|
}
|
|
@@ -2071,6 +1909,12 @@ function formatManagedWorkerLaunchSource(computer) {
|
|
|
2071
1909
|
}
|
|
2072
1910
|
return `saved custom source ${computer.user_source_id}`;
|
|
2073
1911
|
}
|
|
1912
|
+
function formatDesiredPowerState(state) {
|
|
1913
|
+
return state === "off" ? chalk4.yellow("off") : chalk4.green("on");
|
|
1914
|
+
}
|
|
1915
|
+
function supportsPowerLifecycle(computer) {
|
|
1916
|
+
return computer.runtime_family === "managed-worker";
|
|
1917
|
+
}
|
|
2074
1918
|
function printComputerTable(computers) {
|
|
2075
1919
|
const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
|
|
2076
1920
|
const statusWidth = 12;
|
|
@@ -2327,6 +2171,72 @@ var removeCommand = new Command5("rm").description("Delete a computer").argument
|
|
|
2327
2171
|
process.exit(1);
|
|
2328
2172
|
}
|
|
2329
2173
|
});
|
|
2174
|
+
var powerOffCommand = new Command5("power-off").description("Power off a managed worker without deleting its storage").argument("<id-or-handle>", "Computer id or handle").option("--json", "Print raw JSON").action(async (identifier, options) => {
|
|
2175
|
+
const spinner = options.json ? null : ora5("Resolving computer...").start();
|
|
2176
|
+
try {
|
|
2177
|
+
const computer = await resolveComputer(identifier);
|
|
2178
|
+
if (!supportsPowerLifecycle(computer)) {
|
|
2179
|
+
throw new Error("power lifecycle is only available for managed-worker machines");
|
|
2180
|
+
}
|
|
2181
|
+
spinner?.start("Powering off computer...");
|
|
2182
|
+
const poweredOff = await powerOffComputer(computer.id);
|
|
2183
|
+
spinner?.succeed(chalk4.green(`Powered off ${chalk4.bold(poweredOff.handle)}`));
|
|
2184
|
+
await notifyMountDaemon(
|
|
2185
|
+
getMountPaths((readMountConfig() ?? defaultMountServiceConfig()).rootPath)
|
|
2186
|
+
).catch(
|
|
2187
|
+
() => false
|
|
2188
|
+
);
|
|
2189
|
+
if (options.json) {
|
|
2190
|
+
console.log(JSON.stringify(poweredOff, null, 2));
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
printComputer(poweredOff);
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
if (spinner) {
|
|
2196
|
+
spinner.fail(
|
|
2197
|
+
error instanceof Error ? error.message : "Failed to power off computer"
|
|
2198
|
+
);
|
|
2199
|
+
} else {
|
|
2200
|
+
console.error(
|
|
2201
|
+
error instanceof Error ? error.message : "Failed to power off computer"
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
var powerOnCommand = new Command5("power-on").description("Power on a managed worker and recreate its runtime").argument("<id-or-handle>", "Computer id or handle").option("--json", "Print raw JSON").action(async (identifier, options) => {
|
|
2208
|
+
const spinner = options.json ? null : ora5("Resolving computer...").start();
|
|
2209
|
+
try {
|
|
2210
|
+
const computer = await resolveComputer(identifier);
|
|
2211
|
+
if (!supportsPowerLifecycle(computer)) {
|
|
2212
|
+
throw new Error("power lifecycle is only available for managed-worker machines");
|
|
2213
|
+
}
|
|
2214
|
+
spinner?.start("Powering on computer...");
|
|
2215
|
+
const poweredOn = await powerOnComputer(computer.id);
|
|
2216
|
+
spinner?.succeed(chalk4.green(`Powered on ${chalk4.bold(poweredOn.handle)}`));
|
|
2217
|
+
await notifyMountDaemon(
|
|
2218
|
+
getMountPaths((readMountConfig() ?? defaultMountServiceConfig()).rootPath)
|
|
2219
|
+
).catch(
|
|
2220
|
+
() => false
|
|
2221
|
+
);
|
|
2222
|
+
if (options.json) {
|
|
2223
|
+
console.log(JSON.stringify(poweredOn, null, 2));
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
printComputer(poweredOn);
|
|
2227
|
+
} catch (error) {
|
|
2228
|
+
if (spinner) {
|
|
2229
|
+
spinner.fail(
|
|
2230
|
+
error instanceof Error ? error.message : "Failed to power on computer"
|
|
2231
|
+
);
|
|
2232
|
+
} else {
|
|
2233
|
+
console.error(
|
|
2234
|
+
error instanceof Error ? error.message : "Failed to power on computer"
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
process.exit(1);
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2330
2240
|
function parseRuntimeFamilyOption(value) {
|
|
2331
2241
|
switch (value) {
|
|
2332
2242
|
case void 0:
|
|
@@ -2462,6 +2372,8 @@ _computer() {
|
|
|
2462
2372
|
'create:Create a computer'
|
|
2463
2373
|
'ls:List computers'
|
|
2464
2374
|
'get:Show computer details'
|
|
2375
|
+
'power-on:Power on a managed worker'
|
|
2376
|
+
'power-off:Power off a managed worker'
|
|
2465
2377
|
'image:Manage machine image sources'
|
|
2466
2378
|
'open:Open in browser'
|
|
2467
2379
|
'ssh:SSH into a computer'
|
|
@@ -2552,6 +2464,11 @@ _computer() {
|
|
|
2552
2464
|
'--json[Print raw JSON]' \\
|
|
2553
2465
|
'1:computer:_computer_handles'
|
|
2554
2466
|
;;
|
|
2467
|
+
power-on|power-off)
|
|
2468
|
+
_arguments \\
|
|
2469
|
+
'--json[Print raw JSON]' \\
|
|
2470
|
+
'1:computer:_computer_handles'
|
|
2471
|
+
;;
|
|
2555
2472
|
image)
|
|
2556
2473
|
_arguments -C \\
|
|
2557
2474
|
'1:command:->image_command' \\
|
|
@@ -2816,10 +2733,10 @@ var completionCommand = new Command6("completion").description("Generate shell c
|
|
|
2816
2733
|
});
|
|
2817
2734
|
|
|
2818
2735
|
// src/commands/codex-login.ts
|
|
2819
|
-
import { spawn as
|
|
2820
|
-
import { readFile
|
|
2821
|
-
import { homedir
|
|
2822
|
-
import { join
|
|
2736
|
+
import { spawn as spawn2 } from "child_process";
|
|
2737
|
+
import { readFile } from "fs/promises";
|
|
2738
|
+
import { homedir } from "os";
|
|
2739
|
+
import { join } from "path";
|
|
2823
2740
|
import { Command as Command7 } from "commander";
|
|
2824
2741
|
import chalk5 from "chalk";
|
|
2825
2742
|
import ora6 from "ora";
|
|
@@ -3019,10 +2936,10 @@ async function getLocalCodexStatus() {
|
|
|
3019
2936
|
return parseCodexStatusOutput(result.stdout, result.stderr);
|
|
3020
2937
|
}
|
|
3021
2938
|
async function readLocalCodexAuthFile() {
|
|
3022
|
-
const authPath =
|
|
2939
|
+
const authPath = join(homedir(), ".codex", "auth.json");
|
|
3023
2940
|
let raw;
|
|
3024
2941
|
try {
|
|
3025
|
-
raw = await
|
|
2942
|
+
raw = await readFile(authPath, "utf8");
|
|
3026
2943
|
} catch (error) {
|
|
3027
2944
|
throw new Error(
|
|
3028
2945
|
error instanceof Error ? `failed to read ${authPath}: ${error.message}` : `failed to read ${authPath}`
|
|
@@ -3040,7 +2957,7 @@ async function readLocalCodexAuthFile() {
|
|
|
3040
2957
|
}
|
|
3041
2958
|
async function runInteractiveCodexLogin() {
|
|
3042
2959
|
await new Promise((resolve, reject) => {
|
|
3043
|
-
const child =
|
|
2960
|
+
const child = spawn2("codex", ["login"], {
|
|
3044
2961
|
stdio: "inherit"
|
|
3045
2962
|
});
|
|
3046
2963
|
child.on("error", (error) => {
|
|
@@ -3063,7 +2980,7 @@ async function runInteractiveCodexLogin() {
|
|
|
3063
2980
|
}
|
|
3064
2981
|
async function captureLocalCommand(command, args) {
|
|
3065
2982
|
return new Promise((resolve, reject) => {
|
|
3066
|
-
const child =
|
|
2983
|
+
const child = spawn2(command, args, {
|
|
3067
2984
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3068
2985
|
});
|
|
3069
2986
|
let stdout = "";
|
|
@@ -3567,16 +3484,42 @@ async function confirmDeletion(sourceID) {
|
|
|
3567
3484
|
});
|
|
3568
3485
|
}
|
|
3569
3486
|
|
|
3570
|
-
// src/commands/internal-install-
|
|
3487
|
+
// src/commands/internal-install-autossh.ts
|
|
3571
3488
|
import { Command as Command9 } from "commander";
|
|
3572
3489
|
import chalk7 from "chalk";
|
|
3573
|
-
var
|
|
3490
|
+
var internalInstallAutosshCommand = new Command9(
|
|
3491
|
+
"internal-install-autossh"
|
|
3492
|
+
).option("--quiet", "Suppress output unless installation fails").action(async (options) => {
|
|
3493
|
+
try {
|
|
3494
|
+
const executablePath = await ensureBundledAutosshInstalled();
|
|
3495
|
+
if (!options.quiet) {
|
|
3496
|
+
console.log();
|
|
3497
|
+
console.log(
|
|
3498
|
+
chalk7.green(` Bundled autossh ready at ${executablePath}.`)
|
|
3499
|
+
);
|
|
3500
|
+
console.log();
|
|
3501
|
+
}
|
|
3502
|
+
} catch (error) {
|
|
3503
|
+
if (!options.quiet) {
|
|
3504
|
+
const message = error instanceof Error ? error.message : "failed to install bundled autossh";
|
|
3505
|
+
console.error();
|
|
3506
|
+
console.error(chalk7.red(` ${message}`));
|
|
3507
|
+
console.error();
|
|
3508
|
+
}
|
|
3509
|
+
process.exit(1);
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
3512
|
+
|
|
3513
|
+
// src/commands/internal-install-mutagen.ts
|
|
3514
|
+
import { Command as Command10 } from "commander";
|
|
3515
|
+
import chalk8 from "chalk";
|
|
3516
|
+
var internalInstallMutagenCommand = new Command10("internal-install-mutagen").option("--quiet", "Suppress output unless installation fails").action(async (options) => {
|
|
3574
3517
|
try {
|
|
3575
3518
|
const executablePath = await ensureBundledMutagenInstalled();
|
|
3576
3519
|
if (!options.quiet) {
|
|
3577
3520
|
console.log();
|
|
3578
3521
|
console.log(
|
|
3579
|
-
|
|
3522
|
+
chalk8.green(
|
|
3580
3523
|
` Bundled Mutagen ready at ${executablePath}.`
|
|
3581
3524
|
)
|
|
3582
3525
|
);
|
|
@@ -3586,7 +3529,7 @@ var internalInstallMutagenCommand = new Command9("internal-install-mutagen").opt
|
|
|
3586
3529
|
if (!options.quiet) {
|
|
3587
3530
|
const message = error instanceof Error ? error.message : "failed to install bundled Mutagen";
|
|
3588
3531
|
console.error();
|
|
3589
|
-
console.error(
|
|
3532
|
+
console.error(chalk8.red(` ${message}`));
|
|
3590
3533
|
console.error();
|
|
3591
3534
|
}
|
|
3592
3535
|
process.exit(1);
|
|
@@ -3594,8 +3537,8 @@ var internalInstallMutagenCommand = new Command9("internal-install-mutagen").opt
|
|
|
3594
3537
|
});
|
|
3595
3538
|
|
|
3596
3539
|
// src/commands/login.ts
|
|
3597
|
-
import { Command as
|
|
3598
|
-
import
|
|
3540
|
+
import { Command as Command11 } from "commander";
|
|
3541
|
+
import chalk9 from "chalk";
|
|
3599
3542
|
import ora8 from "ora";
|
|
3600
3543
|
|
|
3601
3544
|
// src/lib/browser-login.ts
|
|
@@ -3863,12 +3806,12 @@ function escapeHTML(value) {
|
|
|
3863
3806
|
}
|
|
3864
3807
|
|
|
3865
3808
|
// src/commands/login.ts
|
|
3866
|
-
var loginCommand = new
|
|
3809
|
+
var loginCommand = new Command11("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
|
|
3867
3810
|
const existingKey = getStoredAPIKey();
|
|
3868
3811
|
if (existingKey && !options.force) {
|
|
3869
3812
|
console.log();
|
|
3870
3813
|
console.log(
|
|
3871
|
-
|
|
3814
|
+
chalk9.yellow(" Already logged in. Use --force to overwrite.")
|
|
3872
3815
|
);
|
|
3873
3816
|
console.log();
|
|
3874
3817
|
return;
|
|
@@ -3877,8 +3820,8 @@ var loginCommand = new Command10("login").description("Authenticate the CLI").op
|
|
|
3877
3820
|
const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
|
|
3878
3821
|
if (!apiKey && wantsManualLogin) {
|
|
3879
3822
|
console.log();
|
|
3880
|
-
console.log(
|
|
3881
|
-
console.log(
|
|
3823
|
+
console.log(chalk9.dim(" Usage: computer login --api-key <ac_live_...>"));
|
|
3824
|
+
console.log(chalk9.dim(` API: ${getBaseURL()}`));
|
|
3882
3825
|
console.log();
|
|
3883
3826
|
process.exit(1);
|
|
3884
3827
|
}
|
|
@@ -3888,7 +3831,7 @@ var loginCommand = new Command10("login").description("Authenticate the CLI").op
|
|
|
3888
3831
|
}
|
|
3889
3832
|
if (!apiKey.startsWith("ac_live_")) {
|
|
3890
3833
|
console.log();
|
|
3891
|
-
console.log(
|
|
3834
|
+
console.log(chalk9.red(" API key must start with ac_live_"));
|
|
3892
3835
|
console.log();
|
|
3893
3836
|
process.exit(1);
|
|
3894
3837
|
}
|
|
@@ -3896,7 +3839,7 @@ var loginCommand = new Command10("login").description("Authenticate the CLI").op
|
|
|
3896
3839
|
try {
|
|
3897
3840
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
3898
3841
|
setAPIKey(apiKey);
|
|
3899
|
-
spinner.succeed(`Logged in as ${
|
|
3842
|
+
spinner.succeed(`Logged in as ${chalk9.bold(me.user.email)}`);
|
|
3900
3843
|
} catch (error) {
|
|
3901
3844
|
spinner.fail(
|
|
3902
3845
|
error instanceof Error ? error.message : "Failed to validate API key"
|
|
@@ -3916,15 +3859,15 @@ async function runBrowserLogin() {
|
|
|
3916
3859
|
spinner.stop();
|
|
3917
3860
|
console.log();
|
|
3918
3861
|
console.log(
|
|
3919
|
-
|
|
3862
|
+
chalk9.yellow(" Browser auto-open failed. Open this URL to continue:")
|
|
3920
3863
|
);
|
|
3921
|
-
console.log(
|
|
3864
|
+
console.log(chalk9.dim(` ${attempt.loginURL}`));
|
|
3922
3865
|
console.log();
|
|
3923
3866
|
spinner.start("Waiting for browser login...");
|
|
3924
3867
|
}
|
|
3925
3868
|
spinner.text = "Waiting for browser login...";
|
|
3926
3869
|
const result = await attempt.waitForResult();
|
|
3927
|
-
spinner.succeed(`Logged in as ${
|
|
3870
|
+
spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
|
|
3928
3871
|
await continueFirstLoginFlow(result);
|
|
3929
3872
|
} catch (error) {
|
|
3930
3873
|
spinner.fail(
|
|
@@ -3958,8 +3901,8 @@ async function continueFirstLoginFlow(result) {
|
|
|
3958
3901
|
}
|
|
3959
3902
|
console.log();
|
|
3960
3903
|
console.log(
|
|
3961
|
-
|
|
3962
|
-
`Continuing first-time setup for ${
|
|
3904
|
+
chalk9.cyan(
|
|
3905
|
+
`Continuing first-time setup for ${chalk9.bold(machineHandle)}...
|
|
3963
3906
|
`
|
|
3964
3907
|
)
|
|
3965
3908
|
);
|
|
@@ -3972,8 +3915,8 @@ async function continueFirstLoginFlow(result) {
|
|
|
3972
3915
|
const spinner = ora8(`Preparing SSH access for ${machineHandle}...`).start();
|
|
3973
3916
|
try {
|
|
3974
3917
|
const connection = await prepareSSHConnectionByIdentifier(machineHandle);
|
|
3975
|
-
spinner.succeed(`Connecting to ${
|
|
3976
|
-
console.log(
|
|
3918
|
+
spinner.succeed(`Connecting to ${chalk9.bold(machineHandle)}`);
|
|
3919
|
+
console.log(chalk9.dim(` ${connection.command}`));
|
|
3977
3920
|
console.log();
|
|
3978
3921
|
await openSSHConnection(connection);
|
|
3979
3922
|
} catch (error) {
|
|
@@ -3984,19 +3927,19 @@ async function continueFirstLoginFlow(result) {
|
|
|
3984
3927
|
}
|
|
3985
3928
|
} catch (error) {
|
|
3986
3929
|
const message = error instanceof Error ? error.message : "Failed to finish first-time setup";
|
|
3987
|
-
console.error(
|
|
3930
|
+
console.error(chalk9.red(`
|
|
3988
3931
|
${message}`));
|
|
3989
3932
|
console.log();
|
|
3990
3933
|
if (result.provider === "claude") {
|
|
3991
3934
|
console.log(
|
|
3992
|
-
|
|
3935
|
+
chalk9.dim(` computer claude-login --machine ${machineHandle}`)
|
|
3993
3936
|
);
|
|
3994
3937
|
} else if (result.provider === "codex") {
|
|
3995
3938
|
console.log(
|
|
3996
|
-
|
|
3939
|
+
chalk9.dim(` computer codex-login --machine ${machineHandle}`)
|
|
3997
3940
|
);
|
|
3998
3941
|
}
|
|
3999
|
-
console.log(
|
|
3942
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4000
3943
|
console.log();
|
|
4001
3944
|
process.exit(1);
|
|
4002
3945
|
}
|
|
@@ -4010,23 +3953,23 @@ async function runSelectedProvider(provider, machineHandle) {
|
|
|
4010
3953
|
await runCodexLogin({ machine: machineHandle });
|
|
4011
3954
|
return;
|
|
4012
3955
|
}
|
|
4013
|
-
console.log(
|
|
3956
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
4014
3957
|
console.log();
|
|
4015
3958
|
}
|
|
4016
3959
|
function printNextStep(machineHandle) {
|
|
4017
|
-
console.log(
|
|
4018
|
-
console.log(
|
|
3960
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
3961
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4019
3962
|
console.log();
|
|
4020
3963
|
}
|
|
4021
3964
|
|
|
4022
3965
|
// src/commands/mount.ts
|
|
4023
|
-
import { spawn as
|
|
4024
|
-
import { Command as
|
|
4025
|
-
import
|
|
3966
|
+
import { spawn as spawn3 } from "child_process";
|
|
3967
|
+
import { Command as Command12, Option } from "commander";
|
|
3968
|
+
import chalk10 from "chalk";
|
|
4026
3969
|
import ora9 from "ora";
|
|
4027
3970
|
|
|
4028
3971
|
// src/lib/mount-daemon.ts
|
|
4029
|
-
import { mkdir
|
|
3972
|
+
import { mkdir } from "fs/promises";
|
|
4030
3973
|
function getMountControllerState(rootPath = defaultMountServiceConfig().rootPath) {
|
|
4031
3974
|
const lock = readMountControllerLock(rootPath);
|
|
4032
3975
|
if (!lock) {
|
|
@@ -4042,7 +3985,7 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4042
3985
|
const { onReady, onStarted } = options;
|
|
4043
3986
|
const paths = getMountPaths(config.rootPath);
|
|
4044
3987
|
ensureMountDirectories(paths);
|
|
4045
|
-
await
|
|
3988
|
+
await mkdir(paths.rootPath, { recursive: true });
|
|
4046
3989
|
await acquireControllerLock(config.rootPath);
|
|
4047
3990
|
writeMountStatusSnapshot(
|
|
4048
3991
|
{
|
|
@@ -4072,10 +4015,11 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4072
4015
|
config.rootPath
|
|
4073
4016
|
);
|
|
4074
4017
|
await teardownManagedSessions(config, paths);
|
|
4075
|
-
await
|
|
4018
|
+
await mkdir(paths.rootPath, { recursive: true });
|
|
4076
4019
|
let running = false;
|
|
4077
4020
|
let queued = false;
|
|
4078
4021
|
let shuttingDown = false;
|
|
4022
|
+
let readyNotified = false;
|
|
4079
4023
|
let activeRun = null;
|
|
4080
4024
|
let activeRunController = null;
|
|
4081
4025
|
const runOnce = async () => {
|
|
@@ -4097,6 +4041,11 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4097
4041
|
process.pid,
|
|
4098
4042
|
controller.signal
|
|
4099
4043
|
);
|
|
4044
|
+
const snapshot = readMountStatusSnapshot(config.rootPath);
|
|
4045
|
+
if (!readyNotified && snapshot?.startupPhase === "ready") {
|
|
4046
|
+
readyNotified = true;
|
|
4047
|
+
onReady?.();
|
|
4048
|
+
}
|
|
4100
4049
|
} catch (error) {
|
|
4101
4050
|
if (shuttingDown && isAbortError(error)) {
|
|
4102
4051
|
return;
|
|
@@ -4198,8 +4147,6 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4198
4147
|
void shutdown();
|
|
4199
4148
|
});
|
|
4200
4149
|
await runOnce();
|
|
4201
|
-
writeMountStatusSnapshot(markMountStartupReady(readMountStatusSnapshot(config.rootPath), process.pid), config.rootPath);
|
|
4202
|
-
onReady?.();
|
|
4203
4150
|
await new Promise(() => {
|
|
4204
4151
|
});
|
|
4205
4152
|
}
|
|
@@ -4228,28 +4175,13 @@ function processExists(pid) {
|
|
|
4228
4175
|
return false;
|
|
4229
4176
|
}
|
|
4230
4177
|
}
|
|
4231
|
-
function markMountStartupReady(snapshot, controllerPid) {
|
|
4232
|
-
return {
|
|
4233
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4234
|
-
controllerPid,
|
|
4235
|
-
running: true,
|
|
4236
|
-
startupPhase: "ready",
|
|
4237
|
-
startupMessage: void 0,
|
|
4238
|
-
lastHealthySyncAt: snapshot?.lastHealthySyncAt,
|
|
4239
|
-
lastSuccessfulSyncAt: snapshot?.lastSuccessfulSyncAt,
|
|
4240
|
-
lastIssueAt: snapshot?.lastIssueAt,
|
|
4241
|
-
lastIssue: snapshot?.lastIssue,
|
|
4242
|
-
lastError: snapshot?.lastError,
|
|
4243
|
-
mounts: snapshot?.mounts ?? []
|
|
4244
|
-
};
|
|
4245
|
-
}
|
|
4246
4178
|
|
|
4247
4179
|
// src/commands/mount.ts
|
|
4248
4180
|
var MOUNT_START_SPINNER = {
|
|
4249
4181
|
interval: 90,
|
|
4250
4182
|
frames: ["\u25F0", "\u25F3", "\u25F2", "\u25F1"]
|
|
4251
4183
|
};
|
|
4252
|
-
var mountCommand = new
|
|
4184
|
+
var mountCommand = new Command12("mount").description("Mirror SSH-ready machines under ~/agentcomputer with a local mount controller").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").option("--poll-interval <ms>", "Reconcile interval in milliseconds", "5000").option("--connect-timeout <seconds>", "SSH connect timeout for Mutagen", "5").option(
|
|
4253
4185
|
"--background",
|
|
4254
4186
|
"Run the mount controller in the background and print its PID"
|
|
4255
4187
|
).addOption(new Option("--daemonized").hideHelp()).addOption(new Option("--open-root-when-ready").hideHelp()).action(async (options) => {
|
|
@@ -4283,14 +4215,22 @@ var mountCommand = new Command11("mount").description("Mirror SSH-ready machines
|
|
|
4283
4215
|
return;
|
|
4284
4216
|
}
|
|
4285
4217
|
const child = await startMountControllerInForeground(resolved.config.rootPath);
|
|
4286
|
-
|
|
4218
|
+
const controls = attachForegroundControls(child);
|
|
4287
4219
|
spinner.succeed("Machine mount controller running");
|
|
4288
4220
|
printMountStartSummary(
|
|
4289
4221
|
resolved.config,
|
|
4290
4222
|
child.pid ?? process.pid,
|
|
4291
4223
|
"foreground"
|
|
4292
4224
|
);
|
|
4293
|
-
|
|
4225
|
+
try {
|
|
4226
|
+
await superviseForegroundMountController(
|
|
4227
|
+
resolved.config.rootPath,
|
|
4228
|
+
child,
|
|
4229
|
+
controls
|
|
4230
|
+
);
|
|
4231
|
+
} finally {
|
|
4232
|
+
controls.close();
|
|
4233
|
+
}
|
|
4294
4234
|
} catch (error) {
|
|
4295
4235
|
spinner.fail(
|
|
4296
4236
|
error instanceof Error ? error.message : "Failed to start machine mount controller"
|
|
@@ -4298,42 +4238,9 @@ var mountCommand = new Command11("mount").description("Mirror SSH-ready machines
|
|
|
4298
4238
|
process.exit(1);
|
|
4299
4239
|
}
|
|
4300
4240
|
}).addCommand(
|
|
4301
|
-
new
|
|
4241
|
+
new Command12("status").description("Show machine mount controller status").action(() => {
|
|
4302
4242
|
const config = readMountConfig() ?? defaultMountServiceConfig();
|
|
4303
|
-
|
|
4304
|
-
const snapshot = readMountStatusSnapshot(config.rootPath);
|
|
4305
|
-
console.log();
|
|
4306
|
-
console.log(` ${chalk9.bold("Machine Mounts")}`);
|
|
4307
|
-
console.log();
|
|
4308
|
-
console.log(
|
|
4309
|
-
` ${chalk9.dim("Running")} ${controller.running ? chalk9.green("yes") : chalk9.dim("no")}`
|
|
4310
|
-
);
|
|
4311
|
-
if (controller.pid) {
|
|
4312
|
-
console.log(` ${chalk9.dim("PID")} ${controller.pid}`);
|
|
4313
|
-
}
|
|
4314
|
-
console.log(` ${chalk9.dim("Root")} ${config.rootPath}`);
|
|
4315
|
-
console.log(` ${chalk9.dim("Alias")} ${config.alias}`);
|
|
4316
|
-
console.log(
|
|
4317
|
-
` ${chalk9.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk9.dim("never")}`
|
|
4318
|
-
);
|
|
4319
|
-
console.log(
|
|
4320
|
-
` ${chalk9.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk9.dim("never")}`
|
|
4321
|
-
);
|
|
4322
|
-
if (controller.running && snapshot?.mounts.length) {
|
|
4323
|
-
console.log();
|
|
4324
|
-
for (const mount of snapshot.mounts) {
|
|
4325
|
-
const state = formatMountState(mount.state);
|
|
4326
|
-
console.log(` ${chalk9.white(mount.handle)} ${state} ${chalk9.dim(mount.mountPath)}`);
|
|
4327
|
-
if (mount.message) {
|
|
4328
|
-
console.log(` ${chalk9.dim(mount.message)}`);
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
|
-
}
|
|
4332
|
-
if (snapshot?.lastIssue) {
|
|
4333
|
-
console.log();
|
|
4334
|
-
console.log(` ${chalk9.dim("Last issue")} ${chalk9.yellow(snapshot.lastIssue)}`);
|
|
4335
|
-
}
|
|
4336
|
-
console.log();
|
|
4243
|
+
renderMountStatus(config);
|
|
4337
4244
|
})
|
|
4338
4245
|
);
|
|
4339
4246
|
function parsePositiveInt(raw, label) {
|
|
@@ -4377,7 +4284,7 @@ async function startMountControllerInBackground(rootPath) {
|
|
|
4377
4284
|
return child.pid;
|
|
4378
4285
|
}
|
|
4379
4286
|
async function startMountControllerInForeground(rootPath) {
|
|
4380
|
-
const child = startMountControllerProcess(rootPath,
|
|
4287
|
+
const child = startMountControllerProcess(rootPath, true, true);
|
|
4381
4288
|
await waitForMountControllerRunning(rootPath, child.pid);
|
|
4382
4289
|
return child;
|
|
4383
4290
|
}
|
|
@@ -4394,7 +4301,7 @@ function startMountControllerProcess(rootPath, detached, openRootWhenReady) {
|
|
|
4394
4301
|
if (openRootWhenReady) {
|
|
4395
4302
|
args.push("--open-root-when-ready");
|
|
4396
4303
|
}
|
|
4397
|
-
const child =
|
|
4304
|
+
const child = spawn3(process.execPath, [entrypoint, ...args], {
|
|
4398
4305
|
cwd: process.cwd(),
|
|
4399
4306
|
detached,
|
|
4400
4307
|
env: process.env,
|
|
@@ -4419,45 +4326,30 @@ async function waitForMountControllerRunning(rootPath, pid) {
|
|
|
4419
4326
|
}
|
|
4420
4327
|
throw new Error("failed to start machine mount controller in background");
|
|
4421
4328
|
}
|
|
4422
|
-
async function
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
spinner.text = snapshot.startupMessage;
|
|
4429
|
-
}
|
|
4430
|
-
if (snapshot?.controllerPid === pid && snapshot.startupPhase === "ready") {
|
|
4431
|
-
return;
|
|
4432
|
-
}
|
|
4433
|
-
if (!controller.running && !processExists2(pid)) {
|
|
4434
|
-
break;
|
|
4435
|
-
}
|
|
4436
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4437
|
-
}
|
|
4438
|
-
throw new Error("mount controller did not finish startup");
|
|
4439
|
-
}
|
|
4440
|
-
async function superviseForegroundMountController(child) {
|
|
4329
|
+
async function superviseForegroundMountController(rootPath, child, controls) {
|
|
4330
|
+
let lifecycleState = createMountLifecycleState();
|
|
4331
|
+
lifecycleState = updateMountLifecycleDisplay(
|
|
4332
|
+
lifecycleState,
|
|
4333
|
+
readMountStatusSnapshot(rootPath)
|
|
4334
|
+
);
|
|
4441
4335
|
await new Promise((resolve, reject) => {
|
|
4442
4336
|
const cleanup = () => {
|
|
4443
|
-
process.off("SIGINT", onSigint);
|
|
4444
|
-
process.off("SIGTERM", onSigterm);
|
|
4445
4337
|
child.off("error", onError);
|
|
4446
4338
|
child.off("exit", onExit);
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
child.kill(signal);
|
|
4452
|
-
} catch {
|
|
4339
|
+
clearInterval(interval);
|
|
4340
|
+
if (lifecycleState.spinner) {
|
|
4341
|
+
lifecycleState.spinner.stop();
|
|
4342
|
+
lifecycleState.spinner = null;
|
|
4453
4343
|
}
|
|
4454
|
-
process.exit(0);
|
|
4455
|
-
};
|
|
4456
|
-
const onSigint = () => {
|
|
4457
|
-
stopAndExit("SIGINT");
|
|
4458
4344
|
};
|
|
4459
|
-
const
|
|
4460
|
-
|
|
4345
|
+
const backgroundAndExit = () => {
|
|
4346
|
+
cleanup();
|
|
4347
|
+
child.unref();
|
|
4348
|
+
console.log();
|
|
4349
|
+
console.log(chalk10.dim(` Backgrounded mount controller (pid ${child.pid ?? "unknown"}).`));
|
|
4350
|
+
console.log(chalk10.dim(" Use `computer mount status` to inspect sync state."));
|
|
4351
|
+
console.log();
|
|
4352
|
+
resolve();
|
|
4461
4353
|
};
|
|
4462
4354
|
const onError = (error) => {
|
|
4463
4355
|
cleanup();
|
|
@@ -4475,26 +4367,367 @@ async function superviseForegroundMountController(child) {
|
|
|
4475
4367
|
)
|
|
4476
4368
|
);
|
|
4477
4369
|
};
|
|
4478
|
-
|
|
4479
|
-
|
|
4370
|
+
const interval = setInterval(() => {
|
|
4371
|
+
const action = controls.consumeAction();
|
|
4372
|
+
if (action === "background") {
|
|
4373
|
+
backgroundAndExit();
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
if (action === "stop") {
|
|
4377
|
+
cleanup();
|
|
4378
|
+
resolve();
|
|
4379
|
+
return;
|
|
4380
|
+
}
|
|
4381
|
+
lifecycleState = updateMountLifecycleDisplay(
|
|
4382
|
+
lifecycleState,
|
|
4383
|
+
readMountStatusSnapshot(rootPath)
|
|
4384
|
+
);
|
|
4385
|
+
}, 1e3);
|
|
4480
4386
|
child.once("error", onError);
|
|
4481
4387
|
child.once("exit", onExit);
|
|
4482
4388
|
});
|
|
4483
4389
|
}
|
|
4484
4390
|
function printMountStartSummary(config, pid, mode) {
|
|
4485
4391
|
console.log();
|
|
4486
|
-
console.log(
|
|
4487
|
-
console.log(
|
|
4488
|
-
console.log(
|
|
4489
|
-
console.log(
|
|
4392
|
+
console.log(chalk10.dim(` PID: ${pid}`));
|
|
4393
|
+
console.log(chalk10.dim(` Root: ${config.rootPath}`));
|
|
4394
|
+
console.log(chalk10.dim(` SSH alias: ${config.alias}`));
|
|
4395
|
+
console.log(chalk10.dim(` Poll: ${config.pollIntervalMs}ms`));
|
|
4490
4396
|
console.log();
|
|
4491
4397
|
console.log(
|
|
4492
|
-
|
|
4493
|
-
mode === "background" ? " Use `computer mount status` to inspect sync state." : " Press Ctrl-C to stop syncing."
|
|
4398
|
+
chalk10.dim(
|
|
4399
|
+
mode === "background" ? " Use `computer mount status` to inspect sync state." : " Press Ctrl-C to stop syncing or Ctrl-B to move it to the background."
|
|
4494
4400
|
)
|
|
4495
4401
|
);
|
|
4496
4402
|
console.log();
|
|
4497
4403
|
}
|
|
4404
|
+
function renderMountStatus(config) {
|
|
4405
|
+
const controller = getMountControllerState(config.rootPath);
|
|
4406
|
+
const snapshot = readMountStatusSnapshot(config.rootPath);
|
|
4407
|
+
console.log();
|
|
4408
|
+
console.log(` ${chalk10.bold("Machine Mounts")}`);
|
|
4409
|
+
console.log();
|
|
4410
|
+
console.log(
|
|
4411
|
+
` ${chalk10.dim("Running")} ${controller.running ? chalk10.green("yes") : chalk10.dim("no")}`
|
|
4412
|
+
);
|
|
4413
|
+
if (controller.pid) {
|
|
4414
|
+
console.log(` ${chalk10.dim("PID")} ${controller.pid}`);
|
|
4415
|
+
}
|
|
4416
|
+
console.log(` ${chalk10.dim("Root")} ${config.rootPath}`);
|
|
4417
|
+
console.log(` ${chalk10.dim("Alias")} ${config.alias}`);
|
|
4418
|
+
console.log(
|
|
4419
|
+
` ${chalk10.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk10.dim("never")}`
|
|
4420
|
+
);
|
|
4421
|
+
console.log(
|
|
4422
|
+
` ${chalk10.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk10.dim("never")}`
|
|
4423
|
+
);
|
|
4424
|
+
if (snapshot?.startupPhase && snapshot.startupPhase !== "ready") {
|
|
4425
|
+
console.log(` ${chalk10.dim("Startup")} ${chalk10.cyan(snapshot.startupPhase)}`);
|
|
4426
|
+
if (snapshot.startupMessage) {
|
|
4427
|
+
console.log(` ${chalk10.dim(snapshot.startupMessage)}`);
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
if (controller.running && snapshot?.mounts.length) {
|
|
4431
|
+
console.log();
|
|
4432
|
+
for (const mount of snapshot.mounts) {
|
|
4433
|
+
renderMountSnapshot(mount);
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
if (snapshot?.lastIssue) {
|
|
4437
|
+
console.log();
|
|
4438
|
+
console.log(` ${chalk10.dim("Last issue")} ${chalk10.yellow(snapshot.lastIssue)}`);
|
|
4439
|
+
}
|
|
4440
|
+
console.log();
|
|
4441
|
+
}
|
|
4442
|
+
function renderMountSnapshot(mount) {
|
|
4443
|
+
const state = formatMountState(mount.state);
|
|
4444
|
+
console.log(` ${chalk10.white(mount.handle)} ${state} ${chalk10.dim(mount.mountPath)}`);
|
|
4445
|
+
const details = formatMountDetails(mount);
|
|
4446
|
+
if (details) {
|
|
4447
|
+
console.log(` ${chalk10.dim(details)}`);
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
function formatMountDetails(mount) {
|
|
4451
|
+
const details = [];
|
|
4452
|
+
if (mount.message) {
|
|
4453
|
+
details.push(mount.message);
|
|
4454
|
+
}
|
|
4455
|
+
if (mount.etaSeconds !== void 0 && !details.some((detail) => detail.includes("eta ~"))) {
|
|
4456
|
+
details.push(`eta ~${formatEtaSeconds(mount.etaSeconds)}`);
|
|
4457
|
+
}
|
|
4458
|
+
return details.length > 0 ? details.join("; ") : void 0;
|
|
4459
|
+
}
|
|
4460
|
+
function formatEtaSeconds(seconds) {
|
|
4461
|
+
if (seconds < 60) {
|
|
4462
|
+
return `${seconds}s`;
|
|
4463
|
+
}
|
|
4464
|
+
const minutes = Math.floor(seconds / 60);
|
|
4465
|
+
const remainder = seconds % 60;
|
|
4466
|
+
return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
|
|
4467
|
+
}
|
|
4468
|
+
function attachForegroundControls(child) {
|
|
4469
|
+
let pendingAction = null;
|
|
4470
|
+
const canUseRawMode = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
4471
|
+
const requestStop = () => {
|
|
4472
|
+
if (!pendingAction) {
|
|
4473
|
+
pendingAction = "stop";
|
|
4474
|
+
}
|
|
4475
|
+
try {
|
|
4476
|
+
if (child.pid) {
|
|
4477
|
+
process.kill(child.pid, "SIGTERM");
|
|
4478
|
+
}
|
|
4479
|
+
} catch {
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
const onData = (chunk) => {
|
|
4483
|
+
const text = chunk.toString();
|
|
4484
|
+
for (const character of text) {
|
|
4485
|
+
if (character === "") {
|
|
4486
|
+
if (!pendingAction) {
|
|
4487
|
+
pendingAction = "background";
|
|
4488
|
+
}
|
|
4489
|
+
} else if (character === "") {
|
|
4490
|
+
requestStop();
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
};
|
|
4494
|
+
const onSigint = () => {
|
|
4495
|
+
requestStop();
|
|
4496
|
+
};
|
|
4497
|
+
const onSigterm = () => {
|
|
4498
|
+
requestStop();
|
|
4499
|
+
};
|
|
4500
|
+
if (canUseRawMode) {
|
|
4501
|
+
process.stdin.setRawMode(true);
|
|
4502
|
+
process.stdin.resume();
|
|
4503
|
+
process.stdin.on("data", onData);
|
|
4504
|
+
}
|
|
4505
|
+
process.on("SIGINT", onSigint);
|
|
4506
|
+
process.on("SIGTERM", onSigterm);
|
|
4507
|
+
return {
|
|
4508
|
+
consumeAction() {
|
|
4509
|
+
const action = pendingAction;
|
|
4510
|
+
pendingAction = null;
|
|
4511
|
+
return action;
|
|
4512
|
+
},
|
|
4513
|
+
close() {
|
|
4514
|
+
if (canUseRawMode) {
|
|
4515
|
+
process.stdin.off("data", onData);
|
|
4516
|
+
process.stdin.setRawMode(false);
|
|
4517
|
+
process.stdin.pause();
|
|
4518
|
+
}
|
|
4519
|
+
process.off("SIGINT", onSigint);
|
|
4520
|
+
process.off("SIGTERM", onSigterm);
|
|
4521
|
+
}
|
|
4522
|
+
};
|
|
4523
|
+
}
|
|
4524
|
+
function createMountLifecycleState() {
|
|
4525
|
+
return {
|
|
4526
|
+
view: void 0,
|
|
4527
|
+
spinner: null
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
function updateMountLifecycleDisplay(state, snapshot) {
|
|
4531
|
+
const nextView = describeMountLifecycle(snapshot);
|
|
4532
|
+
const previousView = state.view;
|
|
4533
|
+
if (previousView?.key === nextView.key) {
|
|
4534
|
+
if (state.spinner) {
|
|
4535
|
+
state.spinner.text = nextView.text;
|
|
4536
|
+
}
|
|
4537
|
+
return {
|
|
4538
|
+
...state,
|
|
4539
|
+
view: nextView
|
|
4540
|
+
};
|
|
4541
|
+
}
|
|
4542
|
+
if (state.spinner) {
|
|
4543
|
+
if (nextView.status === "success") {
|
|
4544
|
+
state.spinner.succeed(nextView.text);
|
|
4545
|
+
return {
|
|
4546
|
+
view: nextView,
|
|
4547
|
+
spinner: null
|
|
4548
|
+
};
|
|
4549
|
+
}
|
|
4550
|
+
if (nextView.status === "warning") {
|
|
4551
|
+
state.spinner.warn(nextView.text);
|
|
4552
|
+
return {
|
|
4553
|
+
view: nextView,
|
|
4554
|
+
spinner: null
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
state.spinner.succeed(previousView?.completionText ?? previousView?.text ?? "");
|
|
4558
|
+
}
|
|
4559
|
+
if (nextView.status === "loading") {
|
|
4560
|
+
return {
|
|
4561
|
+
view: nextView,
|
|
4562
|
+
spinner: ora9({
|
|
4563
|
+
spinner: MOUNT_START_SPINNER,
|
|
4564
|
+
text: nextView.text
|
|
4565
|
+
}).start()
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
console.log(chalk10.dim(` ${nextView.text}`));
|
|
4569
|
+
return {
|
|
4570
|
+
view: nextView,
|
|
4571
|
+
spinner: null
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
function describeMountLifecycle(snapshot) {
|
|
4575
|
+
if (!snapshot) {
|
|
4576
|
+
return {
|
|
4577
|
+
key: "waiting",
|
|
4578
|
+
text: "Waiting for mount controller state",
|
|
4579
|
+
completionText: "Mount controller state received",
|
|
4580
|
+
status: "loading"
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
const actionableMounts = (snapshot?.mounts ?? []).filter((mount) => mount.state !== "pending");
|
|
4584
|
+
const readyCount = actionableMounts.filter((mount) => mount.state === "mounted").length;
|
|
4585
|
+
const totalCount = actionableMounts.length;
|
|
4586
|
+
if (snapshot?.lastIssue && actionableMounts.some((mount) => mount.state === "error" || mount.state === "degraded")) {
|
|
4587
|
+
return {
|
|
4588
|
+
key: "issue",
|
|
4589
|
+
text: `Needs attention: ${snapshot.lastIssue}`,
|
|
4590
|
+
status: "warning"
|
|
4591
|
+
};
|
|
4592
|
+
}
|
|
4593
|
+
if (snapshot?.startupPhase === "cleaning") {
|
|
4594
|
+
return {
|
|
4595
|
+
key: "cleaning",
|
|
4596
|
+
text: "Cleaning stale mount state",
|
|
4597
|
+
completionText: "Cleaned stale mount state",
|
|
4598
|
+
status: "loading"
|
|
4599
|
+
};
|
|
4600
|
+
}
|
|
4601
|
+
if (actionableMounts.some((mount) => mount.state === "reconnecting")) {
|
|
4602
|
+
return {
|
|
4603
|
+
key: "waiting",
|
|
4604
|
+
text: formatLifecycleText(
|
|
4605
|
+
"Waiting for remote availability",
|
|
4606
|
+
readyCount,
|
|
4607
|
+
totalCount
|
|
4608
|
+
),
|
|
4609
|
+
completionText: "Remote availability restored",
|
|
4610
|
+
status: "loading"
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
4613
|
+
const phaseMatch = findLifecyclePhaseMatch(actionableMounts);
|
|
4614
|
+
if (phaseMatch) {
|
|
4615
|
+
return phaseMatch;
|
|
4616
|
+
}
|
|
4617
|
+
if (snapshot?.startupPhase !== "ready") {
|
|
4618
|
+
return {
|
|
4619
|
+
key: "waiting",
|
|
4620
|
+
text: formatLifecycleText("Waiting for initial sync", readyCount, totalCount),
|
|
4621
|
+
completionText: "Initial sync started",
|
|
4622
|
+
status: "loading"
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
if (totalCount === 0) {
|
|
4626
|
+
return {
|
|
4627
|
+
key: "connected",
|
|
4628
|
+
text: "Connected and waiting for SSH-ready machines",
|
|
4629
|
+
status: "success"
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
return {
|
|
4633
|
+
key: "connected",
|
|
4634
|
+
text: "Connected and serving files bidirectionally",
|
|
4635
|
+
status: "success"
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
function findLifecyclePhaseMatch(mounts) {
|
|
4639
|
+
const phaseDefinitions = [
|
|
4640
|
+
{
|
|
4641
|
+
key: "scanning",
|
|
4642
|
+
label: "Scanning files",
|
|
4643
|
+
completionText: "Finished scanning files",
|
|
4644
|
+
match: (mount) => (mount.status ?? "").toLowerCase().includes("scanning")
|
|
4645
|
+
},
|
|
4646
|
+
{
|
|
4647
|
+
key: "staging",
|
|
4648
|
+
label: "Staging files",
|
|
4649
|
+
completionText: "Finished staging files",
|
|
4650
|
+
match: (mount) => (mount.status ?? "").toLowerCase().includes("staging")
|
|
4651
|
+
},
|
|
4652
|
+
{
|
|
4653
|
+
key: "reconciling",
|
|
4654
|
+
label: "Reconciling changes",
|
|
4655
|
+
completionText: "Reconciled changes",
|
|
4656
|
+
match: (mount) => (mount.status ?? "").toLowerCase().includes("reconciling")
|
|
4657
|
+
},
|
|
4658
|
+
{
|
|
4659
|
+
key: "applying",
|
|
4660
|
+
label: "Applying changes",
|
|
4661
|
+
completionText: "Applied changes",
|
|
4662
|
+
match: (mount) => (mount.status ?? "").toLowerCase().includes("applying")
|
|
4663
|
+
},
|
|
4664
|
+
{
|
|
4665
|
+
key: "saving",
|
|
4666
|
+
label: "Finalizing sync",
|
|
4667
|
+
completionText: "Finalized sync state",
|
|
4668
|
+
match: (mount) => (mount.status ?? "").toLowerCase().includes("saving")
|
|
4669
|
+
}
|
|
4670
|
+
];
|
|
4671
|
+
const readyCount = mounts.filter((mount) => mount.state === "mounted").length;
|
|
4672
|
+
const totalCount = mounts.length;
|
|
4673
|
+
for (const phase of phaseDefinitions) {
|
|
4674
|
+
const matches = mounts.filter(
|
|
4675
|
+
(mount) => mount.state === "syncing" && phase.match(mount)
|
|
4676
|
+
);
|
|
4677
|
+
if (matches.length === 0) {
|
|
4678
|
+
continue;
|
|
4679
|
+
}
|
|
4680
|
+
const representative = selectRepresentativeMount(matches);
|
|
4681
|
+
const progress = formatLifecycleProgress(representative);
|
|
4682
|
+
return {
|
|
4683
|
+
key: phase.key,
|
|
4684
|
+
text: formatLifecycleText(phase.label, readyCount, totalCount, progress),
|
|
4685
|
+
completionText: phase.completionText,
|
|
4686
|
+
status: "loading"
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
return null;
|
|
4690
|
+
}
|
|
4691
|
+
function selectRepresentativeMount(mounts) {
|
|
4692
|
+
let best = mounts[0];
|
|
4693
|
+
for (const candidate of mounts.slice(1)) {
|
|
4694
|
+
const candidateProgress = getMountProgressPercent(candidate) ?? -1;
|
|
4695
|
+
const bestProgress = getMountProgressPercent(best) ?? -1;
|
|
4696
|
+
if (candidateProgress > bestProgress) {
|
|
4697
|
+
best = candidate;
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
return best;
|
|
4701
|
+
}
|
|
4702
|
+
function formatLifecycleText(label, readyCount, totalCount, progress) {
|
|
4703
|
+
const details = [];
|
|
4704
|
+
if (totalCount > 0) {
|
|
4705
|
+
details.push(`${readyCount}/${totalCount} ready`);
|
|
4706
|
+
}
|
|
4707
|
+
if (progress) {
|
|
4708
|
+
details.push(progress);
|
|
4709
|
+
}
|
|
4710
|
+
return details.length > 0 ? `${label} (${details.join(", ")})` : label;
|
|
4711
|
+
}
|
|
4712
|
+
function formatLifecycleProgress(mount) {
|
|
4713
|
+
const parts = [];
|
|
4714
|
+
const percent = getMountProgressPercent(mount);
|
|
4715
|
+
if (percent !== void 0) {
|
|
4716
|
+
parts.push(`${percent}%`);
|
|
4717
|
+
}
|
|
4718
|
+
if (mount.etaSeconds !== void 0) {
|
|
4719
|
+
parts.push(`eta ~${formatEtaSeconds(mount.etaSeconds)}`);
|
|
4720
|
+
}
|
|
4721
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
4722
|
+
}
|
|
4723
|
+
function getMountProgressPercent(mount) {
|
|
4724
|
+
const match = mount.progress?.match(/(\d+)%/);
|
|
4725
|
+
if (!match) {
|
|
4726
|
+
return void 0;
|
|
4727
|
+
}
|
|
4728
|
+
const percent = Number.parseInt(match[1] ?? "", 10);
|
|
4729
|
+
return Number.isFinite(percent) ? percent : void 0;
|
|
4730
|
+
}
|
|
4498
4731
|
function processExists2(pid) {
|
|
4499
4732
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
4500
4733
|
return false;
|
|
@@ -4510,7 +4743,7 @@ function revealMountRootInFinder(rootPath) {
|
|
|
4510
4743
|
if (process.platform !== "darwin") {
|
|
4511
4744
|
return;
|
|
4512
4745
|
}
|
|
4513
|
-
const child =
|
|
4746
|
+
const child = spawn3("open", [rootPath], {
|
|
4514
4747
|
stdio: "ignore",
|
|
4515
4748
|
detached: true
|
|
4516
4749
|
});
|
|
@@ -4521,35 +4754,36 @@ function revealMountRootInFinder(rootPath) {
|
|
|
4521
4754
|
function formatMountState(state) {
|
|
4522
4755
|
switch (state) {
|
|
4523
4756
|
case "mounted":
|
|
4524
|
-
return
|
|
4757
|
+
return chalk10.green(state);
|
|
4758
|
+
case "syncing":
|
|
4525
4759
|
case "reconnecting":
|
|
4526
|
-
return
|
|
4760
|
+
return chalk10.cyan(state);
|
|
4527
4761
|
case "degraded":
|
|
4528
4762
|
case "pending":
|
|
4529
|
-
return
|
|
4763
|
+
return chalk10.yellow(state);
|
|
4530
4764
|
default:
|
|
4531
|
-
return
|
|
4765
|
+
return chalk10.red(state);
|
|
4532
4766
|
}
|
|
4533
4767
|
}
|
|
4534
4768
|
|
|
4535
4769
|
// src/commands/logout.ts
|
|
4536
|
-
import { Command as
|
|
4537
|
-
import
|
|
4538
|
-
var logoutCommand = new
|
|
4770
|
+
import { Command as Command13 } from "commander";
|
|
4771
|
+
import chalk11 from "chalk";
|
|
4772
|
+
var logoutCommand = new Command13("logout").description("Remove stored API key").action(() => {
|
|
4539
4773
|
if (!getStoredAPIKey()) {
|
|
4540
4774
|
console.log();
|
|
4541
|
-
console.log(
|
|
4775
|
+
console.log(chalk11.dim(" Not logged in."));
|
|
4542
4776
|
if (hasEnvAPIKey()) {
|
|
4543
|
-
console.log(
|
|
4777
|
+
console.log(chalk11.dim(" Environment API key is still active in this shell."));
|
|
4544
4778
|
}
|
|
4545
4779
|
console.log();
|
|
4546
4780
|
return;
|
|
4547
4781
|
}
|
|
4548
4782
|
clearAPIKey();
|
|
4549
4783
|
console.log();
|
|
4550
|
-
console.log(
|
|
4784
|
+
console.log(chalk11.green(" Logged out."));
|
|
4551
4785
|
if (hasEnvAPIKey()) {
|
|
4552
|
-
console.log(
|
|
4786
|
+
console.log(chalk11.dim(" Environment API key is still active in this shell."));
|
|
4553
4787
|
}
|
|
4554
4788
|
console.log();
|
|
4555
4789
|
});
|
|
@@ -4557,27 +4791,12 @@ var logoutCommand = new Command12("logout").description("Remove stored API key")
|
|
|
4557
4791
|
// src/commands/upgrade.ts
|
|
4558
4792
|
import { spawnSync } from "child_process";
|
|
4559
4793
|
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
4560
|
-
import { Command as
|
|
4561
|
-
import
|
|
4794
|
+
import { Command as Command14 } from "commander";
|
|
4795
|
+
import chalk12 from "chalk";
|
|
4562
4796
|
import ora10 from "ora";
|
|
4563
4797
|
var pkg2 = JSON.parse(
|
|
4564
4798
|
readFileSync2(new URL("../package.json", import.meta.url), "utf8")
|
|
4565
4799
|
);
|
|
4566
|
-
function normalizeVersion(version) {
|
|
4567
|
-
return version.split("-")[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isNaN(part) ? 0 : part);
|
|
4568
|
-
}
|
|
4569
|
-
function compareVersions(a, b) {
|
|
4570
|
-
const left = normalizeVersion(a);
|
|
4571
|
-
const right = normalizeVersion(b);
|
|
4572
|
-
const size = Math.max(left.length, right.length);
|
|
4573
|
-
for (let index = 0; index < size; index += 1) {
|
|
4574
|
-
const diff = (left[index] ?? 0) - (right[index] ?? 0);
|
|
4575
|
-
if (diff !== 0) {
|
|
4576
|
-
return diff;
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
return 0;
|
|
4580
|
-
}
|
|
4581
4800
|
function resolveExecutablePath() {
|
|
4582
4801
|
const candidate = process.argv[1] || process.execPath;
|
|
4583
4802
|
try {
|
|
@@ -4625,8 +4844,9 @@ function findNixProfileElement(executablePath) {
|
|
|
4625
4844
|
}
|
|
4626
4845
|
return null;
|
|
4627
4846
|
}
|
|
4628
|
-
function resolveUpgradeCommand(method, executablePath) {
|
|
4847
|
+
function resolveUpgradeCommand(method, executablePath, targetVersion) {
|
|
4629
4848
|
const packageName = pkg2.name ?? "aicomputer";
|
|
4849
|
+
const packageSpec = `${packageName}@${targetVersion}`;
|
|
4630
4850
|
switch (method) {
|
|
4631
4851
|
case "nix": {
|
|
4632
4852
|
const element = findNixProfileElement(executablePath);
|
|
@@ -4644,34 +4864,32 @@ function resolveUpgradeCommand(method, executablePath) {
|
|
|
4644
4864
|
case "pnpm":
|
|
4645
4865
|
return {
|
|
4646
4866
|
command: "pnpm",
|
|
4647
|
-
args: ["add", "-g",
|
|
4648
|
-
label: `pnpm add -g ${
|
|
4867
|
+
args: ["add", "-g", packageSpec],
|
|
4868
|
+
label: `pnpm add -g ${packageSpec}`
|
|
4649
4869
|
};
|
|
4650
4870
|
case "yarn":
|
|
4651
4871
|
return {
|
|
4652
4872
|
command: "yarn",
|
|
4653
|
-
args: ["global", "add",
|
|
4654
|
-
label: `yarn global add ${
|
|
4873
|
+
args: ["global", "add", packageSpec],
|
|
4874
|
+
label: `yarn global add ${packageSpec}`
|
|
4655
4875
|
};
|
|
4656
4876
|
case "npm":
|
|
4657
4877
|
case "unknown":
|
|
4658
4878
|
return {
|
|
4659
4879
|
command: "npm",
|
|
4660
|
-
args: ["install", "-g",
|
|
4661
|
-
label: `npm install -g ${
|
|
4880
|
+
args: ["install", "-g", packageSpec],
|
|
4881
|
+
label: `npm install -g ${packageSpec}`
|
|
4662
4882
|
};
|
|
4663
4883
|
}
|
|
4664
4884
|
}
|
|
4665
4885
|
async function getLatestVersion(packageName) {
|
|
4666
|
-
const response = await fetch(`https://registry.npmjs.org/${packageName}
|
|
4886
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
|
4667
4887
|
if (!response.ok) {
|
|
4668
4888
|
throw new Error(`Failed to check npm registry (${response.status})`);
|
|
4669
4889
|
}
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
}
|
|
4674
|
-
return payload.version;
|
|
4890
|
+
return resolveLatestPublishedVersion(
|
|
4891
|
+
await response.json()
|
|
4892
|
+
);
|
|
4675
4893
|
}
|
|
4676
4894
|
function attemptBundledMutagenRefresh(executablePath) {
|
|
4677
4895
|
const install = spawnSync(
|
|
@@ -4686,12 +4904,12 @@ function attemptBundledMutagenRefresh(executablePath) {
|
|
|
4686
4904
|
}
|
|
4687
4905
|
console.log();
|
|
4688
4906
|
console.log(
|
|
4689
|
-
|
|
4907
|
+
chalk12.yellow(
|
|
4690
4908
|
" CLI updated, but bundled Mutagen did not finish installing. `computer mount` will retry on first use."
|
|
4691
4909
|
)
|
|
4692
4910
|
);
|
|
4693
4911
|
}
|
|
4694
|
-
var upgradeCommand = new
|
|
4912
|
+
var upgradeCommand = new Command14("upgrade").description("Update the CLI to the latest version").action(async () => {
|
|
4695
4913
|
const currentVersion = pkg2.version ?? "0.0.0";
|
|
4696
4914
|
const packageName = pkg2.name ?? "aicomputer";
|
|
4697
4915
|
const spinner = ora10("Checking for updates...").start();
|
|
@@ -4713,7 +4931,7 @@ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the
|
|
|
4713
4931
|
const method = detectInstallMethod(executablePath);
|
|
4714
4932
|
let upgrade;
|
|
4715
4933
|
try {
|
|
4716
|
-
upgrade = resolveUpgradeCommand(method, executablePath);
|
|
4934
|
+
upgrade = resolveUpgradeCommand(method, executablePath, latestVersion);
|
|
4717
4935
|
} catch (error) {
|
|
4718
4936
|
spinner.fail(
|
|
4719
4937
|
error instanceof Error ? error.message : "Failed to prepare upgrade"
|
|
@@ -4724,9 +4942,9 @@ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the
|
|
|
4724
4942
|
spinner.stop();
|
|
4725
4943
|
console.log();
|
|
4726
4944
|
console.log(
|
|
4727
|
-
|
|
4945
|
+
chalk12.dim(` Updating ${chalk12.bold(`v${currentVersion}`)} -> ${chalk12.bold(`v${latestVersion}`)}`)
|
|
4728
4946
|
);
|
|
4729
|
-
console.log(
|
|
4947
|
+
console.log(chalk12.dim(` ${upgrade.label}`));
|
|
4730
4948
|
console.log();
|
|
4731
4949
|
const result = spawnSync(upgrade.command, upgrade.args, {
|
|
4732
4950
|
stdio: "inherit"
|
|
@@ -4736,7 +4954,7 @@ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the
|
|
|
4736
4954
|
attemptBundledMutagenRefresh(executablePath);
|
|
4737
4955
|
}
|
|
4738
4956
|
console.log();
|
|
4739
|
-
console.log(
|
|
4957
|
+
console.log(chalk12.green(` Updated to v${latestVersion}.`));
|
|
4740
4958
|
console.log();
|
|
4741
4959
|
return;
|
|
4742
4960
|
}
|
|
@@ -4744,10 +4962,10 @@ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the
|
|
|
4744
4962
|
});
|
|
4745
4963
|
|
|
4746
4964
|
// src/commands/whoami.ts
|
|
4747
|
-
import { Command as
|
|
4748
|
-
import
|
|
4965
|
+
import { Command as Command15 } from "commander";
|
|
4966
|
+
import chalk13 from "chalk";
|
|
4749
4967
|
import ora11 from "ora";
|
|
4750
|
-
var whoamiCommand = new
|
|
4968
|
+
var whoamiCommand = new Command15("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
|
|
4751
4969
|
const spinner = options.json ? null : ora11("Loading user...").start();
|
|
4752
4970
|
try {
|
|
4753
4971
|
const me = await api("/v1/me");
|
|
@@ -4757,39 +4975,66 @@ var whoamiCommand = new Command14("whoami").description("Show current user").opt
|
|
|
4757
4975
|
return;
|
|
4758
4976
|
}
|
|
4759
4977
|
console.log();
|
|
4760
|
-
console.log(
|
|
4978
|
+
console.log(
|
|
4979
|
+
` ${chalk13.bold.white(me.user.display_name || me.user.email)}`
|
|
4980
|
+
);
|
|
4761
4981
|
if (me.user.display_name) {
|
|
4762
|
-
console.log(` ${
|
|
4982
|
+
console.log(` ${chalk13.dim(me.user.email)}`);
|
|
4763
4983
|
}
|
|
4764
4984
|
if (me.api_key.name) {
|
|
4765
|
-
console.log(` ${
|
|
4985
|
+
console.log(` ${chalk13.dim("Key:")} ${me.api_key.name}`);
|
|
4986
|
+
}
|
|
4987
|
+
if (me.runtime_credit) {
|
|
4988
|
+
console.log(
|
|
4989
|
+
` ${chalk13.dim("Credits:")} ${formatRuntimeHours(me.runtime_credit.available_credit_seconds)} remaining`
|
|
4990
|
+
);
|
|
4991
|
+
if (me.runtime_credit.in_grace && me.runtime_credit.grace_expires_at) {
|
|
4992
|
+
console.log(
|
|
4993
|
+
` ${chalk13.dim("Grace:")} Stops at ${new Date(me.runtime_credit.grace_expires_at).toLocaleString()}`
|
|
4994
|
+
);
|
|
4995
|
+
} else {
|
|
4996
|
+
console.log(
|
|
4997
|
+
` ${chalk13.dim("Reset:")} ${new Date(me.runtime_credit.period_ends_at).toLocaleDateString()}`
|
|
4998
|
+
);
|
|
4999
|
+
}
|
|
4766
5000
|
}
|
|
4767
|
-
console.log(` ${
|
|
5001
|
+
console.log(` ${chalk13.dim("API:")} ${chalk13.dim(getBaseURL())}`);
|
|
4768
5002
|
console.log();
|
|
4769
5003
|
} catch (error) {
|
|
4770
5004
|
if (spinner) {
|
|
4771
|
-
spinner.fail(
|
|
5005
|
+
spinner.fail(
|
|
5006
|
+
error instanceof Error ? error.message : "Failed to load user"
|
|
5007
|
+
);
|
|
4772
5008
|
} else {
|
|
4773
|
-
console.error(
|
|
5009
|
+
console.error(
|
|
5010
|
+
error instanceof Error ? error.message : "Failed to load user"
|
|
5011
|
+
);
|
|
4774
5012
|
}
|
|
4775
5013
|
process.exit(1);
|
|
4776
5014
|
}
|
|
4777
5015
|
});
|
|
5016
|
+
function formatRuntimeHours(seconds) {
|
|
5017
|
+
const hours = Math.max(seconds, 0) / 3600;
|
|
5018
|
+
if (hours >= 10) {
|
|
5019
|
+
return `${Math.round(hours)}h`;
|
|
5020
|
+
}
|
|
5021
|
+
return `${hours.toFixed(1)}h`;
|
|
5022
|
+
}
|
|
4778
5023
|
|
|
4779
5024
|
// src/index.ts
|
|
4780
5025
|
var pkg3 = JSON.parse(
|
|
4781
5026
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
4782
5027
|
);
|
|
4783
|
-
var cliName = process.argv[1] ?
|
|
4784
|
-
var program = new
|
|
5028
|
+
var cliName = process.argv[1] ? basename(process.argv[1]) : "agentcomputer";
|
|
5029
|
+
var program = new Command16();
|
|
4785
5030
|
function appendTextSection(lines, title, values) {
|
|
4786
5031
|
if (values.length === 0) {
|
|
4787
5032
|
return;
|
|
4788
5033
|
}
|
|
4789
|
-
lines.push(` ${
|
|
5034
|
+
lines.push(` ${chalk14.dim(title)}`);
|
|
4790
5035
|
lines.push("");
|
|
4791
5036
|
for (const value of values) {
|
|
4792
|
-
lines.push(` ${
|
|
5037
|
+
lines.push(` ${chalk14.white(value)}`);
|
|
4793
5038
|
}
|
|
4794
5039
|
lines.push("");
|
|
4795
5040
|
}
|
|
@@ -4798,10 +5043,10 @@ function appendTableSection(lines, title, entries) {
|
|
|
4798
5043
|
return;
|
|
4799
5044
|
}
|
|
4800
5045
|
const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
|
|
4801
|
-
lines.push(` ${
|
|
5046
|
+
lines.push(` ${chalk14.dim(title)}`);
|
|
4802
5047
|
lines.push("");
|
|
4803
5048
|
for (const entry of entries) {
|
|
4804
|
-
lines.push(` ${
|
|
5049
|
+
lines.push(` ${chalk14.white(padEnd(entry.term, width))}${chalk14.dim(entry.desc)}`);
|
|
4805
5050
|
}
|
|
4806
5051
|
lines.push("");
|
|
4807
5052
|
}
|
|
@@ -4827,10 +5072,10 @@ function formatRootHelp(cmd) {
|
|
|
4827
5072
|
["Other", []]
|
|
4828
5073
|
];
|
|
4829
5074
|
const otherGroup = groups.find(([name]) => name === "Other")[1];
|
|
4830
|
-
lines.push(`${
|
|
5075
|
+
lines.push(`${chalk14.bold(cliName)} ${chalk14.dim(`v${version}`)}`);
|
|
4831
5076
|
lines.push("");
|
|
4832
5077
|
if (cmd.description()) {
|
|
4833
|
-
lines.push(` ${
|
|
5078
|
+
lines.push(` ${chalk14.dim(cmd.description())}`);
|
|
4834
5079
|
lines.push("");
|
|
4835
5080
|
}
|
|
4836
5081
|
appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
|
|
@@ -4839,7 +5084,7 @@ function formatRootHelp(cmd) {
|
|
|
4839
5084
|
const entry = { term: name, desc: sub.description() };
|
|
4840
5085
|
if (["login", "logout", "whoami", "claude-login", "codex-login"].includes(name)) {
|
|
4841
5086
|
groups[0][1].push(entry);
|
|
4842
|
-
} else if (["create", "ls", "get", "rm"].includes(name)) {
|
|
5087
|
+
} else if (["create", "ls", "get", "power-on", "power-off", "rm"].includes(name)) {
|
|
4843
5088
|
groups[1][1].push(entry);
|
|
4844
5089
|
} else if (name === "image") {
|
|
4845
5090
|
groups[2][1].push(entry);
|
|
@@ -4883,10 +5128,10 @@ function formatSubcommandHelp(cmd, helper) {
|
|
|
4883
5128
|
term: helper.optionTerm(option),
|
|
4884
5129
|
desc: helper.optionDescription(option)
|
|
4885
5130
|
}));
|
|
4886
|
-
lines.push(
|
|
5131
|
+
lines.push(chalk14.bold(commandPath(cmd)));
|
|
4887
5132
|
lines.push("");
|
|
4888
5133
|
if (description) {
|
|
4889
|
-
lines.push(` ${
|
|
5134
|
+
lines.push(` ${chalk14.dim(description)}`);
|
|
4890
5135
|
lines.push("");
|
|
4891
5136
|
}
|
|
4892
5137
|
appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
|
|
@@ -4912,6 +5157,7 @@ function applyHelpFormatting(cmd) {
|
|
|
4912
5157
|
program.name(cliName).description("Agent Computer CLI").version(pkg3.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts");
|
|
4913
5158
|
program.addCommand(loginCommand);
|
|
4914
5159
|
program.addCommand(upgradeCommand);
|
|
5160
|
+
program.addCommand(internalInstallAutosshCommand, { hidden: true });
|
|
4915
5161
|
program.addCommand(internalInstallMutagenCommand, { hidden: true });
|
|
4916
5162
|
program.addCommand(logoutCommand);
|
|
4917
5163
|
program.addCommand(whoamiCommand);
|
|
@@ -4920,6 +5166,8 @@ program.addCommand(codexLoginCommand);
|
|
|
4920
5166
|
program.addCommand(createCommand);
|
|
4921
5167
|
program.addCommand(lsCommand);
|
|
4922
5168
|
program.addCommand(getCommand);
|
|
5169
|
+
program.addCommand(powerOnCommand);
|
|
5170
|
+
program.addCommand(powerOffCommand);
|
|
4923
5171
|
program.addCommand(imageCommand);
|
|
4924
5172
|
program.addCommand(agentCommand);
|
|
4925
5173
|
program.addCommand(acpCommand);
|