@astroanywhere/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/chunk-PYFBZGQG.js +351 -0
- package/dist/client.js +10 -0
- package/dist/index.js +2076 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2076 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearAuth,
|
|
3
|
+
getClient,
|
|
4
|
+
getServerUrl,
|
|
5
|
+
loadConfig,
|
|
6
|
+
resetConfig,
|
|
7
|
+
saveConfig,
|
|
8
|
+
streamDispatchToStdout
|
|
9
|
+
} from "./chunk-PYFBZGQG.js";
|
|
10
|
+
|
|
11
|
+
// src/index.ts
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
|
|
14
|
+
// src/output.ts
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
function formatTable(rows, columns) {
|
|
17
|
+
if (rows.length === 0) return chalk.dim(" No results.");
|
|
18
|
+
const widths = columns.map((col) => {
|
|
19
|
+
const headerLen = col.label.length;
|
|
20
|
+
const maxDataLen = rows.reduce((max, row) => {
|
|
21
|
+
const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
|
|
22
|
+
return Math.max(max, val.length);
|
|
23
|
+
}, 0);
|
|
24
|
+
return col.width ?? Math.max(headerLen, Math.min(maxDataLen, 50));
|
|
25
|
+
});
|
|
26
|
+
const header = columns.map((col, i) => col.label.padEnd(widths[i])).join(" ");
|
|
27
|
+
const separator = columns.map((_, i) => "\u2500".repeat(widths[i])).join("\u2500\u2500");
|
|
28
|
+
const dataRows = rows.map(
|
|
29
|
+
(row) => columns.map((col, i) => {
|
|
30
|
+
const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
|
|
31
|
+
return val.slice(0, widths[i]).padEnd(widths[i]);
|
|
32
|
+
}).join(" ")
|
|
33
|
+
);
|
|
34
|
+
return [chalk.bold(header), chalk.dim(separator), ...dataRows].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function formatJson(data) {
|
|
37
|
+
return JSON.stringify(data, null, 2);
|
|
38
|
+
}
|
|
39
|
+
function print(data, opts) {
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
console.log(formatJson(data));
|
|
42
|
+
} else if (opts.columns && Array.isArray(data)) {
|
|
43
|
+
console.log(formatTable(data, opts.columns));
|
|
44
|
+
} else {
|
|
45
|
+
console.log(formatJson(data));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function formatRelativeTime(date) {
|
|
49
|
+
if (!date) return chalk.dim("\u2014");
|
|
50
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const diff = now - d.getTime();
|
|
53
|
+
const seconds = Math.floor(diff / 1e3);
|
|
54
|
+
const minutes = Math.floor(seconds / 60);
|
|
55
|
+
const hours = Math.floor(minutes / 60);
|
|
56
|
+
const days = Math.floor(hours / 24);
|
|
57
|
+
if (seconds < 60) return "just now";
|
|
58
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
59
|
+
if (hours < 24) return `${hours}h ago`;
|
|
60
|
+
if (days < 30) return `${days}d ago`;
|
|
61
|
+
return d.toLocaleDateString();
|
|
62
|
+
}
|
|
63
|
+
function parseDateFilter(value) {
|
|
64
|
+
const trimmed = value.trim().toLowerCase();
|
|
65
|
+
if (trimmed === "today") {
|
|
66
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
67
|
+
d2.setHours(0, 0, 0, 0);
|
|
68
|
+
return d2;
|
|
69
|
+
}
|
|
70
|
+
if (trimmed === "yesterday") {
|
|
71
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
72
|
+
d2.setDate(d2.getDate() - 1);
|
|
73
|
+
d2.setHours(0, 0, 0, 0);
|
|
74
|
+
return d2;
|
|
75
|
+
}
|
|
76
|
+
const relMatch = trimmed.match(/^(\d+)\s*(m|h|d|w)$/);
|
|
77
|
+
if (relMatch) {
|
|
78
|
+
const amount = parseInt(relMatch[1], 10);
|
|
79
|
+
const unit = relMatch[2];
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const ms = {
|
|
82
|
+
m: 60 * 1e3,
|
|
83
|
+
h: 60 * 60 * 1e3,
|
|
84
|
+
d: 24 * 60 * 60 * 1e3,
|
|
85
|
+
w: 7 * 24 * 60 * 60 * 1e3
|
|
86
|
+
};
|
|
87
|
+
return new Date(now - amount * ms[unit]);
|
|
88
|
+
}
|
|
89
|
+
const d = new Date(value);
|
|
90
|
+
if (isNaN(d.getTime())) {
|
|
91
|
+
throw new Error(`Invalid date filter: "${value}". Use relative (2d, 1h, 30m, 1w), named (today, yesterday), or ISO format.`);
|
|
92
|
+
}
|
|
93
|
+
return d;
|
|
94
|
+
}
|
|
95
|
+
function formatDuration(ms) {
|
|
96
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
97
|
+
const totalSeconds = ms / 1e3;
|
|
98
|
+
if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;
|
|
99
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
100
|
+
const seconds = Math.round(totalSeconds % 60);
|
|
101
|
+
if (minutes < 60) {
|
|
102
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
103
|
+
}
|
|
104
|
+
const hours = Math.floor(minutes / 60);
|
|
105
|
+
const remainingMinutes = minutes % 60;
|
|
106
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
107
|
+
}
|
|
108
|
+
function formatStatus(status) {
|
|
109
|
+
const colors = {
|
|
110
|
+
active: chalk.green,
|
|
111
|
+
completed: chalk.blue,
|
|
112
|
+
planned: chalk.yellow,
|
|
113
|
+
in_progress: chalk.cyan,
|
|
114
|
+
dispatched: chalk.cyan,
|
|
115
|
+
auto_verified: chalk.green,
|
|
116
|
+
awaiting_approval: chalk.magenta,
|
|
117
|
+
awaiting_judgment: chalk.magenta,
|
|
118
|
+
pruned: chalk.dim,
|
|
119
|
+
archived: chalk.dim,
|
|
120
|
+
running: chalk.cyan,
|
|
121
|
+
success: chalk.green,
|
|
122
|
+
failure: chalk.red,
|
|
123
|
+
cancelled: chalk.dim,
|
|
124
|
+
pending: chalk.yellow
|
|
125
|
+
};
|
|
126
|
+
const colorFn = colors[status] ?? chalk.white;
|
|
127
|
+
return colorFn(status);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/commands/project.ts
|
|
131
|
+
import chalk2 from "chalk";
|
|
132
|
+
var projectColumns = [
|
|
133
|
+
{ key: "id", label: "ID", width: 8, format: (v) => String(v ?? "").slice(0, 8) },
|
|
134
|
+
{ key: "name", label: "NAME", width: 30 },
|
|
135
|
+
{ key: "status", label: "STATUS", width: 12, format: (v) => formatStatus(String(v ?? "")) },
|
|
136
|
+
{ key: "workingDirectory", label: "WORK DIR", width: 30, format: (v) => v ? String(v) : chalk2.dim("\u2014") },
|
|
137
|
+
{ key: "updatedAt", label: "UPDATED", width: 12, format: (v) => formatRelativeTime(v) }
|
|
138
|
+
];
|
|
139
|
+
function registerProjectCommands(program2) {
|
|
140
|
+
const project = program2.command("project").description("Manage projects");
|
|
141
|
+
project.command("list").description("List all projects").action(async () => {
|
|
142
|
+
const opts = program2.opts();
|
|
143
|
+
const client = getClient(opts.serverUrl);
|
|
144
|
+
const rows = await client.listProjects();
|
|
145
|
+
print(rows, { json: opts.json, columns: projectColumns });
|
|
146
|
+
});
|
|
147
|
+
project.command("show <id>").description("Show project details").action(async (id) => {
|
|
148
|
+
const opts = program2.opts();
|
|
149
|
+
const client = getClient(opts.serverUrl);
|
|
150
|
+
let p;
|
|
151
|
+
try {
|
|
152
|
+
p = await client.resolveProject(id);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(chalk2.red(err.message));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
if (opts.json) {
|
|
158
|
+
print(p, { json: true });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const fields = [
|
|
162
|
+
["ID", p.id],
|
|
163
|
+
["Name", p.name],
|
|
164
|
+
["Status", formatStatus(p.status)],
|
|
165
|
+
["Description", p.description || chalk2.dim("\u2014")],
|
|
166
|
+
["Working Directory", p.workingDirectory || chalk2.dim("\u2014")],
|
|
167
|
+
["Source Directory", p.sourceDirectory || chalk2.dim("\u2014")],
|
|
168
|
+
["Repository", p.repository || chalk2.dim("\u2014")],
|
|
169
|
+
["Delivery Mode", p.deliveryMode || chalk2.dim("\u2014")],
|
|
170
|
+
["Health", p.health || chalk2.dim("\u2014")],
|
|
171
|
+
["Progress", `${p.progress ?? 0}%`],
|
|
172
|
+
["Start Date", p.startDate || chalk2.dim("\u2014")],
|
|
173
|
+
["Target Date", p.targetDate || chalk2.dim("\u2014")],
|
|
174
|
+
["Lead", p.lead || chalk2.dim("\u2014")],
|
|
175
|
+
["Default Environment", p.defaultEnvironment || chalk2.dim("\u2014")],
|
|
176
|
+
["Default Machine ID", p.defaultMachineId || chalk2.dim("\u2014")],
|
|
177
|
+
["Created", formatRelativeTime(p.createdAt)],
|
|
178
|
+
["Updated", formatRelativeTime(p.updatedAt)]
|
|
179
|
+
];
|
|
180
|
+
const maxKeyLen = Math.max(...fields.map(([k]) => k.length));
|
|
181
|
+
for (const [key, value] of fields) {
|
|
182
|
+
console.log(` ${chalk2.bold(key.padEnd(maxKeyLen))} ${value}`);
|
|
183
|
+
}
|
|
184
|
+
if (p.visionDoc) {
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(chalk2.bold(" Vision Document:"));
|
|
187
|
+
console.log(` ${p.visionDoc.slice(0, 500)}${p.visionDoc.length > 500 ? "..." : ""}`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
project.command("create").description("Create a new project").requiredOption("--name <name>", "Project name").option("--description <desc>", "Project description", "").option("--dir <path>", "Working directory").action(async (cmdOpts) => {
|
|
191
|
+
const opts = program2.opts();
|
|
192
|
+
const client = getClient(opts.serverUrl);
|
|
193
|
+
const created = await client.createProject({
|
|
194
|
+
name: cmdOpts.name,
|
|
195
|
+
description: cmdOpts.description,
|
|
196
|
+
workingDirectory: cmdOpts.dir
|
|
197
|
+
});
|
|
198
|
+
if (opts.json) {
|
|
199
|
+
print(created, { json: true });
|
|
200
|
+
} else {
|
|
201
|
+
console.log(chalk2.green("Project created:"));
|
|
202
|
+
console.log(` ${chalk2.bold("ID")} ${created.id}`);
|
|
203
|
+
console.log(` ${chalk2.bold("Name")} ${created.name}`);
|
|
204
|
+
if (created.workingDirectory) {
|
|
205
|
+
console.log(` ${chalk2.bold("Dir")} ${created.workingDirectory}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
project.command("update <id>").description("Update a project").option("--name <name>", "New project name").option("--description <desc>", "New description").option("--status <status>", "New status").option("--dir <path>", "New working directory").option("--vision-doc <text>", "Update vision document").action(async (id, cmdOpts) => {
|
|
210
|
+
const opts = program2.opts();
|
|
211
|
+
const client = getClient(opts.serverUrl);
|
|
212
|
+
let p;
|
|
213
|
+
try {
|
|
214
|
+
p = await client.resolveProject(id);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(chalk2.red(err.message));
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const patch = {};
|
|
221
|
+
if (cmdOpts.name !== void 0) patch.name = cmdOpts.name;
|
|
222
|
+
if (cmdOpts.description !== void 0) patch.description = cmdOpts.description;
|
|
223
|
+
if (cmdOpts.status !== void 0) patch.status = cmdOpts.status;
|
|
224
|
+
if (cmdOpts.dir !== void 0) patch.workingDirectory = cmdOpts.dir;
|
|
225
|
+
if (cmdOpts.visionDoc !== void 0) patch.visionDoc = cmdOpts.visionDoc;
|
|
226
|
+
if (Object.keys(patch).length === 0) {
|
|
227
|
+
console.error(chalk2.red("No update fields provided. Use --name, --description, --status, --dir, or --vision-doc."));
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const updated = await client.updateProject(p.id, patch);
|
|
233
|
+
if (opts.json) {
|
|
234
|
+
print(updated, { json: true });
|
|
235
|
+
} else {
|
|
236
|
+
console.log(chalk2.green(`Project "${updated.name}" updated: ${Object.keys(patch).join(", ")}`));
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
240
|
+
if (opts.json) {
|
|
241
|
+
print({ error: msg }, { json: true });
|
|
242
|
+
} else {
|
|
243
|
+
console.error(chalk2.red(`Update failed: ${msg}`));
|
|
244
|
+
}
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
project.command("stats <id>").description("Show project statistics").action(async (id) => {
|
|
249
|
+
const opts = program2.opts();
|
|
250
|
+
const client = getClient(opts.serverUrl);
|
|
251
|
+
let p;
|
|
252
|
+
try {
|
|
253
|
+
p = await client.resolveProject(id);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(chalk2.red(err.message));
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const { nodes, edges } = await client.getPlan(p.id);
|
|
261
|
+
const active = nodes.filter((n) => !n.deletedAt);
|
|
262
|
+
const executions = await client.getExecutions();
|
|
263
|
+
const statusCounts = {};
|
|
264
|
+
for (const n of active) {
|
|
265
|
+
statusCounts[n.status] = (statusCounts[n.status] || 0) + 1;
|
|
266
|
+
}
|
|
267
|
+
const projectExecs = Object.values(executions).filter((e) => e.projectId === p.id);
|
|
268
|
+
const successCount = projectExecs.filter((e) => e.status === "success").length;
|
|
269
|
+
const totalTokens = projectExecs.reduce((sum, e) => sum + (e.tokensUsed ?? 0), 0);
|
|
270
|
+
const totalCost = projectExecs.reduce((sum, e) => sum + (e.estimatedCostUsd ?? 0), 0);
|
|
271
|
+
const stats = {
|
|
272
|
+
project: { id: p.id, name: p.name, status: p.status },
|
|
273
|
+
plan: {
|
|
274
|
+
totalNodes: active.length,
|
|
275
|
+
totalEdges: edges.length,
|
|
276
|
+
statusCounts
|
|
277
|
+
},
|
|
278
|
+
executions: {
|
|
279
|
+
total: projectExecs.length,
|
|
280
|
+
successRate: projectExecs.length > 0 ? `${Math.round(successCount / projectExecs.length * 100)}%` : "\u2014",
|
|
281
|
+
totalTokens,
|
|
282
|
+
totalCost: `$${totalCost.toFixed(4)}`
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
if (opts.json) {
|
|
286
|
+
print(stats, { json: true });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(chalk2.bold(` Project: ${p.name}`));
|
|
291
|
+
console.log(chalk2.dim(" " + "\u2500".repeat(50)));
|
|
292
|
+
console.log(` ${chalk2.dim("Status:")} ${formatStatus(p.status)}`);
|
|
293
|
+
console.log(` ${chalk2.dim("Progress:")} ${p.progress ?? 0}%`);
|
|
294
|
+
console.log();
|
|
295
|
+
console.log(chalk2.bold(" Plan"));
|
|
296
|
+
console.log(` ${chalk2.dim("Nodes:")} ${active.length}`);
|
|
297
|
+
console.log(` ${chalk2.dim("Edges:")} ${edges.length}`);
|
|
298
|
+
for (const [status, count] of Object.entries(statusCounts).sort((a, b) => b[1] - a[1])) {
|
|
299
|
+
console.log(` ${formatStatus(status).padEnd(25)} ${count}`);
|
|
300
|
+
}
|
|
301
|
+
console.log();
|
|
302
|
+
console.log(chalk2.bold(" Executions"));
|
|
303
|
+
console.log(` ${chalk2.dim("Total:")} ${projectExecs.length}`);
|
|
304
|
+
console.log(` ${chalk2.dim("Success Rate:")} ${stats.executions.successRate}`);
|
|
305
|
+
console.log(` ${chalk2.dim("Tokens:")} ${totalTokens.toLocaleString()}`);
|
|
306
|
+
console.log(` ${chalk2.dim("Cost:")} ${stats.executions.totalCost}`);
|
|
307
|
+
console.log();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.error(chalk2.red(err.message));
|
|
310
|
+
process.exitCode = 1;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
project.command("delete <id>").description("Delete a project").action(async (id) => {
|
|
314
|
+
const opts = program2.opts();
|
|
315
|
+
const client = getClient(opts.serverUrl);
|
|
316
|
+
let p;
|
|
317
|
+
try {
|
|
318
|
+
p = await client.resolveProject(id);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.error(chalk2.red(err.message));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
console.log(chalk2.yellow(`Deleting project: ${p.name} (${p.id})`));
|
|
324
|
+
await client.deleteProject(p.id);
|
|
325
|
+
if (opts.json) {
|
|
326
|
+
print({ deleted: true, id: p.id, name: p.name }, { json: true });
|
|
327
|
+
} else {
|
|
328
|
+
console.log(chalk2.green("Project deleted."));
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/commands/plan.ts
|
|
334
|
+
import chalk3 from "chalk";
|
|
335
|
+
var nodeColumns = [
|
|
336
|
+
{ key: "id", label: "ID", width: 20 },
|
|
337
|
+
{ key: "title", label: "TITLE", width: 40 },
|
|
338
|
+
{ key: "type", label: "TYPE", width: 10 },
|
|
339
|
+
{ key: "status", label: "STATUS", width: 18, format: (v) => formatStatus(String(v ?? "")) },
|
|
340
|
+
{ key: "startDate", label: "START", width: 12, format: (v) => v ? String(v) : chalk3.dim("\u2014") },
|
|
341
|
+
{ key: "endDate", label: "END", width: 12, format: (v) => v ? String(v) : chalk3.dim("\u2014") }
|
|
342
|
+
];
|
|
343
|
+
function buildTree(nodes, edges) {
|
|
344
|
+
const adj = /* @__PURE__ */ new Map();
|
|
345
|
+
const hasParent = /* @__PURE__ */ new Set();
|
|
346
|
+
for (const edge of edges) {
|
|
347
|
+
const children = adj.get(edge.source) ?? [];
|
|
348
|
+
children.push(edge.target);
|
|
349
|
+
adj.set(edge.source, children);
|
|
350
|
+
hasParent.add(edge.target);
|
|
351
|
+
}
|
|
352
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
353
|
+
for (const node of nodes) {
|
|
354
|
+
lookup.set(node.id, node);
|
|
355
|
+
}
|
|
356
|
+
function buildSubtree(nodeId) {
|
|
357
|
+
const node = lookup.get(nodeId);
|
|
358
|
+
if (!node) return null;
|
|
359
|
+
const childIds = adj.get(nodeId) ?? [];
|
|
360
|
+
const children = [];
|
|
361
|
+
for (const childId of childIds) {
|
|
362
|
+
const child = buildSubtree(childId);
|
|
363
|
+
if (child) children.push(child);
|
|
364
|
+
}
|
|
365
|
+
return { ...node, children };
|
|
366
|
+
}
|
|
367
|
+
const roots = [];
|
|
368
|
+
for (const node of nodes) {
|
|
369
|
+
if (!hasParent.has(node.id)) {
|
|
370
|
+
const tree = buildSubtree(node.id);
|
|
371
|
+
if (tree) roots.push(tree);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return roots;
|
|
375
|
+
}
|
|
376
|
+
function renderTreeLines(roots) {
|
|
377
|
+
const lines = [];
|
|
378
|
+
function walk(node, prefix, isLast) {
|
|
379
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
380
|
+
const statusStr = formatStatus(node.status);
|
|
381
|
+
const typeTag = chalk3.dim(`[${node.type}]`);
|
|
382
|
+
lines.push(`${prefix}${connector}${chalk3.bold(node.id)}: ${node.title} ${typeTag} ${statusStr}`);
|
|
383
|
+
const childPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
384
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
385
|
+
walk(node.children[i], childPrefix, i === node.children.length - 1);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (let i = 0; i < roots.length; i++) {
|
|
389
|
+
walk(roots[i], "", i === roots.length - 1);
|
|
390
|
+
}
|
|
391
|
+
return lines;
|
|
392
|
+
}
|
|
393
|
+
function registerPlanCommands(program2) {
|
|
394
|
+
const plan = program2.command("plan").description("Manage plans");
|
|
395
|
+
plan.command("list").description("List plan nodes for a project").requiredOption("--project-id <id>", "Project ID").action(async (cmdOpts) => {
|
|
396
|
+
const opts = program2.opts();
|
|
397
|
+
const client = getClient(opts.serverUrl);
|
|
398
|
+
try {
|
|
399
|
+
const { nodes } = await client.getPlan(cmdOpts.projectId);
|
|
400
|
+
print(nodes, { json: opts.json, columns: nodeColumns });
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.error(chalk3.red(err.message));
|
|
403
|
+
process.exitCode = 1;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
plan.command("show <nodeId>").description("Show plan node details").option("--project-id <id>", "Project ID (narrows search)").action(async (nodeId, cmdOpts) => {
|
|
407
|
+
const opts = program2.opts();
|
|
408
|
+
const client = getClient(opts.serverUrl);
|
|
409
|
+
let nodes;
|
|
410
|
+
try {
|
|
411
|
+
const result = cmdOpts.projectId ? await client.getPlan(cmdOpts.projectId) : await client.getFullPlan();
|
|
412
|
+
nodes = result.nodes;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
console.error(chalk3.red(err.message));
|
|
415
|
+
process.exitCode = 1;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
419
|
+
if (!node) {
|
|
420
|
+
console.error(chalk3.red(`No plan node found with ID "${nodeId}"`));
|
|
421
|
+
process.exitCode = 1;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (opts.json) {
|
|
425
|
+
print(node, { json: true });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const fields = [
|
|
429
|
+
["ID", node.id],
|
|
430
|
+
["Title", node.title],
|
|
431
|
+
["Type", node.type],
|
|
432
|
+
["Status", formatStatus(node.status)],
|
|
433
|
+
["Description", node.description || chalk3.dim("\u2014")],
|
|
434
|
+
["Project ID", node.projectId],
|
|
435
|
+
["Parent ID", node.parentId || chalk3.dim("\u2014")],
|
|
436
|
+
["Priority", node.priority || chalk3.dim("\u2014")],
|
|
437
|
+
["Estimate", node.estimate || chalk3.dim("\u2014")],
|
|
438
|
+
["Start Date", node.startDate || chalk3.dim("\u2014")],
|
|
439
|
+
["End Date", node.endDate || chalk3.dim("\u2014")],
|
|
440
|
+
["Due Date", node.dueDate || chalk3.dim("\u2014")],
|
|
441
|
+
["Milestone ID", node.milestoneId || chalk3.dim("\u2014")],
|
|
442
|
+
["Verification", node.verification ?? chalk3.dim("\u2014")],
|
|
443
|
+
["Branch Name", node.branchName || chalk3.dim("\u2014")],
|
|
444
|
+
["PR URL", node.prUrl || chalk3.dim("\u2014")],
|
|
445
|
+
["Execution ID", node.executionId || chalk3.dim("\u2014")],
|
|
446
|
+
["Execution Started", node.executionStartedAt ? formatRelativeTime(node.executionStartedAt) : chalk3.dim("\u2014")],
|
|
447
|
+
["Execution Completed", node.executionCompletedAt ? formatRelativeTime(node.executionCompletedAt) : chalk3.dim("\u2014")],
|
|
448
|
+
["Created", formatRelativeTime(node.createdAt)],
|
|
449
|
+
["Updated", formatRelativeTime(node.updatedAt)]
|
|
450
|
+
];
|
|
451
|
+
const maxKeyLen = Math.max(...fields.map(([k]) => k.length));
|
|
452
|
+
for (const [key, value] of fields) {
|
|
453
|
+
console.log(` ${chalk3.bold(key.padEnd(maxKeyLen))} ${value}`);
|
|
454
|
+
}
|
|
455
|
+
if (node.executionOutput) {
|
|
456
|
+
console.log();
|
|
457
|
+
console.log(chalk3.bold(" Execution Output:"));
|
|
458
|
+
const output = node.executionOutput.slice(0, 500);
|
|
459
|
+
console.log(` ${output}${node.executionOutput.length > 500 ? "..." : ""}`);
|
|
460
|
+
}
|
|
461
|
+
if (node.executionError) {
|
|
462
|
+
console.log();
|
|
463
|
+
console.log(chalk3.bold.red(" Execution Error:"));
|
|
464
|
+
console.log(` ${node.executionError}`);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
plan.command("tree").description("Show ASCII dependency tree for a project").requiredOption("--project-id <id>", "Project ID").action(async (cmdOpts) => {
|
|
468
|
+
const opts = program2.opts();
|
|
469
|
+
const client = getClient(opts.serverUrl);
|
|
470
|
+
let nodes, edges;
|
|
471
|
+
try {
|
|
472
|
+
const result = await client.getPlan(cmdOpts.projectId);
|
|
473
|
+
nodes = result.nodes;
|
|
474
|
+
edges = result.edges;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
console.error(chalk3.red(err.message));
|
|
477
|
+
process.exitCode = 1;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (nodes.length === 0) {
|
|
481
|
+
console.log(chalk3.dim(" No plan nodes found."));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (opts.json) {
|
|
485
|
+
const tree2 = buildTree(nodes, edges);
|
|
486
|
+
print(tree2, { json: true });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const tree = buildTree(nodes, edges);
|
|
490
|
+
const lines = renderTreeLines(tree);
|
|
491
|
+
if (lines.length === 0) {
|
|
492
|
+
console.log(chalk3.dim(" No plan nodes found."));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
console.log();
|
|
496
|
+
console.log(chalk3.bold(` Plan tree (${nodes.length} nodes, ${edges.length} edges):`));
|
|
497
|
+
console.log();
|
|
498
|
+
for (const line of lines) {
|
|
499
|
+
console.log(` ${line}`);
|
|
500
|
+
}
|
|
501
|
+
console.log();
|
|
502
|
+
});
|
|
503
|
+
plan.command("create-node").description("Create a new plan node").requiredOption("--project-id <id>", "Project ID").requiredOption("--title <title>", "Node title").option("--type <type>", "Node type: task, milestone, decision", "task").option("--description <desc>", "Node description").option("--status <status>", "Initial status", "planned").option("--parent-id <id>", "Parent node ID").option("--priority <priority>", "Priority: critical, high, normal, low").action(async (cmdOpts) => {
|
|
504
|
+
const opts = program2.opts();
|
|
505
|
+
const client = getClient(opts.serverUrl);
|
|
506
|
+
const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
507
|
+
try {
|
|
508
|
+
await client.createPlanNode({
|
|
509
|
+
id,
|
|
510
|
+
projectId: cmdOpts.projectId,
|
|
511
|
+
title: cmdOpts.title,
|
|
512
|
+
type: cmdOpts.type,
|
|
513
|
+
description: cmdOpts.description,
|
|
514
|
+
status: cmdOpts.status,
|
|
515
|
+
parentId: cmdOpts.parentId ?? null,
|
|
516
|
+
priority: cmdOpts.priority ?? null
|
|
517
|
+
});
|
|
518
|
+
if (opts.json) {
|
|
519
|
+
print({ ok: true, id, projectId: cmdOpts.projectId, title: cmdOpts.title }, { json: true });
|
|
520
|
+
} else {
|
|
521
|
+
console.log(chalk3.green("Node created:"));
|
|
522
|
+
console.log(` ${chalk3.bold("ID")} ${id}`);
|
|
523
|
+
console.log(` ${chalk3.bold("Title")} ${cmdOpts.title}`);
|
|
524
|
+
console.log(` ${chalk3.bold("Type")} ${cmdOpts.type}`);
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
528
|
+
if (opts.json) {
|
|
529
|
+
print({ error: msg }, { json: true });
|
|
530
|
+
} else {
|
|
531
|
+
console.error(chalk3.red(`Create failed: ${msg}`));
|
|
532
|
+
}
|
|
533
|
+
process.exitCode = 1;
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
plan.command("update-node <nodeId>").description("Update a plan node").option("--title <title>", "New title").option("--status <status>", "New status").option("--description <desc>", "New description").option("--priority <priority>", "New priority: critical, high, normal, low").option("--type <type>", "New type: task, milestone, decision").action(async (nodeId, cmdOpts) => {
|
|
537
|
+
const opts = program2.opts();
|
|
538
|
+
const client = getClient(opts.serverUrl);
|
|
539
|
+
const patch = {};
|
|
540
|
+
if (cmdOpts.title !== void 0) patch.title = cmdOpts.title;
|
|
541
|
+
if (cmdOpts.status !== void 0) patch.status = cmdOpts.status;
|
|
542
|
+
if (cmdOpts.description !== void 0) patch.description = cmdOpts.description;
|
|
543
|
+
if (cmdOpts.priority !== void 0) patch.priority = cmdOpts.priority;
|
|
544
|
+
if (cmdOpts.type !== void 0) patch.type = cmdOpts.type;
|
|
545
|
+
if (Object.keys(patch).length === 0) {
|
|
546
|
+
console.error(chalk3.red("No update fields provided. Use --title, --status, --description, --priority, or --type."));
|
|
547
|
+
process.exitCode = 1;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const result = await client.updatePlanNode(nodeId, patch);
|
|
552
|
+
if (opts.json) {
|
|
553
|
+
print({ ...result, nodeId, updated: Object.keys(patch) }, { json: true });
|
|
554
|
+
} else {
|
|
555
|
+
console.log(chalk3.green(`Node ${nodeId} updated: ${Object.keys(patch).join(", ")}`));
|
|
556
|
+
}
|
|
557
|
+
} catch (err) {
|
|
558
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
559
|
+
if (opts.json) {
|
|
560
|
+
print({ error: msg }, { json: true });
|
|
561
|
+
} else {
|
|
562
|
+
console.error(chalk3.red(`Update failed: ${msg}`));
|
|
563
|
+
}
|
|
564
|
+
process.exitCode = 1;
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
plan.command("delete-node <nodeId>").description("Delete a plan node").action(async (nodeId) => {
|
|
568
|
+
const opts = program2.opts();
|
|
569
|
+
const client = getClient(opts.serverUrl);
|
|
570
|
+
try {
|
|
571
|
+
const result = await client.deletePlanNode(nodeId);
|
|
572
|
+
if (opts.json) {
|
|
573
|
+
print({ ...result, nodeId }, { json: true });
|
|
574
|
+
} else {
|
|
575
|
+
console.log(chalk3.green(`Node ${nodeId} deleted.`));
|
|
576
|
+
}
|
|
577
|
+
} catch (err) {
|
|
578
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
579
|
+
if (opts.json) {
|
|
580
|
+
print({ error: msg }, { json: true });
|
|
581
|
+
} else {
|
|
582
|
+
console.error(chalk3.red(`Delete failed: ${msg}`));
|
|
583
|
+
}
|
|
584
|
+
process.exitCode = 1;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
plan.command("stats").description("Show plan statistics for a project").requiredOption("--project-id <id>", "Project ID").action(async (cmdOpts) => {
|
|
588
|
+
const opts = program2.opts();
|
|
589
|
+
const client = getClient(opts.serverUrl);
|
|
590
|
+
try {
|
|
591
|
+
const { nodes, edges } = await client.getPlan(cmdOpts.projectId);
|
|
592
|
+
const active = nodes.filter((n) => !n.deletedAt);
|
|
593
|
+
const statusCounts = {};
|
|
594
|
+
for (const n of active) {
|
|
595
|
+
statusCounts[n.status] = (statusCounts[n.status] || 0) + 1;
|
|
596
|
+
}
|
|
597
|
+
const withExecution = active.filter((n) => n.executionId).length;
|
|
598
|
+
const dates = active.flatMap((n) => [n.startDate, n.endDate].filter(Boolean)).map((d) => new Date(d).getTime()).filter((d) => !isNaN(d));
|
|
599
|
+
const minDate = dates.length > 0 ? new Date(Math.min(...dates)).toISOString().split("T")[0] : null;
|
|
600
|
+
const maxDate = dates.length > 0 ? new Date(Math.max(...dates)).toISOString().split("T")[0] : null;
|
|
601
|
+
const stats = {
|
|
602
|
+
totalNodes: active.length,
|
|
603
|
+
totalEdges: edges.length,
|
|
604
|
+
statusCounts,
|
|
605
|
+
executionCoverage: active.length > 0 ? `${withExecution}/${active.length} (${Math.round(withExecution / active.length * 100)}%)` : "0/0",
|
|
606
|
+
dateRange: minDate && maxDate ? `${minDate} \u2192 ${maxDate}` : "\u2014"
|
|
607
|
+
};
|
|
608
|
+
if (opts.json) {
|
|
609
|
+
print(stats, { json: true });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
console.log();
|
|
613
|
+
console.log(chalk3.bold(" Plan Statistics"));
|
|
614
|
+
console.log(chalk3.dim(" " + "\u2500".repeat(40)));
|
|
615
|
+
console.log(` ${chalk3.dim("Total Nodes:")} ${stats.totalNodes}`);
|
|
616
|
+
console.log(` ${chalk3.dim("Total Edges:")} ${stats.totalEdges}`);
|
|
617
|
+
console.log(` ${chalk3.dim("Execution Coverage:")} ${stats.executionCoverage}`);
|
|
618
|
+
console.log(` ${chalk3.dim("Date Range:")} ${stats.dateRange}`);
|
|
619
|
+
console.log();
|
|
620
|
+
console.log(chalk3.bold(" Status Breakdown"));
|
|
621
|
+
for (const [status, count] of Object.entries(statusCounts).sort((a, b) => b[1] - a[1])) {
|
|
622
|
+
console.log(` ${formatStatus(status).padEnd(25)} ${count}`);
|
|
623
|
+
}
|
|
624
|
+
console.log();
|
|
625
|
+
} catch (err) {
|
|
626
|
+
console.error(chalk3.red(err.message));
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
plan.command("export").description("Export plan in various formats").requiredOption("--project-id <id>", "Project ID").option("--format <fmt>", "Output format: json, dot, mermaid", "json").action(async (cmdOpts) => {
|
|
631
|
+
const opts = program2.opts();
|
|
632
|
+
const client = getClient(opts.serverUrl);
|
|
633
|
+
let nodes, edges;
|
|
634
|
+
try {
|
|
635
|
+
const result = await client.getPlan(cmdOpts.projectId);
|
|
636
|
+
nodes = result.nodes.filter((n) => !n.deletedAt);
|
|
637
|
+
edges = result.edges;
|
|
638
|
+
} catch (err) {
|
|
639
|
+
console.error(chalk3.red(err.message));
|
|
640
|
+
process.exitCode = 1;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
switch (cmdOpts.format) {
|
|
644
|
+
case "json":
|
|
645
|
+
console.log(JSON.stringify({ nodes, edges }, null, 2));
|
|
646
|
+
break;
|
|
647
|
+
case "dot": {
|
|
648
|
+
const lines = ["digraph plan {", " rankdir=LR;", " node [shape=box];"];
|
|
649
|
+
for (const n of nodes) {
|
|
650
|
+
const label = n.title.replace(/"/g, '\\"');
|
|
651
|
+
lines.push(` "${n.id}" [label="${label}\\n[${n.status}]"];`);
|
|
652
|
+
}
|
|
653
|
+
for (const e of edges) {
|
|
654
|
+
lines.push(` "${e.source}" -> "${e.target}";`);
|
|
655
|
+
}
|
|
656
|
+
lines.push("}");
|
|
657
|
+
console.log(lines.join("\n"));
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case "mermaid": {
|
|
661
|
+
const lines = ["graph LR"];
|
|
662
|
+
for (const n of nodes) {
|
|
663
|
+
const label = n.title.replace(/"/g, "'");
|
|
664
|
+
lines.push(` ${n.id}["${label}<br/>${n.status}"]`);
|
|
665
|
+
}
|
|
666
|
+
for (const e of edges) {
|
|
667
|
+
lines.push(` ${e.source} --> ${e.target}`);
|
|
668
|
+
}
|
|
669
|
+
console.log(lines.join("\n"));
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
default:
|
|
673
|
+
console.error(chalk3.red(`Unknown format "${cmdOpts.format}". Use json, dot, or mermaid.`));
|
|
674
|
+
process.exitCode = 1;
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/commands/task.ts
|
|
680
|
+
import chalk4 from "chalk";
|
|
681
|
+
function registerTaskCommands(program2) {
|
|
682
|
+
const cmd = program2.command("task").description("Manage tasks");
|
|
683
|
+
cmd.command("list").description("List tasks across projects").option("--project <id>", "Filter by project ID").option("--status <status>", "Filter by status (planned, in_progress, completed, etc.)").option("--since <date>", "Show tasks updated after date (e.g. 2d, 1h, today, ISO)").option("--until <date>", "Show tasks updated before date (e.g. 2d, 1h, yesterday, ISO)").action(async (opts) => {
|
|
684
|
+
const client = getClient(program2.opts().serverUrl);
|
|
685
|
+
const isJson = program2.opts().json;
|
|
686
|
+
let nodes;
|
|
687
|
+
try {
|
|
688
|
+
const result = opts.project ? await client.getPlan(opts.project) : await client.getFullPlan();
|
|
689
|
+
nodes = result.nodes;
|
|
690
|
+
} catch (err) {
|
|
691
|
+
console.error(chalk4.red(err.message));
|
|
692
|
+
process.exitCode = 1;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
let filtered = nodes.filter((n) => !n.deletedAt);
|
|
696
|
+
if (opts.status) {
|
|
697
|
+
filtered = filtered.filter((n) => n.status === opts.status);
|
|
698
|
+
}
|
|
699
|
+
if (opts.since) {
|
|
700
|
+
const since = parseDateFilter(opts.since);
|
|
701
|
+
filtered = filtered.filter((n) => n.updatedAt && new Date(n.updatedAt) >= since);
|
|
702
|
+
}
|
|
703
|
+
if (opts.until) {
|
|
704
|
+
const until = parseDateFilter(opts.until);
|
|
705
|
+
filtered = filtered.filter((n) => n.updatedAt && new Date(n.updatedAt) <= until);
|
|
706
|
+
}
|
|
707
|
+
filtered.sort((a, b) => {
|
|
708
|
+
const da = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
709
|
+
const db = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
710
|
+
return db - da;
|
|
711
|
+
});
|
|
712
|
+
const projects = await client.listProjects();
|
|
713
|
+
const projectMap = new Map(projects.map((p) => [p.id, p.name]));
|
|
714
|
+
const rows = filtered.map((n) => ({
|
|
715
|
+
id: n.id,
|
|
716
|
+
title: n.title,
|
|
717
|
+
status: n.status,
|
|
718
|
+
projectName: projectMap.get(n.projectId) ?? n.projectId,
|
|
719
|
+
updatedAt: n.updatedAt
|
|
720
|
+
}));
|
|
721
|
+
if (isJson) {
|
|
722
|
+
print(rows, { json: true });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const columns = [
|
|
726
|
+
{ key: "id", label: "ID", width: 12 },
|
|
727
|
+
{ key: "title", label: "TITLE", width: 40 },
|
|
728
|
+
{ key: "status", label: "STATUS", width: 18, format: (v) => formatStatus(String(v ?? "")) },
|
|
729
|
+
{ key: "projectName", label: "PROJECT", width: 20 },
|
|
730
|
+
{ key: "updatedAt", label: "UPDATED", width: 12, format: (v) => formatRelativeTime(v) }
|
|
731
|
+
];
|
|
732
|
+
print(rows, { columns });
|
|
733
|
+
});
|
|
734
|
+
cmd.command("show <id>").description("Show task details").action(async (clientId) => {
|
|
735
|
+
const client = getClient(program2.opts().serverUrl);
|
|
736
|
+
const isJson = program2.opts().json;
|
|
737
|
+
let nodes;
|
|
738
|
+
try {
|
|
739
|
+
const result = await client.getFullPlan();
|
|
740
|
+
nodes = result.nodes;
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.error(chalk4.red(err.message));
|
|
743
|
+
process.exitCode = 1;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const node = nodes.find((n) => n.id === clientId && !n.deletedAt);
|
|
747
|
+
if (!node) {
|
|
748
|
+
console.error(chalk4.red(`Task not found: ${clientId}`));
|
|
749
|
+
process.exitCode = 1;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
let projectName;
|
|
753
|
+
let latestExecution;
|
|
754
|
+
let changes = [];
|
|
755
|
+
try {
|
|
756
|
+
const projects = await client.listProjects();
|
|
757
|
+
const project = projects.find((p) => p.id === node.projectId);
|
|
758
|
+
projectName = project?.name ?? node.projectId;
|
|
759
|
+
const executionMap = await client.getExecutions();
|
|
760
|
+
latestExecution = executionMap[node.id] ?? null;
|
|
761
|
+
if (latestExecution?.executionId) {
|
|
762
|
+
changes = await client.listFileChanges(latestExecution.executionId);
|
|
763
|
+
}
|
|
764
|
+
} catch (err) {
|
|
765
|
+
console.error(chalk4.red(err.message));
|
|
766
|
+
process.exitCode = 1;
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (isJson) {
|
|
770
|
+
print({ ...node, projectName, execution: latestExecution, fileChanges: changes }, { json: true });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
console.log();
|
|
774
|
+
console.log(chalk4.bold(node.title));
|
|
775
|
+
console.log(chalk4.dim("\u2500".repeat(60)));
|
|
776
|
+
console.log(` ${chalk4.dim("ID:")} ${node.id}`);
|
|
777
|
+
console.log(` ${chalk4.dim("Type:")} ${node.type}`);
|
|
778
|
+
console.log(` ${chalk4.dim("Status:")} ${formatStatus(node.status)}`);
|
|
779
|
+
console.log(` ${chalk4.dim("Project:")} ${projectName}`);
|
|
780
|
+
if (node.priority) {
|
|
781
|
+
console.log(` ${chalk4.dim("Priority:")} ${node.priority}`);
|
|
782
|
+
}
|
|
783
|
+
if (node.estimate) {
|
|
784
|
+
console.log(` ${chalk4.dim("Estimate:")} ${node.estimate}`);
|
|
785
|
+
}
|
|
786
|
+
if (node.startDate) {
|
|
787
|
+
console.log(` ${chalk4.dim("Start Date:")} ${node.startDate}`);
|
|
788
|
+
}
|
|
789
|
+
if (node.endDate) {
|
|
790
|
+
console.log(` ${chalk4.dim("End Date:")} ${node.endDate}`);
|
|
791
|
+
}
|
|
792
|
+
if (node.dueDate) {
|
|
793
|
+
console.log(` ${chalk4.dim("Due Date:")} ${node.dueDate}`);
|
|
794
|
+
}
|
|
795
|
+
if (node.branchName) {
|
|
796
|
+
console.log(` ${chalk4.dim("Branch:")} ${node.branchName}`);
|
|
797
|
+
}
|
|
798
|
+
if (node.prUrl) {
|
|
799
|
+
console.log(` ${chalk4.dim("PR:")} ${node.prUrl}`);
|
|
800
|
+
}
|
|
801
|
+
if (node.description) {
|
|
802
|
+
console.log();
|
|
803
|
+
console.log(chalk4.dim(" Description:"));
|
|
804
|
+
console.log(` ${node.description}`);
|
|
805
|
+
}
|
|
806
|
+
console.log();
|
|
807
|
+
console.log(` ${chalk4.dim("Created:")} ${formatRelativeTime(node.createdAt)}`);
|
|
808
|
+
console.log(` ${chalk4.dim("Updated:")} ${formatRelativeTime(node.updatedAt)}`);
|
|
809
|
+
if (latestExecution) {
|
|
810
|
+
console.log();
|
|
811
|
+
console.log(chalk4.bold(" Latest Execution"));
|
|
812
|
+
console.log(chalk4.dim(" " + "\u2500".repeat(56)));
|
|
813
|
+
console.log(` ${chalk4.dim("Execution ID:")} ${latestExecution.executionId}`);
|
|
814
|
+
console.log(` ${chalk4.dim("Status:")} ${formatStatus(latestExecution.status)}`);
|
|
815
|
+
if (latestExecution.providerName) {
|
|
816
|
+
console.log(` ${chalk4.dim("Provider:")} ${latestExecution.providerName}`);
|
|
817
|
+
}
|
|
818
|
+
if (latestExecution.model) {
|
|
819
|
+
console.log(` ${chalk4.dim("Model:")} ${latestExecution.model}`);
|
|
820
|
+
}
|
|
821
|
+
if (latestExecution.machineId) {
|
|
822
|
+
console.log(` ${chalk4.dim("Machine:")} ${latestExecution.machineId}`);
|
|
823
|
+
}
|
|
824
|
+
console.log(` ${chalk4.dim("Started:")} ${formatRelativeTime(latestExecution.startedAt)}`);
|
|
825
|
+
if (latestExecution.completedAt) {
|
|
826
|
+
console.log(` ${chalk4.dim("Completed:")} ${formatRelativeTime(latestExecution.completedAt)}`);
|
|
827
|
+
}
|
|
828
|
+
if (latestExecution.durationMs != null) {
|
|
829
|
+
const secs = (latestExecution.durationMs / 1e3).toFixed(1);
|
|
830
|
+
console.log(` ${chalk4.dim("Duration:")} ${secs}s`);
|
|
831
|
+
}
|
|
832
|
+
if (latestExecution.tokensUsed != null) {
|
|
833
|
+
console.log(` ${chalk4.dim("Tokens:")} ${latestExecution.tokensUsed.toLocaleString()}`);
|
|
834
|
+
}
|
|
835
|
+
if (latestExecution.estimatedCostUsd != null) {
|
|
836
|
+
console.log(` ${chalk4.dim("Cost:")} $${latestExecution.estimatedCostUsd.toFixed(4)}`);
|
|
837
|
+
}
|
|
838
|
+
if (latestExecution.error) {
|
|
839
|
+
console.log(` ${chalk4.red("Error:")} ${latestExecution.error}`);
|
|
840
|
+
}
|
|
841
|
+
if (latestExecution.markdownSummary) {
|
|
842
|
+
console.log();
|
|
843
|
+
console.log(chalk4.dim(" Summary:"));
|
|
844
|
+
console.log(` ${latestExecution.markdownSummary}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (changes.length > 0) {
|
|
848
|
+
console.log();
|
|
849
|
+
console.log(chalk4.bold(" File Changes"));
|
|
850
|
+
console.log(chalk4.dim(" " + "\u2500".repeat(56)));
|
|
851
|
+
for (const fc of changes) {
|
|
852
|
+
const actionColor = fc.action === "create" ? chalk4.green : fc.action === "delete" ? chalk4.red : chalk4.yellow;
|
|
853
|
+
const stats = [];
|
|
854
|
+
if (fc.linesAdded != null && fc.linesAdded > 0) stats.push(chalk4.green(`+${fc.linesAdded}`));
|
|
855
|
+
if (fc.linesRemoved != null && fc.linesRemoved > 0) stats.push(chalk4.red(`-${fc.linesRemoved}`));
|
|
856
|
+
const statsStr = stats.length > 0 ? ` (${stats.join(", ")})` : "";
|
|
857
|
+
console.log(` ${actionColor(fc.action.padEnd(8))} ${fc.path}${statsStr}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
console.log();
|
|
861
|
+
});
|
|
862
|
+
cmd.command("dispatch <id>").description("Dispatch a task for execution").requiredOption("--project-id <id>", "Project ID").option("--force", "Force re-dispatch even if already running").action(async (nodeId, opts) => {
|
|
863
|
+
const client = getClient(program2.opts().serverUrl);
|
|
864
|
+
console.log(chalk4.dim(`Dispatching task ${chalk4.bold(nodeId)} to server...`));
|
|
865
|
+
console.log();
|
|
866
|
+
try {
|
|
867
|
+
const response = await client.dispatchTask({
|
|
868
|
+
nodeId,
|
|
869
|
+
projectId: opts.projectId,
|
|
870
|
+
force: opts.force
|
|
871
|
+
});
|
|
872
|
+
await streamDispatchToStdout(response);
|
|
873
|
+
console.log();
|
|
874
|
+
console.log(chalk4.green("Task dispatch complete."));
|
|
875
|
+
} catch (err) {
|
|
876
|
+
console.error(chalk4.red(`Dispatch failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
877
|
+
process.exitCode = 1;
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
cmd.command("cancel <executionId>").description("Cancel a running task execution").option("--machine <id>", "Target machine ID").option("--node-id <id>", "Node ID").action(async (executionId, opts) => {
|
|
881
|
+
const client = getClient(program2.opts().serverUrl);
|
|
882
|
+
const isJson = program2.opts().json;
|
|
883
|
+
try {
|
|
884
|
+
const result = await client.cancelTask({
|
|
885
|
+
executionId,
|
|
886
|
+
machineId: opts.machine,
|
|
887
|
+
nodeId: opts.nodeId
|
|
888
|
+
});
|
|
889
|
+
if (isJson) {
|
|
890
|
+
print(result, { json: true });
|
|
891
|
+
} else {
|
|
892
|
+
console.log(chalk4.green(`Task ${executionId} cancelled.`));
|
|
893
|
+
}
|
|
894
|
+
} catch (err) {
|
|
895
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
896
|
+
if (isJson) {
|
|
897
|
+
print({ error: msg }, { json: true });
|
|
898
|
+
} else {
|
|
899
|
+
console.error(chalk4.red(`Cancel failed: ${msg}`));
|
|
900
|
+
}
|
|
901
|
+
process.exitCode = 1;
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
cmd.command("steer <executionId>").description("Send steering guidance to a running task").requiredOption("--machine <id>", "Target machine ID").requiredOption("--message <msg>", "Steering message").option("--action <action>", "Action type: guidance, redirect, pause, resume").action(async (executionId, opts) => {
|
|
905
|
+
const client = getClient(program2.opts().serverUrl);
|
|
906
|
+
const isJson = program2.opts().json;
|
|
907
|
+
try {
|
|
908
|
+
const result = await client.steerTask({
|
|
909
|
+
taskId: executionId,
|
|
910
|
+
machineId: opts.machine,
|
|
911
|
+
message: opts.message,
|
|
912
|
+
action: opts.action
|
|
913
|
+
});
|
|
914
|
+
if (isJson) {
|
|
915
|
+
print(result, { json: true });
|
|
916
|
+
} else {
|
|
917
|
+
console.log(chalk4.green(`Steering message sent to ${executionId}.`));
|
|
918
|
+
}
|
|
919
|
+
} catch (err) {
|
|
920
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
921
|
+
if (isJson) {
|
|
922
|
+
print({ error: msg }, { json: true });
|
|
923
|
+
} else {
|
|
924
|
+
console.error(chalk4.red(`Steer failed: ${msg}`));
|
|
925
|
+
}
|
|
926
|
+
process.exitCode = 1;
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
cmd.command("update-status <nodeId>").description("Update a task node status").requiredOption("--status <status>", "New status (planned, in_progress, completed, etc.)").action(async (nodeId, opts) => {
|
|
930
|
+
const client = getClient(program2.opts().serverUrl);
|
|
931
|
+
const isJson = program2.opts().json;
|
|
932
|
+
try {
|
|
933
|
+
const result = await client.updatePlanNode(nodeId, { status: opts.status });
|
|
934
|
+
if (isJson) {
|
|
935
|
+
print(result, { json: true });
|
|
936
|
+
} else {
|
|
937
|
+
console.log(chalk4.green(`Task ${nodeId} status updated to ${formatStatus(opts.status)}.`));
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
941
|
+
if (isJson) {
|
|
942
|
+
print({ error: msg }, { json: true });
|
|
943
|
+
} else {
|
|
944
|
+
console.error(chalk4.red(`Update failed: ${msg}`));
|
|
945
|
+
}
|
|
946
|
+
process.exitCode = 1;
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
cmd.command("watch <executionId>").description("Watch real-time output from a running task via SSE").action(async (executionId) => {
|
|
950
|
+
const client = getClient(program2.opts().serverUrl);
|
|
951
|
+
const isJson = program2.opts().json;
|
|
952
|
+
try {
|
|
953
|
+
const response = await client.streamEvents();
|
|
954
|
+
if (!response.body) {
|
|
955
|
+
console.error(chalk4.red("No stream body received"));
|
|
956
|
+
process.exitCode = 1;
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const reader = response.body.getReader();
|
|
960
|
+
const decoder = new TextDecoder();
|
|
961
|
+
let buffer = "";
|
|
962
|
+
while (true) {
|
|
963
|
+
const { done, value } = await reader.read();
|
|
964
|
+
if (done) break;
|
|
965
|
+
buffer += decoder.decode(value, { stream: true });
|
|
966
|
+
const lines = buffer.split("\n");
|
|
967
|
+
buffer = lines.pop() ?? "";
|
|
968
|
+
for (const line of lines) {
|
|
969
|
+
if (!line.startsWith("data: ")) continue;
|
|
970
|
+
try {
|
|
971
|
+
const event = JSON.parse(line.slice(6));
|
|
972
|
+
if (event.taskId && event.taskId !== executionId) continue;
|
|
973
|
+
if (isJson) {
|
|
974
|
+
console.log(JSON.stringify(event));
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
switch (event.type) {
|
|
978
|
+
case "task:text":
|
|
979
|
+
process.stdout.write(event.content ?? event.data ?? "");
|
|
980
|
+
break;
|
|
981
|
+
case "task:tool_trace":
|
|
982
|
+
console.log(chalk4.dim(`[tool] ${event.name ?? "unknown"}`));
|
|
983
|
+
break;
|
|
984
|
+
case "task:result":
|
|
985
|
+
console.log(`
|
|
986
|
+
${chalk4.bold("--- Result:")} ${formatStatus(event.status ?? "unknown")} ${event.duration != null ? chalk4.dim(`(${formatDuration(event.duration)})`) : ""}`);
|
|
987
|
+
if (event.summary) console.log(event.summary);
|
|
988
|
+
reader.cancel();
|
|
989
|
+
return;
|
|
990
|
+
case "task:progress":
|
|
991
|
+
if (event.message) console.log(chalk4.dim(`[progress] ${event.message}`));
|
|
992
|
+
break;
|
|
993
|
+
case "task:stdout":
|
|
994
|
+
process.stdout.write(event.data ?? "");
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
} catch {
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1003
|
+
if (isJson) {
|
|
1004
|
+
print({ error: msg }, { json: true });
|
|
1005
|
+
} else {
|
|
1006
|
+
console.error(chalk4.red(`Watch failed: ${msg}`));
|
|
1007
|
+
}
|
|
1008
|
+
process.exitCode = 1;
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/commands/search.ts
|
|
1014
|
+
import chalk5 from "chalk";
|
|
1015
|
+
function registerSearchCommands(program2) {
|
|
1016
|
+
program2.command("search <query>").description("Search across projects, tasks, and executions").option("--type <type>", "Filter by type: projects, tasks, executions").option("--since <date>", "Show results updated after date (e.g. 2d, 1h, today, ISO)").option("--until <date>", "Show results updated before date (e.g. 2d, 1h, yesterday, ISO)").action(async (query, cmdOpts) => {
|
|
1017
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1018
|
+
const isJson = program2.opts().json;
|
|
1019
|
+
let projectResults, taskResults, executionResults;
|
|
1020
|
+
try {
|
|
1021
|
+
const results = await client.search(query);
|
|
1022
|
+
projectResults = results.projects;
|
|
1023
|
+
taskResults = results.tasks;
|
|
1024
|
+
executionResults = results.executions;
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
console.error(chalk5.red(err.message));
|
|
1027
|
+
process.exitCode = 1;
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (cmdOpts.since) {
|
|
1031
|
+
const since = parseDateFilter(cmdOpts.since);
|
|
1032
|
+
projectResults = projectResults.filter((p) => p.updatedAt && new Date(p.updatedAt) >= since);
|
|
1033
|
+
taskResults = taskResults.filter((t) => t.updatedAt && new Date(t.updatedAt) >= since);
|
|
1034
|
+
executionResults = executionResults.filter((e) => e.startedAt && new Date(e.startedAt) >= since);
|
|
1035
|
+
}
|
|
1036
|
+
if (cmdOpts.until) {
|
|
1037
|
+
const until = parseDateFilter(cmdOpts.until);
|
|
1038
|
+
projectResults = projectResults.filter((p) => p.updatedAt && new Date(p.updatedAt) <= until);
|
|
1039
|
+
taskResults = taskResults.filter((t) => t.updatedAt && new Date(t.updatedAt) <= until);
|
|
1040
|
+
executionResults = executionResults.filter((e) => e.startedAt && new Date(e.startedAt) <= until);
|
|
1041
|
+
}
|
|
1042
|
+
if (cmdOpts.type) {
|
|
1043
|
+
if (cmdOpts.type !== "projects") projectResults = [];
|
|
1044
|
+
if (cmdOpts.type !== "tasks") taskResults = [];
|
|
1045
|
+
if (cmdOpts.type !== "executions") executionResults = [];
|
|
1046
|
+
}
|
|
1047
|
+
if (isJson) {
|
|
1048
|
+
print(
|
|
1049
|
+
{
|
|
1050
|
+
projects: projectResults,
|
|
1051
|
+
tasks: taskResults,
|
|
1052
|
+
executions: executionResults
|
|
1053
|
+
},
|
|
1054
|
+
{ json: true }
|
|
1055
|
+
);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const totalMatches = projectResults.length + taskResults.length + executionResults.length;
|
|
1059
|
+
if (totalMatches === 0) {
|
|
1060
|
+
console.log(chalk5.dim(`No results for "${query}"`));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (projectResults.length > 0) {
|
|
1064
|
+
console.log();
|
|
1065
|
+
console.log(chalk5.bold(`Projects (${projectResults.length} match${projectResults.length === 1 ? "" : "es"}):`));
|
|
1066
|
+
const projectCols = [
|
|
1067
|
+
{ key: "id", label: "ID", width: 8, format: (v) => String(v ?? "").slice(0, 8) },
|
|
1068
|
+
{ key: "name", label: "NAME", width: 30 },
|
|
1069
|
+
{ key: "status", label: "STATUS", width: 12, format: (v) => formatStatus(String(v ?? "")) },
|
|
1070
|
+
{ key: "workingDirectory", label: "DIRECTORY", width: 40, format: (v) => String(v ?? "") }
|
|
1071
|
+
];
|
|
1072
|
+
print(projectResults, { columns: projectCols });
|
|
1073
|
+
}
|
|
1074
|
+
if (taskResults.length > 0) {
|
|
1075
|
+
console.log();
|
|
1076
|
+
console.log(chalk5.bold(`Tasks (${taskResults.length} match${taskResults.length === 1 ? "" : "es"}):`));
|
|
1077
|
+
const taskCols = [
|
|
1078
|
+
{ key: "clientId", label: "CLIENT_ID", width: 12 },
|
|
1079
|
+
{ key: "title", label: "TITLE", width: 35 },
|
|
1080
|
+
{ key: "status", label: "STATUS", width: 18, format: (v) => formatStatus(String(v ?? "")) },
|
|
1081
|
+
{ key: "projectName", label: "PROJECT", width: 20, format: (v) => String(v ?? "") }
|
|
1082
|
+
];
|
|
1083
|
+
print(taskResults, { columns: taskCols });
|
|
1084
|
+
}
|
|
1085
|
+
if (executionResults.length > 0) {
|
|
1086
|
+
console.log();
|
|
1087
|
+
console.log(chalk5.bold(`Executions (${executionResults.length} match${executionResults.length === 1 ? "" : "es"}):`));
|
|
1088
|
+
const execCols = [
|
|
1089
|
+
{ key: "executionId", label: "EXECUTION_ID", width: 20 },
|
|
1090
|
+
{ key: "nodeClientId", label: "NODE_ID", width: 12 },
|
|
1091
|
+
{ key: "status", label: "STATUS", width: 12, format: (v) => formatStatus(String(v ?? "")) },
|
|
1092
|
+
{
|
|
1093
|
+
key: "startedAt",
|
|
1094
|
+
label: "STARTED",
|
|
1095
|
+
width: 20,
|
|
1096
|
+
format: (v) => {
|
|
1097
|
+
if (!v) return "";
|
|
1098
|
+
const d = v instanceof Date ? v : new Date(String(v));
|
|
1099
|
+
return d.toLocaleString();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
];
|
|
1103
|
+
print(executionResults, { columns: execCols });
|
|
1104
|
+
}
|
|
1105
|
+
console.log();
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/commands/activity.ts
|
|
1110
|
+
import chalk6 from "chalk";
|
|
1111
|
+
var activityColumns = [
|
|
1112
|
+
{ key: "time", label: "TIME", width: 12, format: (v) => formatRelativeTime(v) },
|
|
1113
|
+
{ key: "type", label: "TYPE", width: 20 },
|
|
1114
|
+
{ key: "title", label: "TITLE", width: 40 },
|
|
1115
|
+
{ key: "projectId", label: "PROJECT_ID", width: 12, format: (v) => v ? String(v).slice(0, 8) : "\u2014" },
|
|
1116
|
+
{ key: "nodeId", label: "NODE_ID", width: 12, format: (v) => v ? String(v).slice(0, 8) : "\u2014" }
|
|
1117
|
+
];
|
|
1118
|
+
function registerActivityCommands(program2) {
|
|
1119
|
+
const cmd = program2.command("activity").description("View activity feed");
|
|
1120
|
+
cmd.command("list").description("List recent activity").option("--project <id>", "Filter by project").option("--limit <n>", "Number of entries", "20").option("--type <type>", "Filter by event type").option("--since <date>", "Show events after date (e.g. 2d, 1h, today, ISO)").option("--until <date>", "Show events before date (e.g. 2d, 1h, yesterday, ISO)").action(async (opts) => {
|
|
1121
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1122
|
+
const json = program2.opts().json;
|
|
1123
|
+
let rows;
|
|
1124
|
+
try {
|
|
1125
|
+
rows = await client.listActivities({
|
|
1126
|
+
projectId: opts.project,
|
|
1127
|
+
limit: opts.limit
|
|
1128
|
+
});
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
console.error(err.message);
|
|
1131
|
+
process.exitCode = 1;
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (opts.type) {
|
|
1135
|
+
rows = rows.filter((r) => r.type === opts.type);
|
|
1136
|
+
}
|
|
1137
|
+
if (opts.since) {
|
|
1138
|
+
const since = parseDateFilter(opts.since);
|
|
1139
|
+
rows = rows.filter((r) => r.createdAt && new Date(r.createdAt) >= since);
|
|
1140
|
+
}
|
|
1141
|
+
if (opts.until) {
|
|
1142
|
+
const until = parseDateFilter(opts.until);
|
|
1143
|
+
rows = rows.filter((r) => r.createdAt && new Date(r.createdAt) <= until);
|
|
1144
|
+
}
|
|
1145
|
+
const formatted = rows.map((r) => ({
|
|
1146
|
+
time: r.createdAt,
|
|
1147
|
+
type: r.type,
|
|
1148
|
+
title: r.title,
|
|
1149
|
+
projectId: r.projectId,
|
|
1150
|
+
nodeId: r.nodeId,
|
|
1151
|
+
description: r.description,
|
|
1152
|
+
metadata: r.metadata,
|
|
1153
|
+
id: r.id
|
|
1154
|
+
}));
|
|
1155
|
+
print(formatted, { json, columns: activityColumns });
|
|
1156
|
+
});
|
|
1157
|
+
cmd.command("watch").description("Watch real-time activity events via SSE").option("--project <id>", "Filter by project").option("--type <type>", "Filter by event type").action(async (opts) => {
|
|
1158
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1159
|
+
const isJson = program2.opts().json;
|
|
1160
|
+
try {
|
|
1161
|
+
const response = await client.streamEvents({ projectId: opts.project });
|
|
1162
|
+
if (!response.body) {
|
|
1163
|
+
console.error(chalk6.red("No stream body received"));
|
|
1164
|
+
process.exitCode = 1;
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (!isJson) {
|
|
1168
|
+
console.log(chalk6.dim("Watching activity events... (Ctrl+C to stop)"));
|
|
1169
|
+
}
|
|
1170
|
+
const reader = response.body.getReader();
|
|
1171
|
+
const decoder = new TextDecoder();
|
|
1172
|
+
let buffer = "";
|
|
1173
|
+
while (true) {
|
|
1174
|
+
const { done, value } = await reader.read();
|
|
1175
|
+
if (done) break;
|
|
1176
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1177
|
+
const lines = buffer.split("\n");
|
|
1178
|
+
buffer = lines.pop() ?? "";
|
|
1179
|
+
for (const line of lines) {
|
|
1180
|
+
if (!line.startsWith("data: ")) continue;
|
|
1181
|
+
try {
|
|
1182
|
+
const event = JSON.parse(line.slice(6));
|
|
1183
|
+
if (event.type === "heartbeat") continue;
|
|
1184
|
+
if (opts.type && event.type !== opts.type) continue;
|
|
1185
|
+
if (isJson) {
|
|
1186
|
+
console.log(JSON.stringify(event));
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
1190
|
+
const taskStr = event.taskId ? chalk6.dim(` ${event.taskId.slice(0, 12)}`) : "";
|
|
1191
|
+
const statusStr = event.status ? ` ${event.status}` : "";
|
|
1192
|
+
const durationStr = event.duration != null ? chalk6.dim(` (${formatDuration(event.duration)})`) : "";
|
|
1193
|
+
const msgStr = event.message ? ` ${event.message}` : "";
|
|
1194
|
+
console.log(`[${chalk6.dim(time)}] ${chalk6.bold(event.type)}${taskStr}${statusStr}${durationStr}${msgStr}`);
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1201
|
+
if (isJson) {
|
|
1202
|
+
print({ error: msg }, { json: true });
|
|
1203
|
+
} else {
|
|
1204
|
+
console.error(chalk6.red(`Watch failed: ${msg}`));
|
|
1205
|
+
}
|
|
1206
|
+
process.exitCode = 1;
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/commands/trace.ts
|
|
1212
|
+
import chalk7 from "chalk";
|
|
1213
|
+
function formatTime(date) {
|
|
1214
|
+
if (!date) return "\u2014";
|
|
1215
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
1216
|
+
return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
1217
|
+
}
|
|
1218
|
+
function formatTraceDuration(ms) {
|
|
1219
|
+
if (ms == null) return "\u2014";
|
|
1220
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1221
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1222
|
+
}
|
|
1223
|
+
function formatTokens(n) {
|
|
1224
|
+
if (n == null) return "\u2014";
|
|
1225
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
1226
|
+
return String(n);
|
|
1227
|
+
}
|
|
1228
|
+
var traceColumns = [
|
|
1229
|
+
{ key: "time", label: "TIME", width: 12, format: (v) => formatTime(v) },
|
|
1230
|
+
{ key: "tool", label: "TOOL", width: 24 },
|
|
1231
|
+
{ key: "duration", label: "DURATION", width: 12, format: (v) => formatTraceDuration(v) },
|
|
1232
|
+
{ key: "status", label: "STATUS", width: 8, format: (v) => v ? chalk7.green("\u2713") : chalk7.red("\u2717") }
|
|
1233
|
+
];
|
|
1234
|
+
function registerTraceCommands(program2) {
|
|
1235
|
+
const cmd = program2.command("trace").description("View execution traces");
|
|
1236
|
+
cmd.command("list").description("List tool traces for an execution").requiredOption("--execution-id <id>", "Execution ID").option("--since <date>", "Show traces after date (e.g. 2d, 1h, today, ISO)").option("--until <date>", "Show traces before date (e.g. 2d, 1h, yesterday, ISO)").action(async (opts) => {
|
|
1237
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1238
|
+
const json = program2.opts().json;
|
|
1239
|
+
const executionId = opts.executionId;
|
|
1240
|
+
let traces;
|
|
1241
|
+
let changes;
|
|
1242
|
+
let execution;
|
|
1243
|
+
try {
|
|
1244
|
+
if (opts.since || opts.until) {
|
|
1245
|
+
const params = { executionId };
|
|
1246
|
+
if (opts.since) params.startAfter = parseDateFilter(opts.since).toISOString();
|
|
1247
|
+
if (opts.until) params.startBefore = parseDateFilter(opts.until).toISOString();
|
|
1248
|
+
}
|
|
1249
|
+
traces = await client.listToolTraces(executionId);
|
|
1250
|
+
if (opts.since) {
|
|
1251
|
+
const since = parseDateFilter(opts.since);
|
|
1252
|
+
traces = traces.filter((t) => t.timestamp && new Date(t.timestamp) >= since);
|
|
1253
|
+
}
|
|
1254
|
+
if (opts.until) {
|
|
1255
|
+
const until = parseDateFilter(opts.until);
|
|
1256
|
+
traces = traces.filter((t) => t.timestamp && new Date(t.timestamp) <= until);
|
|
1257
|
+
}
|
|
1258
|
+
changes = await client.listFileChanges(executionId);
|
|
1259
|
+
const executionsMap = await client.getExecutions();
|
|
1260
|
+
execution = Object.values(executionsMap).find(
|
|
1261
|
+
(e) => e.executionId === executionId
|
|
1262
|
+
) ?? null;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
console.error(chalk7.red(err.message));
|
|
1265
|
+
process.exitCode = 1;
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const formatted = traces.map((t) => ({
|
|
1269
|
+
time: t.timestamp,
|
|
1270
|
+
tool: t.toolName,
|
|
1271
|
+
duration: t.duration,
|
|
1272
|
+
status: t.success
|
|
1273
|
+
}));
|
|
1274
|
+
if (json) {
|
|
1275
|
+
print({
|
|
1276
|
+
traces,
|
|
1277
|
+
fileChanges: changes,
|
|
1278
|
+
execution,
|
|
1279
|
+
summary: {
|
|
1280
|
+
totalToolCalls: traces.length,
|
|
1281
|
+
totalDurationMs: traces.reduce((sum, t) => sum + (t.duration ?? 0), 0),
|
|
1282
|
+
filesChanged: changes.length,
|
|
1283
|
+
totalTokens: execution ? (execution.inputTokens ?? 0) + (execution.outputTokens ?? 0) : 0
|
|
1284
|
+
}
|
|
1285
|
+
}, { json: true });
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
print(formatted, { columns: traceColumns });
|
|
1289
|
+
if (changes.length > 0) {
|
|
1290
|
+
const actionCounts = {};
|
|
1291
|
+
for (const c of changes) {
|
|
1292
|
+
actionCounts[c.action] = (actionCounts[c.action] || 0) + 1;
|
|
1293
|
+
}
|
|
1294
|
+
const parts = Object.entries(actionCounts).map(([action, count]) => `${count} ${action}d`);
|
|
1295
|
+
console.log(`
|
|
1296
|
+
${chalk7.bold("Files changed:")} ${changes.length} (${parts.join(", ")})`);
|
|
1297
|
+
} else {
|
|
1298
|
+
console.log(`
|
|
1299
|
+
${chalk7.bold("Files changed:")} 0`);
|
|
1300
|
+
}
|
|
1301
|
+
const totalCalls = traces.length;
|
|
1302
|
+
const totalDurationMs = traces.reduce((sum, t) => sum + (t.duration ?? 0), 0);
|
|
1303
|
+
const totalTokens = execution ? (execution.inputTokens ?? 0) + (execution.outputTokens ?? 0) : 0;
|
|
1304
|
+
console.log(
|
|
1305
|
+
`${chalk7.bold("Total:")} ${totalCalls} tool calls, ${formatTraceDuration(totalDurationMs)}, ${formatTokens(totalTokens)} tokens`
|
|
1306
|
+
);
|
|
1307
|
+
});
|
|
1308
|
+
cmd.command("show").description("Show full trace details for an execution").requiredOption("--execution-id <id>", "Execution ID").action(async (opts) => {
|
|
1309
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1310
|
+
const json = program2.opts().json;
|
|
1311
|
+
const executionId = opts.executionId;
|
|
1312
|
+
let observations;
|
|
1313
|
+
try {
|
|
1314
|
+
const result = await client.listObservations(executionId);
|
|
1315
|
+
observations = result.data;
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
console.error(chalk7.red(err.message));
|
|
1318
|
+
process.exitCode = 1;
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (json) {
|
|
1322
|
+
print(observations, { json: true });
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (observations.length === 0) {
|
|
1326
|
+
console.log(chalk7.dim(" No observation events found for this execution."));
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
console.log(chalk7.bold(`Trace: ${executionId}
|
|
1330
|
+
`));
|
|
1331
|
+
console.log(chalk7.bold("NAME".padEnd(30) + " TYPE".padEnd(14) + " MODEL".padEnd(22) + " TOKENS".padEnd(12) + " COST"));
|
|
1332
|
+
console.log(chalk7.dim("\u2500".repeat(90)));
|
|
1333
|
+
for (const obs of observations) {
|
|
1334
|
+
const name = (obs.name ?? "\u2014").slice(0, 28).padEnd(30);
|
|
1335
|
+
const type = (obs.type ?? "\u2014").padEnd(12);
|
|
1336
|
+
const model = (obs.model ?? "\u2014").slice(0, 20).padEnd(22);
|
|
1337
|
+
const tokens = obs.inputTokens != null || obs.outputTokens != null ? formatTokens((obs.inputTokens ?? 0) + (obs.outputTokens ?? 0)).padEnd(12) : "\u2014".padEnd(12);
|
|
1338
|
+
const cost = obs.estimatedCostUsd != null ? `$${obs.estimatedCostUsd.toFixed(4)}` : "\u2014";
|
|
1339
|
+
console.log(`${name}${type}${model}${tokens}${cost}`);
|
|
1340
|
+
}
|
|
1341
|
+
const totalTokens = observations.reduce(
|
|
1342
|
+
(sum, o) => sum + (o.inputTokens ?? 0) + (o.outputTokens ?? 0),
|
|
1343
|
+
0
|
|
1344
|
+
);
|
|
1345
|
+
const totalCost = observations.reduce(
|
|
1346
|
+
(sum, o) => sum + (o.estimatedCostUsd ?? 0),
|
|
1347
|
+
0
|
|
1348
|
+
);
|
|
1349
|
+
console.log(chalk7.dim("\u2500".repeat(90)));
|
|
1350
|
+
console.log(
|
|
1351
|
+
chalk7.bold(`Total: ${observations.length} events, ${formatTokens(totalTokens)} tokens, $${totalCost.toFixed(4)}`)
|
|
1352
|
+
);
|
|
1353
|
+
});
|
|
1354
|
+
cmd.command("summary").description("Show markdown summary for an execution").requiredOption("--execution-id <id>", "Execution ID").action(async (opts) => {
|
|
1355
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1356
|
+
const json = program2.opts().json;
|
|
1357
|
+
const executionId = opts.executionId;
|
|
1358
|
+
try {
|
|
1359
|
+
const summary = await client.getTraceSummary(executionId);
|
|
1360
|
+
if (json) {
|
|
1361
|
+
print({ executionId, summary }, { json: true });
|
|
1362
|
+
} else {
|
|
1363
|
+
console.log(summary);
|
|
1364
|
+
}
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1367
|
+
if (json) {
|
|
1368
|
+
print({ error: msg }, { json: true });
|
|
1369
|
+
} else {
|
|
1370
|
+
console.error(chalk7.red(`Failed to get summary: ${msg}`));
|
|
1371
|
+
}
|
|
1372
|
+
process.exitCode = 1;
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
cmd.command("stats").description("Show observation statistics for an execution").requiredOption("--execution-id <id>", "Execution ID").action(async (opts) => {
|
|
1376
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1377
|
+
const json = program2.opts().json;
|
|
1378
|
+
const executionId = opts.executionId;
|
|
1379
|
+
try {
|
|
1380
|
+
const stats = await client.getObservationStats(executionId);
|
|
1381
|
+
if (json) {
|
|
1382
|
+
print(stats, { json: true });
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
console.log();
|
|
1386
|
+
console.log(chalk7.bold(" Observation Statistics"));
|
|
1387
|
+
console.log(chalk7.dim(" " + "\u2500".repeat(40)));
|
|
1388
|
+
console.log(` ${chalk7.dim("Total Events:")} ${stats.totalEvents}`);
|
|
1389
|
+
console.log(` ${chalk7.dim("Total Spans:")} ${stats.totalSpans}`);
|
|
1390
|
+
console.log(` ${chalk7.dim("Total Generations:")} ${stats.totalGenerations}`);
|
|
1391
|
+
console.log(` ${chalk7.dim("Total Tools:")} ${stats.totalTools}`);
|
|
1392
|
+
console.log(` ${chalk7.dim("Errors:")} ${stats.errorCount > 0 ? chalk7.red(String(stats.errorCount)) : "0"}`);
|
|
1393
|
+
console.log(` ${chalk7.dim("Warnings:")} ${stats.warningCount > 0 ? chalk7.yellow(String(stats.warningCount)) : "0"}`);
|
|
1394
|
+
console.log(` ${chalk7.dim("Input Tokens:")} ${formatTokens(stats.totalInputTokens)}`);
|
|
1395
|
+
console.log(` ${chalk7.dim("Output Tokens:")} ${formatTokens(stats.totalOutputTokens)}`);
|
|
1396
|
+
console.log(` ${chalk7.dim("Total Cost:")} $${stats.totalCostUsd.toFixed(4)}`);
|
|
1397
|
+
console.log();
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1400
|
+
if (json) {
|
|
1401
|
+
print({ error: msg }, { json: true });
|
|
1402
|
+
} else {
|
|
1403
|
+
console.error(chalk7.red(`Failed to get stats: ${msg}`));
|
|
1404
|
+
}
|
|
1405
|
+
process.exitCode = 1;
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// src/commands/env.ts
|
|
1411
|
+
import chalk8 from "chalk";
|
|
1412
|
+
var envColumns = [
|
|
1413
|
+
{ key: "id", label: "ID", width: 10, format: (v) => String(v ?? "").slice(0, 8) },
|
|
1414
|
+
{ key: "name", label: "NAME", width: 20 },
|
|
1415
|
+
{ key: "hostname", label: "HOSTNAME", width: 24 },
|
|
1416
|
+
{ key: "platform", label: "PLATFORM", width: 10 },
|
|
1417
|
+
{ key: "providers", label: "PROVIDERS", width: 24, format: (v) => Array.isArray(v) ? v.join(", ") : "\u2014" },
|
|
1418
|
+
{ key: "connected", label: "CONNECTED", width: 10, format: (v) => v ? chalk8.green("\u2713") : chalk8.dim("\u2717") },
|
|
1419
|
+
{ key: "lastSeen", label: "LAST_SEEN", width: 14, format: (v) => formatRelativeTime(v) }
|
|
1420
|
+
];
|
|
1421
|
+
function registerEnvCommands(program2) {
|
|
1422
|
+
const cmd = program2.command("env").description("Manage environments and machines");
|
|
1423
|
+
cmd.command("list").description("List registered machines").action(async () => {
|
|
1424
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1425
|
+
const json = program2.opts().json;
|
|
1426
|
+
const machines = await client.listMachines();
|
|
1427
|
+
const active = machines.filter((m) => !m.isRevoked);
|
|
1428
|
+
const formatted = active.map((m) => ({
|
|
1429
|
+
id: m.id,
|
|
1430
|
+
name: m.name,
|
|
1431
|
+
hostname: m.hostname,
|
|
1432
|
+
platform: m.platform,
|
|
1433
|
+
providers: m.providers,
|
|
1434
|
+
connected: m.isConnected,
|
|
1435
|
+
lastSeen: m.lastSeenAt,
|
|
1436
|
+
environmentType: m.environmentType
|
|
1437
|
+
}));
|
|
1438
|
+
print(formatted, { json, columns: envColumns });
|
|
1439
|
+
});
|
|
1440
|
+
cmd.command("show <id>").description("Show machine details").action(async (id) => {
|
|
1441
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1442
|
+
const json = program2.opts().json;
|
|
1443
|
+
let machine;
|
|
1444
|
+
try {
|
|
1445
|
+
machine = await client.resolveMachine(id);
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1448
|
+
if (json) {
|
|
1449
|
+
print({ error: msg }, { json: true });
|
|
1450
|
+
} else {
|
|
1451
|
+
console.error(chalk8.red(msg));
|
|
1452
|
+
}
|
|
1453
|
+
process.exitCode = 1;
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (json) {
|
|
1457
|
+
print(machine, { json: true });
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
console.log(chalk8.bold(`Machine: ${machine.name}
|
|
1461
|
+
`));
|
|
1462
|
+
const fields = [
|
|
1463
|
+
["ID", machine.id],
|
|
1464
|
+
["Name", machine.name],
|
|
1465
|
+
["Hostname", machine.hostname],
|
|
1466
|
+
["Platform", machine.platform],
|
|
1467
|
+
["Environment", machine.environmentType],
|
|
1468
|
+
["Providers", (machine.providers ?? []).join(", ") || "\u2014"],
|
|
1469
|
+
["Connected", machine.isConnected ? chalk8.green("Yes") : chalk8.dim("No")],
|
|
1470
|
+
["Revoked", machine.isRevoked ? chalk8.red("Yes") : "No"],
|
|
1471
|
+
["Workspace ID", machine.workspaceId ?? "\u2014"],
|
|
1472
|
+
["Registered", machine.registeredAt ?? "\u2014"],
|
|
1473
|
+
["Last Seen", formatRelativeTime(machine.lastSeenAt)]
|
|
1474
|
+
];
|
|
1475
|
+
for (const [label, value] of fields) {
|
|
1476
|
+
console.log(` ${chalk8.dim(label.padEnd(14))} ${value}`);
|
|
1477
|
+
}
|
|
1478
|
+
if (machine.metadata && Object.keys(machine.metadata).length > 0) {
|
|
1479
|
+
console.log(`
|
|
1480
|
+
${chalk8.dim("Metadata:")}`);
|
|
1481
|
+
for (const [k, v] of Object.entries(machine.metadata)) {
|
|
1482
|
+
console.log(` ${chalk8.dim(k)}: ${v}`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (machine.providerConfigs && Array.isArray(machine.providerConfigs) && machine.providerConfigs.length > 0) {
|
|
1486
|
+
console.log(`
|
|
1487
|
+
${chalk8.dim("Provider Configs:")}`);
|
|
1488
|
+
for (const cfg of machine.providerConfigs) {
|
|
1489
|
+
const enabled = cfg.enabled ? chalk8.green("enabled") : chalk8.dim("disabled");
|
|
1490
|
+
console.log(` ${cfg.provider}: ${enabled}${cfg.defaultModel ? ` (model: ${cfg.defaultModel})` : ""}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
cmd.command("remove <id>").description("Remove/revoke a registered machine").action(async (id) => {
|
|
1495
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1496
|
+
const json = program2.opts().json;
|
|
1497
|
+
let machine;
|
|
1498
|
+
try {
|
|
1499
|
+
machine = await client.resolveMachine(id);
|
|
1500
|
+
} catch (err) {
|
|
1501
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1502
|
+
if (json) {
|
|
1503
|
+
print({ error: msg }, { json: true });
|
|
1504
|
+
} else {
|
|
1505
|
+
console.error(chalk8.red(msg));
|
|
1506
|
+
}
|
|
1507
|
+
process.exitCode = 1;
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
await client.revokeMachine(machine.id);
|
|
1512
|
+
if (json) {
|
|
1513
|
+
print({ ok: true, machineId: machine.id, name: machine.name }, { json: true });
|
|
1514
|
+
} else {
|
|
1515
|
+
console.log(chalk8.green(`Revoked machine "${machine.name}" (${machine.id.slice(0, 8)})`));
|
|
1516
|
+
}
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1519
|
+
if (json) {
|
|
1520
|
+
print({ error: errMsg }, { json: true });
|
|
1521
|
+
} else {
|
|
1522
|
+
console.error(chalk8.red(`Failed to revoke machine: ${errMsg}`));
|
|
1523
|
+
}
|
|
1524
|
+
process.exitCode = 1;
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
cmd.command("set-default <id>").description("Set default machine for task dispatch").action(async (id) => {
|
|
1528
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1529
|
+
const json = program2.opts().json;
|
|
1530
|
+
let machine;
|
|
1531
|
+
try {
|
|
1532
|
+
machine = await client.resolveMachine(id);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1535
|
+
if (json) {
|
|
1536
|
+
print({ error: msg }, { json: true });
|
|
1537
|
+
} else {
|
|
1538
|
+
console.error(chalk8.red(msg));
|
|
1539
|
+
}
|
|
1540
|
+
process.exitCode = 1;
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
saveConfig({ defaultMachineId: machine.id });
|
|
1544
|
+
if (json) {
|
|
1545
|
+
print({ ok: true, defaultMachineId: machine.id, name: machine.name }, { json: true });
|
|
1546
|
+
} else {
|
|
1547
|
+
console.log(chalk8.green(`Default machine set to "${machine.name}" (${machine.id.slice(0, 8)})`));
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
cmd.command("status").description("Show relay server status and summary").action(async () => {
|
|
1551
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1552
|
+
const json = program2.opts().json;
|
|
1553
|
+
try {
|
|
1554
|
+
const status = await client.getRelayStatus();
|
|
1555
|
+
if (json) {
|
|
1556
|
+
print(status, { json: true });
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
console.log();
|
|
1560
|
+
console.log(chalk8.bold(" Relay Server Status"));
|
|
1561
|
+
console.log(chalk8.dim(" " + "\u2500".repeat(40)));
|
|
1562
|
+
const server = status.server;
|
|
1563
|
+
if (server) {
|
|
1564
|
+
console.log(` ${chalk8.dim("Running:")} ${server.running ? chalk8.green("Yes") : chalk8.red("No")}`);
|
|
1565
|
+
if (server.port) console.log(` ${chalk8.dim("Port:")} ${server.port}`);
|
|
1566
|
+
}
|
|
1567
|
+
const machines = status.machines;
|
|
1568
|
+
if (machines) {
|
|
1569
|
+
console.log();
|
|
1570
|
+
console.log(chalk8.bold(" Machines"));
|
|
1571
|
+
console.log(` ${chalk8.dim("Total:")} ${machines.total ?? 0}`);
|
|
1572
|
+
console.log(` ${chalk8.dim("Connected:")} ${machines.connected ?? 0}`);
|
|
1573
|
+
console.log(` ${chalk8.dim("Available:")} ${machines.available ?? 0}`);
|
|
1574
|
+
}
|
|
1575
|
+
const redis = status.redis;
|
|
1576
|
+
if (redis) {
|
|
1577
|
+
console.log();
|
|
1578
|
+
console.log(` ${chalk8.dim("Redis:")} ${redis.connected ? chalk8.green("Connected") : chalk8.dim("Not connected")}`);
|
|
1579
|
+
}
|
|
1580
|
+
if (status.timestamp) {
|
|
1581
|
+
console.log(` ${chalk8.dim("Timestamp:")} ${status.timestamp}`);
|
|
1582
|
+
}
|
|
1583
|
+
console.log();
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1586
|
+
if (json) {
|
|
1587
|
+
print({ error: msg }, { json: true });
|
|
1588
|
+
} else {
|
|
1589
|
+
console.error(chalk8.red(`Failed to get relay status: ${msg}`));
|
|
1590
|
+
}
|
|
1591
|
+
process.exitCode = 1;
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
cmd.command("providers").description("List all providers across machines").action(async () => {
|
|
1595
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1596
|
+
const json = program2.opts().json;
|
|
1597
|
+
try {
|
|
1598
|
+
const machines = await client.listMachines();
|
|
1599
|
+
const active = machines.filter((m) => !m.isRevoked);
|
|
1600
|
+
const providers = [];
|
|
1601
|
+
for (const m of active) {
|
|
1602
|
+
if (m.providerConfigs && Array.isArray(m.providerConfigs)) {
|
|
1603
|
+
for (const cfg of m.providerConfigs) {
|
|
1604
|
+
providers.push({
|
|
1605
|
+
machine: m.name,
|
|
1606
|
+
machineId: m.id.slice(0, 8),
|
|
1607
|
+
provider: cfg.provider,
|
|
1608
|
+
enabled: cfg.enabled,
|
|
1609
|
+
model: cfg.defaultModel ?? "\u2014",
|
|
1610
|
+
connected: m.isConnected
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
} else if (m.providers && Array.isArray(m.providers)) {
|
|
1614
|
+
for (const p of m.providers) {
|
|
1615
|
+
providers.push({
|
|
1616
|
+
machine: m.name,
|
|
1617
|
+
machineId: m.id.slice(0, 8),
|
|
1618
|
+
provider: p,
|
|
1619
|
+
enabled: true,
|
|
1620
|
+
model: "\u2014",
|
|
1621
|
+
connected: m.isConnected
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (json) {
|
|
1627
|
+
print(providers, { json: true });
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const columns = [
|
|
1631
|
+
{ key: "machine", label: "MACHINE", width: 16 },
|
|
1632
|
+
{ key: "provider", label: "PROVIDER", width: 16 },
|
|
1633
|
+
{ key: "enabled", label: "ENABLED", width: 8, format: (v) => v ? chalk8.green("\u2713") : chalk8.dim("\u2717") },
|
|
1634
|
+
{ key: "model", label: "MODEL", width: 24 },
|
|
1635
|
+
{ key: "connected", label: "CONNECTED", width: 10, format: (v) => v ? chalk8.green("\u2713") : chalk8.dim("\u2717") }
|
|
1636
|
+
];
|
|
1637
|
+
print(providers, { columns });
|
|
1638
|
+
} catch (err) {
|
|
1639
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1640
|
+
if (json) {
|
|
1641
|
+
print({ error: msg }, { json: true });
|
|
1642
|
+
} else {
|
|
1643
|
+
console.error(chalk8.red(`Failed to list providers: ${msg}`));
|
|
1644
|
+
}
|
|
1645
|
+
process.exitCode = 1;
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
cmd.command("clusters").description("List HPC/SLURM clusters").action(async () => {
|
|
1649
|
+
const client = getClient(program2.opts().serverUrl);
|
|
1650
|
+
const json = program2.opts().json;
|
|
1651
|
+
try {
|
|
1652
|
+
const clusters = await client.getSlurmClusters();
|
|
1653
|
+
if (json) {
|
|
1654
|
+
print(clusters, { json: true });
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (!Array.isArray(clusters) || clusters.length === 0) {
|
|
1658
|
+
console.log(chalk8.dim(" No SLURM clusters registered."));
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const columns = [
|
|
1662
|
+
{ key: "name", label: "NAME", width: 20 },
|
|
1663
|
+
{ key: "hostname", label: "HOSTNAME", width: 24 },
|
|
1664
|
+
{ key: "gpus", label: "GPUS", width: 8 },
|
|
1665
|
+
{ key: "activeJobs", label: "ACTIVE JOBS", width: 12 },
|
|
1666
|
+
{ key: "status", label: "STATUS", width: 12 }
|
|
1667
|
+
];
|
|
1668
|
+
print(clusters, { columns });
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1671
|
+
if (json) {
|
|
1672
|
+
print({ error: msg }, { json: true });
|
|
1673
|
+
} else {
|
|
1674
|
+
console.error(chalk8.red(`Failed to list clusters: ${msg}`));
|
|
1675
|
+
}
|
|
1676
|
+
process.exitCode = 1;
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/commands/config.ts
|
|
1682
|
+
import chalk9 from "chalk";
|
|
1683
|
+
var KEY_MAP = {
|
|
1684
|
+
"server-url": "serverUrl",
|
|
1685
|
+
"default-machine": "defaultMachineId",
|
|
1686
|
+
"auth-token": "authToken",
|
|
1687
|
+
"refresh-token": "refreshToken"
|
|
1688
|
+
};
|
|
1689
|
+
var SUPPORTED_KEYS = Object.keys(KEY_MAP);
|
|
1690
|
+
function resolveKey(key) {
|
|
1691
|
+
if (KEY_MAP[key]) return KEY_MAP[key];
|
|
1692
|
+
if (Object.values(KEY_MAP).includes(key)) return key;
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
function registerConfigCommands(program2) {
|
|
1696
|
+
const cmd = program2.command("config").description("Manage CLI configuration");
|
|
1697
|
+
cmd.command("show").description("Show current configuration").action(() => {
|
|
1698
|
+
const json = program2.opts().json;
|
|
1699
|
+
const config = loadConfig();
|
|
1700
|
+
if (json) {
|
|
1701
|
+
print(config, { json: true });
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const entries = Object.entries(config);
|
|
1705
|
+
if (entries.length === 0) {
|
|
1706
|
+
console.log(chalk9.dim(" No configuration set."));
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
console.log(chalk9.bold("Configuration:\n"));
|
|
1710
|
+
for (const [key, value] of entries) {
|
|
1711
|
+
const displayValue = (key === "authToken" || key === "refreshToken") && typeof value === "string" && value.length > 8 ? value.slice(0, 4) + "..." + value.slice(-4) : String(value ?? "\u2014");
|
|
1712
|
+
console.log(` ${chalk9.dim(key.padEnd(20))} ${displayValue}`);
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
cmd.command("set <key> <value>").description(`Set a configuration value. Keys: ${SUPPORTED_KEYS.join(", ")}
|
|
1716
|
+
|
|
1717
|
+
Example: astro config set server-url http://localhost:3001`).action((key, value) => {
|
|
1718
|
+
const json = program2.opts().json;
|
|
1719
|
+
const resolved = resolveKey(key);
|
|
1720
|
+
if (!resolved) {
|
|
1721
|
+
if (json) {
|
|
1722
|
+
print({ error: `Unknown config key "${key}". Supported: ${SUPPORTED_KEYS.join(", ")}` }, { json: true });
|
|
1723
|
+
} else {
|
|
1724
|
+
console.error(chalk9.red(`Unknown config key "${key}"`));
|
|
1725
|
+
console.error(`Supported keys: ${SUPPORTED_KEYS.join(", ")}`);
|
|
1726
|
+
}
|
|
1727
|
+
process.exitCode = 1;
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
saveConfig({ [resolved]: value });
|
|
1731
|
+
if (json) {
|
|
1732
|
+
print({ ok: true, key: resolved, value }, { json: true });
|
|
1733
|
+
} else {
|
|
1734
|
+
console.log(chalk9.green(`Set ${key} = ${value}`));
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
cmd.command("get <key>").description(`Get a configuration value. Keys: ${SUPPORTED_KEYS.join(", ")}`).action((key) => {
|
|
1738
|
+
const json = program2.opts().json;
|
|
1739
|
+
const resolved = resolveKey(key);
|
|
1740
|
+
if (!resolved) {
|
|
1741
|
+
if (json) {
|
|
1742
|
+
print({ error: `Unknown config key "${key}". Supported: ${SUPPORTED_KEYS.join(", ")}` }, { json: true });
|
|
1743
|
+
} else {
|
|
1744
|
+
console.error(chalk9.red(`Unknown config key "${key}"`));
|
|
1745
|
+
console.error(`Supported keys: ${SUPPORTED_KEYS.join(", ")}`);
|
|
1746
|
+
}
|
|
1747
|
+
process.exitCode = 1;
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const config = loadConfig();
|
|
1751
|
+
const value = config[resolved];
|
|
1752
|
+
if (json) {
|
|
1753
|
+
print({ key: resolved, value: value ?? null }, { json: true });
|
|
1754
|
+
} else {
|
|
1755
|
+
if (value != null) {
|
|
1756
|
+
const displayValue = (resolved === "authToken" || resolved === "refreshToken") && typeof value === "string" && value.length > 8 ? value.slice(0, 4) + "..." + value.slice(-4) : String(value);
|
|
1757
|
+
console.log(displayValue);
|
|
1758
|
+
} else {
|
|
1759
|
+
console.log(chalk9.dim("(not set)"));
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// src/commands/auth.ts
|
|
1766
|
+
import chalk10 from "chalk";
|
|
1767
|
+
import { exec } from "child_process";
|
|
1768
|
+
import { hostname, platform } from "os";
|
|
1769
|
+
function openUrl(url) {
|
|
1770
|
+
const cmd = platform() === "darwin" ? "open" : "xdg-open";
|
|
1771
|
+
exec(`${cmd} ${JSON.stringify(url)}`, () => {
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
function sleep(ms) {
|
|
1775
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1776
|
+
}
|
|
1777
|
+
function registerAuthCommands(program2) {
|
|
1778
|
+
program2.command("login").description("Authenticate with the Astro server via device code flow").action(async () => {
|
|
1779
|
+
const json = program2.opts().json;
|
|
1780
|
+
resetConfig();
|
|
1781
|
+
const serverUrl = program2.opts().serverUrl || getServerUrl();
|
|
1782
|
+
try {
|
|
1783
|
+
const authRes = await fetch(new URL("/api/device/authorize", serverUrl).toString(), {
|
|
1784
|
+
method: "POST",
|
|
1785
|
+
headers: { "Content-Type": "application/json" },
|
|
1786
|
+
body: JSON.stringify({
|
|
1787
|
+
scopes: ["machine:connect", "machine:execute", "machine:read"],
|
|
1788
|
+
machineInfo: {
|
|
1789
|
+
hostname: hostname(),
|
|
1790
|
+
platform: platform()
|
|
1791
|
+
}
|
|
1792
|
+
})
|
|
1793
|
+
});
|
|
1794
|
+
if (!authRes.ok) {
|
|
1795
|
+
const text = await authRes.text();
|
|
1796
|
+
throw new Error(`Failed to request device code: ${text}`);
|
|
1797
|
+
}
|
|
1798
|
+
const authData = await authRes.json();
|
|
1799
|
+
if (!json) {
|
|
1800
|
+
console.log();
|
|
1801
|
+
console.log(chalk10.bold("Device Authorization"));
|
|
1802
|
+
console.log();
|
|
1803
|
+
console.log(` Code: ${chalk10.bold.cyan(authData.deviceCode)}`);
|
|
1804
|
+
console.log(` URL: ${chalk10.underline(authData.verificationUri)}`);
|
|
1805
|
+
console.log();
|
|
1806
|
+
console.log(chalk10.dim("Opening browser..."));
|
|
1807
|
+
}
|
|
1808
|
+
openUrl(authData.verificationUriComplete);
|
|
1809
|
+
let interval = authData.interval * 1e3;
|
|
1810
|
+
const deadline = Date.now() + authData.expiresIn * 1e3;
|
|
1811
|
+
while (Date.now() < deadline) {
|
|
1812
|
+
await sleep(interval);
|
|
1813
|
+
const tokenRes = await fetch(new URL("/api/device/token", serverUrl).toString(), {
|
|
1814
|
+
method: "POST",
|
|
1815
|
+
headers: { "Content-Type": "application/json" },
|
|
1816
|
+
body: JSON.stringify({
|
|
1817
|
+
userCode: authData.userCode,
|
|
1818
|
+
grantType: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1819
|
+
})
|
|
1820
|
+
});
|
|
1821
|
+
if (tokenRes.ok) {
|
|
1822
|
+
const tokenData = await tokenRes.json();
|
|
1823
|
+
saveConfig({
|
|
1824
|
+
serverUrl,
|
|
1825
|
+
authToken: tokenData.accessToken,
|
|
1826
|
+
refreshToken: tokenData.refreshToken
|
|
1827
|
+
});
|
|
1828
|
+
if (json) {
|
|
1829
|
+
print({ ok: true, expiresIn: tokenData.expiresIn }, { json: true });
|
|
1830
|
+
} else {
|
|
1831
|
+
console.log(chalk10.green("\nAuthenticated successfully!"));
|
|
1832
|
+
}
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
const errData = await tokenRes.json();
|
|
1836
|
+
switch (errData.error) {
|
|
1837
|
+
case "authorization_pending":
|
|
1838
|
+
if (!json) {
|
|
1839
|
+
process.stdout.write(chalk10.dim("."));
|
|
1840
|
+
}
|
|
1841
|
+
break;
|
|
1842
|
+
case "slow_down":
|
|
1843
|
+
interval += 5e3;
|
|
1844
|
+
break;
|
|
1845
|
+
case "access_denied":
|
|
1846
|
+
throw new Error("Authorization denied by user");
|
|
1847
|
+
case "expired_token":
|
|
1848
|
+
throw new Error("Device code expired. Please try again.");
|
|
1849
|
+
default:
|
|
1850
|
+
throw new Error(errData.errorDescription || errData.error);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
throw new Error("Device code expired. Please try again.");
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
if (json) {
|
|
1856
|
+
print({ error: err instanceof Error ? err.message : String(err) }, { json: true });
|
|
1857
|
+
} else {
|
|
1858
|
+
console.error(chalk10.red(`
|
|
1859
|
+
Login failed: ${err instanceof Error ? err.message : err}`));
|
|
1860
|
+
}
|
|
1861
|
+
process.exitCode = 1;
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
program2.command("logout").description("Clear stored authentication tokens").action(async () => {
|
|
1865
|
+
const json = program2.opts().json;
|
|
1866
|
+
const config = loadConfig();
|
|
1867
|
+
const serverUrl = getServerUrl(program2.opts().serverUrl);
|
|
1868
|
+
if (config.refreshToken) {
|
|
1869
|
+
try {
|
|
1870
|
+
await fetch(new URL("/api/device/revoke", serverUrl).toString(), {
|
|
1871
|
+
method: "POST",
|
|
1872
|
+
headers: { "Content-Type": "application/json" },
|
|
1873
|
+
body: JSON.stringify({ token: config.refreshToken })
|
|
1874
|
+
});
|
|
1875
|
+
} catch {
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
clearAuth();
|
|
1879
|
+
if (json) {
|
|
1880
|
+
print({ ok: true }, { json: true });
|
|
1881
|
+
} else {
|
|
1882
|
+
console.log(chalk10.green("Logged out."));
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
program2.command("whoami").description("Show current authentication status").action(async () => {
|
|
1886
|
+
const json = program2.opts().json;
|
|
1887
|
+
const config = loadConfig();
|
|
1888
|
+
if (!config.authToken) {
|
|
1889
|
+
if (json) {
|
|
1890
|
+
print({ authenticated: false }, { json: true });
|
|
1891
|
+
} else {
|
|
1892
|
+
console.log(chalk10.yellow("Not logged in. Run `astro login` to authenticate."));
|
|
1893
|
+
}
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const serverUrl = getServerUrl(program2.opts().serverUrl);
|
|
1897
|
+
try {
|
|
1898
|
+
const res = await fetch(new URL("/api/health", serverUrl).toString(), {
|
|
1899
|
+
headers: { Authorization: `Bearer ${config.authToken}` }
|
|
1900
|
+
});
|
|
1901
|
+
if (res.ok) {
|
|
1902
|
+
const data = await res.json();
|
|
1903
|
+
if (json) {
|
|
1904
|
+
print({ authenticated: true, server: serverUrl, ...data }, { json: true });
|
|
1905
|
+
} else {
|
|
1906
|
+
console.log(chalk10.green("Authenticated"));
|
|
1907
|
+
console.log(` Server: ${serverUrl}`);
|
|
1908
|
+
if (data.mode) console.log(` Mode: ${data.mode}`);
|
|
1909
|
+
}
|
|
1910
|
+
} else if (res.status === 401) {
|
|
1911
|
+
if (json) {
|
|
1912
|
+
print({ authenticated: false, expired: true }, { json: true });
|
|
1913
|
+
} else {
|
|
1914
|
+
console.log(chalk10.yellow("Token expired. Run `astro login` to re-authenticate."));
|
|
1915
|
+
}
|
|
1916
|
+
} else {
|
|
1917
|
+
throw new Error(`Server returned ${res.status}`);
|
|
1918
|
+
}
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
if (json) {
|
|
1921
|
+
print({ authenticated: false, error: err instanceof Error ? err.message : String(err) }, { json: true });
|
|
1922
|
+
} else {
|
|
1923
|
+
console.error(chalk10.red(`Failed to reach server: ${err instanceof Error ? err.message : err}`));
|
|
1924
|
+
}
|
|
1925
|
+
process.exitCode = 1;
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// src/commands/completion.ts
|
|
1931
|
+
function generateBashCompletion(program2) {
|
|
1932
|
+
const topLevel = program2.commands.map((c) => c.name()).join(" ");
|
|
1933
|
+
return `# bash completion for astro-cli
|
|
1934
|
+
_astro_cli_completions() {
|
|
1935
|
+
local cur prev commands
|
|
1936
|
+
COMPREPLY=()
|
|
1937
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1938
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1939
|
+
|
|
1940
|
+
# Top-level commands
|
|
1941
|
+
commands="${topLevel}"
|
|
1942
|
+
|
|
1943
|
+
case "\${prev}" in
|
|
1944
|
+
${program2.commands.map((cmd) => {
|
|
1945
|
+
const subs = cmd.commands.map((c) => c.name()).join(" ");
|
|
1946
|
+
return ` ${cmd.name()})
|
|
1947
|
+
COMPREPLY=( $(compgen -W "${subs}" -- "\${cur}") )
|
|
1948
|
+
return 0
|
|
1949
|
+
;;`;
|
|
1950
|
+
}).join("\n")}
|
|
1951
|
+
esac
|
|
1952
|
+
|
|
1953
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
1954
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
1955
|
+
return 0
|
|
1956
|
+
fi
|
|
1957
|
+
|
|
1958
|
+
# Options
|
|
1959
|
+
if [[ "\${cur}" == -* ]]; then
|
|
1960
|
+
COMPREPLY=( $(compgen -W "--json --quiet --server-url --help --version" -- "\${cur}") )
|
|
1961
|
+
return 0
|
|
1962
|
+
fi
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
complete -F _astro_cli_completions astro-cli
|
|
1966
|
+
`;
|
|
1967
|
+
}
|
|
1968
|
+
function generateZshCompletion(program2) {
|
|
1969
|
+
const groups = program2.commands.map((cmd) => {
|
|
1970
|
+
const desc = cmd.description() ?? cmd.name();
|
|
1971
|
+
return ` '${cmd.name()}:${desc.replace(/'/g, "")}'`;
|
|
1972
|
+
}).join("\n");
|
|
1973
|
+
const subcommandCases = program2.commands.map((cmd) => {
|
|
1974
|
+
const subs = cmd.commands.map((sub) => {
|
|
1975
|
+
const desc = sub.description() ?? sub.name();
|
|
1976
|
+
return ` '${sub.name()}:${desc.replace(/'/g, "")}'`;
|
|
1977
|
+
}).join("\n");
|
|
1978
|
+
if (!subs) return "";
|
|
1979
|
+
return ` ${cmd.name()})
|
|
1980
|
+
local -a subcmds
|
|
1981
|
+
subcmds=(
|
|
1982
|
+
${subs}
|
|
1983
|
+
)
|
|
1984
|
+
_describe 'subcommand' subcmds
|
|
1985
|
+
;;`;
|
|
1986
|
+
}).filter(Boolean).join("\n");
|
|
1987
|
+
return `#compdef astro-cli
|
|
1988
|
+
|
|
1989
|
+
_astro-cli() {
|
|
1990
|
+
local -a commands
|
|
1991
|
+
commands=(
|
|
1992
|
+
${groups}
|
|
1993
|
+
)
|
|
1994
|
+
|
|
1995
|
+
_arguments -C \\
|
|
1996
|
+
'--json[Machine-readable JSON output]' \\
|
|
1997
|
+
'--quiet[Suppress spinners and decorative output]' \\
|
|
1998
|
+
'--server-url[Override server URL]:url:_urls' \\
|
|
1999
|
+
'--help[Show help]' \\
|
|
2000
|
+
'--version[Show version]' \\
|
|
2001
|
+
'1:command:->command' \\
|
|
2002
|
+
'*::arg:->args'
|
|
2003
|
+
|
|
2004
|
+
case ${"$"}state in
|
|
2005
|
+
command)
|
|
2006
|
+
_describe 'command' commands
|
|
2007
|
+
;;
|
|
2008
|
+
args)
|
|
2009
|
+
case ${"$"}words[1] in
|
|
2010
|
+
${subcommandCases}
|
|
2011
|
+
esac
|
|
2012
|
+
;;
|
|
2013
|
+
esac
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
_astro-cli "$@"
|
|
2017
|
+
`;
|
|
2018
|
+
}
|
|
2019
|
+
function generateFishCompletion(program2) {
|
|
2020
|
+
const lines = [
|
|
2021
|
+
"# Fish completions for astro-cli",
|
|
2022
|
+
"",
|
|
2023
|
+
"# Disable file completions by default",
|
|
2024
|
+
"complete -c astro-cli -f",
|
|
2025
|
+
"",
|
|
2026
|
+
"# Global options",
|
|
2027
|
+
'complete -c astro-cli -l json -d "Machine-readable JSON output"',
|
|
2028
|
+
'complete -c astro-cli -l quiet -d "Suppress spinners and decorative output"',
|
|
2029
|
+
'complete -c astro-cli -l server-url -x -d "Override server URL"',
|
|
2030
|
+
""
|
|
2031
|
+
];
|
|
2032
|
+
for (const cmd of program2.commands) {
|
|
2033
|
+
const desc = cmd.description() ?? cmd.name();
|
|
2034
|
+
lines.push(`# ${cmd.name()} commands`);
|
|
2035
|
+
lines.push(`complete -c astro-cli -n "__fish_use_subcommand" -a "${cmd.name()}" -d "${desc.replace(/"/g, '\\"')}"`);
|
|
2036
|
+
for (const sub of cmd.commands) {
|
|
2037
|
+
const subDesc = sub.description() ?? sub.name();
|
|
2038
|
+
lines.push(`complete -c astro-cli -n "__fish_seen_subcommand_from ${cmd.name()}" -a "${sub.name()}" -d "${subDesc.replace(/"/g, '\\"')}"`);
|
|
2039
|
+
}
|
|
2040
|
+
lines.push("");
|
|
2041
|
+
}
|
|
2042
|
+
return lines.join("\n");
|
|
2043
|
+
}
|
|
2044
|
+
function registerCompletionCommands(program2) {
|
|
2045
|
+
program2.command("completion").description("Generate shell completion script").option("--shell <shell>", "Shell type: bash, zsh, fish", "bash").action((opts) => {
|
|
2046
|
+
switch (opts.shell) {
|
|
2047
|
+
case "bash":
|
|
2048
|
+
console.log(generateBashCompletion(program2));
|
|
2049
|
+
break;
|
|
2050
|
+
case "zsh":
|
|
2051
|
+
console.log(generateZshCompletion(program2));
|
|
2052
|
+
break;
|
|
2053
|
+
case "fish":
|
|
2054
|
+
console.log(generateFishCompletion(program2));
|
|
2055
|
+
break;
|
|
2056
|
+
default:
|
|
2057
|
+
console.error(`Unknown shell "${opts.shell}". Use bash, zsh, or fish.`);
|
|
2058
|
+
process.exitCode = 1;
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/index.ts
|
|
2064
|
+
var program = new Command();
|
|
2065
|
+
program.name("astro-cli").description("CLI for managing Astro projects, plans, tasks, and environments").version("0.1.0").option("--json", "Machine-readable JSON output").option("--quiet", "Suppress spinners and decorative output").option("--server-url <url>", "Override server URL");
|
|
2066
|
+
registerProjectCommands(program);
|
|
2067
|
+
registerPlanCommands(program);
|
|
2068
|
+
registerTaskCommands(program);
|
|
2069
|
+
registerSearchCommands(program);
|
|
2070
|
+
registerActivityCommands(program);
|
|
2071
|
+
registerTraceCommands(program);
|
|
2072
|
+
registerEnvCommands(program);
|
|
2073
|
+
registerConfigCommands(program);
|
|
2074
|
+
registerAuthCommands(program);
|
|
2075
|
+
registerCompletionCommands(program);
|
|
2076
|
+
program.parse();
|