@agents24/cli 0.1.1 → 0.2.0

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/dist/cli.js CHANGED
@@ -1,30 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertImportAllowed,
4
- assertInstallPreviewReady,
5
4
  createRemoteClient,
6
5
  initializePackage,
6
+ loadPackageFiles,
7
7
  packPackage,
8
8
  parseMappings,
9
9
  shouldPromptForDependency,
10
10
  validatePackage
11
- } from "./chunk-MNGBA3HP.js";
11
+ } from "./chunk-NLUCQTMN.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { basename, join, resolve } from "path";
15
- import { chmod, readFile, writeFile } from "fs/promises";
15
+ import { chmod, mkdir, readFile, stat, writeFile } from "fs/promises";
16
16
  import { createInterface } from "readline/promises";
17
- import { createHash } from "crypto";
17
+ import { createHash, createHmac, randomBytes } from "crypto";
18
+ import { spawn } from "child_process";
19
+ import { hostname } from "os";
20
+ import { parse as parseYaml } from "yaml";
18
21
  import { isCancel, password, select, text } from "@clack/prompts";
19
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "remote", "yes", "allow-incomplete", "no-write-env"]);
22
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "remote", "yes", "allow-incomplete", "no-write-env", "prune"]);
23
+ var LIFECYCLE_COMMANDS = /* @__PURE__ */ new Set(["prepare", "plan", "apply", "dev", "publish", "setup", "status", "link", "resources"]);
20
24
  var DEFAULT_API_BASE_URL = "https://api.agents24.dev";
21
25
  var DEFAULT_LOCAL_CLIENT_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
