critique 0.1.134 → 0.1.135

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.
@@ -1 +1 @@
1
- {"version":3,"file":"balance-delimiters.d.ts","sourceRoot":"","sources":["../src/balance-delimiters.ts"],"names":[],"mappings":"AA4DA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CActE;AAmPD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAqE5E"}
1
+ {"version":3,"file":"balance-delimiters.d.ts","sourceRoot":"","sources":["../src/balance-delimiters.ts"],"names":[],"mappings":"AAmEA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CActE;AAwbD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA4E5E"}
@@ -32,7 +32,7 @@ const htmlCommentRule = {
32
32
  const LANGUAGE_DELIMITERS = {
33
33
  typescript: [{ token: "`" }, cStyleBlockCommentRule],
34
34
  python: [{ token: '"""' }, { token: "'''" }],
35
- markdown: [{ token: "```" }],
35
+ markdown: [{ token: "```", classifyFence: classifyMarkdownFence }],
36
36
  go: [{ token: "`" }, cStyleBlockCommentRule],
37
37
  rust: [cStyleBlockCommentRule],
38
38
  cpp: [cStyleBlockCommentRule],
@@ -201,6 +201,148 @@ function escapeDelimiterAt(lines, hunkLineIndex, column) {
201
201
  return prefix + content.slice(0, column) + "\\" + content.slice(column);
202
202
  });
203
203
  }
