@nodii/dev 0.6.26 → 0.6.28
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/README.md +42 -5
- package/dist/api.js +1 -0
- package/dist/cli.js +62 -15
- package/dist/core.js +6 -2
- package/dist/devops-openapi.json +1 -1
- package/dist/lanes.js +12 -0
- package/dist/managed-hooks.js +30 -27
- package/dist/nodii-dev.js +13 -1
- package/dist/runs.js +341 -0
- package/dist/upload.js +10 -1
- package/package.json +1 -1
package/dist/lanes.js
CHANGED
|
@@ -182,6 +182,18 @@ export async function laneCommand(action, args, flags, { request = devRequest, c
|
|
|
182
182
|
if (action === "heartbeat") {
|
|
183
183
|
return { payload: await request("POST", `/api/v1/lanes/${encode(id)}/heartbeat`, { ...(flags.note !== undefined ? { note: flags.note } : {}), ...via }), human: `${ref}: heartbeat recorded.` };
|
|
184
184
|
}
|
|
185
|
+
if (action === "subagent-quota") {
|
|
186
|
+
const raw = need(args[1], "lane subagent-quota needs <key> <n>");
|
|
187
|
+
const quota = Number(raw);
|
|
188
|
+
if (!Number.isInteger(quota) || quota < 0 || quota > 100)
|
|
189
|
+
throw new CliError("USAGE", "Subagent quota must be an integer from 0 to 100.", 2);
|
|
190
|
+
const payload = await request(
|
|
191
|
+
"PUT",
|
|
192
|
+
`/api/v1/lanes/${encode(id)}/subagent-quota`,
|
|
193
|
+
{ quota, ...via },
|
|
194
|
+
);
|
|
195
|
+
return { payload, human: `${ref}: subagent quota set to ${quota}.` };
|
|
196
|
+
}
|
|
185
197
|
if (action === "land") return { payload: await request("POST", `/api/v1/lanes/${encode(id)}/land`, via), human: `${ref}: recorded on main.` };
|
|
186
198
|
if (action === "review") {
|
|
187
199
|
const body = await reviewBody(flags, read);
|
package/dist/managed-hooks.js
CHANGED
|
@@ -11,7 +11,7 @@ export const CODEX_HOOK_EVENTS = [
|
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
export const managedHooksDir = (home = homedir()) => join(home, ".config", "nodii-dev", "codex-managed-hooks");
|
|
14
|
-
export const MANAGED_DISPATCH_MIN_VERSION = "0.6.
|
|
14
|
+
export const MANAGED_DISPATCH_MIN_VERSION = "0.6.27";
|
|
15
15
|
|
|
16
16
|
/** Only a single nodii-dev invocation is allowed; shell composition is never accepted. */
|
|
17
17
|
export function nodiiCommandArgs(command) {
|
|
@@ -110,33 +110,36 @@ function responseOf(stdout) {
|
|
|
110
110
|
|
|
111
111
|
/** Execute the current generated hook set behind the stable managed dispatcher. */
|
|
112
112
|
export async function dispatchManagedHook(event, payloadText, deps = {}) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
113
|
+
try {
|
|
114
|
+
if (!CODEX_HOOK_EVENTS.includes(event)) return emptyResponse(event);
|
|
115
|
+
const env = deps.env ?? process.env;
|
|
116
|
+
if (!laneConfig(env)) return emptyResponse(event);
|
|
117
|
+
const dir = deps.dir ?? env.NODII_DEV_MANAGED_HOOKS_DIR ?? managedHooksDir();
|
|
118
|
+
const config = JSON.parse(await (deps.read ?? readFile)(join(dir, "hooks.json"), "utf8"));
|
|
119
|
+
validateManagedHooks(config.hooks ?? {});
|
|
120
|
+
const payload = JSON.parse(payloadText);
|
|
121
|
+
const target = matchTarget(event, payload);
|
|
122
|
+
const handlers = (config.hooks?.[event] ?? []).filter((entry) => {
|
|
123
|
+
try { return new RegExp(entry.matcher || ".*").test(target); } catch { return false; }
|
|
124
|
+
}).flatMap((entry) => entry.hooks ?? []);
|
|
125
|
+
const results = await Promise.all(handlers.map((handler) => (deps.run ?? runHandler)(nodiiCommandArgs(handler.command), payloadText, deps)));
|
|
126
|
+
// A failed handler is unavailable policy, not a denial. Ignore both its
|
|
127
|
+
// output and stderr; doctor is where hook health remains visible.
|
|
128
|
+
const successful = results.filter((result) => result.status === 0);
|
|
129
|
+
const messages = successful.map((result) => responseOf(result.stdout)).filter(Boolean);
|
|
130
|
+
if (["PreToolUse", "PermissionRequest"].includes(event)) {
|
|
131
|
+
const denied = messages.find((message) => message.hookSpecificOutput?.permissionDecision === "deny");
|
|
132
|
+
if (denied) return `${JSON.stringify(denied)}\n`;
|
|
133
|
+
const contexts = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
|
|
134
|
+
return contexts.length ? `${JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: contexts.join("\n") } })}\n` : "";
|
|
135
|
+
}
|
|
136
|
+
const structured = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
|
|
137
|
+
const plain = successful.map((result) => responseOf(result.stdout) ? "" : result.stdout.trim()).filter(Boolean);
|
|
138
|
+
if (["Stop", "SubagentStop"].includes(event)) return "{}\n";
|
|
139
|
+
return [...structured, ...plain].join("\n") + ([...structured, ...plain].length ? "\n" : "");
|
|
140
|
+
} catch {
|
|
141
|
+
return emptyResponse(event);
|
|
135
142
|
}
|
|
136
|
-
const structured = messages.map((message) => message.hookSpecificOutput?.additionalContext).filter(Boolean);
|
|
137
|
-
const plain = results.map((result) => responseOf(result.stdout) ? "" : result.stdout.trim()).filter(Boolean);
|
|
138
|
-
if (["Stop", "SubagentStop"].includes(event)) return "{}\n";
|
|
139
|
-
return [...structured, ...plain, ...errors].join("\n") + ([...structured, ...plain, ...errors].length ? "\n" : "");
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
const versionParts = (value) => String(value ?? "").split(".").map((part) => Number.parseInt(part, 10));
|
package/dist/nodii-dev.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fail, main } from "./cli.js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Hooks run in the agent's environment, which may be incomplete or may not be
|
|
5
|
+
// a git worktree. An unavailable hook must never turn into a refused tool call.
|
|
6
|
+
// Intentional policy denials are handled inside the hook command before they
|
|
7
|
+
// reach this boundary.
|
|
8
|
+
main().catch((error) => {
|
|
9
|
+
const denial = String(error?.code ?? "").startsWith("SAFETY_") ||
|
|
10
|
+
["FIGMA_FILE_KEY_MISSING", "FIGMA_LOCK_DENIED", "WRITE_OUT_OF_SCOPE"].includes(error?.code);
|
|
11
|
+
if (process.argv[2] === "hook" && !denial) {
|
|
12
|
+
process.exitCode = 0;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
fail(error);
|
|
16
|
+
});
|
package/dist/runs.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { CliError, devRequest, encode } from "./core.js";
|
|
4
|
+
|
|
5
|
+
export const RUN_ID_ENV = "NODII_DEV_RUN_ID";
|
|
6
|
+
const DEFAULT_HEARTBEAT_MS = 60_000;
|
|
7
|
+
const DEFAULT_TAIL_BYTES = 8 * 1024;
|
|
8
|
+
const RUN_KINDS = new Set(["build", "test_run", "subagent", "deploy"]);
|
|
9
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10
|
+
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
11
|
+
|
|
12
|
+
export function redactRunText(value) {
|
|
13
|
+
return String(value)
|
|
14
|
+
.replace(/-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi, "[REDACTED PRIVATE KEY]")
|
|
15
|
+
.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]")
|
|
16
|
+
.replace(/\b(?:github_pat_|gh[pousr]_|sk-)[A-Za-z0-9_-]{12,}\b/g, "[REDACTED]")
|
|
17
|
+
.replace(
|
|
18
|
+
/\b((?=[A-Za-z0-9_-]*(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key))[A-Za-z][A-Za-z0-9_-]*\s*[:=]\s*)[^\s,;]+/gi,
|
|
19
|
+
"$1[REDACTED]",
|
|
20
|
+
)
|
|
21
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/[^\s:/]+:)[^@\s]+@/gi, "$1[REDACTED]@");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function appendTail(current, chunk, maxBytes) {
|
|
25
|
+
const next = Buffer.concat([current, Buffer.from(chunk)]);
|
|
26
|
+
return next.length <= maxBytes ? next : next.subarray(next.length - maxBytes);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeSafe(write, value) {
|
|
30
|
+
try {
|
|
31
|
+
write(value);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error?.code !== "EPIPE") throw error;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function currentRunId(runId = process.env[RUN_ID_ENV]) {
|
|
40
|
+
if (runId === undefined || runId === "") return null;
|
|
41
|
+
if (!UUID.test(runId))
|
|
42
|
+
throw new CliError(
|
|
43
|
+
"RUN_CONTEXT_INVALID",
|
|
44
|
+
`${RUN_ID_ENV} must be the UUID of this lane, subagent, or coordinator run.`,
|
|
45
|
+
2,
|
|
46
|
+
);
|
|
47
|
+
return runId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function evidenceText(evidence) {
|
|
51
|
+
return (evidence ?? [])
|
|
52
|
+
.map((item) => {
|
|
53
|
+
const head = ` ${redactRunText(item.kind)}: ${redactRunText(item.ref)}`;
|
|
54
|
+
if (!item.note) return head;
|
|
55
|
+
return `${head}\n${redactRunText(item.note)
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((line) => ` ${line}`)
|
|
58
|
+
.join("\n")}`;
|
|
59
|
+
})
|
|
60
|
+
.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resultText(result) {
|
|
64
|
+
const lines = [
|
|
65
|
+
`Result ${redactRunText(result.id)} · ${redactRunText(result.outcome)} · run ${redactRunText(result.sourceRunId)}`,
|
|
66
|
+
];
|
|
67
|
+
if (result.failureReason)
|
|
68
|
+
lines.push(` reason: ${redactRunText(result.failureReason)}`);
|
|
69
|
+
if (result.handoff) lines.push(` handoff: ${redactRunText(result.handoff)}`);
|
|
70
|
+
const evidence = evidenceText(result.evidence);
|
|
71
|
+
if (evidence) lines.push(evidence);
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function showWaitingResults({
|
|
76
|
+
runId = process.env[RUN_ID_ENV],
|
|
77
|
+
request = devRequest,
|
|
78
|
+
write = (value) => process.stderr.write(value),
|
|
79
|
+
acknowledge = true,
|
|
80
|
+
} = {}) {
|
|
81
|
+
const waiterRunId = currentRunId(runId);
|
|
82
|
+
if (!waiterRunId) return [];
|
|
83
|
+
const payload = await request(
|
|
84
|
+
"GET",
|
|
85
|
+
`/api/v1/runs/${encode(waiterRunId)}/results`,
|
|
86
|
+
);
|
|
87
|
+
const results = payload.data ?? [];
|
|
88
|
+
if (!results.length) return [];
|
|
89
|
+
|
|
90
|
+
const rendered = writeSafe(
|
|
91
|
+
write,
|
|
92
|
+
`${results.map(resultText).join("\n\n")}\n`,
|
|
93
|
+
);
|
|
94
|
+
if (!rendered || !acknowledge) return results;
|
|
95
|
+
for (const result of results) {
|
|
96
|
+
try {
|
|
97
|
+
await request(
|
|
98
|
+
"POST",
|
|
99
|
+
`/api/v1/results/${encode(result.id)}/acknowledge`,
|
|
100
|
+
{ waiterRunId },
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
writeSafe(
|
|
104
|
+
write,
|
|
105
|
+
`Result ${redactRunText(result.id)} remains waiting: acknowledgement failed (${redactRunText(error?.code ?? error?.message ?? "error")}).\n`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function commandLabel(command) {
|
|
113
|
+
return basename(command[0] ?? "command").slice(0, 240) || "command";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runKind(value) {
|
|
117
|
+
const kind = value ?? "build";
|
|
118
|
+
if (!RUN_KINDS.has(kind))
|
|
119
|
+
throw new CliError(
|
|
120
|
+
"USAGE",
|
|
121
|
+
`run --kind must be one of ${[...RUN_KINDS].join(", ")}.`,
|
|
122
|
+
2,
|
|
123
|
+
);
|
|
124
|
+
return kind;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runEvidence(command, exitCode, signal, tail) {
|
|
128
|
+
const evidence = [
|
|
129
|
+
{ kind: "command", ref: commandLabel(command) },
|
|
130
|
+
{ kind: "exit_code", ref: String(exitCode ?? -1) },
|
|
131
|
+
{
|
|
132
|
+
kind: "log_tail",
|
|
133
|
+
ref: "combined stdout/stderr",
|
|
134
|
+
note: redactRunText(tail.toString("utf8")) || "(no output)",
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
if (signal) evidence.push({ kind: "signal", ref: signal });
|
|
138
|
+
return evidence;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function endRun(request, runId, body, attempts = 3) {
|
|
142
|
+
let error;
|
|
143
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
144
|
+
try {
|
|
145
|
+
return await request("POST", `/api/v1/runs/${encode(runId)}/end`, body);
|
|
146
|
+
} catch (caught) {
|
|
147
|
+
error = caught;
|
|
148
|
+
if (attempt + 1 < attempts)
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function superviseRun({
|
|
156
|
+
command,
|
|
157
|
+
waiterRunId = process.env[RUN_ID_ENV],
|
|
158
|
+
kind,
|
|
159
|
+
name,
|
|
160
|
+
expectedHeartbeatSeconds = 60,
|
|
161
|
+
heartbeatIntervalMs = DEFAULT_HEARTBEAT_MS,
|
|
162
|
+
tailBytes = DEFAULT_TAIL_BYTES,
|
|
163
|
+
request = devRequest,
|
|
164
|
+
spawn = nodeSpawn,
|
|
165
|
+
stdoutWrite = (value) => process.stdout.write(value),
|
|
166
|
+
stderrWrite = (value) => process.stderr.write(value),
|
|
167
|
+
} = {}) {
|
|
168
|
+
const waiter = currentRunId(waiterRunId);
|
|
169
|
+
if (!waiter)
|
|
170
|
+
throw new CliError(
|
|
171
|
+
"RUN_CONTEXT_REQUIRED",
|
|
172
|
+
`nodii-dev run needs ${RUN_ID_ENV}, supplied by the harness launcher for the current waiter run.`,
|
|
173
|
+
2,
|
|
174
|
+
);
|
|
175
|
+
if (!Array.isArray(command) || !command.length || !command[0])
|
|
176
|
+
throw new CliError("USAGE", "run needs a command after `--`.", 2);
|
|
177
|
+
const selectedKind = runKind(kind);
|
|
178
|
+
const label = redactRunText(String(name ?? commandLabel(command)))
|
|
179
|
+
.trim()
|
|
180
|
+
.slice(0, 300);
|
|
181
|
+
if (!label) throw new CliError("USAGE", "run --name cannot be empty.", 2);
|
|
182
|
+
const registered = await request("POST", "/api/v1/runs", {
|
|
183
|
+
kind: selectedKind,
|
|
184
|
+
name: label,
|
|
185
|
+
externalRef: `pid:${process.pid}`,
|
|
186
|
+
waiterRunId: waiter,
|
|
187
|
+
expectedHeartbeatSeconds,
|
|
188
|
+
metadata: { executable: redactRunText(commandLabel(command)) },
|
|
189
|
+
});
|
|
190
|
+
const runId = registered.data?.id;
|
|
191
|
+
if (!runId) throw new CliError("RUN_REGISTER_INVALID", "Dev registered a run without an id.", 1);
|
|
192
|
+
|
|
193
|
+
let tail = Buffer.alloc(0);
|
|
194
|
+
let spawnError = null;
|
|
195
|
+
let heartbeatInFlight = false;
|
|
196
|
+
let child;
|
|
197
|
+
let timer;
|
|
198
|
+
const forwardSignals = new Map();
|
|
199
|
+
for (const signal of SIGNALS) {
|
|
200
|
+
const forward = () => {
|
|
201
|
+
if (child?.pid && child.exitCode === null && child.signalCode === null)
|
|
202
|
+
child.kill(signal);
|
|
203
|
+
};
|
|
204
|
+
forwardSignals.set(signal, forward);
|
|
205
|
+
process.on(signal, forward);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
try {
|
|
210
|
+
child = spawn(command[0], command.slice(1), {
|
|
211
|
+
shell: false,
|
|
212
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const evidence = runEvidence(command, 1, null, tail);
|
|
216
|
+
await endRun(request, runId, {
|
|
217
|
+
status: "failed",
|
|
218
|
+
reason: `Command could not start: ${redactRunText(error?.code ?? error?.message ?? "error")}`,
|
|
219
|
+
evidence,
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
runId,
|
|
223
|
+
exitCode: 1,
|
|
224
|
+
signal: null,
|
|
225
|
+
tail: "",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
child.stdout?.on("data", (chunk) => {
|
|
229
|
+
tail = appendTail(tail, chunk, tailBytes);
|
|
230
|
+
writeSafe(stdoutWrite, chunk);
|
|
231
|
+
});
|
|
232
|
+
child.stderr?.on("data", (chunk) => {
|
|
233
|
+
tail = appendTail(tail, chunk, tailBytes);
|
|
234
|
+
writeSafe(stderrWrite, chunk);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const heartbeat = async () => {
|
|
238
|
+
if (heartbeatInFlight) return;
|
|
239
|
+
heartbeatInFlight = true;
|
|
240
|
+
try {
|
|
241
|
+
await request("POST", `/api/v1/runs/${encode(runId)}/heartbeat`, {
|
|
242
|
+
note: `command pid ${child.pid ?? "starting"}`,
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
writeSafe(
|
|
246
|
+
stderrWrite,
|
|
247
|
+
`nodii-dev run heartbeat failed (${redactRunText(error?.code ?? error?.message ?? "error")}); retrying.\n`,
|
|
248
|
+
);
|
|
249
|
+
} finally {
|
|
250
|
+
heartbeatInFlight = false;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
timer = setInterval(heartbeat, heartbeatIntervalMs);
|
|
254
|
+
const outcome = await new Promise((resolve) => {
|
|
255
|
+
child.once("error", (error) => {
|
|
256
|
+
spawnError = error;
|
|
257
|
+
});
|
|
258
|
+
child.once("close", (exitCode, signal) => resolve({ exitCode, signal }));
|
|
259
|
+
});
|
|
260
|
+
clearInterval(timer);
|
|
261
|
+
timer = undefined;
|
|
262
|
+
while (heartbeatInFlight)
|
|
263
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
264
|
+
|
|
265
|
+
const evidence = runEvidence(
|
|
266
|
+
command,
|
|
267
|
+
outcome.exitCode,
|
|
268
|
+
outcome.signal,
|
|
269
|
+
tail,
|
|
270
|
+
);
|
|
271
|
+
const succeeded = outcome.exitCode === 0 && !outcome.signal && !spawnError;
|
|
272
|
+
const reason = spawnError
|
|
273
|
+
? `Command could not start: ${redactRunText(spawnError.code ?? spawnError.message)}`
|
|
274
|
+
: outcome.signal
|
|
275
|
+
? `Command ended by signal ${outcome.signal}`
|
|
276
|
+
: `Command exited with code ${outcome.exitCode ?? -1}`;
|
|
277
|
+
await endRun(
|
|
278
|
+
request,
|
|
279
|
+
runId,
|
|
280
|
+
succeeded
|
|
281
|
+
? { status: "done", evidence }
|
|
282
|
+
: { status: "failed", reason, evidence },
|
|
283
|
+
);
|
|
284
|
+
return {
|
|
285
|
+
runId,
|
|
286
|
+
exitCode: succeeded ? 0 : (outcome.exitCode ?? 1),
|
|
287
|
+
signal: outcome.signal,
|
|
288
|
+
tail: redactRunText(tail.toString("utf8")),
|
|
289
|
+
};
|
|
290
|
+
} finally {
|
|
291
|
+
if (timer) clearInterval(timer);
|
|
292
|
+
for (const [signal, forward] of forwardSignals)
|
|
293
|
+
process.off(signal, forward);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function launchRunSupervisor(
|
|
298
|
+
command,
|
|
299
|
+
{
|
|
300
|
+
entrypoint = process.argv[1],
|
|
301
|
+
spawn = nodeSpawn,
|
|
302
|
+
env = process.env,
|
|
303
|
+
kind,
|
|
304
|
+
name,
|
|
305
|
+
} = {},
|
|
306
|
+
) {
|
|
307
|
+
if (!entrypoint)
|
|
308
|
+
throw new CliError("RUN_SUPERVISOR_UNAVAILABLE", "Cannot locate the nodii-dev entrypoint.", 1);
|
|
309
|
+
const args = [entrypoint, "__run-supervisor"];
|
|
310
|
+
if (kind) args.push("--kind", kind);
|
|
311
|
+
if (name) args.push("--name", name);
|
|
312
|
+
args.push("--", ...command);
|
|
313
|
+
const supervisor = spawn(process.execPath, args, {
|
|
314
|
+
detached: true,
|
|
315
|
+
shell: false,
|
|
316
|
+
stdio: "inherit",
|
|
317
|
+
env,
|
|
318
|
+
});
|
|
319
|
+
const forwardSignals = new Map();
|
|
320
|
+
for (const signal of SIGNALS) {
|
|
321
|
+
const forward = () => {
|
|
322
|
+
if (
|
|
323
|
+
supervisor.pid &&
|
|
324
|
+
supervisor.exitCode === null &&
|
|
325
|
+
supervisor.signalCode === null
|
|
326
|
+
)
|
|
327
|
+
supervisor.kill(signal);
|
|
328
|
+
};
|
|
329
|
+
forwardSignals.set(signal, forward);
|
|
330
|
+
process.on(signal, forward);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
return await new Promise((resolve, reject) => {
|
|
334
|
+
supervisor.once("error", reject);
|
|
335
|
+
supervisor.once("close", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
336
|
+
});
|
|
337
|
+
} finally {
|
|
338
|
+
for (const [signal, forward] of forwardSignals)
|
|
339
|
+
process.off(signal, forward);
|
|
340
|
+
}
|
|
341
|
+
}
|
package/dist/upload.js
CHANGED
|
@@ -168,6 +168,7 @@ const uploadBody = (docs, cls, flags, decision) => ({
|
|
|
168
168
|
...(flags["meaning-unchanged"] && cls === "editorial" ? { meaningUnchangedReason: String(flags["meaning-unchanged"]) } : {}),
|
|
169
169
|
})),
|
|
170
170
|
...(decision ? { decision } : {}),
|
|
171
|
+
...(flags.pending ? { pending: true } : {}),
|
|
171
172
|
});
|
|
172
173
|
|
|
173
174
|
export async function docsCommand(action, args, flags, { request = devRequest, log = console.error, cwd = process.cwd(), waitInterval } = {}) {
|
|
@@ -185,6 +186,12 @@ export async function docsCommand(action, args, flags, { request = devRequest, l
|
|
|
185
186
|
|
|
186
187
|
if (action === "validate" || action === "upload") {
|
|
187
188
|
const cls = changeClass(flags);
|
|
189
|
+
if (flags.pending !== undefined && flags.pending !== true)
|
|
190
|
+
throw new CliError("USAGE", "--pending is a switch and takes no value.", 2);
|
|
191
|
+
if (flags.pending && action !== "upload")
|
|
192
|
+
throw new CliError("USAGE", "--pending is valid only with docs upload.", 2);
|
|
193
|
+
if (flags.pending && cls !== "ruling")
|
|
194
|
+
throw new CliError("USAGE", "--pending requires --class ruling and --decision FILE.", 2);
|
|
188
195
|
const decision = await readJsonFile(flags.decision, "decision");
|
|
189
196
|
if (cls === "ruling" && !decision) throw new CliError("RULING_NEEDS_DECISION", "A ruling write needs its decision: --decision DECISION.json (title, context, options, ruling, consequences, provenance, amends).", 2);
|
|
190
197
|
if (cls !== "ruling" && decision) throw new CliError("DECISION_NEEDS_RULING", "--decision goes with --class ruling only.", 2);
|
|
@@ -220,7 +227,9 @@ export async function docsCommand(action, args, flags, { request = devRequest, l
|
|
|
220
227
|
payload: { data: result },
|
|
221
228
|
human: [
|
|
222
229
|
...result.documents.map((d) => `${d.created ? "+" : "~"} ${d.code} ${d.version}\t${d.changeNote}`),
|
|
223
|
-
...(result.decision
|
|
230
|
+
...(result.decision
|
|
231
|
+
? [`${result.decision.status === "pending" ? "Drafted" : "Locked"} ${result.decision.id}: ${result.decision.title}`]
|
|
232
|
+
: []),
|
|
224
233
|
`Uploaded ${result.documents.length} document${result.documents.length === 1 ? "" : "s"} (${cls}).`,
|
|
225
234
|
].join("\n"),
|
|
226
235
|
};
|