@openagentpack/cli 0.0.0-beta.run-29740517091.sha-65ccfac
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/LICENSE +199 -0
- package/README.md +34 -0
- package/dist/bin/agents.d.ts +1 -0
- package/dist/bin/agents.js +24 -0
- package/dist/chunk-W5APGQN3.js +1864 -0
- package/dist/src/program.d.ts +5 -0
- package/dist/src/program.js +6 -0
- package/package.json +63 -0
|
@@ -0,0 +1,1864 @@
|
|
|
1
|
+
// src/program.ts
|
|
2
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3
|
+
import { dirname as dirname4, resolve as resolve5 } from "path";
|
|
4
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5
|
+
import { Command as Command2, Option as Option2 } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/commands/apply.ts
|
|
8
|
+
import * as p2 from "@clack/prompts";
|
|
9
|
+
import { decideDestructive, executePlannedProject, UserError as UserError2 } from "@openagentpack/sdk";
|
|
10
|
+
import chalk3 from "chalk";
|
|
11
|
+
|
|
12
|
+
// src/config-loader.ts
|
|
13
|
+
import { createProjectRuntime, resolveProjectConfig, UserError } from "@openagentpack/sdk";
|
|
14
|
+
|
|
15
|
+
// src/credentials.ts
|
|
16
|
+
import { bootstrapRuntimeCredentialsSync } from "@openagentpack/sdk";
|
|
17
|
+
var bootstrapped = false;
|
|
18
|
+
function ensureCredentials() {
|
|
19
|
+
if (bootstrapped) return;
|
|
20
|
+
bootstrapped = true;
|
|
21
|
+
bootstrapRuntimeCredentialsSync();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/file-state-manager.ts
|
|
25
|
+
import { basename, dirname, resolve } from "path";
|
|
26
|
+
import { LocalFileStateBackend, StateManager } from "@openagentpack/sdk";
|
|
27
|
+
function createCliStateScope(configPath, projectName) {
|
|
28
|
+
const resolved = resolve(configPath);
|
|
29
|
+
return {
|
|
30
|
+
projectId: projectName ?? basename(dirname(resolved))
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function loadFileState(configPath, statePath, projectName) {
|
|
34
|
+
const resolved = resolve(configPath);
|
|
35
|
+
const backend = new LocalFileStateBackend({ configPath: resolved, statePath });
|
|
36
|
+
const path = backend.getStatePath(createCliStateScope(resolved, projectName));
|
|
37
|
+
return StateManager.load(path);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/config-loader.ts
|
|
41
|
+
async function buildCliRuntime(filePath, options = {}) {
|
|
42
|
+
ensureCredentials();
|
|
43
|
+
const { config, configPath, projectName } = await resolveProjectConfig(filePath, options);
|
|
44
|
+
const state = await loadFileState(configPath, options.statePath, projectName);
|
|
45
|
+
const ctx = createProjectRuntime({
|
|
46
|
+
projectName,
|
|
47
|
+
config,
|
|
48
|
+
state,
|
|
49
|
+
configPath,
|
|
50
|
+
providers: config.providers
|
|
51
|
+
});
|
|
52
|
+
return { ...ctx, configPath };
|
|
53
|
+
}
|
|
54
|
+
function assertProviderConfigured(ctx, provider) {
|
|
55
|
+
if (!provider || provider === "all") return;
|
|
56
|
+
if (ctx.providers.has(provider)) return;
|
|
57
|
+
const available = Array.from(ctx.providers.keys()).join(", ") || "none";
|
|
58
|
+
throw new UserError(`Provider '${provider}' is not configured. Available providers: ${available}.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/logger.ts
|
|
62
|
+
import chalk from "chalk";
|
|
63
|
+
var PRIORITY = {
|
|
64
|
+
error: 0,
|
|
65
|
+
warn: 1,
|
|
66
|
+
success: 2,
|
|
67
|
+
info: 3,
|
|
68
|
+
debug: 4
|
|
69
|
+
};
|
|
70
|
+
var maxLevel = "info";
|
|
71
|
+
function configureLogger(opts) {
|
|
72
|
+
if (opts.quiet) {
|
|
73
|
+
maxLevel = "error";
|
|
74
|
+
} else if ((opts.verbose ?? 0) >= 2) {
|
|
75
|
+
maxLevel = "debug";
|
|
76
|
+
} else if ((opts.verbose ?? 0) >= 1) {
|
|
77
|
+
maxLevel = "success";
|
|
78
|
+
} else {
|
|
79
|
+
maxLevel = "info";
|
|
80
|
+
}
|
|
81
|
+
if (opts.color === false || process.env.NO_COLOR) {
|
|
82
|
+
chalk.level = 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function shouldEmit(level) {
|
|
86
|
+
return PRIORITY[level] <= PRIORITY[maxLevel];
|
|
87
|
+
}
|
|
88
|
+
function emit(level, icon, msg) {
|
|
89
|
+
if (!shouldEmit(level)) return;
|
|
90
|
+
console.error(`${icon} ${msg}`);
|
|
91
|
+
}
|
|
92
|
+
var log = {
|
|
93
|
+
debug(msg) {
|
|
94
|
+
emit("debug", chalk.dim("\u2022"), msg);
|
|
95
|
+
},
|
|
96
|
+
info(msg) {
|
|
97
|
+
emit("info", chalk.blue("\u2139"), msg);
|
|
98
|
+
},
|
|
99
|
+
success(msg) {
|
|
100
|
+
emit("success", chalk.green("\u2713"), msg);
|
|
101
|
+
},
|
|
102
|
+
warn(msg) {
|
|
103
|
+
emit("warn", chalk.yellow("\u26A0"), msg);
|
|
104
|
+
},
|
|
105
|
+
error(msg) {
|
|
106
|
+
emit("error", chalk.red("\u2717"), msg);
|
|
107
|
+
},
|
|
108
|
+
adopt(msg) {
|
|
109
|
+
emit("info", chalk.cyan("\u27F3"), msg);
|
|
110
|
+
},
|
|
111
|
+
gone(msg) {
|
|
112
|
+
emit("warn", chalk.yellow("\u2298"), msg);
|
|
113
|
+
},
|
|
114
|
+
plain(msg = "") {
|
|
115
|
+
if (shouldEmit("info")) console.error(msg);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/plan-workflow.ts
|
|
120
|
+
import * as p from "@clack/prompts";
|
|
121
|
+
import { planProjectContext } from "@openagentpack/sdk";
|
|
122
|
+
|
|
123
|
+
// src/render-feedback.ts
|
|
124
|
+
function createRuntimeFeedbackBuffer() {
|
|
125
|
+
const events = [];
|
|
126
|
+
return {
|
|
127
|
+
onFeedback(event) {
|
|
128
|
+
events.push(event);
|
|
129
|
+
},
|
|
130
|
+
flush() {
|
|
131
|
+
for (const event of events.splice(0)) {
|
|
132
|
+
renderRuntimeFeedback(event);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function renderRuntimeFeedback(event) {
|
|
138
|
+
if (event.type === "resource_adopted") {
|
|
139
|
+
log.adopt(event.message);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (event.type === "resource_already_gone" || event.type === "refresh_resource_missing") {
|
|
143
|
+
log.gone(event.message);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (event.level === "success") {
|
|
147
|
+
log.success(event.message);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (event.level === "warning") {
|
|
151
|
+
log.warn(event.message);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (event.level === "error") {
|
|
155
|
+
log.error(event.message);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
log.info(event.message);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/plan-workflow.ts
|
|
162
|
+
async function planProjectWithRefresh(ctx, options = {}) {
|
|
163
|
+
const resourceCount = ctx.state.listResources().length;
|
|
164
|
+
const showRefreshUx = options.refresh !== false && resourceCount > 0 && !options.quiet;
|
|
165
|
+
let spinner4;
|
|
166
|
+
if (showRefreshUx) {
|
|
167
|
+
spinner4 = p.spinner({ output: process.stderr });
|
|
168
|
+
spinner4.start("Refreshing state...");
|
|
169
|
+
} else if (options.refresh === false && !options.quiet) {
|
|
170
|
+
log.warn("Refresh disabled. Remote drift will not be checked.");
|
|
171
|
+
}
|
|
172
|
+
const feedbackBuffer = spinner4 ? createRuntimeFeedbackBuffer() : void 0;
|
|
173
|
+
const planned = await planProjectContext(ctx, {
|
|
174
|
+
provider: options.provider,
|
|
175
|
+
refresh: options.refresh,
|
|
176
|
+
quiet: !!options.quiet,
|
|
177
|
+
onFeedback: options.quiet ? void 0 : feedbackBuffer?.onFeedback ?? renderRuntimeFeedback
|
|
178
|
+
});
|
|
179
|
+
spinner4?.stop("State refreshed.");
|
|
180
|
+
feedbackBuffer?.flush();
|
|
181
|
+
return planned;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/render-diagnostics.ts
|
|
185
|
+
import chalk2 from "chalk";
|
|
186
|
+
function renderDiagnostics(diagnostics) {
|
|
187
|
+
if (diagnostics.length === 0) return;
|
|
188
|
+
console.log("\nDiagnostics:");
|
|
189
|
+
for (const d of diagnostics) {
|
|
190
|
+
const icon = d.severity === "error" ? "\u2717" : d.severity === "warning" ? "\u26A0" : "\u2139";
|
|
191
|
+
const color = d.severity === "error" ? chalk2.red : d.severity === "warning" ? chalk2.yellow : chalk2.blue;
|
|
192
|
+
console.log(color(` ${icon} ${d.code}`));
|
|
193
|
+
if (d.resource) {
|
|
194
|
+
console.log(` Resource: ${d.resource.type}.${d.resource.name} (${d.resource.provider})`);
|
|
195
|
+
}
|
|
196
|
+
console.log(` ${d.message}`);
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
200
|
+
function diagnosticsHaveErrors(diagnostics) {
|
|
201
|
+
return diagnostics.some((d) => d.severity === "error");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/utils/address-utils.ts
|
|
205
|
+
function formatResourceAddress(address) {
|
|
206
|
+
return `${address.provider}.${address.type}.${address.name}`;
|
|
207
|
+
}
|
|
208
|
+
function formatResourceLabel(address) {
|
|
209
|
+
return `${address.type}.${address.name} (${address.provider})`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/commands/apply.ts
|
|
213
|
+
function classifyApplyPrompt(actions) {
|
|
214
|
+
if (actions.some((a) => a.driftKind === "both")) return "combined_drift";
|
|
215
|
+
if (actions.some((a) => a.driftKind === "remote")) return "remote_drift";
|
|
216
|
+
if (actions.some((a) => a.driftKind === "local")) return "local_change";
|
|
217
|
+
return "planned_change";
|
|
218
|
+
}
|
|
219
|
+
async function confirmDestroy(deletes) {
|
|
220
|
+
log.plain(chalk3.red.bold(`
|
|
221
|
+
Resources will be destroyed.`));
|
|
222
|
+
log.plain(chalk3.red(`Applying will delete ${deletes.length} resource(s) listed above.`));
|
|
223
|
+
const shouldApply = await p2.confirm({
|
|
224
|
+
message: `Apply and destroy ${deletes.length} resource(s)?`,
|
|
225
|
+
output: process.stderr
|
|
226
|
+
});
|
|
227
|
+
return !p2.isCancel(shouldApply) && shouldApply;
|
|
228
|
+
}
|
|
229
|
+
async function confirmDrift(actions, file) {
|
|
230
|
+
const kind = classifyApplyPrompt(actions);
|
|
231
|
+
if (kind === "combined_drift" || kind === "remote_drift") {
|
|
232
|
+
const hasCombinedDrift = kind === "combined_drift";
|
|
233
|
+
log.plain();
|
|
234
|
+
if (hasCombinedDrift) {
|
|
235
|
+
log.plain(chalk3.yellow("Both local YAML and remote resource changed since the last apply."));
|
|
236
|
+
log.plain(
|
|
237
|
+
chalk3.yellow(
|
|
238
|
+
"Applying will update the remote resource to match the current YAML and may overwrite remote-only changes."
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
log.plain(chalk3.yellow("Remote-only changes were detected."));
|
|
243
|
+
log.plain(chalk3.yellow("Applying will overwrite the remote resource with the current YAML."));
|
|
244
|
+
}
|
|
245
|
+
const driftActions = actions.filter((a) => a.driftKind === "remote" || a.driftKind === "both");
|
|
246
|
+
const driftAction = driftActions.length === 1 ? driftActions[0] : void 0;
|
|
247
|
+
const inspectAddress = driftAction ? formatResourceAddress(driftAction.address) : "<address>";
|
|
248
|
+
log.plain(
|
|
249
|
+
chalk3.gray(
|
|
250
|
+
`OpenAgentPack will not pull remote changes into YAML automatically. To keep them, cancel and inspect with: agents state show ${inspectAddress} -f ${file}`
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
const choice = await p2.select({
|
|
254
|
+
message: hasCombinedDrift ? "How do you want to handle this conflict?" : "How do you want to handle this drift?",
|
|
255
|
+
output: process.stderr,
|
|
256
|
+
options: [
|
|
257
|
+
{
|
|
258
|
+
value: "apply",
|
|
259
|
+
label: hasCombinedDrift ? "Apply YAML to remote" : "Overwrite remote with YAML",
|
|
260
|
+
hint: hasCombinedDrift ? "YAML wins; remote-only changes may be overwritten" : "YAML wins"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
value: "cancel",
|
|
264
|
+
label: "Cancel and keep remote unchanged",
|
|
265
|
+
hint: "Update YAML manually if you want to keep remote changes"
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
return !p2.isCancel(choice) && choice === "apply";
|
|
270
|
+
}
|
|
271
|
+
const message = kind === "local_change" ? "Local YAML changes were detected. Apply YAML to remote?" : "Apply planned YAML changes to remote?";
|
|
272
|
+
const shouldApply = await p2.confirm({ message, output: process.stderr });
|
|
273
|
+
return !p2.isCancel(shouldApply) && shouldApply;
|
|
274
|
+
}
|
|
275
|
+
async function applyCommand(options) {
|
|
276
|
+
const ctx = await buildCliRuntime(options.file);
|
|
277
|
+
assertProviderConfigured(ctx, options.provider);
|
|
278
|
+
const planned = await planProjectWithRefresh(ctx, {
|
|
279
|
+
provider: options.provider,
|
|
280
|
+
refresh: options.refresh
|
|
281
|
+
});
|
|
282
|
+
const plan = planned.plan;
|
|
283
|
+
renderDiagnostics(plan.diagnostics);
|
|
284
|
+
if (diagnosticsHaveErrors(plan.diagnostics)) {
|
|
285
|
+
throw new UserError2("Cannot apply: resolve the errors above first.");
|
|
286
|
+
}
|
|
287
|
+
const actionable = plan.actions.filter((a) => a.action !== "no-op");
|
|
288
|
+
if (actionable.length === 0) {
|
|
289
|
+
log.success("No changes. Infrastructure is up-to-date.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const creates = actionable.filter((a) => a.action === "create");
|
|
293
|
+
const updates = actionable.filter((a) => a.action === "update");
|
|
294
|
+
const deletes = planned.destructiveActions;
|
|
295
|
+
console.log(
|
|
296
|
+
`
|
|
297
|
+
${chalk3.green(`${creates.length} to create`)}, ${chalk3.yellow(`${updates.length} to update`)}, ${chalk3.red(`${deletes.length} to destroy`)}
|
|
298
|
+
`
|
|
299
|
+
);
|
|
300
|
+
for (const a of actionable) {
|
|
301
|
+
const icon = a.action === "create" ? "+" : a.action === "update" ? "~" : "-";
|
|
302
|
+
const color = a.action === "create" ? chalk3.green : a.action === "update" ? chalk3.yellow : chalk3.red;
|
|
303
|
+
console.log(color(` ${icon} ${formatResourceLabel(a.address)}`));
|
|
304
|
+
if (a.action === "update") {
|
|
305
|
+
console.log(color(` ${a.reason}`));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (options.refreshOnly) {
|
|
309
|
+
log.info("Refresh-only mode: no remote mutations will be performed.");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (deletes.length > 0) {
|
|
313
|
+
console.log(chalk3.red.bold(`
|
|
314
|
+
\u26A0 Resources to be DESTROYED:`));
|
|
315
|
+
for (const a of deletes) {
|
|
316
|
+
console.log(chalk3.red(` - ${formatResourceLabel(a.address)}`));
|
|
317
|
+
}
|
|
318
|
+
console.log();
|
|
319
|
+
}
|
|
320
|
+
const destructiveDecision = await decideDestructive(deletes, {
|
|
321
|
+
policy: options.yes ? "force" : "prompt",
|
|
322
|
+
confirm: confirmDestroy
|
|
323
|
+
});
|
|
324
|
+
if (destructiveDecision !== "proceed") {
|
|
325
|
+
p2.cancel("Apply cancelled. No remote resources were changed.", {
|
|
326
|
+
output: process.stderr
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!options.yes && deletes.length === 0) {
|
|
331
|
+
const shouldApply = await confirmDrift(actionable, options.file);
|
|
332
|
+
if (!shouldApply) {
|
|
333
|
+
p2.cancel("Apply cancelled. No remote resources were changed.", {
|
|
334
|
+
output: process.stderr
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const s = p2.spinner({ output: process.stderr });
|
|
340
|
+
s.start("Applying changes...");
|
|
341
|
+
const result = await executePlannedProject(planned, {
|
|
342
|
+
onFeedback: renderRuntimeFeedback,
|
|
343
|
+
policy: "force",
|
|
344
|
+
concurrency: options.concurrency
|
|
345
|
+
});
|
|
346
|
+
const succeeded = result.results.filter((r) => r.status === "success").length;
|
|
347
|
+
const failed = result.results.filter((r) => r.status === "failed").length;
|
|
348
|
+
const skipped = result.results.filter((r) => r.status === "skipped").length;
|
|
349
|
+
s.stop("Apply finished.");
|
|
350
|
+
if (failed > 0) {
|
|
351
|
+
p2.log.warning(`${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`, { output: process.stderr });
|
|
352
|
+
throw new UserError2("Apply failed.");
|
|
353
|
+
} else {
|
|
354
|
+
p2.log.success(`Apply complete! ${succeeded} actions executed successfully.`, { output: process.stderr });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/commands/deployment.ts
|
|
359
|
+
import {
|
|
360
|
+
getDeploymentDetailsForContext,
|
|
361
|
+
getDeploymentRuntimeProviderForContext,
|
|
362
|
+
listDeploymentsForContext,
|
|
363
|
+
runDeploymentForContext,
|
|
364
|
+
UserError as UserError3
|
|
365
|
+
} from "@openagentpack/sdk";
|
|
366
|
+
import chalk5 from "chalk";
|
|
367
|
+
|
|
368
|
+
// src/render-table.ts
|
|
369
|
+
import chalk4 from "chalk";
|
|
370
|
+
var DEFAULT_INDENT = " ";
|
|
371
|
+
function columnWidth(lengths, min = 4, padding = 2) {
|
|
372
|
+
if (lengths.length === 0) return min + padding;
|
|
373
|
+
return Math.max(min, ...lengths) + padding;
|
|
374
|
+
}
|
|
375
|
+
function printTableTitle(title, count) {
|
|
376
|
+
console.log(`
|
|
377
|
+
${chalk4.bold(title)} (${count}):
|
|
378
|
+
`);
|
|
379
|
+
}
|
|
380
|
+
function printTableHeader(headers, separatorWidth, indent = DEFAULT_INDENT) {
|
|
381
|
+
console.log(chalk4.gray(`${indent}${headers.join(" ")}`));
|
|
382
|
+
console.log(chalk4.gray(`${indent}${"\u2500".repeat(separatorWidth)}`));
|
|
383
|
+
}
|
|
384
|
+
function printTableRow(cells, indent = DEFAULT_INDENT) {
|
|
385
|
+
console.log(`${indent}${cells.join(" ")}`);
|
|
386
|
+
}
|
|
387
|
+
function printTableFooter() {
|
|
388
|
+
console.log();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/commands/deployment.ts
|
|
392
|
+
async function deploymentListCommand(options) {
|
|
393
|
+
const ctx = await buildCliRuntime(options.file);
|
|
394
|
+
const rows = listDeploymentsForContext(ctx, options.provider);
|
|
395
|
+
if (rows.length === 0) {
|
|
396
|
+
log.info("No deployments in state. Run `agents apply` first.");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const nameWidth = columnWidth(rows.map((r) => r.name.length));
|
|
400
|
+
printTableTitle("Deployments", rows.length);
|
|
401
|
+
printTableHeader(
|
|
402
|
+
["Name".padEnd(nameWidth), "Provider".padEnd(10), "Remote ID".padEnd(28), "Schedule".padEnd(18), "Agent"],
|
|
403
|
+
nameWidth + 70
|
|
404
|
+
);
|
|
405
|
+
for (const r of rows) {
|
|
406
|
+
const nameCell = chalk5.bold(r.name.padEnd(nameWidth));
|
|
407
|
+
const provCell = chalk5.cyan(r.provider.padEnd(10));
|
|
408
|
+
const idCell = r.remoteId ? r.remoteId.slice(0, 26).padEnd(28) : chalk5.dim("(emulated)".padEnd(28));
|
|
409
|
+
const schedCell = r.scheduleExpression.padEnd(18);
|
|
410
|
+
printTableRow([nameCell, provCell, idCell, schedCell, r.agent]);
|
|
411
|
+
}
|
|
412
|
+
printTableFooter();
|
|
413
|
+
}
|
|
414
|
+
async function deploymentGetCommand(name, options) {
|
|
415
|
+
const ctx = await buildCliRuntime(options.file);
|
|
416
|
+
const { bindings, provider, info } = await getDeploymentDetailsForContext(ctx, name, void 0, options.provider);
|
|
417
|
+
console.log(` Name: ${chalk5.bold(name)}`);
|
|
418
|
+
console.log(` Provider: ${provider}`);
|
|
419
|
+
console.log(` Remote ID: ${info.id ?? chalk5.dim("(emulated / local)")}`);
|
|
420
|
+
console.log(` Status: ${info.status}`);
|
|
421
|
+
if (info.paused_reason) {
|
|
422
|
+
const pr = info.paused_reason;
|
|
423
|
+
const detail = pr.error?.type ? `${pr.type} (${pr.error.type})` : pr.type;
|
|
424
|
+
console.log(` Paused: ${detail}`);
|
|
425
|
+
}
|
|
426
|
+
if (info.schedule) {
|
|
427
|
+
const tz = info.schedule.timezone ? ` (${info.schedule.timezone})` : "";
|
|
428
|
+
console.log(` Schedule: ${info.schedule.expression}${tz}`);
|
|
429
|
+
}
|
|
430
|
+
console.log(` Agent: ${bindings.agentId}`);
|
|
431
|
+
console.log(` Environment: ${bindings.environmentId}`);
|
|
432
|
+
if (bindings.vaultIds.length) console.log(` Vaults: ${bindings.vaultIds.join(", ")}`);
|
|
433
|
+
if (bindings.memoryStoreIds.length) console.log(` Memory: ${bindings.memoryStoreIds.join(", ")}`);
|
|
434
|
+
}
|
|
435
|
+
async function deploymentRunCommand(name, options) {
|
|
436
|
+
const ctx = await buildCliRuntime(options.file);
|
|
437
|
+
const provider = getDeploymentRuntimeProviderForContext(ctx, name, options.provider);
|
|
438
|
+
log.info(`Running deployment '${name}' on ${provider}...`);
|
|
439
|
+
const { result } = await runDeploymentForContext(ctx, name, void 0, options.provider);
|
|
440
|
+
if (result.error) {
|
|
441
|
+
if (result.run_id) console.log(` Run ID: ${result.run_id}`);
|
|
442
|
+
throw new UserError3(`Deployment run failed: ${result.error.type} - ${result.error.message}`);
|
|
443
|
+
}
|
|
444
|
+
log.success(`Deployment '${name}' run started.`);
|
|
445
|
+
if (result.run_id) console.log(` Run ID: ${chalk5.bold(result.run_id)}`);
|
|
446
|
+
console.log(` Session ID: ${result.session_id ? chalk5.bold(result.session_id) : chalk5.dim("(pending)")}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/commands/destroy.ts
|
|
450
|
+
import * as p3 from "@clack/prompts";
|
|
451
|
+
import {
|
|
452
|
+
destroyPlannedProjectResources,
|
|
453
|
+
planDestroyProjectContext
|
|
454
|
+
} from "@openagentpack/sdk";
|
|
455
|
+
import chalk6 from "chalk";
|
|
456
|
+
async function destroyCommand(options) {
|
|
457
|
+
const ctx = await buildCliRuntime(options.file);
|
|
458
|
+
const planned = planDestroyProjectContext(ctx);
|
|
459
|
+
const resources = planned.resources;
|
|
460
|
+
if (resources.length === 0) {
|
|
461
|
+
log.info("No resources in state. Nothing to destroy.");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
console.log(chalk6.red(`
|
|
465
|
+
Destroy ${resources.length} resource(s):
|
|
466
|
+
`));
|
|
467
|
+
for (const r of resources) {
|
|
468
|
+
console.log(chalk6.red(` - ${formatResourceLabel(r.address)} [${r.remote_id}]`));
|
|
469
|
+
}
|
|
470
|
+
if (!options.yes) {
|
|
471
|
+
const shouldDestroy = await p3.confirm({
|
|
472
|
+
message: "Are you sure you want to destroy ALL resources?",
|
|
473
|
+
output: process.stderr
|
|
474
|
+
});
|
|
475
|
+
if (p3.isCancel(shouldDestroy) || !shouldDestroy) {
|
|
476
|
+
p3.cancel("Destroy cancelled.", { output: process.stderr });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
let activeSpinner;
|
|
481
|
+
const result = await destroyPlannedProjectResources(planned, {
|
|
482
|
+
cascade: options.cascade,
|
|
483
|
+
onResourceStart: (resource) => {
|
|
484
|
+
activeSpinner = p3.spinner({ output: process.stderr });
|
|
485
|
+
activeSpinner.start(`Destroying ${formatResourceLabel(resource.address)}`);
|
|
486
|
+
},
|
|
487
|
+
onCascadeRequired: async (blocked) => {
|
|
488
|
+
activeSpinner?.stop(
|
|
489
|
+
chalk6.yellow(`\u26A0 ${formatResourceLabel(blocked.resource.address)} \u2014 ${blocked.error ?? "cascade required"}`)
|
|
490
|
+
);
|
|
491
|
+
activeSpinner = void 0;
|
|
492
|
+
if (options.yes) {
|
|
493
|
+
log.info(`Hint: ${chalk6.bold(`agents destroy -f ${options.file} --cascade`)}`);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
const cascadeConfirm = await p3.confirm({
|
|
497
|
+
message: "Delete associated sessions and retry?",
|
|
498
|
+
output: process.stderr
|
|
499
|
+
});
|
|
500
|
+
if (p3.isCancel(cascadeConfirm) || !cascadeConfirm) return false;
|
|
501
|
+
activeSpinner = p3.spinner({ output: process.stderr });
|
|
502
|
+
activeSpinner.start(`Destroying ${formatResourceLabel(blocked.resource.address)} with cascade`);
|
|
503
|
+
return true;
|
|
504
|
+
},
|
|
505
|
+
onResourceResult: (item) => {
|
|
506
|
+
stopResourceSpinner(activeSpinner, item);
|
|
507
|
+
activeSpinner = void 0;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
const summary = result.destroyed === result.resources.length ? chalk6.green(`Destroy complete. ${result.destroyed}/${result.resources.length} resources removed.`) : chalk6.yellow(`Destroy complete. ${result.destroyed}/${result.resources.length} resources removed.`);
|
|
511
|
+
p3.outro(summary, { output: process.stderr });
|
|
512
|
+
}
|
|
513
|
+
function stopResourceSpinner(spinner4, result) {
|
|
514
|
+
const label = formatResourceLabel(result.resource.address);
|
|
515
|
+
if (!spinner4) {
|
|
516
|
+
if (result.reason === "provider_missing") {
|
|
517
|
+
log.warn(result.error ?? `No provider for '${result.resource.address.provider}', skipping ${label}`);
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (result.status === "success") {
|
|
522
|
+
if (result.reason === "reference_removed") {
|
|
523
|
+
spinner4.stop(chalk6.green(`\u2713 ${label} \u2014 local reference removed (remote left intact)`));
|
|
524
|
+
} else if (result.reason === "already_gone") {
|
|
525
|
+
spinner4.stop(chalk6.yellow(`\u2298 ${label} \u2014 already deleted remotely, cleaned up state`));
|
|
526
|
+
} else if (result.cascaded) {
|
|
527
|
+
spinner4.stop(chalk6.green(`\u2713 ${label} \u2014 destroyed (cascaded)`));
|
|
528
|
+
} else {
|
|
529
|
+
spinner4.stop(chalk6.green(`\u2713 ${label} \u2014 destroyed`));
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (result.reason === "provider_missing") {
|
|
534
|
+
spinner4.stop(
|
|
535
|
+
chalk6.yellow(result.error ?? `No provider for '${result.resource.address.provider}', skipping ${label}`)
|
|
536
|
+
);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (result.status === "blocked") {
|
|
540
|
+
spinner4.stop(chalk6.yellow(`\u26A0 ${label} \u2014 ${result.error ?? "blocked"}`));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
spinner4.stop(chalk6.red(`\u2717 ${label} \u2014 ${result.error ?? "unknown error"}`));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/commands/init.ts
|
|
547
|
+
import { readFile, writeFile } from "fs/promises";
|
|
548
|
+
import * as p4 from "@clack/prompts";
|
|
549
|
+
|
|
550
|
+
// src/utils/file-utils.ts
|
|
551
|
+
import { existsSync } from "fs";
|
|
552
|
+
import { access } from "fs/promises";
|
|
553
|
+
function fileExistsSync(path) {
|
|
554
|
+
return existsSync(path);
|
|
555
|
+
}
|
|
556
|
+
async function fileExists(path) {
|
|
557
|
+
try {
|
|
558
|
+
await access(path);
|
|
559
|
+
return true;
|
|
560
|
+
} catch {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/commands/init.ts
|
|
566
|
+
var GITIGNORE_ADDITIONS = `
|
|
567
|
+
# agents
|
|
568
|
+
agents.state.json
|
|
569
|
+
.env
|
|
570
|
+
`;
|
|
571
|
+
function buildTemplate(opts) {
|
|
572
|
+
const providers = {
|
|
573
|
+
bailian: ` bailian:
|
|
574
|
+
api_key: \${DASHSCOPE_API_KEY}
|
|
575
|
+
workspace_id: \${BAILIAN_WORKSPACE_ID}`,
|
|
576
|
+
claude: ` claude:
|
|
577
|
+
api_key: \${ANTHROPIC_API_KEY}`,
|
|
578
|
+
qoder: ` qoder:
|
|
579
|
+
api_key: \${QODER_PAT}
|
|
580
|
+
gateway: "https://api.qoder.com/api/v1/cloud"`,
|
|
581
|
+
ark: ` ark:
|
|
582
|
+
api_key: \${ARK_API_KEY}`
|
|
583
|
+
};
|
|
584
|
+
let providerBlock;
|
|
585
|
+
if (opts.provider === "all") {
|
|
586
|
+
providerBlock = `${providers.bailian}
|
|
587
|
+
${providers.claude}
|
|
588
|
+
${providers.qoder}
|
|
589
|
+
${providers.ark}`;
|
|
590
|
+
} else {
|
|
591
|
+
providerBlock = providers[opts.provider];
|
|
592
|
+
}
|
|
593
|
+
const singleModel = {
|
|
594
|
+
bailian: ` model: qwen3.7-max`,
|
|
595
|
+
claude: ` model: claude-sonnet-4-6`,
|
|
596
|
+
qoder: ` model: ultimate`,
|
|
597
|
+
ark: ` model: doubao-seed-2-1-pro-260628`
|
|
598
|
+
};
|
|
599
|
+
const modelBlock = opts.provider === "all" ? ` model:
|
|
600
|
+
bailian: qwen3.7-max
|
|
601
|
+
claude: claude-sonnet-4-6
|
|
602
|
+
qoder: ultimate
|
|
603
|
+
ark: doubao-seed-2-1-pro-260628` : singleModel[opts.provider];
|
|
604
|
+
const toolBlock = opts.provider === "bailian" ? "[bash, read, glob, grep]" : "[read, glob, grep, web_search, web_fetch]";
|
|
605
|
+
return `version: "1"
|
|
606
|
+
|
|
607
|
+
providers:
|
|
608
|
+
${providerBlock}
|
|
609
|
+
|
|
610
|
+
defaults:
|
|
611
|
+
provider: ${opts.provider === "all" ? "all" : opts.provider}
|
|
612
|
+
|
|
613
|
+
environments:
|
|
614
|
+
dev:
|
|
615
|
+
config:
|
|
616
|
+
type: cloud
|
|
617
|
+
networking:
|
|
618
|
+
type: unrestricted
|
|
619
|
+
|
|
620
|
+
agents:
|
|
621
|
+
${opts.agentName}:
|
|
622
|
+
description: "General-purpose assistant"
|
|
623
|
+
${modelBlock}
|
|
624
|
+
instructions: |
|
|
625
|
+
You are a helpful assistant.
|
|
626
|
+
environment: dev
|
|
627
|
+
tools:
|
|
628
|
+
builtin: ${toolBlock}
|
|
629
|
+
`;
|
|
630
|
+
}
|
|
631
|
+
async function initCommand() {
|
|
632
|
+
const configPath = "agents.yaml";
|
|
633
|
+
if (await fileExists(configPath)) {
|
|
634
|
+
log.warn(`${configPath} already exists, skipping.`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
p4.intro("agents init", { output: process.stderr });
|
|
638
|
+
const answers = await p4.group(
|
|
639
|
+
{
|
|
640
|
+
provider: () => p4.select({
|
|
641
|
+
message: "Which provider(s) do you want to use?",
|
|
642
|
+
options: [
|
|
643
|
+
{ value: "bailian", label: "Bailian (\u963F\u91CC\u4E91\u767E\u70BC)" },
|
|
644
|
+
{ value: "claude", label: "Claude" },
|
|
645
|
+
{ value: "qoder", label: "Qoder" },
|
|
646
|
+
{ value: "ark", label: "Ark\uFF08\u706B\u5C71\u65B9\u821F\uFF09" },
|
|
647
|
+
{ value: "all", label: "All providers" }
|
|
648
|
+
],
|
|
649
|
+
output: process.stderr
|
|
650
|
+
}),
|
|
651
|
+
agentName: () => p4.text({
|
|
652
|
+
message: "Name your first agent:",
|
|
653
|
+
placeholder: "assistant",
|
|
654
|
+
defaultValue: "assistant",
|
|
655
|
+
output: process.stderr
|
|
656
|
+
})
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
onCancel: () => {
|
|
660
|
+
p4.cancel("Init cancelled.", { output: process.stderr });
|
|
661
|
+
process.exit(0);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
const template = buildTemplate({
|
|
666
|
+
provider: answers.provider,
|
|
667
|
+
agentName: answers.agentName
|
|
668
|
+
});
|
|
669
|
+
await writeFile(configPath, template, "utf8");
|
|
670
|
+
p4.log.success(`Created ${configPath}`, { output: process.stderr });
|
|
671
|
+
const gitignorePath = ".gitignore";
|
|
672
|
+
if (await fileExists(gitignorePath)) {
|
|
673
|
+
const content = await readFile(gitignorePath, "utf8");
|
|
674
|
+
if (!content.includes("agents.state.json")) {
|
|
675
|
+
await writeFile(gitignorePath, content + GITIGNORE_ADDITIONS, "utf8");
|
|
676
|
+
p4.log.success("Updated .gitignore", { output: process.stderr });
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
await writeFile(gitignorePath, `${GITIGNORE_ADDITIONS.trim()}
|
|
680
|
+
`, "utf8");
|
|
681
|
+
p4.log.success("Created .gitignore", { output: process.stderr });
|
|
682
|
+
}
|
|
683
|
+
p4.outro("Done! Next: edit agents.yaml, then run agents plan", {
|
|
684
|
+
output: process.stderr
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/commands/migrate.ts
|
|
689
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
690
|
+
import { migrateConfig, UserError as UserError4 } from "@openagentpack/sdk";
|
|
691
|
+
async function migrateCommand(options) {
|
|
692
|
+
const fromPath = options.from ?? "agents.synced.yaml";
|
|
693
|
+
const toPath = options.to ?? "agents.yaml";
|
|
694
|
+
const toExists = await fileExists(toPath);
|
|
695
|
+
if (!toExists) {
|
|
696
|
+
throw new UserError4(
|
|
697
|
+
`Target file '${toPath}' not found. Create a agents.yaml first (e.g. \`agents init\`), then run migrate.`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const result = await migrateConfig({ fromPath, toPath });
|
|
701
|
+
await writeFile2(toPath, result.yaml, "utf8");
|
|
702
|
+
const addedParts = [];
|
|
703
|
+
for (const [group2, count] of Object.entries(result.added)) {
|
|
704
|
+
addedParts.push(`${count} ${group2}`);
|
|
705
|
+
}
|
|
706
|
+
const skippedParts = [];
|
|
707
|
+
for (const [group2, count] of Object.entries(result.skipped)) {
|
|
708
|
+
skippedParts.push(`${count} ${group2}`);
|
|
709
|
+
}
|
|
710
|
+
if (addedParts.length) {
|
|
711
|
+
log.success(`Migrated ${addedParts.join(", ")} into ${toPath}.`);
|
|
712
|
+
} else {
|
|
713
|
+
log.info("No new resources to migrate (all already exist in target).");
|
|
714
|
+
}
|
|
715
|
+
if (skippedParts.length) {
|
|
716
|
+
log.info(`Skipped (already exist): ${skippedParts.join(", ")}.`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/commands/models.ts
|
|
721
|
+
import { listProviderModelsForContext } from "@openagentpack/sdk";
|
|
722
|
+
import chalk7 from "chalk";
|
|
723
|
+
async function modelsListCommand(options) {
|
|
724
|
+
const ctx = await buildCliRuntime(options.file);
|
|
725
|
+
const listings = await listProviderModelsForContext(ctx.providers, options.provider);
|
|
726
|
+
for (const listing of listings) {
|
|
727
|
+
const name = listing.provider;
|
|
728
|
+
if (!listing.supportsDynamicListing) {
|
|
729
|
+
if (options.json) {
|
|
730
|
+
process.stdout.write(
|
|
731
|
+
`${JSON.stringify({ provider: name, supportsDynamicListing: false, models: [] }, null, 2)}
|
|
732
|
+
`
|
|
733
|
+
);
|
|
734
|
+
} else {
|
|
735
|
+
console.log(chalk7.yellow(`
|
|
736
|
+
Provider '${name}' does not support dynamic model listing.`));
|
|
737
|
+
if (name === "claude") {
|
|
738
|
+
console.log(chalk7.dim(` Claude models are specified directly (e.g. claude-sonnet-4-6, claude-opus-4-6).`));
|
|
739
|
+
console.log(chalk7.dim(` See: https://docs.anthropic.com/en/docs/about-claude/models
|
|
740
|
+
`));
|
|
741
|
+
} else if (name === "bailian") {
|
|
742
|
+
console.log(chalk7.dim(` Bailian models are specified directly (e.g. qwen-max, qwen-plus).`));
|
|
743
|
+
console.log(chalk7.dim(` See: https://help.aliyun.com/zh/model-studio/getting-started/models
|
|
744
|
+
`));
|
|
745
|
+
} else if (name === "ark") {
|
|
746
|
+
console.log(chalk7.dim(` Ark models are specified directly (e.g. doubao-seed-2-1-pro-260628).`));
|
|
747
|
+
console.log(chalk7.dim(` See: https://www.volcengine.com/docs/82379
|
|
748
|
+
`));
|
|
749
|
+
} else {
|
|
750
|
+
console.log(chalk7.dim(` Refer to the provider's documentation for available model identifiers.
|
|
751
|
+
`));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const models = listing.models;
|
|
757
|
+
if (options.json) {
|
|
758
|
+
process.stdout.write(`${JSON.stringify({ provider: name, models }, null, 2)}
|
|
759
|
+
`);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
console.log(chalk7.bold(`
|
|
763
|
+
Available models (${name}):
|
|
764
|
+
`));
|
|
765
|
+
const colId = columnWidth(
|
|
766
|
+
models.map((m) => m.id.length),
|
|
767
|
+
4
|
|
768
|
+
);
|
|
769
|
+
const colName = columnWidth(
|
|
770
|
+
models.map((m) => m.display_name.length),
|
|
771
|
+
12
|
|
772
|
+
);
|
|
773
|
+
const colPrice = 7;
|
|
774
|
+
const colEfforts = columnWidth(
|
|
775
|
+
models.map((m) => formatEfforts(m.efforts).length),
|
|
776
|
+
7
|
|
777
|
+
);
|
|
778
|
+
printTableHeader(
|
|
779
|
+
["ID".padEnd(colId), "Name".padEnd(colName), "Price".padEnd(colPrice), "Efforts".padEnd(colEfforts), "Default"],
|
|
780
|
+
colId + colName + colPrice + colEfforts + 7 + 4
|
|
781
|
+
);
|
|
782
|
+
for (const m of models) {
|
|
783
|
+
const id = m.id.padEnd(colId);
|
|
784
|
+
const displayName = m.display_name.padEnd(colName);
|
|
785
|
+
const price = formatPrice(m.price_factor).padEnd(colPrice);
|
|
786
|
+
const efforts = formatEfforts(m.efforts).padEnd(colEfforts);
|
|
787
|
+
const defaultEffort = m.default_effort ?? "\u2014";
|
|
788
|
+
const isNew = m.is_new ? chalk7.green(" NEW") : "";
|
|
789
|
+
printTableRow([id, displayName, price, efforts, `${defaultEffort}${isNew}`]);
|
|
790
|
+
}
|
|
791
|
+
console.log(chalk7.dim(`
|
|
792
|
+
Use ${chalk7.reset("model: <ID>")} in your agents.yaml agent configuration.`));
|
|
793
|
+
console.log(chalk7.dim(` Use ${chalk7.reset("model: { id: <ID>, effort: <EFFORT> }")} for effort control.
|
|
794
|
+
`));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
var EFFORT_ORDER = ["none", "low", "medium", "high", "xhigh", "max"];
|
|
798
|
+
function formatEfforts(efforts) {
|
|
799
|
+
if (!efforts?.length) return "\u2014";
|
|
800
|
+
return [...efforts].sort((a, b) => EFFORT_ORDER.indexOf(a) - EFFORT_ORDER.indexOf(b)).join(", ");
|
|
801
|
+
}
|
|
802
|
+
function formatPrice(factor) {
|
|
803
|
+
if (factor === void 0) return "\u2014";
|
|
804
|
+
if (factor === 0) return "free";
|
|
805
|
+
return `\xD7${factor}`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/commands/plan.ts
|
|
809
|
+
import { UserError as UserError6 } from "@openagentpack/sdk";
|
|
810
|
+
import chalk8 from "chalk";
|
|
811
|
+
|
|
812
|
+
// src/runtime.ts
|
|
813
|
+
import { listProviderNames, UserError as UserError5 } from "@openagentpack/sdk";
|
|
814
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
815
|
+
var DEFAULT_CONFIG_FILE = "agents.yaml";
|
|
816
|
+
function isExplicitSource(source) {
|
|
817
|
+
return source !== void 0 && source !== "default";
|
|
818
|
+
}
|
|
819
|
+
function rootCommand(command) {
|
|
820
|
+
let current = command;
|
|
821
|
+
while (current.parent) current = current.parent;
|
|
822
|
+
return current;
|
|
823
|
+
}
|
|
824
|
+
function configFileArgs(args = process.argv.slice(2)) {
|
|
825
|
+
const values = [];
|
|
826
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
827
|
+
const arg = args[i];
|
|
828
|
+
if (!arg) continue;
|
|
829
|
+
if (arg === "--") break;
|
|
830
|
+
if (arg === "-f" || arg === "--file") {
|
|
831
|
+
const value = args[i + 1];
|
|
832
|
+
if (value) {
|
|
833
|
+
values.push(value);
|
|
834
|
+
i += 1;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (arg.startsWith("--file=")) {
|
|
839
|
+
values.push(arg.slice("--file=".length));
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (arg.startsWith("-f") && arg.length > 2) {
|
|
843
|
+
values.push(arg.slice(2));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return values;
|
|
847
|
+
}
|
|
848
|
+
function configFileOption() {
|
|
849
|
+
return new Option("-f, --file <path>", "Config file path");
|
|
850
|
+
}
|
|
851
|
+
function resolveConfigFile(command) {
|
|
852
|
+
const explicitFiles = [...new Set(configFileArgs())];
|
|
853
|
+
if (explicitFiles.length > 1) {
|
|
854
|
+
throw new UserError5(
|
|
855
|
+
`Conflicting config files supplied: ${explicitFiles.join(" and ")}. Use only one --file value.`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
const root = rootCommand(command);
|
|
859
|
+
const rootFile = root.getOptionValue("file");
|
|
860
|
+
const rootSource = root.getOptionValueSource("file");
|
|
861
|
+
const localFile = command.getOptionValue("file");
|
|
862
|
+
const localSource = command.getOptionValueSource("file");
|
|
863
|
+
if (isExplicitSource(rootSource) && isExplicitSource(localSource) && rootFile && localFile && rootFile !== localFile) {
|
|
864
|
+
throw new UserError5(`Conflicting config files supplied: ${rootFile} and ${localFile}. Use only one --file value.`);
|
|
865
|
+
}
|
|
866
|
+
if (isExplicitSource(localSource) && localFile) return localFile;
|
|
867
|
+
if (rootFile) return rootFile;
|
|
868
|
+
return DEFAULT_CONFIG_FILE;
|
|
869
|
+
}
|
|
870
|
+
function withResolvedConfigFile(handler) {
|
|
871
|
+
return async (...args) => {
|
|
872
|
+
const command = args[args.length - 1];
|
|
873
|
+
if (!(command instanceof Command)) {
|
|
874
|
+
await handler(...args);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const handlerArgs = args.slice(0, -1);
|
|
878
|
+
const options = handlerArgs[handlerArgs.length - 1];
|
|
879
|
+
if (options && typeof options === "object") {
|
|
880
|
+
options.file = resolveConfigFile(command);
|
|
881
|
+
}
|
|
882
|
+
await handler(...handlerArgs);
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function registeredProviderNames() {
|
|
886
|
+
return listProviderNames();
|
|
887
|
+
}
|
|
888
|
+
function providerOption(description, opts = {}) {
|
|
889
|
+
const choices = opts.allowAll ? ["all", ...registeredProviderNames()] : registeredProviderNames();
|
|
890
|
+
const option = new Option("--provider <name>", description).choices(choices);
|
|
891
|
+
if (opts.defaultValue !== void 0) option.default(opts.defaultValue);
|
|
892
|
+
return option;
|
|
893
|
+
}
|
|
894
|
+
function parsePositiveInteger(value) {
|
|
895
|
+
if (!/^\d+$/.test(value)) {
|
|
896
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
897
|
+
}
|
|
898
|
+
const parsed = Number(value);
|
|
899
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
900
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
901
|
+
}
|
|
902
|
+
return parsed;
|
|
903
|
+
}
|
|
904
|
+
function parseBooleanOption(value) {
|
|
905
|
+
if (value === "true") return true;
|
|
906
|
+
if (value === "false") return false;
|
|
907
|
+
throw new InvalidArgumentError("must be true or false");
|
|
908
|
+
}
|
|
909
|
+
function writeJson(value) {
|
|
910
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
911
|
+
`);
|
|
912
|
+
}
|
|
913
|
+
function writeJsonLine(value) {
|
|
914
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
915
|
+
`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/commands/plan.ts
|
|
919
|
+
async function planCommand(options) {
|
|
920
|
+
const ctx = await buildCliRuntime(options.file);
|
|
921
|
+
assertProviderConfigured(ctx, options.provider);
|
|
922
|
+
const { plan } = await planProjectWithRefresh(ctx, {
|
|
923
|
+
provider: options.provider,
|
|
924
|
+
refresh: options.refresh,
|
|
925
|
+
quiet: !!options.json
|
|
926
|
+
});
|
|
927
|
+
if (options.json) {
|
|
928
|
+
writeJson(plan);
|
|
929
|
+
if (plan.diagnostics.some((d) => d.severity === "error")) {
|
|
930
|
+
throw new UserError6("Plan contains errors.");
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
renderDiagnostics(plan.diagnostics);
|
|
935
|
+
if (diagnosticsHaveErrors(plan.diagnostics)) {
|
|
936
|
+
throw new UserError6("Plan contains errors.");
|
|
937
|
+
}
|
|
938
|
+
const creates = plan.actions.filter((a) => a.action === "create");
|
|
939
|
+
const updates = plan.actions.filter((a) => a.action === "update");
|
|
940
|
+
const deletes = plan.actions.filter((a) => a.action === "delete");
|
|
941
|
+
const driftByAddress = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const r of ctx.state.listResources()) {
|
|
943
|
+
driftByAddress.set(formatResourceAddress(r.address), r.drift_status);
|
|
944
|
+
}
|
|
945
|
+
const unverified = plan.actions.filter(
|
|
946
|
+
(a) => a.action === "no-op" && driftByAddress.get(formatResourceAddress(a.address)) === "unchecked"
|
|
947
|
+
);
|
|
948
|
+
const hasChanges = creates.length > 0 || updates.length > 0 || deletes.length > 0;
|
|
949
|
+
if (!hasChanges && unverified.length === 0) {
|
|
950
|
+
log.success("No changes. Infrastructure is up-to-date.");
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (hasChanges) {
|
|
954
|
+
console.log("\nPlanned actions:\n");
|
|
955
|
+
for (const a of creates) {
|
|
956
|
+
console.log(chalk8.green(` + ${formatResourceLabel(a.address)}`));
|
|
957
|
+
}
|
|
958
|
+
for (const a of updates) {
|
|
959
|
+
console.log(chalk8.yellow(` ~ ${formatResourceLabel(a.address)}`));
|
|
960
|
+
console.log(chalk8.yellow(` ${a.reason}`));
|
|
961
|
+
}
|
|
962
|
+
for (const a of deletes) {
|
|
963
|
+
console.log(chalk8.red(` - ${formatResourceLabel(a.address)}`));
|
|
964
|
+
}
|
|
965
|
+
} else {
|
|
966
|
+
console.log("\nNo changes to apply.");
|
|
967
|
+
}
|
|
968
|
+
if (unverified.length > 0) {
|
|
969
|
+
console.log(chalk8.blue("\nUnverified (provider can't compare content):"));
|
|
970
|
+
for (const a of unverified) {
|
|
971
|
+
console.log(
|
|
972
|
+
chalk8.blue(` ! ${formatResourceLabel(a.address)}`) + chalk8.dim(" \u2014 exists remotely; drift undetectable")
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (hasChanges) {
|
|
977
|
+
console.log(
|
|
978
|
+
`
|
|
979
|
+
Plan: ${chalk8.green(`${creates.length} to create`)}, ${chalk8.yellow(`${updates.length} to update`)}, ${chalk8.red(`${deletes.length} to destroy`)}.`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
if (options.refreshOnly) {
|
|
983
|
+
console.log(chalk8.blue("Refresh-only mode: no remote mutations will be performed."));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/commands/playground.ts
|
|
988
|
+
import { spawn } from "child_process";
|
|
989
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
990
|
+
import { createRequire } from "module";
|
|
991
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
992
|
+
import { fileURLToPath } from "url";
|
|
993
|
+
import { AGENTS_CONFIG_PROVIDERS } from "@openagentpack/sdk";
|
|
994
|
+
var CLI_PKG = "@openagentpack/cli";
|
|
995
|
+
var PLAYGROUND_PKG = "@openagentpack/playground";
|
|
996
|
+
var DEFAULT_PORT = 4848;
|
|
997
|
+
var PLAYGROUND_URL_RE = /running at http:\/\/localhost:(\d+)/;
|
|
998
|
+
var SUPPORTED_PLAYGROUND_PROVIDERS = new Set(AGENTS_CONFIG_PROVIDERS);
|
|
999
|
+
function cliVersion() {
|
|
1000
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
1001
|
+
const candidates = [
|
|
1002
|
+
resolve2(here, "../package.json"),
|
|
1003
|
+
// bundled dist/chunk-*.js -> package root
|
|
1004
|
+
resolve2(here, "../../package.json")
|
|
1005
|
+
// source src/commands/*.ts -> package root
|
|
1006
|
+
];
|
|
1007
|
+
for (const pkgPath of candidates) {
|
|
1008
|
+
if (!existsSync2(pkgPath)) continue;
|
|
1009
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1010
|
+
if (pkg.name === CLI_PKG && pkg.version) return pkg.version;
|
|
1011
|
+
}
|
|
1012
|
+
throw new Error(`Unable to determine ${CLI_PKG} version for launching ${PLAYGROUND_PKG}.`);
|
|
1013
|
+
}
|
|
1014
|
+
function findLocalPlaygroundBin(startDir) {
|
|
1015
|
+
let dir = startDir;
|
|
1016
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
1017
|
+
const candidate = resolve2(dir, "packages/playground/dist/bin/playground.js");
|
|
1018
|
+
if (existsSync2(candidate)) return candidate;
|
|
1019
|
+
const parent = dirname2(dir);
|
|
1020
|
+
if (parent === dir) break;
|
|
1021
|
+
dir = parent;
|
|
1022
|
+
}
|
|
1023
|
+
return void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function resolveLauncher(version) {
|
|
1026
|
+
const explicit = process.env.AGENTS_PLAYGROUND_BIN?.trim();
|
|
1027
|
+
if (explicit && existsSync2(explicit)) return { cmd: process.execPath, args: [explicit] };
|
|
1028
|
+
try {
|
|
1029
|
+
const require2 = createRequire(import.meta.url);
|
|
1030
|
+
const pkgJsonPath = require2.resolve(`${PLAYGROUND_PKG}/package.json`);
|
|
1031
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
1032
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["agents-playground"];
|
|
1033
|
+
if (binRel) {
|
|
1034
|
+
const binPath = resolve2(dirname2(pkgJsonPath), binRel);
|
|
1035
|
+
if (existsSync2(binPath)) return { cmd: process.execPath, args: [binPath] };
|
|
1036
|
+
}
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
const monorepoBin = findLocalPlaygroundBin(process.cwd());
|
|
1040
|
+
if (monorepoBin) return { cmd: process.execPath, args: [monorepoBin] };
|
|
1041
|
+
return { cmd: "npx", args: ["-y", `${PLAYGROUND_PKG}@${version}`] };
|
|
1042
|
+
}
|
|
1043
|
+
function watchPlaygroundPort(stdout, onPort) {
|
|
1044
|
+
let buffer = "";
|
|
1045
|
+
stdout.on("data", (chunk) => {
|
|
1046
|
+
process.stdout.write(chunk);
|
|
1047
|
+
buffer += chunk.toString();
|
|
1048
|
+
const match = buffer.match(PLAYGROUND_URL_RE);
|
|
1049
|
+
if (!match) return;
|
|
1050
|
+
onPort(Number(match[1]));
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
async function waitForPlaygroundReady(child, fallbackPort, timeoutMs) {
|
|
1054
|
+
let port = fallbackPort;
|
|
1055
|
+
let settled = false;
|
|
1056
|
+
return new Promise((resolve6) => {
|
|
1057
|
+
const deadline = Date.now() + timeoutMs;
|
|
1058
|
+
if (child.stdout) {
|
|
1059
|
+
watchPlaygroundPort(child.stdout, (nextPort) => {
|
|
1060
|
+
port = nextPort;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
const poll = async () => {
|
|
1064
|
+
if (settled) return;
|
|
1065
|
+
if (Date.now() > deadline) {
|
|
1066
|
+
settled = true;
|
|
1067
|
+
resolve6(null);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
const res = await fetch(`http://localhost:${port}/health`);
|
|
1072
|
+
if (res.ok) {
|
|
1073
|
+
settled = true;
|
|
1074
|
+
resolve6(port);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
} catch {
|
|
1078
|
+
}
|
|
1079
|
+
setTimeout(poll, 300);
|
|
1080
|
+
};
|
|
1081
|
+
void poll();
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
function openBrowser(url) {
|
|
1085
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1086
|
+
const args = process.platform === "win32" ? ["", url] : [url];
|
|
1087
|
+
try {
|
|
1088
|
+
spawn(cmd, args, { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
1089
|
+
} catch {
|
|
1090
|
+
log.warn(`Could not open a browser automatically \u2014 visit ${url}`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async function playgroundCommand(options) {
|
|
1094
|
+
const port = options.port ? Number(options.port) : DEFAULT_PORT;
|
|
1095
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
1096
|
+
throw new Error(`Invalid --port '${options.port}'`);
|
|
1097
|
+
}
|
|
1098
|
+
if (options.provider && !SUPPORTED_PLAYGROUND_PROVIDERS.has(options.provider)) {
|
|
1099
|
+
const supported = [...SUPPORTED_PLAYGROUND_PROVIDERS].join(", ");
|
|
1100
|
+
throw new Error(`Playground supports providers: ${supported}; received '${options.provider}'.`);
|
|
1101
|
+
}
|
|
1102
|
+
const env = { ...process.env, PORT: String(port) };
|
|
1103
|
+
if (options.provider) {
|
|
1104
|
+
env.AGENTS_PROVIDER = options.provider;
|
|
1105
|
+
env.AGENTS_CLI_PROVIDER = options.provider;
|
|
1106
|
+
}
|
|
1107
|
+
const { cmd, args } = resolveLauncher(cliVersion());
|
|
1108
|
+
if (cmd === "npx") log.info(`Fetching ${PLAYGROUND_PKG} (first run may take a moment)...`);
|
|
1109
|
+
const child = spawn(cmd, args, { env, stdio: ["inherit", "pipe", "inherit"] });
|
|
1110
|
+
const forward = (signal) => child.kill(signal);
|
|
1111
|
+
process.on("SIGINT", () => forward("SIGINT"));
|
|
1112
|
+
process.on("SIGTERM", () => forward("SIGTERM"));
|
|
1113
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
1114
|
+
child.on("error", (err) => {
|
|
1115
|
+
log.error(`Failed to start playground: ${err instanceof Error ? err.message : String(err)}`);
|
|
1116
|
+
process.exit(1);
|
|
1117
|
+
});
|
|
1118
|
+
const readyPort = await waitForPlaygroundReady(child, port, 3e4);
|
|
1119
|
+
if (readyPort === null) {
|
|
1120
|
+
log.warn(`Playground did not become ready in time \u2014 check the logs above, then open http://localhost:${port}`);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const url = `http://localhost:${readyPort}`;
|
|
1124
|
+
log.success(`Playground ready at ${url}`);
|
|
1125
|
+
if (options.open !== false) openBrowser(url);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/commands/session.ts
|
|
1129
|
+
import {
|
|
1130
|
+
createSessionForAgent,
|
|
1131
|
+
deleteSession,
|
|
1132
|
+
getSession,
|
|
1133
|
+
isTerminalSessionStatus,
|
|
1134
|
+
listSessionEvents,
|
|
1135
|
+
listSessionSummaries,
|
|
1136
|
+
sendSessionMessagePolling,
|
|
1137
|
+
sendSessionMessageStreaming,
|
|
1138
|
+
startSessionRun,
|
|
1139
|
+
startSessionRunPolling,
|
|
1140
|
+
UserError as UserError7
|
|
1141
|
+
} from "@openagentpack/sdk";
|
|
1142
|
+
import { sanitizeSessionEvent, sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
|
|
1143
|
+
import chalk9 from "chalk";
|
|
1144
|
+
|
|
1145
|
+
// src/utils/pagination.ts
|
|
1146
|
+
async function fetchAllPages(fetchPage, all) {
|
|
1147
|
+
const first = await fetchPage();
|
|
1148
|
+
const items = [...first.items];
|
|
1149
|
+
let hasMore = first.hasMore;
|
|
1150
|
+
let nextPage = first.nextPage;
|
|
1151
|
+
while (all && nextPage) {
|
|
1152
|
+
const next = await fetchPage(nextPage);
|
|
1153
|
+
items.push(...next.items);
|
|
1154
|
+
hasMore = next.hasMore;
|
|
1155
|
+
nextPage = next.nextPage;
|
|
1156
|
+
}
|
|
1157
|
+
return { items, hasMore, nextPage };
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/commands/session.ts
|
|
1161
|
+
function formatTimestamp(iso) {
|
|
1162
|
+
const d = new Date(iso);
|
|
1163
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
1164
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1165
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1166
|
+
}
|
|
1167
|
+
function formatDuration(startIso, endIso) {
|
|
1168
|
+
const start = new Date(startIso).getTime();
|
|
1169
|
+
const end = endIso ? new Date(endIso).getTime() : Date.now();
|
|
1170
|
+
if (Number.isNaN(start)) return "-";
|
|
1171
|
+
const sec = Math.max(0, Math.floor((end - start) / 1e3));
|
|
1172
|
+
if (sec < 60) return `${sec}s`;
|
|
1173
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m${sec % 60}s`;
|
|
1174
|
+
const h = Math.floor(sec / 3600);
|
|
1175
|
+
const m = Math.floor(sec % 3600 / 60);
|
|
1176
|
+
return `${h}h${m}m`;
|
|
1177
|
+
}
|
|
1178
|
+
async function sessionCreateCommand(agentNameOrOptions, maybeOptions) {
|
|
1179
|
+
const options = maybeOptions ?? agentNameOrOptions;
|
|
1180
|
+
const positionalAgent = typeof agentNameOrOptions === "string" ? agentNameOrOptions : void 0;
|
|
1181
|
+
if (positionalAgent && options.agent && positionalAgent !== options.agent) {
|
|
1182
|
+
throw new UserError7("Specify agent either positionally or with --agent, not both.");
|
|
1183
|
+
}
|
|
1184
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1185
|
+
const run = await createSessionForAgent(ctx, {
|
|
1186
|
+
agent: positionalAgent ?? options.agent,
|
|
1187
|
+
identityId: options.identityId,
|
|
1188
|
+
provider: options.provider,
|
|
1189
|
+
environment: options.environment,
|
|
1190
|
+
environmentId: options.environmentId,
|
|
1191
|
+
tunnel: options.tunnel,
|
|
1192
|
+
tunnelId: options.tunnelId,
|
|
1193
|
+
vault: options.vault,
|
|
1194
|
+
memoryStores: parseMemoryStores(options.memoryStores),
|
|
1195
|
+
title: options.title
|
|
1196
|
+
});
|
|
1197
|
+
const { agentName, session } = run;
|
|
1198
|
+
log.success(`Session created: ${chalk9.bold(session.id)}`);
|
|
1199
|
+
console.log(` Agent: ${agentName}`);
|
|
1200
|
+
console.log(` Environment: ${session.environment_id}`);
|
|
1201
|
+
if (session.tunnel_id) console.log(` Tunnel: ${session.tunnel_id}`);
|
|
1202
|
+
console.log(` Status: ${session.status}`);
|
|
1203
|
+
if (session.vault_ids.length) console.log(` Vaults: ${session.vault_ids.join(", ")}`);
|
|
1204
|
+
if (session.memory_store_ids.length) console.log(` Memory: ${session.memory_store_ids.join(", ")}`);
|
|
1205
|
+
}
|
|
1206
|
+
async function sessionListCommand(options) {
|
|
1207
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1208
|
+
const { items: summaries, hasMore } = await fetchAllPages(async (page) => {
|
|
1209
|
+
const result = page ? await listSessionSummaries(ctx, {
|
|
1210
|
+
agent: options.agent,
|
|
1211
|
+
provider: options.provider,
|
|
1212
|
+
filter: { page }
|
|
1213
|
+
}) : await listSessionSummaries(ctx, {
|
|
1214
|
+
agent: options.agent,
|
|
1215
|
+
provider: options.provider
|
|
1216
|
+
});
|
|
1217
|
+
return { items: result.summaries, hasMore: result.hasMore, nextPage: result.nextPage };
|
|
1218
|
+
}, options.all);
|
|
1219
|
+
const sessions = summaries.map((summary) => summary.session);
|
|
1220
|
+
if (sessions.length === 0) {
|
|
1221
|
+
log.info("No sessions found.");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const agentNameMap = new Map(
|
|
1225
|
+
summaries.filter((summary) => summary.agentName).map((summary) => [summary.session.id, summary.agentName])
|
|
1226
|
+
);
|
|
1227
|
+
const idWidth = columnWidth(sessions.map((s) => s.id.length));
|
|
1228
|
+
printTableTitle("Sessions", sessions.length);
|
|
1229
|
+
printTableHeader(
|
|
1230
|
+
[
|
|
1231
|
+
"ID".padEnd(idWidth),
|
|
1232
|
+
"Title".padEnd(20),
|
|
1233
|
+
"Agent".padEnd(14),
|
|
1234
|
+
"Status".padEnd(12),
|
|
1235
|
+
"Created".padEnd(20),
|
|
1236
|
+
"Duration"
|
|
1237
|
+
],
|
|
1238
|
+
idWidth + 80
|
|
1239
|
+
);
|
|
1240
|
+
for (const s of sessions) {
|
|
1241
|
+
const id = s.id.padEnd(idWidth);
|
|
1242
|
+
const title = (s.title ?? "").slice(0, 18).padEnd(20);
|
|
1243
|
+
const agent = (agentNameMap.get(s.id) ?? s.agent_id.slice(0, 12)).padEnd(14);
|
|
1244
|
+
const statusText = s.status.padEnd(12);
|
|
1245
|
+
const status = s.status === "idle" ? chalk9.green(statusText) : s.status === "processing" ? chalk9.yellow(statusText) : s.status === "failed" ? chalk9.red(statusText) : chalk9.gray(statusText);
|
|
1246
|
+
const created = formatTimestamp(s.created_at).padEnd(20);
|
|
1247
|
+
const duration = formatDuration(s.created_at, s.status === "idle" ? s.updated_at : void 0);
|
|
1248
|
+
printTableRow([chalk9.bold(id), title, chalk9.cyan(agent), status, chalk9.dim(created), duration]);
|
|
1249
|
+
}
|
|
1250
|
+
printTableFooter();
|
|
1251
|
+
if (hasMore) {
|
|
1252
|
+
log.info("More sessions available. Use --all to fetch all.");
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async function sessionGetCommand(sessionId, options) {
|
|
1256
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1257
|
+
const session = await getSession(ctx, sessionId, options.provider);
|
|
1258
|
+
console.log(` ID: ${chalk9.bold(session.id)}`);
|
|
1259
|
+
console.log(` Agent: ${session.agent_id}`);
|
|
1260
|
+
console.log(` Environment: ${session.environment_id}`);
|
|
1261
|
+
if (session.tunnel_id) console.log(` Tunnel: ${session.tunnel_id}`);
|
|
1262
|
+
console.log(` Status: ${session.status}`);
|
|
1263
|
+
if (session.title) console.log(` Title: ${session.title}`);
|
|
1264
|
+
if (session.vault_ids.length) console.log(` Vaults: ${session.vault_ids.join(", ")}`);
|
|
1265
|
+
if (session.memory_store_ids.length) console.log(` Memory: ${session.memory_store_ids.join(", ")}`);
|
|
1266
|
+
console.log(` Created: ${session.created_at}`);
|
|
1267
|
+
console.log(` Updated: ${session.updated_at}`);
|
|
1268
|
+
}
|
|
1269
|
+
async function sessionDeleteCommand(sessionId, options) {
|
|
1270
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1271
|
+
await deleteSession(ctx, sessionId, options.provider);
|
|
1272
|
+
log.success(`Session ${sessionId} deleted.`);
|
|
1273
|
+
}
|
|
1274
|
+
function shouldRenderLiveEvent(event) {
|
|
1275
|
+
return event.type !== "thinking" && !(event.type === "message" && event.role === "user");
|
|
1276
|
+
}
|
|
1277
|
+
function renderTerminalStatus(status, json) {
|
|
1278
|
+
if (json) return;
|
|
1279
|
+
const color = status === "idle" || status === "completed" ? chalk9.green : chalk9.red;
|
|
1280
|
+
log.plain(color(`
|
|
1281
|
+
[session ${status}]`));
|
|
1282
|
+
}
|
|
1283
|
+
function toEventListJson(events, hasMore, nextPage) {
|
|
1284
|
+
const out = { events: sanitizeSessionEvents(events), has_more: hasMore };
|
|
1285
|
+
if (nextPage !== void 0) out.next_page = nextPage;
|
|
1286
|
+
return out;
|
|
1287
|
+
}
|
|
1288
|
+
function renderEvent(event) {
|
|
1289
|
+
if (!shouldRenderLiveEvent(event)) return;
|
|
1290
|
+
if (event.type === "message" && event.content) {
|
|
1291
|
+
process.stdout.write(event.content);
|
|
1292
|
+
} else if (event.type === "tool_use") {
|
|
1293
|
+
log.plain(chalk9.cyan(`
|
|
1294
|
+
[tool] ${event.tool_name}`));
|
|
1295
|
+
} else if (event.type === "tool_result" && event.content) {
|
|
1296
|
+
const preview = event.content.length > 200 ? `${event.content.slice(0, 200)}...` : event.content;
|
|
1297
|
+
log.plain(chalk9.dim(preview));
|
|
1298
|
+
} else if (event.type === "status") {
|
|
1299
|
+
if (event.status === "running") {
|
|
1300
|
+
log.plain(chalk9.yellow("\n[session running]"));
|
|
1301
|
+
}
|
|
1302
|
+
} else if (event.type === "error") {
|
|
1303
|
+
log.plain(chalk9.red(`
|
|
1304
|
+
[error] ${event.content ?? "unknown error"}`));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async function streamAndRender(events, json) {
|
|
1308
|
+
for await (const event of events) {
|
|
1309
|
+
if (json) {
|
|
1310
|
+
writeJsonLine(sanitizeSessionEvent(event));
|
|
1311
|
+
} else {
|
|
1312
|
+
renderEvent(event);
|
|
1313
|
+
}
|
|
1314
|
+
if (event.type === "status" && isTerminalSessionStatus(event.status)) {
|
|
1315
|
+
renderTerminalStatus(event.status, json);
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function renderCollectedEvents(result, json) {
|
|
1321
|
+
if (json) {
|
|
1322
|
+
writeJson(toEventListJson(result.result.events, result.result.has_more, result.result.next_page));
|
|
1323
|
+
} else {
|
|
1324
|
+
for (const event of result.result.events) {
|
|
1325
|
+
renderEvent(event);
|
|
1326
|
+
}
|
|
1327
|
+
renderTerminalStatus(result.terminalStatus, json);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async function sessionRunCommand(promptOrAgent, promptOrOptions, maybeOptions) {
|
|
1331
|
+
const hasPositionalAgent = typeof promptOrOptions === "string";
|
|
1332
|
+
const positionalAgent = hasPositionalAgent ? promptOrAgent : void 0;
|
|
1333
|
+
const prompt = hasPositionalAgent ? promptOrOptions : promptOrAgent;
|
|
1334
|
+
const options = hasPositionalAgent ? maybeOptions : promptOrOptions ?? maybeOptions;
|
|
1335
|
+
if (positionalAgent && options.agent && positionalAgent !== options.agent) {
|
|
1336
|
+
throw new UserError7("Specify agent either positionally or with --agent, not both.");
|
|
1337
|
+
}
|
|
1338
|
+
const runOptions = {
|
|
1339
|
+
agent: positionalAgent ?? options.agent,
|
|
1340
|
+
identityId: options.identityId,
|
|
1341
|
+
provider: options.provider,
|
|
1342
|
+
environment: options.environment,
|
|
1343
|
+
environmentId: options.environmentId,
|
|
1344
|
+
tunnel: options.tunnel,
|
|
1345
|
+
tunnelId: options.tunnelId,
|
|
1346
|
+
vault: options.vault,
|
|
1347
|
+
memoryStores: parseMemoryStores(options.memoryStores),
|
|
1348
|
+
title: options.title
|
|
1349
|
+
};
|
|
1350
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1351
|
+
const run = options.noStream ? await startSessionRunPolling(ctx, prompt, runOptions) : await startSessionRun(ctx, prompt, runOptions);
|
|
1352
|
+
const session = run.session;
|
|
1353
|
+
if (!options.json) {
|
|
1354
|
+
log.success(`Session created: ${chalk9.bold(session.id)}`);
|
|
1355
|
+
}
|
|
1356
|
+
if (options.noStream) {
|
|
1357
|
+
renderCollectedEvents(run, !!options.json);
|
|
1358
|
+
} else {
|
|
1359
|
+
await streamAndRender(run.events, !!options.json);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function sessionSendCommand(sessionId, message, options) {
|
|
1363
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1364
|
+
if (options.noStream) {
|
|
1365
|
+
const result = await sendSessionMessagePolling(ctx, sessionId, message, { provider: options.provider });
|
|
1366
|
+
renderCollectedEvents(result, !!options.json);
|
|
1367
|
+
} else {
|
|
1368
|
+
const events = await sendSessionMessageStreaming(ctx, sessionId, message, { provider: options.provider });
|
|
1369
|
+
await streamAndRender(events, !!options.json);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function sessionEventsCommand(sessionId, options) {
|
|
1373
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1374
|
+
const {
|
|
1375
|
+
items: events,
|
|
1376
|
+
hasMore,
|
|
1377
|
+
nextPage
|
|
1378
|
+
} = await fetchAllPages(async (page) => {
|
|
1379
|
+
const result = page ? await listSessionEvents(ctx, sessionId, {
|
|
1380
|
+
provider: options.provider,
|
|
1381
|
+
limit: options.limit,
|
|
1382
|
+
page_token: page
|
|
1383
|
+
}) : await listSessionEvents(ctx, sessionId, { provider: options.provider, limit: options.limit });
|
|
1384
|
+
return { items: result.events, hasMore: result.has_more, nextPage: result.next_page };
|
|
1385
|
+
}, options.all);
|
|
1386
|
+
if (options.json) {
|
|
1387
|
+
writeJson(toEventListJson(events, hasMore, nextPage));
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (events.length === 0) {
|
|
1391
|
+
log.info("No events found.");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
printTableTitle("Events", events.length);
|
|
1395
|
+
printTableHeader(["#".padEnd(4), "Type".padEnd(14), "Content"], 60);
|
|
1396
|
+
for (let i = 0; i < events.length; i++) {
|
|
1397
|
+
const e = events[i];
|
|
1398
|
+
const idx = String(i + 1).padEnd(4);
|
|
1399
|
+
const typeLabel = e.type.padEnd(14);
|
|
1400
|
+
let preview = "";
|
|
1401
|
+
if (e.type === "message") preview = (e.content ?? "").slice(0, 60);
|
|
1402
|
+
else if (e.type === "tool_use") preview = e.tool_name ?? "";
|
|
1403
|
+
else if (e.type === "tool_result") preview = (e.content ?? "").slice(0, 60);
|
|
1404
|
+
else if (e.type === "status") preview = `${e.status ?? ""}${e.stop_reason ? ` (${e.stop_reason})` : ""}`;
|
|
1405
|
+
else if (e.type === "error") preview = (e.content ?? "").slice(0, 60);
|
|
1406
|
+
else if (e.type === "thinking") preview = chalk9.dim("(thinking)");
|
|
1407
|
+
else preview = e.raw_type;
|
|
1408
|
+
const typeColor = e.type === "error" ? chalk9.red(typeLabel) : e.type === "status" ? chalk9.yellow(typeLabel) : e.type === "tool_use" ? chalk9.cyan(typeLabel) : typeLabel;
|
|
1409
|
+
printTableRow([chalk9.dim(idx), typeColor, preview]);
|
|
1410
|
+
}
|
|
1411
|
+
printTableFooter();
|
|
1412
|
+
if (hasMore) {
|
|
1413
|
+
log.info("More events available. Use --all to fetch all.");
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function parseMemoryStores(value) {
|
|
1417
|
+
return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/commands/state.ts
|
|
1421
|
+
import { importResource, parseStateAddress, UserError as UserError8 } from "@openagentpack/sdk";
|
|
1422
|
+
import chalk10 from "chalk";
|
|
1423
|
+
async function stateListCommand(options) {
|
|
1424
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1425
|
+
const resources = ctx.state.listResources();
|
|
1426
|
+
if (resources.length === 0) {
|
|
1427
|
+
log.info("No resources tracked in state.");
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
printTableTitle("Managed resources", resources.length);
|
|
1431
|
+
printTableHeader(["TYPE NAME PROVIDER REMOTE ID"], 70);
|
|
1432
|
+
for (const r of resources) {
|
|
1433
|
+
const type = r.address.type.padEnd(14);
|
|
1434
|
+
const name = r.address.name.padEnd(20);
|
|
1435
|
+
const provider = r.address.provider.padEnd(10);
|
|
1436
|
+
const id = (r.remote_id ?? "(local)").slice(0, 30);
|
|
1437
|
+
console.log(` ${type} ${name}${provider} ${chalk10.dim(id)}`);
|
|
1438
|
+
}
|
|
1439
|
+
printTableFooter();
|
|
1440
|
+
}
|
|
1441
|
+
async function stateShowCommand(address, options) {
|
|
1442
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1443
|
+
const parsed = parseStateAddress(address, { requireProvider: false });
|
|
1444
|
+
const found = ctx.state.findResource(parsed);
|
|
1445
|
+
if (!found) throw new UserError8(`Resource not found: ${address}`);
|
|
1446
|
+
console.log(JSON.stringify(found, null, 2));
|
|
1447
|
+
}
|
|
1448
|
+
async function stateRemoveCommand(address, options) {
|
|
1449
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1450
|
+
const parsed = parseStateAddress(address, { requireProvider: false });
|
|
1451
|
+
const found = ctx.state.findResource(parsed);
|
|
1452
|
+
if (!found) throw new UserError8(`Resource not found: ${address}`);
|
|
1453
|
+
ctx.state.removeResource(found.address);
|
|
1454
|
+
await ctx.state.save();
|
|
1455
|
+
log.success(`Removed ${address} from state (remote resource not deleted).`);
|
|
1456
|
+
}
|
|
1457
|
+
async function stateImportCommand(address, remoteId, options) {
|
|
1458
|
+
const ctx = await buildCliRuntime(options.file);
|
|
1459
|
+
const parsed = parseStateAddress(address, { requireProvider: true });
|
|
1460
|
+
await importResource(ctx, parsed, remoteId, { resourceVersion: options.resourceVersion });
|
|
1461
|
+
log.success(`Imported ${address} (remote_id: ${remoteId}) into state.`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/commands/sync.ts
|
|
1465
|
+
import { copyFileSync, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
|
|
1466
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1467
|
+
import { basename as basename2, dirname as dirname3, join, resolve as resolve3 } from "path";
|
|
1468
|
+
import * as p5 from "@clack/prompts";
|
|
1469
|
+
import {
|
|
1470
|
+
resolveSyncProvider,
|
|
1471
|
+
syncProviderResourcesFromContext,
|
|
1472
|
+
syncProviderResourcesFromEnv,
|
|
1473
|
+
UserError as UserError9
|
|
1474
|
+
} from "@openagentpack/sdk";
|
|
1475
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
1476
|
+
var DEFAULT_SYNC_OUTPUT = "agents.synced.yaml";
|
|
1477
|
+
function ensureSyncOutputWritable(outPath, force) {
|
|
1478
|
+
if (force) return;
|
|
1479
|
+
if (fileExistsSync(outPath)) {
|
|
1480
|
+
throw new UserError9(
|
|
1481
|
+
`Output file '${outPath}' already exists. Use --force to overwrite, or -o/--out to write elsewhere.`
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function syncCommand(options) {
|
|
1486
|
+
const outPath = options.out ?? DEFAULT_SYNC_OUTPUT;
|
|
1487
|
+
ensureSyncOutputWritable(outPath, options.force);
|
|
1488
|
+
const configPath = resolve3(options.file);
|
|
1489
|
+
const { provider, result } = fileExistsSync(configPath) ? await syncFromConfig(configPath, options.provider) : await syncFromEnv(options.provider);
|
|
1490
|
+
const baseDir = dirname3(outPath);
|
|
1491
|
+
const removedFiles = options.skipMissingFiles ? removeMissingFileAssociations(result.config, baseDir) : await promptFileAssociation(result.config, baseDir);
|
|
1492
|
+
if (removedFiles.length > 0) {
|
|
1493
|
+
const files = result.config.files ?? {};
|
|
1494
|
+
for (const key of removedFiles) {
|
|
1495
|
+
delete files[key];
|
|
1496
|
+
}
|
|
1497
|
+
if (Object.keys(files).length === 0) {
|
|
1498
|
+
delete result.config.files;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const yamlContent = removedFiles.length > 0 ? await serializeConfig(result.config) : result.yaml;
|
|
1502
|
+
await writeFile3(outPath, yamlContent, "utf8");
|
|
1503
|
+
if (result.skillFiles?.size) {
|
|
1504
|
+
let skillFileCount = 0;
|
|
1505
|
+
for (const [skillName, files] of result.skillFiles) {
|
|
1506
|
+
const skillDir = join(baseDir, "skills", skillName);
|
|
1507
|
+
for (const file of files) {
|
|
1508
|
+
const filePath = join(skillDir, file.relativePath);
|
|
1509
|
+
mkdirSync(dirname3(filePath), { recursive: true });
|
|
1510
|
+
writeFileSync(filePath, file.content);
|
|
1511
|
+
skillFileCount++;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
log.info(`Downloaded ${result.skillFiles.size} skill(s) (${skillFileCount} files) into ./skills/`);
|
|
1515
|
+
}
|
|
1516
|
+
const parts = [];
|
|
1517
|
+
for (const [type, count] of Object.entries(result.counts)) {
|
|
1518
|
+
parts.push(`${count} ${type}(s)`);
|
|
1519
|
+
}
|
|
1520
|
+
log.success(`Synced ${parts.join(", ")} from ${provider} into ${outPath}.`);
|
|
1521
|
+
await promptCustomSkillFiles(result.config, baseDir);
|
|
1522
|
+
if (result.secretPlaceholders?.length) {
|
|
1523
|
+
await promptSecretValues(result.secretPlaceholders);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function removeMissingFileAssociations(config, baseDir) {
|
|
1527
|
+
const files = config.files ?? {};
|
|
1528
|
+
const removed = Object.entries(files).filter(([, decl]) => !fileExistsSync(join(baseDir, decl.source))).map(([key]) => key);
|
|
1529
|
+
if (removed.length > 0) {
|
|
1530
|
+
log.info(`${removed.length} file(s) removed (skipped).`);
|
|
1531
|
+
}
|
|
1532
|
+
return removed;
|
|
1533
|
+
}
|
|
1534
|
+
async function syncFromConfig(configPath, explicitProvider) {
|
|
1535
|
+
const ctx = await buildCliRuntime(configPath);
|
|
1536
|
+
const provider = resolveSyncProvider(ctx.config, explicitProvider);
|
|
1537
|
+
const result = await syncProviderResourcesFromContext(ctx, { provider });
|
|
1538
|
+
return { provider, result };
|
|
1539
|
+
}
|
|
1540
|
+
async function syncFromEnv(explicitProvider) {
|
|
1541
|
+
if (!explicitProvider) {
|
|
1542
|
+
throw new UserError9(
|
|
1543
|
+
"agents sync requires --provider when no config file exists, e.g. `agents sync --provider claude`."
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
ensureCredentials();
|
|
1547
|
+
const result = await syncProviderResourcesFromEnv({ provider: explicitProvider });
|
|
1548
|
+
return { provider: explicitProvider, result };
|
|
1549
|
+
}
|
|
1550
|
+
async function promptSecretValues(placeholders) {
|
|
1551
|
+
p5.note(
|
|
1552
|
+
"Vault credentials contain secret placeholders.\nEnter values below (stored locally in .env, never uploaded). Press Enter to skip.",
|
|
1553
|
+
"Secrets"
|
|
1554
|
+
);
|
|
1555
|
+
const envPath = ".env";
|
|
1556
|
+
const existingEnv = loadExistingEnv(envPath);
|
|
1557
|
+
let written = 0;
|
|
1558
|
+
let skipped = 0;
|
|
1559
|
+
for (const ph of placeholders) {
|
|
1560
|
+
if (existingEnv.has(ph.envVar)) {
|
|
1561
|
+
skipped++;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const value = await p5.password({
|
|
1565
|
+
message: `${ph.vaultName} / ${ph.credentialName} [${ph.envVar}]`
|
|
1566
|
+
});
|
|
1567
|
+
if (p5.isCancel(value)) {
|
|
1568
|
+
log.info("Cancelled. Remaining secrets skipped.");
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1571
|
+
const trimmed = (value ?? "").trim();
|
|
1572
|
+
appendEnvLine(envPath, ph.envVar, trimmed);
|
|
1573
|
+
if (trimmed) {
|
|
1574
|
+
written++;
|
|
1575
|
+
} else {
|
|
1576
|
+
skipped++;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (written > 0 || skipped > 0) {
|
|
1580
|
+
const msg = [];
|
|
1581
|
+
if (written > 0) msg.push(`${written} secret(s) written to .env`);
|
|
1582
|
+
if (skipped > 0) msg.push(`${skipped} skipped`);
|
|
1583
|
+
log.info(msg.join(", ") + ".");
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function loadExistingEnv(path) {
|
|
1587
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1588
|
+
if (!fileExistsSync(path)) return keys;
|
|
1589
|
+
const content = readFileSync2(path, "utf8");
|
|
1590
|
+
for (const line of content.split("\n")) {
|
|
1591
|
+
const trimmed = line.trim();
|
|
1592
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1593
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1594
|
+
if (eqIdx > 0) {
|
|
1595
|
+
keys.add(trimmed.slice(0, eqIdx).trim());
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return keys;
|
|
1599
|
+
}
|
|
1600
|
+
function appendEnvLine(path, key, value) {
|
|
1601
|
+
let content = "";
|
|
1602
|
+
if (fileExistsSync(path)) {
|
|
1603
|
+
content = readFileSync2(path, "utf8");
|
|
1604
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
1605
|
+
content += "\n";
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
content += `${key}=${value}
|
|
1609
|
+
`;
|
|
1610
|
+
writeFileSync(path, content);
|
|
1611
|
+
}
|
|
1612
|
+
async function promptCustomSkillFiles(config, baseDir) {
|
|
1613
|
+
const skills = config.skills ?? {};
|
|
1614
|
+
const missing = [];
|
|
1615
|
+
for (const [key, decl] of Object.entries(skills)) {
|
|
1616
|
+
if (decl.origin !== "custom") continue;
|
|
1617
|
+
const skillName = decl.name ?? key;
|
|
1618
|
+
const skillSource = join(baseDir, decl.source);
|
|
1619
|
+
if (fileExistsSync(skillSource)) {
|
|
1620
|
+
const stat = statSync(skillSource);
|
|
1621
|
+
if (stat.isFile()) continue;
|
|
1622
|
+
if (stat.isDirectory() && readdirSync(skillSource).length > 0) continue;
|
|
1623
|
+
}
|
|
1624
|
+
missing.push({ key, name: skillName, dir: skillSource, decl });
|
|
1625
|
+
}
|
|
1626
|
+
if (missing.length === 0) return;
|
|
1627
|
+
p5.note(
|
|
1628
|
+
"Some custom skills could not be downloaded.\nProvide a local path to the skill directory, or press Enter to create an empty placeholder.",
|
|
1629
|
+
"Skills"
|
|
1630
|
+
);
|
|
1631
|
+
let provided = 0;
|
|
1632
|
+
let skippedCount = 0;
|
|
1633
|
+
for (const skill of missing) {
|
|
1634
|
+
const sourcePath = await p5.text({
|
|
1635
|
+
message: `Skill "${skill.name}" \u2014 local path (or Enter to skip):`,
|
|
1636
|
+
placeholder: "./path/to/skill/"
|
|
1637
|
+
});
|
|
1638
|
+
if (p5.isCancel(sourcePath)) {
|
|
1639
|
+
log.info("Cancelled. Remaining skills skipped.");
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
const trimmed = (sourcePath ?? "").trim();
|
|
1643
|
+
if (trimmed && fileExistsSync(trimmed)) {
|
|
1644
|
+
const stat = statSync(trimmed);
|
|
1645
|
+
if (stat.isDirectory()) {
|
|
1646
|
+
copyDirRecursive(trimmed, skill.dir);
|
|
1647
|
+
provided++;
|
|
1648
|
+
} else if (stat.isFile() && trimmed.endsWith(".zip")) {
|
|
1649
|
+
mkdirSync(skill.dir, { recursive: true });
|
|
1650
|
+
copyFileSync(trimmed, join(skill.dir, basename2(trimmed)));
|
|
1651
|
+
skill.decl.source = `./skills/${skill.name}/${basename2(trimmed)}`;
|
|
1652
|
+
provided++;
|
|
1653
|
+
} else if (stat.isFile()) {
|
|
1654
|
+
mkdirSync(skill.dir, { recursive: true });
|
|
1655
|
+
copyFileSync(trimmed, join(skill.dir, basename2(trimmed)));
|
|
1656
|
+
provided++;
|
|
1657
|
+
} else {
|
|
1658
|
+
createEmptySkill(skill.dir, skill.name);
|
|
1659
|
+
skippedCount++;
|
|
1660
|
+
}
|
|
1661
|
+
} else {
|
|
1662
|
+
createEmptySkill(skill.dir, skill.name);
|
|
1663
|
+
skippedCount++;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (provided > 0 || skippedCount > 0) {
|
|
1667
|
+
const msg = [];
|
|
1668
|
+
if (provided > 0) msg.push(`${provided} skill(s) provided`);
|
|
1669
|
+
if (skippedCount > 0) msg.push(`${skippedCount} skipped (empty placeholder created)`);
|
|
1670
|
+
log.info(msg.join(", ") + ".");
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function createEmptySkill(dir, name) {
|
|
1674
|
+
mkdirSync(dir, { recursive: true });
|
|
1675
|
+
if (!fileExistsSync(join(dir, "SKILL.md"))) {
|
|
1676
|
+
writeFileSync(
|
|
1677
|
+
join(dir, "SKILL.md"),
|
|
1678
|
+
`---
|
|
1679
|
+
name: ${name}
|
|
1680
|
+
description: ""
|
|
1681
|
+
---
|
|
1682
|
+
|
|
1683
|
+
# ${name}
|
|
1684
|
+
|
|
1685
|
+
> **NOTE**: This is a placeholder file generated by \`agents sync\`. It does NOT contain your original skill content.
|
|
1686
|
+
> Please replace this file with the actual skill definition, then run \`agents apply\` to upload it.
|
|
1687
|
+
`
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
function copyDirRecursive(src, dest) {
|
|
1692
|
+
mkdirSync(dest, { recursive: true });
|
|
1693
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
1694
|
+
const srcPath = join(src, entry.name);
|
|
1695
|
+
const destPath = join(dest, entry.name);
|
|
1696
|
+
if (entry.isDirectory()) {
|
|
1697
|
+
copyDirRecursive(srcPath, destPath);
|
|
1698
|
+
} else {
|
|
1699
|
+
copyFileSync(srcPath, destPath);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
async function promptFileAssociation(config, baseDir) {
|
|
1704
|
+
const files = config.files ?? {};
|
|
1705
|
+
const fileEntries = Object.entries(files);
|
|
1706
|
+
if (fileEntries.length === 0) return [];
|
|
1707
|
+
p5.note(
|
|
1708
|
+
"Files cannot be downloaded from the remote platform.\nProvide a local file path, or press Enter to skip (file will be removed from sync).",
|
|
1709
|
+
"Files"
|
|
1710
|
+
);
|
|
1711
|
+
const removed = [];
|
|
1712
|
+
let provided = 0;
|
|
1713
|
+
for (const [key, decl] of fileEntries) {
|
|
1714
|
+
const fileName = decl.name ?? decl.source ?? key;
|
|
1715
|
+
const targetPath = join(baseDir, decl.source);
|
|
1716
|
+
if (fileExistsSync(targetPath)) {
|
|
1717
|
+
provided++;
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
const sourcePath = await p5.text({
|
|
1721
|
+
message: `File "${fileName}" \u2014 local path (or Enter to skip/remove):`,
|
|
1722
|
+
placeholder: "./path/to/file"
|
|
1723
|
+
});
|
|
1724
|
+
if (p5.isCancel(sourcePath)) {
|
|
1725
|
+
log.info("Cancelled. Remaining files skipped and removed.");
|
|
1726
|
+
for (const [remainingKey] of fileEntries.slice(fileEntries.indexOf([key, decl]))) {
|
|
1727
|
+
removed.push(remainingKey);
|
|
1728
|
+
}
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
const trimmed = (sourcePath ?? "").trim();
|
|
1732
|
+
if (trimmed && fileExistsSync(trimmed)) {
|
|
1733
|
+
mkdirSync(dirname3(targetPath), { recursive: true });
|
|
1734
|
+
copyFileSync(trimmed, targetPath);
|
|
1735
|
+
provided++;
|
|
1736
|
+
} else {
|
|
1737
|
+
removed.push(key);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (provided > 0 || removed.length > 0) {
|
|
1741
|
+
const msg = [];
|
|
1742
|
+
if (provided > 0) msg.push(`${provided} file(s) associated`);
|
|
1743
|
+
if (removed.length > 0) msg.push(`${removed.length} removed (skipped)`);
|
|
1744
|
+
log.info(msg.join(", ") + ".");
|
|
1745
|
+
}
|
|
1746
|
+
return removed;
|
|
1747
|
+
}
|
|
1748
|
+
async function serializeConfig(config) {
|
|
1749
|
+
return stringifyYaml(config, { lineWidth: 0 });
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/commands/validate.ts
|
|
1753
|
+
import { resolve as resolve4 } from "path";
|
|
1754
|
+
import { resolveProjectConfig as resolveProjectConfig2, UserError as UserError10, validateProjectConfig } from "@openagentpack/sdk";
|
|
1755
|
+
async function validateCommand(options) {
|
|
1756
|
+
ensureCredentials();
|
|
1757
|
+
const configPath = resolve4(options.file);
|
|
1758
|
+
log.info(`Validating ${configPath}...`);
|
|
1759
|
+
const { config } = await resolveProjectConfig2(options.file);
|
|
1760
|
+
const diagnostics = validateProjectConfig(config);
|
|
1761
|
+
for (const d of diagnostics) {
|
|
1762
|
+
const line = d.resource ? `${d.message} (${d.resource.type}.${d.resource.name})` : d.message;
|
|
1763
|
+
if (d.severity === "error") log.error(line);
|
|
1764
|
+
else if (d.severity === "warning") log.warn(line);
|
|
1765
|
+
else log.info(line);
|
|
1766
|
+
}
|
|
1767
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
1768
|
+
if (errorCount > 0) {
|
|
1769
|
+
throw new UserError10(`Validation failed with ${errorCount} error(s).`);
|
|
1770
|
+
}
|
|
1771
|
+
log.success("Configuration is valid.");
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/program.ts
|
|
1775
|
+
function formatCliError(message, args = process.argv.slice(2)) {
|
|
1776
|
+
const trimmed = message.trimEnd();
|
|
1777
|
+
if (trimmed.startsWith("error: unknown option")) {
|
|
1778
|
+
return `${trimmed}
|
|
1779
|
+
|
|
1780
|
+
Run \`agents --help\` for available commands, or \`agents <command> --help\` for command options.
|
|
1781
|
+
`;
|
|
1782
|
+
}
|
|
1783
|
+
if (trimmed.includes("missing required argument")) {
|
|
1784
|
+
const cmd = args.filter((a) => !a.startsWith("-")).join(" ");
|
|
1785
|
+
const examples = {
|
|
1786
|
+
"session run": 'agents session run "your prompt here" -f agents.yaml',
|
|
1787
|
+
"session send": 'agents session send <session-id> "your message" -f agents.yaml',
|
|
1788
|
+
"session get": "agents session get <session-id> -f agents.yaml",
|
|
1789
|
+
"session delete": "agents session delete <session-id> -f agents.yaml"
|
|
1790
|
+
};
|
|
1791
|
+
const example = Object.entries(examples).find(([k]) => cmd.startsWith(k));
|
|
1792
|
+
if (example) {
|
|
1793
|
+
return `${trimmed}
|
|
1794
|
+
|
|
1795
|
+
Example:
|
|
1796
|
+
${example[1]}
|
|
1797
|
+
`;
|
|
1798
|
+
}
|
|
1799
|
+
return `${trimmed}
|
|
1800
|
+
|
|
1801
|
+
Run \`agents ${cmd} --help\` for usage details.
|
|
1802
|
+
`;
|
|
1803
|
+
}
|
|
1804
|
+
return `${message}`;
|
|
1805
|
+
}
|
|
1806
|
+
function readCliVersion() {
|
|
1807
|
+
const packageJsonPath = resolve5(dirname4(fileURLToPath2(import.meta.url)), "../package.json");
|
|
1808
|
+
const manifest = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
|
|
1809
|
+
return manifest.version ?? "0.0.0-dev";
|
|
1810
|
+
}
|
|
1811
|
+
function countVerbose(_value, previous) {
|
|
1812
|
+
return previous + 1;
|
|
1813
|
+
}
|
|
1814
|
+
var program = new Command2().name("agents").version(readCliVersion()).description("Open Agent Pack \u2014 Declaratively manage AI agent infrastructure").option("-v, --verbose", "Increase logging verbosity (repeat: -vv)", countVerbose, 0).option("-q, --quiet", "Suppress non-error output").option("--no-color", "Disable colored output").addOption(configFileOption().default(DEFAULT_CONFIG_FILE)).configureOutput({
|
|
1815
|
+
outputError: (message, write) => write(formatCliError(message))
|
|
1816
|
+
}).hook("preAction", (cmd) => {
|
|
1817
|
+
const opts = cmd.opts();
|
|
1818
|
+
configureLogger({
|
|
1819
|
+
verbose: typeof opts.verbose === "number" ? opts.verbose : opts.verbose ? 1 : 0,
|
|
1820
|
+
quiet: !!opts.quiet,
|
|
1821
|
+
color: opts.color !== false
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
program.command("init").description("Create a new agents.yaml template").action(initCommand);
|
|
1825
|
+
program.command("playground").description("Launch the local web UI (fetches @openagentpack/playground on demand) and open it in a browser").option("--port <n>", "Port to serve on (default 4848)").addOption(providerOption("Provider the UI targets")).option("--no-open", "Do not open a browser automatically").action(playgroundCommand);
|
|
1826
|
+
program.command("validate").description("Validate the configuration file (offline)").addOption(configFileOption()).action(withResolvedConfigFile(validateCommand));
|
|
1827
|
+
program.command("plan").description("Show what changes would be applied").addOption(configFileOption()).addOption(providerOption("Target provider", { allowAll: true, defaultValue: "all" })).option("--refresh <value>", "Refresh state from remote before planning (true/false)", parseBooleanOption, true).option("--refresh-only", "Refresh state and show drift without planning remote mutations").option("--json", "Output as JSON").action(withResolvedConfigFile(planCommand));
|
|
1828
|
+
program.command("apply").description("Apply the planned changes to create/update/delete resources").addOption(configFileOption()).option("-y, --yes", "Skip confirmation prompt").option("--refresh <value>", "Refresh state from remote before planning (true/false)", parseBooleanOption, true).option("--refresh-only", "Refresh state without mutating remote resources").option(
|
|
1829
|
+
"--concurrency <n>",
|
|
1830
|
+
"Max independent resources to apply in parallel (default 6, max 10)",
|
|
1831
|
+
parsePositiveInteger
|
|
1832
|
+
).addOption(providerOption("Target provider", { allowAll: true, defaultValue: "all" })).action(withResolvedConfigFile(applyCommand));
|
|
1833
|
+
program.command("destroy").description("Destroy all managed resources").addOption(configFileOption()).option("-y, --yes", "Skip confirmation prompt").option("--cascade", "Auto-delete dependent resources (e.g., sessions referencing an environment)").action(withResolvedConfigFile(destroyCommand));
|
|
1834
|
+
program.command("sync").description("Export a provider's remote configuration into a local agents.yaml").addOption(configFileOption()).addOption(providerOption("Source provider to sync from (defaults from config when -f is set)")).option("-o, --out <path>", "Output file path", "agents.synced.yaml").option("--force", "Overwrite the output file if it already exists").option("--skip-missing-files", "Do not prompt for remote files that cannot be downloaded; omit them from output").action(withResolvedConfigFile(syncCommand));
|
|
1835
|
+
program.command("migrate").description("Merge synced resources into the project agents.yaml (incremental, skip existing)").option("--from <path>", "Source synced file", "agents.synced.yaml").option("--to <path>", "Target agents.yaml file", "agents.yaml").action(migrateCommand);
|
|
1836
|
+
var stateCmd = program.command("state").description("Manage state file");
|
|
1837
|
+
stateCmd.command("list").description("List all resources in state").addOption(configFileOption()).action(withResolvedConfigFile(stateListCommand));
|
|
1838
|
+
stateCmd.command("show <address>").description("Show details of a resource in state").addOption(configFileOption()).action(withResolvedConfigFile(stateShowCommand));
|
|
1839
|
+
stateCmd.command("rm <address>").description("Remove a resource from state without destroying it remotely").addOption(configFileOption()).action(withResolvedConfigFile(stateRemoveCommand));
|
|
1840
|
+
stateCmd.command("import <address> <remote-id>").description("Import an existing remote resource into state").addOption(configFileOption()).addOption(
|
|
1841
|
+
new Option2("--resource-version <number>", "Resource version (for versioned resources like agents)").argParser(
|
|
1842
|
+
parsePositiveInteger
|
|
1843
|
+
)
|
|
1844
|
+
).action(withResolvedConfigFile(stateImportCommand));
|
|
1845
|
+
var sessionCmd = program.command("session").description("Manage agent sessions (runtime)");
|
|
1846
|
+
sessionCmd.command("create [agent-name]").description("Create a new session for an agent").addOption(configFileOption()).option("--agent <name>", "Agent name (auto-detected when only one agent is configured)").option("--identity-id <id>", "Override the configured Qoder Forward Identity").option("--environment <name>", "Override agent's declared environment").option("--environment-id <id>", "Use an explicit remote environment id instead of the configured one").option("--tunnel <name>", "Override agent's declared tunnel").option("--tunnel-id <id>", "Use an explicit remote tunnel id instead of the configured one").option("--vault <name>", "Override agent's declared vault").option("--memory-stores <names>", "Override agent's declared memory stores (comma-separated)").option("--title <title>", "Session title").addOption(providerOption("Target provider (required for multi-provider agents)")).action(withResolvedConfigFile(sessionCreateCommand));
|
|
1847
|
+
sessionCmd.command("list").description("List sessions from the provider").addOption(configFileOption()).option("--agent <name>", "Filter by agent name").option("--all", "Fetch all pages by following the cursor").addOption(providerOption("Target provider")).action(withResolvedConfigFile(sessionListCommand));
|
|
1848
|
+
sessionCmd.command("get <session-id>").description("Get details of a session").addOption(configFileOption()).addOption(providerOption("Target provider")).action(withResolvedConfigFile(sessionGetCommand));
|
|
1849
|
+
sessionCmd.command("delete <session-id>").description("Delete a session").addOption(configFileOption()).addOption(providerOption("Target provider")).action(withResolvedConfigFile(sessionDeleteCommand));
|
|
1850
|
+
sessionCmd.command("run <prompt-or-agent> [prompt]").description("Create a session, send a message, and stream the response").addOption(configFileOption()).option("--agent <name>", "Agent name (auto-detected when only one agent is configured)").option("--identity-id <id>", "Override the configured Qoder Forward Identity").option("--environment <name>", "Override agent's declared environment").option("--environment-id <id>", "Use an explicit remote environment id instead of the configured one").option("--tunnel <name>", "Override agent's declared tunnel").option("--tunnel-id <id>", "Use an explicit remote tunnel id instead of the configured one").option("--vault <name>", "Override agent's declared vault").option("--memory-stores <names>", "Override agent's declared memory stores (comma-separated)").option("--title <title>", "Session title").addOption(providerOption("Target provider")).option("--json", "Output events as JSONL").option("--no-stream", "Use polling instead of SSE streaming").action(withResolvedConfigFile(sessionRunCommand));
|
|
1851
|
+
sessionCmd.command("send <session-id> <message>").description("Send a message to an existing session and stream the response").addOption(configFileOption()).addOption(providerOption("Target provider")).option("--json", "Output events as JSONL").option("--no-stream", "Use polling instead of SSE streaming").action(withResolvedConfigFile(sessionSendCommand));
|
|
1852
|
+
sessionCmd.command("events <session-id>").description("List event history for a session").addOption(configFileOption()).addOption(providerOption("Target provider")).addOption(new Option2("--limit <count>", "Maximum number of events to fetch").argParser(parsePositiveInteger)).option("--all", "Fetch all pages by following the cursor").option("--json", "Output as JSON").action(withResolvedConfigFile(sessionEventsCommand));
|
|
1853
|
+
var deploymentCmd = program.command("deployment").description("Manage agent deployments (scheduled / triggered runs)");
|
|
1854
|
+
deploymentCmd.command("list").description("List deployments tracked in state").addOption(configFileOption()).addOption(providerOption("Filter by provider")).action(withResolvedConfigFile(deploymentListCommand));
|
|
1855
|
+
deploymentCmd.command("get <name>").description("Show a deployment's status and resolved bindings").addOption(configFileOption()).addOption(providerOption("Target provider")).action(withResolvedConfigFile(deploymentGetCommand));
|
|
1856
|
+
deploymentCmd.command("run <name>").description("Trigger a deployment run (native on Claude, emulated as a session on Qoder)").addOption(configFileOption()).addOption(providerOption("Target provider")).action(withResolvedConfigFile(deploymentRunCommand));
|
|
1857
|
+
var modelsCmd = program.command("models").description("Discover available models from providers");
|
|
1858
|
+
modelsCmd.command("list").description("List models available on the configured provider(s)").addOption(configFileOption()).addOption(providerOption("Target provider")).option("--json", "Output as JSON").action(withResolvedConfigFile(modelsListCommand));
|
|
1859
|
+
|
|
1860
|
+
export {
|
|
1861
|
+
configureLogger,
|
|
1862
|
+
log,
|
|
1863
|
+
program
|
|
1864
|
+
};
|