204
+ // ---------------------------------------------------------------------------
205
+ // Contextual fence classification and repair (markdown code fences)
206
+ // ---------------------------------------------------------------------------
207
+ /**
208
+ * Classify a ``` occurrence as a markdown fence opener, closer, or not a fence.
209
+ *
210
+ * Returns null if the occurrence is not a valid block-level fence (e.g. inline
211
+ * triple-backtick in prose, or indented more than 3 spaces).
212
+ * Returns "open" if followed by an info string (language tag).
213
+ * Returns "close" if nothing follows the backtick run (only whitespace).
214
+ */
215
+ function classifyMarkdownFence(content, column) {
216
+ // Must be at start of line with at most 3 spaces of indentation
217
+ const beforeFence = content.slice(0, column);
218
+ if (beforeFence.length > 3 || /\S/.test(beforeFence))
219
+ return null;
220
+ // Count consecutive backticks (support 4+ backtick fences per CommonMark)
221
+ let fenceLen = 0;
222
+ for (let i = column; i < content.length && content[i] === "`"; i++)
223
+ fenceLen++;
224
+ if (fenceLen < 3)
225
+ return null;
226
+ // What comes after the backtick run?
227
+ const afterFence = content.slice(column + fenceLen).trim();
228
+ // Closing fence: nothing after backticks (only whitespace)
229
+ if (!afterFence)
230
+ return "close";
231
+ // Opening fence: has info string (language tag)
232
+ // Info string must not contain backticks (CommonMark spec)
233
+ if (!afterFence.includes("`"))
234
+ return "open";
235
+ return null;
236
+ }
237
+ /**
238
+ * Simulate a sequential walk through classified fences to detect boundary
239
+ * artifacts. Markdown code fences don't nest, so depth toggles between 0
240
+ * (outside) and 1 (inside).
241
+ *
242
+ * - "open" (has info string) always pushes depth to 1.
243
+ * - "close" (bare fence) decrements if inside, or acts as a bare opener
244
+ * if already outside (a code block without a language tag).
245
+ *
246
+ * Conflicts are counted when a must-open fires while already inside (depth
247
+ * was already 1) or when other impossible transitions occur.
248
+ */
249
+ function walkFences(fences, startDepth) {
250
+ let depth = startDepth;
251
+ let conflicts = 0;
252
+ for (const fence of fences) {
253
+ if (fence.kind === "open") {
254
+ if (depth > 0)
255
+ conflicts++;
256
+ depth = 1;
257
+ }
258
+ else {
259
+ // "close" (bare fence): close if inside, otherwise bare opener
260
+ if (depth > 0) {
261
+ depth = 0;
262
+ }
263
+ else {
264
+ depth = 1;
265
+ }
266
+ }
267
+ }
268
+ return { startDepth, endDepth: depth, conflicts };
269
+ }
270
+ /**
271
+ * Repair context-dependent fences (like markdown ```) using sequential
272
+ * open/close pairing instead of simple odd/even counting.
273
+ *
274
+ * Tries two starting states (outside vs inside a code block), picks the
275
+ * walk with fewer conflicts, and adds synthetic fence tokens inline:
276
+ * - If the hunk starts inside a block: prepend ``` to the first content
277
+ * line so the boundary closer has something to close.
278
+ * - If the hunk ends inside a block: append ``` to the last content
279
+ * line so the boundary opener is properly closed.
280
+ * Tokens are added inline (no new lines) to preserve patch header counts.
281
+ */
282
+ function repairContextualFences(contentLines, lines, rule) {
283
+ if (!rule.classifyFence)
284
+ return [...lines];
285
+ const occurrences = findDelimiterOccurrences(contentLines, rule.token);
286
+ if (occurrences.length === 0)
287
+ return [...lines];
288
+ // Classify each occurrence as fence open, close, or not-a-fence
289
+ const fences = [];
290
+ for (const occ of occurrences) {
291
+ const content = contentLines[occ.contentLineIndex]?.content;
292
+ if (!content)
293
+ continue;
294
+ const kind = rule.classifyFence(content, occ.column);
295
+ if (kind) {
296
+ fences.push({ occurrence: occ, kind });
297
+ }
298
+ }
299
+ if (fences.length === 0)
300
+ return [...lines];
301
+ // Try both starting states
302
+ const walk0 = walkFences(fences, 0);
303
+ const walk1 = walkFences(fences, 1);
304
+ // Pick better walk: fewer conflicts → fewer repairs → content heuristic
305
+ let chosen;
306
+ if (walk0.conflicts < walk1.conflicts) {
307
+ chosen = walk0;
308
+ }
309
+ else if (walk1.conflicts < walk0.conflicts) {
310
+ chosen = walk1;
311
+ }
312
+ else {
313
+ const repairs0 = (walk0.startDepth > 0 ? 1 : 0) + (walk0.endDepth > 0 ? 1 : 0);
314
+ const repairs1 = (walk1.startDepth > 0 ? 1 : 0) + (walk1.endDepth > 0 ? 1 : 0);
315
+ if (repairs0 < repairs1) {
316
+ chosen = walk0;
317
+ }
318
+ else if (repairs1 < repairs0) {
319
+ chosen = walk1;
320
+ }
321
+ else {
322
+ // Still tied: disambiguate by content position relative to the first fence.
323
+ // If there's non-empty content before the first fence in the hunk, the fence
324
+ // is likely closing a block from before the hunk → prefer depth=1 (starts inside).
325
+ // This produces better tree-sitter pairing: the prepended ``` + original ```
326
+ // form a matched pair, while appended inline ``` doesn't close a fence.
327
+ const firstIdx = fences[0]?.occurrence.contentLineIndex ?? 0;
328
+ const hasContentBefore = contentLines.slice(0, firstIdx).some((line) => line.content.trim() !== "");
329
+ chosen = hasContentBefore ? walk1 : walk0;
330
+ }
331
+ }
332
+ let result = [...lines];
333
+ // Append synthetic closer to end of last content line (inline, no new lines)
334
+ if (chosen.endDepth > 0) {
335
+ result = appendClosingTokensToLastContentLine(result, rule.token, 1);
336
+ }
337
+ // Prepend synthetic opener to start of first content line (inline, no new lines).
338
+ // Pass the first fence's hunk line index so the search only looks at lines
339
+ // before the boundary closer — the opener must precede it to form a pair.
340
+ if (chosen.startDepth > 0) {
341
+ const firstFenceHunkLine = fences[0]?.occurrence.hunkLineIndex;
342
+ result = prependOpeningTokenToFirstContentLine(result, rule.token, firstFenceHunkLine);
343
+ }
344
+ return result;
345
+ }
204
346
  function getRuleOpenTokens(rule) {
205
347
  return rule.openTokens ?? [rule.token];
206
348
  }
@@ -239,6 +381,35 @@ function appendClosingTokensToLastContentLine(lines, closeToken, count) {
239
381
  return `${line} ${closingSuffix}`;
240
382
  });
241
383
  }
