@opencomputer/cli 0.6.5 → 0.6.7
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 +21 -17
- package/dist/api.d.ts +96 -2
- package/dist/api.js +60 -7
- package/dist/api.js.map +1 -1
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +21 -0
- package/dist/api.test.js.map +1 -0
- package/dist/binding.d.ts +0 -2
- package/dist/binding.js +7 -31
- package/dist/binding.js.map +1 -1
- package/dist/binding.test.js +24 -3
- package/dist/binding.test.js.map +1 -1
- package/dist/commands.d.ts +1 -0
- package/dist/commands.js +387 -42
- package/dist/commands.js.map +1 -1
- package/dist/dev.d.ts +0 -8
- package/dist/dev.js +1 -159
- package/dist/dev.js.map +1 -1
- package/dist/dev.test.js +2 -90
- package/dist/dev.test.js.map +1 -1
- package/dist/doctor.d.ts +19 -0
- package/dist/doctor.js +207 -0
- package/dist/doctor.js.map +1 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +71 -0
- package/dist/doctor.test.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +74 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +14 -0
- package/dist/errors.test.js.map +1 -0
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/project-local.d.ts +7 -0
- package/dist/project-local.js +106 -0
- package/dist/project-local.js.map +1 -0
- package/dist/project-local.test.d.ts +1 -0
- package/dist/project-local.test.js +30 -0
- package/dist/project-local.test.js.map +1 -0
- package/dist/project.js +10 -10
- package/dist/project.test.js +1 -1
- package/dist/project.test.js.map +1 -1
- package/dist/template-local.d.ts +6 -0
- package/dist/template-local.js +57 -0
- package/dist/template-local.js.map +1 -0
- package/dist/template.d.ts +72 -0
- package/dist/template.js +326 -0
- package/dist/template.js.map +1 -0
- package/dist/template.test.d.ts +1 -0
- package/dist/template.test.js +109 -0
- package/dist/template.test.js.map +1 -0
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -7,6 +7,12 @@ import { ensureProjectBinding, findOpenComputerProjectRoot, } from "./binding.js
|
|
|
7
7
|
import { assertStarterTarget, buildAgentArtifact, findAgentRoot, initializeAgentProject, } from "./project.js";
|
|
8
8
|
import { developmentAgentReference, parseSessionCommand, resolveProjectAgent, } from "./session-command.js";
|
|
9
9
|
import { formatSessionEvent } from "./session-prompt.js";
|
|
10
|
+
import { buildTemplateProject, normalizeTemplateRepositoryUrl, readTemplateManifest, templateDeployUrl, } from "./template.js";
|
|
11
|
+
import { defaultTemplateDirectory, materializeTemplateCheckout, } from "./template-local.js";
|
|
12
|
+
import { materializeProjectArchive } from "./project-local.js";
|
|
13
|
+
import { createInterface } from "node:readline/promises";
|
|
14
|
+
import { doctorProject } from "./doctor.js";
|
|
15
|
+
import { CLIError } from "./errors.js";
|
|
10
16
|
export function deploymentAlias(requestedAlias) {
|
|
11
17
|
return requestedAlias ?? "development";
|
|
12
18
|
}
|
|
@@ -16,6 +22,9 @@ export function shouldBindModelAccessProject(projectReference, currentAgentRoot)
|
|
|
16
22
|
function printJSON(value) {
|
|
17
23
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
18
24
|
}
|
|
25
|
+
function printJSONLine(value) {
|
|
26
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
27
|
+
}
|
|
19
28
|
function flag(args, name) {
|
|
20
29
|
const index = args.indexOf(name);
|
|
21
30
|
if (index < 0)
|
|
@@ -56,15 +65,13 @@ function consumeModelAccessProvider(args) {
|
|
|
56
65
|
}
|
|
57
66
|
return "codex";
|
|
58
67
|
}
|
|
59
|
-
async function
|
|
68
|
+
async function readTemplateSecretValue() {
|
|
60
69
|
if (!process.stdin.isTTY) {
|
|
61
70
|
const chunks = [];
|
|
62
71
|
for await (const chunk of process.stdin) {
|
|
63
72
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
64
73
|
}
|
|
65
|
-
const value = Buffer.concat(chunks)
|
|
66
|
-
.toString("utf8")
|
|
67
|
-
.replace(/\r?\n$/, "");
|
|
74
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
68
75
|
if (!value)
|
|
69
76
|
throw new Error("Secret value was empty");
|
|
70
77
|
return value;
|
|
@@ -101,7 +108,33 @@ async function readSecretValue() {
|
|
|
101
108
|
process.stdin.on("data", onData);
|
|
102
109
|
});
|
|
103
110
|
}
|
|
104
|
-
async function
|
|
111
|
+
async function readStdinValue(enabled) {
|
|
112
|
+
if (!enabled) {
|
|
113
|
+
throw new CLIError("value_stdin_required", "A value must be supplied through standard input.", "Pipe the value into this command and add `--value-stdin`.");
|
|
114
|
+
}
|
|
115
|
+
if (process.stdin.isTTY) {
|
|
116
|
+
throw new CLIError("value_stdin_required", "--value-stdin requires piped standard input.", "Use `printf %s \"$VALUE\" | opencomputer ... --value-stdin`.");
|
|
117
|
+
}
|
|
118
|
+
const chunks = [];
|
|
119
|
+
for await (const chunk of process.stdin) {
|
|
120
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
121
|
+
}
|
|
122
|
+
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
123
|
+
if (!value)
|
|
124
|
+
throw new Error("Standard-input value was empty");
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function printDoctor(result, json) {
|
|
128
|
+
if (json)
|
|
129
|
+
return printJSON(result);
|
|
130
|
+
for (const item of result.diagnostics) {
|
|
131
|
+
process.stdout.write(`${item.severity.toUpperCase()} ${item.code} ${item.file}${item.line ? `:${item.line}` : ""}\n` +
|
|
132
|
+
` ${item.message}\n fix: ${item.hint}\n`);
|
|
133
|
+
}
|
|
134
|
+
process.stdout.write(`${result.ok ? "Doctor passed" : "Doctor failed"}: ${result.summary.errors} errors, ` +
|
|
135
|
+
`${result.summary.warnings} warnings in ${result.durationMs}ms.\n`);
|
|
136
|
+
}
|
|
137
|
+
async function selectedProject(client, config, reference) {
|
|
105
138
|
if (reference) {
|
|
106
139
|
const project = (await client.projects()).find((candidate) => candidate.id === reference || candidate.slug === reference);
|
|
107
140
|
if (!project)
|
|
@@ -112,9 +145,7 @@ async function selectedProject(client, config, reference, interactive = true) {
|
|
|
112
145
|
return { projectId: project.id, agentId: agent.id };
|
|
113
146
|
}
|
|
114
147
|
const root = await findOpenComputerProjectRoot(process.cwd());
|
|
115
|
-
const binding = await ensureProjectBinding(client, config, root
|
|
116
|
-
interactive,
|
|
117
|
-
});
|
|
148
|
+
const binding = await ensureProjectBinding(client, config, root);
|
|
118
149
|
return { projectId: binding.projectId, agentId: binding.agentId };
|
|
119
150
|
}
|
|
120
151
|
async function selectedSessionAgent(client, project, selector) {
|
|
@@ -200,7 +231,7 @@ function printAgentEvent(event, json) {
|
|
|
200
231
|
printToolProgress(event);
|
|
201
232
|
}
|
|
202
233
|
}
|
|
203
|
-
async function sendAgentTurn(client, sessionId, prompt, keep, json) {
|
|
234
|
+
async function sendAgentTurn(client, sessionId, prompt, keep, json, idempotencyKey) {
|
|
204
235
|
const existing = await client.events(sessionId, 0);
|
|
205
236
|
let cursor = existing.at(-1)?.seq ?? 0;
|
|
206
237
|
const session = await client.session(sessionId);
|
|
@@ -210,7 +241,7 @@ async function sendAgentTurn(client, sessionId, prompt, keep, json) {
|
|
|
210
241
|
const connected = await waitForEvent(client, sessionId, cursor, (event) => event.type === "runtime.connected", () => undefined, 90_000);
|
|
211
242
|
cursor = connected.cursor;
|
|
212
243
|
}
|
|
213
|
-
const turn = await client.createTurn(sessionId, prompt);
|
|
244
|
+
const turn = await client.createTurn(sessionId, prompt, idempotencyKey);
|
|
214
245
|
let streamedText = "";
|
|
215
246
|
let completedText = "";
|
|
216
247
|
const completed = await waitForEvent(client, sessionId, cursor, (event) => event.type === "turn.completed" || event.type === "turn.failed", (event) => {
|
|
@@ -274,6 +305,33 @@ async function attachSession(client, sessionId, json) {
|
|
|
274
305
|
process.off("SIGINT", stop);
|
|
275
306
|
}
|
|
276
307
|
}
|
|
308
|
+
async function tailSession(client, sessionId, after, follow, json) {
|
|
309
|
+
let cursor = after;
|
|
310
|
+
let stopped = false;
|
|
311
|
+
const stop = () => {
|
|
312
|
+
stopped = true;
|
|
313
|
+
};
|
|
314
|
+
process.once("SIGINT", stop);
|
|
315
|
+
try {
|
|
316
|
+
do {
|
|
317
|
+
const events = await client.events(sessionId, cursor);
|
|
318
|
+
for (const event of events) {
|
|
319
|
+
cursor = Math.max(cursor, event.seq);
|
|
320
|
+
if (json)
|
|
321
|
+
printJSONLine({ sessionId, cursor: event.seq, ...event });
|
|
322
|
+
else {
|
|
323
|
+
process.stdout.write(`${event.seq} ${event.type} ${JSON.stringify(event.data)}\n`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (follow && !stopped) {
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
328
|
+
}
|
|
329
|
+
} while (follow && !stopped);
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
process.off("SIGINT", stop);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
277
335
|
export function nextAgentEventDeadline(deadline, timeoutMs, receivedEvents, now = Date.now()) {
|
|
278
336
|
return receivedEvents > 0 ? now + timeoutMs : deadline;
|
|
279
337
|
}
|
|
@@ -301,11 +359,11 @@ async function waitForEvent(client, sessionId, after, terminal, onEvent, timeout
|
|
|
301
359
|
}
|
|
302
360
|
throw new Error("Timed out waiting for the agent.");
|
|
303
361
|
}
|
|
304
|
-
async function runAgent(client, agent, prompt, keep, json, verbose) {
|
|
362
|
+
async function runAgent(client, agent, prompt, keep, json, verbose, idempotencyKey) {
|
|
305
363
|
const created = await client.createSession(agent);
|
|
306
364
|
process.stderr.write(`Starting ${agent}…\n`);
|
|
307
365
|
const connected = await waitForEvent(client, created.session.id, 0, (event) => event.type === "runtime.connected", (event) => printSessionProgress(event, json, verbose), 90_000);
|
|
308
|
-
const turn = await client.createTurn(created.session.id, prompt);
|
|
366
|
+
const turn = await client.createTurn(created.session.id, prompt, idempotencyKey);
|
|
309
367
|
let streamed = false;
|
|
310
368
|
let streamedText = "";
|
|
311
369
|
let completedText = "";
|
|
@@ -347,8 +405,224 @@ async function runAgent(client, agent, prompt, keep, json, verbose) {
|
|
|
347
405
|
}
|
|
348
406
|
export async function runCommand(command, rawArgs, globals) {
|
|
349
407
|
const args = [...rawArgs];
|
|
408
|
+
if (command === "template" && args[0] === "validate") {
|
|
409
|
+
args.shift();
|
|
410
|
+
const repositoryUrl = option(args, "--repository-url");
|
|
411
|
+
const appUrl = option(args, "--app-url");
|
|
412
|
+
const directory = args.shift() ?? process.cwd();
|
|
413
|
+
if (args.length)
|
|
414
|
+
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
415
|
+
const manifest = await readTemplateManifest(directory);
|
|
416
|
+
const deployUrl = repositoryUrl
|
|
417
|
+
? templateDeployUrl(repositoryUrl, appUrl)
|
|
418
|
+
: undefined;
|
|
419
|
+
if (globals.json) {
|
|
420
|
+
printJSON({ file: "oc-template.toml", manifest, deployUrl });
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
process.stdout.write(`Valid template: ${manifest.template.name}\n` +
|
|
424
|
+
(deployUrl ? `Deploy URL: ${deployUrl}\n` : ""));
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (command === "template" && args[0] === "build") {
|
|
429
|
+
args.shift();
|
|
430
|
+
const output = option(args, "--output");
|
|
431
|
+
const directory = args.shift() ?? process.cwd();
|
|
432
|
+
if (args.length)
|
|
433
|
+
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
434
|
+
const bundle = await buildTemplateProject(directory);
|
|
435
|
+
const serialized = `${JSON.stringify(bundle)}\n`;
|
|
436
|
+
if (output) {
|
|
437
|
+
const { writeFile } = await import("node:fs/promises");
|
|
438
|
+
await writeFile(output, serialized, { mode: 0o600 });
|
|
439
|
+
if (!globals.json)
|
|
440
|
+
process.stdout.write(`Built template bundle: ${output}\n`);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
process.stdout.write(serialized);
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
350
447
|
const config = await resolveConfig(globals);
|
|
351
|
-
const client = new OpenComputerClient(config);
|
|
448
|
+
const client = new OpenComputerClient(config, globals.idempotencyKey);
|
|
449
|
+
if (command === "project" && args[0] === "clone") {
|
|
450
|
+
args.shift();
|
|
451
|
+
const directory = option(args, "--directory");
|
|
452
|
+
const projectId = args.shift();
|
|
453
|
+
if (!projectId || args.length) {
|
|
454
|
+
throw new Error("Usage: opencomputer project clone <project-id> [--directory <path>]");
|
|
455
|
+
}
|
|
456
|
+
const project = (await client.projects()).find((candidate) => candidate.id === projectId);
|
|
457
|
+
if (!project)
|
|
458
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
459
|
+
const checkout = await materializeProjectArchive({
|
|
460
|
+
response: await client.projectSourceArchive(project.id),
|
|
461
|
+
directory: directory ?? project.slug,
|
|
462
|
+
});
|
|
463
|
+
const binding = await ensureProjectBinding(client, config, checkout.directory, {
|
|
464
|
+
project: project.id,
|
|
465
|
+
});
|
|
466
|
+
if (globals.json)
|
|
467
|
+
printJSON({ checkout, binding });
|
|
468
|
+
else {
|
|
469
|
+
process.stdout.write(`Cloned ${binding.projectName} (${binding.projectId}).\n\n` +
|
|
470
|
+
`Next:\n cd ${checkout.directory}\n npm install\n` +
|
|
471
|
+
` npm run deploy -- --watch --api-url ${config.apiUrl}\n`);
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (command === "template") {
|
|
476
|
+
const action = args.shift();
|
|
477
|
+
if (action === "clone") {
|
|
478
|
+
const commitSha = option(args, "--commit");
|
|
479
|
+
const project = option(args, "--project");
|
|
480
|
+
const directory = option(args, "--directory");
|
|
481
|
+
const repositoryUrl = args.shift();
|
|
482
|
+
if (!repositoryUrl || !commitSha || !project || args.length) {
|
|
483
|
+
throw new Error("Usage: opencomputer template clone <repository-url> --commit <sha> --project <id> [--directory <path>]");
|
|
484
|
+
}
|
|
485
|
+
const normalized = normalizeTemplateRepositoryUrl(repositoryUrl);
|
|
486
|
+
const checkout = await materializeTemplateCheckout(normalized, commitSha, directory);
|
|
487
|
+
const binding = await ensureProjectBinding(client, config, checkout.directory, { project });
|
|
488
|
+
if (globals.json)
|
|
489
|
+
printJSON({ checkout, binding });
|
|
490
|
+
else {
|
|
491
|
+
process.stdout.write(`Cloned ${normalized}@${commitSha.slice(0, 12)}\n` +
|
|
492
|
+
`Linked to ${binding.projectName} (${binding.projectId}).\n\n` +
|
|
493
|
+
`Next:\n cd ${checkout.directory}\n npm install\n npm run deploy -- --watch\n`);
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (action !== "deploy") {
|
|
498
|
+
throw new Error("Usage: opencomputer template <deploy|clone> ...");
|
|
499
|
+
}
|
|
500
|
+
const projectNameOption = option(args, "--project-name");
|
|
501
|
+
const directoryOption = option(args, "--directory");
|
|
502
|
+
const confirmed = flag(args, "--yes");
|
|
503
|
+
const repositoryUrl = args.shift();
|
|
504
|
+
if (!repositoryUrl) {
|
|
505
|
+
throw new Error("A GitHub repository URL is required");
|
|
506
|
+
}
|
|
507
|
+
if (args.length)
|
|
508
|
+
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
509
|
+
const inspection = await client.inspectTemplate(normalizeTemplateRepositoryUrl(repositoryUrl));
|
|
510
|
+
if (!globals.json) {
|
|
511
|
+
process.stdout.write(`\n${inspection.template.name}\n${inspection.template.description}\n` +
|
|
512
|
+
`Source: ${inspection.repository.fullName}@${inspection.repository.commitSha.slice(0, 12)}\n` +
|
|
513
|
+
`Agents: ${inspection.agents.map((agent) => agent.name).join(", ")}\n` +
|
|
514
|
+
`Target: Development\n\n`);
|
|
515
|
+
}
|
|
516
|
+
if (inspection.requirements.connections.length) {
|
|
517
|
+
throw new Error("This template needs an interactive provider connection. Open its deploy URL in the dashboard to continue.");
|
|
518
|
+
}
|
|
519
|
+
const requiredSecrets = inspection.requirements.secrets.filter((requirement) => requirement.required !== false);
|
|
520
|
+
if (!process.stdin.isTTY && requiredSecrets.length) {
|
|
521
|
+
throw new Error(`Secret ${requiredSecrets[0].name} requires an interactive terminal; secret values are never accepted as command-line arguments`);
|
|
522
|
+
}
|
|
523
|
+
if ((!process.stdin.isTTY || !process.stdout.isTTY) && !projectNameOption) {
|
|
524
|
+
throw new Error("--project-name is required in non-interactive mode");
|
|
525
|
+
}
|
|
526
|
+
const terminal = process.stdin.isTTY && process.stdout.isTTY
|
|
527
|
+
? createInterface({ input: process.stdin, output: process.stdout })
|
|
528
|
+
: undefined;
|
|
529
|
+
let projectName = projectNameOption ??
|
|
530
|
+
inspection.template.defaultProjectName ??
|
|
531
|
+
inspection.template.name;
|
|
532
|
+
if (terminal && !projectNameOption) {
|
|
533
|
+
projectName =
|
|
534
|
+
(await terminal.question(`Project name [${projectName}]: `)).trim() ||
|
|
535
|
+
projectName;
|
|
536
|
+
}
|
|
537
|
+
if (terminal && !confirmed) {
|
|
538
|
+
const answer = (await terminal.question("Create this Development project? [y/N] "))
|
|
539
|
+
.trim()
|
|
540
|
+
.toLowerCase();
|
|
541
|
+
if (answer !== "y" && answer !== "yes") {
|
|
542
|
+
terminal.close();
|
|
543
|
+
throw new Error("Template deployment cancelled");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const checkout = await materializeTemplateCheckout(inspection.repository.url, inspection.repository.commitSha, directoryOption ?? defaultTemplateDirectory(inspection.repository.url));
|
|
547
|
+
const runtimeValues = new Map();
|
|
548
|
+
for (const requirement of inspection.requirements.runtimeVariables) {
|
|
549
|
+
if (!terminal) {
|
|
550
|
+
if (requirement.required) {
|
|
551
|
+
throw new Error(`Runtime variable ${requirement.name} requires an interactive terminal`);
|
|
552
|
+
}
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const hint = requirement.example ? ` [${requirement.example}]` : "";
|
|
556
|
+
const value = (await terminal.question(`${requirement.name}${hint}: `)).trim() ||
|
|
557
|
+
requirement.example ||
|
|
558
|
+
"";
|
|
559
|
+
if (requirement.required && !value) {
|
|
560
|
+
terminal.close();
|
|
561
|
+
throw new Error(`${requirement.name} is required`);
|
|
562
|
+
}
|
|
563
|
+
if (value)
|
|
564
|
+
runtimeValues.set(requirement.name, value);
|
|
565
|
+
}
|
|
566
|
+
terminal?.close();
|
|
567
|
+
const installation = await client.createTemplateInstallation({
|
|
568
|
+
inspectionId: inspection.id,
|
|
569
|
+
projectName,
|
|
570
|
+
idempotencyKey: crypto.randomUUID(),
|
|
571
|
+
});
|
|
572
|
+
const installationAgentId = (localAgentId) => !localAgentId || localAgentId === inspection.agents[0]?.id
|
|
573
|
+
? installation.projectAgentId
|
|
574
|
+
: `${installation.projectAgentId}--${localAgentId}`;
|
|
575
|
+
for (const requirement of requiredSecrets) {
|
|
576
|
+
process.stderr.write(`${requirement.name}\n`);
|
|
577
|
+
const value = await readTemplateSecretValue();
|
|
578
|
+
await client.putSecret({
|
|
579
|
+
projectId: installation.projectId,
|
|
580
|
+
name: requirement.name,
|
|
581
|
+
value,
|
|
582
|
+
environment: "development",
|
|
583
|
+
agentId: installationAgentId(requirement.agentId),
|
|
584
|
+
allowedOrigins: requirement.allowedOrigins,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
for (const requirement of inspection.requirements.runtimeVariables) {
|
|
588
|
+
const value = runtimeValues.get(requirement.name);
|
|
589
|
+
if (!value)
|
|
590
|
+
continue;
|
|
591
|
+
await client.putRuntimeVariable({
|
|
592
|
+
projectId: installation.projectId,
|
|
593
|
+
name: requirement.name,
|
|
594
|
+
value,
|
|
595
|
+
environment: "development",
|
|
596
|
+
agentId: installationAgentId(requirement.agentId),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
let current = await client.finalizeTemplateInstallation(installation.id);
|
|
600
|
+
const deadline = Date.now() + 10 * 60_000;
|
|
601
|
+
while (!["ready", "failed"].includes(current.state) &&
|
|
602
|
+
Date.now() < deadline) {
|
|
603
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1_000));
|
|
604
|
+
current = await client.templateInstallation(current.id);
|
|
605
|
+
if (!globals.json)
|
|
606
|
+
process.stderr.write(`Template installation: ${current.state}\r`);
|
|
607
|
+
}
|
|
608
|
+
if (current.state === "failed") {
|
|
609
|
+
throw new Error(current.error?.message ?? "Template installation failed");
|
|
610
|
+
}
|
|
611
|
+
if (current.state !== "ready") {
|
|
612
|
+
throw new Error(`Template installation is still ${current.state}; ${current.projectUrl}`);
|
|
613
|
+
}
|
|
614
|
+
const binding = await ensureProjectBinding(client, config, checkout.directory, {
|
|
615
|
+
project: current.projectId,
|
|
616
|
+
});
|
|
617
|
+
if (globals.json)
|
|
618
|
+
printJSON({ installation: current, checkout, binding });
|
|
619
|
+
else {
|
|
620
|
+
process.stdout.write(`\nReady: ${current.projectUrl}\n` +
|
|
621
|
+
`Local: ${checkout.directory}\n\n` +
|
|
622
|
+
`Next:\n cd ${checkout.directory}\n npm install\n npm run deploy -- --watch\n`);
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
352
626
|
if (command === "login") {
|
|
353
627
|
const identity = await login(config, {
|
|
354
628
|
noBrowser: flag(args, "--no-browser"),
|
|
@@ -409,7 +683,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
409
683
|
const enterDirectory = directory === "." ? "" : ` cd ${directory}\n`;
|
|
410
684
|
process.stdout.write(`Created the ${initialized.manifest.name} OpenComputer app\n` +
|
|
411
685
|
`Directory: ${initialized.root}\n` +
|
|
412
|
-
`Project:
|
|
686
|
+
`Project: link explicitly with --project or --create-project\n` +
|
|
413
687
|
`Agents: opencomputer/\n` +
|
|
414
688
|
(spa
|
|
415
689
|
? `Web app: src/ (separate lifecycle)\n\n`
|
|
@@ -442,13 +716,28 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
442
716
|
}
|
|
443
717
|
return;
|
|
444
718
|
}
|
|
719
|
+
if (command === "doctor") {
|
|
720
|
+
if (args.length)
|
|
721
|
+
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
722
|
+
const root = await findOpenComputerProjectRoot(process.cwd());
|
|
723
|
+
const result = await doctorProject(root);
|
|
724
|
+
if (!result.ok) {
|
|
725
|
+
if (!globals.json)
|
|
726
|
+
printDoctor(result, false);
|
|
727
|
+
throw new CLIError("doctor_failed", "Local project diagnostics failed.", "Fix the reported errors and rerun `opencomputer doctor --json`.", result);
|
|
728
|
+
}
|
|
729
|
+
printDoctor(result, globals.json);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
445
732
|
if (command === "link") {
|
|
733
|
+
const project = option(args, "--project");
|
|
734
|
+
const createProjectName = option(args, "--create-project");
|
|
446
735
|
if (args.length)
|
|
447
736
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
448
737
|
const root = await findOpenComputerProjectRoot(process.cwd());
|
|
449
738
|
const binding = await ensureProjectBinding(client, config, root, {
|
|
450
|
-
|
|
451
|
-
|
|
739
|
+
project,
|
|
740
|
+
createProjectName,
|
|
452
741
|
});
|
|
453
742
|
if (globals.json)
|
|
454
743
|
printJSON(binding);
|
|
@@ -468,6 +757,10 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
468
757
|
if (!root) {
|
|
469
758
|
throw new Error("No OpenComputer project found. Run `opencomputer init <directory>` first.");
|
|
470
759
|
}
|
|
760
|
+
const diagnosis = await doctorProject(root);
|
|
761
|
+
if (!diagnosis.ok) {
|
|
762
|
+
throw new CLIError("doctor_failed", "Deploy stopped because local project diagnostics failed.", "Run `opencomputer doctor --json`, fix the errors, and deploy again.", diagnosis);
|
|
763
|
+
}
|
|
471
764
|
if (watch) {
|
|
472
765
|
if (requestedAlias && requestedAlias !== "development") {
|
|
473
766
|
throw new Error("--watch deploys only to development; omit --alias or use --alias development");
|
|
@@ -475,7 +768,6 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
475
768
|
await runDeploymentWatch(client, config, root, {
|
|
476
769
|
project,
|
|
477
770
|
createProjectName,
|
|
478
|
-
interactive: !globals.json,
|
|
479
771
|
});
|
|
480
772
|
return;
|
|
481
773
|
}
|
|
@@ -483,10 +775,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
483
775
|
throw new Error("--project and --create-project require --watch");
|
|
484
776
|
}
|
|
485
777
|
const alias = deploymentAlias(requestedAlias);
|
|
486
|
-
const binding = await ensureProjectBinding(client, config, root
|
|
487
|
-
interactive: !globals.json,
|
|
488
|
-
select: true,
|
|
489
|
-
});
|
|
778
|
+
const binding = await ensureProjectBinding(client, config, root);
|
|
490
779
|
const results = await publishProjectDeployment(client, root, binding, alias);
|
|
491
780
|
for (const { built } of results) {
|
|
492
781
|
process.stderr.write(`Built ${built.agentId} in ${String(built.elapsedMs)}ms\n`);
|
|
@@ -509,7 +798,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
509
798
|
if (!agent || !prompt) {
|
|
510
799
|
throw new Error("Usage: opencomputer run <agent> <prompt>");
|
|
511
800
|
}
|
|
512
|
-
const result = await runAgent(client, agent, prompt, keep, globals.json, globals.verbose === true);
|
|
801
|
+
const result = await runAgent(client, agent, prompt, keep, globals.json, globals.verbose === true, globals.idempotencyKey);
|
|
513
802
|
if (globals.json)
|
|
514
803
|
printJSON(result);
|
|
515
804
|
return;
|
|
@@ -520,10 +809,14 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
520
809
|
if (args.length)
|
|
521
810
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
522
811
|
process.stderr.write("`opencomputer dev` is deprecated. Use `opencomputer deploy --watch`; start any web app separately.\n");
|
|
523
|
-
|
|
812
|
+
const root = await findOpenComputerProjectRoot(process.cwd());
|
|
813
|
+
const diagnosis = await doctorProject(root);
|
|
814
|
+
if (!diagnosis.ok) {
|
|
815
|
+
throw new CLIError("doctor_failed", "Deploy stopped because local project diagnostics failed.", "Run `opencomputer doctor --json`, fix the errors, and deploy again.", diagnosis);
|
|
816
|
+
}
|
|
817
|
+
await runCloudDevelopment(client, config, root, {
|
|
524
818
|
project,
|
|
525
819
|
createProjectName,
|
|
526
|
-
interactive: !globals.json,
|
|
527
820
|
});
|
|
528
821
|
return;
|
|
529
822
|
}
|
|
@@ -532,7 +825,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
532
825
|
const projectReference = option(args, "--project");
|
|
533
826
|
const agentOption = option(args, "--agent");
|
|
534
827
|
const environment = environmentOption(option(args, "--environment"));
|
|
535
|
-
const project = await selectedProject(client, config, projectReference
|
|
828
|
+
const project = await selectedProject(client, config, projectReference);
|
|
536
829
|
const agentId = agentOption
|
|
537
830
|
? agentOption === "current"
|
|
538
831
|
? project.agentId
|
|
@@ -565,6 +858,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
565
858
|
}
|
|
566
859
|
if (action === "set") {
|
|
567
860
|
const explicitOrigins = options(args, "--allow-origin");
|
|
861
|
+
const valueStdin = flag(args, "--value-stdin");
|
|
568
862
|
if (args.length)
|
|
569
863
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
570
864
|
let allowedOrigins = explicitOrigins;
|
|
@@ -584,7 +878,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
584
878
|
const secret = await client.putSecret({
|
|
585
879
|
projectId: project.projectId,
|
|
586
880
|
name,
|
|
587
|
-
value: await
|
|
881
|
+
value: await readStdinValue(valueStdin),
|
|
588
882
|
environment,
|
|
589
883
|
...(agentId ? { agentId } : {}),
|
|
590
884
|
allowedOrigins,
|
|
@@ -636,7 +930,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
636
930
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
637
931
|
const currentAgentRoot = projectReference ? null : await findAgentRoot();
|
|
638
932
|
const project = shouldBindModelAccessProject(projectReference, currentAgentRoot)
|
|
639
|
-
? await selectedProject(client, config, projectReference
|
|
933
|
+
? await selectedProject(client, config, projectReference)
|
|
640
934
|
: undefined;
|
|
641
935
|
const environments = project
|
|
642
936
|
? ["development", "production"]
|
|
@@ -709,7 +1003,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
709
1003
|
const projectReference = option(args, "--project");
|
|
710
1004
|
const agentOption = option(args, "--agent");
|
|
711
1005
|
const environment = environmentOption(option(args, "--environment"));
|
|
712
|
-
const project = await selectedProject(client, config, projectReference
|
|
1006
|
+
const project = await selectedProject(client, config, projectReference);
|
|
713
1007
|
const agentId = agentOption
|
|
714
1008
|
? agentOption === "current"
|
|
715
1009
|
? project.agentId
|
|
@@ -740,12 +1034,13 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
740
1034
|
throw new Error("Use `opencomputer env set|list|remove <name>`.");
|
|
741
1035
|
}
|
|
742
1036
|
if (action === "set") {
|
|
1037
|
+
const valueStdin = flag(args, "--value-stdin");
|
|
743
1038
|
if (args.length)
|
|
744
1039
|
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
745
1040
|
const variable = await client.putRuntimeVariable({
|
|
746
1041
|
projectId: project.projectId,
|
|
747
1042
|
name,
|
|
748
|
-
value: await
|
|
1043
|
+
value: await readStdinValue(valueStdin),
|
|
749
1044
|
environment,
|
|
750
1045
|
...(agentId ? { agentId } : {}),
|
|
751
1046
|
});
|
|
@@ -779,7 +1074,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
779
1074
|
const projectReference = option(args, "--project");
|
|
780
1075
|
const agentOption = option(args, "--agent");
|
|
781
1076
|
const environment = environmentOption(option(args, "--environment"));
|
|
782
|
-
const project = await selectedProject(client, config, projectReference
|
|
1077
|
+
const project = await selectedProject(client, config, projectReference);
|
|
783
1078
|
const agentId = await selectedSessionAgent(client, project, !agentOption || agentOption === "current" ? undefined : agentOption);
|
|
784
1079
|
if (action === "list") {
|
|
785
1080
|
if (args.length)
|
|
@@ -806,19 +1101,26 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
806
1101
|
if (!name || args.length) {
|
|
807
1102
|
throw new Error("Use `opencomputer webhooks create <name>`.");
|
|
808
1103
|
}
|
|
809
|
-
const
|
|
1104
|
+
const existing = (await client.webhooks({
|
|
810
1105
|
projectId: project.projectId,
|
|
811
|
-
name,
|
|
812
1106
|
environment,
|
|
813
1107
|
agentId,
|
|
814
|
-
});
|
|
1108
|
+
})).find((candidate) => candidate.name === name);
|
|
1109
|
+
const webhook = existing ??
|
|
1110
|
+
(await client.createWebhook({
|
|
1111
|
+
projectId: project.projectId,
|
|
1112
|
+
name,
|
|
1113
|
+
environment,
|
|
1114
|
+
agentId,
|
|
1115
|
+
}));
|
|
815
1116
|
if (globals.json)
|
|
816
|
-
printJSON(webhook);
|
|
1117
|
+
printJSON({ ...webhook, reused: Boolean(existing) });
|
|
817
1118
|
else {
|
|
818
|
-
process.stdout.write(
|
|
1119
|
+
process.stdout.write(`${existing ? "Reused" : "Created"} ${webhook.name} (${webhook.id}) for ${agentId}@${environment}.\n` +
|
|
819
1120
|
`URL: ${webhook.invocationUrl}\n` +
|
|
820
|
-
|
|
821
|
-
|
|
1121
|
+
(existing
|
|
1122
|
+
? "The existing token remains unchanged.\n"
|
|
1123
|
+
: `Token: ${webhook.token ?? "unavailable"}\nSave this token now. It will not be shown again.\n`));
|
|
822
1124
|
}
|
|
823
1125
|
return;
|
|
824
1126
|
}
|
|
@@ -886,7 +1188,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
886
1188
|
insideProject = false;
|
|
887
1189
|
}
|
|
888
1190
|
if (insideProject) {
|
|
889
|
-
agentId = (await selectedProject(client, config
|
|
1191
|
+
agentId = (await selectedProject(client, config)).agentId;
|
|
890
1192
|
}
|
|
891
1193
|
}
|
|
892
1194
|
let cursor = "";
|
|
@@ -916,7 +1218,50 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
916
1218
|
}
|
|
917
1219
|
return;
|
|
918
1220
|
}
|
|
919
|
-
if (command === "
|
|
1221
|
+
if (command === "channels") {
|
|
1222
|
+
const action = args.shift();
|
|
1223
|
+
if (action !== "status") {
|
|
1224
|
+
throw new Error("Use `opencomputer channels status`.");
|
|
1225
|
+
}
|
|
1226
|
+
const projectReference = option(args, "--project");
|
|
1227
|
+
const agentOption = option(args, "--agent");
|
|
1228
|
+
const environment = environmentOption(option(args, "--environment"));
|
|
1229
|
+
if (args.length)
|
|
1230
|
+
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
1231
|
+
const project = await selectedProject(client, config, projectReference);
|
|
1232
|
+
const agentId = await selectedSessionAgent(client, project, !agentOption || agentOption === "current" ? undefined : agentOption);
|
|
1233
|
+
const channels = (await client.channels()).filter((channel) => channel.agentId === agentId && channel.alias === environment);
|
|
1234
|
+
if (globals.json)
|
|
1235
|
+
printJSON({ projectId: project.projectId, environment, channels });
|
|
1236
|
+
else if (!channels.length)
|
|
1237
|
+
process.stdout.write("No matching channels.\n");
|
|
1238
|
+
else {
|
|
1239
|
+
for (const channel of channels) {
|
|
1240
|
+
process.stdout.write(`${channel.id} ${channel.status} ${channel.channel} ${channel.agentId}@${channel.alias}\n` +
|
|
1241
|
+
` last event: ${channel.lastEventAt ?? "—"}\n` +
|
|
1242
|
+
` last delivery: ${channel.lastDelivery ? `${channel.lastDelivery.status} at ${channel.lastDelivery.at}` : "—"}\n` +
|
|
1243
|
+
` last error: ${channel.lastError ? `${channel.lastError.category} at ${channel.lastError.at}` : "—"}\n`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (command === "session" || command === "sessions") {
|
|
1249
|
+
if (args[0] === "tail") {
|
|
1250
|
+
args.shift();
|
|
1251
|
+
const sessionId = args.shift();
|
|
1252
|
+
const afterValue = option(args, "--after");
|
|
1253
|
+
const follow = !flag(args, "--no-follow");
|
|
1254
|
+
if (!sessionId)
|
|
1255
|
+
throw new Error("A session ID is required.");
|
|
1256
|
+
if (args.length)
|
|
1257
|
+
throw new Error(`Unexpected argument: ${args[0]}`);
|
|
1258
|
+
const after = afterValue ? Number.parseInt(afterValue, 10) : 0;
|
|
1259
|
+
if (!Number.isFinite(after) || after < 0) {
|
|
1260
|
+
throw new Error("--after must be a non-negative event cursor");
|
|
1261
|
+
}
|
|
1262
|
+
await tailSession(client, sessionId, after, follow, globals.json);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
920
1265
|
const session = parseSessionCommand(args);
|
|
921
1266
|
const sessionArgs = session.args;
|
|
922
1267
|
if (session.action === "list") {
|
|
@@ -933,11 +1278,11 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
933
1278
|
}
|
|
934
1279
|
if (session.action === "create") {
|
|
935
1280
|
const prompt = sessionArgs.join(" ").trim();
|
|
936
|
-
const project = await selectedProject(client, config, undefined
|
|
1281
|
+
const project = await selectedProject(client, config, undefined);
|
|
937
1282
|
const agentId = await selectedSessionAgent(client, project, session.agent);
|
|
938
1283
|
const agent = developmentAgentReference(agentId);
|
|
939
1284
|
if (prompt) {
|
|
940
|
-
const result = await runAgent(client, agent, prompt, session.keep, globals.json, globals.verbose === true);
|
|
1285
|
+
const result = await runAgent(client, agent, prompt, session.keep, globals.json, globals.verbose === true, globals.idempotencyKey);
|
|
941
1286
|
if (globals.json)
|
|
942
1287
|
printJSON(result);
|
|
943
1288
|
return;
|
|
@@ -983,7 +1328,7 @@ export async function runCommand(command, rawArgs, globals) {
|
|
|
983
1328
|
const prompt = sessionArgs.join(" ").trim();
|
|
984
1329
|
if (!prompt)
|
|
985
1330
|
throw new Error("A prompt is required.");
|
|
986
|
-
const result = await sendAgentTurn(client, sessionId, prompt, session.keep, globals.json);
|
|
1331
|
+
const result = await sendAgentTurn(client, sessionId, prompt, session.keep, globals.json, globals.idempotencyKey);
|
|
987
1332
|
if (globals.json) {
|
|
988
1333
|
printJSON({ sessionId, ...result, status: "completed" });
|
|
989
1334
|
}
|