@ulpi/cli 0.1.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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/auth-PN7TMQHV-2W4ICG64.js +15 -0
  4. package/dist/chunk-247GVVKK.js +2259 -0
  5. package/dist/chunk-2CLNOKPA.js +793 -0
  6. package/dist/chunk-2HEE5OKX.js +79 -0
  7. package/dist/chunk-2MZER6ND.js +415 -0
  8. package/dist/chunk-3SBPZRB5.js +772 -0
  9. package/dist/chunk-4VNS5WPM.js +42 -0
  10. package/dist/chunk-6JCMYYBT.js +1546 -0
  11. package/dist/chunk-6OCEY7JY.js +422 -0
  12. package/dist/chunk-74WVVWJ4.js +375 -0
  13. package/dist/chunk-7AL4DOEJ.js +131 -0
  14. package/dist/chunk-7LXY5UVC.js +330 -0
  15. package/dist/chunk-DBMUNBNB.js +3048 -0
  16. package/dist/chunk-JWUUVXIV.js +13694 -0
  17. package/dist/chunk-KIKPIH6N.js +4048 -0
  18. package/dist/chunk-KLEASXUR.js +70 -0
  19. package/dist/chunk-MIAQVCFW.js +39 -0
  20. package/dist/chunk-NNUWU6CV.js +1610 -0
  21. package/dist/chunk-PKD4ASEM.js +115 -0
  22. package/dist/chunk-Q4HIY43N.js +4230 -0
  23. package/dist/chunk-QJ5GSMEC.js +146 -0
  24. package/dist/chunk-SIAQVRKG.js +2163 -0
  25. package/dist/chunk-SPOI23SB.js +197 -0
  26. package/dist/chunk-YM2HV4IA.js +505 -0
  27. package/dist/codemap-RRJIDBQ5.js +636 -0
  28. package/dist/config-EGAXXCGL.js +127 -0
  29. package/dist/dist-6G7JC2RA.js +90 -0
  30. package/dist/dist-7LHZ65GC.js +418 -0
  31. package/dist/dist-LZKZFPVX.js +140 -0
  32. package/dist/dist-R5F4MX3I.js +107 -0
  33. package/dist/dist-R5ZJ4LX5.js +56 -0
  34. package/dist/dist-RJGCUS3L.js +87 -0
  35. package/dist/dist-RKOGLK7R.js +151 -0
  36. package/dist/dist-W7K4WPAF.js +597 -0
  37. package/dist/export-import-4A5MWLIA.js +53 -0
  38. package/dist/history-ATTUKOHO.js +934 -0
  39. package/dist/index.js +2120 -0
  40. package/dist/init-AY5C2ZAS.js +393 -0
  41. package/dist/launchd-LF2QMSKZ.js +148 -0
  42. package/dist/log-TVTUXAYD.js +75 -0
  43. package/dist/mcp-installer-NQCGKQ23.js +124 -0
  44. package/dist/memory-J3G24QHS.js +406 -0
  45. package/dist/ollama-3XCUZMZT-FYKHW4TZ.js +7 -0
  46. package/dist/openai-E7G2YAHU-UYY4ZWON.js +8 -0
  47. package/dist/projects-ATHDD3D6.js +271 -0
  48. package/dist/review-ADUPV3PN.js +152 -0
  49. package/dist/rules-E427DKYJ.js +134 -0
  50. package/dist/server-MOYPE4SM-N7SE2AN7.js +18 -0
  51. package/dist/server-X5P6WH2M-7K2RY34N.js +11 -0
  52. package/dist/skills/ulpi-generate-guardian/SKILL.md +511 -0
  53. package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +692 -0
  54. package/dist/skills/ulpi-generate-guardian/references/language-rules.md +596 -0
  55. package/dist/skills-CX73O3IV.js +76 -0
  56. package/dist/status-4DFHDJMN.js +66 -0
  57. package/dist/templates/biome.yml +24 -0
  58. package/dist/templates/conventional-commits.yml +18 -0
  59. package/dist/templates/django.yml +30 -0
  60. package/dist/templates/docker.yml +30 -0
  61. package/dist/templates/eslint.yml +13 -0
  62. package/dist/templates/express.yml +20 -0
  63. package/dist/templates/fastapi.yml +23 -0
  64. package/dist/templates/git-flow.yml +26 -0
  65. package/dist/templates/github-flow.yml +27 -0
  66. package/dist/templates/go.yml +33 -0
  67. package/dist/templates/jest.yml +24 -0
  68. package/dist/templates/laravel.yml +30 -0
  69. package/dist/templates/monorepo.yml +26 -0
  70. package/dist/templates/nestjs.yml +21 -0
  71. package/dist/templates/nextjs.yml +31 -0
  72. package/dist/templates/nodejs.yml +33 -0
  73. package/dist/templates/npm.yml +15 -0
  74. package/dist/templates/php.yml +25 -0
  75. package/dist/templates/pnpm.yml +15 -0
  76. package/dist/templates/prettier.yml +23 -0
  77. package/dist/templates/prisma.yml +21 -0
  78. package/dist/templates/python.yml +33 -0
  79. package/dist/templates/quality-of-life.yml +111 -0
  80. package/dist/templates/ruby.yml +25 -0
  81. package/dist/templates/rust.yml +34 -0
  82. package/dist/templates/typescript.yml +14 -0
  83. package/dist/templates/vitest.yml +24 -0
  84. package/dist/templates/yarn.yml +15 -0
  85. package/dist/templates-U7T6MARD.js +156 -0
  86. package/dist/ui-L7UAWXDY.js +167 -0
  87. package/dist/ui.html +698 -0
  88. package/dist/ulpi-RMMCUAGP-JCJ273T6.js +161 -0
  89. package/dist/uninstall-6SW35IK4.js +25 -0
  90. package/dist/update-M2B4RLGH.js +61 -0
  91. package/dist/version-checker-ANCS3IHR.js +10 -0
  92. package/package.json +92 -0
