@love-moon/conductor-cli 0.2.42 → 0.3.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/bin/conductor-fire.js +21 -3
- package/bin/conductor-issue.js +357 -0
- package/bin/conductor-project.js +436 -0
- package/bin/conductor-task.js +285 -0
- package/bin/conductor.js +25 -1
- package/package.json +9 -4
- package/src/ai-manager-handlers.js +17 -1
- package/src/daemon.js +795 -35
- package/src/entity-helpers.js +345 -0
- package/src/fire/resume.js +113 -870
- package/src/runtime-backends.js +48 -8
package/bin/conductor-fire.js
CHANGED
|
@@ -1466,10 +1466,28 @@ function buildEnv() {
|
|
|
1466
1466
|
return env;
|
|
1467
1467
|
}
|
|
1468
1468
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1469
|
+
function isBackendNotFoundError(error) {
|
|
1470
|
+
return Boolean(error && typeof error === "object" && Number(error.statusCode) === 404);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
export async function ensureTaskContext(conductor, opts) {
|
|
1474
|
+
const providedTaskId =
|
|
1475
|
+
typeof opts.providedTaskId === "string" ? opts.providedTaskId.trim() : "";
|
|
1476
|
+
if (providedTaskId) {
|
|
1477
|
+
if (typeof conductor.getTask === "function") {
|
|
1478
|
+
try {
|
|
1479
|
+
await conductor.getTask(providedTaskId);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
if (isBackendNotFoundError(error)) {
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
`CONDUCTOR_TASK_ID points to missing task ${providedTaskId}; unset CONDUCTOR_TASK_ID or use an existing task id`,
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
throw error;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1471
1489
|
return {
|
|
1472
|
-
taskId:
|
|
1490
|
+
taskId: providedTaskId,
|
|
1473
1491
|
appUrl: null,
|
|
1474
1492
|
shouldProcessInitialPrompt: Boolean(opts.initialPrompt),
|
|
1475
1493
|
initialPromptDelivery: opts.initialPrompt ? "synthetic" : "none",
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* conductor issue — entity-oriented issue management.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* list [--project ...] [--status <s>] [--limit N]
|
|
8
|
+
* show <id>
|
|
9
|
+
* create --title <t> [--description <d> | --description-file FILE | --description-stdin]
|
|
10
|
+
* [--priority P1|P2|P3] [--status backlog|doing|done]
|
|
11
|
+
* [--client-request-id <key>] [--project ...]
|
|
12
|
+
* update <id> [--title ...] [--description ...] [--priority ...] [--status ...]
|
|
13
|
+
* start <id> (alias for update --status doing)
|
|
14
|
+
* done <id> [--evidence <text>|@FILE]
|
|
15
|
+
*
|
|
16
|
+
* Global flags supported on every write subcommand:
|
|
17
|
+
* --json, --dry-run, --project, --config-file
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import process from "node:process";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
import yargs from "yargs/yargs";
|
|
25
|
+
import { hideBin } from "yargs/helpers";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
EXIT,
|
|
29
|
+
buildApis,
|
|
30
|
+
buildAuditMetadata,
|
|
31
|
+
emitDryRun,
|
|
32
|
+
exitCodeForError,
|
|
33
|
+
makeDryRunPayload,
|
|
34
|
+
pad,
|
|
35
|
+
printJson,
|
|
36
|
+
printPretty,
|
|
37
|
+
readDescription,
|
|
38
|
+
readEvidence,
|
|
39
|
+
reportError,
|
|
40
|
+
resolveProject,
|
|
41
|
+
} from "../src/entity-helpers.js";
|
|
42
|
+
|
|
43
|
+
const isMainModule = (() => {
|
|
44
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
45
|
+
const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
46
|
+
return entryFile === currentFile;
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
function buildBaseUrl(config) {
|
|
50
|
+
const raw = (config?.backendUrl || "").replace(/\/+$/, "");
|
|
51
|
+
return raw || "http://localhost";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function issueAsObject(issue) {
|
|
55
|
+
if (!issue) return null;
|
|
56
|
+
if (typeof issue.asObject === "function") return issue.asObject();
|
|
57
|
+
return {
|
|
58
|
+
id: issue.id,
|
|
59
|
+
title: issue.title,
|
|
60
|
+
status: issue.status,
|
|
61
|
+
priority: issue.priority,
|
|
62
|
+
description: issue.description,
|
|
63
|
+
projectId: issue.projectId,
|
|
64
|
+
metadata: issue.metadata,
|
|
65
|
+
createdAt: issue.createdAt,
|
|
66
|
+
updatedAt: issue.updatedAt,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseStatusList(value) {
|
|
71
|
+
if (!value) return undefined;
|
|
72
|
+
return String(value)
|
|
73
|
+
.split(",")
|
|
74
|
+
.map((entry) => entry.trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleList(argv, deps) {
|
|
79
|
+
const apis = await buildApis(deps);
|
|
80
|
+
const project = await resolveProject(apis, { env: deps.env, cwd: deps.cwd, project: argv.project });
|
|
81
|
+
const list = await apis.issues.listIssues({
|
|
82
|
+
projectId: project.id,
|
|
83
|
+
status: parseStatusList(argv.status),
|
|
84
|
+
limit: argv.limit ? Number(argv.limit) : undefined,
|
|
85
|
+
});
|
|
86
|
+
const objects = (Array.isArray(list) ? list : []).map(issueAsObject);
|
|
87
|
+
if (argv.json) {
|
|
88
|
+
printJson(deps.stdout, objects);
|
|
89
|
+
return EXIT.OK;
|
|
90
|
+
}
|
|
91
|
+
if (objects.length === 0) {
|
|
92
|
+
printPretty(deps.stdout, "(no issues)");
|
|
93
|
+
return EXIT.OK;
|
|
94
|
+
}
|
|
95
|
+
printPretty(deps.stdout, `${pad("ID", 24)} ${pad("STATUS", 9)} ${pad("PRIO", 5)} TITLE`);
|
|
96
|
+
for (const issue of objects) {
|
|
97
|
+
printPretty(
|
|
98
|
+
deps.stdout,
|
|
99
|
+
`${pad(issue.id, 24)} ${pad(issue.status, 9)} ${pad(issue.priority ?? "", 5)} ${issue.title ?? ""}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return EXIT.OK;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function handleShow(argv, deps) {
|
|
106
|
+
const apis = await buildApis(deps);
|
|
107
|
+
const issue = await apis.issues.getIssue(argv.id);
|
|
108
|
+
if (!issue) {
|
|
109
|
+
const err = new Error(`Issue not found: ${argv.id}`);
|
|
110
|
+
err.statusCode = 404;
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
const obj = issueAsObject(issue);
|
|
114
|
+
if (argv.json) {
|
|
115
|
+
printJson(deps.stdout, obj);
|
|
116
|
+
return EXIT.OK;
|
|
117
|
+
}
|
|
118
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
119
|
+
if (value === undefined || value === null) continue;
|
|
120
|
+
if (typeof value === "object") {
|
|
121
|
+
printPretty(deps.stdout, `${key}: ${JSON.stringify(value)}`);
|
|
122
|
+
} else {
|
|
123
|
+
printPretty(deps.stdout, `${key}: ${value}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return EXIT.OK;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleCreate(argv, deps) {
|
|
130
|
+
const apis = await buildApis(deps);
|
|
131
|
+
const project = await resolveProject(apis, { env: deps.env, cwd: deps.cwd, project: argv.project });
|
|
132
|
+
const description = readDescription({
|
|
133
|
+
description: argv.description,
|
|
134
|
+
descriptionFile: argv.descriptionFile,
|
|
135
|
+
descriptionStdin: argv.descriptionStdin,
|
|
136
|
+
stdin: deps.stdin,
|
|
137
|
+
});
|
|
138
|
+
const metadata = buildAuditMetadata(deps.env);
|
|
139
|
+
const body = {
|
|
140
|
+
projectId: project.id,
|
|
141
|
+
title: String(argv.title),
|
|
142
|
+
...(description !== undefined ? { description } : {}),
|
|
143
|
+
...(argv.priority ? { priority: String(argv.priority) } : {}),
|
|
144
|
+
...(argv.status ? { status: String(argv.status) } : {}),
|
|
145
|
+
...(argv.clientRequestId ? { clientRequestId: String(argv.clientRequestId) } : {}),
|
|
146
|
+
metadata,
|
|
147
|
+
};
|
|
148
|
+
if (argv.dryRun) {
|
|
149
|
+
emitDryRun(
|
|
150
|
+
deps.stdout,
|
|
151
|
+
argv.json,
|
|
152
|
+
makeDryRunPayload("POST", `${buildBaseUrl(apis.config)}/api/issues`, body),
|
|
153
|
+
);
|
|
154
|
+
return EXIT.OK;
|
|
155
|
+
}
|
|
156
|
+
const created = await apis.issues.createIssue(body);
|
|
157
|
+
const obj = issueAsObject(created);
|
|
158
|
+
if (argv.json) {
|
|
159
|
+
printJson(deps.stdout, obj);
|
|
160
|
+
return EXIT.OK;
|
|
161
|
+
}
|
|
162
|
+
printPretty(deps.stdout, `Created issue ${obj.id}: ${obj.title}`);
|
|
163
|
+
return EXIT.OK;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleUpdate(argv, deps, overrides = {}) {
|
|
167
|
+
const apis = await buildApis(deps);
|
|
168
|
+
const description = readDescription({
|
|
169
|
+
description: argv.description,
|
|
170
|
+
descriptionFile: argv.descriptionFile,
|
|
171
|
+
descriptionStdin: argv.descriptionStdin,
|
|
172
|
+
stdin: deps.stdin,
|
|
173
|
+
});
|
|
174
|
+
// `--description ""` is intentionally treated as a no-op (readDescription
|
|
175
|
+
// returns undefined). To clear an existing description, the server expects
|
|
176
|
+
// an explicit `null` — exposing that requires a follow-up flag and is out of
|
|
177
|
+
// scope here (review M5 — current behavior accepted as documented).
|
|
178
|
+
const status = overrides.status || argv.status;
|
|
179
|
+
const evidence = overrides.evidence;
|
|
180
|
+
const metadata = buildAuditMetadata(deps.env);
|
|
181
|
+
const body = {
|
|
182
|
+
...(argv.title ? { title: String(argv.title) } : {}),
|
|
183
|
+
...(description !== undefined ? { description } : {}),
|
|
184
|
+
...(argv.priority ? { priority: String(argv.priority) } : {}),
|
|
185
|
+
...(status ? { status: String(status) } : {}),
|
|
186
|
+
metadata,
|
|
187
|
+
};
|
|
188
|
+
if (Object.keys(body).filter((key) => key !== "metadata").length === 0) {
|
|
189
|
+
const err = new Error("Nothing to update: pass at least one of --title/--description/--priority/--status");
|
|
190
|
+
err.code = "ARGS";
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
if (argv.dryRun) {
|
|
194
|
+
// Surface evidence in the preview body so the user sees what the SDK will
|
|
195
|
+
// merge into `metadata.qa.evidence` server-side. The real call still
|
|
196
|
+
// round-trips the existing metadata; this preview is an approximation.
|
|
197
|
+
const previewBody = evidence === undefined
|
|
198
|
+
? body
|
|
199
|
+
: {
|
|
200
|
+
...body,
|
|
201
|
+
metadata: {
|
|
202
|
+
...body.metadata,
|
|
203
|
+
qa: { ...((body.metadata && body.metadata.qa) || {}), evidence },
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const previewOptions = evidence !== undefined
|
|
207
|
+
? { note: "preview omits server-side metadata round-trip; live PATCH merges existing metadata.qa fields" }
|
|
208
|
+
: {};
|
|
209
|
+
emitDryRun(
|
|
210
|
+
deps.stdout,
|
|
211
|
+
argv.json,
|
|
212
|
+
makeDryRunPayload(
|
|
213
|
+
"PATCH",
|
|
214
|
+
`${buildBaseUrl(apis.config)}/api/issues/${encodeURIComponent(argv.id)}`,
|
|
215
|
+
previewBody,
|
|
216
|
+
previewOptions,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
return EXIT.OK;
|
|
220
|
+
}
|
|
221
|
+
// Pick the SDK call:
|
|
222
|
+
// - If we have `evidence`, route through `updateIssueStatus` so the SDK
|
|
223
|
+
// round-trips the existing metadata and merges `qa.evidence` instead of
|
|
224
|
+
// clobbering. We pass `metadata` so the CLI's audit namespace flows
|
|
225
|
+
// through.
|
|
226
|
+
// - Otherwise call `updateIssue` so a multi-field patch (title + status)
|
|
227
|
+
// reaches the server intact (review B2: previously the CLI passed the
|
|
228
|
+
// full body as the SDK's third arg, which was ignored).
|
|
229
|
+
let updated;
|
|
230
|
+
if (evidence !== undefined && status && typeof apis.issues.updateIssueStatus === "function") {
|
|
231
|
+
updated = await apis.issues.updateIssueStatus(argv.id, String(status), {
|
|
232
|
+
evidence,
|
|
233
|
+
metadata,
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
updated = await apis.issues.updateIssue(argv.id, body);
|
|
237
|
+
}
|
|
238
|
+
const obj = issueAsObject(updated);
|
|
239
|
+
if (argv.json) {
|
|
240
|
+
printJson(deps.stdout, obj);
|
|
241
|
+
return EXIT.OK;
|
|
242
|
+
}
|
|
243
|
+
printPretty(deps.stdout, `Updated issue ${obj.id}`);
|
|
244
|
+
return EXIT.OK;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function main(argvInput = hideBin(process.argv), deps = {}) {
|
|
248
|
+
const stdout = deps.stdout || process.stdout;
|
|
249
|
+
const stderr = deps.stderr || process.stderr;
|
|
250
|
+
const env = deps.env || process.env;
|
|
251
|
+
const cwd = deps.cwd || process.cwd();
|
|
252
|
+
const consoleErr = { error: (msg) => stderr.write(`${msg}\n`) };
|
|
253
|
+
const handlerDeps = { ...deps, stdout, stderr, env, cwd };
|
|
254
|
+
|
|
255
|
+
let exitCode = EXIT.OK;
|
|
256
|
+
try {
|
|
257
|
+
await yargs(argvInput)
|
|
258
|
+
.scriptName("conductor issue")
|
|
259
|
+
.strict()
|
|
260
|
+
.help()
|
|
261
|
+
.option("json", { type: "boolean", default: false })
|
|
262
|
+
.option("dry-run", { type: "boolean", default: false })
|
|
263
|
+
.option("project", { type: "string", describe: "Project id or name override" })
|
|
264
|
+
.option("config-file", { type: "string", describe: "Path to Conductor config file" })
|
|
265
|
+
.command(
|
|
266
|
+
"list",
|
|
267
|
+
"List issues",
|
|
268
|
+
(cmd) => cmd
|
|
269
|
+
.option("status", { type: "string", describe: "Comma-separated status filter (e.g. backlog,doing)" })
|
|
270
|
+
.option("limit", { type: "number" }),
|
|
271
|
+
async (argv) => {
|
|
272
|
+
exitCode = await handleList(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
.command(
|
|
276
|
+
"show <id>",
|
|
277
|
+
"Show one issue's full detail",
|
|
278
|
+
(cmd) => cmd.positional("id", { type: "string", demandOption: true }),
|
|
279
|
+
async (argv) => {
|
|
280
|
+
exitCode = await handleShow(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
281
|
+
},
|
|
282
|
+
)
|
|
283
|
+
.command(
|
|
284
|
+
"create",
|
|
285
|
+
"Create a new issue",
|
|
286
|
+
(cmd) => cmd
|
|
287
|
+
.option("title", { type: "string", demandOption: true })
|
|
288
|
+
.option("description", { type: "string" })
|
|
289
|
+
.option("description-file", { type: "string" })
|
|
290
|
+
.option("description-stdin", { type: "boolean", default: false })
|
|
291
|
+
.option("priority", { choices: ["P1", "P2", "P3"] })
|
|
292
|
+
.option("status", { choices: ["backlog", "doing", "done"] })
|
|
293
|
+
.option("client-request-id", { type: "string" }),
|
|
294
|
+
async (argv) => {
|
|
295
|
+
exitCode = await handleCreate(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
.command(
|
|
299
|
+
"update <id>",
|
|
300
|
+
"Update an issue's fields (any subset)",
|
|
301
|
+
(cmd) => cmd
|
|
302
|
+
.positional("id", { type: "string", demandOption: true })
|
|
303
|
+
.option("title", { type: "string" })
|
|
304
|
+
.option("description", { type: "string" })
|
|
305
|
+
.option("description-file", { type: "string" })
|
|
306
|
+
.option("description-stdin", { type: "boolean", default: false })
|
|
307
|
+
.option("priority", { choices: ["P1", "P2", "P3"] })
|
|
308
|
+
.option("status", { choices: ["backlog", "doing", "done"] }),
|
|
309
|
+
async (argv) => {
|
|
310
|
+
exitCode = await handleUpdate(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
.command(
|
|
314
|
+
"start <id>",
|
|
315
|
+
"Mark issue as doing (alias for update --status doing)",
|
|
316
|
+
(cmd) => cmd.positional("id", { type: "string", demandOption: true }),
|
|
317
|
+
async (argv) => {
|
|
318
|
+
exitCode = await handleUpdate(argv, { ...handlerDeps, configFile: argv.configFile }, { status: "doing" });
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
.command(
|
|
322
|
+
"done <id>",
|
|
323
|
+
"Mark issue as done (optionally attach QA evidence)",
|
|
324
|
+
(cmd) => cmd
|
|
325
|
+
.positional("id", { type: "string", demandOption: true })
|
|
326
|
+
.option("evidence", { type: "string", describe: "Inline text or @path/to/file" }),
|
|
327
|
+
async (argv) => {
|
|
328
|
+
const evidence = readEvidence(argv.evidence);
|
|
329
|
+
exitCode = await handleUpdate(argv, { ...handlerDeps, configFile: argv.configFile }, {
|
|
330
|
+
status: "done",
|
|
331
|
+
...(evidence !== undefined ? { evidence } : {}),
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
)
|
|
335
|
+
.demandCommand(1)
|
|
336
|
+
.fail((msg, err) => {
|
|
337
|
+
if (err) {
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
stderr.write(`${msg}\n`);
|
|
341
|
+
exitCode = EXIT.ARGS;
|
|
342
|
+
})
|
|
343
|
+
.parseAsync();
|
|
344
|
+
} catch (err) {
|
|
345
|
+
exitCode = reportError(consoleErr, err);
|
|
346
|
+
}
|
|
347
|
+
return exitCode;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (isMainModule) {
|
|
351
|
+
main().then((code) => {
|
|
352
|
+
if (code !== 0) process.exit(code);
|
|
353
|
+
}).catch((err) => {
|
|
354
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
355
|
+
process.exit(exitCodeForError(err));
|
|
356
|
+
});
|
|
357
|
+
}
|