lee-spec-kit 0.7.11 → 0.8.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/README.en.md +32 -54
- package/README.md +32 -54
- package/dist/bootstrap-ZIJP7O72.js +5 -0
- package/dist/bootstrap-ZIJP7O72.js.map +1 -0
- package/dist/chunk-7V7RMGEU.js +11 -0
- package/dist/chunk-7V7RMGEU.js.map +1 -0
- package/dist/chunk-GR7JQBWF.js +26 -0
- package/dist/chunk-GR7JQBWF.js.map +1 -0
- package/dist/chunk-RYSDBL6X.js +250 -0
- package/dist/chunk-RYSDBL6X.js.map +1 -0
- package/dist/hooks-IP6FICAV.js +1004 -0
- package/dist/hooks-IP6FICAV.js.map +1 -0
- package/dist/index.js +4129 -15544
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/en/common/README.md +12 -10
- package/templates/en/common/agents/agents.md +31 -89
- package/templates/en/common/agents/skills/create-feature.md +30 -57
- package/templates/en/common/agents/skills/create-issue.md +5 -10
- package/templates/en/common/agents/skills/create-pr.md +4 -11
- package/templates/en/common/agents/skills/execute-task.md +32 -96
- package/templates/en/common/features/README.md +1 -1
- package/templates/en/common/features/feature-base/tasks.md +5 -5
- package/templates/ko/common/README.md +12 -10
- package/templates/ko/common/agents/agents.md +30 -87
- package/templates/ko/common/agents/skills/create-feature.md +30 -58
- package/templates/ko/common/agents/skills/create-issue.md +5 -10
- package/templates/ko/common/agents/skills/create-pr.md +4 -11
- package/templates/ko/common/agents/skills/execute-task.md +32 -102
- package/templates/ko/common/features/README.md +1 -1
- package/templates/ko/common/features/feature-base/tasks.md +5 -5
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runGitCapture } from './chunk-GR7JQBWF.js';
|
|
3
|
+
import './chunk-7V7RMGEU.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
var MANAGED_HOOK_FILENAMES = [
|
|
9
|
+
"_lee_spec_kit_hook_utils.mjs",
|
|
10
|
+
"session_start_lee_spec_kit.mjs",
|
|
11
|
+
"user_prompt_submit_lee_spec_kit.mjs",
|
|
12
|
+
"pre_tool_use_policy.mjs",
|
|
13
|
+
"stop_workflow_audit.mjs"
|
|
14
|
+
];
|
|
15
|
+
function getHookScriptContent(fileName) {
|
|
16
|
+
switch (fileName) {
|
|
17
|
+
case "_lee_spec_kit_hook_utils.mjs":
|
|
18
|
+
return `#!/usr/bin/env node
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
|
|
22
|
+
export function readHookInput() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
25
|
+
if (!raw) return { ok: true, value: {} };
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
value: JSON.parse(raw),
|
|
29
|
+
};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const message =
|
|
32
|
+
error instanceof Error ? error.message : 'Invalid Codex hook payload';
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: message,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CLI_ENTRYPOINT = ${JSON.stringify(getInstalledCliEntrypoint())};
|
|
41
|
+
|
|
42
|
+
export function runLeeSpecKit(args, cwd = process.cwd()) {
|
|
43
|
+
return spawnSync(process.execPath, [CLI_ENTRYPOINT, ...args], {
|
|
44
|
+
cwd,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function runLeeSpecKitJson(args, cwd = process.cwd()) {
|
|
51
|
+
const result = runLeeSpecKit(args, cwd);
|
|
52
|
+
const stdout = String(result.stdout || '').trim();
|
|
53
|
+
const stderr = String(result.stderr || '').trim();
|
|
54
|
+
|
|
55
|
+
if (result.error) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: result.error.message || String(result.error),
|
|
59
|
+
status: result.status ?? 1,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: stderr || stdout || \`lee-spec-kit \${args.join(' ')} failed\`,
|
|
67
|
+
status: result.status ?? 1,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!stdout) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: \`lee-spec-kit \${args.join(' ')} returned empty JSON output\`,
|
|
75
|
+
status: result.status ?? 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
data: JSON.parse(stdout),
|
|
83
|
+
status: result.status ?? 0,
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const message =
|
|
87
|
+
error instanceof Error ? error.message : 'Invalid JSON output from lee-spec-kit';
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: \`\${message}: \${stdout.slice(0, 200)}\`,
|
|
91
|
+
status: result.status ?? 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function printAdditionalContext(hookEventName, additionalContext) {
|
|
97
|
+
process.stdout.write(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
hookSpecificOutput: {
|
|
100
|
+
hookEventName,
|
|
101
|
+
additionalContext,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function printBlock(reason) {
|
|
108
|
+
process.stdout.write(
|
|
109
|
+
JSON.stringify({
|
|
110
|
+
decision: 'block',
|
|
111
|
+
reason,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
case "session_start_lee_spec_kit.mjs":
|
|
117
|
+
return `#!/usr/bin/env node
|
|
118
|
+
import { printAdditionalContext, readHookInput, runLeeSpecKitJson } from './_lee_spec_kit_hook_utils.mjs';
|
|
119
|
+
|
|
120
|
+
// Equivalent CLI probe: npx lee-spec-kit detect --json
|
|
121
|
+
const inputResult = readHookInput();
|
|
122
|
+
if (!inputResult.ok) {
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
const input = inputResult.value;
|
|
126
|
+
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
|
|
127
|
+
const detectedResult = runLeeSpecKitJson(['detect', '--json'], cwd);
|
|
128
|
+
const detected = detectedResult.ok ? detectedResult.data : null;
|
|
129
|
+
|
|
130
|
+
if (detected?.status === 'ok' && detected?.isLeeSpecKitProject === true) {
|
|
131
|
+
const docsDir = detected.docsDir || '(unknown docs dir)';
|
|
132
|
+
const lines = [
|
|
133
|
+
'lee-spec-kit project detected.',
|
|
134
|
+
'Use lee-spec-kit docs and workflow policy only when explicitly detected.',
|
|
135
|
+
'Prefer Codex native execution with workspace-scoped AGENTS.md plus official hooks for the default runtime path.',
|
|
136
|
+
'If the user gives a generic request such as continuing the next feature according to the rules, interpret it through this workflow automatically.',
|
|
137
|
+
'infer the workflow automatically even for generic rule-following requests.',
|
|
138
|
+
\`Docs dir: \${docsDir}\`,
|
|
139
|
+
'Start by reading npx lee-spec-kit docs get agents --json and the active feature docs before editing code.',
|
|
140
|
+
'Keep docs as the SSOT and treat workflow-audit as the end-of-turn sync guard.',
|
|
141
|
+
];
|
|
142
|
+
printAdditionalContext('SessionStart', lines.join('\\n'));
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
case "user_prompt_submit_lee_spec_kit.mjs":
|
|
146
|
+
return `#!/usr/bin/env node
|
|
147
|
+
import { printAdditionalContext, readHookInput, runLeeSpecKitJson } from './_lee_spec_kit_hook_utils.mjs';
|
|
148
|
+
|
|
149
|
+
const inputResult = readHookInput();
|
|
150
|
+
if (!inputResult.ok) {
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
const input = inputResult.value;
|
|
154
|
+
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
|
|
155
|
+
const detectedResult = runLeeSpecKitJson(['detect', '--json'], cwd);
|
|
156
|
+
const detected = detectedResult.ok ? detectedResult.data : null;
|
|
157
|
+
|
|
158
|
+
if (detected?.status === 'ok' && detected?.isLeeSpecKitProject === true) {
|
|
159
|
+
const lines = [
|
|
160
|
+
'This prompt is inside a lee-spec-kit workspace.',
|
|
161
|
+
'Interpret generic rule-following requests through the lee-spec-kit docs workflow automatically.',
|
|
162
|
+
'Prefer docs get plus feature-local docs as the primary context source.',
|
|
163
|
+
];
|
|
164
|
+
printAdditionalContext('UserPromptSubmit', lines.join('\\n'));
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
case "pre_tool_use_policy.mjs":
|
|
168
|
+
return `#!/usr/bin/env node
|
|
169
|
+
import { printBlock, readHookInput, runLeeSpecKitJson } from './_lee_spec_kit_hook_utils.mjs';
|
|
170
|
+
import fs from 'node:fs';
|
|
171
|
+
import path from 'node:path';
|
|
172
|
+
|
|
173
|
+
const inputResult = readHookInput();
|
|
174
|
+
if (!inputResult.ok) {
|
|
175
|
+
printBlock('Codex hook input was malformed. Resolve the local hook setup before continuing.');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
const input = inputResult.value;
|
|
179
|
+
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
|
|
180
|
+
const command = String(input?.tool_input?.command || '').trim();
|
|
181
|
+
|
|
182
|
+
function tokenizeShellCommand(value) {
|
|
183
|
+
const matches = value.match(/"(?:\\\\.|[^"])*"|'(?:\\\\.|[^'])*'|\\S+/g) || [];
|
|
184
|
+
return matches.map((token) => {
|
|
185
|
+
if (
|
|
186
|
+
(token.startsWith('"') && token.endsWith('"')) ||
|
|
187
|
+
(token.startsWith("'") && token.endsWith("'"))
|
|
188
|
+
) {
|
|
189
|
+
return token.slice(1, -1);
|
|
190
|
+
}
|
|
191
|
+
return token;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeExecutableToken(token) {
|
|
196
|
+
const base = token.split(/[\\\\/]/).pop() || token;
|
|
197
|
+
return base.replace(/\\.(?:bat|cmd|exe)$/i, '').toLowerCase();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stripEnvWrapper(tokens) {
|
|
201
|
+
let index = 1;
|
|
202
|
+
while (index < tokens.length) {
|
|
203
|
+
const token = tokens[index];
|
|
204
|
+
if (!token) {
|
|
205
|
+
index += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (token === '--') {
|
|
209
|
+
return tokens.slice(index + 1);
|
|
210
|
+
}
|
|
211
|
+
if (token.startsWith('-')) {
|
|
212
|
+
index += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) {
|
|
216
|
+
index += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
return tokens.slice(index);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return tokens;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function stripSudoWrapper(tokens) {
|
|
226
|
+
let index = 1;
|
|
227
|
+
while (index < tokens.length) {
|
|
228
|
+
const token = tokens[index];
|
|
229
|
+
if (!token) {
|
|
230
|
+
index += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (token === '--') {
|
|
234
|
+
return tokens.slice(index + 1);
|
|
235
|
+
}
|
|
236
|
+
if (token === '-u' || token === '-g' || token === '-h' || token === '-p') {
|
|
237
|
+
index += 2;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (token.startsWith('-')) {
|
|
241
|
+
index += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
return tokens.slice(index);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return tokens;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const KNOWN_SHELL_EXECUTABLES = new Set([
|
|
251
|
+
'ash',
|
|
252
|
+
'bash',
|
|
253
|
+
'cmd',
|
|
254
|
+
'dash',
|
|
255
|
+
'fish',
|
|
256
|
+
'ksh',
|
|
257
|
+
'powershell',
|
|
258
|
+
'pwsh',
|
|
259
|
+
'sh',
|
|
260
|
+
'zsh',
|
|
261
|
+
]);
|
|
262
|
+
const DIRECT_GIT_OR_GH_EXECUTABLES = new Set(['git', 'gh']);
|
|
263
|
+
|
|
264
|
+
function isShellCommandFlag(token) {
|
|
265
|
+
const lower = token.toLowerCase();
|
|
266
|
+
if (lower === '-c' || lower === '/c' || lower === '-command') {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
if (token === lower && /^-[a-z]*c[a-z]*$/.test(token)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isExecutablePayloadFlag(token) {
|
|
276
|
+
const lower = token.toLowerCase();
|
|
277
|
+
if (isShellCommandFlag(token)) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return lower === '-e' || lower === '-r' || lower === '--eval' || lower === '--execute';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function findShellCommandFlagIndex(tokens) {
|
|
284
|
+
return tokens.findIndex((token, index) => index > 0 && isShellCommandFlag(token));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function findExecutablePayloadFlagIndex(tokens) {
|
|
288
|
+
return tokens.findIndex((token, index) => index > 0 && isExecutablePayloadFlag(token));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function containsDangerousGitOrGhPayload(value) {
|
|
292
|
+
return (
|
|
293
|
+
/\\bgit(?:\\.cmd|\\.exe)?\\b[\\s\\S]{0,80}\\b(?:commit|push|checkout|switch|restore|clean|rebase|merge|cherry-pick|revert|stash|reset|branch|tag)\\b/i.test(
|
|
294
|
+
value
|
|
295
|
+
) ||
|
|
296
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\b[\\s\\S]{0,80}\\b(?:issue|pr|repo|release)\\b/i.test(
|
|
297
|
+
value
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function containsProcessExecutionPayload(value) {
|
|
303
|
+
return (
|
|
304
|
+
/\\bchild_process\\b/i.test(value) ||
|
|
305
|
+
/\\bspawn(?:Sync)?\\s*\\(/i.test(value) ||
|
|
306
|
+
/\\bexec(?:Sync|FileSync|File)?\\s*\\(/i.test(value) ||
|
|
307
|
+
/\\bfork\\s*\\(/i.test(value) ||
|
|
308
|
+
/\\bsubprocess\\b/i.test(value) ||
|
|
309
|
+
/\\bos\\.system\\s*\\(/i.test(value) ||
|
|
310
|
+
/\\bsystem\\s*\\(/i.test(value) ||
|
|
311
|
+
/\\bpopen\\s*\\(/i.test(value) ||
|
|
312
|
+
/\\bcreateprocess\\b/i.test(value) ||
|
|
313
|
+
/\\bstart-process\\b/i.test(value)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const KNOWN_EXECUTABLE_WRAPPERS = new Set([
|
|
318
|
+
'bun',
|
|
319
|
+
'deno',
|
|
320
|
+
'node',
|
|
321
|
+
'nodejs',
|
|
322
|
+
'perl',
|
|
323
|
+
'php',
|
|
324
|
+
'python',
|
|
325
|
+
'python2',
|
|
326
|
+
'python3',
|
|
327
|
+
'ruby',
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
const KNOWN_WRAPPER_LAUNCHERS = new Set(['uv', 'uvx']);
|
|
331
|
+
|
|
332
|
+
const EXECUTABLE_WRAPPER_OPTIONS_WITH_VALUE = new Set([
|
|
333
|
+
'--experimental-loader',
|
|
334
|
+
'--import',
|
|
335
|
+
'--loader',
|
|
336
|
+
'--require',
|
|
337
|
+
'-m',
|
|
338
|
+
'-r',
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
const UNSUPPORTED_WRAPPER_PAYLOAD = '__LEE_SPEC_KIT_UNSUPPORTED_WRAPPER_PAYLOAD__';
|
|
342
|
+
|
|
343
|
+
function readWrapperScriptPayload(executable, tokens, rawValue, baseCwd) {
|
|
344
|
+
if (KNOWN_WRAPPER_LAUNCHERS.has(executable)) {
|
|
345
|
+
return UNSUPPORTED_WRAPPER_PAYLOAD;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!KNOWN_EXECUTABLE_WRAPPERS.has(executable)) {
|
|
349
|
+
const flagIndex = findExecutablePayloadFlagIndex(tokens);
|
|
350
|
+
return flagIndex === -1 || flagIndex + 1 >= tokens.length
|
|
351
|
+
? null
|
|
352
|
+
: UNSUPPORTED_WRAPPER_PAYLOAD;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (rawValue.includes('<<')) {
|
|
356
|
+
return UNSUPPORTED_WRAPPER_PAYLOAD;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const flagIndex = findExecutablePayloadFlagIndex(tokens);
|
|
360
|
+
if (flagIndex !== -1 && flagIndex + 1 < tokens.length) {
|
|
361
|
+
return UNSUPPORTED_WRAPPER_PAYLOAD;
|
|
362
|
+
}
|
|
363
|
+
return resolveScriptToken(tokens) ? UNSUPPORTED_WRAPPER_PAYLOAD : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function resolveScriptToken(tokens) {
|
|
367
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
368
|
+
const token = tokens[index];
|
|
369
|
+
if (!token) continue;
|
|
370
|
+
if (token === '--') {
|
|
371
|
+
return tokens[index + 1] || null;
|
|
372
|
+
}
|
|
373
|
+
if (token === '-') {
|
|
374
|
+
return token;
|
|
375
|
+
}
|
|
376
|
+
if (token.startsWith('-')) {
|
|
377
|
+
if (
|
|
378
|
+
EXECUTABLE_WRAPPER_OPTIONS_WITH_VALUE.has(token.toLowerCase()) &&
|
|
379
|
+
index + 1 < tokens.length
|
|
380
|
+
) {
|
|
381
|
+
index += 1;
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
return token;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function resolvesToExistingFile(token, baseCwd) {
|
|
391
|
+
if (!token || token.startsWith('-')) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const resolvedPath = path.resolve(baseCwd, token);
|
|
396
|
+
try {
|
|
397
|
+
return fs.statSync(resolvedPath).isFile();
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function unwrapShellCommand(value) {
|
|
404
|
+
let currentValue = value;
|
|
405
|
+
|
|
406
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
407
|
+
const tokens = tokenizeShellCommand(currentValue);
|
|
408
|
+
const executable = normalizeExecutableToken(tokens[0] || '');
|
|
409
|
+
|
|
410
|
+
if (executable === 'sudo') {
|
|
411
|
+
const stripped = stripSudoWrapper(tokens);
|
|
412
|
+
currentValue = stripped.join(' ');
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (executable === 'command' || executable === 'nohup') {
|
|
417
|
+
if (tokens.length <= 1) return currentValue;
|
|
418
|
+
currentValue = tokens.slice(1).join(' ');
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (executable === 'env') {
|
|
423
|
+
const stripped = stripEnvWrapper(tokens);
|
|
424
|
+
currentValue = stripped.join(' ');
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!KNOWN_SHELL_EXECUTABLES.has(executable)) {
|
|
429
|
+
return currentValue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const flagIndex = findShellCommandFlagIndex(tokens);
|
|
433
|
+
if (flagIndex === -1 || flagIndex + 1 >= tokens.length) {
|
|
434
|
+
return currentValue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
currentValue = tokens.slice(flagIndex + 1).join(' ');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return currentValue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function hasUnsupportedDangerousShellWrapper(value, baseCwd) {
|
|
444
|
+
let currentValue = value;
|
|
445
|
+
|
|
446
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
447
|
+
const tokens = tokenizeShellCommand(currentValue);
|
|
448
|
+
const executable = normalizeExecutableToken(tokens[0] || '');
|
|
449
|
+
|
|
450
|
+
if (executable === 'sudo') {
|
|
451
|
+
currentValue = stripSudoWrapper(tokens).join(' ');
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (executable === 'command' || executable === 'nohup') {
|
|
456
|
+
if (tokens.length <= 1) return false;
|
|
457
|
+
currentValue = tokens.slice(1).join(' ');
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (executable === 'env') {
|
|
462
|
+
currentValue = stripEnvWrapper(tokens).join(' ');
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (DIRECT_GIT_OR_GH_EXECUTABLES.has(executable)) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const flagIndex = findExecutablePayloadFlagIndex(tokens);
|
|
471
|
+
if (!KNOWN_SHELL_EXECUTABLES.has(executable)) {
|
|
472
|
+
const payload = readWrapperScriptPayload(
|
|
473
|
+
executable,
|
|
474
|
+
tokens,
|
|
475
|
+
currentValue,
|
|
476
|
+
baseCwd
|
|
477
|
+
);
|
|
478
|
+
if (payload === UNSUPPORTED_WRAPPER_PAYLOAD) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
if (!payload) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return containsDangerousGitOrGhPayload(payload) || containsProcessExecutionPayload(payload);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (flagIndex === -1 || flagIndex + 1 >= tokens.length) {
|
|
488
|
+
if (currentValue.includes('<<') || resolveScriptToken(tokens)) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const payload = tokens.slice(flagIndex + 1).join(' ');
|
|
495
|
+
const payloadTokens = tokenizeShellCommand(payload);
|
|
496
|
+
if (resolvesToExistingFile(payloadTokens[0] || '', baseCwd)) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
currentValue = payload;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const GIT_OPTIONS_WITH_VALUE = new Set([
|
|
506
|
+
'-C',
|
|
507
|
+
'-c',
|
|
508
|
+
'--exec-path',
|
|
509
|
+
'--git-dir',
|
|
510
|
+
'--namespace',
|
|
511
|
+
'--super-prefix',
|
|
512
|
+
'--work-tree',
|
|
513
|
+
'--config-env',
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
function getGitSubcommand(value) {
|
|
517
|
+
const unwrappedValue = unwrapShellCommand(value);
|
|
518
|
+
const tokens = tokenizeShellCommand(unwrappedValue);
|
|
519
|
+
const gitIndex = tokens.findIndex(
|
|
520
|
+
(token) => normalizeExecutableToken(token) === 'git'
|
|
521
|
+
);
|
|
522
|
+
if (gitIndex === -1) return null;
|
|
523
|
+
|
|
524
|
+
for (let index = gitIndex + 1; index < tokens.length; index += 1) {
|
|
525
|
+
const token = tokens[index];
|
|
526
|
+
if (!token) continue;
|
|
527
|
+
if (token === '--') {
|
|
528
|
+
return tokens[index + 1] || null;
|
|
529
|
+
}
|
|
530
|
+
if (!token.startsWith('-')) {
|
|
531
|
+
return token;
|
|
532
|
+
}
|
|
533
|
+
if (GIT_OPTIONS_WITH_VALUE.has(token) && index + 1 < tokens.length) {
|
|
534
|
+
index += 1;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function getGitCommandCwd(value, baseCwd) {
|
|
542
|
+
const unwrappedValue = unwrapShellCommand(value);
|
|
543
|
+
const tokens = tokenizeShellCommand(unwrappedValue);
|
|
544
|
+
const gitIndex = tokens.findIndex(
|
|
545
|
+
(token) => normalizeExecutableToken(token) === 'git'
|
|
546
|
+
);
|
|
547
|
+
if (gitIndex === -1) return baseCwd;
|
|
548
|
+
|
|
549
|
+
let currentCwd = baseCwd;
|
|
550
|
+
for (let index = gitIndex + 1; index < tokens.length; index += 1) {
|
|
551
|
+
const token = tokens[index];
|
|
552
|
+
if (!token) continue;
|
|
553
|
+
if (token === '--') break;
|
|
554
|
+
if (!token.startsWith('-')) break;
|
|
555
|
+
if (token === '-C' && index + 1 < tokens.length) {
|
|
556
|
+
currentCwd = path.resolve(currentCwd, tokens[index + 1]);
|
|
557
|
+
index += 1;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (GIT_OPTIONS_WITH_VALUE.has(token) && index + 1 < tokens.length) {
|
|
561
|
+
index += 1;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return currentCwd;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function hasUnsupportedGitTargetOptions(value) {
|
|
569
|
+
const unwrappedValue = unwrapShellCommand(value);
|
|
570
|
+
const tokens = tokenizeShellCommand(unwrappedValue);
|
|
571
|
+
return tokens.some((token) => {
|
|
572
|
+
const normalized = String(token || '').toLowerCase();
|
|
573
|
+
return (
|
|
574
|
+
normalized === '--git-dir' ||
|
|
575
|
+
normalized.startsWith('--git-dir=') ||
|
|
576
|
+
normalized === '--work-tree' ||
|
|
577
|
+
normalized.startsWith('--work-tree=')
|
|
578
|
+
);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function hasGitTargetEnvOverrides(value) {
|
|
583
|
+
const tokens = tokenizeShellCommand(value);
|
|
584
|
+
return tokens.some((token) => {
|
|
585
|
+
const normalized = String(token || '').trim().toUpperCase();
|
|
586
|
+
return (
|
|
587
|
+
normalized.startsWith('GIT_DIR=') ||
|
|
588
|
+
normalized.startsWith('GIT_WORK_TREE=')
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const normalizedCommand = unwrapShellCommand(command);
|
|
594
|
+
const hasUnsupportedShellWrappedDangerousCommand =
|
|
595
|
+
hasUnsupportedDangerousShellWrapper(command, cwd);
|
|
596
|
+
const gitSubcommand = getGitSubcommand(command);
|
|
597
|
+
const gitCommandCwd = getGitCommandCwd(command, cwd);
|
|
598
|
+
const hasUnsupportedGitTarget = hasUnsupportedGitTargetOptions(command);
|
|
599
|
+
const hasGitTargetEnvOverride = hasGitTargetEnvOverrides(command);
|
|
600
|
+
const isGitCommit = gitSubcommand === 'commit';
|
|
601
|
+
const isGitPush = gitSubcommand === 'push';
|
|
602
|
+
const isGitCheckout = gitSubcommand === 'checkout';
|
|
603
|
+
const isGitSwitch = gitSubcommand === 'switch';
|
|
604
|
+
const isGitRestore = gitSubcommand === 'restore';
|
|
605
|
+
const isGitClean = gitSubcommand === 'clean';
|
|
606
|
+
const isGitRebase = gitSubcommand === 'rebase';
|
|
607
|
+
const isGitMerge = gitSubcommand === 'merge';
|
|
608
|
+
const isGitCherryPick = gitSubcommand === 'cherry-pick';
|
|
609
|
+
const isGitRevert = gitSubcommand === 'revert';
|
|
610
|
+
const isGitStash = gitSubcommand === 'stash';
|
|
611
|
+
const isGitBranchDelete =
|
|
612
|
+
gitSubcommand === 'branch' &&
|
|
613
|
+
/(^|\\s)(?:-D|-d|--delete)(\\s|$)/.test(normalizedCommand);
|
|
614
|
+
const isGitTagDelete =
|
|
615
|
+
gitSubcommand === 'tag' &&
|
|
616
|
+
/(^|\\s)-d(\\s|$)/.test(normalizedCommand);
|
|
617
|
+
const isGitResetHard =
|
|
618
|
+
gitSubcommand === 'reset' && /(^|\\s)--hard(\\s|$)/.test(normalizedCommand);
|
|
619
|
+
const isAlwaysBlockedGhCommand =
|
|
620
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+repo\\s+delete\\b/i.test(command) ||
|
|
621
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+release\\s+delete\\b/i.test(command) ||
|
|
622
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+api\\b[\\s\\S]{0,160}(?:--method=DELETE|(?:-X|--method)\\s+DELETE)\\b/i.test(command) ||
|
|
623
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+api\\b[\\s\\S]{0,120}\\bgraphql\\b/i.test(command);
|
|
624
|
+
const isAlwaysBlockedGhOperation =
|
|
625
|
+
isAlwaysBlockedGhCommand;
|
|
626
|
+
const isDangerousGhCommand =
|
|
627
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+issue\\s+(?:create|delete|edit|close|reopen)\\b/i.test(command) ||
|
|
628
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+pr\\s+(?:create|merge|close|reopen|review|ready)\\b/i.test(command) ||
|
|
629
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+repo\\s+(?:delete|archive|rename|edit)\\b/i.test(command) ||
|
|
630
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+release\\s+(?:create|delete|edit)\\b/i.test(command) ||
|
|
631
|
+
/\\bgh(?:\\.cmd|\\.exe)?\\s+api\\b[\\s\\S]{0,160}(?:--method=(?:DELETE|PATCH|POST|PUT)|(?:-X|--method)\\s+(?:DELETE|PATCH|POST|PUT))\\b/i.test(command);
|
|
632
|
+
const isDangerousCommand =
|
|
633
|
+
isAlwaysBlockedGhOperation ||
|
|
634
|
+
hasUnsupportedShellWrappedDangerousCommand ||
|
|
635
|
+
isGitCommit ||
|
|
636
|
+
isGitPush ||
|
|
637
|
+
isGitCheckout ||
|
|
638
|
+
isGitSwitch ||
|
|
639
|
+
isGitRestore ||
|
|
640
|
+
isGitClean ||
|
|
641
|
+
isGitRebase ||
|
|
642
|
+
isGitMerge ||
|
|
643
|
+
isGitCherryPick ||
|
|
644
|
+
isGitRevert ||
|
|
645
|
+
isGitStash ||
|
|
646
|
+
isGitBranchDelete ||
|
|
647
|
+
isGitTagDelete ||
|
|
648
|
+
isGitResetHard ||
|
|
649
|
+
isDangerousGhCommand;
|
|
650
|
+
|
|
651
|
+
if (!command || !isDangerousCommand) {
|
|
652
|
+
process.exit(0);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (isAlwaysBlockedGhOperation) {
|
|
656
|
+
printBlock('Destructive GitHub CLI commands such as repo or release deletion are not supported by lee-spec-kit hooks. Re-run them manually after explicit review.');
|
|
657
|
+
process.exit(0);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (hasUnsupportedShellWrappedDangerousCommand) {
|
|
661
|
+
printBlock('lee-spec-kit hooks do not support this shell wrapper for git or gh commands. Re-run the command from a supported shell or the target repo root instead.');
|
|
662
|
+
process.exit(0);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (hasUnsupportedGitTarget || hasGitTargetEnvOverride) {
|
|
666
|
+
printBlock('Git commands using --git-dir, --work-tree, GIT_DIR, or GIT_WORK_TREE are not supported by lee-spec-kit hooks. Re-run the command from the target repo root instead.');
|
|
667
|
+
process.exit(0);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const detectedResult = runLeeSpecKitJson(['detect', '--json'], cwd);
|
|
671
|
+
if (!detectedResult.ok) {
|
|
672
|
+
printBlock('lee-spec-kit detection failed inside the Codex hook. Fix the local CLI or hook setup before continuing.');
|
|
673
|
+
process.exit(0);
|
|
674
|
+
}
|
|
675
|
+
const detected = detectedResult.data;
|
|
676
|
+
if (!(detected?.status === 'ok' && detected?.isLeeSpecKitProject === true)) {
|
|
677
|
+
process.exit(0);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (path.resolve(gitCommandCwd) !== path.resolve(cwd) && !isGitCommit) {
|
|
681
|
+
printBlock('Git commands targeting another repo via -C are only supported for git commit. Re-run the command from the target repo root instead.');
|
|
682
|
+
process.exit(0);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (isGitCommit) {
|
|
686
|
+
const commitAuditResult = runLeeSpecKitJson(
|
|
687
|
+
['commit-audit', '--json', '--git-root', gitCommandCwd],
|
|
688
|
+
cwd
|
|
689
|
+
);
|
|
690
|
+
if (!commitAuditResult.ok) {
|
|
691
|
+
printBlock('lee-spec-kit commit-audit failed inside the Codex hook. Resolve the docs guardrail failure before committing.');
|
|
692
|
+
process.exit(0);
|
|
693
|
+
}
|
|
694
|
+
const commitAudit = commitAuditResult.data;
|
|
695
|
+
if (commitAudit?.status === 'blocked') {
|
|
696
|
+
if (commitAudit?.reasonCode === 'UNSUPPORTED_GIT_TARGET') {
|
|
697
|
+
printBlock('Git commit targets outside the current lee-spec-kit project topology are not supported. Re-run the command from the active workspace or target repo root instead.');
|
|
698
|
+
process.exit(0);
|
|
699
|
+
}
|
|
700
|
+
printBlock('Normalize or allowlist non-canonical docs paths before committing.');
|
|
701
|
+
process.exit(0);
|
|
702
|
+
}
|
|
703
|
+
if (!(commitAudit?.status === 'ok' || commitAudit?.status === 'skipped')) {
|
|
704
|
+
printBlock('lee-spec-kit commit-audit returned a non-ok status inside the Codex hook. Resolve the docs guardrail failure before committing.');
|
|
705
|
+
process.exit(0);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const auditResult = runLeeSpecKitJson(['workflow-audit', '--json'], cwd);
|
|
710
|
+
if (!auditResult.ok) {
|
|
711
|
+
printBlock('lee-spec-kit workflow-audit failed inside the Codex hook. Resolve the docs sync guardrail failure before continuing.');
|
|
712
|
+
process.exit(0);
|
|
713
|
+
}
|
|
714
|
+
const audit = auditResult.data;
|
|
715
|
+
if (audit?.status === 'needs_sync') {
|
|
716
|
+
printBlock('Sync the active feature docs before running remote or destructive commands.');
|
|
717
|
+
process.exit(0);
|
|
718
|
+
}
|
|
719
|
+
if (!(audit?.status === 'ok' || audit?.status === 'skipped')) {
|
|
720
|
+
printBlock('lee-spec-kit workflow-audit returned a non-ok status inside the Codex hook. Resolve the docs sync guardrail failure before continuing.');
|
|
721
|
+
}
|
|
722
|
+
`;
|
|
723
|
+
case "stop_workflow_audit.mjs":
|
|
724
|
+
return `#!/usr/bin/env node
|
|
725
|
+
import { printBlock, readHookInput, runLeeSpecKitJson } from './_lee_spec_kit_hook_utils.mjs';
|
|
726
|
+
|
|
727
|
+
// Equivalent CLI probe: npx lee-spec-kit workflow-audit --json
|
|
728
|
+
const inputResult = readHookInput();
|
|
729
|
+
if (!inputResult.ok) {
|
|
730
|
+
printBlock('Codex stop hook input was malformed. Resolve the local hook setup before stopping.');
|
|
731
|
+
process.exit(0);
|
|
732
|
+
}
|
|
733
|
+
const input = inputResult.value;
|
|
734
|
+
if (input?.stop_hook_active === true) {
|
|
735
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
736
|
+
process.exit(0);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
|
|
740
|
+
const detectedResult = runLeeSpecKitJson(['detect', '--json'], cwd);
|
|
741
|
+
if (!detectedResult.ok) {
|
|
742
|
+
printBlock('lee-spec-kit detection failed inside the stop hook. Resolve the local CLI or hook setup before stopping.');
|
|
743
|
+
process.exit(0);
|
|
744
|
+
}
|
|
745
|
+
const detected = detectedResult.data;
|
|
746
|
+
if (!(detected?.status === 'ok' && detected?.isLeeSpecKitProject === true)) {
|
|
747
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
748
|
+
process.exit(0);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const auditResult = runLeeSpecKitJson(['workflow-audit', '--json'], cwd);
|
|
752
|
+
if (!auditResult.ok) {
|
|
753
|
+
printBlock('lee-spec-kit workflow-audit failed inside the stop hook. Resolve the docs sync guardrail failure before stopping.');
|
|
754
|
+
process.exit(0);
|
|
755
|
+
}
|
|
756
|
+
const audit = auditResult.data;
|
|
757
|
+
if (audit?.status === 'needs_sync') {
|
|
758
|
+
printBlock('Run one more pass and sync the active feature docs before stopping.');
|
|
759
|
+
process.exit(0);
|
|
760
|
+
}
|
|
761
|
+
if (!(audit?.status === 'ok' || audit?.status === 'skipped')) {
|
|
762
|
+
printBlock('lee-spec-kit workflow-audit returned a non-ok status inside the stop hook. Resolve the docs sync guardrail failure before stopping.');
|
|
763
|
+
process.exit(0);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
767
|
+
`;
|
|
768
|
+
default:
|
|
769
|
+
return "";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function getManagedHookRelativePath(fileName) {
|
|
773
|
+
return path.posix.join(".codex", "hooks", fileName);
|
|
774
|
+
}
|
|
775
|
+
function normalizePathSlashes(value) {
|
|
776
|
+
return value.replace(/\\/g, "/");
|
|
777
|
+
}
|
|
778
|
+
function escapeRegExp(value) {
|
|
779
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
780
|
+
}
|
|
781
|
+
function getPortableHookCommandSuffix(fileName) {
|
|
782
|
+
const relativeHookPath = normalizePathSlashes(getManagedHookRelativePath(fileName));
|
|
783
|
+
const loaderSource = [
|
|
784
|
+
"(async () => {",
|
|
785
|
+
" const fs = require('node:fs');",
|
|
786
|
+
" const path = require('node:path');",
|
|
787
|
+
" const { pathToFileURL } = require('node:url');",
|
|
788
|
+
` const relativeHookPath = ${JSON.stringify(relativeHookPath)};`,
|
|
789
|
+
" let dir = process.cwd();",
|
|
790
|
+
" while (true) {",
|
|
791
|
+
" const candidate = path.join(dir, relativeHookPath);",
|
|
792
|
+
" if (fs.existsSync(candidate)) {",
|
|
793
|
+
" await import(pathToFileURL(candidate).href);",
|
|
794
|
+
" return;",
|
|
795
|
+
" }",
|
|
796
|
+
" const parent = path.dirname(dir);",
|
|
797
|
+
" if (parent === dir) {",
|
|
798
|
+
" throw new Error('lee-spec-kit hook script not found: ' + relativeHookPath);",
|
|
799
|
+
" }",
|
|
800
|
+
" dir = parent;",
|
|
801
|
+
" }",
|
|
802
|
+
"})().catch((error) => {",
|
|
803
|
+
" console.error(error && error.stack ? error.stack : String(error));",
|
|
804
|
+
" process.exit(1);",
|
|
805
|
+
"});"
|
|
806
|
+
].join(" ");
|
|
807
|
+
return ` -e ${JSON.stringify(loaderSource)}`;
|
|
808
|
+
}
|
|
809
|
+
function isManagedCommand(command) {
|
|
810
|
+
const normalized = normalizePathSlashes(command).trim();
|
|
811
|
+
return MANAGED_HOOK_FILENAMES.some((fileName) => {
|
|
812
|
+
const currentCommand = normalizePathSlashes(toPortableHookCommand(fileName)).trim();
|
|
813
|
+
const portableSuffix = normalizePathSlashes(
|
|
814
|
+
getPortableHookCommandSuffix(fileName)
|
|
815
|
+
).trim();
|
|
816
|
+
if (normalized === currentCommand || normalized.endsWith(portableSuffix)) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
const relativeHookPath = escapeRegExp(getManagedHookRelativePath(fileName));
|
|
820
|
+
const legacyAbsolutePattern = new RegExp(
|
|
821
|
+
`^node\\s+["']?.*${relativeHookPath}["']?$`
|
|
822
|
+
);
|
|
823
|
+
return legacyAbsolutePattern.test(normalized);
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
function toPortableHookCommand(fileName) {
|
|
827
|
+
const nodeCommand = JSON.stringify(process.execPath);
|
|
828
|
+
return `${nodeCommand}${getPortableHookCommandSuffix(fileName)}`;
|
|
829
|
+
}
|
|
830
|
+
function getManagedHooksConfig() {
|
|
831
|
+
const commandFor = (fileName) => toPortableHookCommand(fileName);
|
|
832
|
+
return {
|
|
833
|
+
SessionStart: [
|
|
834
|
+
{
|
|
835
|
+
matcher: "startup|resume",
|
|
836
|
+
hooks: [
|
|
837
|
+
{
|
|
838
|
+
type: "command",
|
|
839
|
+
command: commandFor("session_start_lee_spec_kit.mjs"),
|
|
840
|
+
statusMessage: "Loading lee-spec-kit workflow context"
|
|
841
|
+
}
|
|
842
|
+
]
|
|
843
|
+
}
|
|
844
|
+
],
|
|
845
|
+
UserPromptSubmit: [
|
|
846
|
+
{
|
|
847
|
+
hooks: [
|
|
848
|
+
{
|
|
849
|
+
type: "command",
|
|
850
|
+
command: commandFor("user_prompt_submit_lee_spec_kit.mjs")
|
|
851
|
+
}
|
|
852
|
+
]
|
|
853
|
+
}
|
|
854
|
+
],
|
|
855
|
+
PreToolUse: [
|
|
856
|
+
{
|
|
857
|
+
matcher: "Bash",
|
|
858
|
+
hooks: [
|
|
859
|
+
{
|
|
860
|
+
type: "command",
|
|
861
|
+
command: commandFor("pre_tool_use_policy.mjs"),
|
|
862
|
+
statusMessage: "Checking lee-spec-kit workflow guardrails"
|
|
863
|
+
}
|
|
864
|
+
]
|
|
865
|
+
}
|
|
866
|
+
],
|
|
867
|
+
Stop: [
|
|
868
|
+
{
|
|
869
|
+
hooks: [
|
|
870
|
+
{
|
|
871
|
+
type: "command",
|
|
872
|
+
command: commandFor("stop_workflow_audit.mjs"),
|
|
873
|
+
timeout: 30
|
|
874
|
+
}
|
|
875
|
+
]
|
|
876
|
+
}
|
|
877
|
+
]
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function getInstalledCliEntrypoint() {
|
|
881
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
882
|
+
}
|
|
883
|
+
function pruneManagedGroups(groups) {
|
|
884
|
+
if (!Array.isArray(groups)) return [];
|
|
885
|
+
return groups.map((group) => {
|
|
886
|
+
if (!group || typeof group !== "object" || !Array.isArray(group.hooks)) {
|
|
887
|
+
return group;
|
|
888
|
+
}
|
|
889
|
+
const hooks = group.hooks.filter(
|
|
890
|
+
(hook) => !(hook && typeof hook === "object" && typeof hook.command === "string" && isManagedCommand(hook.command))
|
|
891
|
+
);
|
|
892
|
+
if (hooks.length === 0) {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
...group,
|
|
897
|
+
hooks
|
|
898
|
+
};
|
|
899
|
+
}).filter((group) => !!group);
|
|
900
|
+
}
|
|
901
|
+
function mergeManagedGroups(current, managedHooks) {
|
|
902
|
+
const nextHooks = {
|
|
903
|
+
...current.hooks && typeof current.hooks === "object" ? current.hooks : {}
|
|
904
|
+
};
|
|
905
|
+
for (const eventName of Object.keys(managedHooks)) {
|
|
906
|
+
const existing = pruneManagedGroups(
|
|
907
|
+
Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : void 0
|
|
908
|
+
);
|
|
909
|
+
nextHooks[eventName] = [...existing, ...managedHooks[eventName]];
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
...current,
|
|
913
|
+
hooks: nextHooks
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function removeManagedGroups(current) {
|
|
917
|
+
const nextHooks = {
|
|
918
|
+
...current.hooks && typeof current.hooks === "object" ? current.hooks : {}
|
|
919
|
+
};
|
|
920
|
+
for (const eventName of ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop"]) {
|
|
921
|
+
const pruned = pruneManagedGroups(
|
|
922
|
+
Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : void 0
|
|
923
|
+
);
|
|
924
|
+
if (pruned.length > 0) {
|
|
925
|
+
nextHooks[eventName] = pruned;
|
|
926
|
+
} else {
|
|
927
|
+
delete nextHooks[eventName];
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
...current,
|
|
932
|
+
hooks: nextHooks
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function getRepoCodexDir(repoRoot = process.cwd()) {
|
|
936
|
+
return path.join(repoRoot, ".codex");
|
|
937
|
+
}
|
|
938
|
+
function resolveCodexHooksRepoRoot(cwd = process.cwd()) {
|
|
939
|
+
return runGitCapture(["rev-parse", "--show-toplevel"], cwd) || cwd;
|
|
940
|
+
}
|
|
941
|
+
function getRepoHooksDir(repoRoot = process.cwd()) {
|
|
942
|
+
return path.join(getRepoCodexDir(repoRoot), "hooks");
|
|
943
|
+
}
|
|
944
|
+
function getRepoHooksConfigPath(repoRoot = process.cwd()) {
|
|
945
|
+
return path.join(getRepoCodexDir(repoRoot), "hooks.json");
|
|
946
|
+
}
|
|
947
|
+
async function upsertLeeSpecKitCodexHooks(repoRoot = process.cwd()) {
|
|
948
|
+
const hooksDir = getRepoHooksDir(repoRoot);
|
|
949
|
+
const hooksJsonPath = getRepoHooksConfigPath(repoRoot);
|
|
950
|
+
await fs.ensureDir(hooksDir);
|
|
951
|
+
for (const fileName of MANAGED_HOOK_FILENAMES) {
|
|
952
|
+
const targetPath = path.join(hooksDir, fileName);
|
|
953
|
+
await fs.writeFile(targetPath, getHookScriptContent(fileName), {
|
|
954
|
+
encoding: "utf-8",
|
|
955
|
+
mode: 493
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const managedHooks = getManagedHooksConfig();
|
|
959
|
+
const exists = await fs.pathExists(hooksJsonPath);
|
|
960
|
+
const current = exists ? await fs.readJson(hooksJsonPath) : { hooks: {} };
|
|
961
|
+
const next = mergeManagedGroups(current, managedHooks);
|
|
962
|
+
const nextJson = `${JSON.stringify(next, null, 2)}
|
|
963
|
+
`;
|
|
964
|
+
const currentJson = exists ? `${JSON.stringify(current, null, 2)}
|
|
965
|
+
` : null;
|
|
966
|
+
if (currentJson === nextJson) {
|
|
967
|
+
return { changed: false, action: "noop", hooksJsonPath };
|
|
968
|
+
}
|
|
969
|
+
await fs.writeFile(hooksJsonPath, nextJson, "utf-8");
|
|
970
|
+
return {
|
|
971
|
+
changed: true,
|
|
972
|
+
action: exists ? "updated" : "created",
|
|
973
|
+
hooksJsonPath
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
async function removeLeeSpecKitCodexHooks(repoRoot = process.cwd()) {
|
|
977
|
+
const hooksDir = getRepoHooksDir(repoRoot);
|
|
978
|
+
const hooksJsonPath = getRepoHooksConfigPath(repoRoot);
|
|
979
|
+
let changed = false;
|
|
980
|
+
if (await fs.pathExists(hooksJsonPath)) {
|
|
981
|
+
const current = await fs.readJson(hooksJsonPath);
|
|
982
|
+
const next = removeManagedGroups(current);
|
|
983
|
+
const currentJson = `${JSON.stringify(current, null, 2)}
|
|
984
|
+
`;
|
|
985
|
+
const nextJson = `${JSON.stringify(next, null, 2)}
|
|
986
|
+
`;
|
|
987
|
+
if (currentJson !== nextJson) {
|
|
988
|
+
await fs.writeFile(hooksJsonPath, nextJson, "utf-8");
|
|
989
|
+
changed = true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
for (const fileName of MANAGED_HOOK_FILENAMES) {
|
|
993
|
+
const targetPath = path.join(hooksDir, fileName);
|
|
994
|
+
if (await fs.pathExists(targetPath)) {
|
|
995
|
+
await fs.remove(targetPath);
|
|
996
|
+
changed = true;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return { changed, hooksJsonPath };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export { getRepoCodexDir, getRepoHooksConfigPath, getRepoHooksDir, removeLeeSpecKitCodexHooks, resolveCodexHooksRepoRoot, upsertLeeSpecKitCodexHooks };
|
|
1003
|
+
//# sourceMappingURL=hooks-IP6FICAV.js.map
|
|
1004
|
+
//# sourceMappingURL=hooks-IP6FICAV.js.map
|