@@ -0,0 +1,772 @@
1
+ import {
2
+ external_exports
3
+ } from "./chunk-KIKPIH6N.js";
4
+ import {
5
+ REVIEWS_DIR
6
+ } from "./chunk-7LXY5UVC.js";
7
+ import {
8
+ __require
9
+ } from "./chunk-4VNS5WPM.js";
10
+
11
+ // ../../packages/review-engine/dist/index.js
12
+ import { promises as fs } from "fs";
13
+ import { join } from "path";
14
+ var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
15
+ get: (a, b) => (typeof __require !== "undefined" ? __require : a)[b]
16
+ }) : x)(function(x) {
17
+ if (typeof __require !== "undefined") return __require.apply(this, arguments);
18
+ throw Error('Dynamic require of "' + x + '" is not supported');
19
+ });
20
+ function parseMarkdownToBlocks(markdown) {
21
+ if (!markdown) return [];
22
+ try {
23
+ return parseMarkdownToBlocksInternal(markdown);
24
+ } catch {
25
+ return [{
26
+ id: "block-0",
27
+ type: "paragraph",
28
+ content: markdown,
29
+ order: 0,
30
+ startLine: 1
31
+ }];
32
+ }
33
+ }
34
+ function parseMarkdownToBlocksInternal(markdown) {
35
+ const lines = markdown.split("\n");
36
+ const blocks = [];
37
+ let blockIndex = 0;
38
+ let i = 0;
39
+ while (i < lines.length) {
40
+ const line = lines[i];
41
+ if (line.trim() === "") {
42
+ i++;
43
+ continue;
44
+ }
45
+ if (/^(-{3,}|_{3,}|\*{3,})$/.test(line.trim())) {
46
+ blocks.push({
47
+ id: `block-${blockIndex}`,
48
+ type: "hr",
49
+ content: line,
50
+ order: blockIndex,
51
+ startLine: i + 1
52
+ });
53
+ blockIndex++;
54
+ i++;
55
+ continue;
56
+ }
57
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
58
+ if (headingMatch) {
59
+ blocks.push({
60
+ id: `block-${blockIndex}`,
61
+ type: "heading",
62
+ content: headingMatch[2].trim(),
63
+ level: headingMatch[1].length,
64
+ order: blockIndex,
65
+ startLine: i + 1
66
+ });
67
+ blockIndex++;
68
+ i++;
69
+ continue;
70
+ }
71
+ const codeMatch = line.match(/^(`{3,}|~{3,})(\w*)/);
72
+ if (codeMatch) {
73
+ const fence = codeMatch[1];
74
+ const language = codeMatch[2] || void 0;
75
+ const codeLines = [];
76
+ const startLine = i + 1;
77
+ i++;
78
+ while (i < lines.length) {
79
+ if (lines[i].startsWith(fence.charAt(0).repeat(fence.length))) {
80
+ i++;
81
+ break;
82
+ }
83
+ codeLines.push(lines[i]);
84
+ i++;
85
+ }
86
+ blocks.push({
87
+ id: `block-${blockIndex}`,
88
+ type: "code",
89
+ content: codeLines.join("\n"),
90
+ language,
91
+ order: blockIndex,
92
+ startLine
93
+ });
94
+ blockIndex++;
95
+ continue;
96
+ }
97
+ if (line.startsWith(">")) {
98
+ const quoteLines = [];
99
+ const startLine = i + 1;
100
+ while (i < lines.length && lines[i].startsWith(">")) {
101
+ quoteLines.push(lines[i].replace(/^>\s?/, ""));
102
+ i++;
103
+ }
104
+ blocks.push({
105
+ id: `block-${blockIndex}`,
106
+ type: "blockquote",
107
+ content: quoteLines.join("\n"),
108
+ order: blockIndex,
109
+ startLine
110
+ });
111
+ blockIndex++;
112
+ continue;
113
+ }
114
+ if (line.startsWith("|") || line.includes("|") && i + 1 < lines.length && /^\|?\s*[-:]+/.test(lines[i + 1])) {
115
+ const tableLines = [];
116
+ const startLine = i + 1;
117
+ while (i < lines.length && lines[i].includes("|") && lines[i].trim() !== "") {
118
+ tableLines.push(lines[i]);
119
+ i++;
120
+ }
121
+ blocks.push({
122
+ id: `block-${blockIndex}`,
123
+ type: "table",
124
+ content: tableLines.join("\n"),
125
+ order: blockIndex,
126
+ startLine
127
+ });
128
+ blockIndex++;
129
+ continue;
130
+ }
131
+ const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
132
+ if (listMatch) {
133
+ const startLine = i + 1;
134
+ const checkboxMatch = listMatch[3].match(/^\[([ xX])\]\s*(.*)/);
135
+ const checked = checkboxMatch ? checkboxMatch[1].toLowerCase() === "x" : void 0;
136
+ const content = checkboxMatch ? checkboxMatch[2] : listMatch[3];
137
+ const allContent = [content];
138
+ i++;
139
+ while (i < lines.length) {
140
+ const nextLine = lines[i];
141
+ if (nextLine.match(/^\s{2,}/) && !nextLine.match(/^\s*([-*+]|\d+\.)\s/)) {
142
+ allContent.push(nextLine.trim());
143
+ i++;
144
+ } else {
145
+ break;
146
+ }
147
+ }
148
+ blocks.push({
149
+ id: `block-${blockIndex}`,
150
+ type: "list-item",
151
+ content: allContent.join("\n"),
152
+ checked,
153
+ order: blockIndex,
154
+ startLine
155
+ });
156
+ blockIndex++;
157
+ continue;
158
+ }
159
+ {
160
+ const paraLines = [];
161
+ const startLine = i + 1;
162
+ while (i < lines.length) {
163
+ const l = lines[i];
164
+ if (l.trim() === "") break;
165
+ if (l.match(/^#{1,6}\s/)) break;
166
+ if (l.match(/^(`{3,}|~{3,})/)) break;
167
+ if (l.startsWith(">")) break;
168
+ if (l.match(/^(-{3,}|_{3,}|\*{3,})$/)) break;
169
+ if (l.match(/^\s*([-*+]|\d+\.)\s/)) break;
170
+ if (l.startsWith("|") && i + 1 < lines.length && /^\|?\s*[-:]/.test(lines[i + 1] || ""))
171
+ break;
172
+ paraLines.push(l);
173
+ i++;
174
+ }
175
+ if (paraLines.length > 0) {
176
+ blocks.push({
177
+ id: `block-${blockIndex}`,
178
+ type: "paragraph",
179
+ content: paraLines.join("\n"),
180
+ order: blockIndex,
181
+ startLine
182
+ });
183
+ blockIndex++;
184
+ }
185
+ }
186
+ }
187
+ return blocks;
188
+ }
189
+ function extractTitle(blocks) {
190
+ const heading = blocks.find((b) => b.type === "heading");
191
+ return heading?.content ?? "Untitled Plan";
192
+ }
193
+ function generateSlug(title, date) {
194
+ const d = date ?? /* @__PURE__ */ new Date();
195
+ const dateStr = d.toISOString().split("T")[0];
196
+ const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
197
+ return `${dateStr}-${titleSlug || "untitled-plan"}`;
198
+ }
199
+ function extractSections(blocks) {
200
+ const sections = [];
201
+ let currentSection = null;
202
+ let sectionOrder = 0;
203
+ const headingStack = [];
204
+ const usedSlugs = /* @__PURE__ */ new Set();
205
+ function sectionSlug(title) {
206
+ const base = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section";
207
+ let slug = base;
208
+ let n = 2;
209
+ while (usedSlugs.has(slug)) {
210
+ slug = `${base}-${n++}`;
211
+ }
212
+ usedSlugs.add(slug);
213
+ return slug;
214
+ }
215
+ for (const block of blocks) {
216
+ if (block.type === "heading" && block.level != null) {
217
+ while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= block.level) {
218
+ headingStack.pop();
219
+ }
220
+ headingStack.push({ level: block.level, title: block.content });
221
+ const path = headingStack.map((h) => h.title).join(" > ");
222
+ if (currentSection) {
223
+ sections.push(currentSection);
224
+ }
225
+ currentSection = {
226
+ id: sectionSlug(block.content),
227
+ blockIds: [block.id],
228
+ title: block.content,
229
+ level: block.level,
230
+ path,
231
+ status: "pending",
232
+ order: sectionOrder++
233
+ };
234
+ } else {
235
+ if (!currentSection) {
236
+ currentSection = {
237
+ id: sectionSlug("Overview"),
238
+ blockIds: [],
239
+ title: "Overview",
240
+ level: 0,
241
+ path: "Overview",
242
+ status: "pending",
243
+ order: sectionOrder++
244
+ };
245
+ }
246
+ currentSection.blockIds.push(block.id);
247
+ }
248
+ }
249
+ if (currentSection) {
250
+ sections.push(currentSection);
251
+ }
252
+ return sections;
253
+ }
254
+ function getSectionFullText(section, blocks) {
255
+ return section.blockIds.map((id) => blocks.find((b) => b.id === id)).filter((b) => b != null).map((b) => b.content).join("\n\n");
256
+ }
257
+ var FILE_PATH_PATTERN = /(?:src\/|\.\/|\/)[a-zA-Z0-9_\-/.]+\.[a-zA-Z]{1,5}/g;
258
+ var DIMENSION_CHECKERS = [
259
+ {
260
+ name: "Test Plan",
261
+ check(ctx) {
262
+ const testKeywords = /\b(test plan|testing|test cases?|unit tests?|integration tests?|e2e tests?|test coverage)\b/i;
263
+ const hasTestHeading = ctx.sections.some(
264
+ (s) => testKeywords.test(s.title)
265
+ );
266
+ const hasTestContent = testKeywords.test(ctx.fullText);
267
+ const found = hasTestHeading || hasTestContent;
268
+ return {
269
+ found,
270
+ score: hasTestHeading ? 100 : hasTestContent ? 60 : 0,
271
+ details: hasTestHeading ? "Dedicated test section found" : hasTestContent ? "Testing mentioned but no dedicated section" : "No test plan found"
272
+ };
273
+ }
274
+ },
275
+ {
276
+ name: "Error Handling",
277
+ check(ctx) {
278
+ const keywords = /\b(error handling|edge cases?|fallback|graceful degradation|try.?catch|error boundary|validation)\b/i;
279
+ const found = keywords.test(ctx.fullText);
280
+ const mentionCount = (ctx.fullText.match(keywords) || []).length;
281
+ return {
282
+ found,
283
+ score: found ? Math.min(mentionCount * 25, 100) : 0,
284
+ details: found ? `${mentionCount} error handling reference(s)` : "No error handling mentioned"
285
+ };
286
+ }
287
+ },
288
+ {
289
+ name: "Rollback Strategy",
290
+ check(ctx) {
291
+ const keywords = /\b(rollback|revert|migration down|undo|backward.?compat|breaking change)\b/i;
292
+ const found = keywords.test(ctx.fullText);
293
+ return {
294
+ found,
295
+ score: found ? 100 : 0,
296
+ details: found ? "Rollback/revert strategy mentioned" : "No rollback strategy found"
297
+ };
298
+ }
299
+ },
300
+ {
301
+ name: "File Specificity",
302
+ check(ctx) {
303
+ const paths = ctx.fullText.match(FILE_PATH_PATTERN) || [];
304
+ const uniquePaths = new Set(paths).size;
305
+ const found = uniquePaths > 0;
306
+ return {
307
+ found,
308
+ score: found ? Math.min(uniquePaths * 15, 100) : 0,
309
+ details: found ? `${uniquePaths} specific file path(s) referenced` : "No concrete file paths \u2014 plan may be too vague"
310
+ };
311
+ }
312
+ },
313
+ {
314
+ name: "Scope Clarity",
315
+ check(ctx) {
316
+ const headingCount = ctx.sections.length;
317
+ const hasMultipleLevels = new Set(ctx.sections.map((s) => s.level)).size > 1;
318
+ const found = headingCount >= 3 && hasMultipleLevels;
319
+ return {
320
+ found,
321
+ score: found ? 100 : headingCount >= 2 ? 60 : 30,
322
+ details: `${headingCount} section(s), ${hasMultipleLevels ? "multiple heading levels" : "single heading level"}`
323
+ };
324
+ }
325
+ },
326
+ {
327
+ name: "Verification Steps",
328
+ check(ctx) {
329
+ const keywords = /\b(verification|how to test|verify|acceptance criteria|smoke test|manual test|check that|should see|expected result)\b/i;
330
+ const hasVerifyHeading = ctx.sections.some(
331
+ (s) => /\b(verification|testing|test)\b/i.test(s.title)
332
+ );
333
+ const found = keywords.test(ctx.fullText) || hasVerifyHeading;
334
+ return {
335
+ found,
336
+ score: hasVerifyHeading ? 100 : found ? 50 : 0,
337
+ details: hasVerifyHeading ? "Dedicated verification section" : found ? "Verification mentioned but no dedicated section" : "No verification steps found"
338
+ };
339
+ }
340
+ },
341
+ {
342
+ name: "Security Considerations",
343
+ check(ctx) {
344
+ const keywords = /\b(security|authentication|authorization|auth|CSRF|XSS|SQL injection|sanitiz|validat|CORS|rate.?limit|encrypt)\b/i;
345
+ const found = keywords.test(ctx.fullText);
346
+ const isRelevant = /\b(API|endpoint|route|database|user|login|form|input|request)\b/i.test(
347
+ ctx.fullText
348
+ );
349
+ return {
350
+ found: found || !isRelevant,
351
+ score: found ? 100 : isRelevant ? 0 : 100,
352
+ details: found ? "Security considerations mentioned" : isRelevant ? "Plan involves user-facing features but no security considerations" : "Not applicable (no user-facing features detected)"
353
+ };
354
+ }
355
+ },
356
+ {
357
+ name: "Dependencies Identified",
358
+ check(ctx) {
359
+ const keywords = /\b(dependenc|npm|package|library|import|install|require|third.?party|external|API key|service)\b/i;
360
+ const found = keywords.test(ctx.fullText);
361
+ return {
362
+ found,
363
+ score: found ? 100 : 0,
364
+ details: found ? "External dependencies or services identified" : "No dependencies mentioned \u2014 may be self-contained or incomplete"
365
+ };
366
+ }
367
+ }
368
+ ];
369
+ var MAX_SCORING_INPUT_LENGTH = 5e5;
370
+ function scorePlanQuality(blocks, sections) {
371
+ const fullText = blocks.map((b) => b.content).join("\n\n");
372
+ if (fullText.length > MAX_SCORING_INPUT_LENGTH) {
373
+ return {
374
+ overall: 0,
375
+ breakdown: DIMENSION_CHECKERS.map((c) => ({
376
+ name: c.name,
377
+ score: 0,
378
+ found: false,
379
+ details: "Input too large to score"
380
+ })),
381
+ suggestions: ["Plan text exceeds maximum scoring size"],
382
+ scoredAt: Date.now()
383
+ };
384
+ }
385
+ const ctx = { blocks, sections, fullText };
386
+ const breakdown = DIMENSION_CHECKERS.map((checker) => {
387
+ const result = checker.check(ctx);
388
+ return {
389
+ name: checker.name,
390
+ score: result.score,
391
+ found: result.found,
392
+ details: result.details
393
+ };
394
+ });
395
+ const overall = Math.round(
396
+ breakdown.reduce((sum, d) => sum + d.score, 0) / breakdown.length
397
+ );
398
+ const suggestions = breakdown.filter((d) => !d.found).map((d) => `Consider adding: ${d.name.toLowerCase()} \u2014 ${d.details}`);
399
+ return {
400
+ overall,
401
+ breakdown,
402
+ suggestions,
403
+ scoredAt: Date.now()
404
+ };
405
+ }
406
+ function buildAiScoringPrompt(markdown) {
407
+ return `You are a senior engineering reviewer. Score this implementation plan on 8 quality dimensions.
408
+
409
+ For each dimension, provide:
410
+ - name: dimension name (exactly as listed)
411
+ - score: 0-100
412
+ - found: whether the dimension is adequately addressed
413
+ - details: brief explanation
414
+
415
+ Dimensions to score:
416
+ 1. Test Plan \u2014 testing strategy, coverage requirements
417
+ 2. Error Handling \u2014 edge cases, fallbacks, graceful degradation
418
+ 3. Rollback Strategy \u2014 revert plan, backward compatibility
419
+ 4. File Specificity \u2014 concrete file paths and locations
420
+ 5. Scope Clarity \u2014 well-structured sections, clear boundaries
421
+ 6. Verification Steps \u2014 how to verify the implementation works
422
+ 7. Security Considerations \u2014 auth, validation, XSS, injection risks
423
+ 8. Dependencies Identified \u2014 external libraries, services, APIs
424
+
425
+ Also provide:
426
+ - overall: weighted average score (0-100)
427
+ - suggestions: array of improvement suggestions (strings)
428
+
429
+ Return ONLY valid JSON matching this schema:
430
+ {
431
+ "overall": number,
432
+ "breakdown": [{ "name": string, "score": number, "found": boolean, "details": string }],
433
+ "suggestions": [string]
434
+ }
435
+
436
+ Plan to review:
437
+
438
+ ${markdown}`;
439
+ }
440
+ function validateSlug(slug) {
441
+ if (!slug || slug.includes("..") || slug.includes("/") || slug.includes("\\") || slug.length > 200) {
442
+ throw new Error("Invalid plan slug");
443
+ }
444
+ if (!/^[a-zA-Z0-9_-]+$/.test(slug)) {
445
+ throw new Error("Invalid plan slug characters");
446
+ }
447
+ }
448
+ function toProjectSlug(projectPath) {
449
+ return projectPath.toLowerCase().replace(/^\//, "").replace(/[/\\]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 200);
450
+ }
451
+ async function ensurePlanDir(slug, projectPath) {
452
+ validateSlug(slug);
453
+ let dir;
454
+ if (projectPath) {
455
+ const projectSlug = toProjectSlug(projectPath);
456
+ validateSlug(projectSlug);
457
+ dir = join(REVIEWS_DIR, projectSlug, slug);
458
+ } else {
459
+ dir = join(REVIEWS_DIR, slug);
460
+ }
461
+ await fs.mkdir(dir, { recursive: true });
462
+ return dir;
463
+ }
464
+ function extractTitleFromMarkdown(plan) {
465
+ const match = plan.match(/^#\s+(.+)$/m);
466
+ if (match) return match[1].trim();
467
+ const firstLine = plan.split("\n").find((line) => line.trim());
468
+ return firstLine?.trim().slice(0, 50) || "Untitled Plan";
469
+ }
470
+ async function savePlan(plan, version, slug, projectPath) {
471
+ const title = extractTitleFromMarkdown(plan);
472
+ const resolvedSlug = slug || title.toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
473
+ const dir = await ensurePlanDir(resolvedSlug, projectPath);
474
+ const planFile = join(dir, "plan.json");
475
+ await fs.writeFile(
476
+ planFile,
477
+ JSON.stringify(
478
+ { title, slug: resolvedSlug, plan, version, projectPath },
479
+ null,
480
+ 2
481
+ ),
482
+ "utf-8"
483
+ );
484
+ const versionFile = join(dir, `v${version.versionNumber}.json`);
485
+ await fs.writeFile(
486
+ versionFile,
487
+ JSON.stringify({ plan, version, projectPath }, null, 2),
488
+ "utf-8"
489
+ );
490
+ }
491
+ async function loadPlan(slug, projectPath) {
492
+ try {
493
+ validateSlug(slug);
494
+ const candidates = [];
495
+ if (projectPath) {
496
+ const projectSlug = toProjectSlug(projectPath);
497
+ candidates.push(join(REVIEWS_DIR, projectSlug, slug));
498
+ }
499
+ candidates.push(join(REVIEWS_DIR, slug));
500
+ for (const dir of candidates) {
501
+ const result = await tryLoadPlanFromDir(dir);
502
+ if (result) return result;
503
+ }
504
+ try {
505
+ const topEntries = await fs.readdir(REVIEWS_DIR, { withFileTypes: true });
506
+ for (const entry of topEntries) {
507
+ if (!entry.isDirectory()) continue;
508
+ const dir = join(REVIEWS_DIR, entry.name, slug);
509
+ const result = await tryLoadPlanFromDir(dir);
510
+ if (result) return result;
511
+ }
512
+ } catch {
513
+ }
514
+ return null;
515
+ } catch {
516
+ return null;
517
+ }
518
+ }
519
+ async function tryLoadPlanFromDir(dir) {
520
+ try {
521
+ const planFile = join(dir, "plan.json");
522
+ const content = await fs.readFile(planFile, "utf-8");
523
+ const data = JSON.parse(content);
524
+ const files = await fs.readdir(dir);
525
+ const versionFiles = files.filter((f) => f.match(/^v\d+\.json$/));
526
+ const versions = [];
527
+ for (const file of versionFiles) {
528
+ const versionContent = await fs.readFile(join(dir, file), "utf-8");
529
+ const versionData = JSON.parse(versionContent);
530
+ versions.push(versionData.version);
531
+ }
532
+ versions.sort((a, b) => a.versionNumber - b.versionNumber);
533
+ return {
534
+ title: data.title,
535
+ slug: data.slug,
536
+ plan: data.plan,
537
+ version: data.version,
538
+ versions,
539
+ projectPath: data.projectPath
540
+ };
541
+ } catch {
542
+ return null;
543
+ }
544
+ }
545
+ async function getNextVersionNumber(slug, projectPath) {
546
+ try {
547
+ validateSlug(slug);
548
+ let dir;
549
+ if (projectPath) {
550
+ const projectSlug = toProjectSlug(projectPath);
551
+ dir = join(REVIEWS_DIR, projectSlug, slug);
552
+ } else {
553
+ dir = join(REVIEWS_DIR, slug);
554
+ }
555
+ const files = await fs.readdir(dir);
556
+ const versionFiles = files.filter((f) => f.match(/^v\d+\.json$/));
557
+ if (versionFiles.length === 0) return 1;
558
+ const versions = versionFiles.map((f) => {
559
+ const match = f.match(/^v(\d+)\.json$/);
560
+ return match ? parseInt(match[1], 10) : 0;
561
+ });
562
+ return Math.max(...versions) + 1;
563
+ } catch {
564
+ return 1;
565
+ }
566
+ }
567
+ async function listPlansWithMeta(projectPath) {
568
+ try {
569
+ const summaries = [];
570
+ if (projectPath) {
571
+ const projectSlug = toProjectSlug(projectPath);
572
+ const baseDir = join(REVIEWS_DIR, projectSlug);
573
+ await collectPlansFromDir(baseDir, summaries);
574
+ } else {
575
+ await fs.mkdir(REVIEWS_DIR, { recursive: true });
576
+ const topEntries = await fs.readdir(REVIEWS_DIR, { withFileTypes: true });
577
+ for (const entry of topEntries) {
578
+ if (!entry.isDirectory()) continue;
579
+ const projectDir = join(REVIEWS_DIR, entry.name);
580
+ await collectPlansFromDir(projectDir, summaries);
581
+ }
582
+ }
583
+ summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
584
+ return summaries;
585
+ } catch {
586
+ return [];
587
+ }
588
+ }
589
+ async function collectPlansFromDir(baseDir, summaries) {
590
+ try {
591
+ const entries = await fs.readdir(baseDir, { withFileTypes: true });
592
+ for (const entry of entries) {
593
+ if (!entry.isDirectory()) continue;
594
+ try {
595
+ const dir = join(baseDir, entry.name);
596
+ const planFile = join(dir, "plan.json");
597
+ const content = await fs.readFile(planFile, "utf-8");
598
+ const data = JSON.parse(content);
599
+ const files = await fs.readdir(dir);
600
+ const versionFiles = files.filter((f) => f.match(/^v\d+\.json$/));
601
+ let status = "pending";
602
+ if (data.version?.decision) {
603
+ status = data.version.decision.behavior === "allow" ? "approved" : "denied";
604
+ }
605
+ const stat = await fs.stat(planFile);
606
+ summaries.push({
607
+ slug: entry.name,
608
+ title: data.title || entry.name,
609
+ versionCount: versionFiles.length,
610
+ updatedAt: new Date(stat.mtimeMs).toISOString(),
611
+ status,
612
+ projectPath: data.projectPath
613
+ });
614
+ } catch {
615
+ }
616
+ }
617
+ } catch {
618
+ }
619
+ }
620
+ function isGitCommitCommand(command) {
621
+ if (!command) return false;
622
+ const subcommands = command.split(/\s*(?:&&|\|\||[;|])\s*/);
623
+ return subcommands.some((sub) => /(?:^|\b)(?:\w+=\S*\s+)*git(?:\s+-c\s+\S+)*\s+commit\b/.test(sub));
624
+ }
625
+ function gitOutputSync(args, cwd) {
626
+ const { execFileSync } = __require2("child_process");
627
+ try {
628
+ return execFileSync(args[0], args.slice(1), {
629
+ cwd,
630
+ encoding: "utf-8",
631
+ timeout: 1e4,
632
+ maxBuffer: 10 * 1024 * 1024
633
+ });
634
+ } catch {
635
+ return "";
636
+ }
637
+ }
638
+ function captureUntrackedDiff(files, cwd) {
639
+ const { existsSync, readFileSync } = __require2("fs");
640
+ const { join: join2 } = __require2("path");
641
+ const parts = [];
642
+ for (const file of files) {
643
+ const fullPath = join2(cwd, file);
644
+ if (!existsSync(fullPath)) continue;
645
+ const noIndexDiff = gitOutputSync(["git", "diff", "--no-index", "--", "/dev/null", file], cwd);
646
+ if (noIndexDiff.trim()) {
647
+ parts.push(noIndexDiff);
648
+ } else {
649
+ try {
650
+ const content = readFileSync(fullPath, "utf-8");
651
+ const lines = content.split("\n");
652
+ const header = `diff --git a/${file} b/${file}
653
+ new file mode 100644
654
+ --- /dev/null
655
+ +++ b/${file}
656
+ @@ -0,0 +1,${lines.length} @@`;
657
+ const body = lines.map((l) => `+${l}`).join("\n");
658
+ parts.push(`${header}
659
+ ${body}`);
660
+ } catch {
661
+ }
662
+ }
663
+ }
664
+ return parts.join("\n");
665
+ }
666
+ function captureCommitDiff(opts) {
667
+ const { command, cwd } = opts;
668
+ let commitMessage = "";
669
+ const heredocMatch = command.match(/<<'?EOF'?\s*\n([\s\S]*?)\n\s*EOF/);
670
+ if (heredocMatch) {
671
+ commitMessage = heredocMatch[1].trim();
672
+ } else {
673
+ const mMatch = command.match(/-m\s+(?:"([^"]*(?:\\.[^"]*)*)"|'([^']*)'|(\S+))/);
674
+ if (mMatch) {
675
+ commitMessage = mMatch[1] ?? mMatch[2] ?? mMatch[3] ?? "";
676
+ }
677
+ }
678
+ const hasAllFlag = /\bcommit\b[^|&;]*(?:-[a-z]*a[a-z]*\b|--all\b)/.test(command);
679
+ const isAmend = /\bcommit\b[^|&;\n]*--amend\b/.test(command);
680
+ const allowEmpty = /\bcommit\b[^|&;\n]*--allow-empty\b/.test(command);
681
+ const scopedFiles = [];
682
+ let hasChainedAdd = false;
683
+ let isBroadAdd = false;
684
+ const chainedAddMatch = command.match(/\bgit\s+add\s+([\s\S]*?)(?:&&|;|\n)\s*git\b.*\bcommit\b/);
685
+ if (chainedAddMatch) {
686
+ hasChainedAdd = true;
687
+ const addArgs = chainedAddMatch[1].trim();
688
+ isBroadAdd = addArgs === "." || /(?:^|\s)(?:-A|--all|-u|--update)(?:\s|$)/.test(addArgs);
689
+ const files = addArgs.split(/\s+/).filter((arg) => !arg.startsWith("-") && arg !== ".");
690
+ scopedFiles.push(...files);
691
+ }
692
+ const commitPart = command.match(/\bgit\b.*\bcommit\b(.*)/s);
693
+ if (commitPart) {
694
+ let rest = commitPart[1];
695
+ rest = rest.replace(/-m\s+(?:"[^"]*(?:\\.[^"]*)*"|'[^']*'|\S+)/, "");
696
+ rest = rest.replace(/<<'?EOF'?\s*\n[\s\S]*?\n\s*EOF/, "");
697
+ rest = rest.replace(/-m\s*"?\$\(cat\s*<<'?EOF'?[\s\S]*?EOF\s*\)"?/, "");
698
+ rest = rest.replace(/\s--(?:amend|no-edit|no-verify|allow-empty|signoff|gpg-sign|no-gpg-sign|fixup|squash|reset-author|short|branch|long|porcelain|dry-run|verbose|quiet|all)\b/g, "");
699
+ rest = rest.replace(/\s-[aSsnvq]\b/g, "");
700
+ rest = rest.replace(/\s--\s/, " ");
701
+ const commitFiles = rest.trim().split(/\s+/).filter((f) => f && !f.startsWith("-"));
702
+ if (commitFiles.length > 0) {
703
+ scopedFiles.push(...commitFiles);
704
+ }
705
+ }
706
+ let diff = "";
707
+ if (isAmend) {
708
+ const baseArgs = scopedFiles.length > 0 ? ["git", "diff", "HEAD~1", "--", ...scopedFiles] : ["git", "diff", "HEAD~1"];
709
+ diff = gitOutputSync(baseArgs, cwd);
710
+ if (!diff.trim()) {
711
+ const fallbackArgs = scopedFiles.length > 0 ? ["git", "diff", "HEAD", "--", ...scopedFiles] : ["git", "diff", "HEAD"];
712
+ diff = gitOutputSync(fallbackArgs, cwd);
713
+ }
714
+ } else if (hasChainedAdd && !isBroadAdd && scopedFiles.length > 0) {
715
+ const stagedDiff = gitOutputSync(["git", "diff", "--cached", "--", ...scopedFiles], cwd);
716
+ const unstagedDiff = gitOutputSync(["git", "diff", "--", ...scopedFiles], cwd);
717
+ diff = [stagedDiff, unstagedDiff].filter((d) => d.trim()).join("\n");
718
+ if (!diff.trim()) {
719
+ diff = captureUntrackedDiff(scopedFiles, cwd);
720
+ }
721
+ } else if (hasChainedAdd && isBroadAdd) {
722
+ const stagedDiff = gitOutputSync(["git", "diff", "--cached"], cwd);
723
+ const unstagedDiff = gitOutputSync(["git", "diff"], cwd);
724
+ diff = [stagedDiff, unstagedDiff].filter((d) => d.trim()).join("\n");
725
+ if (!diff.trim()) {
726
+ const untrackedFiles = gitOutputSync(["git", "ls-files", "--others", "--exclude-standard"], cwd).split("\n").filter((f) => f.trim());
727
+ if (untrackedFiles.length > 0) {
728
+ diff = captureUntrackedDiff(untrackedFiles, cwd);
729
+ }
730
+ }
731
+ } else {
732
+ const baseArgs = hasAllFlag ? ["git", "diff", "HEAD"] : ["git", "diff", "--cached"];
733
+ const diffArgs = scopedFiles.length > 0 ? [...baseArgs, "--", ...scopedFiles] : baseArgs;
734
+ diff = gitOutputSync(diffArgs, cwd);
735
+ if (!diff.trim() && hasAllFlag) {
736
+ const fallbackArgs = scopedFiles.length > 0 ? ["git", "diff", "--cached", "--", ...scopedFiles] : ["git", "diff", "--cached"];
737
+ diff = gitOutputSync(fallbackArgs, cwd);
738
+ }
739
+ if (!diff.trim() && scopedFiles.length > 0) {
740
+ diff = captureUntrackedDiff(scopedFiles, cwd);
741
+ }
742
+ }
743
+ return { diff, commitMessage, allowEmpty, isAmend };
744
+ }
745
+ var ReviewConfigSchema = external_exports.object({
746
+ enabled: external_exports.boolean().default(true),
747
+ plan_review: external_exports.boolean().default(true),
748
+ code_review: external_exports.boolean().default(true),
749
+ auto_open_browser: external_exports.boolean().default(true),
750
+ require_server: external_exports.boolean().default(false),
751
+ review_timeout_seconds: external_exports.number().int().min(0).max(600).default(0),
752
+ timeout_behavior: external_exports.enum(["allow", "deny"]).default("allow"),
753
+ default_export_format: external_exports.enum(["markdown", "github", "jira", "json"]).default("markdown"),
754
+ webhook_url: external_exports.string().default("")
755
+ });
756
+
757
+ export {
758
+ parseMarkdownToBlocks,
759
+ extractTitle,
760
+ generateSlug,
761
+ extractSections,
762
+ getSectionFullText,
763
+ scorePlanQuality,
764
+ buildAiScoringPrompt,
765
+ savePlan,
766
+ loadPlan,
767
+ getNextVersionNumber,
768
+ listPlansWithMeta,
769
+ isGitCommitCommand,
770
+ captureCommitDiff,
771
+ ReviewConfigSchema
772
+ };