22
26
  function parse(argv) {
23
- const standaloneInstall = argv[0] === "install";
24
- if (!standaloneInstall && (argv[0] !== "package" || !argv[1])) throw new Error("Usage: agents24 install <directory-or-zip> | agents24 package <init|validate|pack|export|compile|preview|import>");
27
+ const lifecycle = LIFECYCLE_COMMANDS.has(argv[0]);
28
+ if (!lifecycle && (argv[0] !== "package" || !argv[1])) throw new Error("Usage: agents24 <prepare|plan|apply|dev|publish|setup|status|link|resources> | agents24 package <init|validate|pack|export|compile|preview|import>");
25
29
  const flags = /* @__PURE__ */ new Map();
26
30
  const positionals = [];
27
- for (let index = standaloneInstall ? 1 : 2; index < argv.length; index += 1) {
31
+ for (let index = lifecycle ? 1 : 2; index < argv.length; index += 1) {
28
32
  const current = argv[index];
29
33
  if (!current.startsWith("--")) {
30
34
  positionals.push(current);
@@ -39,7 +43,7 @@ function parse(argv) {
39
43
  if (value === void 0 || value.startsWith("--")) throw new Error(`--${rawName} requires a value`);
40
44
  flags.set(rawName, [...flags.get(rawName) || [], value]);
41
45
  }
42
- return { command: standaloneInstall ? "install" : argv[1], positionals, flags };
46
+ return { command: lifecycle ? argv[0] : argv[1], positionals, flags };
43
47
  }
44
48
  function flag(parsed, name) {
45
49
  return parsed.flags.get(name)?.at(-1);
@@ -53,6 +57,128 @@ function mappings(parsed) {
53
57
  function upload(data, input) {
54
58
  return { data, filename: basename(input).endsWith(".zip") ? basename(input) : "resource.agents24.zip" };
55
59
  }
60
+ async function packageDirectory(input) {
61
+ try {
62
+ return (await stat(input)).isDirectory() ? resolve(input) : void 0;
63
+ } catch {
64
+ return void 0;
65
+ }
66
+ }
67
+ async function installationId(input) {
68
+ const explicit = String(process.env.AGENTS24_INSTALLATION_ID || "").trim();
69
+ if (explicit) return explicit;
70
+ const directory = await packageDirectory(input);
71
+ if (!directory) return void 0;
72
+ try {
73
+ return (await readFile(join(directory, ".agents24/installation-id"), "utf8")).trim() || void 0;
74
+ } catch (error) {
75
+ if (error.code === "ENOENT") return void 0;
76
+ throw error;
77
+ }
78
+ }
79
+ async function writeInstallationId(input, id) {
80
+ if (String(process.env.AGENTS24_INSTALLATION_ID || "").trim()) return void 0;
81
+ const directory = await packageDirectory(input);
82
+ if (!directory) return void 0;
83
+ const stateDirectory = join(directory, ".agents24");
84
+ await mkdir(stateDirectory, { recursive: true, mode: 448 });
85
+ const target = join(stateDirectory, "installation-id");
86
+ await writeFile(target, `${id}
87
+ `, { mode: 384 });
88
+ await chmod(target, 384);
89
+ await ensureIgnored(directory, [".agents24/", ".env.local"]);
90
+ return target;
91
+ }
92
+ async function ensureIgnored(directory, entries) {
93
+ const target = join(directory, ".gitignore");
94
+ let current = "";
95
+ try {
96
+ current = await readFile(target, "utf8");
97
+ } catch (error) {
98
+ if (error.code !== "ENOENT") throw error;
99
+ }
100
+ const lines = new Set(current.split(/\r?\n/).map((item) => item.trim()).filter(Boolean));
101
+ const missing = entries.filter((item) => !lines.has(item));
102
+ if (!missing.length) return;
103
+ const separator = current && !current.endsWith("\n") ? "\n" : "";
104
+ await writeFile(target, `${current}${separator}${missing.join("\n")}
105
+ `);
106
+ }
107
+ function parseEnv(textValue) {
108
+ const result = {};
109
+ for (const line of textValue.split(/\r?\n/)) {
110
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line.trim());
111
+ if (!match) continue;
112
+ const raw = match[2].trim();
113
+ try {
114
+ result[match[1]] = raw.startsWith('"') ? JSON.parse(raw) : raw;
115
+ } catch {
116
+ result[match[1]] = raw;
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+ async function packageSecretRequirements(input) {
122
+ const files = await loadPackageFiles(input);
123
+ const manifest = parseYaml(files.get("agents24.yaml") || "");
124
+ const requires = manifest.requires && typeof manifest.requires === "object" ? manifest.requires : {};
125
+ const secrets = requires.secrets && typeof requires.secrets === "object" ? requires.secrets : {};
126
+ return Object.entries(secrets).map(([key, raw]) => {
127
+ const item = raw && typeof raw === "object" ? raw : {};
128
+ const env = String(item.env || "").trim();
129
+ if (!env) throw new Error(`Secret requirement ${key} must declare env`);
130
+ return { key: `$secrets.${key}`, env, generate: item.generate === true };
131
+ });
132
+ }
133
+ async function preparePackage(input) {
134
+ const directory = await packageDirectory(input);
135
+ if (!directory) throw new Error("prepare requires a Resource Package directory");
136
+ const local = await validatePackage(input);
137
+ if (!local.valid) throw Object.assign(new Error("Resource package is invalid"), { diagnostics: local.diagnostics });
138
+ const target = join(directory, ".env.local");
139
+ let current = "";
140
+ try {
141
+ current = await readFile(target, "utf8");
142
+ } catch (error) {
143
+ if (error.code !== "ENOENT") throw error;
144
+ }
145
+ const values2 = parseEnv(current);
146
+ const generated = [];
147
+ const preserved = [];
148
+ let next = current;
149
+ for (const requirement of await packageSecretRequirements(input)) {
150
+ if (values2[requirement.env] || process.env[requirement.env]) {
151
+ preserved.push(requirement.env);
152
+ continue;
153
+ }
154
+ if (!requirement.generate) continue;
155
+ const value = randomBytes(32).toString("base64url");
156
+ next += `${next && !next.endsWith("\n") ? "\n" : ""}${requirement.env}=${JSON.stringify(value)}
157
+ `;
158
+ generated.push(requirement.env);
159
+ }
160
+ await writeFile(target, next, { mode: 384 });
161
+ await chmod(target, 384);
162
+ await ensureIgnored(directory, [".env.local", ".agents24/"]);
163
+ return { ok: true, package: directory, env_file: target, generated, preserved };
164
+ }
165
+ async function secretValues(input) {
166
+ const directory = await packageDirectory(input);
167
+ let local = {};
168
+ if (directory) {
169
+ try {
170
+ local = parseEnv(await readFile(join(directory, ".env.local"), "utf8"));
171
+ } catch (error) {
172
+ if (error.code !== "ENOENT") throw error;
173
+ }
174
+ }
175
+ const result = {};
176
+ for (const requirement of await packageSecretRequirements(input)) {
177
+ const value = String(process.env[requirement.env] || local[requirement.env] || "");
178
+ if (value) result[requirement.key] = value;
179
+ }
180
+ return result;
181
+ }
56
182
  function safeResult(result) {
57
183
  return result;
58
184
  }
@@ -70,6 +196,26 @@ async function confirmation(parsed, preview) {
70
196
  prompt.close();
71
197
  if (!/^y(?:es)?$/i.test(answer.trim())) throw new Error("Import cancelled");
72
198
  }
199
+ async function applyConfirmation(parsed, plan) {
200
+ if (flag(parsed, "yes") === "true") return;
201
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("apply requires --yes in noninteractive mode");
202
+ const actions = Array.isArray(plan.actions) ? plan.actions.length : 0;
203
+ const removals = Array.isArray(plan.content) ? plan.content.reduce((count, item) => count + (Array.isArray(item.remove) ? item.remove.length : 0), 0) : 0;
204
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
205
+ const answer = await prompt.question(`Apply ${actions} resource action(s)${removals ? ` and ${removals} content removal(s)` : ""}? [y/N] `);
206
+ prompt.close();
207
+ if (!/^y(?:es)?$/i.test(answer.trim())) throw new Error("Apply cancelled");
208
+ }
209
+ async function pollOperation(client, operation) {
210
+ let current = operation;
211
+ for (let attempt = 0; attempt < 900; attempt += 1) {
212
+ const status = String(current.status || "");
213
+ if (["completed", "failed"].includes(status)) return current;
214
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 1e3));
215
+ current = await client.resourceInstallations.operationStatus(String(current.operation_id));
216
+ }
217
+ throw new Error("Resource apply did not finish within 15 minutes");
218
+ }
73
219
  async function promptMappings(parsed, client, preview, current) {
74
220
  if (!process.stdin.isTTY || !process.stdout.isTTY || flag(parsed, "yes") === "true") return current;
75
221
  const dependencies = Array.isArray(preview.dependencies) ? preview.dependencies.filter((item) => Boolean(item && typeof item === "object" && item.status === "unresolved")) : [];
@@ -119,11 +265,8 @@ function canonical(value) {
119
265
  if (value && typeof value === "object") return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
120
266
  return JSON.stringify(value);
121
267
  }
122
- function installKey(bundle, map) {
123
- return `install-${createHash("sha256").update(canonical({ bundle, mappings: map })).digest("hex").slice(0, 40)}`;
124
- }
125
- function importedRows(imported) {
126
- return Array.isArray(imported.resources) ? imported.resources.filter((item) => Boolean(item && typeof item === "object")) : [];
268
+ function importKey(bundle, map) {
269
+ return `import-${createHash("sha256").update(canonical({ bundle, mappings: map })).digest("hex").slice(0, 40)}`;
127
270
  }
128
271
  function interactive(parsed) {
129
272
  return flag(parsed, "yes") !== "true" && Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -131,14 +274,14 @@ function interactive(parsed) {
131
274
  function cancelled(value) {
132
275
  if (isCancel(value)) throw new Error("Installation cancelled");
133
276
  }
134
- async function apiKeyForInstall(parsed) {
277
+ async function apiKeyForLifecycle(parsed) {
135
278
  let apiKey = String(process.env.AGENTS24_API_KEY || "").trim();
136
279
  if (!apiKey && interactive(parsed)) {
137
280
  const answer = await password({ message: "Agents24 API key", validate: (value) => String(value || "").trim() ? void 0 : "The API key is required." });
138
281
  cancelled(answer);
139
282
  apiKey = String(answer).trim();
140
283
  }
141
- if (!apiKey) throw new Error("AGENTS24_API_KEY is required for noninteractive installation");
284
+ if (!apiKey) throw new Error("AGENTS24_API_KEY is required for noninteractive resource management");
142
285
  if (/[\r\n\0]/.test(apiKey)) throw new Error("AGENTS24_API_KEY contains invalid control characters");
143
286
  return apiKey;
144
287
  }
@@ -191,21 +334,6 @@ async function resolveInstallMode(parsed, app) {
191
334
  cancelled(answer);
192
335
  return answer;
193
336
  }
194
- async function selectInstalledAgent(parsed, imported) {
195
- const agents = importedRows(imported).filter((row) => row.kind === "agent");
196
- if (!agents.length) throw new Error("The installed package does not contain an Agent");
197
- const requested = flag(parsed, "agent");
198
- if (requested) {
199
- const match = agents.find((row) => [row.id, row.resource_key, row.name].map(String).includes(requested));
200
- if (!match) throw new Error(`--agent did not match an imported Agent: ${requested}`);
201
- return match;
202
- }
203
- if (agents.length === 1) return agents[0];
204
- if (!interactive(parsed)) throw new Error("--agent is required when the package contains multiple Agents");
205
- const answer = await select({ message: "Which Agent should be published?", options: agents.map((row) => ({ value: String(row.id), label: String(row.name) })) });
206
- cancelled(answer);
207
- return agents.find((row) => String(row.id) === answer);
208
- }
209
337
  async function clientDeployment(parsed, client, agent, app) {
210
338
  const policies = (await client.resourcePolicies.list()).filter((row) => row.is_active === true);
211
339
  let policyId = flag(parsed, "policy-set");
@@ -258,7 +386,362 @@ async function updateEnvFile(app, values2) {
258
386
  await chmod(target, 384);
259
387
  return target;
260
388
  }
389
+ async function ensureInstallation(input, client, data) {
390
+ const current = await installationId(input);
391
+ if (current) return current;
392
+ const initial = await client.resourceInstallations.plan(upload(data, input));
393
+ const created = await client.resourceInstallations.create(
394
+ { package_name: String(initial.package_name), operation_id: String(initial.operation_id) },
395
+ { idempotencyKey: `installation-${String(initial.package_hash).slice(0, 40)}` }
396
+ );
397
+ const id = String(created.id);
398
+ await writeInstallationId(input, id);
399
+ return id;
400
+ }
401
+ async function applyDraft(parsed, options = {}) {
402
+ const input = parsed.positionals[0];
403
+ if (!input) throw new Error(`${parsed.command} requires a Resource Package directory or ZIP`);
404
+ const apiKey = await apiKeyForLifecycle(parsed);
405
+ const client = await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey });
406
+ const data = await packPackage(input);
407
+ const currentId = await installationId(input);
408
+ const plan = await client.resourceInstallations.plan({
409
+ ...upload(data, input),
410
+ ...currentId ? { installationId: currentId } : {},
411
+ prune: flag(parsed, "prune") === "true",
412
+ development: options.development === true
413
+ });
414
+ if (plan.can_apply !== true) throw Object.assign(new Error("The Resource Package plan is blocked"), { plan });
415
+ if (!options.skipConfirmation) await applyConfirmation(parsed, plan);
416
+ const secrets = await secretValues(input);
417
+ let operation = await client.resourceInstallations.apply(String(plan.operation_id), {
418
+ primary_resource_key: flag(parsed, "agent"),
419
+ integration_mode: options.integrationMode || flag(parsed, "integration") || "publish-only",
420
+ secrets,
421
+ ...options.developmentSessions ? { development_sessions: options.developmentSessions } : {}
422
+ }, { idempotencyKey: `apply-${String(plan.package_hash).slice(0, 40)}` });
423
+ operation = await pollOperation(client, operation);
424
+ if (operation.status !== "completed") throw Object.assign(new Error("Draft apply failed"), { operation });
425
+ const installationIdValue = String(operation.installation_id || "");
426
+ if (!installationIdValue) throw new Error("Apply completed without an installation ID");
427
+ await writeInstallationId(input, installationIdValue);
428
+ const result = operation.result && typeof operation.result === "object" ? operation.result : {};
429
+ const agentId = String(result.primary_agent_id || "");
430
+ const resources = Array.isArray(result.resources) ? result.resources : [];
431
+ const agent = resources.find((item) => String(item.id) === agentId) || { id: agentId, name: "Agent" };
432
+ return { client, apiKey, input, installationId: installationIdValue, plan, operation, result, agent };
433
+ }
434
+ async function publishDraft(parsed, applied) {
435
+ const input = applied?.input || parsed.positionals[0];
436
+ if (!input) throw new Error("publish requires a Resource Package directory or ZIP");
437
+ const apiKey = applied?.apiKey || await apiKeyForLifecycle(parsed);
438
+ const client = applied?.client || await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey });
439
+ const id = applied?.installationId || await installationId(input);
440
+ if (!id) throw new Error("No installation is linked; run agents24 apply first");
441
+ const data = await packPackage(input);
442
+ const plan = applied?.plan || await client.resourceInstallations.plan({ ...upload(data, input), installationId: id });
443
+ const publication = await client.resourceInstallations.publish(id, { package_hash: String(plan.package_hash) }, {
444
+ idempotencyKey: `publish-${String(plan.package_hash).slice(0, 40)}`
445
+ });
446
+ return { publication, client, apiKey, input, installation_id: id, plan };
447
+ }
448
+ async function developmentArtifacts(input) {
449
+ const directory = await packageDirectory(input);
450
+ if (!directory) throw new Error("dev requires a Resource Package directory");
451
+ const files = await loadPackageFiles(input);
452
+ const manifest = parseYaml(files.get("agents24.yaml") || "");
453
+ const resources = Array.isArray(manifest.resources) ? manifest.resources : [];
454
+ const requires = manifest.requires && typeof manifest.requires === "object" ? manifest.requires : {};
455
+ const requiredSecrets = requires.secrets && typeof requires.secrets === "object" ? requires.secrets : {};
456
+ const result = [];
457
+ for (const resource of resources) {
458
+ const path = String(resource.path || "");
459
+ const sourceText = files.get(path);
460
+ if (!sourceText) continue;
461
+ const source = parseYaml(sourceText);
462
+ if (source.execution_target !== "self_hosted") continue;
463
+ const development = source.development && typeof source.development === "object" ? source.development : void 0;
464
+ if (!development) continue;
465
+ const server = source.server && typeof source.server === "object" ? source.server : {};
466
+ const auth = server.auth && typeof server.auth === "object" ? server.auth : {};
467
+ const secretRequirement = String(auth.secret || "");
468
+ const secretKey = secretRequirement.startsWith("$secrets.") ? secretRequirement.slice("$secrets.".length) : "";
469
+ const secretDeclaration = secretKey && requiredSecrets[secretKey] && typeof requiredSecrets[secretKey] === "object" ? requiredSecrets[secretKey] : void 0;
470
+ const cwd = resolve(directory, String(development.cwd || "."));
471
+ if (cwd !== directory && !cwd.startsWith(`${directory}/`)) throw new Error(`Artifact ${resource.key} development.cwd must remain inside the package`);
472
+ result.push({
473
+ key: String(resource.key || ""),
474
+ baseUrl: String(development.base_url || ""),
475
+ command: development.command ? String(development.command) : void 0,
476
+ args: Array.isArray(development.args) ? development.args.map(String) : [],
477
+ cwd,
478
+ protocol: {
479
+ manifest_path: server.manifest_path,
480
+ health_path: server.health_path,
481
+ verify_path: server.verify_path,
482
+ invoke_path: server.invoke_path,
483
+ auth_mode: auth.mode,
484
+ signing_secret_requirement: auth.secret
485
+ },
486
+ signingSecretEnv: secretDeclaration?.env ? String(secretDeclaration.env) : void 0
487
+ });
488
+ }
489
+ return result;
490
+ }
491
+ async function waitForServer(baseUrl, healthPath) {
492
+ const target = new URL(String(healthPath || "/.well-known/agents24/artifact/health"), baseUrl);
493
+ for (let attempt = 0; attempt < 60; attempt += 1) {
494
+ try {
495
+ const response = await fetch(target);
496
+ if (response.ok) return;
497
+ } catch {
498
+ }
499
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
500
+ }
501
+ throw new Error(`Local Artifact server did not become healthy at ${target}`);
502
+ }
503
+ async function openDevelopmentRelay(client, installationIdValue, artifact, localEnv) {
504
+ const admission = await client.resourceInstallations.prepareDevelopmentSession(installationIdValue, {
505
+ resource_key: artifact.key,
506
+ machine_label: hostname()
507
+ });
508
+ const apiUrl = new URL(String(process.env.AGENTS24_BASE_URL || DEFAULT_API_BASE_URL));
509
+ apiUrl.protocol = apiUrl.protocol === "https:" ? "wss:" : "ws:";
510
+ apiUrl.pathname = String(admission.relay_path);
511
+ const socket = new WebSocket(apiUrl, [
512
+ "agents24-artifact-relay",
513
+ `agents24-credential-${String(admission.relay_token)}`
514
+ ]);
515
+ const state = {};
516
+ await new Promise((resolveReady, rejectReady) => {
517
+ socket.addEventListener("error", () => rejectReady(new Error(`Artifact relay failed for ${artifact.key}`)), { once: true });
518
+ socket.addEventListener("message", async (event) => {
519
+ const message = JSON.parse(String(event.data));
520
+ if (message.type === "ready") {
521
+ resolveReady();
522
+ return;
523
+ }
524
+ if (message.type === "superseded" || message.type === "revoked") {
525
+ state.terminalReason = String(message.type);
526
+ socket.close();
527
+ return;
528
+ }
529
+ if (message.type !== "request") return;
530
+ const method = message.operation === "health" || message.operation === "manifest" ? "GET" : "POST";
531
+ try {
532
+ const requestBody = method === "POST" ? JSON.stringify(message.body || {}) : void 0;
533
+ const headers = method === "POST" ? { "content-type": "application/json" } : {};
534
+ if (method === "POST" && String(artifact.protocol.auth_mode || "hmac_sha256") === "hmac_sha256") {
535
+ const signingSecret = String(
536
+ artifact.signingSecretEnv && (localEnv[artifact.signingSecretEnv] || process.env[artifact.signingSecretEnv]) || ""
537
+ );
538
+ if (!signingSecret) throw new Error(`Artifact ${artifact.key} signing secret is unavailable`);
539
+ const timestamp = String(Date.now());
540
+ const signature = createHmac("sha256", signingSecret).update(`${timestamp}.${requestBody}`).digest("hex");
541
+ headers["x-agents24-timestamp"] = timestamp;
542
+ headers["x-agents24-signature"] = `sha256=${signature}`;
543
+ }
544
+ const response = await fetch(new URL(String(message.path || "/"), artifact.baseUrl), {
545
+ method,
546
+ headers,
547
+ body: requestBody
548
+ });
549
+ const textBody = await response.text();
550
+ let body = textBody;
551
+ try {
552
+ body = textBody ? JSON.parse(textBody) : null;
553
+ } catch {
554
+ }
555
+ socket.send(JSON.stringify({ type: "response", request_id: message.request_id, status: response.status, body }));
556
+ } catch {
557
+ socket.send(JSON.stringify({ type: "response", request_id: message.request_id, status: 502, body: { status: "failed" } }));
558
+ }
559
+ });
560
+ const heartbeat = setInterval(() => {
561
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "heartbeat" }));
562
+ }, 15e3);
563
+ socket.addEventListener("close", () => clearInterval(heartbeat), { once: true });
564
+ });
565
+ return { sessionId: String(admission.id), socket, state };
566
+ }
261
567
  async function execute(parsed) {
568
+ if (parsed.command === "prepare") {
569
+ const input = parsed.positionals[0];
570
+ if (!input) throw new Error("prepare requires a Resource Package directory");
571
+ return preparePackage(input);
572
+ }
573
+ if (parsed.command === "plan") {
574
+ const input = parsed.positionals[0];
575
+ if (!input) throw new Error("plan requires a Resource Package directory or ZIP");
576
+ const apiKey = await apiKeyForLifecycle(parsed);
577
+ const client = await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey });
578
+ const data = await packPackage(input);
579
+ const currentId = await installationId(input);
580
+ const plan = await client.resourceInstallations.plan({
581
+ ...upload(data, input),
582
+ ...currentId ? { installationId: currentId } : {},
583
+ prune: flag(parsed, "prune") === "true"
584
+ });
585
+ return { ok: plan.can_apply === true, plan };
586
+ }
587
+ if (parsed.command === "status") {
588
+ const input = parsed.positionals[0];
589
+ if (!input) throw new Error("status requires a Resource Package directory or ZIP");
590
+ const id = await installationId(input);
591
+ if (!id) throw new Error("No installation is linked; run agents24 apply/link first or set AGENTS24_INSTALLATION_ID");
592
+ const apiKey = await apiKeyForLifecycle(parsed);
593
+ const status = await (await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey })).resourceInstallations.get(id);
594
+ return { ok: true, installation: status };
595
+ }
596
+ if (parsed.command === "resources") {
597
+ if (parsed.positionals[0] !== "list") throw new Error("Usage: agents24 resources list --kind <kind>");
598
+ const kind = flag(parsed, "kind");
599
+ if (!kind) throw new Error("resources list requires --kind");
600
+ const apiKey = await apiKeyForLifecycle(parsed);
601
+ const result = await (await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey })).resourceInstallations.resources({
602
+ kind,
603
+ query: flag(parsed, "query"),
604
+ limit: Number(flag(parsed, "limit") || 100)
605
+ });
606
+ return { ok: true, ...result };
607
+ }
608
+ if (parsed.command === "link") {
609
+ const input = parsed.positionals[0];
610
+ if (!input) throw new Error("link requires a Resource Package directory or ZIP");
611
+ const assignments = values(parsed, "resource");
612
+ if (!assignments.length) throw new Error("link requires --resource <stable-key>=<uuid>");
613
+ const apiKey = await apiKeyForLifecycle(parsed);
614
+ const client = await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey });
615
+ let id = await installationId(input);
616
+ if (!id) {
617
+ const data = await packPackage(input);
618
+ const initialPlan = await client.resourceInstallations.plan(upload(data, input));
619
+ const created = await client.resourceInstallations.create(
620
+ { package_name: String(initialPlan.package_name), operation_id: String(initialPlan.operation_id) },
621
+ { idempotencyKey: `installation-${String(initialPlan.package_hash).slice(0, 40)}` }
622
+ );
623
+ id = String(created.id);
624
+ await writeInstallationId(input, id);
625
+ await client.resourceInstallations.plan({ ...upload(data, input), installationId: id });
626
+ }
627
+ const linked = [];
628
+ for (const assignment of assignments) {
629
+ const separator = assignment.indexOf("=");
630
+ if (separator <= 0 || separator === assignment.length - 1) throw new Error("--resource must be <stable-key>=<uuid>");
631
+ const resourceKey = assignment.slice(0, separator);
632
+ const resourceId = assignment.slice(separator + 1);
633
+ linked.push(await client.resourceInstallations.link(id, { resource_key: resourceKey, resource_id: resourceId }, {
634
+ idempotencyKey: `link-${createHash("sha256").update(`${id}:${assignment}`).digest("hex").slice(0, 40)}`
635
+ }));
636
+ }
637
+ return { ok: true, installation_id: id, linked };
638
+ }
639
+ if (parsed.command === "apply") {
640
+ const applied = await applyDraft(parsed);
641
+ return { ok: true, phase: "draft_applied", installation_id: applied.installationId, agent_id: applied.agent.id, plan: applied.plan, operation: applied.operation };
642
+ }
643
+ if (parsed.command === "publish") {
644
+ const published = await publishDraft(parsed);
645
+ return { ok: true, phase: "published", ...published };
646
+ }
647
+ if (parsed.command === "setup") {
648
+ const app = await resolveApp(parsed);
649
+ const integration = await resolveInstallMode(parsed, app);
650
+ const applied = await applyDraft(parsed, { integrationMode: integration });
651
+ const published = await publishDraft(parsed, applied);
652
+ const installed = await applied.client.resourceInstallations.get(applied.installationId);
653
+ let deployment;
654
+ let deploymentId;
655
+ if (integration === "client-deployment") {
656
+ deployment = installed.client_deployment_id ? await applied.client.clientDeployments.get(String(installed.client_deployment_id)) : await clientDeployment(parsed, applied.client, applied.agent, app);
657
+ if (!installed.client_deployment_id) {
658
+ await applied.client.resourceInstallations.bindDeployment(applied.installationId, { client_deployment_id: String(deployment.id) }, { idempotencyKey: `deployment-bind-${applied.installationId}` });
659
+ }
660
+ deploymentId = String(deployment.client_id || "");
661
+ }
662
+ let envFile;
663
+ if (app && flag(parsed, "no-write-env") !== "true" && integration !== "publish-only") {
664
+ const localClientBaseUrl = integration === "client-deployment" ? clientAppBaseUrl() : void 0;
665
+ envFile = await updateEnvFile(app, integration === "client-deployment" ? { VITE_AGENTS24_DEPLOYMENT_ID: String(deploymentId), ...localClientBaseUrl ? { VITE_AGENTS24_BASE_URL: localClientBaseUrl } : {} } : { AGENTS24_API_KEY: applied.apiKey, AGENTS24_AGENT_ID: String(applied.agent.id) });
666
+ }
667
+ return { ok: true, phase: deployment ? "deployed" : "published", integration, installation_id: applied.installationId, agent_id: applied.agent.id, publication: published.publication, ...deployment ? { deployment, deployment_id: deploymentId } : {}, ...envFile ? { env_file: envFile } : {} };
668
+ }
669
+ if (parsed.command === "dev") {
670
+ const input = parsed.positionals[0];
671
+ if (!input) throw new Error("dev requires a Resource Package directory");
672
+ await preparePackage(input);
673
+ const apiKey = await apiKeyForLifecycle(parsed);
674
+ const client = await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey });
675
+ const data = await packPackage(input);
676
+ const id = await ensureInstallation(input, client, data);
677
+ const configuredSecrets = await secretValues(input);
678
+ if (Object.keys(configuredSecrets).length) await client.resourceInstallations.configureSecrets(id, { secrets: configuredSecrets });
679
+ const declared = await developmentArtifacts(input);
680
+ const selectedKey = flag(parsed, "artifact");
681
+ const artifacts = selectedKey ? declared.filter((item) => item.key === selectedKey) : declared;
682
+ if (!artifacts.length) throw new Error(selectedKey ? `Artifact ${selectedKey} has no development configuration` : "No self-hosted Artifact development configuration was found");
683
+ const directory = await packageDirectory(input);
684
+ const localEnv = directory ? parseEnv(await readFile(join(directory, ".env.local"), "utf8")) : {};
685
+ const children = [];
686
+ const relays = [];
687
+ try {
688
+ for (const artifact of artifacts) {
689
+ if (artifact.command) children.push(spawn(artifact.command, artifact.args, { cwd: artifact.cwd, env: { ...process.env, ...localEnv }, stdio: "inherit", shell: false }));
690
+ await waitForServer(artifact.baseUrl, artifact.protocol.health_path);
691
+ relays.push(await openDevelopmentRelay(client, id, artifact, localEnv));
692
+ }
693
+ const developmentSessions = Object.fromEntries(artifacts.map((item, index) => [item.key, relays[index].sessionId]));
694
+ for (const artifact of declared) {
695
+ if (developmentSessions[artifact.key]) continue;
696
+ const status = await client.resourceInstallations.developmentSessionStatus(id, artifact.key);
697
+ if (status.status !== "connected" || !status.id) throw new Error(`Artifact ${artifact.key} requires an active development connection`);
698
+ developmentSessions[artifact.key] = String(status.id);
699
+ }
700
+ const applied = await applyDraft(parsed, { development: true, developmentSessions, skipConfirmation: true });
701
+ process.stdout.write(`${JSON.stringify({ ok: true, phase: "development_connected", installation_id: applied.installationId, development_sessions: developmentSessions }, null, 2)}
702
+ `);
703
+ await new Promise((resolveStop) => {
704
+ let stopping = false;
705
+ const reconnecting = /* @__PURE__ */ new Set();
706
+ const stop = () => {
707
+ stopping = true;
708
+ resolveStop();
709
+ };
710
+ const watch = (relay, index) => {
711
+ relay.socket.addEventListener("close", async () => {
712
+ if (stopping) return;
713
+ if (relay.state.terminalReason || reconnecting.has(index)) {
714
+ stop();
715
+ return;
716
+ }
717
+ reconnecting.add(index);
718
+ for (let attempt = 1; attempt <= 3 && !stopping; attempt += 1) {
719
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, attempt * 500));
720
+ try {
721
+ const replacement = await openDevelopmentRelay(client, id, artifacts[index], localEnv);
722
+ relays[index] = replacement;
723
+ developmentSessions[artifacts[index].key] = replacement.sessionId;
724
+ await applyDraft(parsed, { development: true, developmentSessions, skipConfirmation: true });
725
+ reconnecting.delete(index);
726
+ watch(replacement, index);
727
+ return;
728
+ } catch {
729
+ }
730
+ }
731
+ reconnecting.delete(index);
732
+ stop();
733
+ }, { once: true });
734
+ };
735
+ process.once("SIGINT", stop);
736
+ process.once("SIGTERM", stop);
737
+ relays.forEach(watch);
738
+ });
739
+ return { ok: true, phase: "development_stopped", installation_id: id };
740
+ } finally {
741
+ for (const relay of relays) relay.socket.close();
742
+ for (const child of children) child.kill("SIGTERM");
743
+ }
744
+ }
262
745
  if (parsed.command === "init") {
263
746
  const directory = resolve(parsed.positionals[0] || ".");
264
747
  const name = flag(parsed, "name") || basename(directory);
@@ -308,11 +791,8 @@ async function execute(parsed) {
308
791
  `);
309
792
  return { ok: result.valid === true, ...output ? { output: resolve(output) } : {}, result: safeResult(result) };
310
793
  }
311
- if (parsed.command === "preview" || parsed.command === "import" || parsed.command === "install") {
312
- const app = parsed.command === "install" ? await resolveApp(parsed) : void 0;
313
- const integration = parsed.command === "install" ? await resolveInstallMode(parsed, app) : void 0;
314
- const apiKey = parsed.command === "install" ? await apiKeyForInstall(parsed) : void 0;
315
- const client = apiKey ? await createRemoteClient({ ...process.env, AGENTS24_API_KEY: apiKey }) : await createRemoteClient();
794
+ if (parsed.command === "preview" || parsed.command === "import") {
795
+ const client = await createRemoteClient();
316
796
  const { result } = await compiledPackage(parsed, client);
317
797
  if (result.valid !== true || !result.bundle || typeof result.bundle !== "object") throw new Error("Remote compilation did not produce a bundle");
318
798
  const request = {
@@ -323,64 +803,10 @@ async function execute(parsed) {
323
803
  request.mappings = await promptMappings(parsed, client, preview, request.mappings);
324
804
  if (Object.keys(request.mappings).length) preview = await client.resourceBundles.importPreview(request);
325
805
  if (parsed.command === "preview") return { ok: preview.can_import === true, preview };
326
- if (parsed.command === "install") assertInstallPreviewReady(preview);
327
806
  if (preview.can_import !== true) return { ok: false, preview };
328
807
  await confirmation(parsed, preview);
329
- const imported = await client.resourceBundles.importBundle(request, { idempotencyKey: installKey(request.bundle, request.mappings) });
330
- if (parsed.command === "import") return { ok: true, phase: "imported", preview, result: imported };
331
- const agent = await selectInstalledAgent(parsed, imported);
332
- if (importedRows(imported).some((row) => row.status === "incomplete") && flag(parsed, "allow-incomplete") !== "true") {
333
- throw Object.assign(new Error("Publication is blocked by incomplete imported dependencies"), { phase: "post_import", imported });
334
- }
335
- try {
336
- await client.agents.publish(String(agent.id), {
337
- idempotencyKey: `publish-${createHash("sha256").update(`agent:${String(agent.id)}`).digest("hex").slice(0, 40)}`
338
- });
339
- } catch (error) {
340
- throw Object.assign(error instanceof Error ? error : new Error("Agent publication failed"), {
341
- phase: "publish",
342
- imported
343
- });
344
- }
345
- let deployment;
346
- try {
347
- if (integration === "client-deployment") deployment = await clientDeployment(parsed, client, agent, app);
348
- } catch (error) {
349
- throw Object.assign(error instanceof Error ? error : new Error("Client deployment setup failed"), {
350
- phase: "deployment",
351
- imported
352
- });
353
- }
354
- const deploymentId = deployment && typeof deployment.client_id === "string" ? deployment.client_id : void 0;
355
- let envFile;
356
- if (app && flag(parsed, "no-write-env") !== "true" && integration !== "publish-only") {
357
- try {
358
- const localClientBaseUrl = integration === "client-deployment" ? clientAppBaseUrl() : void 0;
359
- envFile = await updateEnvFile(app, integration === "client-deployment" ? {
360
- VITE_AGENTS24_DEPLOYMENT_ID: String(deploymentId),
361
- ...localClientBaseUrl ? { VITE_AGENTS24_BASE_URL: localClientBaseUrl } : {}
362
- } : { AGENTS24_API_KEY: apiKey, AGENTS24_AGENT_ID: String(agent.id) });
363
- } catch (error) {
364
- throw Object.assign(error instanceof Error ? error : new Error("Could not configure the generated app"), {
365
- phase: "env_write",
366
- imported
367
- });
368
- }
369
- }
370
- const packageManager = app?.packageManager || "pnpm";
371
- const nextCommand = app && integration !== "publish-only" ? packageManager === "npm" ? "npm run dev" : packageManager === "bun" ? "bun run dev" : `${packageManager} dev` : void 0;
372
- return {
373
- ok: true,
374
- phase: deployment ? "deployed" : "published",
375
- integration,
376
- agent_id: String(agent.id),
377
- ...deployment ? { deployment, deployment_id: deploymentId } : {},
378
- env_written: Boolean(envFile),
379
- ...envFile ? { env_file: envFile } : {},
380
- ...nextCommand ? { next_command: nextCommand } : {},
381
- preview,
382
- result: imported
383
- };
808
+ const imported = await client.resourceBundles.importBundle(request, { idempotencyKey: importKey(request.bundle, request.mappings) });
809
+ return { ok: true, phase: "imported", preview, result: imported };
384
810
  }
385
811
  throw new Error(`Unknown package command: ${parsed.command}`);
386
812
  }
@@ -395,20 +821,9 @@ function diagnostics(error) {
395
821
  return Array.isArray(value) ? value : void 0;
396
822
  }
397
823
  function errorMessage(error, parsed) {
398
- if (parsed?.command === "install" && error && typeof error === "object" && "status" in error && error.status === 403) {
399
- return "This API key cannot install Agents. Create a new Agent integration API key in Settings; existing keys cannot gain additional scopes.";
400
- }
824
+ void parsed;
401
825
  return error instanceof Error ? error.message : "Unexpected CLI failure";
402
826
  }
403
- function installSummary(result) {
404
- const lines = ["Agent installed and published.", `Agent ID: ${String(result.agent_id)}`];
405
- if (result.deployment_id) lines.push(`Deployment ID: ${String(result.deployment_id)}`);
406
- if (result.env_written) lines.push(`Configured: ${String(result.env_file)}`);
407
- else if (result.integration !== "publish-only") lines.push("Environment configuration was not written.");
408
- if (result.next_command) lines.push(`Next: ${String(result.next_command)}`);
409
- return `${lines.join("\n")}
410
- `;
411
- }
412
827
  async function run(argv = process.argv.slice(2)) {
413
828
  let parsed;
414
829
  try {
@@ -418,7 +833,6 @@ async function run(argv = process.argv.slice(2)) {
418
833
  `);
419
834
  else if (result.ok === false) process.stderr.write(`${JSON.stringify(result, null, 2)}
420
835
  `);
421
- else if (parsed.command === "install") process.stdout.write(installSummary(result));
422
836
  else process.stdout.write(`${JSON.stringify(result, null, 2)}
423
837
  `);
424
838
  return result.ok === false ? 1 : 0;