aicomputer 0.1.13 → 0.1.15
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 +19 -5
- package/dist/chunk-5JVJROSI.js +186 -0
- package/dist/chunk-KQQUR2YX.js +655 -0
- package/dist/chunk-OWK5N76S.js +70 -0
- package/dist/index.js +892 -665
- package/dist/lib/mount-config.d.ts +69 -0
- package/dist/lib/mount-config.js +40 -0
- package/dist/lib/mount-host.d.ts +14 -0
- package/dist/lib/mount-host.js +10 -0
- package/dist/lib/mount-reconcile.d.ts +25 -0
- package/dist/lib/mount-reconcile.js +13 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,400 +1,82 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
formatMountHostInstallGuidance,
|
|
4
|
+
getMountHostValidationIssues
|
|
5
|
+
} from "./chunk-OWK5N76S.js";
|
|
6
|
+
import {
|
|
7
|
+
ApiError,
|
|
8
|
+
api,
|
|
9
|
+
apiWithKey,
|
|
10
|
+
clearAPIKey,
|
|
11
|
+
createBrowserAccess,
|
|
12
|
+
createComputer,
|
|
13
|
+
deleteComputer,
|
|
14
|
+
deletePublishedPort,
|
|
15
|
+
formatStatus,
|
|
16
|
+
getAPIKey,
|
|
17
|
+
getBaseURL,
|
|
18
|
+
getComputerByID,
|
|
19
|
+
getConnectionInfo,
|
|
20
|
+
getFilesystemSettings,
|
|
21
|
+
getStoredAPIKey,
|
|
22
|
+
getWebURL,
|
|
23
|
+
hasEnvAPIKey,
|
|
24
|
+
listComputers,
|
|
25
|
+
listPublishedPorts,
|
|
26
|
+
minuteSecondAgo,
|
|
27
|
+
padEnd,
|
|
28
|
+
promptForSSHComputer,
|
|
29
|
+
publishPort,
|
|
30
|
+
reconcileMounts,
|
|
31
|
+
resolveComputer,
|
|
32
|
+
setAPIKey,
|
|
33
|
+
teardownManagedSessions,
|
|
34
|
+
timeAgo,
|
|
35
|
+
vncURL,
|
|
36
|
+
webURL
|
|
37
|
+
} from "./chunk-KQQUR2YX.js";
|
|
38
|
+
import {
|
|
39
|
+
defaultMountServiceConfig,
|
|
40
|
+
ensureMountDirectories,
|
|
41
|
+
getMountPaths,
|
|
42
|
+
readMountConfig,
|
|
43
|
+
readMountControllerLock,
|
|
44
|
+
readMountStatusSnapshot,
|
|
45
|
+
removeMountControllerLock,
|
|
46
|
+
writeMountConfig,
|
|
47
|
+
writeMountControllerLock,
|
|
48
|
+
writeMountStatusSnapshot
|
|
49
|
+
} from "./chunk-5JVJROSI.js";
|
|
2
50
|
|
|
3
51
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
52
|
+
import { Command as Command14 } from "commander";
|
|
5
53
|
import chalk12 from "chalk";
|
|
6
54
|
import { readFileSync as readFileSync3 } from "fs";
|
|
7
55
|
import { basename as basename2 } from "path";
|
|
8
56
|
|
|
9
57
|
// src/commands/access.ts
|
|
10
58
|
import { Command } from "commander";
|
|
11
|
-
import chalk3 from "chalk";
|
|
12
|
-
import ora from "ora";
|
|
13
|
-
|
|
14
|
-
// src/lib/config.ts
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
import { join } from "path";
|
|
18
|
-
var CONFIG_DIR = join(homedir(), ".computer");
|
|
19
|
-
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
20
|
-
function ensureConfigDir() {
|
|
21
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
22
|
-
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function readConfig() {
|
|
26
|
-
ensureConfigDir();
|
|
27
|
-
if (!existsSync(CONFIG_FILE)) {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function writeConfig(config) {
|
|
37
|
-
ensureConfigDir();
|
|
38
|
-
const tempFile = `${CONFIG_FILE}.${process.pid}.tmp`;
|
|
39
|
-
writeFileSync(tempFile, JSON.stringify(config, null, 2), { mode: 384 });
|
|
40
|
-
renameSync(tempFile, CONFIG_FILE);
|
|
41
|
-
}
|
|
42
|
-
function getAPIKey() {
|
|
43
|
-
const envValue = process.env.COMPUTER_API_KEY ?? process.env.AGENTCOMPUTER_API_KEY;
|
|
44
|
-
if (envValue) {
|
|
45
|
-
return envValue.trim();
|
|
46
|
-
}
|
|
47
|
-
return getStoredAPIKey();
|
|
48
|
-
}
|
|
49
|
-
function getStoredAPIKey() {
|
|
50
|
-
return readConfig().auth?.apiKey?.trim() || null;
|
|
51
|
-
}
|
|
52
|
-
function hasEnvAPIKey() {
|
|
53
|
-
return Boolean(process.env.COMPUTER_API_KEY ?? process.env.AGENTCOMPUTER_API_KEY);
|
|
54
|
-
}
|
|
55
|
-
function setAPIKey(apiKey) {
|
|
56
|
-
const config = readConfig();
|
|
57
|
-
config.auth = { apiKey: apiKey.trim() };
|
|
58
|
-
writeConfig(config);
|
|
59
|
-
}
|
|
60
|
-
function clearAPIKey() {
|
|
61
|
-
const config = readConfig();
|
|
62
|
-
delete config.auth;
|
|
63
|
-
writeConfig(config);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// src/lib/api.ts
|
|
67
|
-
var BASE_URL = process.env.COMPUTER_API_URL ?? process.env.AGENTCOMPUTER_API_URL ?? "https://api.computer.agentcomputer.ai";
|
|
68
|
-
var WEB_URL = process.env.COMPUTER_WEB_URL ?? process.env.AGENTCOMPUTER_WEB_URL ?? resolveDefaultWebURL(BASE_URL);
|
|
69
|
-
var ApiError = class extends Error {
|
|
70
|
-
constructor(status, message) {
|
|
71
|
-
super(message);
|
|
72
|
-
this.status = status;
|
|
73
|
-
this.name = "ApiError";
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
function getBaseURL() {
|
|
77
|
-
return BASE_URL;
|
|
78
|
-
}
|
|
79
|
-
function getWebURL() {
|
|
80
|
-
return WEB_URL;
|
|
81
|
-
}
|
|
82
|
-
async function api(path, options = {}) {
|
|
83
|
-
const apiKey = getAPIKey();
|
|
84
|
-
if (!apiKey) {
|
|
85
|
-
throw new ApiError(401, "not logged in; run 'computer login' first");
|
|
86
|
-
}
|
|
87
|
-
return requestWithKey(apiKey, path, options);
|
|
88
|
-
}
|
|
89
|
-
function resolveDefaultWebURL(apiURL) {
|
|
90
|
-
try {
|
|
91
|
-
const parsed = new URL(apiURL);
|
|
92
|
-
if (parsed.hostname === "api.computer.agentcomputer.ai") {
|
|
93
|
-
return "https://agentcomputer.ai";
|
|
94
|
-
}
|
|
95
|
-
if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
|
|
96
|
-
return `${parsed.protocol}//${parsed.hostname}:3000`;
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
return "https://agentcomputer.ai";
|
|
100
|
-
}
|
|
101
|
-
return "https://agentcomputer.ai";
|
|
102
|
-
}
|
|
103
|
-
async function apiWithKey(apiKey, path, options = {}) {
|
|
104
|
-
return requestWithKey(apiKey, path, options);
|
|
105
|
-
}
|
|
106
|
-
async function requestWithKey(apiKey, path, options) {
|
|
107
|
-
const headers = {
|
|
108
|
-
Accept: "application/json",
|
|
109
|
-
...options.headers ?? {}
|
|
110
|
-
};
|
|
111
|
-
if (options.body !== void 0) {
|
|
112
|
-
headers["Content-Type"] = "application/json";
|
|
113
|
-
}
|
|
114
|
-
if (apiKey) {
|
|
115
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
116
|
-
}
|
|
117
|
-
const response = await fetch(`${BASE_URL}${path}`, {
|
|
118
|
-
...options,
|
|
119
|
-
headers
|
|
120
|
-
});
|
|
121
|
-
if (!response.ok) {
|
|
122
|
-
throw new ApiError(response.status, await readErrorMessage(response));
|
|
123
|
-
}
|
|
124
|
-
if (response.status === 204) {
|
|
125
|
-
return void 0;
|
|
126
|
-
}
|
|
127
|
-
return await response.json();
|
|
128
|
-
}
|
|
129
|
-
async function readErrorMessage(response) {
|
|
130
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
131
|
-
if (contentType.includes("application/json")) {
|
|
132
|
-
try {
|
|
133
|
-
const payload = await response.json();
|
|
134
|
-
if (payload.error) {
|
|
135
|
-
return payload.error;
|
|
136
|
-
}
|
|
137
|
-
return JSON.stringify(payload);
|
|
138
|
-
} catch {
|
|
139
|
-
return response.statusText || "request failed";
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const body = await response.text();
|
|
143
|
-
return body || response.statusText || "request failed";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// src/lib/computers.ts
|
|
147
|
-
async function listComputers() {
|
|
148
|
-
const response = await api("/v1/computers");
|
|
149
|
-
return response.computers;
|
|
150
|
-
}
|
|
151
|
-
async function getComputerByID(id) {
|
|
152
|
-
return api(`/v1/computers/${id}`);
|
|
153
|
-
}
|
|
154
|
-
async function createComputer(input) {
|
|
155
|
-
return api("/v1/computers", {
|
|
156
|
-
method: "POST",
|
|
157
|
-
body: JSON.stringify(input)
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
async function deleteComputer(computerID) {
|
|
161
|
-
return api(`/v1/computers/${computerID}`, {
|
|
162
|
-
method: "DELETE"
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
async function getFilesystemSettings() {
|
|
166
|
-
return api("/v1/me/filesystem");
|
|
167
|
-
}
|
|
168
|
-
async function getConnectionInfo(computerID) {
|
|
169
|
-
return api(`/v1/computers/${computerID}/connection`);
|
|
170
|
-
}
|
|
171
|
-
async function createBrowserAccess(computerID) {
|
|
172
|
-
return api(`/v1/computers/${computerID}/access/browser`, {
|
|
173
|
-
method: "POST"
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
async function createTerminalAccess(computerID) {
|
|
177
|
-
return api(`/v1/computers/${computerID}/access/terminal`, {
|
|
178
|
-
method: "POST"
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
async function listPublishedPorts(computerID) {
|
|
182
|
-
const response = await api(`/v1/computers/${computerID}/ports`);
|
|
183
|
-
return response.ports;
|
|
184
|
-
}
|
|
185
|
-
async function publishPort(computerID, input) {
|
|
186
|
-
return api(`/v1/computers/${computerID}/ports`, {
|
|
187
|
-
method: "POST",
|
|
188
|
-
body: JSON.stringify(input)
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
async function deletePublishedPort(computerID, targetPort) {
|
|
192
|
-
return api(`/v1/computers/${computerID}/ports/${targetPort}`, {
|
|
193
|
-
method: "DELETE"
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
async function resolveComputer(identifier) {
|
|
197
|
-
try {
|
|
198
|
-
return await getComputerByID(identifier);
|
|
199
|
-
} catch (error) {
|
|
200
|
-
if (!(error instanceof Error) || !("status" in error)) {
|
|
201
|
-
throw error;
|
|
202
|
-
}
|
|
203
|
-
const status = Reflect.get(error, "status");
|
|
204
|
-
if (status !== 404) {
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const computers = await listComputers();
|
|
209
|
-
const exact = computers.find(
|
|
210
|
-
(computer) => computer.handle === identifier || computer.id === identifier
|
|
211
|
-
);
|
|
212
|
-
if (exact) {
|
|
213
|
-
return exact;
|
|
214
|
-
}
|
|
215
|
-
throw new Error(`computer '${identifier}' not found`);
|
|
216
|
-
}
|
|
217
|
-
function webURL(computer) {
|
|
218
|
-
return `https://${computer.primary_web_host}${normalizePrimaryPath(computer.primary_path)}`;
|
|
219
|
-
}
|
|
220
|
-
function vncURL(computer) {
|
|
221
|
-
if (!computer.vnc_enabled) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
const domain = computer.primary_web_host.replace(/^[^.]+\./, "");
|
|
225
|
-
return `https://6080--${computer.handle}.${domain}`;
|
|
226
|
-
}
|
|
227
|
-
function terminalURL(computer) {
|
|
228
|
-
if (computer.runtime_family !== "managed-worker") {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
const domain = computer.primary_web_host.replace(/^[^.]+\./, "");
|
|
232
|
-
return `https://8788--${computer.handle}.${domain}`;
|
|
233
|
-
}
|
|
234
|
-
function normalizePrimaryPath(primaryPath) {
|
|
235
|
-
const trimmed = primaryPath?.trim();
|
|
236
|
-
if (!trimmed) {
|
|
237
|
-
return "/";
|
|
238
|
-
}
|
|
239
|
-
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// src/lib/computer-picker.ts
|
|
243
|
-
import { select } from "@inquirer/prompts";
|
|
244
|
-
import chalk2 from "chalk";
|
|
245
|
-
|
|
246
|
-
// src/lib/format.ts
|
|
247
59
|
import chalk from "chalk";
|
|
248
|
-
|
|
249
|
-
const visible = str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
250
|
-
return str + " ".repeat(Math.max(0, len - visible.length));
|
|
251
|
-
}
|
|
252
|
-
function timeAgo(dateStr) {
|
|
253
|
-
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
|
|
254
|
-
if (seconds < 60) return `${seconds}s ago`;
|
|
255
|
-
const minutes = Math.floor(seconds / 60);
|
|
256
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
257
|
-
const hours = Math.floor(minutes / 60);
|
|
258
|
-
if (hours < 24) return `${hours}h ago`;
|
|
259
|
-
const days = Math.floor(hours / 24);
|
|
260
|
-
return `${days}d ago`;
|
|
261
|
-
}
|
|
262
|
-
function formatStatus(status) {
|
|
263
|
-
switch (status) {
|
|
264
|
-
case "running":
|
|
265
|
-
return chalk.green(status);
|
|
266
|
-
case "pending":
|
|
267
|
-
case "provisioning":
|
|
268
|
-
case "starting":
|
|
269
|
-
return chalk.yellow(status);
|
|
270
|
-
case "stopping":
|
|
271
|
-
case "stopped":
|
|
272
|
-
case "deleted":
|
|
273
|
-
return chalk.gray(status);
|
|
274
|
-
case "error":
|
|
275
|
-
return chalk.red(status);
|
|
276
|
-
default:
|
|
277
|
-
return status;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// src/lib/computer-picker.ts
|
|
282
|
-
async function promptForSSHComputer(computers, message) {
|
|
283
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
284
|
-
throw new Error("computer id or handle is required when not running interactively");
|
|
285
|
-
}
|
|
286
|
-
const available = computers.filter(isSSHSelectable);
|
|
287
|
-
if (available.length === 0) {
|
|
288
|
-
if (computers.length === 0) {
|
|
289
|
-
throw new Error("no computers found");
|
|
290
|
-
}
|
|
291
|
-
throw new Error("no running computers with SSH enabled");
|
|
292
|
-
}
|
|
293
|
-
const handleWidth = Math.max(6, ...available.map((computer) => computer.handle.length));
|
|
294
|
-
const selectedID = await select({
|
|
295
|
-
message,
|
|
296
|
-
pageSize: Math.min(available.length, 10),
|
|
297
|
-
choices: available.map((computer) => ({
|
|
298
|
-
name: `${padEnd(chalk2.white(computer.handle), handleWidth + 2)}${padEnd(formatStatus(computer.status), 12)}${chalk2.dim(describeSSHChoice(computer))}`,
|
|
299
|
-
value: computer.id
|
|
300
|
-
}))
|
|
301
|
-
});
|
|
302
|
-
return available.find((computer) => computer.id === selectedID) ?? available[0];
|
|
303
|
-
}
|
|
304
|
-
function isSSHSelectable(computer) {
|
|
305
|
-
return computer.ssh_enabled && computer.status === "running";
|
|
306
|
-
}
|
|
307
|
-
function describeSSHChoice(computer) {
|
|
308
|
-
const displayName = computer.display_name.trim();
|
|
309
|
-
if (displayName && displayName !== computer.handle) {
|
|
310
|
-
return `${displayName} ${timeAgo(computer.updated_at)}`;
|
|
311
|
-
}
|
|
312
|
-
return `${computer.runtime_family} ${timeAgo(computer.updated_at)}`;
|
|
313
|
-
}
|
|
60
|
+
import ora from "ora";
|
|
314
61
|
|
|
315
|
-
// src/lib/ssh-
|
|
316
|
-
import {
|
|
317
|
-
import { join as join2 } from "path";
|
|
318
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
319
|
-
var MANAGED_BLOCK_START = "# >>> agentcomputer ssh setup >>>";
|
|
320
|
-
var MANAGED_BLOCK_END = "# <<< agentcomputer ssh setup <<<";
|
|
321
|
-
async function ensureSSHAliasConfig(options) {
|
|
322
|
-
const sshDir = join2(homedir2(), ".ssh");
|
|
323
|
-
const configPath = join2(sshDir, "config");
|
|
324
|
-
await mkdir(sshDir, { recursive: true, mode: 448 });
|
|
325
|
-
let existing = "";
|
|
326
|
-
try {
|
|
327
|
-
existing = await readFile(configPath, "utf8");
|
|
328
|
-
} catch (error) {
|
|
329
|
-
if (error.code !== "ENOENT") {
|
|
330
|
-
throw error;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
const nextBlock = renderManagedBlock(options);
|
|
334
|
-
const managedBlockPattern = new RegExp(
|
|
335
|
-
`${escapeRegex(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegex(MANAGED_BLOCK_END)}\\n?`,
|
|
336
|
-
"m"
|
|
337
|
-
);
|
|
338
|
-
let nextContents;
|
|
339
|
-
if (managedBlockPattern.test(existing)) {
|
|
340
|
-
nextContents = existing.replace(managedBlockPattern, nextBlock);
|
|
341
|
-
} else {
|
|
342
|
-
const normalized = existing.length === 0 ? "" : existing.endsWith("\n") ? existing : `${existing}
|
|
343
|
-
`;
|
|
344
|
-
nextContents = normalized.length === 0 ? nextBlock : `${normalized}
|
|
345
|
-
${nextBlock}`;
|
|
346
|
-
}
|
|
347
|
-
const changed = nextContents !== existing;
|
|
348
|
-
if (changed) {
|
|
349
|
-
await writeFile(configPath, nextContents, { mode: 384 });
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
configPath,
|
|
353
|
-
changed
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
function renderManagedBlock(options) {
|
|
357
|
-
const user = options.user?.trim() || "agentcomputer";
|
|
358
|
-
const identityFile = formatIdentityFilePath(options.identityFilePath);
|
|
359
|
-
return `${MANAGED_BLOCK_START}
|
|
360
|
-
Host ${options.alias}
|
|
361
|
-
HostName ${options.host}
|
|
362
|
-
Port ${options.port}
|
|
363
|
-
User ${user}
|
|
364
|
-
IdentityFile ${identityFile}
|
|
365
|
-
IdentitiesOnly yes
|
|
366
|
-
ServerAliveInterval 30
|
|
367
|
-
ServerAliveCountMax 4
|
|
368
|
-
${MANAGED_BLOCK_END}
|
|
369
|
-
`;
|
|
370
|
-
}
|
|
371
|
-
function formatIdentityFilePath(path) {
|
|
372
|
-
const normalized = path.trim();
|
|
373
|
-
const homePath = `${homedir2()}/`;
|
|
374
|
-
if (normalized.startsWith(homePath)) {
|
|
375
|
-
return `~/${normalized.slice(homePath.length)}`;
|
|
376
|
-
}
|
|
377
|
-
return normalized.replaceAll(" ", "\\ ");
|
|
378
|
-
}
|
|
379
|
-
function escapeRegex(value) {
|
|
380
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
381
|
-
}
|
|
62
|
+
// src/lib/ssh-access.ts
|
|
63
|
+
import { spawn } from "child_process";
|
|
382
64
|
|
|
383
65
|
// src/lib/ssh-keys.ts
|
|
384
66
|
import { basename } from "path";
|
|
385
|
-
import { homedir
|
|
386
|
-
import { readFile
|
|
67
|
+
import { homedir } from "os";
|
|
68
|
+
import { readFile, mkdir } from "fs/promises";
|
|
387
69
|
import { execFileSync } from "child_process";
|
|
388
|
-
import { existsSync
|
|
70
|
+
import { existsSync } from "fs";
|
|
389
71
|
var DEFAULT_PUBLIC_KEY_PATHS = [
|
|
390
|
-
`${
|
|
391
|
-
`${
|
|
392
|
-
`${
|
|
72
|
+
`${homedir()}/.ssh/id_ed25519.pub`,
|
|
73
|
+
`${homedir()}/.ssh/id_ecdsa.pub`,
|
|
74
|
+
`${homedir()}/.ssh/id_rsa.pub`
|
|
393
75
|
];
|
|
394
76
|
async function ensureDefaultSSHKeyRegistered() {
|
|
395
77
|
for (const path of DEFAULT_PUBLIC_KEY_PATHS) {
|
|
396
78
|
try {
|
|
397
|
-
const publicKey2 = (await
|
|
79
|
+
const publicKey2 = (await readFile(path, "utf8")).trim();
|
|
398
80
|
if (!publicKey2) {
|
|
399
81
|
continue;
|
|
400
82
|
}
|
|
@@ -418,7 +100,7 @@ async function ensureDefaultSSHKeyRegistered() {
|
|
|
418
100
|
}
|
|
419
101
|
}
|
|
420
102
|
const generated = await generateSSHKey();
|
|
421
|
-
const publicKey = (await
|
|
103
|
+
const publicKey = (await readFile(generated.publicKeyPath, "utf8")).trim();
|
|
422
104
|
const key = await api("/v1/ssh-keys", {
|
|
423
105
|
method: "POST",
|
|
424
106
|
body: JSON.stringify({
|
|
@@ -433,9 +115,9 @@ async function ensureDefaultSSHKeyRegistered() {
|
|
|
433
115
|
};
|
|
434
116
|
}
|
|
435
117
|
async function generateSSHKey() {
|
|
436
|
-
const sshDir = `${
|
|
437
|
-
if (!
|
|
438
|
-
await
|
|
118
|
+
const sshDir = `${homedir()}/.ssh`;
|
|
119
|
+
if (!existsSync(sshDir)) {
|
|
120
|
+
await mkdir(sshDir, { mode: 448 });
|
|
439
121
|
}
|
|
440
122
|
const privateKeyPath = `${sshDir}/id_ed25519`;
|
|
441
123
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
@@ -447,7 +129,6 @@ async function generateSSHKey() {
|
|
|
447
129
|
}
|
|
448
130
|
|
|
449
131
|
// src/lib/ssh-access.ts
|
|
450
|
-
import { spawn } from "child_process";
|
|
451
132
|
async function prepareSSHConnection(computer) {
|
|
452
133
|
const registered = await ensureDefaultSSHKeyRegistered();
|
|
453
134
|
const info = await getConnectionInfo(computer.id);
|
|
@@ -504,6 +185,11 @@ import { constants } from "fs";
|
|
|
504
185
|
import { access } from "fs/promises";
|
|
505
186
|
import open from "open";
|
|
506
187
|
var IMAGE_BROWSER_LAUNCHER = "/usr/local/bin/browser-launcher";
|
|
188
|
+
var IMAGE_BROWSER_CANDIDATES = [
|
|
189
|
+
"/usr/local/bin/google-chrome",
|
|
190
|
+
"/usr/local/bin/chromium",
|
|
191
|
+
IMAGE_BROWSER_LAUNCHER
|
|
192
|
+
];
|
|
507
193
|
async function isExecutable(path) {
|
|
508
194
|
try {
|
|
509
195
|
await access(path, constants.X_OK);
|
|
@@ -518,6 +204,8 @@ function hasBrokenChromeBrowserEnv() {
|
|
|
518
204
|
case "chrome":
|
|
519
205
|
case "google-chrome":
|
|
520
206
|
case "google chrome":
|
|
207
|
+
case "browser":
|
|
208
|
+
case "browser-launcher":
|
|
521
209
|
return true;
|
|
522
210
|
default:
|
|
523
211
|
return false;
|
|
@@ -531,9 +219,12 @@ async function openBrowserURL(url) {
|
|
|
531
219
|
if (hasBrokenChromeBrowserEnv()) {
|
|
532
220
|
delete process.env.BROWSER;
|
|
533
221
|
}
|
|
534
|
-
|
|
222
|
+
for (const browserPath of IMAGE_BROWSER_CANDIDATES) {
|
|
223
|
+
if (!await isExecutable(browserPath)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
535
226
|
try {
|
|
536
|
-
await open(url, { app: { name:
|
|
227
|
+
await open(url, { app: { name: browserPath } });
|
|
537
228
|
return;
|
|
538
229
|
} catch {
|
|
539
230
|
}
|
|
@@ -546,39 +237,146 @@ async function openBrowserURL(url) {
|
|
|
546
237
|
await open(url);
|
|
547
238
|
}
|
|
548
239
|
|
|
240
|
+
// src/lib/ssh-config.ts
|
|
241
|
+
import { homedir as homedir2 } from "os";
|
|
242
|
+
import { join } from "path";
|
|
243
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
244
|
+
var MANAGED_BLOCK_START = "# >>> agentcomputer ssh setup >>>";
|
|
245
|
+
var MANAGED_BLOCK_END = "# <<< agentcomputer ssh setup <<<";
|
|
246
|
+
async function ensureSSHAliasConfig(options) {
|
|
247
|
+
const sshDir = join(homedir2(), ".ssh");
|
|
248
|
+
const configPath = join(sshDir, "config");
|
|
249
|
+
await mkdir2(sshDir, { recursive: true, mode: 448 });
|
|
250
|
+
let existing = "";
|
|
251
|
+
try {
|
|
252
|
+
existing = await readFile2(configPath, "utf8");
|
|
253
|
+
} catch (error) {
|
|
254
|
+
if (error.code !== "ENOENT") {
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const nextBlock = renderManagedBlock(options);
|
|
259
|
+
const managedBlockPattern = new RegExp(
|
|
260
|
+
`${escapeRegex(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegex(MANAGED_BLOCK_END)}\\n?`,
|
|
261
|
+
"m"
|
|
262
|
+
);
|
|
263
|
+
let nextContents;
|
|
264
|
+
if (managedBlockPattern.test(existing)) {
|
|
265
|
+
nextContents = existing.replace(managedBlockPattern, nextBlock);
|
|
266
|
+
} else {
|
|
267
|
+
const normalized = existing.length === 0 ? "" : existing.endsWith("\n") ? existing : `${existing}
|
|
268
|
+
`;
|
|
269
|
+
nextContents = normalized.length === 0 ? nextBlock : `${normalized}
|
|
270
|
+
${nextBlock}`;
|
|
271
|
+
}
|
|
272
|
+
const changed = nextContents !== existing;
|
|
273
|
+
if (changed) {
|
|
274
|
+
await writeFile(configPath, nextContents, { mode: 384 });
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
configPath,
|
|
278
|
+
changed
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function renderManagedBlock(options) {
|
|
282
|
+
const user = options.user?.trim() || "agentcomputer";
|
|
283
|
+
const identityFile = formatIdentityFilePath(options.identityFilePath);
|
|
284
|
+
return `${MANAGED_BLOCK_START}
|
|
285
|
+
Host ${options.alias}
|
|
286
|
+
HostName ${options.host}
|
|
287
|
+
Port ${options.port}
|
|
288
|
+
User ${user}
|
|
289
|
+
IdentityFile ${identityFile}
|
|
290
|
+
IdentitiesOnly yes
|
|
291
|
+
ServerAliveInterval 30
|
|
292
|
+
ServerAliveCountMax 4
|
|
293
|
+
${MANAGED_BLOCK_END}
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
function formatIdentityFilePath(path) {
|
|
297
|
+
const normalized = path.trim();
|
|
298
|
+
const homePath = `${homedir2()}/`;
|
|
299
|
+
if (normalized.startsWith(homePath)) {
|
|
300
|
+
return `~/${normalized.slice(homePath.length)}`;
|
|
301
|
+
}
|
|
302
|
+
return normalized.replaceAll(" ", "\\ ");
|
|
303
|
+
}
|
|
304
|
+
function escapeRegex(value) {
|
|
305
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/lib/ssh-setup.ts
|
|
309
|
+
async function ensureSSHAccessConfigured(options = {}) {
|
|
310
|
+
const alias = normalizeSSHAlias(options.alias ?? "agentcomputer.ai");
|
|
311
|
+
const host = normalizeSSHHost(options.host ?? "ssh.agentcomputer.ai");
|
|
312
|
+
const port = typeof options.port === "number" ? parseSSHPort(String(options.port)) : parseSSHPort(options.port ?? "443");
|
|
313
|
+
const registered = await ensureDefaultSSHKeyRegistered();
|
|
314
|
+
const configResult = await ensureSSHAliasConfig({
|
|
315
|
+
alias,
|
|
316
|
+
host,
|
|
317
|
+
port,
|
|
318
|
+
user: "agentcomputer",
|
|
319
|
+
identityFilePath: registered.privateKeyPath
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
alias,
|
|
323
|
+
host,
|
|
324
|
+
port,
|
|
325
|
+
configPath: configResult.configPath,
|
|
326
|
+
identityFilePath: registered.privateKeyPath,
|
|
327
|
+
changed: configResult.changed
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function normalizeSSHAlias(value) {
|
|
331
|
+
const alias = value.trim();
|
|
332
|
+
if (!alias) {
|
|
333
|
+
throw new Error("ssh alias cannot be empty");
|
|
334
|
+
}
|
|
335
|
+
if (!/^[A-Za-z0-9._-]+$/.test(alias)) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
"ssh alias may contain only letters, numbers, dot, dash, or underscore"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return alias;
|
|
341
|
+
}
|
|
342
|
+
function normalizeSSHHost(value) {
|
|
343
|
+
const host = value.trim();
|
|
344
|
+
if (!host) {
|
|
345
|
+
throw new Error("ssh host cannot be empty");
|
|
346
|
+
}
|
|
347
|
+
if (host.includes(" ")) {
|
|
348
|
+
throw new Error("ssh host cannot contain spaces");
|
|
349
|
+
}
|
|
350
|
+
return host;
|
|
351
|
+
}
|
|
352
|
+
function parseSSHPort(value) {
|
|
353
|
+
const parsed = Number.parseInt(value, 10);
|
|
354
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
355
|
+
throw new Error("ssh port must be between 1 and 65535");
|
|
356
|
+
}
|
|
357
|
+
return parsed;
|
|
358
|
+
}
|
|
359
|
+
|
|
549
360
|
// src/commands/access.ts
|
|
550
|
-
var openCommand = new Command("open").description("Open a computer in your browser").argument("<id-or-handle>", "Computer id or handle").option("--vnc", "Open VNC desktop instead of gateway").
|
|
361
|
+
var openCommand = new Command("open").description("Open a computer in your browser").argument("<id-or-handle>", "Computer id or handle").option("--vnc", "Open VNC desktop instead of gateway").action(async (identifier, options) => {
|
|
551
362
|
const spinner = ora("Preparing access...").start();
|
|
552
363
|
try {
|
|
553
364
|
const computer = await resolveComputer(identifier);
|
|
554
365
|
const info = await getConnectionInfo(computer.id);
|
|
555
|
-
if (options.vnc && options.terminal) {
|
|
556
|
-
throw new Error("choose either --vnc or --terminal");
|
|
557
|
-
}
|
|
558
366
|
if (options.vnc) {
|
|
559
367
|
if (!info.connection.vnc_available) {
|
|
560
368
|
throw new Error("VNC is not available for this computer");
|
|
561
369
|
}
|
|
562
370
|
const url = info.connection.vnc_url;
|
|
563
|
-
spinner.succeed(`Opening VNC for ${
|
|
371
|
+
spinner.succeed(`Opening VNC for ${chalk.bold(computer.handle)}`);
|
|
564
372
|
await openBrowserURL(url);
|
|
565
|
-
console.log(
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (options.terminal) {
|
|
569
|
-
if (!info.connection.terminal_available) {
|
|
570
|
-
throw new Error("Terminal access is not available for this computer");
|
|
571
|
-
}
|
|
572
|
-
const access3 = await createTerminalAccess(computer.id);
|
|
573
|
-
spinner.succeed(`Opening terminal for ${chalk3.bold(computer.handle)}`);
|
|
574
|
-
await openBrowserURL(access3.access_url);
|
|
575
|
-
console.log(chalk3.dim(` ${access3.access_url}`));
|
|
373
|
+
console.log(chalk.dim(` ${url}`));
|
|
576
374
|
return;
|
|
577
375
|
}
|
|
578
376
|
const access2 = await createBrowserAccess(computer.id);
|
|
579
|
-
spinner.succeed(`Opening ${
|
|
377
|
+
spinner.succeed(`Opening ${chalk.bold(computer.handle)}`);
|
|
580
378
|
await openBrowserURL(access2.access_url);
|
|
581
|
-
console.log(
|
|
379
|
+
console.log(chalk.dim(` ${access2.access_url}`));
|
|
582
380
|
} catch (error) {
|
|
583
381
|
spinner.fail(
|
|
584
382
|
error instanceof Error ? error.message : "Failed to open computer"
|
|
@@ -586,7 +384,7 @@ var openCommand = new Command("open").description("Open a computer in your brows
|
|
|
586
384
|
process.exit(1);
|
|
587
385
|
}
|
|
588
386
|
});
|
|
589
|
-
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", "
|
|
387
|
+
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(
|
|
590
388
|
async (identifier, options) => {
|
|
591
389
|
if (options.setup) {
|
|
592
390
|
await setupSSHAlias(options);
|
|
@@ -598,8 +396,8 @@ var sshCommand = new Command("ssh").description("Open an SSH session to a comput
|
|
|
598
396
|
try {
|
|
599
397
|
const computer = await resolveSSHComputer(identifier, spinner);
|
|
600
398
|
const connection = await prepareSSHConnection(computer);
|
|
601
|
-
spinner.succeed(`Connecting to ${
|
|
602
|
-
console.log(
|
|
399
|
+
spinner.succeed(`Connecting to ${chalk.bold(computer.handle)}`);
|
|
400
|
+
console.log(chalk.dim(` ${connection.command}`));
|
|
603
401
|
console.log();
|
|
604
402
|
await openSSHConnection(connection);
|
|
605
403
|
} catch (error) {
|
|
@@ -621,24 +419,24 @@ portsCommand.command("ls").description("List published ports for a computer").ar
|
|
|
621
419
|
spinner.stop();
|
|
622
420
|
if (ports.length === 0) {
|
|
623
421
|
console.log();
|
|
624
|
-
console.log(
|
|
422
|
+
console.log(chalk.dim(" No published ports."));
|
|
625
423
|
console.log();
|
|
626
424
|
return;
|
|
627
425
|
}
|
|
628
426
|
const subWidth = Math.max(10, ...ports.map((p) => p.subdomain.length));
|
|
629
427
|
console.log();
|
|
630
428
|
console.log(
|
|
631
|
-
` ${
|
|
429
|
+
` ${chalk.dim(padEnd("Subdomain", subWidth + 2))}${chalk.dim(padEnd("Port", 8))}${chalk.dim("Protocol")}`
|
|
632
430
|
);
|
|
633
431
|
console.log(
|
|
634
|
-
` ${
|
|
432
|
+
` ${chalk.dim("-".repeat(subWidth + 2))}${chalk.dim("-".repeat(8))}${chalk.dim("-".repeat(8))}`
|
|
635
433
|
);
|
|
636
434
|
for (const port of ports) {
|
|
637
435
|
const url = `https://${port.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
|
|
638
436
|
console.log(
|
|
639
437
|
` ${padEnd(port.subdomain, subWidth + 2)}${padEnd(String(port.target_port), 8)}${port.protocol}`
|
|
640
438
|
);
|
|
641
|
-
console.log(` ${
|
|
439
|
+
console.log(` ${chalk.dim(url)}`);
|
|
642
440
|
}
|
|
643
441
|
console.log();
|
|
644
442
|
} catch (error) {
|
|
@@ -666,9 +464,9 @@ portsCommand.command("publish").description("Publish an HTTP app port").argument
|
|
|
666
464
|
});
|
|
667
465
|
const url = `https://${published.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
|
|
668
466
|
spinner.succeed(
|
|
669
|
-
`Published port ${
|
|
467
|
+
`Published port ${chalk.bold(String(published.target_port))}`
|
|
670
468
|
);
|
|
671
|
-
console.log(
|
|
469
|
+
console.log(chalk.dim(` ${url}`));
|
|
672
470
|
} catch (error) {
|
|
673
471
|
spinner.fail(
|
|
674
472
|
error instanceof Error ? error.message : "Failed to publish port"
|
|
@@ -685,71 +483,32 @@ portsCommand.command("rm").description("Unpublish an app port").argument("<id-or
|
|
|
685
483
|
}
|
|
686
484
|
const computer = await resolveComputer(identifier);
|
|
687
485
|
await deletePublishedPort(computer.id, targetPort);
|
|
688
|
-
spinner.succeed(`Removed port ${
|
|
486
|
+
spinner.succeed(`Removed port ${chalk.bold(String(targetPort))}`);
|
|
689
487
|
} catch (error) {
|
|
690
488
|
spinner.fail(
|
|
691
489
|
error instanceof Error ? error.message : "Failed to remove port"
|
|
692
490
|
);
|
|
693
|
-
process.exit(1);
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
async function setupSSHAlias(options) {
|
|
697
|
-
const spinner = ora("Configuring global SSH access...").start();
|
|
698
|
-
try {
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
console.log(chalk3.dim(` Identity: ${registered.privateKeyPath}`));
|
|
714
|
-
console.log();
|
|
715
|
-
console.log(` ${chalk3.bold("Shell:")} ssh ${alias}`);
|
|
716
|
-
console.log(` ${chalk3.bold("Direct:")} ssh <handle>@${alias}`);
|
|
717
|
-
console.log();
|
|
718
|
-
} catch (error) {
|
|
719
|
-
spinner.fail(
|
|
720
|
-
error instanceof Error ? error.message : "Failed to configure SSH alias"
|
|
721
|
-
);
|
|
722
|
-
process.exit(1);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
function normalizeSSHAlias(value) {
|
|
726
|
-
const alias = value.trim();
|
|
727
|
-
if (!alias) {
|
|
728
|
-
throw new Error("ssh alias cannot be empty");
|
|
729
|
-
}
|
|
730
|
-
if (!/^[A-Za-z0-9._-]+$/.test(alias)) {
|
|
731
|
-
throw new Error(
|
|
732
|
-
"ssh alias may contain only letters, numbers, dot, dash, or underscore"
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
return alias;
|
|
736
|
-
}
|
|
737
|
-
function normalizeSSHHost(value) {
|
|
738
|
-
const host = value.trim();
|
|
739
|
-
if (!host) {
|
|
740
|
-
throw new Error("ssh host cannot be empty");
|
|
741
|
-
}
|
|
742
|
-
if (host.includes(" ")) {
|
|
743
|
-
throw new Error("ssh host cannot contain spaces");
|
|
744
|
-
}
|
|
745
|
-
return host;
|
|
746
|
-
}
|
|
747
|
-
function parseSSHPort(value) {
|
|
748
|
-
const parsed = Number.parseInt(value, 10);
|
|
749
|
-
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
750
|
-
throw new Error("ssh port must be between 1 and 65535");
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
async function setupSSHAlias(options) {
|
|
495
|
+
const spinner = ora("Configuring global SSH access...").start();
|
|
496
|
+
try {
|
|
497
|
+
const setup = await ensureSSHAccessConfigured(options);
|
|
498
|
+
spinner.succeed(`SSH alias '${setup.alias}' is ready`);
|
|
499
|
+
console.log();
|
|
500
|
+
console.log(chalk.dim(` SSH config: ${setup.configPath}`));
|
|
501
|
+
console.log(chalk.dim(` Identity: ${setup.identityFilePath}`));
|
|
502
|
+
console.log();
|
|
503
|
+
console.log(` ${chalk.bold("Shell:")} ssh ${setup.alias}`);
|
|
504
|
+
console.log(` ${chalk.bold("Direct:")} ssh <handle>@${setup.alias}`);
|
|
505
|
+
console.log();
|
|
506
|
+
} catch (error) {
|
|
507
|
+
spinner.fail(
|
|
508
|
+
error instanceof Error ? error.message : "Failed to configure SSH alias"
|
|
509
|
+
);
|
|
510
|
+
process.exit(1);
|
|
751
511
|
}
|
|
752
|
-
return parsed;
|
|
753
512
|
}
|
|
754
513
|
async function resolveSSHComputer(identifier, spinner) {
|
|
755
514
|
const trimmed = identifier?.trim();
|
|
@@ -769,7 +528,7 @@ async function resolveSSHComputer(identifier, spinner) {
|
|
|
769
528
|
}
|
|
770
529
|
|
|
771
530
|
// src/commands/acp.ts
|
|
772
|
-
import { readFileSync
|
|
531
|
+
import { readFileSync } from "fs";
|
|
773
532
|
import readline from "readline";
|
|
774
533
|
import { Command as Command2 } from "commander";
|
|
775
534
|
|
|
@@ -782,10 +541,6 @@ async function listAgentSessions(computerID) {
|
|
|
782
541
|
const response = await api(`/v1/computers/${computerID}/agent-sessions`);
|
|
783
542
|
return response.sessions;
|
|
784
543
|
}
|
|
785
|
-
async function listFleetAgentSessions() {
|
|
786
|
-
const response = await api("/v1/fleet/agent-sessions");
|
|
787
|
-
return response.sessions;
|
|
788
|
-
}
|
|
789
544
|
async function createAgentSession(computerID, input) {
|
|
790
545
|
const response = await api(`/v1/computers/${computerID}/agent-sessions`, {
|
|
791
546
|
method: "POST",
|
|
@@ -849,7 +604,7 @@ async function openAgentSessionEventsStream(computerID, sessionID, options = {})
|
|
|
849
604
|
signal: options.signal
|
|
850
605
|
});
|
|
851
606
|
if (!response.ok) {
|
|
852
|
-
const message = await
|
|
607
|
+
const message = await readErrorMessage(response);
|
|
853
608
|
throw new ApiError(response.status, message);
|
|
854
609
|
}
|
|
855
610
|
return response;
|
|
@@ -869,7 +624,7 @@ async function waitForSessionToSettle(computerID, sessionID, promptID, options =
|
|
|
869
624
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
870
625
|
}
|
|
871
626
|
}
|
|
872
|
-
async function
|
|
627
|
+
async function readErrorMessage(response) {
|
|
873
628
|
const contentType = response.headers.get("content-type") ?? "";
|
|
874
629
|
if (contentType.includes("application/json")) {
|
|
875
630
|
try {
|
|
@@ -885,7 +640,7 @@ async function readErrorMessage2(response) {
|
|
|
885
640
|
|
|
886
641
|
// src/commands/acp.ts
|
|
887
642
|
var pkg = JSON.parse(
|
|
888
|
-
|
|
643
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
889
644
|
);
|
|
890
645
|
function emit(message) {
|
|
891
646
|
process.stdout.write(`${JSON.stringify(message)}
|
|
@@ -1116,22 +871,22 @@ acpCommand.command("serve").description("Serve a local stdio ACP bridge backed b
|
|
|
1116
871
|
|
|
1117
872
|
// src/commands/agent.ts
|
|
1118
873
|
import { Command as Command3 } from "commander";
|
|
1119
|
-
import
|
|
874
|
+
import chalk2 from "chalk";
|
|
1120
875
|
import ora2 from "ora";
|
|
1121
876
|
function formatAgentSessionStatus(status) {
|
|
1122
877
|
switch (status) {
|
|
1123
878
|
case "idle":
|
|
1124
|
-
return
|
|
879
|
+
return chalk2.green(status);
|
|
1125
880
|
case "running":
|
|
1126
|
-
return
|
|
881
|
+
return chalk2.blue(status);
|
|
1127
882
|
case "cancelling":
|
|
1128
|
-
return
|
|
883
|
+
return chalk2.yellow(status);
|
|
1129
884
|
case "interrupted":
|
|
1130
|
-
return
|
|
885
|
+
return chalk2.yellow(status);
|
|
1131
886
|
case "failed":
|
|
1132
|
-
return
|
|
887
|
+
return chalk2.red(status);
|
|
1133
888
|
case "closed":
|
|
1134
|
-
return
|
|
889
|
+
return chalk2.gray(status);
|
|
1135
890
|
default:
|
|
1136
891
|
return status;
|
|
1137
892
|
}
|
|
@@ -1140,14 +895,14 @@ function printAgents(agents) {
|
|
|
1140
895
|
const idWidth = Math.max(5, ...agents.map((agent) => agent.id.length));
|
|
1141
896
|
console.log();
|
|
1142
897
|
console.log(
|
|
1143
|
-
` ${
|
|
898
|
+
` ${chalk2.dim(padEnd("Agent", idWidth + 2))}${chalk2.dim(padEnd("Installed", 12))}${chalk2.dim(padEnd("Creds", 8))}${chalk2.dim("Version")}`
|
|
1144
899
|
);
|
|
1145
900
|
console.log(
|
|
1146
|
-
` ${
|
|
901
|
+
` ${chalk2.dim("-".repeat(idWidth + 2))}${chalk2.dim("-".repeat(12))}${chalk2.dim("-".repeat(8))}${chalk2.dim("-".repeat(12))}`
|
|
1147
902
|
);
|
|
1148
903
|
for (const agent of agents) {
|
|
1149
904
|
console.log(
|
|
1150
|
-
` ${padEnd(agent.id, idWidth + 2)}${padEnd(agent.installed ?
|
|
905
|
+
` ${padEnd(agent.id, idWidth + 2)}${padEnd(agent.installed ? chalk2.green("yes") : chalk2.gray("no"), 12)}${padEnd(agent.credentialsAvailable ? chalk2.green("yes") : chalk2.yellow("no"), 8)}${agent.version ?? chalk2.dim("unknown")}`
|
|
1151
906
|
);
|
|
1152
907
|
}
|
|
1153
908
|
console.log();
|
|
@@ -1155,7 +910,7 @@ function printAgents(agents) {
|
|
|
1155
910
|
function printSessions(sessions, handleByComputerID = /* @__PURE__ */ new Map()) {
|
|
1156
911
|
if (sessions.length === 0) {
|
|
1157
912
|
console.log();
|
|
1158
|
-
console.log(
|
|
913
|
+
console.log(chalk2.dim(" No agent sessions found."));
|
|
1159
914
|
console.log();
|
|
1160
915
|
return;
|
|
1161
916
|
}
|
|
@@ -1164,19 +919,19 @@ function printSessions(sessions, handleByComputerID = /* @__PURE__ */ new Map())
|
|
|
1164
919
|
const statusWidth = 13;
|
|
1165
920
|
console.log();
|
|
1166
921
|
console.log(
|
|
1167
|
-
` ${
|
|
922
|
+
` ${chalk2.dim(padEnd("Session", 14))}${chalk2.dim(padEnd("Name", nameWidth + 2))}${chalk2.dim(padEnd("Agent", agentWidth + 2))}${chalk2.dim(padEnd("Status", statusWidth + 2))}${chalk2.dim("Location")}`
|
|
1168
923
|
);
|
|
1169
924
|
console.log(
|
|
1170
|
-
` ${
|
|
925
|
+
` ${chalk2.dim("-".repeat(14))}${chalk2.dim("-".repeat(nameWidth + 2))}${chalk2.dim("-".repeat(agentWidth + 2))}${chalk2.dim("-".repeat(statusWidth + 2))}${chalk2.dim("-".repeat(20))}`
|
|
1171
926
|
);
|
|
1172
927
|
for (const session of sessions) {
|
|
1173
928
|
const location = handleByComputerID.get(session.computer_id) ?? session.computer_id;
|
|
1174
929
|
console.log(
|
|
1175
930
|
` ${padEnd(session.id.slice(0, 12), 14)}${padEnd(session.name || "default", nameWidth + 2)}${padEnd(session.agent, agentWidth + 2)}${padEnd(formatAgentSessionStatus(session.status), statusWidth + 2)}${location}`
|
|
1176
931
|
);
|
|
1177
|
-
console.log(` ${
|
|
932
|
+
console.log(` ${chalk2.dim(` cwd=${session.cwd} updated=${timeAgo(session.updated_at)}`)}`);
|
|
1178
933
|
if (session.last_stop_reason || session.last_error) {
|
|
1179
|
-
console.log(` ${
|
|
934
|
+
console.log(` ${chalk2.dim(` ${session.last_error ? `error=${session.last_error}` : `stop=${session.last_stop_reason}`}`)}`);
|
|
1180
935
|
}
|
|
1181
936
|
}
|
|
1182
937
|
console.log();
|
|
@@ -1190,7 +945,7 @@ var StreamPrinter = class {
|
|
|
1190
945
|
}
|
|
1191
946
|
writeDim(text) {
|
|
1192
947
|
this.ensureBreak();
|
|
1193
|
-
process.stdout.write(
|
|
948
|
+
process.stdout.write(chalk2.dim(text));
|
|
1194
949
|
this.inlineOpen = !text.endsWith("\n");
|
|
1195
950
|
}
|
|
1196
951
|
writeLine(text) {
|
|
@@ -1258,7 +1013,7 @@ function renderSSEChunk(chunk, printer, asJson) {
|
|
|
1258
1013
|
try {
|
|
1259
1014
|
envelope = JSON.parse(payload);
|
|
1260
1015
|
} catch {
|
|
1261
|
-
printer.writeLine(
|
|
1016
|
+
printer.writeLine(chalk2.dim(payload));
|
|
1262
1017
|
return;
|
|
1263
1018
|
}
|
|
1264
1019
|
const method = typeof envelope.method === "string" ? envelope.method : "";
|
|
@@ -1285,14 +1040,14 @@ function renderSSEChunk(chunk, printer, asJson) {
|
|
|
1285
1040
|
case "tool_call_update": {
|
|
1286
1041
|
const title = typeof update?.title === "string" && update.title ? update.title : "tool";
|
|
1287
1042
|
const status = typeof update?.status === "string" && update.status ? update.status : "in_progress";
|
|
1288
|
-
printer.writeLine(
|
|
1043
|
+
printer.writeLine(chalk2.dim(`[tool:${status}] ${title}`));
|
|
1289
1044
|
return;
|
|
1290
1045
|
}
|
|
1291
1046
|
case "plan": {
|
|
1292
1047
|
const entries = Array.isArray(update?.entries) ? update.entries : [];
|
|
1293
1048
|
const detail = entries.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => `- [${entry.status ?? "pending"}] ${entry.content ?? ""}`).join("\n");
|
|
1294
1049
|
if (detail) {
|
|
1295
|
-
printer.writeLine(
|
|
1050
|
+
printer.writeLine(chalk2.dim(`Plan
|
|
1296
1051
|
${detail}`));
|
|
1297
1052
|
}
|
|
1298
1053
|
return;
|
|
@@ -1302,7 +1057,7 @@ ${detail}`));
|
|
|
1302
1057
|
}
|
|
1303
1058
|
}
|
|
1304
1059
|
if (method === "session/request_permission") {
|
|
1305
|
-
printer.writeLine(
|
|
1060
|
+
printer.writeLine(chalk2.yellow("Permission requested by remote agent"));
|
|
1306
1061
|
return;
|
|
1307
1062
|
}
|
|
1308
1063
|
}
|
|
@@ -1430,12 +1185,12 @@ agentCommand.command("prompt").description("Send a prompt to a machine agent ses
|
|
|
1430
1185
|
return;
|
|
1431
1186
|
}
|
|
1432
1187
|
console.log();
|
|
1433
|
-
console.log(` ${
|
|
1188
|
+
console.log(` ${chalk2.bold(session.name || "default")} ${formatAgentSessionStatus(settled.status)}`);
|
|
1434
1189
|
if (settled.last_stop_reason) {
|
|
1435
|
-
console.log(
|
|
1190
|
+
console.log(chalk2.dim(` stop_reason=${settled.last_stop_reason}`));
|
|
1436
1191
|
}
|
|
1437
1192
|
if (settled.last_error) {
|
|
1438
|
-
console.log(
|
|
1193
|
+
console.log(chalk2.red(` error=${settled.last_error}`));
|
|
1439
1194
|
}
|
|
1440
1195
|
console.log();
|
|
1441
1196
|
} catch (error) {
|
|
@@ -1541,35 +1296,12 @@ agentCommand.command("close").description("Close and delete a machine agent sess
|
|
|
1541
1296
|
process.exit(1);
|
|
1542
1297
|
}
|
|
1543
1298
|
});
|
|
1544
|
-
var fleetCommand = new Command3("fleet").description("View agent activity across your fleet");
|
|
1545
|
-
fleetCommand.command("status").description("List open agent sessions across all machines").option("--json", "Print raw JSON").action(async (options) => {
|
|
1546
|
-
const spinner = options.json ? null : ora2("Fetching fleet status...").start();
|
|
1547
|
-
try {
|
|
1548
|
-
const [sessions, computers] = await Promise.all([
|
|
1549
|
-
listFleetAgentSessions(),
|
|
1550
|
-
listComputers()
|
|
1551
|
-
]);
|
|
1552
|
-
spinner?.stop();
|
|
1553
|
-
if (options.json) {
|
|
1554
|
-
console.log(JSON.stringify({ sessions }, null, 2));
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
const handleByComputerID = new Map(computers.map((computer) => [computer.id, computer.handle]));
|
|
1558
|
-
printSessions(sessions, handleByComputerID);
|
|
1559
|
-
} catch (error) {
|
|
1560
|
-
spinner?.fail(error instanceof Error ? error.message : "Failed to fetch fleet status");
|
|
1561
|
-
if (!spinner) {
|
|
1562
|
-
console.error(error instanceof Error ? error.message : "Failed to fetch fleet status");
|
|
1563
|
-
}
|
|
1564
|
-
process.exit(1);
|
|
1565
|
-
}
|
|
1566
|
-
});
|
|
1567
1299
|
|
|
1568
1300
|
// src/commands/claude-auth.ts
|
|
1569
1301
|
import { randomBytes as randomBytes2, createHash } from "crypto";
|
|
1570
1302
|
import { input as textInput } from "@inquirer/prompts";
|
|
1571
1303
|
import { Command as Command4 } from "commander";
|
|
1572
|
-
import
|
|
1304
|
+
import chalk3 from "chalk";
|
|
1573
1305
|
import ora4 from "ora";
|
|
1574
1306
|
|
|
1575
1307
|
// src/lib/remote-auth.ts
|
|
@@ -1782,7 +1514,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1782
1514
|
await runClaudeLogin(options);
|
|
1783
1515
|
} catch (error) {
|
|
1784
1516
|
const message = error instanceof Error ? error.message : "Failed to authenticate Claude";
|
|
1785
|
-
console.error(
|
|
1517
|
+
console.error(chalk3.red(`
|
|
1786
1518
|
${message}`));
|
|
1787
1519
|
process.exit(1);
|
|
1788
1520
|
}
|
|
@@ -1795,7 +1527,7 @@ async function runClaudeLogin(options) {
|
|
|
1795
1527
|
let activeTodoID = "target";
|
|
1796
1528
|
let failureMessage = null;
|
|
1797
1529
|
console.log();
|
|
1798
|
-
console.log(
|
|
1530
|
+
console.log(chalk3.cyan("Authenticating with Claude Code...\n"));
|
|
1799
1531
|
try {
|
|
1800
1532
|
const prepared = await prepareTargetMachine(options);
|
|
1801
1533
|
target = prepared.computer;
|
|
@@ -1880,7 +1612,7 @@ async function runClaudeLogin(options) {
|
|
|
1880
1612
|
}
|
|
1881
1613
|
if (target) {
|
|
1882
1614
|
console.log(
|
|
1883
|
-
|
|
1615
|
+
chalk3.green(`Claude login installed on ${chalk3.bold(target.handle)}.`)
|
|
1884
1616
|
);
|
|
1885
1617
|
console.log();
|
|
1886
1618
|
}
|
|
@@ -1921,11 +1653,11 @@ function markVerificationTodo(items, id, result, successDetail) {
|
|
|
1921
1653
|
}
|
|
1922
1654
|
function printTodoList(items) {
|
|
1923
1655
|
console.log();
|
|
1924
|
-
console.log(
|
|
1656
|
+
console.log(chalk3.dim("TODO"));
|
|
1925
1657
|
console.log();
|
|
1926
1658
|
for (const item of items) {
|
|
1927
|
-
const marker = item.state === "done" ?
|
|
1928
|
-
const detail = item.detail ?
|
|
1659
|
+
const marker = item.state === "done" ? chalk3.green("[x]") : item.state === "skipped" ? chalk3.yellow("[-]") : item.state === "failed" ? chalk3.red("[!]") : chalk3.dim("[ ]");
|
|
1660
|
+
const detail = item.detail ? chalk3.dim(` ${item.detail}`) : "";
|
|
1929
1661
|
console.log(` ${marker} ${item.label}${detail ? ` ${detail}` : ""}`);
|
|
1930
1662
|
}
|
|
1931
1663
|
console.log();
|
|
@@ -1953,7 +1685,7 @@ async function runManualOAuthFlow() {
|
|
|
1953
1685
|
try {
|
|
1954
1686
|
await openBrowserURL(url);
|
|
1955
1687
|
} catch {
|
|
1956
|
-
console.log(
|
|
1688
|
+
console.log(chalk3.yellow("Unable to open the browser automatically."));
|
|
1957
1689
|
}
|
|
1958
1690
|
console.log(
|
|
1959
1691
|
"After completing authentication, copy the code shown on the success page."
|
|
@@ -2044,7 +1776,7 @@ function parseAuthorizationInput(value, expectedState) {
|
|
|
2044
1776
|
}
|
|
2045
1777
|
async function installClaudeAuth(target, oauth) {
|
|
2046
1778
|
const spinner = ora4(
|
|
2047
|
-
`Installing Claude auth on ${
|
|
1779
|
+
`Installing Claude auth on ${chalk3.bold(target.handle)}...`
|
|
2048
1780
|
).start();
|
|
2049
1781
|
try {
|
|
2050
1782
|
const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
|
|
@@ -2054,10 +1786,10 @@ async function installClaudeAuth(target, oauth) {
|
|
|
2054
1786
|
installScript
|
|
2055
1787
|
);
|
|
2056
1788
|
if (result.stdout.trim()) {
|
|
2057
|
-
spinner.succeed(`Installed Claude auth on ${
|
|
1789
|
+
spinner.succeed(`Installed Claude auth on ${chalk3.bold(target.handle)}`);
|
|
2058
1790
|
return;
|
|
2059
1791
|
}
|
|
2060
|
-
spinner.succeed(`Installed Claude auth on ${
|
|
1792
|
+
spinner.succeed(`Installed Claude auth on ${chalk3.bold(target.handle)}`);
|
|
2061
1793
|
} catch (error) {
|
|
2062
1794
|
spinner.fail(
|
|
2063
1795
|
error instanceof Error ? error.message : `Failed to install Claude auth on ${target.handle}`
|
|
@@ -2067,11 +1799,11 @@ async function installClaudeAuth(target, oauth) {
|
|
|
2067
1799
|
}
|
|
2068
1800
|
async function verifyTargetMachine(handle, target) {
|
|
2069
1801
|
const spinner = ora4(
|
|
2070
|
-
`Verifying Claude login on ${
|
|
1802
|
+
`Verifying Claude login on ${chalk3.bold(handle)}...`
|
|
2071
1803
|
).start();
|
|
2072
1804
|
const result = await verifyStoredAuth(target);
|
|
2073
1805
|
if (result.status === "verified") {
|
|
2074
|
-
spinner.succeed(`Verified Claude login on ${
|
|
1806
|
+
spinner.succeed(`Verified Claude login on ${chalk3.bold(handle)}`);
|
|
2075
1807
|
return result;
|
|
2076
1808
|
}
|
|
2077
1809
|
spinner.warn(result.detail);
|
|
@@ -2079,7 +1811,7 @@ async function verifyTargetMachine(handle, target) {
|
|
|
2079
1811
|
}
|
|
2080
1812
|
async function verifySharedInstall(primaryHandle, primaryComputerID, sharedInstall, skip, verify) {
|
|
2081
1813
|
const spinner = ora4(
|
|
2082
|
-
`Verifying shared-home Claude login from ${
|
|
1814
|
+
`Verifying shared-home Claude login from ${chalk3.bold(primaryHandle)}...`
|
|
2083
1815
|
).start();
|
|
2084
1816
|
const result = await verifySecondaryMachine(
|
|
2085
1817
|
primaryComputerID,
|
|
@@ -2089,7 +1821,7 @@ async function verifySharedInstall(primaryHandle, primaryComputerID, sharedInsta
|
|
|
2089
1821
|
);
|
|
2090
1822
|
if (result.status === "verified") {
|
|
2091
1823
|
spinner.succeed(
|
|
2092
|
-
`Verified shared-home Claude login on ${
|
|
1824
|
+
`Verified shared-home Claude login on ${chalk3.bold(result.handle)}`
|
|
2093
1825
|
);
|
|
2094
1826
|
return result;
|
|
2095
1827
|
}
|
|
@@ -2173,9 +1905,9 @@ function randomSuffix2(length) {
|
|
|
2173
1905
|
|
|
2174
1906
|
// src/commands/computers.ts
|
|
2175
1907
|
import { Command as Command5 } from "commander";
|
|
2176
|
-
import
|
|
1908
|
+
import chalk4 from "chalk";
|
|
2177
1909
|
import ora5 from "ora";
|
|
2178
|
-
import { select
|
|
1910
|
+
import { select, input as textInput2, confirm } from "@inquirer/prompts";
|
|
2179
1911
|
|
|
2180
1912
|
// src/lib/machine-sources.ts
|
|
2181
1913
|
async function getMachineSourceSettings() {
|
|
@@ -2220,48 +1952,90 @@ function summarizeMachineSource(source) {
|
|
|
2220
1952
|
return parts.join(" | ");
|
|
2221
1953
|
}
|
|
2222
1954
|
|
|
1955
|
+
// src/lib/mount-control.ts
|
|
1956
|
+
import { unlinkSync } from "fs";
|
|
1957
|
+
import net from "net";
|
|
1958
|
+
async function notifyMountDaemon(paths) {
|
|
1959
|
+
return new Promise((resolve) => {
|
|
1960
|
+
const socket = net.createConnection(paths.socketPath);
|
|
1961
|
+
socket.on("connect", () => {
|
|
1962
|
+
socket.end("reconcile\n");
|
|
1963
|
+
resolve(true);
|
|
1964
|
+
});
|
|
1965
|
+
socket.on("error", () => {
|
|
1966
|
+
resolve(false);
|
|
1967
|
+
});
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
function createMountControlServer(paths, onReconcile) {
|
|
1971
|
+
try {
|
|
1972
|
+
unlinkSync(paths.socketPath);
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
const server = net.createServer((socket) => {
|
|
1976
|
+
socket.setEncoding("utf8");
|
|
1977
|
+
let buffer = "";
|
|
1978
|
+
socket.on("data", (chunk) => {
|
|
1979
|
+
buffer += chunk;
|
|
1980
|
+
if (buffer.includes("\n")) {
|
|
1981
|
+
void Promise.resolve(onReconcile()).finally(() => {
|
|
1982
|
+
socket.end("ok\n");
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
});
|
|
1986
|
+
socket.on("error", () => {
|
|
1987
|
+
socket.destroy();
|
|
1988
|
+
});
|
|
1989
|
+
});
|
|
1990
|
+
server.on("close", () => {
|
|
1991
|
+
try {
|
|
1992
|
+
unlinkSync(paths.socketPath);
|
|
1993
|
+
} catch {
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
return server;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
2223
1999
|
// src/commands/computers.ts
|
|
2224
2000
|
function isInternalCondition(value) {
|
|
2225
2001
|
return /^[A-Z][a-zA-Z]+$/.test(value);
|
|
2226
2002
|
}
|
|
2227
2003
|
function printComputer(computer) {
|
|
2228
2004
|
const vnc = vncURL(computer);
|
|
2229
|
-
const terminal = terminalURL(computer);
|
|
2230
2005
|
const ssh = computer.ssh_enabled ? formatSSHCommand2(computer.handle, computer.ssh_host, computer.ssh_port) : "disabled";
|
|
2231
2006
|
const isCustom = computer.runtime_family === "custom-machine";
|
|
2232
2007
|
console.log();
|
|
2233
|
-
console.log(` ${
|
|
2008
|
+
console.log(` ${chalk4.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
|
|
2234
2009
|
console.log();
|
|
2235
|
-
console.log(` ${
|
|
2236
|
-
console.log(` ${
|
|
2010
|
+
console.log(` ${chalk4.dim("ID")} ${computer.id}`);
|
|
2011
|
+
console.log(` ${chalk4.dim("Tier")} ${computer.tier}`);
|
|
2237
2012
|
if (isCustom) {
|
|
2238
|
-
console.log(` ${
|
|
2239
|
-
console.log(` ${
|
|
2240
|
-
console.log(` ${
|
|
2013
|
+
console.log(` ${chalk4.dim("Runtime")} ${computer.runtime_family}`);
|
|
2014
|
+
console.log(` ${chalk4.dim("Source")} ${computer.source_kind}`);
|
|
2015
|
+
console.log(` ${chalk4.dim("Image")} ${computer.image_family}`);
|
|
2241
2016
|
} else {
|
|
2242
|
-
console.log(` ${
|
|
2243
|
-
console.log(` ${
|
|
2017
|
+
console.log(` ${chalk4.dim("Runtime")} ${computer.runtime_family}`);
|
|
2018
|
+
console.log(` ${chalk4.dim("Launch")} ${formatManagedWorkerLaunchSource(computer)}`);
|
|
2244
2019
|
if (computer.user_source_id && computer.resolved_image_ref) {
|
|
2245
|
-
console.log(` ${
|
|
2020
|
+
console.log(` ${chalk4.dim("Image")} ${computer.resolved_image_ref}`);
|
|
2246
2021
|
}
|
|
2247
2022
|
}
|
|
2248
|
-
console.log(` ${
|
|
2249
|
-
console.log(` ${
|
|
2023
|
+
console.log(` ${chalk4.dim("Primary")} :${computer.primary_port}${computer.primary_path}`);
|
|
2024
|
+
console.log(` ${chalk4.dim("Health")} ${computer.healthcheck_type}${computer.healthcheck_value ? ` (${computer.healthcheck_value})` : ""}`);
|
|
2250
2025
|
console.log();
|
|
2251
|
-
console.log(` ${
|
|
2252
|
-
console.log(` ${
|
|
2253
|
-
console.log(` ${
|
|
2254
|
-
console.log(` ${chalk6.dim("SSH")} ${computer.ssh_enabled ? chalk6.white(ssh) : chalk6.dim(ssh)}`);
|
|
2026
|
+
console.log(` ${chalk4.dim("Gateway")} ${chalk4.cyan(webURL(computer))}`);
|
|
2027
|
+
console.log(` ${chalk4.dim("VNC")} ${vnc ? chalk4.cyan(vnc) : chalk4.dim("not available")}`);
|
|
2028
|
+
console.log(` ${chalk4.dim("SSH")} ${computer.ssh_enabled ? chalk4.white(ssh) : chalk4.dim(ssh)}`);
|
|
2255
2029
|
if (computer.last_error) {
|
|
2256
2030
|
console.log();
|
|
2257
2031
|
if (isInternalCondition(computer.last_error)) {
|
|
2258
|
-
console.log(` ${
|
|
2032
|
+
console.log(` ${chalk4.dim("Condition")} ${chalk4.dim(computer.last_error)}`);
|
|
2259
2033
|
} else {
|
|
2260
|
-
console.log(` ${
|
|
2034
|
+
console.log(` ${chalk4.dim("Error")} ${chalk4.red(computer.last_error)}`);
|
|
2261
2035
|
}
|
|
2262
2036
|
}
|
|
2263
2037
|
console.log();
|
|
2264
|
-
console.log(` ${
|
|
2038
|
+
console.log(` ${chalk4.dim("Created")} ${timeAgo(computer.created_at)}`);
|
|
2265
2039
|
console.log();
|
|
2266
2040
|
}
|
|
2267
2041
|
function formatSSHCommand2(user, host, port) {
|
|
@@ -2298,16 +2072,16 @@ function printComputerTable(computers) {
|
|
|
2298
2072
|
const createdWidth = 10;
|
|
2299
2073
|
console.log();
|
|
2300
2074
|
console.log(
|
|
2301
|
-
` ${
|
|
2075
|
+
` ${chalk4.dim(padEnd("Handle", handleWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim(padEnd("Created", createdWidth + 2))}${chalk4.dim("URL")}`
|
|
2302
2076
|
);
|
|
2303
2077
|
console.log(
|
|
2304
|
-
` ${
|
|
2078
|
+
` ${chalk4.dim("-".repeat(handleWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(createdWidth + 2))}${chalk4.dim("-".repeat(20))}`
|
|
2305
2079
|
);
|
|
2306
2080
|
for (const computer of computers) {
|
|
2307
2081
|
const status = formatStatus(computer.status);
|
|
2308
|
-
const created =
|
|
2082
|
+
const created = chalk4.dim(timeAgo(computer.created_at));
|
|
2309
2083
|
console.log(
|
|
2310
|
-
` ${
|
|
2084
|
+
` ${chalk4.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk4.cyan(webURL(computer))}`
|
|
2311
2085
|
);
|
|
2312
2086
|
}
|
|
2313
2087
|
console.log();
|
|
@@ -2317,23 +2091,19 @@ function printComputerTableVerbose(computers) {
|
|
|
2317
2091
|
const statusWidth = 10;
|
|
2318
2092
|
console.log();
|
|
2319
2093
|
console.log(
|
|
2320
|
-
` ${
|
|
2094
|
+
` ${chalk4.dim(padEnd("Handle", handleWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim("URLs")}`
|
|
2321
2095
|
);
|
|
2322
2096
|
console.log(
|
|
2323
|
-
` ${
|
|
2097
|
+
` ${chalk4.dim("-".repeat(handleWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(20))}`
|
|
2324
2098
|
);
|
|
2325
2099
|
for (const computer of computers) {
|
|
2326
2100
|
const status = formatStatus(computer.status);
|
|
2327
2101
|
const vnc = vncURL(computer);
|
|
2328
|
-
const terminal = terminalURL(computer);
|
|
2329
2102
|
console.log(
|
|
2330
|
-
` ${
|
|
2103
|
+
` ${chalk4.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk4.cyan(webURL(computer))}`
|
|
2331
2104
|
);
|
|
2332
2105
|
console.log(
|
|
2333
|
-
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${
|
|
2334
|
-
);
|
|
2335
|
-
console.log(
|
|
2336
|
-
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(terminal ?? "Terminal not available")}`
|
|
2106
|
+
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk4.dim(vnc ?? "VNC not available")}`
|
|
2337
2107
|
);
|
|
2338
2108
|
}
|
|
2339
2109
|
console.log();
|
|
@@ -2349,7 +2119,7 @@ var lsCommand = new Command5("ls").description("List computers").option("--json"
|
|
|
2349
2119
|
}
|
|
2350
2120
|
if (computers.length === 0) {
|
|
2351
2121
|
console.log();
|
|
2352
|
-
console.log(
|
|
2122
|
+
console.log(chalk4.dim(" No computers found."));
|
|
2353
2123
|
console.log();
|
|
2354
2124
|
return;
|
|
2355
2125
|
}
|
|
@@ -2405,11 +2175,11 @@ var createCommand = new Command5("create").description("Create a computer").argu
|
|
|
2405
2175
|
Boolean(selectedOptions.usePlatformDefault)
|
|
2406
2176
|
);
|
|
2407
2177
|
if (machineSourceNote) {
|
|
2408
|
-
console.log(
|
|
2178
|
+
console.log(chalk4.dim(machineSourceNote));
|
|
2409
2179
|
}
|
|
2410
2180
|
const provisioningNote = createProvisioningNote(runtimeFamily, filesystemSettings);
|
|
2411
2181
|
if (provisioningNote) {
|
|
2412
|
-
console.log(
|
|
2182
|
+
console.log(chalk4.dim(provisioningNote));
|
|
2413
2183
|
}
|
|
2414
2184
|
spinner = ora5(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
|
|
2415
2185
|
startTime = Date.now();
|
|
@@ -2447,7 +2217,12 @@ var createCommand = new Command5("create").description("Create a computer").argu
|
|
|
2447
2217
|
}
|
|
2448
2218
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2449
2219
|
spinner.succeed(
|
|
2450
|
-
|
|
2220
|
+
chalk4.green(`Created ${chalk4.bold(computer.handle)} ${chalk4.dim(`[${elapsed}s]`)}`)
|
|
2221
|
+
);
|
|
2222
|
+
await notifyMountDaemon(
|
|
2223
|
+
getMountPaths((readMountConfig() ?? defaultMountServiceConfig()).rootPath)
|
|
2224
|
+
).catch(
|
|
2225
|
+
() => false
|
|
2451
2226
|
);
|
|
2452
2227
|
printComputer(computer);
|
|
2453
2228
|
} catch (error) {
|
|
@@ -2456,10 +2231,10 @@ var createCommand = new Command5("create").description("Create a computer").argu
|
|
|
2456
2231
|
}
|
|
2457
2232
|
const message = error instanceof Error ? error.message : "Failed to create computer";
|
|
2458
2233
|
if (spinner) {
|
|
2459
|
-
const suffix = startTime ? ` ${
|
|
2234
|
+
const suffix = startTime ? ` ${chalk4.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
|
|
2460
2235
|
spinner.fail(`${message}${suffix}`);
|
|
2461
2236
|
} else {
|
|
2462
|
-
console.error(
|
|
2237
|
+
console.error(chalk4.red(message));
|
|
2463
2238
|
}
|
|
2464
2239
|
process.exit(1);
|
|
2465
2240
|
}
|
|
@@ -2471,11 +2246,11 @@ async function resolveCreateOptions(options) {
|
|
|
2471
2246
|
validateCreateOptions(selectedOptions);
|
|
2472
2247
|
return selectedOptions;
|
|
2473
2248
|
}
|
|
2474
|
-
const runtimeChoice = await
|
|
2249
|
+
const runtimeChoice = await select({
|
|
2475
2250
|
message: "Select runtime",
|
|
2476
2251
|
choices: [
|
|
2477
2252
|
{
|
|
2478
|
-
name: "managed-worker - default Ubuntu desktop, SSH, VNC
|
|
2253
|
+
name: "managed-worker - default Ubuntu desktop, SSH, and VNC",
|
|
2479
2254
|
value: "managed-worker"
|
|
2480
2255
|
},
|
|
2481
2256
|
{
|
|
@@ -2491,7 +2266,7 @@ async function resolveCreateOptions(options) {
|
|
|
2491
2266
|
selectedOptions.imageRef = (await textInput2({ message: "OCI image ref (required):" })).trim();
|
|
2492
2267
|
selectedOptions.primaryPort = (await textInput2({ message: "Primary port:", default: "3000" })).trim();
|
|
2493
2268
|
selectedOptions.primaryPath = (await textInput2({ message: "Primary path:", default: "/" })).trim();
|
|
2494
|
-
selectedOptions.healthcheckType = await
|
|
2269
|
+
selectedOptions.healthcheckType = await select({
|
|
2495
2270
|
message: "Healthcheck type",
|
|
2496
2271
|
choices: [
|
|
2497
2272
|
{ name: "tcp", value: "tcp" },
|
|
@@ -2522,11 +2297,11 @@ var removeCommand = new Command5("rm").description("Delete a computer").argument
|
|
|
2522
2297
|
spinner.stop();
|
|
2523
2298
|
if (!skipConfirm && process.stdin.isTTY) {
|
|
2524
2299
|
const confirmed = await confirm({
|
|
2525
|
-
message: `Delete computer ${
|
|
2300
|
+
message: `Delete computer ${chalk4.bold(computer.handle)}?`,
|
|
2526
2301
|
default: false
|
|
2527
2302
|
});
|
|
2528
2303
|
if (!confirmed) {
|
|
2529
|
-
console.log(
|
|
2304
|
+
console.log(chalk4.dim(" Cancelled."));
|
|
2530
2305
|
return;
|
|
2531
2306
|
}
|
|
2532
2307
|
}
|
|
@@ -2534,7 +2309,12 @@ var removeCommand = new Command5("rm").description("Delete a computer").argument
|
|
|
2534
2309
|
await api(`/v1/computers/${computer.id}`, {
|
|
2535
2310
|
method: "DELETE"
|
|
2536
2311
|
});
|
|
2537
|
-
deleteSpinner.succeed(
|
|
2312
|
+
deleteSpinner.succeed(chalk4.green(`Deleted ${chalk4.bold(computer.handle)}`));
|
|
2313
|
+
await notifyMountDaemon(
|
|
2314
|
+
getMountPaths((readMountConfig() ?? defaultMountServiceConfig()).rootPath)
|
|
2315
|
+
).catch(
|
|
2316
|
+
() => false
|
|
2317
|
+
);
|
|
2538
2318
|
} catch (error) {
|
|
2539
2319
|
spinner.fail(
|
|
2540
2320
|
error instanceof Error ? error.message : "Failed to delete computer"
|
|
@@ -2590,9 +2370,9 @@ function createMachineSourceNote(runtimeFamily, machineSourceSettings, usePlatfo
|
|
|
2590
2370
|
return `Using managed-worker image source: ${summarizeMachineSourceSelection(machineSourceSettings)}.`;
|
|
2591
2371
|
}
|
|
2592
2372
|
function createSpinnerText(runtimeFamily, filesystemSettings, elapsedSeconds) {
|
|
2593
|
-
const elapsedLabel =
|
|
2373
|
+
const elapsedLabel = chalk4.dim(`${elapsedSeconds.toFixed(1)}s`);
|
|
2594
2374
|
if (runtimeFamily === "managed-worker" && filesystemSettings?.shared_enabled && elapsedSeconds >= 5) {
|
|
2595
|
-
return `Creating computer... ${elapsedLabel} ${
|
|
2375
|
+
return `Creating computer... ${elapsedLabel} ${chalk4.dim("mounting shared home")}`;
|
|
2596
2376
|
}
|
|
2597
2377
|
return `Creating computer... ${elapsedLabel}`;
|
|
2598
2378
|
}
|
|
@@ -2667,6 +2447,7 @@ _computer() {
|
|
|
2667
2447
|
local -a commands
|
|
2668
2448
|
commands=(
|
|
2669
2449
|
'login:Authenticate the CLI'
|
|
2450
|
+
'upgrade:Update the CLI to the latest version'
|
|
2670
2451
|
'logout:Remove stored API key'
|
|
2671
2452
|
'whoami:Show current user'
|
|
2672
2453
|
'claude-login:Authenticate Claude Code on a computer'
|
|
@@ -2680,8 +2461,8 @@ _computer() {
|
|
|
2680
2461
|
'open:Open in browser'
|
|
2681
2462
|
'ssh:SSH into a computer'
|
|
2682
2463
|
'ports:Manage published ports'
|
|
2464
|
+
'mount:Mirror SSH-ready machine homes into ~/agentcomputer while running'
|
|
2683
2465
|
'agent:Manage cloud agent sessions'
|
|
2684
|
-
'fleet:View agent activity across your fleet'
|
|
2685
2466
|
'acp:Run a local ACP bridge for remote agent sessions'
|
|
2686
2467
|
'rm:Delete a computer'
|
|
2687
2468
|
'completion:Generate shell completions'
|
|
@@ -2704,6 +2485,11 @@ _computer() {
|
|
|
2704
2485
|
'rm:Delete a machine image source'
|
|
2705
2486
|
)
|
|
2706
2487
|
|
|
2488
|
+
local -a mount_commands
|
|
2489
|
+
mount_commands=(
|
|
2490
|
+
'status:Show machine mount controller status'
|
|
2491
|
+
)
|
|
2492
|
+
|
|
2707
2493
|
_arguments -C \\
|
|
2708
2494
|
'(-h --help)'{-h,--help}'[Display help]' \\
|
|
2709
2495
|
'(-V --version)'{-V,--version}'[Show version]' \\
|
|
@@ -2808,12 +2594,33 @@ _computer() {
|
|
|
2808
2594
|
open)
|
|
2809
2595
|
_arguments \\
|
|
2810
2596
|
'--vnc[Open VNC desktop]' \\
|
|
2811
|
-
'--terminal[Open terminal]' \\
|
|
2812
2597
|
'1:computer:_computer_handles'
|
|
2813
2598
|
;;
|
|
2814
2599
|
ssh)
|
|
2815
2600
|
_arguments '1:computer:_computer_handles'
|
|
2816
2601
|
;;
|
|
2602
|
+
mount)
|
|
2603
|
+
_arguments -C \\
|
|
2604
|
+
'--alias[SSH host alias]:alias:' \\
|
|
2605
|
+
'--host[SSH gateway host]:host:' \\
|
|
2606
|
+
'--port[SSH gateway port]:port:' \\
|
|
2607
|
+
'--poll-interval[Reconcile interval in milliseconds]:ms:' \\
|
|
2608
|
+
'--connect-timeout[SSH connect timeout for Mutagen]:seconds:' \\
|
|
2609
|
+
'1:command:->mount_command' \\
|
|
2610
|
+
'*::arg:->mount_args'
|
|
2611
|
+
case "$state" in
|
|
2612
|
+
mount_command)
|
|
2613
|
+
_describe -t commands 'mount command' mount_commands
|
|
2614
|
+
;;
|
|
2615
|
+
mount_args)
|
|
2616
|
+
case "$words[2]" in
|
|
2617
|
+
status)
|
|
2618
|
+
_arguments
|
|
2619
|
+
;;
|
|
2620
|
+
esac
|
|
2621
|
+
;;
|
|
2622
|
+
esac
|
|
2623
|
+
;;
|
|
2817
2624
|
rm)
|
|
2818
2625
|
_arguments \\
|
|
2819
2626
|
'(-y --yes)'{-y,--yes}'[Skip confirmation]' \\
|
|
@@ -2877,9 +2684,10 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2877
2684
|
local cur prev words cword
|
|
2878
2685
|
_init_completion || return
|
|
2879
2686
|
|
|
2880
|
-
local commands="login logout whoami claude-login claude-auth codex-login codex-auth create ls get image open ssh ports agent
|
|
2687
|
+
local commands="login upgrade logout whoami claude-login claude-auth codex-login codex-auth create ls get image open ssh ports mount agent acp rm completion help"
|
|
2881
2688
|
local ports_commands="ls publish rm"
|
|
2882
2689
|
local image_commands="ls save default rebuild rm"
|
|
2690
|
+
local mount_commands="status"
|
|
2883
2691
|
|
|
2884
2692
|
if [[ $cword -eq 1 ]]; then
|
|
2885
2693
|
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
@@ -2957,11 +2765,18 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2957
2765
|
else
|
|
2958
2766
|
case "$cmd" in
|
|
2959
2767
|
get) COMPREPLY=($(compgen -W "--json" -- "$cur")) ;;
|
|
2960
|
-
open) COMPREPLY=($(compgen -W "--vnc
|
|
2768
|
+
open) COMPREPLY=($(compgen -W "--vnc" -- "$cur")) ;;
|
|
2961
2769
|
rm) COMPREPLY=($(compgen -W "--yes -y" -- "$cur")) ;;
|
|
2962
2770
|
esac
|
|
2963
2771
|
fi
|
|
2964
2772
|
;;
|
|
2773
|
+
mount)
|
|
2774
|
+
if [[ $cword -eq 2 ]]; then
|
|
2775
|
+
COMPREPLY=($(compgen -W "$mount_commands" -- "$cur"))
|
|
2776
|
+
elif [[ $cword -ge 3 ]]; then
|
|
2777
|
+
COMPREPLY=($(compgen -W "--alias --host --port --poll-interval --connect-timeout" -- "$cur"))
|
|
2778
|
+
fi
|
|
2779
|
+
;;
|
|
2965
2780
|
ports)
|
|
2966
2781
|
if [[ $cword -eq 2 ]]; then
|
|
2967
2782
|
COMPREPLY=($(compgen -W "$ports_commands" -- "$cur"))
|
|
@@ -2997,17 +2812,17 @@ var completionCommand = new Command6("completion").description("Generate shell c
|
|
|
2997
2812
|
// src/commands/codex-login.ts
|
|
2998
2813
|
import { spawn as spawn3 } from "child_process";
|
|
2999
2814
|
import { readFile as readFile3 } from "fs/promises";
|
|
3000
|
-
import { homedir as
|
|
3001
|
-
import { join as
|
|
2815
|
+
import { homedir as homedir3 } from "os";
|
|
2816
|
+
import { join as join2 } from "path";
|
|
3002
2817
|
import { Command as Command7 } from "commander";
|
|
3003
|
-
import
|
|
2818
|
+
import chalk5 from "chalk";
|
|
3004
2819
|
import ora6 from "ora";
|
|
3005
2820
|
var codexLoginCommand = new Command7("codex-login").alias("codex-auth").description("Authenticate Codex on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").option("--verbose", "Show step-by-step auth diagnostics").action(async (options) => {
|
|
3006
2821
|
try {
|
|
3007
2822
|
await runCodexLogin(options);
|
|
3008
2823
|
} catch (error) {
|
|
3009
2824
|
const message = error instanceof Error ? error.message : "Failed to authenticate Codex";
|
|
3010
|
-
console.error(
|
|
2825
|
+
console.error(chalk5.red(`
|
|
3011
2826
|
${message}`));
|
|
3012
2827
|
process.exit(1);
|
|
3013
2828
|
}
|
|
@@ -3020,7 +2835,7 @@ async function runCodexLogin(options) {
|
|
|
3020
2835
|
let activeTodoID = "target";
|
|
3021
2836
|
let failureMessage = null;
|
|
3022
2837
|
console.log();
|
|
3023
|
-
console.log(
|
|
2838
|
+
console.log(chalk5.cyan("Authenticating with Codex...\n"));
|
|
3024
2839
|
try {
|
|
3025
2840
|
const prepared = await prepareTargetMachine2(options);
|
|
3026
2841
|
target = prepared.computer;
|
|
@@ -3105,7 +2920,7 @@ async function runCodexLogin(options) {
|
|
|
3105
2920
|
}
|
|
3106
2921
|
if (target) {
|
|
3107
2922
|
console.log(
|
|
3108
|
-
|
|
2923
|
+
chalk5.green(`Codex login installed on ${chalk5.bold(target.handle)}.`)
|
|
3109
2924
|
);
|
|
3110
2925
|
console.log();
|
|
3111
2926
|
}
|
|
@@ -3146,11 +2961,11 @@ function markVerificationTodo2(items, id, result, successDetail) {
|
|
|
3146
2961
|
}
|
|
3147
2962
|
function printTodoList2(items) {
|
|
3148
2963
|
console.log();
|
|
3149
|
-
console.log(
|
|
2964
|
+
console.log(chalk5.dim("TODO"));
|
|
3150
2965
|
console.log();
|
|
3151
2966
|
for (const item of items) {
|
|
3152
|
-
const marker = item.state === "done" ?
|
|
3153
|
-
const detail = item.detail ?
|
|
2967
|
+
const marker = item.state === "done" ? chalk5.green("[x]") : item.state === "skipped" ? chalk5.yellow("[-]") : item.state === "failed" ? chalk5.red("[!]") : chalk5.dim("[ ]");
|
|
2968
|
+
const detail = item.detail ? chalk5.dim(` ${item.detail}`) : "";
|
|
3154
2969
|
console.log(` ${marker} ${item.label}${detail ? ` ${detail}` : ""}`);
|
|
3155
2970
|
}
|
|
3156
2971
|
console.log();
|
|
@@ -3198,7 +3013,7 @@ async function getLocalCodexStatus() {
|
|
|
3198
3013
|
return parseCodexStatusOutput(result.stdout, result.stderr);
|
|
3199
3014
|
}
|
|
3200
3015
|
async function readLocalCodexAuthFile() {
|
|
3201
|
-
const authPath =
|
|
3016
|
+
const authPath = join2(homedir3(), ".codex", "auth.json");
|
|
3202
3017
|
let raw;
|
|
3203
3018
|
try {
|
|
3204
3019
|
raw = await readFile3(authPath, "utf8");
|
|
@@ -3261,12 +3076,12 @@ async function captureLocalCommand(command, args) {
|
|
|
3261
3076
|
}
|
|
3262
3077
|
async function installCodexAuth(target, authJSON) {
|
|
3263
3078
|
const spinner = ora6(
|
|
3264
|
-
`Installing Codex login on ${
|
|
3079
|
+
`Installing Codex login on ${chalk5.bold(target.handle)}...`
|
|
3265
3080
|
).start();
|
|
3266
3081
|
try {
|
|
3267
3082
|
const installScript = buildInstallScript2(authJSON);
|
|
3268
3083
|
await runRemoteCommand(target, ["bash", "-s"], installScript);
|
|
3269
|
-
spinner.succeed(`Installed Codex login on ${
|
|
3084
|
+
spinner.succeed(`Installed Codex login on ${chalk5.bold(target.handle)}`);
|
|
3270
3085
|
} catch (error) {
|
|
3271
3086
|
spinner.fail(
|
|
3272
3087
|
error instanceof Error ? error.message : `Failed to install Codex login on ${target.handle}`
|
|
@@ -3276,11 +3091,11 @@ async function installCodexAuth(target, authJSON) {
|
|
|
3276
3091
|
}
|
|
3277
3092
|
async function verifyTargetMachine2(handle, target) {
|
|
3278
3093
|
const spinner = ora6(
|
|
3279
|
-
`Verifying Codex login on ${
|
|
3094
|
+
`Verifying Codex login on ${chalk5.bold(handle)}...`
|
|
3280
3095
|
).start();
|
|
3281
3096
|
const result = await verifyStoredCodexAuth(target);
|
|
3282
3097
|
if (result.status === "verified") {
|
|
3283
|
-
spinner.succeed(`Verified Codex login on ${
|
|
3098
|
+
spinner.succeed(`Verified Codex login on ${chalk5.bold(handle)}`);
|
|
3284
3099
|
return result;
|
|
3285
3100
|
}
|
|
3286
3101
|
spinner.warn(result.detail);
|
|
@@ -3288,7 +3103,7 @@ async function verifyTargetMachine2(handle, target) {
|
|
|
3288
3103
|
}
|
|
3289
3104
|
async function verifySharedInstall2(primaryHandle, primaryComputerID, sharedInstall, skip, verify) {
|
|
3290
3105
|
const spinner = ora6(
|
|
3291
|
-
`Verifying shared-home Codex login from ${
|
|
3106
|
+
`Verifying shared-home Codex login from ${chalk5.bold(primaryHandle)}...`
|
|
3292
3107
|
).start();
|
|
3293
3108
|
const result = await verifySecondaryMachine(
|
|
3294
3109
|
primaryComputerID,
|
|
@@ -3298,7 +3113,7 @@ async function verifySharedInstall2(primaryHandle, primaryComputerID, sharedInst
|
|
|
3298
3113
|
);
|
|
3299
3114
|
if (result.status === "verified") {
|
|
3300
3115
|
spinner.succeed(
|
|
3301
|
-
`Verified shared-home Codex login on ${
|
|
3116
|
+
`Verified shared-home Codex login on ${chalk5.bold(result.handle)}`
|
|
3302
3117
|
);
|
|
3303
3118
|
return result;
|
|
3304
3119
|
}
|
|
@@ -3356,9 +3171,9 @@ function firstStatusLine2(value) {
|
|
|
3356
3171
|
}
|
|
3357
3172
|
|
|
3358
3173
|
// src/commands/images.ts
|
|
3359
|
-
import { confirm as confirm2, input as textInput3, select as
|
|
3174
|
+
import { confirm as confirm2, input as textInput3, select as select2 } from "@inquirer/prompts";
|
|
3360
3175
|
import { Command as Command8 } from "commander";
|
|
3361
|
-
import
|
|
3176
|
+
import chalk6 from "chalk";
|
|
3362
3177
|
import ora7 from "ora";
|
|
3363
3178
|
var imageCommand = new Command8("image").description("Manage machine image sources");
|
|
3364
3179
|
imageCommand.command("ls").description("List machine image sources").option("--json", "Print raw JSON").action(async (options) => {
|
|
@@ -3391,7 +3206,7 @@ imageCommand.command("save").description("Create or update a machine image sourc
|
|
|
3391
3206
|
return;
|
|
3392
3207
|
}
|
|
3393
3208
|
console.log();
|
|
3394
|
-
console.log(
|
|
3209
|
+
console.log(chalk6.green("Saved machine image source."));
|
|
3395
3210
|
printMachineSourceSettings(settings);
|
|
3396
3211
|
} catch (error) {
|
|
3397
3212
|
if (spinner) {
|
|
@@ -3430,9 +3245,9 @@ imageCommand.command("default").description("Set the default machine image sourc
|
|
|
3430
3245
|
if (!usePlatformDefault) {
|
|
3431
3246
|
const selected = settings.default_machine_source ?? void 0;
|
|
3432
3247
|
const label = selected ? summarizeMachineSource(selected) : sourceID;
|
|
3433
|
-
console.log(
|
|
3248
|
+
console.log(chalk6.green(`Selected ${chalk6.bold(label)} as the default machine image.`));
|
|
3434
3249
|
} else {
|
|
3435
|
-
console.log(
|
|
3250
|
+
console.log(chalk6.green("Using the AgentComputer platform default image."));
|
|
3436
3251
|
}
|
|
3437
3252
|
printMachineSourceSettings(settings);
|
|
3438
3253
|
} catch (error) {
|
|
@@ -3454,7 +3269,7 @@ imageCommand.command("rebuild").description("Rebuild a machine image source").ar
|
|
|
3454
3269
|
return;
|
|
3455
3270
|
}
|
|
3456
3271
|
console.log();
|
|
3457
|
-
console.log(
|
|
3272
|
+
console.log(chalk6.green(`Queued rebuild for ${chalk6.bold(sourceID)}.`));
|
|
3458
3273
|
printMachineSourceSettings(settings);
|
|
3459
3274
|
} catch (error) {
|
|
3460
3275
|
if (spinner) {
|
|
@@ -3473,7 +3288,7 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
3473
3288
|
if (!skipConfirm && process.stdin.isTTY) {
|
|
3474
3289
|
const confirmed = await confirmDeletion(sourceID);
|
|
3475
3290
|
if (!confirmed) {
|
|
3476
|
-
console.log(
|
|
3291
|
+
console.log(chalk6.dim(" Cancelled."));
|
|
3477
3292
|
return;
|
|
3478
3293
|
}
|
|
3479
3294
|
}
|
|
@@ -3485,7 +3300,7 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
3485
3300
|
return;
|
|
3486
3301
|
}
|
|
3487
3302
|
console.log();
|
|
3488
|
-
console.log(
|
|
3303
|
+
console.log(chalk6.green(`Deleted machine image source ${chalk6.bold(sourceID)}.`));
|
|
3489
3304
|
printMachineSourceSettings(settings);
|
|
3490
3305
|
} catch (error) {
|
|
3491
3306
|
if (spinner) {
|
|
@@ -3497,14 +3312,14 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
3497
3312
|
}
|
|
3498
3313
|
});
|
|
3499
3314
|
function printMachineSourceSettings(settings) {
|
|
3500
|
-
console.log(` ${
|
|
3315
|
+
console.log(` ${chalk6.dim("Default")} ${chalk6.white(summarizeDefaultMachineSource(settings))}`);
|
|
3501
3316
|
console.log();
|
|
3502
3317
|
if (settings.sources.length === 0) {
|
|
3503
|
-
console.log(
|
|
3318
|
+
console.log(chalk6.dim(" No custom machine images configured yet."));
|
|
3504
3319
|
console.log();
|
|
3505
3320
|
return;
|
|
3506
3321
|
}
|
|
3507
|
-
console.log(` ${
|
|
3322
|
+
console.log(` ${chalk6.dim("Custom")} ${chalk6.white(formatMachineSourceCounts(settings.sources))}`);
|
|
3508
3323
|
console.log();
|
|
3509
3324
|
for (const source of sortMachineSources(settings.sources, settings.default_machine_source_id)) {
|
|
3510
3325
|
printMachineSourceCard(source, settings.default_machine_source_id === source.id);
|
|
@@ -3519,17 +3334,17 @@ function printMachineSourceCard(source, isDefault) {
|
|
|
3519
3334
|
];
|
|
3520
3335
|
const extra = machineSourceExtraParts(source);
|
|
3521
3336
|
console.log(
|
|
3522
|
-
` ${statusLabel}${
|
|
3337
|
+
` ${statusLabel}${chalk6.bold(machineSourceTitle(source))}${isDefault ? chalk6.green(" default") : ""}`
|
|
3523
3338
|
);
|
|
3524
|
-
console.log(` ${
|
|
3525
|
-
console.log(` ${
|
|
3339
|
+
console.log(` ${chalk6.dim(meta.concat(extra).join(" | "))}`);
|
|
3340
|
+
console.log(` ${chalk6.dim(machineSourceStatusSummary(source))}`);
|
|
3526
3341
|
if (source.resolved_image_ref) {
|
|
3527
|
-
console.log(` ${
|
|
3342
|
+
console.log(` ${chalk6.dim("resolved")} ${source.resolved_image_ref}`);
|
|
3528
3343
|
} else if (source.last_good_resolved_image_ref) {
|
|
3529
|
-
console.log(` ${
|
|
3344
|
+
console.log(` ${chalk6.dim("last good")} ${source.last_good_resolved_image_ref}`);
|
|
3530
3345
|
}
|
|
3531
3346
|
if (source.error) {
|
|
3532
|
-
console.log(` ${
|
|
3347
|
+
console.log(` ${chalk6.red(source.error)}`);
|
|
3533
3348
|
}
|
|
3534
3349
|
console.log();
|
|
3535
3350
|
}
|
|
@@ -3599,13 +3414,13 @@ function formatMachineSourceStatus(status) {
|
|
|
3599
3414
|
const text = status.toUpperCase();
|
|
3600
3415
|
switch (status) {
|
|
3601
3416
|
case "ready":
|
|
3602
|
-
return
|
|
3417
|
+
return chalk6.green(text);
|
|
3603
3418
|
case "failed":
|
|
3604
|
-
return
|
|
3419
|
+
return chalk6.red(text);
|
|
3605
3420
|
case "pending":
|
|
3606
3421
|
case "resolving":
|
|
3607
3422
|
case "building":
|
|
3608
|
-
return
|
|
3423
|
+
return chalk6.yellow(text);
|
|
3609
3424
|
default:
|
|
3610
3425
|
return text;
|
|
3611
3426
|
}
|
|
@@ -3714,7 +3529,7 @@ async function resolveSaveInput(options) {
|
|
|
3714
3529
|
return input;
|
|
3715
3530
|
}
|
|
3716
3531
|
async function selectMachineSourceKind() {
|
|
3717
|
-
const kind = await
|
|
3532
|
+
const kind = await select2({
|
|
3718
3533
|
message: "Select machine image kind",
|
|
3719
3534
|
choices: [
|
|
3720
3535
|
{ name: "oci-image - resolve an OCI image digest", value: "oci-image" },
|
|
@@ -3748,7 +3563,7 @@ async function confirmDeletion(sourceID) {
|
|
|
3748
3563
|
|
|
3749
3564
|
// src/commands/login.ts
|
|
3750
3565
|
import { Command as Command9 } from "commander";
|
|
3751
|
-
import
|
|
3566
|
+
import chalk7 from "chalk";
|
|
3752
3567
|
import ora8 from "ora";
|
|
3753
3568
|
|
|
3754
3569
|
// src/lib/browser-login.ts
|
|
@@ -4021,7 +3836,7 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
|
|
|
4021
3836
|
if (existingKey && !options.force) {
|
|
4022
3837
|
console.log();
|
|
4023
3838
|
console.log(
|
|
4024
|
-
|
|
3839
|
+
chalk7.yellow(" Already logged in. Use --force to overwrite.")
|
|
4025
3840
|
);
|
|
4026
3841
|
console.log();
|
|
4027
3842
|
return;
|
|
@@ -4030,8 +3845,8 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
|
|
|
4030
3845
|
const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
|
|
4031
3846
|
if (!apiKey && wantsManualLogin) {
|
|
4032
3847
|
console.log();
|
|
4033
|
-
console.log(
|
|
4034
|
-
console.log(
|
|
3848
|
+
console.log(chalk7.dim(" Usage: computer login --api-key <ac_live_...>"));
|
|
3849
|
+
console.log(chalk7.dim(` API: ${getBaseURL()}`));
|
|
4035
3850
|
console.log();
|
|
4036
3851
|
process.exit(1);
|
|
4037
3852
|
}
|
|
@@ -4041,7 +3856,7 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
|
|
|
4041
3856
|
}
|
|
4042
3857
|
if (!apiKey.startsWith("ac_live_")) {
|
|
4043
3858
|
console.log();
|
|
4044
|
-
console.log(
|
|
3859
|
+
console.log(chalk7.red(" API key must start with ac_live_"));
|
|
4045
3860
|
console.log();
|
|
4046
3861
|
process.exit(1);
|
|
4047
3862
|
}
|
|
@@ -4049,7 +3864,7 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
|
|
|
4049
3864
|
try {
|
|
4050
3865
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
4051
3866
|
setAPIKey(apiKey);
|
|
4052
|
-
spinner.succeed(`Logged in as ${
|
|
3867
|
+
spinner.succeed(`Logged in as ${chalk7.bold(me.user.email)}`);
|
|
4053
3868
|
} catch (error) {
|
|
4054
3869
|
spinner.fail(
|
|
4055
3870
|
error instanceof Error ? error.message : "Failed to validate API key"
|
|
@@ -4069,15 +3884,15 @@ async function runBrowserLogin() {
|
|
|
4069
3884
|
spinner.stop();
|
|
4070
3885
|
console.log();
|
|
4071
3886
|
console.log(
|
|
4072
|
-
|
|
3887
|
+
chalk7.yellow(" Browser auto-open failed. Open this URL to continue:")
|
|
4073
3888
|
);
|
|
4074
|
-
console.log(
|
|
3889
|
+
console.log(chalk7.dim(` ${attempt.loginURL}`));
|
|
4075
3890
|
console.log();
|
|
4076
3891
|
spinner.start("Waiting for browser login...");
|
|
4077
3892
|
}
|
|
4078
3893
|
spinner.text = "Waiting for browser login...";
|
|
4079
3894
|
const result = await attempt.waitForResult();
|
|
4080
|
-
spinner.succeed(`Logged in as ${
|
|
3895
|
+
spinner.succeed(`Logged in as ${chalk7.bold(result.me.user.email)}`);
|
|
4081
3896
|
await continueFirstLoginFlow(result);
|
|
4082
3897
|
} catch (error) {
|
|
4083
3898
|
spinner.fail(
|
|
@@ -4111,8 +3926,8 @@ async function continueFirstLoginFlow(result) {
|
|
|
4111
3926
|
}
|
|
4112
3927
|
console.log();
|
|
4113
3928
|
console.log(
|
|
4114
|
-
|
|
4115
|
-
`Continuing first-time setup for ${
|
|
3929
|
+
chalk7.cyan(
|
|
3930
|
+
`Continuing first-time setup for ${chalk7.bold(machineHandle)}...
|
|
4116
3931
|
`
|
|
4117
3932
|
)
|
|
4118
3933
|
);
|
|
@@ -4125,8 +3940,8 @@ async function continueFirstLoginFlow(result) {
|
|
|
4125
3940
|
const spinner = ora8(`Preparing SSH access for ${machineHandle}...`).start();
|
|
4126
3941
|
try {
|
|
4127
3942
|
const connection = await prepareSSHConnectionByIdentifier(machineHandle);
|
|
4128
|
-
spinner.succeed(`Connecting to ${
|
|
4129
|
-
console.log(
|
|
3943
|
+
spinner.succeed(`Connecting to ${chalk7.bold(machineHandle)}`);
|
|
3944
|
+
console.log(chalk7.dim(` ${connection.command}`));
|
|
4130
3945
|
console.log();
|
|
4131
3946
|
await openSSHConnection(connection);
|
|
4132
3947
|
} catch (error) {
|
|
@@ -4137,19 +3952,19 @@ async function continueFirstLoginFlow(result) {
|
|
|
4137
3952
|
}
|
|
4138
3953
|
} catch (error) {
|
|
4139
3954
|
const message = error instanceof Error ? error.message : "Failed to finish first-time setup";
|
|
4140
|
-
console.error(
|
|
3955
|
+
console.error(chalk7.red(`
|
|
4141
3956
|
${message}`));
|
|
4142
3957
|
console.log();
|
|
4143
3958
|
if (result.provider === "claude") {
|
|
4144
3959
|
console.log(
|
|
4145
|
-
|
|
3960
|
+
chalk7.dim(` computer claude-login --machine ${machineHandle}`)
|
|
4146
3961
|
);
|
|
4147
3962
|
} else if (result.provider === "codex") {
|
|
4148
3963
|
console.log(
|
|
4149
|
-
|
|
3964
|
+
chalk7.dim(` computer codex-login --machine ${machineHandle}`)
|
|
4150
3965
|
);
|
|
4151
3966
|
}
|
|
4152
|
-
console.log(
|
|
3967
|
+
console.log(chalk7.dim(` computer ssh ${machineHandle}`));
|
|
4153
3968
|
console.log();
|
|
4154
3969
|
process.exit(1);
|
|
4155
3970
|
}
|
|
@@ -4163,43 +3978,451 @@ async function runSelectedProvider(provider, machineHandle) {
|
|
|
4163
3978
|
await runCodexLogin({ machine: machineHandle });
|
|
4164
3979
|
return;
|
|
4165
3980
|
}
|
|
4166
|
-
console.log(
|
|
3981
|
+
console.log(chalk7.green(`Sandbox ${chalk7.bold(machineHandle)} is ready.`));
|
|
4167
3982
|
console.log();
|
|
4168
3983
|
}
|
|
4169
3984
|
function printNextStep(machineHandle) {
|
|
4170
|
-
console.log(
|
|
4171
|
-
console.log(
|
|
3985
|
+
console.log(chalk7.green(`Sandbox ${chalk7.bold(machineHandle)} is ready.`));
|
|
3986
|
+
console.log(chalk7.dim(` computer ssh ${machineHandle}`));
|
|
4172
3987
|
console.log();
|
|
4173
3988
|
}
|
|
4174
3989
|
|
|
4175
|
-
// src/commands/
|
|
3990
|
+
// src/commands/mount.ts
|
|
4176
3991
|
import { Command as Command10 } from "commander";
|
|
4177
|
-
import
|
|
4178
|
-
|
|
3992
|
+
import chalk8 from "chalk";
|
|
3993
|
+
import ora9 from "ora";
|
|
3994
|
+
|
|
3995
|
+
// src/lib/mount-daemon.ts
|
|
3996
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
3997
|
+
function getMountControllerState(rootPath = defaultMountServiceConfig().rootPath) {
|
|
3998
|
+
const lock = readMountControllerLock(rootPath);
|
|
3999
|
+
if (!lock) {
|
|
4000
|
+
return { running: false };
|
|
4001
|
+
}
|
|
4002
|
+
if (!processExists(lock.pid)) {
|
|
4003
|
+
removeMountControllerLock(rootPath);
|
|
4004
|
+
return { running: false };
|
|
4005
|
+
}
|
|
4006
|
+
return { running: true, pid: lock.pid };
|
|
4007
|
+
}
|
|
4008
|
+
async function runMountDaemon(config) {
|
|
4009
|
+
const paths = getMountPaths(config.rootPath);
|
|
4010
|
+
ensureMountDirectories(paths);
|
|
4011
|
+
await mkdir3(paths.rootPath, { recursive: true });
|
|
4012
|
+
await acquireControllerLock(config.rootPath);
|
|
4013
|
+
writeMountStatusSnapshot(
|
|
4014
|
+
{
|
|
4015
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4016
|
+
controllerPid: process.pid,
|
|
4017
|
+
running: true,
|
|
4018
|
+
mounts: []
|
|
4019
|
+
},
|
|
4020
|
+
config.rootPath
|
|
4021
|
+
);
|
|
4022
|
+
await teardownManagedSessions(config, paths);
|
|
4023
|
+
let running = false;
|
|
4024
|
+
let queued = false;
|
|
4025
|
+
let shuttingDown = false;
|
|
4026
|
+
let activeRun = null;
|
|
4027
|
+
const runOnce = async () => {
|
|
4028
|
+
if (shuttingDown) {
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
if (running) {
|
|
4032
|
+
queued = true;
|
|
4033
|
+
return activeRun ?? void 0;
|
|
4034
|
+
}
|
|
4035
|
+
activeRun = (async () => {
|
|
4036
|
+
running = true;
|
|
4037
|
+
try {
|
|
4038
|
+
await reconcileMounts(config, paths, process.pid);
|
|
4039
|
+
} catch (error) {
|
|
4040
|
+
if (shuttingDown) {
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
const previous = readMountStatusSnapshot(config.rootPath);
|
|
4044
|
+
writeMountStatusSnapshot(
|
|
4045
|
+
{
|
|
4046
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4047
|
+
controllerPid: process.pid,
|
|
4048
|
+
running: true,
|
|
4049
|
+
lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
|
|
4050
|
+
lastError: error instanceof Error ? error.message : "mount reconcile failed",
|
|
4051
|
+
mounts: previous?.mounts ?? []
|
|
4052
|
+
},
|
|
4053
|
+
config.rootPath
|
|
4054
|
+
);
|
|
4055
|
+
console.error(
|
|
4056
|
+
error instanceof Error ? error.message : "mount reconcile failed"
|
|
4057
|
+
);
|
|
4058
|
+
} finally {
|
|
4059
|
+
running = false;
|
|
4060
|
+
activeRun = null;
|
|
4061
|
+
if (queued && !shuttingDown) {
|
|
4062
|
+
queued = false;
|
|
4063
|
+
void runOnce();
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
})();
|
|
4067
|
+
return activeRun;
|
|
4068
|
+
};
|
|
4069
|
+
const server = createMountControlServer(paths, async () => {
|
|
4070
|
+
await runOnce();
|
|
4071
|
+
});
|
|
4072
|
+
await new Promise((resolve, reject) => {
|
|
4073
|
+
server.once("error", reject);
|
|
4074
|
+
server.listen(paths.socketPath, () => resolve());
|
|
4075
|
+
});
|
|
4076
|
+
const interval = setInterval(() => {
|
|
4077
|
+
void runOnce();
|
|
4078
|
+
}, config.pollIntervalMs);
|
|
4079
|
+
const shutdown = async () => {
|
|
4080
|
+
if (shuttingDown) {
|
|
4081
|
+
return;
|
|
4082
|
+
}
|
|
4083
|
+
shuttingDown = true;
|
|
4084
|
+
clearInterval(interval);
|
|
4085
|
+
server.close();
|
|
4086
|
+
if (activeRun) {
|
|
4087
|
+
await activeRun.catch(() => {
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
await teardownManagedSessions(config, paths);
|
|
4091
|
+
const previous = readMountStatusSnapshot(config.rootPath);
|
|
4092
|
+
writeMountStatusSnapshot(
|
|
4093
|
+
{
|
|
4094
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4095
|
+
controllerPid: previous?.controllerPid,
|
|
4096
|
+
running: false,
|
|
4097
|
+
lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
|
|
4098
|
+
lastError: void 0,
|
|
4099
|
+
mounts: []
|
|
4100
|
+
},
|
|
4101
|
+
config.rootPath
|
|
4102
|
+
);
|
|
4103
|
+
removeMountControllerLock(config.rootPath);
|
|
4104
|
+
process.exit(0);
|
|
4105
|
+
};
|
|
4106
|
+
process.on("SIGINT", () => {
|
|
4107
|
+
void shutdown();
|
|
4108
|
+
});
|
|
4109
|
+
process.on("SIGTERM", () => {
|
|
4110
|
+
void shutdown();
|
|
4111
|
+
});
|
|
4112
|
+
await runOnce();
|
|
4113
|
+
await new Promise(() => {
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
async function acquireControllerLock(rootPath) {
|
|
4117
|
+
const existing = readMountControllerLock(rootPath);
|
|
4118
|
+
if (existing && processExists(existing.pid)) {
|
|
4119
|
+
throw new Error(`computer mount is already running (pid ${existing.pid})`);
|
|
4120
|
+
}
|
|
4121
|
+
removeMountControllerLock(rootPath);
|
|
4122
|
+
writeMountControllerLock(
|
|
4123
|
+
{
|
|
4124
|
+
pid: process.pid,
|
|
4125
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4126
|
+
},
|
|
4127
|
+
rootPath
|
|
4128
|
+
);
|
|
4129
|
+
}
|
|
4130
|
+
function processExists(pid) {
|
|
4131
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
4132
|
+
return false;
|
|
4133
|
+
}
|
|
4134
|
+
try {
|
|
4135
|
+
process.kill(pid, 0);
|
|
4136
|
+
return true;
|
|
4137
|
+
} catch {
|
|
4138
|
+
return false;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
// src/commands/mount.ts
|
|
4143
|
+
var mountCommand = new Command10("mount").description("Mirror SSH-ready machines under ~/agentcomputer while this command is running").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").action(async (options) => {
|
|
4144
|
+
const spinner = ora9("Starting machine mount controller...").start();
|
|
4145
|
+
try {
|
|
4146
|
+
const issues = getMountHostValidationIssues();
|
|
4147
|
+
if (issues.length > 0) {
|
|
4148
|
+
throw new Error(
|
|
4149
|
+
[
|
|
4150
|
+
...issues.map((issue) => issue.message),
|
|
4151
|
+
...formatMountHostInstallGuidance(issues)
|
|
4152
|
+
].join("\n")
|
|
4153
|
+
);
|
|
4154
|
+
}
|
|
4155
|
+
const sshSetup = await ensureSSHAccessConfigured(options);
|
|
4156
|
+
const config = {
|
|
4157
|
+
...defaultMountServiceConfig(),
|
|
4158
|
+
alias: sshSetup.alias,
|
|
4159
|
+
host: sshSetup.host,
|
|
4160
|
+
port: sshSetup.port,
|
|
4161
|
+
pollIntervalMs: parsePositiveInt(options.pollInterval, "poll interval"),
|
|
4162
|
+
connectTimeoutSeconds: parsePositiveInt(
|
|
4163
|
+
options.connectTimeout,
|
|
4164
|
+
"connect timeout"
|
|
4165
|
+
)
|
|
4166
|
+
};
|
|
4167
|
+
writeMountConfig(config);
|
|
4168
|
+
spinner.succeed("Machine mount controller running");
|
|
4169
|
+
console.log();
|
|
4170
|
+
console.log(chalk8.dim(` Root: ${config.rootPath}`));
|
|
4171
|
+
console.log(chalk8.dim(` SSH alias: ${config.alias}`));
|
|
4172
|
+
console.log(chalk8.dim(` Poll: ${config.pollIntervalMs}ms`));
|
|
4173
|
+
console.log();
|
|
4174
|
+
console.log(chalk8.dim(" Press Ctrl-C to stop syncing."));
|
|
4175
|
+
console.log();
|
|
4176
|
+
await runMountDaemon(config);
|
|
4177
|
+
} catch (error) {
|
|
4178
|
+
spinner.fail(
|
|
4179
|
+
error instanceof Error ? error.message : "Failed to start machine mount controller"
|
|
4180
|
+
);
|
|
4181
|
+
process.exit(1);
|
|
4182
|
+
}
|
|
4183
|
+
}).addCommand(
|
|
4184
|
+
new Command10("status").description("Show machine mount controller status").action(() => {
|
|
4185
|
+
const config = readMountConfig() ?? defaultMountServiceConfig();
|
|
4186
|
+
const controller = getMountControllerState(config.rootPath);
|
|
4187
|
+
const snapshot = readMountStatusSnapshot(config.rootPath);
|
|
4188
|
+
console.log();
|
|
4189
|
+
console.log(` ${chalk8.bold("Machine Mounts")}`);
|
|
4190
|
+
console.log();
|
|
4191
|
+
console.log(
|
|
4192
|
+
` ${chalk8.dim("Running")} ${controller.running ? chalk8.green("yes") : chalk8.dim("no")}`
|
|
4193
|
+
);
|
|
4194
|
+
if (controller.pid) {
|
|
4195
|
+
console.log(` ${chalk8.dim("PID")} ${controller.pid}`);
|
|
4196
|
+
}
|
|
4197
|
+
console.log(` ${chalk8.dim("Root")} ${config.rootPath}`);
|
|
4198
|
+
console.log(` ${chalk8.dim("Alias")} ${config.alias}`);
|
|
4199
|
+
console.log(
|
|
4200
|
+
` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ? minuteSecondAgo(snapshot.updatedAt) : chalk8.dim("never")}`
|
|
4201
|
+
);
|
|
4202
|
+
console.log(
|
|
4203
|
+
` ${chalk8.dim("Last sync")} ${snapshot?.lastSuccessfulSyncAt ? minuteSecondAgo(snapshot.lastSuccessfulSyncAt) : chalk8.dim("never")}`
|
|
4204
|
+
);
|
|
4205
|
+
if (controller.running && snapshot?.mounts.length) {
|
|
4206
|
+
console.log();
|
|
4207
|
+
for (const mount of snapshot.mounts) {
|
|
4208
|
+
const state = mount.state === "mounted" ? chalk8.green(mount.state) : mount.state === "pending" ? chalk8.yellow(mount.state) : chalk8.red(mount.state);
|
|
4209
|
+
console.log(` ${chalk8.white(mount.handle)} ${state} ${chalk8.dim(mount.mountPath)}`);
|
|
4210
|
+
if (mount.message) {
|
|
4211
|
+
console.log(` ${chalk8.dim(mount.message)}`);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
if (snapshot?.lastError) {
|
|
4216
|
+
console.log();
|
|
4217
|
+
console.log(` ${chalk8.dim("Last error")} ${chalk8.red(snapshot.lastError)}`);
|
|
4218
|
+
}
|
|
4219
|
+
console.log();
|
|
4220
|
+
})
|
|
4221
|
+
);
|
|
4222
|
+
function parsePositiveInt(raw, label) {
|
|
4223
|
+
const value = Number(raw);
|
|
4224
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
4225
|
+
throw new Error(`${label} must be a positive number`);
|
|
4226
|
+
}
|
|
4227
|
+
return Math.round(value);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
// src/commands/logout.ts
|
|
4231
|
+
import { Command as Command11 } from "commander";
|
|
4232
|
+
import chalk9 from "chalk";
|
|
4233
|
+
var logoutCommand = new Command11("logout").description("Remove stored API key").action(() => {
|
|
4179
4234
|
if (!getStoredAPIKey()) {
|
|
4180
4235
|
console.log();
|
|
4181
|
-
console.log(
|
|
4236
|
+
console.log(chalk9.dim(" Not logged in."));
|
|
4182
4237
|
if (hasEnvAPIKey()) {
|
|
4183
|
-
console.log(
|
|
4238
|
+
console.log(chalk9.dim(" Environment API key is still active in this shell."));
|
|
4184
4239
|
}
|
|
4185
4240
|
console.log();
|
|
4186
4241
|
return;
|
|
4187
4242
|
}
|
|
4188
4243
|
clearAPIKey();
|
|
4189
4244
|
console.log();
|
|
4190
|
-
console.log(
|
|
4245
|
+
console.log(chalk9.green(" Logged out."));
|
|
4191
4246
|
if (hasEnvAPIKey()) {
|
|
4192
|
-
console.log(
|
|
4247
|
+
console.log(chalk9.dim(" Environment API key is still active in this shell."));
|
|
4248
|
+
}
|
|
4249
|
+
console.log();
|
|
4250
|
+
});
|
|
4251
|
+
|
|
4252
|
+
// src/commands/upgrade.ts
|
|
4253
|
+
import { spawnSync } from "child_process";
|
|
4254
|
+
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
4255
|
+
import { Command as Command12 } from "commander";
|
|
4256
|
+
import chalk10 from "chalk";
|
|
4257
|
+
import ora10 from "ora";
|
|
4258
|
+
var pkg2 = JSON.parse(
|
|
4259
|
+
readFileSync2(new URL("../package.json", import.meta.url), "utf8")
|
|
4260
|
+
);
|
|
4261
|
+
function normalizeVersion(version) {
|
|
4262
|
+
return version.split("-")[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isNaN(part) ? 0 : part);
|
|
4263
|
+
}
|
|
4264
|
+
function compareVersions(a, b) {
|
|
4265
|
+
const left = normalizeVersion(a);
|
|
4266
|
+
const right = normalizeVersion(b);
|
|
4267
|
+
const size = Math.max(left.length, right.length);
|
|
4268
|
+
for (let index = 0; index < size; index += 1) {
|
|
4269
|
+
const diff = (left[index] ?? 0) - (right[index] ?? 0);
|
|
4270
|
+
if (diff !== 0) {
|
|
4271
|
+
return diff;
|
|
4272
|
+
}
|
|
4193
4273
|
}
|
|
4274
|
+
return 0;
|
|
4275
|
+
}
|
|
4276
|
+
function resolveExecutablePath() {
|
|
4277
|
+
const candidate = process.argv[1] || process.execPath;
|
|
4278
|
+
try {
|
|
4279
|
+
return realpathSync(candidate);
|
|
4280
|
+
} catch {
|
|
4281
|
+
return candidate;
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
function detectInstallMethod(executablePath) {
|
|
4285
|
+
const resolved = `${executablePath}\0${process.execPath}`.toLowerCase();
|
|
4286
|
+
if (resolved.includes("/nix/store/") || resolved.includes("\\nix\\store\\")) {
|
|
4287
|
+
return "nix";
|
|
4288
|
+
}
|
|
4289
|
+
if (resolved.includes("/.pnpm/") || resolved.includes("/pnpm/") || resolved.includes("\\pnpm\\")) {
|
|
4290
|
+
return "pnpm";
|
|
4291
|
+
}
|
|
4292
|
+
if (resolved.includes("/.yarn/") || resolved.includes("/yarn/") || resolved.includes("\\yarn\\")) {
|
|
4293
|
+
return "yarn";
|
|
4294
|
+
}
|
|
4295
|
+
if (resolved.includes("/node_modules/") || resolved.includes("\\node_modules\\")) {
|
|
4296
|
+
return "npm";
|
|
4297
|
+
}
|
|
4298
|
+
return "unknown";
|
|
4299
|
+
}
|
|
4300
|
+
function findNixProfileElement(executablePath) {
|
|
4301
|
+
const result = spawnSync("nix", ["profile", "list", "--json"], {
|
|
4302
|
+
encoding: "utf8"
|
|
4303
|
+
});
|
|
4304
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
4305
|
+
return null;
|
|
4306
|
+
}
|
|
4307
|
+
const profile = JSON.parse(result.stdout);
|
|
4308
|
+
const resolvedExecutable = executablePath.toLowerCase();
|
|
4309
|
+
for (const [name, element] of Object.entries(profile.elements ?? {})) {
|
|
4310
|
+
const storePaths = Array.isArray(element.storePaths) ? element.storePaths.map((storePath) => storePath.toLowerCase()) : [];
|
|
4311
|
+
if (storePaths.some((storePath) => resolvedExecutable.startsWith(storePath))) {
|
|
4312
|
+
return name;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
for (const [name, element] of Object.entries(profile.elements ?? {})) {
|
|
4316
|
+
const originalUrl = String(element.originalUrl ?? "").toLowerCase();
|
|
4317
|
+
if (originalUrl.includes("agentcomputer") || originalUrl.includes("apps/cli")) {
|
|
4318
|
+
return name;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
return null;
|
|
4322
|
+
}
|
|
4323
|
+
function resolveUpgradeCommand(method, executablePath) {
|
|
4324
|
+
const packageName = pkg2.name ?? "aicomputer";
|
|
4325
|
+
switch (method) {
|
|
4326
|
+
case "nix": {
|
|
4327
|
+
const element = findNixProfileElement(executablePath);
|
|
4328
|
+
if (!element) {
|
|
4329
|
+
throw new Error(
|
|
4330
|
+
"Nix install detected, but no matching Nix profile entry was found. Rerun your original Nix install command or update that profile manually."
|
|
4331
|
+
);
|
|
4332
|
+
}
|
|
4333
|
+
return {
|
|
4334
|
+
command: "nix",
|
|
4335
|
+
args: ["profile", "upgrade", element],
|
|
4336
|
+
label: `nix profile upgrade ${element}`
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
case "pnpm":
|
|
4340
|
+
return {
|
|
4341
|
+
command: "pnpm",
|
|
4342
|
+
args: ["add", "-g", `${packageName}@latest`],
|
|
4343
|
+
label: `pnpm add -g ${packageName}@latest`
|
|
4344
|
+
};
|
|
4345
|
+
case "yarn":
|
|
4346
|
+
return {
|
|
4347
|
+
command: "yarn",
|
|
4348
|
+
args: ["global", "add", `${packageName}@latest`],
|
|
4349
|
+
label: `yarn global add ${packageName}@latest`
|
|
4350
|
+
};
|
|
4351
|
+
case "npm":
|
|
4352
|
+
case "unknown":
|
|
4353
|
+
return {
|
|
4354
|
+
command: "npm",
|
|
4355
|
+
args: ["install", "-g", `${packageName}@latest`],
|
|
4356
|
+
label: `npm install -g ${packageName}@latest`
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
async function getLatestVersion(packageName) {
|
|
4361
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
4362
|
+
if (!response.ok) {
|
|
4363
|
+
throw new Error(`Failed to check npm registry (${response.status})`);
|
|
4364
|
+
}
|
|
4365
|
+
const payload = await response.json();
|
|
4366
|
+
if (!payload.version) {
|
|
4367
|
+
throw new Error("Registry response missing version");
|
|
4368
|
+
}
|
|
4369
|
+
return payload.version;
|
|
4370
|
+
}
|
|
4371
|
+
var upgradeCommand = new Command12("upgrade").description("Update the CLI to the latest version").action(async () => {
|
|
4372
|
+
const currentVersion = pkg2.version ?? "0.0.0";
|
|
4373
|
+
const packageName = pkg2.name ?? "aicomputer";
|
|
4374
|
+
const spinner = ora10("Checking for updates...").start();
|
|
4375
|
+
let latestVersion;
|
|
4376
|
+
try {
|
|
4377
|
+
latestVersion = await getLatestVersion(packageName);
|
|
4378
|
+
} catch (error) {
|
|
4379
|
+
spinner.fail(
|
|
4380
|
+
error instanceof Error ? error.message : "Failed to check for updates"
|
|
4381
|
+
);
|
|
4382
|
+
process.exit(1);
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
|
4386
|
+
spinner.succeed(`You're up to date (v${currentVersion}).`);
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
const executablePath = resolveExecutablePath();
|
|
4390
|
+
const method = detectInstallMethod(executablePath);
|
|
4391
|
+
let upgrade;
|
|
4392
|
+
try {
|
|
4393
|
+
upgrade = resolveUpgradeCommand(method, executablePath);
|
|
4394
|
+
} catch (error) {
|
|
4395
|
+
spinner.fail(
|
|
4396
|
+
error instanceof Error ? error.message : "Failed to prepare upgrade"
|
|
4397
|
+
);
|
|
4398
|
+
process.exit(1);
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
spinner.stop();
|
|
4194
4402
|
console.log();
|
|
4403
|
+
console.log(
|
|
4404
|
+
chalk10.dim(` Updating ${chalk10.bold(`v${currentVersion}`)} -> ${chalk10.bold(`v${latestVersion}`)}`)
|
|
4405
|
+
);
|
|
4406
|
+
console.log(chalk10.dim(` ${upgrade.label}`));
|
|
4407
|
+
console.log();
|
|
4408
|
+
const result = spawnSync(upgrade.command, upgrade.args, {
|
|
4409
|
+
stdio: "inherit"
|
|
4410
|
+
});
|
|
4411
|
+
if (result.status === 0) {
|
|
4412
|
+
console.log();
|
|
4413
|
+
console.log(chalk10.green(` Updated to v${latestVersion}.`));
|
|
4414
|
+
console.log();
|
|
4415
|
+
return;
|
|
4416
|
+
}
|
|
4417
|
+
process.exit(result.status ?? 1);
|
|
4195
4418
|
});
|
|
4196
4419
|
|
|
4197
4420
|
// src/commands/whoami.ts
|
|
4198
|
-
import { Command as
|
|
4421
|
+
import { Command as Command13 } from "commander";
|
|
4199
4422
|
import chalk11 from "chalk";
|
|
4200
|
-
import
|
|
4201
|
-
var whoamiCommand = new
|
|
4202
|
-
const spinner = options.json ? null :
|
|
4423
|
+
import ora11 from "ora";
|
|
4424
|
+
var whoamiCommand = new Command13("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
|
|
4425
|
+
const spinner = options.json ? null : ora11("Loading user...").start();
|
|
4203
4426
|
try {
|
|
4204
4427
|
const me = await api("/v1/me");
|
|
4205
4428
|
spinner?.stop();
|
|
@@ -4228,11 +4451,11 @@ var whoamiCommand = new Command11("whoami").description("Show current user").opt
|
|
|
4228
4451
|
});
|
|
4229
4452
|
|
|
4230
4453
|
// src/index.ts
|
|
4231
|
-
var
|
|
4454
|
+
var pkg3 = JSON.parse(
|
|
4232
4455
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
4233
4456
|
);
|
|
4234
4457
|
var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
|
|
4235
|
-
var program = new
|
|
4458
|
+
var program = new Command14();
|
|
4236
4459
|
function appendTextSection(lines, title, values) {
|
|
4237
4460
|
if (values.length === 0) {
|
|
4238
4461
|
return;
|
|
@@ -4266,13 +4489,14 @@ function commandPath(cmd) {
|
|
|
4266
4489
|
return parts.join(" ");
|
|
4267
4490
|
}
|
|
4268
4491
|
function formatRootHelp(cmd) {
|
|
4269
|
-
const version =
|
|
4492
|
+
const version = pkg3.version ?? "0.0.0";
|
|
4270
4493
|
const lines = [];
|
|
4271
4494
|
const groups = [
|
|
4272
4495
|
["Auth", []],
|
|
4273
4496
|
["Computers", []],
|
|
4274
4497
|
["Images", []],
|
|
4275
4498
|
["Access", []],
|
|
4499
|
+
["Mounts", []],
|
|
4276
4500
|
["Agents", []],
|
|
4277
4501
|
["Other", []]
|
|
4278
4502
|
];
|
|
@@ -4295,8 +4519,10 @@ function formatRootHelp(cmd) {
|
|
|
4295
4519
|
groups[2][1].push(entry);
|
|
4296
4520
|
} else if (["open", "ssh", "ports"].includes(name)) {
|
|
4297
4521
|
groups[3][1].push(entry);
|
|
4298
|
-
} else if (
|
|
4522
|
+
} else if (name === "mount") {
|
|
4299
4523
|
groups[4][1].push(entry);
|
|
4524
|
+
} else if (["agent", "acp"].includes(name)) {
|
|
4525
|
+
groups[5][1].push(entry);
|
|
4300
4526
|
} else {
|
|
4301
4527
|
otherGroup.push(entry);
|
|
4302
4528
|
}
|
|
@@ -4357,8 +4583,9 @@ function applyHelpFormatting(cmd) {
|
|
|
4357
4583
|
applyHelpFormatting(subcommand);
|
|
4358
4584
|
}
|
|
4359
4585
|
}
|
|
4360
|
-
program.name(cliName).description("Agent Computer CLI").version(
|
|
4586
|
+
program.name(cliName).description("Agent Computer CLI").version(pkg3.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts");
|
|
4361
4587
|
program.addCommand(loginCommand);
|
|
4588
|
+
program.addCommand(upgradeCommand);
|
|
4362
4589
|
program.addCommand(logoutCommand);
|
|
4363
4590
|
program.addCommand(whoamiCommand);
|
|
4364
4591
|
program.addCommand(claudeLoginCommand);
|
|
@@ -4368,11 +4595,11 @@ program.addCommand(lsCommand);
|
|
|
4368
4595
|
program.addCommand(getCommand);
|
|
4369
4596
|
program.addCommand(imageCommand);
|
|
4370
4597
|
program.addCommand(agentCommand);
|
|
4371
|
-
program.addCommand(fleetCommand);
|
|
4372
4598
|
program.addCommand(acpCommand);
|
|
4373
4599
|
program.addCommand(openCommand);
|
|
4374
4600
|
program.addCommand(sshCommand);
|
|
4375
4601
|
program.addCommand(portsCommand);
|
|
4602
|
+
program.addCommand(mountCommand);
|
|
4376
4603
|
program.addCommand(removeCommand);
|
|
4377
4604
|
program.addCommand(completionCommand);
|
|
4378
4605
|
applyHelpFormatting(program);
|