@lovrozagar/grunt 0.9.4 → 0.10.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/.agents/skills/jev/SKILL.md +18 -0
- package/.claude/skills/jev/SKILL.md +18 -0
- package/.grok/hooks/orchestrate-parent.js +9 -1
- package/.grok/skills/jev/SKILL.md +18 -0
- package/.rulesync/reference/INDEX.md +4 -2
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/reference/jev.md +56 -0
- package/.rulesync/reference/law.md +2 -2
- package/.rulesync/reference/refs-map.md +1 -0
- package/.rulesync/reference/setup.md +3 -1
- package/.rulesync/reference/skills-map.md +1 -0
- package/.rulesync/skills/jev/SKILL.md +15 -0
- package/CHANGELOG.md +15 -0
- package/README.md +19 -7
- package/cli/grunt.mjs +60 -8
- package/cli/grunt.test.ts +122 -1
- package/cli/init.mjs +118 -10
- package/cli/init.test.ts +302 -5
- package/cli/prompt.mjs +1 -0
- package/cli/prompt.test.ts +29 -0
- package/package.json +4 -1
- package/scripts/board.mjs +390 -0
- package/scripts/doctor.mjs +53 -15
- package/scripts/feature-packs.mjs +123 -0
- package/scripts/guarded-md.mjs +28 -0
- package/scripts/jev.mjs +355 -0
- package/scripts/listen.mjs +2 -1
- package/scripts/pipeline.mjs +84 -2
- package/scripts/prompt.mjs +4 -0
- package/scripts/session-map.mjs +4 -2
- package/scripts/setup.mjs +85 -47
- package/scripts/speak.mjs +2 -0
package/scripts/jev.mjs
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** One TypeSafe System One call for an external research decision. Caps are local. */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
export const MODEL = "jev-1.13.0";
|
|
9
|
+
export const ENDPOINT = "https://api.typesafe.ai/v1/systemone";
|
|
10
|
+
export const MAX_STATE_CHARS = 64_000;
|
|
11
|
+
export const MAX_QUESTIONS = 8;
|
|
12
|
+
export const MAX_REQUESTS_PER_DAY = 40;
|
|
13
|
+
export const MAX_INPUT_TOKENS_PER_DAY = 1_000_000;
|
|
14
|
+
export const TIMEOUT_MS = 10_000;
|
|
15
|
+
export const MAX_RETRY_WAIT_MS = 2_000;
|
|
16
|
+
export const LOCK_STALE_MS = 10_000;
|
|
17
|
+
|
|
18
|
+
const USAGE = "usage: node scripts/jev.mjs --available | --state FILE --questions FILE\n";
|
|
19
|
+
const MISSING_KEY =
|
|
20
|
+
"missing TYPESAFE_API_KEY or ~/.grunt/jev.json. setup: node scripts/setup.mjs jev\n";
|
|
21
|
+
|
|
22
|
+
export function jevJsonPath(home) {
|
|
23
|
+
return path.join(home, ".grunt", "jev.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function jevUsagePath(home) {
|
|
27
|
+
return path.join(home, ".grunt", "jev-usage.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function estimateTokens(stateText, questionsText) {
|
|
31
|
+
return Math.ceil((String(stateText).length + String(questionsText).length) / 4);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadJevKey({ env = process.env, home = os.homedir() } = {}) {
|
|
35
|
+
const fromEnv = String(env.TYPESAFE_API_KEY || "").trim();
|
|
36
|
+
if (fromEnv) return fromEnv;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(fs.readFileSync(jevJsonPath(home), "utf8"));
|
|
39
|
+
return String(parsed && parsed.apiKey ? parsed.apiKey : "").trim();
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function retryWaitMs(header, now = Date.now()) {
|
|
46
|
+
if (header == null || String(header).trim() === "") return null;
|
|
47
|
+
const raw = String(header).trim();
|
|
48
|
+
if (/^\d+$/.test(raw)) {
|
|
49
|
+
const ms = Number(raw) * 1000;
|
|
50
|
+
return ms <= MAX_RETRY_WAIT_MS ? ms : null;
|
|
51
|
+
}
|
|
52
|
+
const when = Date.parse(raw);
|
|
53
|
+
if (Number.isNaN(when)) return null;
|
|
54
|
+
const ms = when - now;
|
|
55
|
+
if (ms < 0) return 0;
|
|
56
|
+
return ms <= MAX_RETRY_WAIT_MS ? ms : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fail(text) {
|
|
60
|
+
return { code: 2, text };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function headerGet(headers, name) {
|
|
64
|
+
if (!headers) return null;
|
|
65
|
+
if (typeof headers.get === "function") return headers.get(name);
|
|
66
|
+
const lower = name.toLowerCase();
|
|
67
|
+
return headers[name] ?? headers[lower] ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readLedger(file, day) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
73
|
+
if (!parsed || parsed.day !== day) return { day, requests: 0, input_tokens: 0 };
|
|
74
|
+
return {
|
|
75
|
+
day,
|
|
76
|
+
requests: Number(parsed.requests) || 0,
|
|
77
|
+
input_tokens: Number(parsed.input_tokens) || 0,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return { day, requests: 0, input_tokens: 0 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeLedger(file, row) {
|
|
85
|
+
fs.writeFileSync(file, JSON.stringify(row) + "\n", { mode: 0o600 });
|
|
86
|
+
try {
|
|
87
|
+
fs.chmodSync(file, 0o600);
|
|
88
|
+
} catch {
|
|
89
|
+
/* win */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function withLock(home, fn) {
|
|
94
|
+
const dir = path.join(home, ".grunt");
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
96
|
+
const lock = path.join(dir, "jev-usage.lock");
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
for (;;) {
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(lock);
|
|
101
|
+
break;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (!err || err.code !== "EEXIST") throw err;
|
|
104
|
+
let stale = false;
|
|
105
|
+
try {
|
|
106
|
+
stale = Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS;
|
|
107
|
+
} catch {
|
|
108
|
+
stale = true;
|
|
109
|
+
}
|
|
110
|
+
if (stale) {
|
|
111
|
+
try {
|
|
112
|
+
fs.rmdirSync(lock);
|
|
113
|
+
} catch {
|
|
114
|
+
/* owner still has it */
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (Date.now() - start > LOCK_STALE_MS) return { busy: true };
|
|
119
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
return fn();
|
|
124
|
+
} finally {
|
|
125
|
+
try {
|
|
126
|
+
fs.rmdirSync(lock);
|
|
127
|
+
} catch {
|
|
128
|
+
/* already gone */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function beginAttempt(home, nowMs, estimate) {
|
|
134
|
+
return withLock(home, () => {
|
|
135
|
+
const file = jevUsagePath(home);
|
|
136
|
+
const day = new Date(nowMs).toISOString().slice(0, 10);
|
|
137
|
+
const cur = readLedger(file, day);
|
|
138
|
+
if (cur.requests >= MAX_REQUESTS_PER_DAY) return { stop: "requests" };
|
|
139
|
+
if (
|
|
140
|
+
cur.input_tokens >= MAX_INPUT_TOKENS_PER_DAY ||
|
|
141
|
+
cur.input_tokens + estimate > MAX_INPUT_TOKENS_PER_DAY
|
|
142
|
+
) {
|
|
143
|
+
return { stop: "tokens" };
|
|
144
|
+
}
|
|
145
|
+
const next = {
|
|
146
|
+
day,
|
|
147
|
+
requests: cur.requests + 1,
|
|
148
|
+
input_tokens: cur.input_tokens + estimate,
|
|
149
|
+
};
|
|
150
|
+
writeLedger(file, next);
|
|
151
|
+
return { ok: true, estimate };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function settleTokens(home, nowMs, estimate, actual) {
|
|
156
|
+
return withLock(home, () => {
|
|
157
|
+
const file = jevUsagePath(home);
|
|
158
|
+
const day = new Date(nowMs).toISOString().slice(0, 10);
|
|
159
|
+
const cur = readLedger(file, day);
|
|
160
|
+
const input_tokens = Math.max(0, cur.input_tokens - estimate + actual);
|
|
161
|
+
writeLedger(file, { day, requests: cur.requests, input_tokens });
|
|
162
|
+
return { ok: true };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseState(stateText) {
|
|
167
|
+
const trimmed = stateText.trim();
|
|
168
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return stateText;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(stateText);
|
|
171
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
172
|
+
} catch {
|
|
173
|
+
/* keep the raw text */
|
|
174
|
+
}
|
|
175
|
+
return stateText;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatAnswers(payload) {
|
|
179
|
+
const model = String(payload.model || MODEL);
|
|
180
|
+
const used = Number(payload.usage && payload.usage.input_tokens);
|
|
181
|
+
const lines = [`${model}. input_tokens=${Number.isFinite(used) ? used : 0}`];
|
|
182
|
+
const answers = payload.answers && typeof payload.answers === "object" ? payload.answers : {};
|
|
183
|
+
for (const [id, answer] of Object.entries(answers)) {
|
|
184
|
+
if (lines.length >= 8) break;
|
|
185
|
+
if (!answer || typeof answer !== "object") continue;
|
|
186
|
+
if (answer.type === "choice") {
|
|
187
|
+
lines.push(`- ${id} choice=${answer.choice} confidence=${answer.confidence}`);
|
|
188
|
+
} else if (answer.type === "noul") {
|
|
189
|
+
lines.push(`- ${id} noul=${answer.noul}`);
|
|
190
|
+
} else if (answer.type === "score") {
|
|
191
|
+
lines.push(`- ${id} score=${answer.score} confidence=${answer.confidence}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return lines.join("\n") + "\n";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function postOnce(fetchImpl, key, state, questions) {
|
|
198
|
+
const response = await fetchImpl(ENDPOINT, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: {
|
|
201
|
+
Authorization: `Bearer ${key}`,
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({ model: MODEL, state, questions }),
|
|
205
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
206
|
+
});
|
|
207
|
+
let json = null;
|
|
208
|
+
try {
|
|
209
|
+
json = await response.json();
|
|
210
|
+
} catch {
|
|
211
|
+
json = null;
|
|
212
|
+
}
|
|
213
|
+
return { status: response.status, json, headers: response.headers };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stopText(kind) {
|
|
217
|
+
if (kind === "requests") return `daily request cap ${MAX_REQUESTS_PER_DAY}\n`;
|
|
218
|
+
if (kind === "tokens") return `daily input token cap ${MAX_INPUT_TOKENS_PER_DAY}\n`;
|
|
219
|
+
return "jev usage lock busy\n";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createJevRunner() {
|
|
223
|
+
let spent = false;
|
|
224
|
+
return async function runJev(opts = {}) {
|
|
225
|
+
if (spent) return fail("process request cap 1\n");
|
|
226
|
+
const home = opts.home || os.homedir();
|
|
227
|
+
const env = opts.env || process.env;
|
|
228
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
229
|
+
const sleep = opts.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
230
|
+
const nowFn = typeof opts.now === "function" ? opts.now : () => opts.now ?? Date.now();
|
|
231
|
+
const stateText = opts.stateText;
|
|
232
|
+
const questionsText = opts.questionsText;
|
|
233
|
+
if (typeof stateText !== "string" || typeof questionsText !== "string") {
|
|
234
|
+
return fail(USAGE);
|
|
235
|
+
}
|
|
236
|
+
if (stateText.length > MAX_STATE_CHARS) return fail("state exceeds 64000 characters\n");
|
|
237
|
+
let questions;
|
|
238
|
+
try {
|
|
239
|
+
questions = JSON.parse(questionsText);
|
|
240
|
+
} catch {
|
|
241
|
+
return fail("questions must be a JSON object\n");
|
|
242
|
+
}
|
|
243
|
+
if (!questions || typeof questions !== "object" || Array.isArray(questions)) {
|
|
244
|
+
return fail("questions must be a JSON object\n");
|
|
245
|
+
}
|
|
246
|
+
const count = Object.keys(questions).length;
|
|
247
|
+
if (count < 1) return fail("questions missing\n");
|
|
248
|
+
if (count > MAX_QUESTIONS) return fail("questions exceed 8\n");
|
|
249
|
+
const key = loadJevKey({ env, home });
|
|
250
|
+
if (!key) return fail(MISSING_KEY);
|
|
251
|
+
const estimate = estimateTokens(stateText, questionsText);
|
|
252
|
+
const state = parseState(stateText);
|
|
253
|
+
|
|
254
|
+
const attempt = async () => {
|
|
255
|
+
const nowMs = nowFn();
|
|
256
|
+
const begun = beginAttempt(home, nowMs, estimate);
|
|
257
|
+
if (begun.busy) return fail(stopText("lock"));
|
|
258
|
+
if (begun.stop) return fail(stopText(begun.stop));
|
|
259
|
+
spent = true;
|
|
260
|
+
let response;
|
|
261
|
+
try {
|
|
262
|
+
response = await postOnce(fetchImpl, key, state, questions);
|
|
263
|
+
} catch {
|
|
264
|
+
return fail("jev request failed\n");
|
|
265
|
+
}
|
|
266
|
+
return { response, nowMs };
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const first = await attempt();
|
|
270
|
+
if (first.code) return first;
|
|
271
|
+
let { response, nowMs } = first;
|
|
272
|
+
if (response.status === 429 || response.status === 529) {
|
|
273
|
+
const wait = retryWaitMs(headerGet(response.headers, "retry-after"), nowMs);
|
|
274
|
+
if (wait != null) {
|
|
275
|
+
await sleep(wait);
|
|
276
|
+
const second = await attempt();
|
|
277
|
+
if (second.code) return second;
|
|
278
|
+
response = second.response;
|
|
279
|
+
nowMs = second.nowMs;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (response.status < 200 || response.status >= 300 || !response.json || typeof response.json !== "object") {
|
|
283
|
+
return fail("jev request failed\n");
|
|
284
|
+
}
|
|
285
|
+
const actual = Number(response.json.usage && response.json.usage.input_tokens);
|
|
286
|
+
if (Number.isFinite(actual) && actual >= 0) settleTokens(home, nowMs, estimate, actual);
|
|
287
|
+
const text = formatAnswers(response.json);
|
|
288
|
+
if (text.includes(key)) return fail("jev request failed\n");
|
|
289
|
+
return { code: 0, text };
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function parseArgv(argv) {
|
|
294
|
+
const flags = {};
|
|
295
|
+
const args = Array.isArray(argv) ? argv : [];
|
|
296
|
+
for (let i = 0; i < args.length; i++) {
|
|
297
|
+
const arg = args[i];
|
|
298
|
+
if (typeof arg !== "string" || !arg.startsWith("--")) continue;
|
|
299
|
+
const eq = arg.indexOf("=");
|
|
300
|
+
if (eq > 2) {
|
|
301
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const name = arg.slice(2);
|
|
305
|
+
const next = args[i + 1];
|
|
306
|
+
if (next && !String(next).startsWith("--")) {
|
|
307
|
+
flags[name] = next;
|
|
308
|
+
i += 1;
|
|
309
|
+
} else flags[name] = "1";
|
|
310
|
+
}
|
|
311
|
+
return { flags };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const runner = createJevRunner();
|
|
315
|
+
|
|
316
|
+
export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
317
|
+
const { flags } = parseArgv(argv);
|
|
318
|
+
if (flags.available) {
|
|
319
|
+
const present = Boolean(loadJevKey({ env: opts.env || process.env, home: opts.home || os.homedir() }));
|
|
320
|
+
return { code: 0, text: present ? "yes\n" : "no\n" };
|
|
321
|
+
}
|
|
322
|
+
if (!flags.state || !flags.questions) return fail(USAGE);
|
|
323
|
+
let stateText;
|
|
324
|
+
let questionsText;
|
|
325
|
+
try {
|
|
326
|
+
stateText = fs.readFileSync(flags.state, "utf8");
|
|
327
|
+
questionsText = fs.readFileSync(flags.questions, "utf8");
|
|
328
|
+
} catch {
|
|
329
|
+
return fail("state or questions file missing\n");
|
|
330
|
+
}
|
|
331
|
+
const run = opts.run || runner;
|
|
332
|
+
return run({
|
|
333
|
+
home: opts.home,
|
|
334
|
+
env: opts.env,
|
|
335
|
+
fetchImpl: opts.fetchImpl,
|
|
336
|
+
sleep: opts.sleep,
|
|
337
|
+
now: opts.now,
|
|
338
|
+
stateText,
|
|
339
|
+
questionsText,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
344
|
+
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
345
|
+
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
346
|
+
main()
|
|
347
|
+
.then((result) => {
|
|
348
|
+
process.stdout.write(result.text);
|
|
349
|
+
process.exit(result.code);
|
|
350
|
+
})
|
|
351
|
+
.catch(() => {
|
|
352
|
+
process.stdout.write("jev request failed\n");
|
|
353
|
+
process.exit(2);
|
|
354
|
+
});
|
|
355
|
+
}
|
package/scripts/listen.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { whichBin } from "./doctor.mjs";
|
|
8
|
+
import { loadOptionalConsumerEnv, whichBin } from "./doctor.mjs";
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_MODEL = "whisper-1";
|
|
11
11
|
export const DEFAULT_GGML = "ggml-base.en.bin";
|
|
@@ -576,6 +576,7 @@ function waitPidExit(pid, killFn, timeoutMs = 8000) {
|
|
|
576
576
|
|
|
577
577
|
export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
578
578
|
const { _, flags } = parseArgv(argv);
|
|
579
|
+
if (!opts.env) await loadOptionalConsumerEnv();
|
|
579
580
|
const env = opts.env || process.env;
|
|
580
581
|
const home = opts.home || str(env.HOME || env.USERPROFILE, os.homedir());
|
|
581
582
|
const cwd = str(flags.cwd || opts.cwd, process.cwd());
|
package/scripts/pipeline.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Inner generate/check/watch chain. Called by guarded-roots; not a public npm script. */
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { stripJsonc } from "./jsonc.mjs";
|
|
8
|
+
|
|
9
|
+
export const PIPELINE_CONFIG_REL = ".rulesync/grunt.pipeline.jsonc";
|
|
6
10
|
|
|
7
11
|
export const COMMANDS = {
|
|
8
12
|
generate: [
|
|
@@ -41,12 +45,90 @@ export function envWithLocalBin(cwd, env = process.env) {
|
|
|
41
45
|
return merged;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
function asStringList(v) {
|
|
49
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x) : [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function quotedCommandsInBlock(src, key) {
|
|
53
|
+
const m = String(src ?? "").match(new RegExp(`${key}:\\s*\\[([\\s\\S]*?)\\]`, "m"));
|
|
54
|
+
if (!m) return [];
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
57
|
+
const code = line.replace(/\/\/.*$/, "").trim();
|
|
58
|
+
if (!code) continue;
|
|
59
|
+
for (const x of code.matchAll(/"((?:\\.|[^"\\])*)"/g)) {
|
|
60
|
+
out.push(x[1].replace(/\\"/g, '"'));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function inferPipelineSkip(src, mode) {
|
|
67
|
+
const stock = COMMANDS[mode];
|
|
68
|
+
if (!stock || !src || !new RegExp(`${mode}:\\s*\\[`).test(src)) return [];
|
|
69
|
+
const active = new Set(quotedCommandsInBlock(src, mode));
|
|
70
|
+
return stock.filter((c) => !active.has(c));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function loadPipelineOverlay(cwd, mode, warn = (m) => process.stderr.write(`${m}\n`)) {
|
|
74
|
+
const abs = path.join(cwd, PIPELINE_CONFIG_REL);
|
|
75
|
+
if (!fs.existsSync(abs)) return { skip: [], extra: [] };
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = JSON.parse(stripJsonc(fs.readFileSync(abs, "utf8")));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
warn(`pipeline: ignoring ${PIPELINE_CONFIG_REL}: ${err.message}`);
|
|
81
|
+
return { skip: [], extra: [] };
|
|
82
|
+
}
|
|
83
|
+
const section = raw && typeof raw === "object" && !Array.isArray(raw) ? raw[mode] : null;
|
|
84
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
85
|
+
return { skip: [], extra: [] };
|
|
86
|
+
}
|
|
87
|
+
return { skip: asStringList(section.skip), extra: asStringList(section.extra) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolvePipelineCommands(mode, cwd, warn) {
|
|
91
|
+
const stock = COMMANDS[mode];
|
|
92
|
+
if (!stock) return null;
|
|
93
|
+
const { skip, extra } = loadPipelineOverlay(cwd, mode, warn);
|
|
94
|
+
const skipSet = new Set(skip);
|
|
95
|
+
return [...stock.filter((c) => !skipSet.has(c)), ...extra];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatCheckFailures(failures) {
|
|
99
|
+
const n = failures.length;
|
|
100
|
+
const lines = [`pipeline check failed (${n} step${n === 1 ? "" : "s"}):`];
|
|
101
|
+
for (const { cmd, err } of failures) {
|
|
102
|
+
lines.push(`- ${cmd}`);
|
|
103
|
+
const msg = err && err.message ? err.message : String(err);
|
|
104
|
+
for (const line of msg.split(/\r?\n/)) {
|
|
105
|
+
if (line) lines.push(` ${line}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
lines.push(
|
|
109
|
+
"If a consumer post-step rewrites this output (hooks-union, codex-sync), add the stock command to check.skip in .rulesync/grunt.pipeline.jsonc",
|
|
110
|
+
);
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function runPipeline(mode, { cwd = process.cwd(), exec = execSync, warn } = {}) {
|
|
115
|
+
const cmds = resolvePipelineCommands(mode, cwd, warn);
|
|
46
116
|
if (!cmds) {
|
|
47
117
|
throw new Error("usage: pipeline.mjs generate|check|watch");
|
|
48
118
|
}
|
|
49
119
|
const env = envWithLocalBin(cwd);
|
|
120
|
+
if (mode === "check") {
|
|
121
|
+
const failures = [];
|
|
122
|
+
for (const cmd of cmds) {
|
|
123
|
+
try {
|
|
124
|
+
exec(cmd, { cwd, stdio: "inherit", shell: true, env });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
failures.push({ cmd, err });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (failures.length) throw new Error(formatCheckFailures(failures));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
50
132
|
for (const cmd of cmds) {
|
|
51
133
|
exec(cmd, { cwd, stdio: "inherit", shell: true, env });
|
|
52
134
|
}
|
package/scripts/prompt.mjs
CHANGED
|
@@ -15,6 +15,10 @@ export async function select(opts) {
|
|
|
15
15
|
return bailIfCancel(await clack.select(opts));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export async function multiselect(opts) {
|
|
19
|
+
return bailIfCancel(await clack.multiselect(opts));
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
export async function confirm(opts) {
|
|
19
23
|
return bailIfCancel(await clack.confirm(opts));
|
|
20
24
|
}
|
package/scripts/session-map.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { recordHook } from "./board.mjs";
|
|
10
11
|
import { folderMap } from "./folder-map.mjs";
|
|
11
12
|
|
|
12
13
|
/** First line of the injected map; `depth` = levels per package, null when nothing was cut. */
|
|
@@ -49,8 +50,9 @@ function logSize(root, data, out) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function main() {
|
|
53
|
+
const data = readStdin();
|
|
54
|
+
recordHook(data, process.env);
|
|
52
55
|
try {
|
|
53
|
-
const data = readStdin();
|
|
54
56
|
const root = workspaceRootOf(data);
|
|
55
57
|
const out = folderMap({ root, maxChars: HOOK_CONTEXT_CHARS - RESERVE });
|
|
56
58
|
if (!out || !out.text) return 0;
|
|
@@ -63,7 +65,7 @@ function main() {
|
|
|
63
65
|
JSON.stringify({
|
|
64
66
|
hookSpecificOutput: {
|
|
65
67
|
hookEventName: "SessionStart",
|
|
66
|
-
additionalContext: `${header(/
|
|
68
|
+
additionalContext: `${header(/ {2}…$/m.test(out.text) ? out.depth : null)}\n${out.text}`,
|
|
67
69
|
},
|
|
68
70
|
}),
|
|
69
71
|
);
|