aicomputer 0.1.19 → 0.1.21
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-5JYMQXKN.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-MDSPJ57B.js → chunk-GD42GHW3.js} +4 -9
- package/dist/chunk-LOGK7YYJ.js +255 -0
- package/dist/{chunk-NN4GECN6.js → chunk-TPFE3CC6.js} +14 -1
- package/dist/index.js +664 -406
- 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 -2
- package/dist/lib/mount-reconcile.js +5 -3
- package/dist/lib/mutagen-runtime.d.ts +1 -2
- package/dist/lib/mutagen-runtime.js +4 -5
- 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/package.json +3 -3
- package/scripts/postinstall.mjs +36 -23
package/dist/index.js
CHANGED
|
@@ -1,42 +1,26 @@
|
|
|
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";
|
|
6
13
|
import {
|
|
7
|
-
ApiError,
|
|
8
|
-
api,
|
|
9
|
-
apiWithKey,
|
|
10
|
-
clearAPIKey,
|
|
11
|
-
createBrowserAccess,
|
|
12
|
-
createComputer,
|
|
13
|
-
deleteComputer,
|
|
14
|
-
deletePublishedPort,
|
|
15
14
|
formatStatus,
|
|
16
|
-
getAPIKey,
|
|
17
|
-
getBaseURL,
|
|
18
|
-
getComputerByID,
|
|
19
|
-
getConnectionInfo,
|
|
20
|
-
getFilesystemSettings,
|
|
21
|
-
getStoredAPIKey,
|
|
22
|
-
getWebURL,
|
|
23
|
-
hasEnvAPIKey,
|
|
24
|
-
listComputers,
|
|
25
|
-
listPublishedPorts,
|
|
26
15
|
padEnd,
|
|
27
16
|
promptForSSHComputer,
|
|
28
|
-
publishPort,
|
|
29
17
|
reconcileMounts,
|
|
30
|
-
resolveComputer,
|
|
31
|
-
setAPIKey,
|
|
32
18
|
teardownManagedSessions,
|
|
33
|
-
timeAgo
|
|
34
|
-
|
|
35
|
-
webURL
|
|
36
|
-
} from "./chunk-5JYMQXKN.js";
|
|
19
|
+
timeAgo
|
|
20
|
+
} from "./chunk-36BZXAAX.js";
|
|
37
21
|
import {
|
|
38
22
|
isAbortError
|
|
39
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-TPFE3CC6.js";
|
|
40
24
|
import {
|
|
41
25
|
defaultMountServiceConfig,
|
|
42
26
|
ensureMountDirectories,
|
|
@@ -53,144 +37,56 @@ import {
|
|
|
53
37
|
AGENTCOMPUTER_MUTAGEN_PATH_ENV,
|
|
54
38
|
ensureBundledMutagenInstalled,
|
|
55
39
|
ensureMutagenCommandPath
|
|
56
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-GD42GHW3.js";
|
|
57
41
|
import {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
ensureDefaultSSHKeyRegistered,
|
|
43
|
+
openSSHConnection,
|
|
44
|
+
prepareSSHConnection,
|
|
45
|
+
prepareSSHConnectionByIdentifier
|
|
46
|
+
} from "./chunk-4TE5XTYE.js";
|
|
47
|
+
import {
|
|
48
|
+
ensureBundledAutosshInstalled
|
|
49
|
+
} from "./chunk-3ZF7JRBW.js";
|
|
50
|
+
import {
|
|
51
|
+
ApiError,
|
|
52
|
+
api,
|
|
53
|
+
apiWithKey,
|
|
54
|
+
clearAPIKey,
|
|
55
|
+
createBrowserAccess,
|
|
56
|
+
createComputer,
|
|
57
|
+
deleteComputer,
|
|
58
|
+
deletePublishedPort,
|
|
59
|
+
getAPIKey,
|
|
60
|
+
getBaseURL,
|
|
61
|
+
getComputerByID,
|
|
62
|
+
getConnectionInfo,
|
|
63
|
+
getFilesystemSettings,
|
|
64
|
+
getStoredAPIKey,
|
|
65
|
+
getWebURL,
|
|
66
|
+
hasEnvAPIKey,
|
|
67
|
+
listComputers,
|
|
68
|
+
listPublishedPorts,
|
|
69
|
+
powerOffComputer,
|
|
70
|
+
powerOnComputer,
|
|
71
|
+
publishPort,
|
|
72
|
+
resolveComputer,
|
|
73
|
+
setAPIKey,
|
|
74
|
+
vncURL,
|
|
75
|
+
webURL
|
|
76
|
+
} from "./chunk-LOGK7YYJ.js";
|
|
77
|
+
import "./chunk-5Y2NWK5I.js";
|
|
61
78
|
|
|
62
79
|
// src/index.ts
|
|
63
|
-
import { Command as
|
|
64
|
-
import
|
|
80
|
+
import { Command as Command16 } from "commander";
|
|
81
|
+
import chalk14 from "chalk";
|
|
65
82
|
import { readFileSync as readFileSync3 } from "fs";
|
|
66
|
-
import { basename
|
|
83
|
+
import { basename } from "path";
|
|
67
84
|
|
|
68
85
|
// src/commands/access.ts
|
|
69
86
|
import { Command } from "commander";
|
|
70
87
|
import chalk from "chalk";
|
|
71
88
|
import ora from "ora";
|
|
72
89
|
|
|
73
|
-
// src/lib/ssh-access.ts
|
|
74
|
-
import { spawn } from "child_process";
|
|
75
|
-
|
|
76
|
-
// src/lib/ssh-keys.ts
|
|
77
|
-
import { basename } from "path";
|
|
78
|
-
import { homedir } from "os";
|
|
79
|
-
import { readFile, mkdir } from "fs/promises";
|
|
80
|
-
import { execFileSync } from "child_process";
|
|
81
|
-
import { existsSync } from "fs";
|
|
82
|
-
var DEFAULT_PUBLIC_KEY_PATHS = [
|
|
83
|
-
`${homedir()}/.ssh/id_ed25519.pub`,
|
|
84
|
-
`${homedir()}/.ssh/id_ecdsa.pub`,
|
|
85
|
-
`${homedir()}/.ssh/id_rsa.pub`
|
|
86
|
-
];
|
|
87
|
-
async function ensureDefaultSSHKeyRegistered() {
|
|
88
|
-
for (const path of DEFAULT_PUBLIC_KEY_PATHS) {
|
|
89
|
-
try {
|
|
90
|
-
const publicKey2 = (await readFile(path, "utf8")).trim();
|
|
91
|
-
if (!publicKey2) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const key2 = await api("/v1/ssh-keys", {
|
|
95
|
-
method: "POST",
|
|
96
|
-
body: JSON.stringify({
|
|
97
|
-
name: basename(path),
|
|
98
|
-
public_key: publicKey2
|
|
99
|
-
})
|
|
100
|
-
});
|
|
101
|
-
return {
|
|
102
|
-
key: key2,
|
|
103
|
-
publicKeyPath: path,
|
|
104
|
-
privateKeyPath: path.replace(/\.pub$/, "")
|
|
105
|
-
};
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (error?.code === "ENOENT") {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const generated = await generateSSHKey();
|
|
114
|
-
const publicKey = (await readFile(generated.publicKeyPath, "utf8")).trim();
|
|
115
|
-
const key = await api("/v1/ssh-keys", {
|
|
116
|
-
method: "POST",
|
|
117
|
-
body: JSON.stringify({
|
|
118
|
-
name: basename(generated.publicKeyPath),
|
|
119
|
-
public_key: publicKey
|
|
120
|
-
})
|
|
121
|
-
});
|
|
122
|
-
return {
|
|
123
|
-
key,
|
|
124
|
-
publicKeyPath: generated.publicKeyPath,
|
|
125
|
-
privateKeyPath: generated.privateKeyPath
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
async function generateSSHKey() {
|
|
129
|
-
const sshDir = `${homedir()}/.ssh`;
|
|
130
|
-
if (!existsSync(sshDir)) {
|
|
131
|
-
await mkdir(sshDir, { mode: 448 });
|
|
132
|
-
}
|
|
133
|
-
const privateKeyPath = `${sshDir}/id_ed25519`;
|
|
134
|
-
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
135
|
-
console.log("No SSH key found \u2014 generating one at", publicKeyPath);
|
|
136
|
-
execFileSync("ssh-keygen", ["-t", "ed25519", "-f", privateKeyPath, "-N", ""], {
|
|
137
|
-
stdio: "inherit"
|
|
138
|
-
});
|
|
139
|
-
return { publicKeyPath, privateKeyPath };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/lib/ssh-access.ts
|
|
143
|
-
async function prepareSSHConnection(computer) {
|
|
144
|
-
const registered = await ensureDefaultSSHKeyRegistered();
|
|
145
|
-
const info = await getConnectionInfo(computer.id);
|
|
146
|
-
if (!info.connection.ssh_available) {
|
|
147
|
-
throw new Error("SSH is not available for this computer");
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
150
|
-
computer,
|
|
151
|
-
command: formatSSHCommand(
|
|
152
|
-
info.connection.ssh_user,
|
|
153
|
-
info.connection.ssh_host,
|
|
154
|
-
info.connection.ssh_port
|
|
155
|
-
),
|
|
156
|
-
args: [
|
|
157
|
-
"-i",
|
|
158
|
-
registered.privateKeyPath,
|
|
159
|
-
"-p",
|
|
160
|
-
String(info.connection.ssh_port),
|
|
161
|
-
`${info.connection.ssh_user}@${info.connection.ssh_host}`
|
|
162
|
-
]
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
async function prepareSSHConnectionByIdentifier(identifier) {
|
|
166
|
-
const computer = await resolveComputer(identifier);
|
|
167
|
-
return prepareSSHConnection(computer);
|
|
168
|
-
}
|
|
169
|
-
async function openSSHConnection(connection) {
|
|
170
|
-
await new Promise((resolve, reject) => {
|
|
171
|
-
const child = spawn("ssh", connection.args, {
|
|
172
|
-
stdio: "inherit"
|
|
173
|
-
});
|
|
174
|
-
child.on("error", reject);
|
|
175
|
-
child.on("exit", (code) => {
|
|
176
|
-
if (code === 0) {
|
|
177
|
-
resolve();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
reject(new Error(`ssh exited with code ${code ?? 1}`));
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
function formatSSHCommand(user, host, port) {
|
|
185
|
-
if (!user.trim() || !host.trim()) {
|
|
186
|
-
return "ssh unavailable";
|
|
187
|
-
}
|
|
188
|
-
if (port <= 0 || port === 22) {
|
|
189
|
-
return `ssh ${user}@${host}`;
|
|
190
|
-
}
|
|
191
|
-
return `ssh -p ${port} ${user}@${host}`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
90
|
// src/lib/open-browser.ts
|
|
195
91
|
import { constants } from "fs";
|
|
196
92
|
import { access } from "fs/promises";
|
|
@@ -248,74 +144,6 @@ async function openBrowserURL(url) {
|
|
|
248
144
|
await open(url);
|
|
249
145
|
}
|
|
250
146
|
|
|
251
|
-
// src/lib/ssh-config.ts
|
|
252
|
-
import { homedir as homedir2 } from "os";
|
|
253
|
-
import { join } from "path";
|
|
254
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
255
|
-
var MANAGED_BLOCK_START = "# >>> agentcomputer ssh setup >>>";
|
|
256
|
-
var MANAGED_BLOCK_END = "# <<< agentcomputer ssh setup <<<";
|
|
257
|
-
async function ensureSSHAliasConfig(options) {
|
|
258
|
-
const sshDir = join(homedir2(), ".ssh");
|
|
259
|
-
const configPath = join(sshDir, "config");
|
|
260
|
-
await mkdir2(sshDir, { recursive: true, mode: 448 });
|
|
261
|
-
let existing = "";
|
|
262
|
-
try {
|
|
263
|
-
existing = await readFile2(configPath, "utf8");
|
|
264
|
-
} catch (error) {
|
|
265
|
-
if (error.code !== "ENOENT") {
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const nextBlock = renderManagedBlock(options);
|
|
270
|
-
const managedBlockPattern = new RegExp(
|
|
271
|
-
`${escapeRegex(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegex(MANAGED_BLOCK_END)}\\n?`,
|
|
272
|
-
"m"
|
|
273
|
-
);
|
|
274
|
-
let nextContents;
|
|
275
|
-
if (managedBlockPattern.test(existing)) {
|
|
276
|
-
nextContents = existing.replace(managedBlockPattern, nextBlock);
|
|
277
|
-
} else {
|
|
278
|
-
const normalized = existing.length === 0 ? "" : existing.endsWith("\n") ? existing : `${existing}
|
|
279
|
-
`;
|
|
280
|
-
nextContents = normalized.length === 0 ? nextBlock : `${normalized}
|
|
281
|
-
${nextBlock}`;
|
|
282
|
-
}
|
|
283
|
-
const changed = nextContents !== existing;
|
|
284
|
-
if (changed) {
|
|
285
|
-
await writeFile(configPath, nextContents, { mode: 384 });
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
configPath,
|
|
289
|
-
changed
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
function renderManagedBlock(options) {
|
|
293
|
-
const user = options.user?.trim() || "agentcomputer";
|
|
294
|
-
const identityFile = formatIdentityFilePath(options.identityFilePath);
|
|
295
|
-
return `${MANAGED_BLOCK_START}
|
|
296
|
-
Host ${options.alias}
|
|
297
|
-
HostName ${options.host}
|
|
298
|
-
Port ${options.port}
|
|
299
|
-
User ${user}
|
|
300
|
-
IdentityFile ${identityFile}
|
|
301
|
-
IdentitiesOnly yes
|
|
302
|
-
ServerAliveInterval 30
|
|
303
|
-
ServerAliveCountMax 4
|
|
304
|
-
${MANAGED_BLOCK_END}
|
|
305
|
-
`;
|
|
306
|
-
}
|
|
307
|
-
function formatIdentityFilePath(path) {
|
|
308
|
-
const normalized = path.trim();
|
|
309
|
-
const homePath = `${homedir2()}/`;
|
|
310
|
-
if (normalized.startsWith(homePath)) {
|
|
311
|
-
return `~/${normalized.slice(homePath.length)}`;
|
|
312
|
-
}
|
|
313
|
-
return normalized.replaceAll(" ", "\\ ");
|
|
314
|
-
}
|
|
315
|
-
function escapeRegex(value) {
|
|
316
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
147
|
// src/lib/ssh-setup.ts
|
|
320
148
|
async function ensureSSHAccessConfigured(options = {}) {
|
|
321
149
|
const alias = normalizeSSHAlias(options.alias ?? "agentcomputer.ai");
|
|
@@ -395,8 +223,8 @@ var openCommand = new Command("open").description("Open a computer in your brows
|
|
|
395
223
|
process.exit(1);
|
|
396
224
|
}
|
|
397
225
|
});
|
|
398
|
-
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(
|
|
399
|
-
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) => {
|
|
400
228
|
if (options.setup) {
|
|
401
229
|
await setupSSHAlias(options);
|
|
402
230
|
return;
|
|
@@ -406,7 +234,10 @@ var sshCommand = new Command("ssh").description("Open an SSH session to a comput
|
|
|
406
234
|
).start();
|
|
407
235
|
try {
|
|
408
236
|
const computer = await resolveSSHComputer(identifier, spinner);
|
|
409
|
-
const connection = await prepareSSHConnection(computer
|
|
237
|
+
const connection = await prepareSSHConnection(computer, {
|
|
238
|
+
extraArgs: sshArgs,
|
|
239
|
+
tmux: options.tmux
|
|
240
|
+
});
|
|
410
241
|
spinner.succeed(`Connecting to ${chalk.bold(computer.handle)}`);
|
|
411
242
|
console.log(chalk.dim(` ${connection.command}`));
|
|
412
243
|
console.log();
|
|
@@ -1317,7 +1148,7 @@ import ora4 from "ora";
|
|
|
1317
1148
|
|
|
1318
1149
|
// src/lib/remote-auth.ts
|
|
1319
1150
|
import { randomBytes } from "crypto";
|
|
1320
|
-
import { spawn
|
|
1151
|
+
import { spawn } from "child_process";
|
|
1321
1152
|
import ora3 from "ora";
|
|
1322
1153
|
var readyPollIntervalMs = 2e3;
|
|
1323
1154
|
var readyPollTimeoutMs = 18e4;
|
|
@@ -1453,7 +1284,7 @@ async function runRemoteCommand(target, remoteArgs, script) {
|
|
|
1453
1284
|
...remoteArgs
|
|
1454
1285
|
];
|
|
1455
1286
|
return new Promise((resolve, reject) => {
|
|
1456
|
-
const child =
|
|
1287
|
+
const child = spawn("ssh", args, {
|
|
1457
1288
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1458
1289
|
});
|
|
1459
1290
|
let stdout = "";
|
|
@@ -2013,13 +1844,14 @@ function isInternalCondition(value) {
|
|
|
2013
1844
|
}
|
|
2014
1845
|
function printComputer(computer) {
|
|
2015
1846
|
const vnc = vncURL(computer);
|
|
2016
|
-
const ssh = computer.ssh_enabled ?
|
|
1847
|
+
const ssh = computer.ssh_enabled ? formatSSHCommand(computer.handle, computer.ssh_host, computer.ssh_port) : "disabled";
|
|
2017
1848
|
const isCustom = computer.runtime_family === "custom-machine";
|
|
2018
1849
|
console.log();
|
|
2019
1850
|
console.log(` ${chalk4.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
|
|
2020
1851
|
console.log();
|
|
2021
1852
|
console.log(` ${chalk4.dim("ID")} ${computer.id}`);
|
|
2022
1853
|
console.log(` ${chalk4.dim("Tier")} ${computer.tier}`);
|
|
1854
|
+
console.log(` ${chalk4.dim("Power")} ${formatDesiredPowerState(computer.desired_power_state)}`);
|
|
2023
1855
|
if (isCustom) {
|
|
2024
1856
|
console.log(` ${chalk4.dim("Runtime")} ${computer.runtime_family}`);
|
|
2025
1857
|
console.log(` ${chalk4.dim("Source")} ${computer.source_kind}`);
|
|
@@ -2049,7 +1881,7 @@ function printComputer(computer) {
|
|
|
2049
1881
|
console.log(` ${chalk4.dim("Created")} ${timeAgo(computer.created_at)}`);
|
|
2050
1882
|
console.log();
|
|
2051
1883
|
}
|
|
2052
|
-
function
|
|
1884
|
+
function formatSSHCommand(user, host, port) {
|
|
2053
1885
|
if (!user.trim() || !host.trim()) {
|
|
2054
1886
|
return "ssh unavailable";
|
|
2055
1887
|
}
|
|
@@ -2077,6 +1909,12 @@ function formatManagedWorkerLaunchSource(computer) {
|
|
|
2077
1909
|
}
|
|
2078
1910
|
return `saved custom source ${computer.user_source_id}`;
|
|
2079
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
|
+
}
|
|
2080
1918
|
function printComputerTable(computers) {
|
|
2081
1919
|
const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
|
|
2082
1920
|
const statusWidth = 12;
|
|
@@ -2333,6 +2171,72 @@ var removeCommand = new Command5("rm").description("Delete a computer").argument
|
|
|
2333
2171
|
process.exit(1);
|
|
2334
2172
|
}
|
|
2335
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
|
+
});
|
|
2336
2240
|
function parseRuntimeFamilyOption(value) {
|
|
2337
2241
|
switch (value) {
|
|
2338
2242
|
case void 0:
|
|
@@ -2468,6 +2372,8 @@ _computer() {
|
|
|
2468
2372
|
'create:Create a computer'
|
|
2469
2373
|
'ls:List computers'
|
|
2470
2374
|
'get:Show computer details'
|
|
2375
|
+
'power-on:Power on a managed worker'
|
|
2376
|
+
'power-off:Power off a managed worker'
|
|
2471
2377
|
'image:Manage machine image sources'
|
|
2472
2378
|
'open:Open in browser'
|
|
2473
2379
|
'ssh:SSH into a computer'
|
|
@@ -2558,6 +2464,11 @@ _computer() {
|
|
|
2558
2464
|
'--json[Print raw JSON]' \\
|
|
2559
2465
|
'1:computer:_computer_handles'
|
|
2560
2466
|
;;
|
|
2467
|
+
power-on|power-off)
|
|
2468
|
+
_arguments \\
|
|
2469
|
+
'--json[Print raw JSON]' \\
|
|
2470
|
+
'1:computer:_computer_handles'
|
|
2471
|
+
;;
|
|
2561
2472
|
image)
|
|
2562
2473
|
_arguments -C \\
|
|
2563
2474
|
'1:command:->image_command' \\
|
|
@@ -2822,10 +2733,10 @@ var completionCommand = new Command6("completion").description("Generate shell c
|
|
|
2822
2733
|
});
|
|
2823
2734
|
|
|
2824
2735
|
// src/commands/codex-login.ts
|
|
2825
|
-
import { spawn as
|
|
2826
|
-
import { readFile
|
|
2827
|
-
import { homedir
|
|
2828
|
-
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";
|
|
2829
2740
|
import { Command as Command7 } from "commander";
|
|
2830
2741
|
import chalk5 from "chalk";
|
|
2831
2742
|
import ora6 from "ora";
|
|
@@ -3025,10 +2936,10 @@ async function getLocalCodexStatus() {
|
|
|
3025
2936
|
return parseCodexStatusOutput(result.stdout, result.stderr);
|
|
3026
2937
|
}
|
|
3027
2938
|
async function readLocalCodexAuthFile() {
|
|
3028
|
-
const authPath =
|
|
2939
|
+
const authPath = join(homedir(), ".codex", "auth.json");
|
|
3029
2940
|
let raw;
|
|
3030
2941
|
try {
|
|
3031
|
-
raw = await
|
|
2942
|
+
raw = await readFile(authPath, "utf8");
|
|
3032
2943
|
} catch (error) {
|
|
3033
2944
|
throw new Error(
|
|
3034
2945
|
error instanceof Error ? `failed to read ${authPath}: ${error.message}` : `failed to read ${authPath}`
|
|
@@ -3046,7 +2957,7 @@ async function readLocalCodexAuthFile() {
|
|
|
3046
2957
|
}
|
|
3047
2958
|
async function runInteractiveCodexLogin() {
|
|
3048
2959
|
await new Promise((resolve, reject) => {
|
|
3049
|
-
const child =
|
|
2960
|
+
const child = spawn2("codex", ["login"], {
|
|
3050
2961
|
stdio: "inherit"
|
|
3051
2962
|
});
|
|
3052
2963
|
child.on("error", (error) => {
|
|
@@ -3069,7 +2980,7 @@ async function runInteractiveCodexLogin() {
|
|
|
3069
2980
|
}
|
|
3070
2981
|
async function captureLocalCommand(command, args) {
|
|
3071
2982
|
return new Promise((resolve, reject) => {
|
|
3072
|
-
const child =
|
|
2983
|
+
const child = spawn2(command, args, {
|
|
3073
2984
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3074
2985
|
});
|
|
3075
2986
|
let stdout = "";
|
|
@@ -3573,16 +3484,42 @@ async function confirmDeletion(sourceID) {
|
|
|
3573
3484
|
});
|
|
3574
3485
|
}
|
|
3575
3486
|
|
|
3576
|
-
// src/commands/internal-install-
|
|
3487
|
+
// src/commands/internal-install-autossh.ts
|
|
3577
3488
|
import { Command as Command9 } from "commander";
|
|
3578
3489
|
import chalk7 from "chalk";
|
|
3579
|
-
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) => {
|
|
3580
3517
|
try {
|
|
3581
3518
|
const executablePath = await ensureBundledMutagenInstalled();
|
|
3582
3519
|
if (!options.quiet) {
|
|
3583
3520
|
console.log();
|
|
3584
3521
|
console.log(
|
|
3585
|
-
|
|
3522
|
+
chalk8.green(
|
|
3586
3523
|
` Bundled Mutagen ready at ${executablePath}.`
|
|
3587
3524
|
)
|
|
3588
3525
|
);
|
|
@@ -3592,7 +3529,7 @@ var internalInstallMutagenCommand = new Command9("internal-install-mutagen").opt
|
|
|
3592
3529
|
if (!options.quiet) {
|
|
3593
3530
|
const message = error instanceof Error ? error.message : "failed to install bundled Mutagen";
|
|
3594
3531
|
console.error();
|
|
3595
|
-
console.error(
|
|
3532
|
+
console.error(chalk8.red(` ${message}`));
|
|
3596
3533
|
console.error();
|
|
3597
3534
|
}
|
|
3598
3535
|
process.exit(1);
|
|
@@ -3600,8 +3537,8 @@ var internalInstallMutagenCommand = new Command9("internal-install-mutagen").opt
|
|
|
3600
3537
|
});
|
|
3601
3538
|
|
|
3602
3539
|
// src/commands/login.ts
|
|
3603
|
-
import { Command as
|
|
3604
|
-
import
|
|
3540
|
+
import { Command as Command11 } from "commander";
|
|
3541
|
+
import chalk9 from "chalk";
|
|
3605
3542
|
import ora8 from "ora";
|
|
3606
3543
|
|
|
3607
3544
|
// src/lib/browser-login.ts
|
|
@@ -3869,12 +3806,12 @@ function escapeHTML(value) {
|
|
|
3869
3806
|
}
|
|
3870
3807
|
|
|
3871
3808
|
// src/commands/login.ts
|
|
3872
|
-
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) => {
|
|
3873
3810
|
const existingKey = getStoredAPIKey();
|
|
3874
3811
|
if (existingKey && !options.force) {
|
|
3875
3812
|
console.log();
|
|
3876
3813
|
console.log(
|
|
3877
|
-
|
|
3814
|
+
chalk9.yellow(" Already logged in. Use --force to overwrite.")
|
|
3878
3815
|
);
|
|
3879
3816
|
console.log();
|
|
3880
3817
|
return;
|
|
@@ -3883,8 +3820,8 @@ var loginCommand = new Command10("login").description("Authenticate the CLI").op
|
|
|
3883
3820
|
const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
|
|
3884
3821
|
if (!apiKey && wantsManualLogin) {
|
|
3885
3822
|
console.log();
|
|
3886
|
-
console.log(
|
|
3887
|
-
console.log(
|
|
3823
|
+
console.log(chalk9.dim(" Usage: computer login --api-key <ac_live_...>"));
|
|
3824
|
+
console.log(chalk9.dim(` API: ${getBaseURL()}`));
|
|
3888
3825
|
console.log();
|
|
3889
3826
|
process.exit(1);
|
|
3890
3827
|
}
|
|
@@ -3894,7 +3831,7 @@ var loginCommand = new Command10("login").description("Authenticate the CLI").op
|
|
|
3894
3831
|
}
|
|
3895
3832
|
if (!apiKey.startsWith("ac_live_")) {
|
|
3896
3833
|
console.log();
|
|
3897
|
-
console.log(
|
|
3834
|
+
console.log(chalk9.red(" API key must start with ac_live_"));
|
|
3898
3835
|
console.log();
|
|
3899
3836
|
process.exit(1);
|
|
3900
3837
|
}
|
|
@@ -3902,7 +3839,7 @@ var loginCommand = new Command10("login").description("Authenticate the CLI").op
|
|
|
3902
3839
|
try {
|
|
3903
3840
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
3904
3841
|
setAPIKey(apiKey);
|
|
3905
|
-
spinner.succeed(`Logged in as ${
|
|
3842
|
+
spinner.succeed(`Logged in as ${chalk9.bold(me.user.email)}`);
|
|
3906
3843
|
} catch (error) {
|
|
3907
3844
|
spinner.fail(
|
|
3908
3845
|
error instanceof Error ? error.message : "Failed to validate API key"
|
|
@@ -3922,15 +3859,15 @@ async function runBrowserLogin() {
|
|
|
3922
3859
|
spinner.stop();
|
|
3923
3860
|
console.log();
|
|
3924
3861
|
console.log(
|
|
3925
|
-
|
|
3862
|
+
chalk9.yellow(" Browser auto-open failed. Open this URL to continue:")
|
|
3926
3863
|
);
|
|
3927
|
-
console.log(
|
|
3864
|
+
console.log(chalk9.dim(` ${attempt.loginURL}`));
|
|
3928
3865
|
console.log();
|
|
3929
3866
|
spinner.start("Waiting for browser login...");
|
|
3930
3867
|
}
|
|
3931
3868
|
spinner.text = "Waiting for browser login...";
|
|
3932
3869
|
const result = await attempt.waitForResult();
|
|
3933
|
-
spinner.succeed(`Logged in as ${
|
|
3870
|
+
spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
|
|
3934
3871
|
await continueFirstLoginFlow(result);
|
|
3935
3872
|
} catch (error) {
|
|
3936
3873
|
spinner.fail(
|
|
@@ -3964,8 +3901,8 @@ async function continueFirstLoginFlow(result) {
|
|
|
3964
3901
|
}
|
|
3965
3902
|
console.log();
|
|
3966
3903
|
console.log(
|
|
3967
|
-
|
|
3968
|
-
`Continuing first-time setup for ${
|
|
3904
|
+
chalk9.cyan(
|
|
3905
|
+
`Continuing first-time setup for ${chalk9.bold(machineHandle)}...
|
|
3969
3906
|
`
|
|
3970
3907
|
)
|
|
3971
3908
|
);
|
|
@@ -3978,8 +3915,8 @@ async function continueFirstLoginFlow(result) {
|
|
|
3978
3915
|
const spinner = ora8(`Preparing SSH access for ${machineHandle}...`).start();
|
|
3979
3916
|
try {
|
|
3980
3917
|
const connection = await prepareSSHConnectionByIdentifier(machineHandle);
|
|
3981
|
-
spinner.succeed(`Connecting to ${
|
|
3982
|
-
console.log(
|
|
3918
|
+
spinner.succeed(`Connecting to ${chalk9.bold(machineHandle)}`);
|
|
3919
|
+
console.log(chalk9.dim(` ${connection.command}`));
|
|
3983
3920
|
console.log();
|
|
3984
3921
|
await openSSHConnection(connection);
|
|
3985
3922
|
} catch (error) {
|
|
@@ -3990,19 +3927,19 @@ async function continueFirstLoginFlow(result) {
|
|
|
3990
3927
|
}
|
|
3991
3928
|
} catch (error) {
|
|
3992
3929
|
const message = error instanceof Error ? error.message : "Failed to finish first-time setup";
|
|
3993
|
-
console.error(
|
|
3930
|
+
console.error(chalk9.red(`
|
|
3994
3931
|
${message}`));
|
|
3995
3932
|
console.log();
|
|
3996
3933
|
if (result.provider === "claude") {
|
|
3997
3934
|
console.log(
|
|
3998
|
-
|
|
3935
|
+
chalk9.dim(` computer claude-login --machine ${machineHandle}`)
|
|
3999
3936
|
);
|
|
4000
3937
|
} else if (result.provider === "codex") {
|
|
4001
3938
|
console.log(
|
|
4002
|
-
|
|
3939
|
+
chalk9.dim(` computer codex-login --machine ${machineHandle}`)
|
|
4003
3940
|
);
|
|
4004
3941
|
}
|
|
4005
|
-
console.log(
|
|
3942
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4006
3943
|
console.log();
|
|
4007
3944
|
process.exit(1);
|
|
4008
3945
|
}
|
|
@@ -4016,23 +3953,23 @@ async function runSelectedProvider(provider, machineHandle) {
|
|
|
4016
3953
|
await runCodexLogin({ machine: machineHandle });
|
|
4017
3954
|
return;
|
|
4018
3955
|
}
|
|
4019
|
-
console.log(
|
|
3956
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
4020
3957
|
console.log();
|
|
4021
3958
|
}
|
|
4022
3959
|
function printNextStep(machineHandle) {
|
|
4023
|
-
console.log(
|
|
4024
|
-
console.log(
|
|
3960
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
3961
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4025
3962
|
console.log();
|
|
4026
3963
|
}
|
|
4027
3964
|
|
|
4028
3965
|
// src/commands/mount.ts
|
|
4029
|
-
import { spawn as
|
|
4030
|
-
import { Command as
|
|
4031
|
-
import
|
|
3966
|
+
import { spawn as spawn3 } from "child_process";
|
|
3967
|
+
import { Command as Command12, Option } from "commander";
|
|
3968
|
+
import chalk10 from "chalk";
|
|
4032
3969
|
import ora9 from "ora";
|
|
4033
3970
|
|
|
4034
3971
|
// src/lib/mount-daemon.ts
|
|
4035
|
-
import { mkdir
|
|
3972
|
+
import { mkdir } from "fs/promises";
|
|
4036
3973
|
function getMountControllerState(rootPath = defaultMountServiceConfig().rootPath) {
|
|
4037
3974
|
const lock = readMountControllerLock(rootPath);
|
|
4038
3975
|
if (!lock) {
|
|
@@ -4048,7 +3985,7 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4048
3985
|
const { onReady, onStarted } = options;
|
|
4049
3986
|
const paths = getMountPaths(config.rootPath);
|
|
4050
3987
|
ensureMountDirectories(paths);
|
|
4051
|
-
await
|
|
3988
|
+
await mkdir(paths.rootPath, { recursive: true });
|
|
4052
3989
|
await acquireControllerLock(config.rootPath);
|
|
4053
3990
|
writeMountStatusSnapshot(
|
|
4054
3991
|
{
|
|
@@ -4078,10 +4015,11 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4078
4015
|
config.rootPath
|
|
4079
4016
|
);
|
|
4080
4017
|
await teardownManagedSessions(config, paths);
|
|
4081
|
-
await
|
|
4018
|
+
await mkdir(paths.rootPath, { recursive: true });
|
|
4082
4019
|
let running = false;
|
|
4083
4020
|
let queued = false;
|
|
4084
4021
|
let shuttingDown = false;
|
|
4022
|
+
let readyNotified = false;
|
|
4085
4023
|
let activeRun = null;
|
|
4086
4024
|
let activeRunController = null;
|
|
4087
4025
|
const runOnce = async () => {
|
|
@@ -4103,6 +4041,11 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4103
4041
|
process.pid,
|
|
4104
4042
|
controller.signal
|
|
4105
4043
|
);
|
|
4044
|
+
const snapshot = readMountStatusSnapshot(config.rootPath);
|
|
4045
|
+
if (!readyNotified && snapshot?.startupPhase === "ready") {
|
|
4046
|
+
readyNotified = true;
|
|
4047
|
+
onReady?.();
|
|
4048
|
+
}
|
|
4106
4049
|
} catch (error) {
|
|
4107
4050
|
if (shuttingDown && isAbortError(error)) {
|
|
4108
4051
|
return;
|
|
@@ -4204,8 +4147,6 @@ async function runMountDaemon(config, options = {}) {
|
|
|
4204
4147
|
void shutdown();
|
|
4205
4148
|
});
|
|
4206
4149
|
await runOnce();
|
|
4207
|
-
writeMountStatusSnapshot(markMountStartupReady(readMountStatusSnapshot(config.rootPath), process.pid), config.rootPath);
|
|
4208
|
-
onReady?.();
|
|
4209
4150
|
await new Promise(() => {
|
|
4210
4151
|
});
|
|
4211
4152
|
}
|
|
@@ -4234,28 +4175,13 @@ function processExists(pid) {
|
|
|
4234
4175
|
return false;
|
|
4235
4176
|
}
|
|
4236
4177
|
}
|
|
4237
|
-
function markMountStartupReady(snapshot, controllerPid) {
|
|
4238
|
-
return {
|
|
4239
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4240
|
-
controllerPid,
|
|
4241
|
-
running: true,
|
|
4242
|
-
startupPhase: "ready",
|
|
4243
|
-
startupMessage: void 0,
|
|
4244
|
-
lastHealthySyncAt: snapshot?.lastHealthySyncAt,
|
|
4245
|
-
lastSuccessfulSyncAt: snapshot?.lastSuccessfulSyncAt,
|
|
4246
|
-
lastIssueAt: snapshot?.lastIssueAt,
|
|
4247
|
-
lastIssue: snapshot?.lastIssue,
|
|
4248
|
-
lastError: snapshot?.lastError,
|
|
4249
|
-
mounts: snapshot?.mounts ?? []
|
|
4250
|
-
};
|
|
4251
|
-
}
|
|
4252
4178
|
|
|
4253
4179
|
// src/commands/mount.ts
|
|
4254
4180
|
var MOUNT_START_SPINNER = {
|
|
4255
4181
|
interval: 90,
|
|
4256
4182
|
frames: ["\u25F0", "\u25F3", "\u25F2", "\u25F1"]
|
|
4257
4183
|
};
|
|
4258
|
-
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(
|
|
4259
4185
|
"--background",
|
|
4260
4186
|
"Run the mount controller in the background and print its PID"
|
|
4261
4187
|
).addOption(new Option("--daemonized").hideHelp()).addOption(new Option("--open-root-when-ready").hideHelp()).action(async (options) => {
|
|
@@ -4289,14 +4215,22 @@ var mountCommand = new Command11("mount").description("Mirror SSH-ready machines
|
|
|
4289
4215
|
return;
|
|
4290
4216
|
}
|
|
4291
4217
|
const child = await startMountControllerInForeground(resolved.config.rootPath);
|
|
4292
|
-
|
|
4218
|
+
const controls = attachForegroundControls(child);
|
|
4293
4219
|
spinner.succeed("Machine mount controller running");
|
|
4294
4220
|
printMountStartSummary(
|
|
4295
4221
|
resolved.config,
|
|
4296
4222
|
child.pid ?? process.pid,
|
|
4297
4223
|
"foreground"
|
|
4298
4224
|
);
|
|
4299
|
-
|
|
4225
|
+
try {
|
|
4226
|
+
await superviseForegroundMountController(
|
|
4227
|
+
resolved.config.rootPath,
|
|
4228
|
+
child,
|
|
4229
|
+
controls
|
|
4230
|
+
);
|
|
4231
|
+
} finally {
|
|
4232
|
+
controls.close();
|
|
4233
|
+
}
|
|
4300
4234
|
} catch (error) {
|
|
4301
4235
|
spinner.fail(
|
|
4302
4236
|
error instanceof Error ? error.message : "Failed to start machine mount controller"
|
|
@@ -4304,42 +4238,9 @@ var mountCommand = new Command11("mount").description("Mirror SSH-ready machines
|
|
|
4304
4238
|
process.exit(1);
|
|
4305
4239
|
}
|
|
4306
4240
|
}).addCommand(
|
|
4307
|
-
new
|
|
4241
|
+
new Command12("status").description("Show machine mount controller status").action(() => {
|
|
4308
4242
|
const config = readMountConfig() ?? defaultMountServiceConfig();
|
|
4309
|
-
|
|
4310
|
-
const snapshot = readMountStatusSnapshot(config.rootPath);
|
|
4311
|
-
console.log();
|
|
4312
|
-
console.log(` ${chalk9.bold("Machine Mounts")}`);
|
|
4313
|
-
console.log();
|
|
4314
|
-
console.log(
|
|
4315
|
-
` ${chalk9.dim("Running")} ${controller.running ? chalk9.green("yes") : chalk9.dim("no")}`
|
|
4316
|
-
);
|
|
4317
|
-
if (controller.pid) {
|
|
4318
|
-
console.log(` ${chalk9.dim("PID")} ${controller.pid}`);
|
|
4319
|
-
}
|
|
4320
|
-
console.log(` ${chalk9.dim("Root")} ${config.rootPath}`);
|
|
4321
|
-
console.log(` ${chalk9.dim("Alias")} ${config.alias}`);
|
|
4322
|
-
console.log(
|
|
4323
|
-
` ${chalk9.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk9.dim("never")}`
|
|
4324
|
-
);
|
|
4325
|
-
console.log(
|
|
4326
|
-
` ${chalk9.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk9.dim("never")}`
|
|
4327
|
-
);
|
|
4328
|
-
if (controller.running && snapshot?.mounts.length) {
|
|
4329
|
-
console.log();
|
|
4330
|
-
for (const mount of snapshot.mounts) {
|
|
4331
|
-
const state = formatMountState(mount.state);
|
|
4332
|
-
console.log(` ${chalk9.white(mount.handle)} ${state} ${chalk9.dim(mount.mountPath)}`);
|
|
4333
|
-
if (mount.message) {
|
|
4334
|
-
console.log(` ${chalk9.dim(mount.message)}`);
|
|
4335
|
-
}
|
|
4336
|
-
}
|
|
4337
|
-
}
|
|
4338
|
-
if (snapshot?.lastIssue) {
|
|
4339
|
-
console.log();
|
|
4340
|
-
console.log(` ${chalk9.dim("Last issue")} ${chalk9.yellow(snapshot.lastIssue)}`);
|
|
4341
|
-
}
|
|
4342
|
-
console.log();
|
|
4243
|
+
renderMountStatus(config);
|
|
4343
4244
|
})
|
|
4344
4245
|
);
|
|
4345
4246
|
function parsePositiveInt(raw, label) {
|
|
@@ -4383,7 +4284,7 @@ async function startMountControllerInBackground(rootPath) {
|
|
|
4383
4284
|
return child.pid;
|
|
4384
4285
|
}
|
|
4385
4286
|
async function startMountControllerInForeground(rootPath) {
|
|
4386
|
-
const child = startMountControllerProcess(rootPath,
|
|
4287
|
+
const child = startMountControllerProcess(rootPath, true, true);
|
|
4387
4288
|
await waitForMountControllerRunning(rootPath, child.pid);
|
|
4388
4289
|
return child;
|
|
4389
4290
|
}
|
|
@@ -4400,7 +4301,7 @@ function startMountControllerProcess(rootPath, detached, openRootWhenReady) {
|
|
|
4400
4301
|
if (openRootWhenReady) {
|
|
4401
4302
|
args.push("--open-root-when-ready");
|
|
4402
4303
|
}
|
|
4403
|
-
const child =
|
|
4304
|
+
const child = spawn3(process.execPath, [entrypoint, ...args], {
|
|
4404
4305
|
cwd: process.cwd(),
|
|
4405
4306
|
detached,
|
|
4406
4307
|
env: process.env,
|
|
@@ -4425,45 +4326,30 @@ async function waitForMountControllerRunning(rootPath, pid) {
|
|
|
4425
4326
|
}
|
|
4426
4327
|
throw new Error("failed to start machine mount controller in background");
|
|
4427
4328
|
}
|
|
4428
|
-
async function
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
spinner.text = snapshot.startupMessage;
|
|
4435
|
-
}
|
|
4436
|
-
if (snapshot?.controllerPid === pid && snapshot.startupPhase === "ready") {
|
|
4437
|
-
return;
|
|
4438
|
-
}
|
|
4439
|
-
if (!controller.running && !processExists2(pid)) {
|
|
4440
|
-
break;
|
|
4441
|
-
}
|
|
4442
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4443
|
-
}
|
|
4444
|
-
throw new Error("mount controller did not finish startup");
|
|
4445
|
-
}
|
|
4446
|
-
async function superviseForegroundMountController(child) {
|
|
4329
|
+
async function superviseForegroundMountController(rootPath, child, controls) {
|
|
4330
|
+
let lifecycleState = createMountLifecycleState();
|
|
4331
|
+
lifecycleState = updateMountLifecycleDisplay(
|
|
4332
|
+
lifecycleState,
|
|
4333
|
+
readMountStatusSnapshot(rootPath)
|
|
4334
|
+
);
|
|
4447
4335
|
await new Promise((resolve, reject) => {
|
|
4448
4336
|
const cleanup = () => {
|
|
4449
|
-
process.off("SIGINT", onSigint);
|
|
4450
|
-
process.off("SIGTERM", onSigterm);
|
|
4451
4337
|
child.off("error", onError);
|
|
4452
4338
|
child.off("exit", onExit);
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
child.kill(signal);
|
|
4458
|
-
} catch {
|
|
4339
|
+
clearInterval(interval);
|
|
4340
|
+
if (lifecycleState.spinner) {
|
|
4341
|
+
lifecycleState.spinner.stop();
|
|
4342
|
+
lifecycleState.spinner = null;
|
|
4459
4343
|
}
|
|
4460
|
-
process.exit(0);
|
|
4461
4344
|
};
|
|
4462
|
-
const
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
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();
|
|
4467
4353
|
};
|
|
4468
4354
|
const onError = (error) => {
|
|
4469
4355
|
cleanup();
|
|
@@ -4481,26 +4367,367 @@ async function superviseForegroundMountController(child) {
|
|
|
4481
4367
|
)
|
|
4482
4368
|
);
|
|
4483
4369
|
};
|
|
4484
|
-
|
|
4485
|
-
|
|
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);
|
|
4486
4386
|
child.once("error", onError);
|
|
4487
4387
|
child.once("exit", onExit);
|
|
4488
4388
|
});
|
|
4489
4389
|
}
|
|
4490
4390
|
function printMountStartSummary(config, pid, mode) {
|
|
4491
4391
|
console.log();
|
|
4492
|
-
console.log(
|
|
4493
|
-
console.log(
|
|
4494
|
-
console.log(
|
|
4495
|
-
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`));
|
|
4496
4396
|
console.log();
|
|
4497
4397
|
console.log(
|
|
4498
|
-
|
|
4499
|
-
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."
|
|
4500
4400
|
)
|
|
4501
4401
|
);
|
|
4502
4402
|
console.log();
|
|
4503
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
|
+
}
|
|
4504
4731
|
function processExists2(pid) {
|
|
4505
4732
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
4506
4733
|
return false;
|
|
@@ -4516,7 +4743,7 @@ function revealMountRootInFinder(rootPath) {
|
|
|
4516
4743
|
if (process.platform !== "darwin") {
|
|
4517
4744
|
return;
|
|
4518
4745
|
}
|
|
4519
|
-
const child =
|
|
4746
|
+
const child = spawn3("open", [rootPath], {
|
|
4520
4747
|
stdio: "ignore",
|
|
4521
4748
|
detached: true
|
|
4522
4749
|
});
|
|
@@ -4527,35 +4754,36 @@ function revealMountRootInFinder(rootPath) {
|
|
|
4527
4754
|
function formatMountState(state) {
|
|
4528
4755
|
switch (state) {
|
|
4529
4756
|
case "mounted":
|
|
4530
|
-
return
|
|
4757
|
+
return chalk10.green(state);
|
|
4758
|
+
case "syncing":
|
|
4531
4759
|
case "reconnecting":
|
|
4532
|
-
return
|
|
4760
|
+
return chalk10.cyan(state);
|
|
4533
4761
|
case "degraded":
|
|
4534
4762
|
case "pending":
|
|
4535
|
-
return
|
|
4763
|
+
return chalk10.yellow(state);
|
|
4536
4764
|
default:
|
|
4537
|
-
return
|
|
4765
|
+
return chalk10.red(state);
|
|
4538
4766
|
}
|
|
4539
4767
|
}
|
|
4540
4768
|
|
|
4541
4769
|
// src/commands/logout.ts
|
|
4542
|
-
import { Command as
|
|
4543
|
-
import
|
|
4544
|
-
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(() => {
|
|
4545
4773
|
if (!getStoredAPIKey()) {
|
|
4546
4774
|
console.log();
|
|
4547
|
-
console.log(
|
|
4775
|
+
console.log(chalk11.dim(" Not logged in."));
|
|
4548
4776
|
if (hasEnvAPIKey()) {
|
|
4549
|
-
console.log(
|
|
4777
|
+
console.log(chalk11.dim(" Environment API key is still active in this shell."));
|
|
4550
4778
|
}
|
|
4551
4779
|
console.log();
|
|
4552
4780
|
return;
|
|
4553
4781
|
}
|
|
4554
4782
|
clearAPIKey();
|
|
4555
4783
|
console.log();
|
|
4556
|
-
console.log(
|
|
4784
|
+
console.log(chalk11.green(" Logged out."));
|
|
4557
4785
|
if (hasEnvAPIKey()) {
|
|
4558
|
-
console.log(
|
|
4786
|
+
console.log(chalk11.dim(" Environment API key is still active in this shell."));
|
|
4559
4787
|
}
|
|
4560
4788
|
console.log();
|
|
4561
4789
|
});
|
|
@@ -4563,8 +4791,8 @@ var logoutCommand = new Command12("logout").description("Remove stored API key")
|
|
|
4563
4791
|
// src/commands/upgrade.ts
|
|
4564
4792
|
import { spawnSync } from "child_process";
|
|
4565
4793
|
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
4566
|
-
import { Command as
|
|
4567
|
-
import
|
|
4794
|
+
import { Command as Command14 } from "commander";
|
|
4795
|
+
import chalk12 from "chalk";
|
|
4568
4796
|
import ora10 from "ora";
|
|
4569
4797
|
var pkg2 = JSON.parse(
|
|
4570
4798
|
readFileSync2(new URL("../package.json", import.meta.url), "utf8")
|
|
@@ -4676,12 +4904,12 @@ function attemptBundledMutagenRefresh(executablePath) {
|
|
|
4676
4904
|
}
|
|
4677
4905
|
console.log();
|
|
4678
4906
|
console.log(
|
|
4679
|
-
|
|
4907
|
+
chalk12.yellow(
|
|
4680
4908
|
" CLI updated, but bundled Mutagen did not finish installing. `computer mount` will retry on first use."
|
|
4681
4909
|
)
|
|
4682
4910
|
);
|
|
4683
4911
|
}
|
|
4684
|
-
var upgradeCommand = new
|
|
4912
|
+
var upgradeCommand = new Command14("upgrade").description("Update the CLI to the latest version").action(async () => {
|
|
4685
4913
|
const currentVersion = pkg2.version ?? "0.0.0";
|
|
4686
4914
|
const packageName = pkg2.name ?? "aicomputer";
|
|
4687
4915
|
const spinner = ora10("Checking for updates...").start();
|
|
@@ -4714,9 +4942,9 @@ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the
|
|
|
4714
4942
|
spinner.stop();
|
|
4715
4943
|
console.log();
|
|
4716
4944
|
console.log(
|
|
4717
|
-
|
|
4945
|
+
chalk12.dim(` Updating ${chalk12.bold(`v${currentVersion}`)} -> ${chalk12.bold(`v${latestVersion}`)}`)
|
|
4718
4946
|
);
|
|
4719
|
-
console.log(
|
|
4947
|
+
console.log(chalk12.dim(` ${upgrade.label}`));
|
|
4720
4948
|
console.log();
|
|
4721
4949
|
const result = spawnSync(upgrade.command, upgrade.args, {
|
|
4722
4950
|
stdio: "inherit"
|
|
@@ -4726,7 +4954,7 @@ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the
|
|
|
4726
4954
|
attemptBundledMutagenRefresh(executablePath);
|
|
4727
4955
|
}
|
|
4728
4956
|
console.log();
|
|
4729
|
-
console.log(
|
|
4957
|
+
console.log(chalk12.green(` Updated to v${latestVersion}.`));
|
|
4730
4958
|
console.log();
|
|
4731
4959
|
return;
|
|
4732
4960
|
}
|
|
@@ -4734,10 +4962,10 @@ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the
|
|
|
4734
4962
|
});
|
|
4735
4963
|
|
|
4736
4964
|
// src/commands/whoami.ts
|
|
4737
|
-
import { Command as
|
|
4738
|
-
import
|
|
4965
|
+
import { Command as Command15 } from "commander";
|
|
4966
|
+
import chalk13 from "chalk";
|
|
4739
4967
|
import ora11 from "ora";
|
|
4740
|
-
var whoamiCommand = new
|
|
4968
|
+
var whoamiCommand = new Command15("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
|
|
4741
4969
|
const spinner = options.json ? null : ora11("Loading user...").start();
|
|
4742
4970
|
try {
|
|
4743
4971
|
const me = await api("/v1/me");
|
|
@@ -4747,39 +4975,66 @@ var whoamiCommand = new Command14("whoami").description("Show current user").opt
|
|
|
4747
4975
|
return;
|
|
4748
4976
|
}
|
|
4749
4977
|
console.log();
|
|
4750
|
-
console.log(
|
|
4978
|
+
console.log(
|
|
4979
|
+
` ${chalk13.bold.white(me.user.display_name || me.user.email)}`
|
|
4980
|
+
);
|
|
4751
4981
|
if (me.user.display_name) {
|
|
4752
|
-
console.log(` ${
|
|
4982
|
+
console.log(` ${chalk13.dim(me.user.email)}`);
|
|
4753
4983
|
}
|
|
4754
4984
|
if (me.api_key.name) {
|
|
4755
|
-
console.log(` ${
|
|
4985
|
+
console.log(` ${chalk13.dim("Key:")} ${me.api_key.name}`);
|
|
4756
4986
|
}
|
|
4757
|
-
|
|
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
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
console.log(` ${chalk13.dim("API:")} ${chalk13.dim(getBaseURL())}`);
|
|
4758
5002
|
console.log();
|
|
4759
5003
|
} catch (error) {
|
|
4760
5004
|
if (spinner) {
|
|
4761
|
-
spinner.fail(
|
|
5005
|
+
spinner.fail(
|
|
5006
|
+
error instanceof Error ? error.message : "Failed to load user"
|
|
5007
|
+
);
|
|
4762
5008
|
} else {
|
|
4763
|
-
console.error(
|
|
5009
|
+
console.error(
|
|
5010
|
+
error instanceof Error ? error.message : "Failed to load user"
|
|
5011
|
+
);
|
|
4764
5012
|
}
|
|
4765
5013
|
process.exit(1);
|
|
4766
5014
|
}
|
|
4767
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
|
+
}
|
|
4768
5023
|
|
|
4769
5024
|
// src/index.ts
|
|
4770
5025
|
var pkg3 = JSON.parse(
|
|
4771
5026
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
4772
5027
|
);
|
|
4773
|
-
var cliName = process.argv[1] ?
|
|
4774
|
-
var program = new
|
|
5028
|
+
var cliName = process.argv[1] ? basename(process.argv[1]) : "agentcomputer";
|
|
5029
|
+
var program = new Command16();
|
|
4775
5030
|
function appendTextSection(lines, title, values) {
|
|
4776
5031
|
if (values.length === 0) {
|
|
4777
5032
|
return;
|
|
4778
5033
|
}
|
|
4779
|
-
lines.push(` ${
|
|
5034
|
+
lines.push(` ${chalk14.dim(title)}`);
|
|
4780
5035
|
lines.push("");
|
|
4781
5036
|
for (const value of values) {
|
|
4782
|
-
lines.push(` ${
|
|
5037
|
+
lines.push(` ${chalk14.white(value)}`);
|
|
4783
5038
|
}
|
|
4784
5039
|
lines.push("");
|
|
4785
5040
|
}
|
|
@@ -4788,10 +5043,10 @@ function appendTableSection(lines, title, entries) {
|
|
|
4788
5043
|
return;
|
|
4789
5044
|
}
|
|
4790
5045
|
const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
|
|
4791
|
-
lines.push(` ${
|
|
5046
|
+
lines.push(` ${chalk14.dim(title)}`);
|
|
4792
5047
|
lines.push("");
|
|
4793
5048
|
for (const entry of entries) {
|
|
4794
|
-
lines.push(` ${
|
|
5049
|
+
lines.push(` ${chalk14.white(padEnd(entry.term, width))}${chalk14.dim(entry.desc)}`);
|
|
4795
5050
|
}
|
|
4796
5051
|
lines.push("");
|
|
4797
5052
|
}
|
|
@@ -4817,10 +5072,10 @@ function formatRootHelp(cmd) {
|
|
|
4817
5072
|
["Other", []]
|
|
4818
5073
|
];
|
|
4819
5074
|
const otherGroup = groups.find(([name]) => name === "Other")[1];
|
|
4820
|
-
lines.push(`${
|
|
5075
|
+
lines.push(`${chalk14.bold(cliName)} ${chalk14.dim(`v${version}`)}`);
|
|
4821
5076
|
lines.push("");
|
|
4822
5077
|
if (cmd.description()) {
|
|
4823
|
-
lines.push(` ${
|
|
5078
|
+
lines.push(` ${chalk14.dim(cmd.description())}`);
|
|
4824
5079
|
lines.push("");
|
|
4825
5080
|
}
|
|
4826
5081
|
appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
|
|
@@ -4829,7 +5084,7 @@ function formatRootHelp(cmd) {
|
|
|
4829
5084
|
const entry = { term: name, desc: sub.description() };
|
|
4830
5085
|
if (["login", "logout", "whoami", "claude-login", "codex-login"].includes(name)) {
|
|
4831
5086
|
groups[0][1].push(entry);
|
|
4832
|
-
} else if (["create", "ls", "get", "rm"].includes(name)) {
|
|
5087
|
+
} else if (["create", "ls", "get", "power-on", "power-off", "rm"].includes(name)) {
|
|
4833
5088
|
groups[1][1].push(entry);
|
|
4834
5089
|
} else if (name === "image") {
|
|
4835
5090
|
groups[2][1].push(entry);
|
|
@@ -4873,10 +5128,10 @@ function formatSubcommandHelp(cmd, helper) {
|
|
|
4873
5128
|
term: helper.optionTerm(option),
|
|
4874
5129
|
desc: helper.optionDescription(option)
|
|
4875
5130
|
}));
|
|
4876
|
-
lines.push(
|
|
5131
|
+
lines.push(chalk14.bold(commandPath(cmd)));
|
|
4877
5132
|
lines.push("");
|
|
4878
5133
|
if (description) {
|
|
4879
|
-
lines.push(` ${
|
|
5134
|
+
lines.push(` ${chalk14.dim(description)}`);
|
|
4880
5135
|
lines.push("");
|
|
4881
5136
|
}
|
|
4882
5137
|
appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
|
|
@@ -4902,6 +5157,7 @@ function applyHelpFormatting(cmd) {
|
|
|
4902
5157
|
program.name(cliName).description("Agent Computer CLI").version(pkg3.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts");
|
|
4903
5158
|
program.addCommand(loginCommand);
|
|
4904
5159
|
program.addCommand(upgradeCommand);
|
|
5160
|
+
program.addCommand(internalInstallAutosshCommand, { hidden: true });
|
|
4905
5161
|
program.addCommand(internalInstallMutagenCommand, { hidden: true });
|
|
4906
5162
|
program.addCommand(logoutCommand);
|
|
4907
5163
|
program.addCommand(whoamiCommand);
|
|
@@ -4910,6 +5166,8 @@ program.addCommand(codexLoginCommand);
|
|
|
4910
5166
|
program.addCommand(createCommand);
|
|
4911
5167
|
program.addCommand(lsCommand);
|
|
4912
5168
|
program.addCommand(getCommand);
|
|
5169
|
+
program.addCommand(powerOnCommand);
|
|
5170
|
+
program.addCommand(powerOffCommand);
|
|
4913
5171
|
program.addCommand(imageCommand);
|
|
4914
5172
|
program.addCommand(agentCommand);
|
|
4915
5173
|
program.addCommand(acpCommand);
|