@love-moon/conductor-cli 0.2.42 → 0.3.1
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/CHANGELOG.md +133 -0
- 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 +13 -7
- 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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @love-moon/conductor-cli
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4e8d4e5: Include `CHANGELOG.md` in published npm tarballs.
|
|
8
|
+
|
|
9
|
+
The `files` array in each package's `package.json` previously only
|
|
10
|
+
listed the build output (`bin`/`src` for the CLI, `dist` for the
|
|
11
|
+
modules). npm's `files` whitelist replaces the default include set,
|
|
12
|
+
and CHANGELOG is not one of the auto-included files (only
|
|
13
|
+
`package.json`, `README*`, `LICENSE*`, and `main` are unconditional).
|
|
14
|
+
|
|
15
|
+
As a result, every release through 0.3.0 published tarballs with no
|
|
16
|
+
CHANGELOG, so a consumer running `npm install` or unpacking the brew
|
|
17
|
+
artifact had no way to see what changed in the version they just
|
|
18
|
+
installed. The repo `cli/CHANGELOG.md` and the GitHub Release body
|
|
19
|
+
remain the canonical source until 0.3.1 ships with this fix.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [4e8d4e5]
|
|
22
|
+
- @love-moon/conductor-sdk@0.3.1
|
|
23
|
+
- @love-moon/ai-sdk@0.3.1
|
|
24
|
+
- @love-moon/ai-manager@0.3.1
|
|
25
|
+
|
|
26
|
+
## 0.3.0
|
|
27
|
+
|
|
28
|
+
### Minor Changes
|
|
29
|
+
|
|
30
|
+
- be3b3cb: Project list now merges same-name git projects that share a remote URL across daemons into a single card.
|
|
31
|
+
|
|
32
|
+
- `ProjectContext.snapshot()` (SDK) now captures the origin remote URL via `git config --get remote.origin.url` and exposes a `normalizeGitRemoteUrl` helper for callers that need their own equality comparison.
|
|
33
|
+
- The daemon's `validate_project_path` response carries a new `git_remote_url` field. Old daemons that don't send it stay forward-compatible — the web server treats missing values as "unmergeable" so projects from those daemons render standalone until they're refreshed.
|
|
34
|
+
- New web API surface:
|
|
35
|
+
- `PATCH /api/projects { mergeOptOut: true }` opts a project out of auto-grouping (manual split for accidental name collisions).
|
|
36
|
+
- `PATCH /api/projects { refresh: true }` re-runs the daemon validation handshake and back-fills `git_remote_url` for projects created before this feature.
|
|
37
|
+
- `GET /api/issues?project_ids=a,b` fetches issues from multiple projects in one call; responses include `daemonHost` + `projectName` for cross-daemon attribution.
|
|
38
|
+
- Schema: two new optional columns on `projects`: `git_remote_url` (nullable string) and `merge_opt_out` (boolean, default false). Run `pnpm -C web db:push` before upgrading.
|
|
39
|
+
|
|
40
|
+
- 23ac015: Add `fire_tmux_mode` for the daemon. When enabled (via the `fire_tmux_mode: true`
|
|
41
|
+
key in `~/.conductor/config.yaml` or the `CONDUCTOR_FIRE_TMUX_MODE=true`
|
|
42
|
+
environment variable), each Fire process is launched inside a detached
|
|
43
|
+
`tmux new-session -d` so that it runs under the tmux server with no
|
|
44
|
+
parent/child relationship to the daemon. Restarting or terminating the daemon
|
|
45
|
+
no longer affects running Fire processes; explicit `stop_task` requests use
|
|
46
|
+
`tmux kill-session` to terminate the corresponding session.
|
|
47
|
+
|
|
48
|
+
If `fire_tmux_mode` is enabled but `tmux` is not installed on PATH, the daemon
|
|
49
|
+
logs a warning at startup and silently falls back to direct spawn instead of
|
|
50
|
+
failing every `create_task` with ENOENT.
|
|
51
|
+
|
|
52
|
+
If a tmux session fails to launch (e.g. duplicate session name, tmux server
|
|
53
|
+
crashed), the daemon now reports a terminal status to the backend instead of
|
|
54
|
+
leaving the task stuck on RUNNING.
|
|
55
|
+
|
|
56
|
+
Session names embed a per-spawn uniqueness suffix
|
|
57
|
+
(`conductor-fire-<taskId>-<base36-time><rand>`) so a re-spawn of the same task
|
|
58
|
+
id while a prior session is still alive does not collide. The daemon also runs
|
|
59
|
+
a periodic best-effort liveness reaper (default every 30s; override via
|
|
60
|
+
`config.TMUX_LIVENESS_POLL_MS`) that walks tmux-mode entries in
|
|
61
|
+
`activeTaskProcesses` and removes any whose session no longer exists, so the
|
|
62
|
+
in-memory map does not accumulate stale entries when Fire exits naturally
|
|
63
|
+
inside its session.
|
|
64
|
+
|
|
65
|
+
### Other Changes (retroactively documented)
|
|
66
|
+
|
|
67
|
+
The following changes shipped in `@love-moon/conductor-cli@0.3.0` but were
|
|
68
|
+
merged without a `changeset` entry and so didn't make it into the
|
|
69
|
+
auto-generated section above. See
|
|
70
|
+
`claw/lessons/arch_release-packages-pnpm-changesets-20260512.md` for the
|
|
71
|
+
process gap and the rule that every PR touching `cli/**` or `modules/**`
|
|
72
|
+
must run `npm run changeset`.
|
|
73
|
+
|
|
74
|
+
**New CLI commands**
|
|
75
|
+
|
|
76
|
+
- `cli`: `conductor project|issue|task` entity commands (RFC 0025). Adds
|
|
77
|
+
scriptable CLI access to project/issue/task CRUD against the connected
|
|
78
|
+
daemon. (`2e10756`)
|
|
79
|
+
- `cli`: `conductor project` accepts `--daemon-host` to disambiguate
|
|
80
|
+
same-named projects across multiple daemons. (`08eefee`)
|
|
81
|
+
- `cli`: `conductor project list` now prints a daemon column. (`552731b`)
|
|
82
|
+
|
|
83
|
+
**Daemon — Fire tmux mode (companion to `fire_tmux_mode`)**
|
|
84
|
+
|
|
85
|
+
- Daemon now `tmux send-keys`-friendly: env vars are propagated via
|
|
86
|
+
`tmux -e` flags so the Fire process inherits the spawn environment.
|
|
87
|
+
(`3cd3022`)
|
|
88
|
+
- Live Fire output is now visible via `tmux attach` thanks to a `tee`
|
|
89
|
+
inside the session shell. (`f07fbc6`)
|
|
90
|
+
- `stop_task` and `cleanup_task_worktree` now reap orphan tmux sessions.
|
|
91
|
+
(`1f7ef28`)
|
|
92
|
+
- A killed tmux-mode task reports `KILLED` directly from the daemon
|
|
93
|
+
instead of waiting on Fire to flip the status. (`59c6472`)
|
|
94
|
+
- `restart_task` clears stale tmux entries before re-spawning and refuses
|
|
95
|
+
with a clearer error if the session can't be acquired. (`c659663`)
|
|
96
|
+
|
|
97
|
+
**Daemon stability**
|
|
98
|
+
|
|
99
|
+
- Fix: reconcile / stale-recovery no longer kills `init` successor tasks
|
|
100
|
+
that haven't received their initial websocket message yet. (`d9258ba`)
|
|
101
|
+
- Fix: late websocket send after disconnect no longer crashes daemon
|
|
102
|
+
restart. (`a3532cc`)
|
|
103
|
+
- Fix: stale fire task attach is now guarded against double-binding.
|
|
104
|
+
(`dc73be9`)
|
|
105
|
+
|
|
106
|
+
**Worktree**
|
|
107
|
+
|
|
108
|
+
- Worktree folders are now named by branch (slugified) rather than by
|
|
109
|
+
the task id, making them human-meaningful inside repos with many
|
|
110
|
+
concurrent tasks. (`ed124b5`)
|
|
111
|
+
- The worktree scanner now skips symlinks that are git-tracked, so user
|
|
112
|
+
symlinks inside a worktree don't get treated as candidates. (`5281952`)
|
|
113
|
+
|
|
114
|
+
**Quota / accounts UI**
|
|
115
|
+
|
|
116
|
+
- Codex quota snapshots are now restored from the daemon cache on
|
|
117
|
+
refresh, so the daemon page doesn't blank out while a fresh fetch is
|
|
118
|
+
in flight. (`130bd93`)
|
|
119
|
+
|
|
120
|
+
**Internal refactor (no consumer API change)**
|
|
121
|
+
|
|
122
|
+
- `modules/ai-sdk`: resume logic has been split into per-provider
|
|
123
|
+
modules under `src/resume/<provider>.js`. The public exports
|
|
124
|
+
(`createAiSession`, `BUILT_IN_BACKENDS`, etc.) are unchanged.
|
|
125
|
+
Source-level only; the published `@love-moon/ai-sdk` version pinned
|
|
126
|
+
in `cli@0.3.0`'s manifest is still `0.2.42`, so consumer behavior is
|
|
127
|
+
identical to before — the refactor will reach npm with the next
|
|
128
|
+
ai-sdk release that includes a `changeset`. (`846f05a`)
|
|
129
|
+
|
|
130
|
+
### Patch Changes
|
|
131
|
+
|
|
132
|
+
- Updated dependencies [be3b3cb]
|
|
133
|
+
- @love-moon/conductor-sdk@0.3.0
|
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
|
+
}
|