@detergent-software/conductor 3.0.0-dev.8 → 3.0.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/CHANGELOG.md +482 -0
- package/README.md +275 -125
- package/build/brief-cli.js +5 -12
- package/build/brief-cli.js.map +1 -1
- package/build/brief.js +4 -3
- package/build/brief.js.map +1 -1
- package/build/broker-cli.js +42 -15
- package/build/broker-cli.js.map +1 -1
- package/build/broker.js +1 -1
- package/build/broker.js.map +1 -1
- package/build/cli.js +221 -63
- package/build/cli.js.map +1 -1
- package/build/clones.js +92 -0
- package/build/clones.js.map +1 -0
- package/build/code-host-cli.js +172 -40
- package/build/code-host-cli.js.map +1 -1
- package/build/code-host-service.js +35 -2
- package/build/code-host-service.js.map +1 -1
- package/build/code-host.js +138 -16
- package/build/code-host.js.map +1 -1
- package/build/conditions.js +33 -16
- package/build/conditions.js.map +1 -1
- package/build/config.js +58 -28
- package/build/config.js.map +1 -1
- package/build/console.js +17 -27
- package/build/console.js.map +1 -1
- package/build/credentials.js +86 -54
- package/build/credentials.js.map +1 -1
- package/build/daemon.js +54 -65
- package/build/daemon.js.map +1 -1
- package/build/file-lock.js +155 -40
- package/build/file-lock.js.map +1 -1
- package/build/follow-ups.js +2 -2
- package/build/follow-ups.js.map +1 -1
- package/build/help.js +241 -83
- package/build/help.js.map +1 -1
- package/build/history.js +10 -4
- package/build/history.js.map +1 -1
- package/build/host/credential-manager.js +109 -0
- package/build/host/credential-manager.js.map +1 -0
- package/build/host/keychain.js +44 -0
- package/build/host/keychain.js.map +1 -0
- package/build/host/launchd.js +118 -0
- package/build/host/launchd.js.map +1 -0
- package/build/host/libsecret.js +40 -0
- package/build/host/libsecret.js.map +1 -0
- package/build/host/macos-terminal.js +63 -0
- package/build/host/macos-terminal.js.map +1 -0
- package/build/host/platform.js +4 -0
- package/build/host/platform.js.map +1 -0
- package/build/host/secrets.js +46 -0
- package/build/host/secrets.js.map +1 -0
- package/build/host/shell.js +26 -0
- package/build/host/shell.js.map +1 -0
- package/build/host/startup.js +26 -0
- package/build/host/startup.js.map +1 -0
- package/build/host/systemd.js +91 -0
- package/build/host/systemd.js.map +1 -0
- package/build/host/task-scheduler.js +78 -0
- package/build/host/task-scheduler.js.map +1 -0
- package/build/host/terminal.js +46 -0
- package/build/host/terminal.js.map +1 -0
- package/build/host/tmux.js +39 -0
- package/build/host/tmux.js.map +1 -0
- package/build/host/windows-terminal.js +46 -0
- package/build/host/windows-terminal.js.map +1 -0
- package/build/in-process.js +6 -5
- package/build/in-process.js.map +1 -1
- package/build/inbox.js +57 -39
- package/build/inbox.js.map +1 -1
- package/build/init.js +509 -0
- package/build/init.js.map +1 -0
- package/build/landing.js +107 -10
- package/build/landing.js.map +1 -1
- package/build/lib/providers/ado/code-host.js +5 -1
- package/build/lib/providers/ado/code-host.js.map +1 -1
- package/build/lib/providers/ado/pipelines.js +138 -26
- package/build/lib/providers/ado/pipelines.js.map +1 -1
- package/build/lib/providers/ado/trigger.js +5 -0
- package/build/lib/providers/ado/trigger.js.map +1 -1
- package/build/lib/providers/ado/writes.js +17 -2
- package/build/lib/providers/ado/writes.js.map +1 -1
- package/build/lib/providers/github/actions.js +90 -3
- package/build/lib/providers/github/actions.js.map +1 -1
- package/build/lib/providers/github/code-host.js +2 -0
- package/build/lib/providers/github/code-host.js.map +1 -1
- package/build/lib/providers/github/writes.js +25 -2
- package/build/lib/providers/github/writes.js.map +1 -1
- package/build/lib/providers/index.js +3 -2
- package/build/lib/providers/index.js.map +1 -1
- package/build/lib/providers/npm/stage.js +14 -1
- package/build/lib/providers/npm/stage.js.map +1 -1
- package/build/lib/providers/timing.js +10 -0
- package/build/lib/providers/timing.js.map +1 -0
- package/build/lib/providers/verdicts/pr.js +47 -6
- package/build/lib/providers/verdicts/pr.js.map +1 -1
- package/build/lib/providers/verdicts/runs.js +12 -2
- package/build/lib/providers/verdicts/runs.js.map +1 -1
- package/build/logins.js +13 -6
- package/build/logins.js.map +1 -1
- package/build/manager.js +121 -55
- package/build/manager.js.map +1 -1
- package/build/note.js +5 -15
- package/build/note.js.map +1 -1
- package/build/notes.js +130 -23
- package/build/notes.js.map +1 -1
- package/build/notify.js +104 -42
- package/build/notify.js.map +1 -1
- package/build/npm-reads.js +151 -22
- package/build/npm-reads.js.map +1 -1
- package/build/ownership.js +2 -2
- package/build/paths.js +44 -1
- package/build/paths.js.map +1 -1
- package/build/plugin.js +4 -2
- package/build/plugin.js.map +1 -1
- package/build/profile-fields.js +153 -0
- package/build/profile-fields.js.map +1 -0
- package/build/profile-layers.js +264 -0
- package/build/profile-layers.js.map +1 -0
- package/build/profile.js +107 -51
- package/build/profile.js.map +1 -1
- package/build/project-check-cli.js +17 -19
- package/build/project-check-cli.js.map +1 -1
- package/build/project-check.js +166 -104
- package/build/project-check.js.map +1 -1
- package/build/protocol.js +5 -21
- package/build/protocol.js.map +1 -1
- package/build/pushover.js +5 -5
- package/build/pushover.js.map +1 -1
- package/build/queue.js +4 -4
- package/build/queue.js.map +1 -1
- package/build/registry.js +195 -36
- package/build/registry.js.map +1 -1
- package/build/release-kinds.js +34 -0
- package/build/release-kinds.js.map +1 -0
- package/build/rule-actions.js +8 -6
- package/build/rule-actions.js.map +1 -1
- package/build/rules-service.js +104 -47
- package/build/rules-service.js.map +1 -1
- package/build/rules.js +23 -23
- package/build/rules.js.map +1 -1
- package/build/run.js +20 -7
- package/build/run.js.map +1 -1
- package/build/runs.js +218 -68
- package/build/runs.js.map +1 -1
- package/build/scheduler.js +89 -13
- package/build/scheduler.js.map +1 -1
- package/build/search.js +2 -1
- package/build/search.js.map +1 -1
- package/build/server.js +51 -14
- package/build/server.js.map +1 -1
- package/build/spend.js +1 -1
- package/build/spend.js.map +1 -1
- package/build/store.js +79 -20
- package/build/store.js.map +1 -1
- package/build/supervise.js +1 -1
- package/build/supervise.js.map +1 -1
- package/build/timeline.js +104 -0
- package/build/timeline.js.map +1 -0
- package/build/tracker-reads-cli.js +4 -5
- package/build/tracker-reads-cli.js.map +1 -1
- package/build/trackers.js +56 -10
- package/build/trackers.js.map +1 -1
- package/build/train-cli.js +6 -5
- package/build/train-cli.js.map +1 -1
- package/build/train.js +2 -2
- package/build/train.js.map +1 -1
- package/build/upgrade.js +8 -9
- package/build/upgrade.js.map +1 -1
- package/build/wait.js +10 -10
- package/build/wait.js.map +1 -1
- package/build/watchers/ado.js +42 -19
- package/build/watchers/ado.js.map +1 -1
- package/build/watchers/github.js +18 -4
- package/build/watchers/github.js.map +1 -1
- package/build/watchers/index.js +2 -2
- package/build/watchers/index.js.map +1 -1
- package/build/watchers/linear-queue.js +47 -14
- package/build/watchers/linear-queue.js.map +1 -1
- package/build/watchers/linear-writes.js +26 -12
- package/build/watchers/linear-writes.js.map +1 -1
- package/build/watchers/linear.js +2 -2
- package/build/watchers/linear.js.map +1 -1
- package/build/watchers/source.js +5 -1
- package/build/watchers/source.js.map +1 -1
- package/build/webhook.js +80 -0
- package/build/webhook.js.map +1 -0
- package/build/why.js +1 -1
- package/build/why.js.map +1 -1
- package/build/worktrees.js +56 -93
- package/build/worktrees.js.map +1 -1
- package/package.json +61 -61
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/model.ts +46 -6
- package/plugin/hooks/register.tsx +41 -2
- package/plugin/skills/conductor-console/SKILL.md +26 -27
- package/plugin/skills/work-item/SKILL.md +58 -54
- package/plugin/skills/work-item/trackers/ado-boards.md +2 -2
- package/plugin/skills/work-item/trackers/github-issues.md +1 -1
- package/plugin/skills/work-item/trackers/linear.md +2 -2
- package/plugin/types/index.d.ts +1 -1
- package/protocol/PROTOCOL.md +3 -9
- package/build/checkout-sync.js +0 -249
- package/build/checkout-sync.js.map +0 -1
- package/build/credential.js +0 -66
- package/build/credential.js.map +0 -1
- package/build/startup.js +0 -92
- package/build/startup.js.map +0 -1
- package/build/tab.js +0 -32
- package/build/tab.js.map +0 -1
package/build/cli.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import {
|
|
4
|
+
import { delimiter, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { briefCommand } from "./brief-cli.js";
|
|
9
9
|
import { heldLocks, lockCommand, lockLine, portsCommand } from "./broker-cli.js";
|
|
10
|
+
import { trustStep } from "./claude.js";
|
|
10
11
|
import { callApi, NotRunningError, ping, readEndpoint } from "./client.js";
|
|
11
|
-
import { branchCommand, checkoutCommand, fileCommand, landCommand, modeCommand, prCreateCommand, prThreadsCommand, replyCommand, runsCommand, stateCommand, } from "./code-host-cli.js";
|
|
12
|
+
import { branchCommand, checkoutCommand, fileCommand, landCommand, modeCommand, prAbandonCommand, prCreateCommand, prStatusCommand, prThreadsCommand, replyCommand, runsCommand, stateCommand, timelineCommand, } from "./code-host-cli.js";
|
|
12
13
|
import { configSchema, loadConfig } from "./config.js";
|
|
13
|
-
import { claimConsole, CONSOLE_WINDOW, consoleArgs, consoleEnv,
|
|
14
|
+
import { claimConsole, CONSOLE_WINDOW, consoleArgs, consoleEnv, consoleTab, escalationLines, escalationSchema, handledSchema, } from "./console.js";
|
|
14
15
|
import { AlreadyRunningError, logCrashes, startDaemon } from "./daemon.js";
|
|
15
16
|
import { ConductorError } from "./errors.js";
|
|
16
17
|
import { eventsCommand } from "./events.js";
|
|
17
18
|
import { commandHelp, isCommand, listing, terminalStyle, terminalWidth, unknownCommand, usageError } from "./help.js";
|
|
18
19
|
import { flagWindow, historyLines, historyReport, historySchema, parseRange, retroMarkdown, } from "./history.js";
|
|
20
|
+
import { startup } from "./host/startup.js";
|
|
21
|
+
import { terminal } from "./host/terminal.js";
|
|
19
22
|
import { inboxLines } from "./inbox.js";
|
|
23
|
+
import { initProject, terminalAsk } from "./init.js";
|
|
20
24
|
import { parseJsonc } from "./jsonc.js";
|
|
21
25
|
import { npmStage } from "./lib/providers/index.js";
|
|
22
26
|
import { processAlive, readPid } from "./lock.js";
|
|
@@ -29,28 +33,27 @@ import { overrideViewSchema, PAUSE } from "./overrides.js";
|
|
|
29
33
|
import { packageInfo } from "./package-info.js";
|
|
30
34
|
import { conductorPaths } from "./paths.js";
|
|
31
35
|
import { refreshConductorPlugin } from "./plugin.js";
|
|
32
|
-
import {
|
|
36
|
+
import { profileLines, projectProfile } from "./profile-layers.js";
|
|
37
|
+
import { projectCheckCommand, projectCheckReport } from "./project-check-cli.js";
|
|
33
38
|
import { noProject } from "./project-check.js";
|
|
34
39
|
import { pushoverCredential, pushoverSender } from "./pushover.js";
|
|
35
|
-
import { registerProject } from "./registry.js";
|
|
40
|
+
import { cloneProject, registerProject } from "./registry.js";
|
|
36
41
|
import { rulesLines, rulesService, rulesTestResultSchema, rulesViewSchema, testLines, } from "./rules-service.js";
|
|
37
42
|
import { resolveUntil } from "./rules.js";
|
|
38
43
|
import { outputOf, run } from "./run.js";
|
|
39
44
|
import { queueLines, queueViewSchema } from "./scheduler.js";
|
|
40
45
|
import { awaitStart, serveDetached, ServeStartError } from "./serve.js";
|
|
41
46
|
import { localDay } from "./spend.js";
|
|
42
|
-
import { installStartup, removeStartup, runStartup, startupRunsCli, startupSupervises, TASK_NAME } from "./startup.js";
|
|
43
47
|
import { answerStopRequest, dropStaleStopRequest, pendingStopRequest, STOP_POLL_MS, stopSupervisor, } from "./stop-request.js";
|
|
44
48
|
import { inboxItemSchema, openStore, projectSchema, waitSchema, workerSchema, } from "./store.js";
|
|
45
49
|
import { RESTART_ENV, restartInfoSchema, SERVE_LOCKED_EXIT, serveChild, supervise, SUPERVISE_POLICY, } from "./supervise.js";
|
|
46
|
-
import { openWindowsTerminal } from "./tab.js";
|
|
47
50
|
import { commentsCommand, notesCommand, searchCommand } from "./tracker-reads-cli.js";
|
|
48
51
|
import { trainCommand } from "./train-cli.js";
|
|
49
52
|
import { parseTag, restartDaemon, tagVersion, upgrade } from "./upgrade.js";
|
|
50
53
|
import { waitCommand } from "./wait.js";
|
|
51
54
|
import { whyLines, whySchema } from "./why.js";
|
|
52
55
|
const STOP_WAIT_MS = 10_000;
|
|
53
|
-
// A restarted daemon opens its store and binds the port; under the logon
|
|
56
|
+
// A restarted daemon opens its store and binds the port; under the logon startup node starts cold.
|
|
54
57
|
const START_WAIT_MS = 30_000;
|
|
55
58
|
const NPM_INSTALL_TIMEOUT_MS = 5 * 60_000;
|
|
56
59
|
// A launch makes a worktree (pnpm install) before the session starts.
|
|
@@ -62,6 +65,9 @@ const RETIRE_TIMEOUT_MS = 12 * 60_000;
|
|
|
62
65
|
const ATTACH_TIMEOUT_MS = 9 * 60_000;
|
|
63
66
|
// Approve and revise ask Linear twice (the issue, then the comment), each within 20 s.
|
|
64
67
|
const ANSWER_TIMEOUT_MS = 60_000;
|
|
68
|
+
// A revise of an approved plan adds up to three brief reads (the tracker and the PR, each within
|
|
69
|
+
// 20 s) 2 s apart after the comment, so a slow tracker doesn't cut off an answer whose comment landed.
|
|
70
|
+
const REVISE_TIMEOUT_MS = 3 * 60_000;
|
|
65
71
|
const projectsBody = z.object({ projects: z.array(projectSchema) });
|
|
66
72
|
const addBody = z.object({ created: z.boolean(), project: projectSchema });
|
|
67
73
|
const removeBody = z.object({ project: projectSchema, resolved: z.array(inboxItemSchema) });
|
|
@@ -86,23 +92,43 @@ const retireBody = z.object({
|
|
|
86
92
|
work: workSchema.extend({ checkouts: z.record(z.string(), workSchema).optional() }).optional(),
|
|
87
93
|
worker: workerSchema,
|
|
88
94
|
});
|
|
89
|
-
|
|
95
|
+
// hint: from ATK-529 on; an older daemon sends none.
|
|
96
|
+
const attachBody = z.object({
|
|
97
|
+
hint: z.string().nullable().default(null),
|
|
98
|
+
resumed: z.boolean(),
|
|
99
|
+
tab: z.string().nullable(),
|
|
100
|
+
worker: workerSchema,
|
|
101
|
+
});
|
|
90
102
|
const inboxBody = z.object({ items: z.array(inboxItemSchema) });
|
|
91
103
|
const answerBody = z.object({
|
|
92
104
|
comment: z.object({ id: z.string(), url: z.string().nullable() }).nullable(),
|
|
93
105
|
item: inboxItemSchema,
|
|
94
106
|
waits: z.array(waitSchema),
|
|
95
107
|
});
|
|
108
|
+
const reviseAnswer = z.object({
|
|
109
|
+
revise: z.object({
|
|
110
|
+
comment: z.object({ id: z.string(), url: z.string().nullable() }),
|
|
111
|
+
item: z.string(),
|
|
112
|
+
landed: z.boolean(),
|
|
113
|
+
pending: z.string().nullable(),
|
|
114
|
+
revised: z.boolean(),
|
|
115
|
+
woken: z.number(),
|
|
116
|
+
worker: z.boolean(),
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
96
119
|
const answerRefusal = z.object({
|
|
97
120
|
details: z.array(z.string()).optional(),
|
|
98
121
|
error: z.string(),
|
|
99
122
|
posted: z.enum(['no', 'unknown']).optional(),
|
|
100
123
|
});
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
// `conduct project add <url | path> [--name <name>]`: the clone is made here, where its progress
|
|
125
|
+
// shows and no API timeout applies, then the daemon registers it (or the store, with none running).
|
|
126
|
+
// `next`: false for `conduct init`, which says what comes next itself.
|
|
127
|
+
async function addProject(paths, source, name, next = true) {
|
|
128
|
+
const made = await cloneProject(paths, source, { ...(name === undefined ? {} : { name }), say: out });
|
|
103
129
|
let result;
|
|
104
130
|
try {
|
|
105
|
-
result = addBody.parse(await expectOk(callApi(paths, { body: {
|
|
131
|
+
result = addBody.parse(await expectOk(callApi(paths, { body: { name: made.name }, method: 'POST', path: '/projects' })));
|
|
106
132
|
}
|
|
107
133
|
catch (error) {
|
|
108
134
|
if (!(error instanceof NotRunningError))
|
|
@@ -110,14 +136,25 @@ async function addProject(paths, path) {
|
|
|
110
136
|
// No daemon: write the store directly. The daemon reads it when it starts.
|
|
111
137
|
// Its rules are checked against config.json here too, as the daemon would.
|
|
112
138
|
const check = rulesService({ config: loadConfig(paths.config), listProjects: () => [], paths }).checkProfile;
|
|
113
|
-
result = withStore(paths, (store) => registerProject(store,
|
|
139
|
+
result = withStore(paths, (store) => registerProject(store, paths, made.name, check));
|
|
114
140
|
}
|
|
115
141
|
out(`${result.created ? 'registered' : 'updated'} ${projectLine(result.project).trim()}`);
|
|
142
|
+
out(made.cloned ? `cloned ${made.url} into ${made.clone}` : `fetched ${made.clone}, the clone it has`);
|
|
143
|
+
out(`its machine profile, which wins over the repo's: ${result.project.profilePath}`);
|
|
144
|
+
// Claude Code trusts a worktree by its repository, and nobody has run Claude in a new clone yet.
|
|
145
|
+
if (made.cloned && next)
|
|
146
|
+
out(`next: ${trustStep(made.clone)}, then \`conduct project check ${made.name}\``);
|
|
147
|
+
return made;
|
|
116
148
|
}
|
|
117
|
-
// `conduct approve|revise|done`:
|
|
118
|
-
// by the item whose plan waits.
|
|
149
|
+
// `conduct approve|revise|done`: the operator's answer to an inbox item, by id, or for approve and revise
|
|
150
|
+
// by the item whose plan waits. A revise of an item with no plan waiting revises its approved plan.
|
|
119
151
|
async function answer(paths, ref, request) {
|
|
120
152
|
const target = request.action === 'done' ? inboxId(ref) : await planItem(paths, ref);
|
|
153
|
+
if (target === null) {
|
|
154
|
+
if (request.action === 'revise')
|
|
155
|
+
return reviseApproved(paths, ref, request.text);
|
|
156
|
+
throw new ConductorError(`no plan of ${ref} waits in the inbox`, ['`conduct inbox` lists what waits']);
|
|
157
|
+
}
|
|
121
158
|
const { body, status } = await callApi(paths, {
|
|
122
159
|
body: request,
|
|
123
160
|
method: 'POST',
|
|
@@ -151,6 +188,8 @@ async function attach(paths, ref) {
|
|
|
151
188
|
out(`${result.resumed ? 'woke' : 'attached to'} ${worker.name} (session ${result.worker.handle ?? '?'})`);
|
|
152
189
|
if (result.tab)
|
|
153
190
|
out(`no tab (${result.tab}); open it yourself: claude attach ${result.worker.handle ?? worker.name}`);
|
|
191
|
+
else if (result.hint)
|
|
192
|
+
out(`to see its tab: ${result.hint}`);
|
|
154
193
|
}
|
|
155
194
|
// The pid of the daemon once it answers /health, for upgrade's restart. failure: the detached
|
|
156
195
|
// serve's spawn error, which ends the wait early.
|
|
@@ -208,34 +247,33 @@ async function consoleCommand(paths, options) {
|
|
|
208
247
|
});
|
|
209
248
|
return;
|
|
210
249
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
'run `conduct console --here` in a terminal of your own',
|
|
214
|
-
]);
|
|
215
|
-
}
|
|
216
|
-
const args = consoleTabArgs(paths.console, process.execPath, realpathSync(fileURLToPath(import.meta.url)));
|
|
250
|
+
const where = terminal({ choice: loadConfig(paths.config).workers.terminal });
|
|
251
|
+
const tab = consoleTab(paths.console, process.execPath, realpathSync(fileURLToPath(import.meta.url)), process.env);
|
|
217
252
|
await claimConsole(paths, options.force);
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
throw new ConductorError(
|
|
221
|
-
|
|
253
|
+
const failed = await where.open(tab);
|
|
254
|
+
if (failed !== null)
|
|
255
|
+
throw new ConductorError(`could not open the console tab in ${where.name}`, [
|
|
256
|
+
failed,
|
|
222
257
|
'run `conduct console --here --force` in a terminal instead',
|
|
223
258
|
]);
|
|
224
|
-
|
|
259
|
+
const hint = where.attachHint(CONSOLE_WINDOW);
|
|
260
|
+
out(`opened the console in ${paths.console} (${hint === null ? `${where.name} window "${CONSOLE_WINDOW}"` : `${where.name}: ${hint}`})`);
|
|
225
261
|
}
|
|
226
262
|
// How `conduct start` and upgrade's restart start the daemon detached from this process.
|
|
227
263
|
function daemonStarter(paths, cliPath) {
|
|
228
264
|
let failure = () => undefined;
|
|
265
|
+
const task = startup();
|
|
229
266
|
return {
|
|
230
267
|
awaitDaemon: () => awaitDaemon(paths, failure),
|
|
231
|
-
runTask:
|
|
268
|
+
runTask: () => task.run(),
|
|
232
269
|
serveDetached: () => {
|
|
233
270
|
// --fail-fast: a first serve that can't start ends the supervisor, so the start says why at once.
|
|
234
271
|
failure = serveDetached(paths.home, process.execPath, [cliPath, 'supervise', '--fail-fast']);
|
|
235
272
|
},
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
273
|
+
taskName: task.name,
|
|
274
|
+
// The startup's daemon uses ~/.conductor, so it can't start one kept in CONDUCTOR_HOME.
|
|
275
|
+
taskRunsCli: async () => !process.env.CONDUCTOR_HOME && (await task.runsCli(cliPath)),
|
|
276
|
+
taskSupervises: () => task.supervises(),
|
|
239
277
|
};
|
|
240
278
|
}
|
|
241
279
|
// `conduct escalations done`: the console reports back on an escalation.
|
|
@@ -360,11 +398,20 @@ function inboxId(ref) {
|
|
|
360
398
|
throw new ConductorError(`"${ref}" is not an inbox item id`, ['`conduct inbox` lists them as #<id>']);
|
|
361
399
|
return Number(id);
|
|
362
400
|
}
|
|
363
|
-
|
|
401
|
+
// `conduct install-startup`: the machine's startup (host/startup.ts) runs this build's supervise.
|
|
402
|
+
async function install() {
|
|
364
403
|
const cliPath = realpathSync(fileURLToPath(import.meta.url));
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
404
|
+
const task = startup();
|
|
405
|
+
// Less what `pnpm exec` or `npm run` put first, which a worktree's removal takes away.
|
|
406
|
+
const path = (process.env.PATH ?? '')
|
|
407
|
+
.split(delimiter)
|
|
408
|
+
.filter((dir) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/.test(dir))
|
|
409
|
+
.join(delimiter);
|
|
410
|
+
const notes = await task.install({ cliPath, nodePath: process.execPath, path });
|
|
411
|
+
out(`registered ${task.name}: ${process.execPath} ${cliPath} supervise`);
|
|
412
|
+
for (const note of notes)
|
|
413
|
+
out(note);
|
|
414
|
+
if (/[\\/](?:\.claude|\.conductor[\\/]projects[\\/][^\\/]+)[\\/]worktrees[\\/]/.test(cliPath)) {
|
|
368
415
|
out('warning: that build is in a worktree, which goes away; run install-startup from the main checkout');
|
|
369
416
|
}
|
|
370
417
|
}
|
|
@@ -399,7 +446,7 @@ async function listProjects(paths, json) {
|
|
|
399
446
|
if (json)
|
|
400
447
|
return out(JSON.stringify(projects, null, 2));
|
|
401
448
|
if (projects.length === 0)
|
|
402
|
-
return out('no projects; add one with `conduct project add <path>`');
|
|
449
|
+
return out('no projects; add one with `conduct project add <url | path>`');
|
|
403
450
|
projects.forEach((p) => out(projectLine(p)));
|
|
404
451
|
}
|
|
405
452
|
async function main(argv) {
|
|
@@ -407,7 +454,7 @@ async function main(argv) {
|
|
|
407
454
|
if (argv[0] === 'wait')
|
|
408
455
|
return argv.slice(1).some(isHelpFlag) ? help('wait') : wait(argv.slice(1));
|
|
409
456
|
// `revise` takes free text, which option parsing would cut at a word such as --force. Every word
|
|
410
|
-
// after the item is the text, quoted or not: a cut-off instruction would be posted as
|
|
457
|
+
// after the item is the text, quoted or not: a cut-off instruction would be posted as the operator's.
|
|
411
458
|
if (argv[0] === 'revise') {
|
|
412
459
|
// A lone help flag as the text is a call for help: posted, it would be a revision of "--help".
|
|
413
460
|
if (isHelpFlag(argv[1]) || (argv.length === 3 && isHelpFlag(argv[2])))
|
|
@@ -432,19 +479,22 @@ async function main(argv) {
|
|
|
432
479
|
throw usageError('escalations');
|
|
433
480
|
return escalationDone(conductorPaths(), ref, { ask, text });
|
|
434
481
|
}
|
|
435
|
-
// The first word that names a command: an option's value (--project
|
|
482
|
+
// The first word that names a command: an option's value (--project web) comes before it at times.
|
|
436
483
|
const { positionals, values } = parsing(argv.find((word) => isCommand(word)), () => parseArgs({
|
|
437
484
|
allowPositionals: true,
|
|
438
485
|
args: argv,
|
|
439
486
|
options: {
|
|
440
487
|
'allow-downgrade': { type: 'boolean' },
|
|
441
488
|
area: { type: 'string' },
|
|
489
|
+
base: { type: 'string' },
|
|
442
490
|
'blocked-by': { multiple: true, type: 'string' },
|
|
443
491
|
'body-file': { type: 'string' },
|
|
492
|
+
branch: { type: 'string' },
|
|
444
493
|
channel: { type: 'string' },
|
|
445
494
|
comments: { type: 'boolean' },
|
|
446
495
|
conductor: { type: 'string' },
|
|
447
496
|
console: { type: 'boolean' },
|
|
497
|
+
count: { type: 'string' },
|
|
448
498
|
'dry-run': { type: 'boolean' },
|
|
449
499
|
'fail-fast': { type: 'boolean' },
|
|
450
500
|
files: { type: 'string' },
|
|
@@ -452,20 +502,24 @@ async function main(argv) {
|
|
|
452
502
|
for: { type: 'string' },
|
|
453
503
|
force: { type: 'boolean' },
|
|
454
504
|
fresh: { type: 'boolean' },
|
|
505
|
+
grep: { type: 'string' },
|
|
455
506
|
help: { short: 'h', type: 'boolean' },
|
|
456
507
|
here: { type: 'boolean' },
|
|
457
508
|
human: { type: 'boolean' },
|
|
458
509
|
item: { type: 'string' },
|
|
459
510
|
json: { type: 'boolean' },
|
|
511
|
+
key: { type: 'string' },
|
|
460
512
|
kind: { type: 'string' },
|
|
461
513
|
last: { type: 'boolean' },
|
|
462
514
|
mode: { type: 'string' },
|
|
515
|
+
name: { type: 'string' },
|
|
463
516
|
'no-worktree': { type: 'boolean' },
|
|
464
517
|
owner: { type: 'string' },
|
|
465
518
|
phase: { type: 'string' },
|
|
466
519
|
probe: { type: 'boolean' },
|
|
467
520
|
project: { type: 'string' },
|
|
468
521
|
prompt: { type: 'string' },
|
|
522
|
+
query: { type: 'string' },
|
|
469
523
|
raise: { type: 'boolean' },
|
|
470
524
|
rearm: { type: 'boolean' },
|
|
471
525
|
reason: { type: 'string' },
|
|
@@ -473,18 +527,23 @@ async function main(argv) {
|
|
|
473
527
|
remove: { type: 'boolean' },
|
|
474
528
|
repo: { type: 'string' },
|
|
475
529
|
requeue: { type: 'string' },
|
|
530
|
+
shared: { type: 'boolean' },
|
|
476
531
|
since: { type: 'string' },
|
|
477
532
|
slots: { type: 'string' },
|
|
478
533
|
tag: { type: 'string' },
|
|
534
|
+
task: { type: 'string' },
|
|
535
|
+
team: { type: 'string' },
|
|
479
536
|
text: { type: 'string' },
|
|
480
537
|
tier: { type: 'string' },
|
|
481
538
|
title: { type: 'string' },
|
|
539
|
+
tracker: { type: 'string' },
|
|
482
540
|
until: { type: 'string' },
|
|
483
541
|
version: { short: 'v', type: 'boolean' },
|
|
484
542
|
wait: { type: 'string' },
|
|
485
543
|
'waits-release': { type: 'boolean' },
|
|
486
544
|
weight: { type: 'string' },
|
|
487
545
|
worker: { type: 'string' },
|
|
546
|
+
yes: { type: 'boolean' },
|
|
488
547
|
},
|
|
489
548
|
}));
|
|
490
549
|
const paths = conductorPaths();
|
|
@@ -511,7 +570,7 @@ async function main(argv) {
|
|
|
511
570
|
case 'branch':
|
|
512
571
|
if (sub === undefined || arg === undefined)
|
|
513
572
|
throw usageError('branch');
|
|
514
|
-
process.exitCode = await branchCommand(sub, arg, item, verbIo());
|
|
573
|
+
process.exitCode = await branchCommand(paths, sub, arg, item, verbIo());
|
|
515
574
|
return;
|
|
516
575
|
case 'brief':
|
|
517
576
|
if (sub === undefined)
|
|
@@ -525,7 +584,7 @@ async function main(argv) {
|
|
|
525
584
|
case 'checkout':
|
|
526
585
|
if (sub === undefined || arg === undefined || fourth === undefined)
|
|
527
586
|
throw usageError('checkout');
|
|
528
|
-
process.exitCode = await checkoutCommand(sub, arg, fourth, item, verbIo());
|
|
587
|
+
process.exitCode = await checkoutCommand(paths, sub, arg, fourth, item, verbIo());
|
|
529
588
|
return;
|
|
530
589
|
case 'comments': {
|
|
531
590
|
if (sub === undefined || arg !== undefined)
|
|
@@ -554,10 +613,10 @@ async function main(argv) {
|
|
|
554
613
|
// /reload-plugins after an upgrade) starts this package's new build, in the console's env.
|
|
555
614
|
return events(paths, values.follow === true, values.since, values.console === true || process.env.CONDUCTOR_CONSOLE === '1');
|
|
556
615
|
case 'file':
|
|
557
|
-
if (sub !== undefined || !values.title || !values['body-file']
|
|
616
|
+
if (sub !== undefined || !values.title || !values['body-file'])
|
|
558
617
|
throw usageError('file');
|
|
559
618
|
process.exitCode = await fileCommand(paths, {
|
|
560
|
-
area: values.area,
|
|
619
|
+
area: values.area ?? '',
|
|
561
620
|
blockedBy: values['blocked-by'] ?? [],
|
|
562
621
|
bodyFile: values['body-file'],
|
|
563
622
|
human: values.human === true,
|
|
@@ -587,6 +646,26 @@ async function main(argv) {
|
|
|
587
646
|
});
|
|
588
647
|
case 'inbox':
|
|
589
648
|
return inbox(paths, values.json === true);
|
|
649
|
+
case 'init':
|
|
650
|
+
if (arg !== undefined)
|
|
651
|
+
throw usageError('init');
|
|
652
|
+
process.exitCode = await initProject({
|
|
653
|
+
add: (checkout, name) => addProject(paths, checkout, name, false),
|
|
654
|
+
ask: terminalAsk(),
|
|
655
|
+
check: (name) => projectCheckReport(paths, name, { worktree: true }, { err: (line) => process.stderr.write(`${line}\n`) }),
|
|
656
|
+
out,
|
|
657
|
+
paths,
|
|
658
|
+
}, sub ?? '.', {
|
|
659
|
+
shared: values.shared === true,
|
|
660
|
+
yes: values.yes === true,
|
|
661
|
+
...(values.base === undefined ? {} : { base: values.base }),
|
|
662
|
+
...(values.key === undefined ? {} : { key: values.key }),
|
|
663
|
+
...(values.name === undefined ? {} : { name: values.name }),
|
|
664
|
+
...(values.query === undefined ? {} : { query: values.query }),
|
|
665
|
+
...(values.team === undefined ? {} : { team: values.team }),
|
|
666
|
+
...(values.tracker === undefined ? {} : { tracker: values.tracker }),
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
590
669
|
case 'install-startup':
|
|
591
670
|
return values.remove ? uninstall() : install();
|
|
592
671
|
case 'land': {
|
|
@@ -606,6 +685,7 @@ async function main(argv) {
|
|
|
606
685
|
...repo,
|
|
607
686
|
...(mode === undefined ? {} : { mode }),
|
|
608
687
|
...(tier === undefined ? {} : { tier }),
|
|
688
|
+
...(values.requeue === undefined ? {} : { requeue: wholeNumber('--requeue', values.requeue) }),
|
|
609
689
|
}, verbIo());
|
|
610
690
|
return;
|
|
611
691
|
}
|
|
@@ -633,7 +713,7 @@ async function main(argv) {
|
|
|
633
713
|
throw usageError('mode');
|
|
634
714
|
if (tier !== 'S' && tier !== 'M' && tier !== 'L')
|
|
635
715
|
throw new ConductorError('--tier takes S, M or L');
|
|
636
|
-
process.exitCode = await modeCommand({ files: values.files, tier }, verbIo());
|
|
716
|
+
process.exitCode = await modeCommand(paths, { files: values.files, tier }, verbIo());
|
|
637
717
|
return;
|
|
638
718
|
}
|
|
639
719
|
case 'note':
|
|
@@ -665,10 +745,20 @@ async function main(argv) {
|
|
|
665
745
|
return notifyTest(paths, { channel: values.channel, kind: values.kind, raise: values.raise === true });
|
|
666
746
|
throw usageError('notify');
|
|
667
747
|
case 'pr':
|
|
748
|
+
if (sub === 'abandon' && arg !== undefined) {
|
|
749
|
+
if (values.reason === undefined)
|
|
750
|
+
throw usageError('pr');
|
|
751
|
+
process.exitCode = await prAbandonCommand(paths, { force: values.force === true, pr: wholeNumber('<pr>', arg), reason: values.reason, ...item, ...repo }, verbIo());
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
668
754
|
if (sub === 'threads' && arg !== undefined) {
|
|
669
755
|
process.exitCode = await prThreadsCommand(paths, { pr: wholeNumber('<pr>', arg), ...repo }, verbIo());
|
|
670
756
|
return;
|
|
671
757
|
}
|
|
758
|
+
if (sub === 'status' && arg !== undefined) {
|
|
759
|
+
process.exitCode = await prStatusCommand(paths, { pr: wholeNumber('<pr>', arg), ...repo }, verbIo());
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
672
762
|
if (sub !== 'create' || values.title === undefined || values['body-file'] === undefined)
|
|
673
763
|
throw usageError('pr');
|
|
674
764
|
process.exitCode = await prCreateCommand(paths, { bodyFile: values['body-file'], dryRun: values['dry-run'] === true, title: values.title, ...item, ...repo }, verbIo());
|
|
@@ -682,9 +772,21 @@ async function main(argv) {
|
|
|
682
772
|
case 'ports':
|
|
683
773
|
process.exitCode = await portsCommand(paths, sub, { json: values.json === true }, brokerIo());
|
|
684
774
|
return;
|
|
775
|
+
case 'profile': {
|
|
776
|
+
if (sub === undefined || arg !== undefined)
|
|
777
|
+
throw usageError('profile');
|
|
778
|
+
// Resolved here from the machine file and the clone, as the daemon resolves it.
|
|
779
|
+
const layered = projectProfile(paths, sub);
|
|
780
|
+
if (values.json === true)
|
|
781
|
+
return out(JSON.stringify({ profile: layered.profile, sources: layered.sources }, null, 2));
|
|
782
|
+
profileLines(sub, layered).forEach((line) => out(line));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
685
785
|
case 'project':
|
|
686
|
-
if (sub === 'add' && arg !== undefined)
|
|
687
|
-
|
|
786
|
+
if (sub === 'add' && arg !== undefined) {
|
|
787
|
+
await addProject(paths, arg, values.name);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
688
790
|
if (sub === 'remove' && arg !== undefined)
|
|
689
791
|
return removeProject(paths, arg);
|
|
690
792
|
if (sub === 'list')
|
|
@@ -725,6 +827,7 @@ async function main(argv) {
|
|
|
725
827
|
...item,
|
|
726
828
|
...repo,
|
|
727
829
|
...(values.for === undefined ? {} : { for: values.for }),
|
|
830
|
+
...(values.grep === undefined ? {} : { grep: values.grep }),
|
|
728
831
|
...(values.requeue === undefined ? {} : { requeue: values.requeue }),
|
|
729
832
|
}, verbIo());
|
|
730
833
|
return;
|
|
@@ -750,6 +853,19 @@ async function main(argv) {
|
|
|
750
853
|
case 'supervise':
|
|
751
854
|
process.exitCode = await superviseCommand(paths, values['fail-fast'] === true);
|
|
752
855
|
return;
|
|
856
|
+
case 'timeline':
|
|
857
|
+
if (arg !== undefined)
|
|
858
|
+
throw usageError('timeline');
|
|
859
|
+
process.exitCode = await timelineCommand(paths, {
|
|
860
|
+
...repo,
|
|
861
|
+
...(sub === undefined ? {} : { run: sub }),
|
|
862
|
+
...(values.branch === undefined ? {} : { branch: values.branch }),
|
|
863
|
+
...(values.count === undefined ? {} : { count: values.count }),
|
|
864
|
+
...(values.for === undefined ? {} : { for: values.for }),
|
|
865
|
+
...(values.grep === undefined ? {} : { grep: values.grep }),
|
|
866
|
+
...(values.task === undefined ? {} : { task: values.task }),
|
|
867
|
+
}, verbIo());
|
|
868
|
+
return;
|
|
753
869
|
case 'train':
|
|
754
870
|
return trainCommand(paths, sub, { dryRun: values['dry-run'] === true }, { out });
|
|
755
871
|
case 'unpause':
|
|
@@ -805,7 +921,7 @@ async function notifyTest(paths, options) {
|
|
|
805
921
|
...(options.kind ? { kind: options.kind } : {}),
|
|
806
922
|
raise: options.raise,
|
|
807
923
|
};
|
|
808
|
-
// Pushover can take its 15 s, after a
|
|
924
|
+
// Pushover can take its 15 s, after a secret store read.
|
|
809
925
|
const answer = await notifyAnswer(paths, { body, method: 'POST', path: '/notify/test', timeoutMs: 60_000 });
|
|
810
926
|
if (options.raise) {
|
|
811
927
|
const { item } = z.object({ item: inboxItemSchema }).parse(answer);
|
|
@@ -883,33 +999,35 @@ function parsing(command, parse) {
|
|
|
883
999
|
: new ConductorError(message, ['run `conduct help` for the commands']);
|
|
884
1000
|
}
|
|
885
1001
|
}
|
|
886
|
-
// The open plan item of `ref`: the item (
|
|
887
|
-
// bare number is refused: it could be an issue's number, and the
|
|
1002
|
+
// The open plan item of `ref`: the item (ENG-312) whose plan waits, or an inbox id written #12; null
|
|
1003
|
+
// when no plan of the item waits. A bare number is refused: it could be an issue's number, and the
|
|
1004
|
+
// answer is posted as the operator's.
|
|
888
1005
|
async function planItem(paths, ref) {
|
|
889
1006
|
if (ref.startsWith('#'))
|
|
890
1007
|
return inboxId(ref);
|
|
1008
|
+
const items = await openItems(paths);
|
|
891
1009
|
if (/^\d+$/.test(ref)) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1010
|
+
// The waiting plans whose key ends in the number, else a placeholder: the project's key isn't known here.
|
|
1011
|
+
const keys = (items ?? []).flatMap((i) => (i.kind === 'plan' && i.item?.endsWith(`-${ref}`) ? [i.item] : []));
|
|
1012
|
+
const item = keys.length > 0 ? keys.join(' or ') : `<KEY>-${ref}`;
|
|
1013
|
+
throw new ConductorError(`"${ref}" is ambiguous`, [`name the item, such as ${item}, or the inbox id as #${ref}`]);
|
|
895
1014
|
}
|
|
896
|
-
const items = await openItems(paths);
|
|
897
1015
|
if (items === null)
|
|
898
1016
|
throw noInbox();
|
|
899
1017
|
const plans = items.filter((i) => i.kind === 'plan' && i.item?.toUpperCase() === ref.toUpperCase());
|
|
900
1018
|
if (plans.length === 1 && plans[0])
|
|
901
1019
|
return plans[0].id;
|
|
902
1020
|
if (plans.length === 0)
|
|
903
|
-
|
|
1021
|
+
return null;
|
|
904
1022
|
throw new ConductorError(`several plans of ${ref} wait`, plans.map((p) => `#${p.id} in ${p.project ?? '?'}: pass #${p.id}`));
|
|
905
1023
|
}
|
|
906
1024
|
function projectLine(p) {
|
|
907
1025
|
const state = p.problem
|
|
908
|
-
? `${p.problem}; the daemon reads
|
|
1026
|
+
? `${p.problem}; the daemon reads the profile again when it starts, when ~/.conductor/projects/${p.name}.json is saved, and after each fetch of the clone`
|
|
909
1027
|
: p.enabled
|
|
910
1028
|
? 'enabled'
|
|
911
1029
|
: 'disabled';
|
|
912
|
-
return ` ${p.name} ${p.
|
|
1030
|
+
return ` ${p.name} ${p.clone} (protocol ${p.protocol}, ${state})`;
|
|
913
1031
|
}
|
|
914
1032
|
// The registered projects: from the daemon, or from the store when it isn't running.
|
|
915
1033
|
async function projectsNow(paths) {
|
|
@@ -954,7 +1072,7 @@ async function removeProject(paths, name) {
|
|
|
954
1072
|
});
|
|
955
1073
|
}
|
|
956
1074
|
const { project, resolved } = result;
|
|
957
|
-
out(`removed ${project.name} (${project.
|
|
1075
|
+
out(`removed ${project.name} (${project.clone})`);
|
|
958
1076
|
for (const item of resolved)
|
|
959
1077
|
out(` resolved #${item.id} (${item.kind}${item.item ? ` ${item.item}` : ''})`);
|
|
960
1078
|
}
|
|
@@ -1004,6 +1122,45 @@ async function retire(paths, ref, force) {
|
|
|
1004
1122
|
out(`retired ${worker.name}`);
|
|
1005
1123
|
steps.forEach((s) => out(` ${s}`));
|
|
1006
1124
|
}
|
|
1125
|
+
// `conduct revise <item>` with no plan of the item in the inbox: the daemon posts the revise on an
|
|
1126
|
+
// approved plan and wakes its worker at once, as its revise sweep would (ATK-664).
|
|
1127
|
+
async function reviseApproved(paths, item, text) {
|
|
1128
|
+
const { body, status } = await callApi(paths, {
|
|
1129
|
+
body: { text },
|
|
1130
|
+
method: 'POST',
|
|
1131
|
+
path: `/items/${encodeURIComponent(item)}/revise`,
|
|
1132
|
+
timeoutMs: REVISE_TIMEOUT_MS,
|
|
1133
|
+
});
|
|
1134
|
+
if (status !== 200) {
|
|
1135
|
+
const refusal = answerRefusal.safeParse(body).data;
|
|
1136
|
+
const details = [...(refusal?.details ?? [])];
|
|
1137
|
+
if (status === 502 && refusal?.posted === 'unknown')
|
|
1138
|
+
details.push('the comment may be on the item already: look before answering again');
|
|
1139
|
+
// A conductor from before ATK-664 has no such route.
|
|
1140
|
+
if (status === 404)
|
|
1141
|
+
throw new ConductorError(`no plan of ${item} waits in the inbox`, [
|
|
1142
|
+
'`conduct inbox` lists what waits',
|
|
1143
|
+
'a conductor started before ATK-664 revises no approved plan: restart it for that',
|
|
1144
|
+
]);
|
|
1145
|
+
throw new ConductorError(refusal?.error ?? `the API answered ${status}`, details);
|
|
1146
|
+
}
|
|
1147
|
+
const r = reviseAnswer.parse(body).revise;
|
|
1148
|
+
out(`asked for a revision of ${r.item}'s approved plan: ${r.comment.url ?? r.comment.id}`);
|
|
1149
|
+
out(reviseOutcome(r));
|
|
1150
|
+
}
|
|
1151
|
+
// What became of a revise of an approved plan after its comment, in one line.
|
|
1152
|
+
function reviseOutcome(r) {
|
|
1153
|
+
const none = 'no worker is on it: one launched on it reads the revise in its brief';
|
|
1154
|
+
if (r.pending !== null)
|
|
1155
|
+
return `nothing recorded yet (${r.pending}): ${r.worker ? 'the revise sweep records it and wakes its worker within 5 minutes' : none}`;
|
|
1156
|
+
if (!r.revised)
|
|
1157
|
+
return 'a revise of its plan was recorded already: its worker re-plans on the first one';
|
|
1158
|
+
if (r.woken > 0)
|
|
1159
|
+
return 'woke its worker to re-plan';
|
|
1160
|
+
if (r.landed)
|
|
1161
|
+
return 'its PR has merged: its worker learns of it at its next note, for a follow-up PR';
|
|
1162
|
+
return r.worker ? 'its worker is running: its next note is held back, and it re-plans' : none;
|
|
1163
|
+
}
|
|
1007
1164
|
// `conduct rules`: from the daemon, or straight from the files when it isn't running.
|
|
1008
1165
|
async function rules(paths, json) {
|
|
1009
1166
|
const view = await rulesAnswer(paths, { method: 'GET', path: '/rules' }, (body) => rulesViewSchema.parse(body), (service) => service.view());
|
|
@@ -1084,7 +1241,7 @@ async function shutdownDaemon(paths) {
|
|
|
1084
1241
|
if (readEndpoint(paths) !== null) {
|
|
1085
1242
|
throw new ConductorError(`the conductor (pid ${pid ?? '?'}) did not stop within ${STOP_WAIT_MS / 1000} s`);
|
|
1086
1243
|
}
|
|
1087
|
-
// The port file goes before the process ends; a start (the logon
|
|
1244
|
+
// The port file goes before the process ends; a start (the logon startup's especially) waits for it.
|
|
1088
1245
|
const exitDeadline = Date.now() + STOP_WAIT_MS;
|
|
1089
1246
|
while (pid !== null && processAlive(pid) && Date.now() < exitDeadline)
|
|
1090
1247
|
await sleep(100);
|
|
@@ -1100,7 +1257,7 @@ async function start(paths) {
|
|
|
1100
1257
|
const pid = readPid(paths.pid);
|
|
1101
1258
|
if (pid !== null && (await ping(paths, pid)))
|
|
1102
1259
|
return out(`the conductor is running (pid ${pid})`);
|
|
1103
|
-
// A daemon that is still starting (the logon
|
|
1260
|
+
// A daemon that is still starting (the logon startup's, say) holds the lock: wait for it rather than
|
|
1104
1261
|
// start a second one, which would exit on that lock. A pid a reboot reused gets a start after.
|
|
1105
1262
|
if (pid !== null && processAlive(pid)) {
|
|
1106
1263
|
try {
|
|
@@ -1148,7 +1305,7 @@ async function status(paths, json) {
|
|
|
1148
1305
|
if (json)
|
|
1149
1306
|
return out(JSON.stringify({ home: paths.home, inbox: items, locks, pid, port, projects, running: true, workers, workersFrom: 'daemon' }, null, 2));
|
|
1150
1307
|
out(`conductor running (pid ${pid ?? '?'}, port ${port ?? '?'}, home ${paths.home})`);
|
|
1151
|
-
out(projects.length === 0 ? 'no projects; add one with `conduct project add <path>`' : 'projects:');
|
|
1308
|
+
out(projects.length === 0 ? 'no projects; add one with `conduct project add <url | path>`' : 'projects:');
|
|
1152
1309
|
projects.forEach((p) => out(projectLine(p)));
|
|
1153
1310
|
out(workers.length === 0 ? 'no workers' : 'workers:');
|
|
1154
1311
|
workers.forEach((w) => out(workerLine(w)));
|
|
@@ -1186,7 +1343,7 @@ async function stopDaemon(paths) {
|
|
|
1186
1343
|
out('the conductor is not running');
|
|
1187
1344
|
return false;
|
|
1188
1345
|
case 'supervisor': {
|
|
1189
|
-
// It answers just before it ends; a start (the logon
|
|
1346
|
+
// It answers just before it ends; a start (the logon startup's especially) needs it gone.
|
|
1190
1347
|
const deadline = Date.now() + STOP_WAIT_MS;
|
|
1191
1348
|
while (processAlive(stop.pid) && Date.now() < deadline)
|
|
1192
1349
|
await sleep(100);
|
|
@@ -1195,7 +1352,7 @@ async function stopDaemon(paths) {
|
|
|
1195
1352
|
}
|
|
1196
1353
|
}
|
|
1197
1354
|
}
|
|
1198
|
-
// `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon
|
|
1355
|
+
// `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon startup runs
|
|
1199
1356
|
// it; `conduct start` runs it with --fail-fast. The config is read again for each push, so one fixed
|
|
1200
1357
|
// while the daemon is down counts, and one that doesn't load (a reason it may be down) leaves the
|
|
1201
1358
|
// defaults: no quiet hours, the default credential.
|
|
@@ -1217,7 +1374,7 @@ async function superviseCommand(paths, failFast) {
|
|
|
1217
1374
|
rotatingLog(paths.log, config().log).log(...line);
|
|
1218
1375
|
}
|
|
1219
1376
|
catch {
|
|
1220
|
-
// Nowhere else to say it: under the logon
|
|
1377
|
+
// Nowhere else to say it: under the logon startup nobody reads stderr.
|
|
1221
1378
|
}
|
|
1222
1379
|
},
|
|
1223
1380
|
};
|
|
@@ -1293,8 +1450,9 @@ async function superviseCommand(paths, failFast) {
|
|
|
1293
1450
|
}
|
|
1294
1451
|
return code;
|
|
1295
1452
|
}
|
|
1296
|
-
function uninstall() {
|
|
1297
|
-
|
|
1453
|
+
async function uninstall() {
|
|
1454
|
+
const task = startup();
|
|
1455
|
+
out((await task.remove()) ? `removed ${task.name}` : `no ${task.name} to remove`);
|
|
1298
1456
|
}
|
|
1299
1457
|
// --until: 17:00 (the next one), tomorrow, a day such as monday, or a date and time.
|
|
1300
1458
|
function untilOf(text) {
|