384
+ function prependOpeningTokenToFirstContentLine(lines, openToken, beforeHunkLineIndex) {
385
+ // Prefer a blank/whitespace content line to avoid creating a fake info string
386
+ // (e.g. "``` handler() {" makes tree-sitter think "handler()" is a language).
387
+ // Only search among lines BEFORE the first fence so the synthetic opener
388
+ // appears before the boundary closer (they need to pair).
389
+ const firstContentLineIndex = lines.findIndex(isDiffContentLine);
390
+ if (firstContentLineIndex === -1) {
391
+ return [...lines];
392
+ }
393
+ const searchEnd = beforeHunkLineIndex ?? lines.length;
394
+ let targetIndex = firstContentLineIndex;
395
+ for (let i = firstContentLineIndex; i < searchEnd; i++) {
396
+ const line = lines[i];
397
+ if (!isDiffContentLine(line))
398
+ continue;
399
+ if (line.slice(1).trim() === "") {
400
+ targetIndex = i;
401
+ break;
402
+ }
403
+ }
404
+ return lines.map((line, index) => {
405
+ if (index !== targetIndex || !isDiffContentLine(line)) {
406
+ return line;
407
+ }
408
+ const prefix = line[0] ?? "";
409
+ const content = line.slice(1);
410
+ return `${prefix}${openToken} ${content}`;
411
+ });
412
+ }
242
413
  /**
243
414
  * Balance paired delimiters in a unified diff patch for correct syntax
244
415
  * highlighting.
@@ -284,6 +455,12 @@ export function balanceDelimiters(rawDiff, filetype) {
284
455
  if (!isSymmetricRule(rule)) {
285
456
  continue;
286
457
  }
458
+ // Contextual fence pairing (markdown code fences): uses sequential
459
+ // open/close tracking instead of simple odd/even parity.
460
+ if (rule.classifyFence) {
461
+ repairedLines = repairContextualFences(contentLines, repairedLines, rule);
462
+ continue;
463
+ }
287
464
  const occurrences = findDelimiterOccurrences(contentLines, rule.token);
288
465
  if (occurrences.length % 2 === 0) {
289
466
  continue;
@@ -439,7 +439,7 @@ describe("balanceDelimiters", () => {
439
439
  ]);
440
440
  expect(balanceDelimiters(patch, "markdown")).toBe(patch);
441
441
  });
442
- it("escapes a leading closing code fence when count is odd", () => {
442
+ it("prepends synthetic opener when hunk starts inside a code block", () => {
443
443
  const patch = mdPatch([
444
444
  " inside fenced block",
445
445
  " ```",
@@ -448,8 +448,9 @@ describe("balanceDelimiters", () => {
448
448
  ]);
449
449
  const result = balanceDelimiters(patch, "markdown");
450
450
  const lines = result.split("\n");
451
- expect(lines[3]).toBe(" inside fenced block");
452
- expect(lines[4]).toBe(" \\```");
451
+ // Synthetic ``` opener prepended inline to first content line
452
+ expect(lines[3]).toBe(" ``` inside fenced block");
453
+ expect(lines[4]).toBe(" ```");
453
454
  });
454
455
  it("does not modify when only inline code backticks are present", () => {
455
456
  const patch = mdPatch([
@@ -459,7 +460,7 @@ describe("balanceDelimiters", () => {
459
460
  ]);
460
461
  expect(balanceDelimiters(patch, "markdown")).toBe(patch);
461
462
  });
462
- it("escapes a trailing unmatched fence opener instead of duplicating it", () => {
463
+ it("appends synthetic closer when hunk ends with unclosed opener", () => {
463
464
  const patch = mdPatch([
464
465
  " ```ts",
465
466
  " const x = 1",
@@ -468,8 +469,319 @@ describe("balanceDelimiters", () => {
468
469
  ]);
469
470
  const result = balanceDelimiters(patch, "markdown");
470
471
  const lines = result.split("\n");
471
- expect(lines[3]).toBe(" \\```ts");
472
+ // Original content untouched
473
+ expect(lines[3]).toBe(" ```ts");
472
474
  expect(lines[4]).toBe(" const x = 1");
475
+ // Synthetic ``` closer appended inline to last content line
476
+ expect(lines[lines.length - 1]).toBe("+new line ```");
477
+ });
478
+ it("adds synthetic fences at both boundaries when even count but first=closer last=opener (6 tokens)", () => {
479
+ const patch = mdPatch([
480
+ " ```",
481
+ " ",
482
+ " ## Section",
483
+ " ",
484
+ " ```ts",
485
+ " const a = 1",
486
+ " ```",
487
+ " ",
488
+ " ```ts",
489
+ " const b = 2",
490
+ " ```",
491
+ " ",
492
+ "+```ts",
493
+ ]);
494
+ const result = balanceDelimiters(patch, "markdown");
495
+ const lines = result.split("\n");
496
+ // Synthetic opener prepended inline to first content line
497
+ expect(lines[3]).toBe(" ``` ```");
498
+ // Synthetic closer appended inline to last content line
499
+ expect(lines[lines.length - 1]).toBe("+```ts ```");
500
+ // middle fences stay untouched
501
+ expect(lines[7]).toBe(" ```ts");
502
+ expect(lines[9]).toBe(" ```");
503
+ expect(lines[11]).toBe(" ```ts");
504
+ expect(lines[13]).toBe(" ```");
505
+ });
506
+ it("adds synthetic fences at both boundaries with 4 tokens (bare, ```ts, bare, ```ts)", () => {
507
+ const patch = mdPatch([
508
+ " inside code block",
509
+ " ```",
510
+ " ",
511
+ " ```ts",
512
+ " const x = 1",
513
+ " ```",
514
+ " ",
515
+ "+```ts",
516
+ ]);
517
+ const result = balanceDelimiters(patch, "markdown");
518
+ const lines = result.split("\n");
519
+ // Synthetic opener prepended inline to first content line
520
+ expect(lines[3]).toBe(" ``` inside code block");
521
+ // Synthetic closer appended inline to last content line
522
+ expect(lines[lines.length - 1]).toBe("+```ts ```");
523
+ });
524
+ it("returns patch unchanged when 4 tokens are fully balanced (```ts, bare, ```ts, bare)", () => {
525
+ const patch = mdPatch([
526
+ " ```ts",
527
+ " const a = 1",
528
+ " ```",
529
+ " ",
530
+ " ```ts",
531
+ " const b = 2",
532
+ " ```",
533
+ "+New paragraph",
534
+ ]);
535
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
536
+ });
537
+ it("adds synthetic fences at both boundaries when 2 tokens are bare-closer then opener", () => {
538
+ const patch = mdPatch([
539
+ " inside block",
540
+ " ```",
541
+ " ",
542
+ "+```ts",
543
+ ]);
544
+ const result = balanceDelimiters(patch, "markdown");
545
+ const lines = result.split("\n");
546
+ // Synthetic opener prepended inline to first content line
547
+ expect(lines[3]).toBe(" ``` inside block");
548
+ // Synthetic closer appended inline to last content line
549
+ expect(lines[lines.length - 1]).toBe("+```ts ```");
550
+ });
551
+ it("returns patch unchanged for 2 balanced tokens (```ts then bare)", () => {
552
+ const patch = mdPatch([
553
+ " ```ts",
554
+ " const x = 1",
555
+ " ```",
556
+ "+New paragraph",
557
+ ]);
558
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
559
+ });
560
+ it("returns patch unchanged for two bare fences (open + close, no language)", () => {
561
+ const patch = mdPatch([
562
+ " ```",
563
+ " some code",
564
+ " ```",
565
+ "+New paragraph",
566
+ ]);
567
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
568
+ });
569
+ it("ignores inline triple backticks in prose (not at start of line)", () => {
570
+ const patch = mdPatch([
571
+ " Use the ``` delimiter for code fences",
572
+ "-old",
573
+ "+new",
574
+ ]);
575
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
576
+ });
577
+ it("treats fences with up to 3 spaces indent as valid", () => {
578
+ const patch = mdPatch([
579
+ " ```ts",
580
+ " const x = 1",
581
+ " ```",
582
+ "+New paragraph",
583
+ ]);
584
+ // 3 spaces + ``` = column 3, indent 3 = valid fence
585
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
586
+ });
587
+ it("ignores fences indented more than 3 spaces (code indentation)", () => {
588
+ const patch = mdPatch([
589
+ " ```ts",
590
+ " const x = 1",
591
+ "-old",
592
+ "+new",
593
+ ]);
594
+ // 4+ spaces = not a fence, treated as code content → no escaping
595
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
596
+ });
597
+ it("handles 4-backtick fence pair correctly", () => {
598
+ const patch = mdPatch([
599
+ " ````ts",
600
+ " const x = 1",
601
+ " ````",
602
+ "+New paragraph",
603
+ ]);
604
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
605
+ });
606
+ it("recognizes closing fence with trailing spaces", () => {
607
+ const patch = mdPatch([
608
+ " ```ts",
609
+ " const x = 1",
610
+ " ``` ",
611
+ "+New paragraph",
612
+ ]);
613
+ expect(balanceDelimiters(patch, "markdown")).toBe(patch);
614
+ });
615
+ it("single bare fence with content on both sides treats as opener (appends closer)", () => {
616
+ // Ambiguous: bare ``` with content before AND after.
617
+ // Tie-break prefers depth=1 (prepend opener) since content exists before fence.
618
+ const patch = mdPatch([
619
+ " Intro paragraph",
620
+ " ```",
621
+ " code line",
622
+ "+new line",
623
+ ]);
624
+ const result = balanceDelimiters(patch, "markdown");
625
+ const lines = result.split("\n");
626
+ // Prepend opener on first content line (content before fence)
627
+ expect(lines[3]).toBe(" ``` Intro paragraph");
628
+ expect(lines[4]).toBe(" ```");
629
+ });
630
+ it("single bare fence with content only after treats as opener (appends closer)", () => {
631
+ const patch = mdPatch([
632
+ " ```",
633
+ " code line",
634
+ "-old",
635
+ "+new",
636
+ ]);
637
+ const result = balanceDelimiters(patch, "markdown");
638
+ const lines = result.split("\n");
639
+ // No content before fence → depth=0 → append closer
640
+ expect(lines[3]).toBe(" ```");
641
+ expect(lines[lines.length - 1]).toBe("+new ```");
642
+ });
643
+ it("prefers blank line for synthetic opener to avoid fake info string", () => {
644
+ const patch = mdPatch([
645
+ " ",
646
+ " inside code block",
647
+ " ```",
648
+ "-old line",
649
+ "+new line",
650
+ ]);
651
+ const result = balanceDelimiters(patch, "markdown");
652
+ const lines = result.split("\n");
653
+ // Blank line before fence is used for synthetic opener (no fake info string)
654
+ expect(lines[3]).toBe(" ``` ");
655
+ expect(lines[4]).toBe(" inside code block");
656
+ expect(lines[5]).toBe(" ```");
657
+ });
658
+ it("handles two hunks independently for markdown fences", () => {
659
+ const patch = [
660
+ "--- file.md",
661
+ "+++ file.md",
662
+ "@@ -5,4 +5,4 @@",
663
+ " ```ts",
664
+ " const x = 1",
665
+ " ```",
666
+ "+New paragraph",
667
+ "@@ -20,4 +20,4 @@",
668
+ " inside block",
669
+ " ```",
670
+ "-old line",
671
+ "+new line",
672
+ ].join("\n");
673
+ const result = balanceDelimiters(patch, "markdown");
674
+ const lines = result.split("\n");
675
+ // First hunk: balanced, no changes
676
+ expect(lines[3]).toBe(" ```ts");
677
+ expect(lines[5]).toBe(" ```");
678
+ // Second hunk: bare closer at boundary → synthetic opener prepended inline
679
+ const secondHunkIdx = lines.findIndex((l, i) => i > 2 && l.startsWith("@@"));
680
+ expect(lines[secondHunkIdx + 1]).toBe(" ``` inside block");
681
+ expect(lines[secondHunkIdx + 2]).toBe(" ```");
682
+ });
683
+ it("handles real README.md hunk with boundary fences (from critique.work patch)", () => {
684
+ // Real hunk from https://critique.work/v/daa808658ee537a745b80101ba3195ae.patch
685
+ // First hunk: starts inside a code block (bare ``` closer), ends with ```ts opener
686
+ const patch = [
687
+ "--- README.md",
688
+ "+++ README.md",
689
+ "@@ -741,12 +741,14 @@",
690
+ " }),",
691
+ " )",
692
+ " ```",
693
+ " ",
694
+ " ## Base Path",
695
+ " ",
696
+ "+For standalone API servers (without Vite), set the base path in the constructor:",
697
+ "+",
698
+ " ```ts",
699
+ " import { Spiceflow } from 'spiceflow'",
700
+ " ",
701
+ " const app = new Spiceflow({ basePath: '/api/v1' })",
702
+ " app.route({",
703
+ " method: 'GET',",
704
+ "@@ -754,12 +756,47 @@",
705
+ " handler() {",
706
+ " return 'Hello'",
707
+ " },",
708
+ " }) // Accessible at /api/v1/hello",
709
+ " ```",
710
+ " ",
711
+ "+### Base Path with Vite (RSC apps)",
712
+ "+",
713
+ "+When using Spiceflow as a full-stack RSC framework with Vite, configure the base path via Vite's `base` option instead of the constructor:",
714
+ "+",
715
+ "+```ts",
716
+ "+// vite.config.ts",
717
+ "+import { defineConfig } from 'vite'",
718
+ "+import { spiceflowPlugin } from 'spiceflow/vite'",
719
+ "+",
720
+ "+export default defineConfig({",
721
+ "+ base: '/my-app',",
722
+ "+ plugins: [spiceflowPlugin({ entry: 'src/main.tsx' })],",
723
+ "+})",
724
+ "+```",
725
+ "+",
726
+ "+The base path must be an absolute path starting with `/`. CDN URLs and relative paths are not supported.",
727
+ "+",
728
+ " ## Async Generators (Streaming)",
729
+ " ",
730
+ " Async generators will create a server sent event response.",
731
+ " ",
732
+ " ```ts",
733
+ " // server.ts",
734
+ ].join("\n");
735
+ const result = balanceDelimiters(patch, "markdown");
736
+ expect(result).toMatchInlineSnapshot(`
737
+ "--- README.md
738
+ +++ README.md
739
+ @@ -741,12 +741,14 @@
740
+ \`\`\` }),
741
+ )
742
+ \`\`\`
743
+
744
+ ## Base Path
745
+
746
+ +For standalone API servers (without Vite), set the base path in the constructor:
747
+ +
748
+ \`\`\`ts
749
+ import { Spiceflow } from 'spiceflow'
750
+
751
+ const app = new Spiceflow({ basePath: '/api/v1' })
752
+ app.route({
753
+ method: 'GET', \`\`\`
754
+ @@ -754,12 +756,47 @@
755
+ \`\`\` handler() {
756
+ return 'Hello'
757
+ },
758
+ }) // Accessible at /api/v1/hello
759
+ \`\`\`
760
+
761
+ +### Base Path with Vite (RSC apps)
762
+ +
763
+ +When using Spiceflow as a full-stack RSC framework with Vite, configure the base path via Vite's \`base\` option instead of the constructor:
764
+ +
765
+ +\`\`\`ts
766
+ +// vite.config.ts
767
+ +import { defineConfig } from 'vite'
768
+ +import { spiceflowPlugin } from 'spiceflow/vite'
769
+ +
770
+ +export default defineConfig({
771
+ + base: '/my-app',
772
+ + plugins: [spiceflowPlugin({ entry: 'src/main.tsx' })],
773
+ +})
774
+ +\`\`\`
775
+ +
776
+ +The base path must be an absolute path starting with \`/\`. CDN URLs and relative paths are not supported.
777
+ +
778
+ ## Async Generators (Streaming)
779
+
780
+ Async generators will create a server sent event response.
781
+
782
+ \`\`\`ts
783
+ // server.ts \`\`\`"
784
+ `);
473
785
  });
474
786
  });
475
787
  describe("scala", () => {
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.tsx"],"names":[],"mappings":";AAOA,OAAO,gCAAgC,CAAC;AAYxC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAwB/B,OAAO,EAgBL,KAAK,UAAU,EAEhB,MAAM,iBAAiB,CAAC;AA45CzB,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,wBAAgB,GAAG,CAAC,EAAE,WAAW,EAAE,EAAE,QAAQ,GAAG,KAAK,CAAC,YAAY,CAkVjE"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.tsx"],"names":[],"mappings":";AAOA,OAAO,gCAAgC,CAAC;AAYxC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAwB/B,OAAO,EAiBL,KAAK,UAAU,EAEhB,MAAM,iBAAiB,CAAC;AA45CzB,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,wBAAgB,GAAG,CAAC,EAAE,WAAW,EAAE,EAAE,QAAQ,GAAG,KAAK,CAAC,YAAY,CAkVjE"}
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import { debounce } from "./utils.js";
26
26
  import { DiffView, DirectoryTreeView } from "./components/index.js";
27
27
  import { logger } from "./logger.js";
28
28
  import { saveStoredLicenseKey } from "./license.js";
29
- import { buildGitCommand, filterParsedFilesByPatterns, getFileName, getFileStatus, getOldFileName, countChanges, getViewMode, processFiles, detectFiletype, stripSubmoduleHeaders, parseGitDiffFiles, getDirtySubmodulePaths, buildSubmoduleDiffCommand, getFilterPatterns, IGNORED_FILES, } from "./diff-utils.js";
29
+ import { buildGitCommand, ensureGitRepo, filterParsedFilesByPatterns, getFileName, getFileStatus, getOldFileName, countChanges, getViewMode, processFiles, detectFiletype, stripSubmoduleHeaders, parseGitDiffFiles, getDirtySubmodulePaths, buildSubmoduleDiffCommand, getFilterPatterns, IGNORED_FILES, } from "./diff-utils.js";
30
30
  import packageJson from "../package.json" assert { type: "json" };
31
31
  // Lazy-load watcher only when --watch is used
32
32
  let watcherModule = null;
@@ -1364,6 +1364,7 @@ cli
1364
1364
  description: "Filter files by glob pattern (can be used multiple times)",
1365
1365
  }))
1366
1366
  .action(async (options) => {
1367
+ ensureGitRepo();
1367
1368
  const { parseHunksWithIds, hunkToStableId, } = await import("./review/index.js");
1368
1369
  // Build git command - unstaged by default, staged with --staged
1369
1370
  const gitCommand = buildGitCommand({
@@ -1427,6 +1428,7 @@ cli
1427
1428
  cli
1428
1429
  .command("hunks add [...ids]", "Stage specific hunks by their stable ID")
1429
1430
  .action(async (ids) => {
1431
+ ensureGitRepo();
1430
1432
  if (!ids || ids.length === 0) {
1431
1433
  console.error("Usage: critique hunks add <hunk-id> [<hunk-id> ...]");
1432
1434
  console.error("Use 'critique hunks list' to see available hunk IDs.");
@@ -1540,6 +1542,10 @@ cli
1540
1542
  .option("--stdin", "Read diff from stdin (for use as a pager)")
1541
1543
  .option("--scrollback", "Output to terminal scrollback instead of TUI (auto-enabled when non-TTY)")
1542
1544
  .action(async (base, head, options) => {
1545
+ // Ensure we're inside a git repository before doing anything
1546
+ if (!options.stdin) {
1547
+ ensureGitRepo();
1548
+ }
1543
1549
  // Apply theme if specified (zustand subscription auto-persists)
1544
1550
  if (options.theme && themeNames.includes(options.theme)) {
1545
1551
  useAppStore.setState({ themeName: options.theme });
@@ -1811,6 +1817,7 @@ cli
1811
1817
  .option("--json", "Output JSON to stdout (implies --web)")
1812
1818
  .option("--resume [id]", "Resume a previous review (shows select if no ID provided)")
1813
1819
  .action(async (base, head, options) => {
1820
+ ensureGitRepo();
1814
1821
  try {
1815
1822
  // Handle resume mode
1816
1823
  if (options.resume !== undefined) {
@@ -1877,6 +1884,7 @@ cli
1877
1884
  cli
1878
1885
  .command("difftool <local> <remote>", "Git difftool integration")
1879
1886
  .action(async (local, remote) => {
1887
+ ensureGitRepo();
1880
1888
  if (!process.stdout.isTTY) {
1881
1889
  execSync(`git diff --no-ext-diff "${local}" "${remote}"`, {
1882
1890
  stdio: "inherit",
@@ -1910,6 +1918,7 @@ cli
1910
1918
  cli
1911
1919
  .command("pick <branch>", "Pick files from another branch to apply to HEAD")
1912
1920
  .action(async (branch) => {
1921
+ ensureGitRepo();
1913
1922
  try {
1914
1923
  const { stdout: currentBranch } = await execAsync("git branch --show-current");
1915
1924
  const current = currentBranch.trim();
@@ -2046,6 +2055,7 @@ cli
2046
2055
  }))
2047
2056
  .option("--title <title>", "HTML document title")
2048
2057
  .action(async (base, head, options) => {
2058
+ ensureGitRepo();
2049
2059
  // Build git command and get diff
2050
2060
  const gitCommand = buildGitCommand({
2051
2061
  staged: options.staged,
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Check if the current directory is inside a git repository.
3
+ * If not, print a friendly error message and exit.
4
+ */
5
+ export declare function ensureGitRepo(): void;
1
6
  /**
2
7
  * Strip submodule status lines from git diff output.
3
8
  * git diff --submodule=diff adds various status lines that the diff parser doesn't understand:
@@ -1 +1 @@
1
- {"version":3,"file":"diff-utils.d.ts","sourceRoot":"","sources":["../src/diff-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"diff-utils.d.ts","sourceRoot":"","sources":["../src/diff-utils.ts"],"names":[],"mappings":"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"}