revspec 0.1.0 → 0.2.1

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/CLAUDE.md ADDED
@@ -0,0 +1,19 @@
1
+ # Revspec
2
+
3
+ - Tech: Bun + TypeScript + @opentui/core
4
+ - npm: `revspec` | GitHub: icyrainz/revspec
5
+ - Run: `bun run bin/revspec.ts <file.md>`
6
+ - Test: `bun test`
7
+ - Release: `./scripts/release.sh [patch|minor|major]`
8
+
9
+ ## OpenTUI Gotchas
10
+ - Don't use StyledText for large content (BigInt FFI crash)
11
+ - Don't use ANSI escape codes in TextRenderable content (renders as literal text)
12
+ - MarkdownRenderable needs `syntaxStyle` + `conceal: true` for proper rendering
13
+ - Use `visible: false` to hide renderables, not removal/re-addition
14
+
15
+ ## Conventions
16
+ - Line mode is default, markdown mode via `m` toggle
17
+ - Tab to submit in all text inputs (works through tmux)
18
+ - Destructive actions need confirmation (dd double-tap, approve confirm dialog)
19
+ - All review actions auto-switch to line mode
package/README.md CHANGED
@@ -10,13 +10,17 @@ When an AI generates a spec, the human review step breaks the agentic loop. You
10
10
 
11
11
  Requires [Bun](https://bun.sh) (install: `curl -fsSL https://bun.sh/install | bash`).
12
12
 
13
+ ```bash
14
+ bun install -g revspec
15
+ ```
16
+
17
+ Or from source:
18
+
13
19
  ```bash
14
20
  git clone https://github.com/icyrainz/revspec.git
15
21
  cd revspec && bun install && bun link
16
22
  ```
17
23
 
18
- This adds `revspec` to your PATH.
19
-
20
24
  ## Usage
21
25
 
22
26
  ```bash
package/bin/revspec.ts CHANGED
@@ -1,12 +1,36 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync, unlinkSync, readFileSync } from "fs";
2
+ import { existsSync } from "fs";
3
3
  import { resolve, basename, extname, dirname, join } from "path";
4
- import { readReviewFile, readDraftFile } from "../src/protocol/read";
5
- import { writeReviewFile } from "../src/protocol/write";
6
- import { mergeDraftIntoReview } from "../src/protocol/merge";
4
+ import { readReviewFile } from "../src/protocol/read";
7
5
  import { runTui } from "../src/tui/app";
6
+ import { readEventsFromOffset } from "../src/protocol/live-events";
8
7
 
9
8
  const args = process.argv.slice(2);
9
+ const subcommand = args[0];
10
+
11
+ if (subcommand === "watch") {
12
+ const specFile = args[1];
13
+ if (!specFile) {
14
+ console.error("Usage: revspec watch <file.md>");
15
+ process.exit(1);
16
+ }
17
+ const { runWatch } = await import("../src/cli/watch");
18
+ await runWatch(specFile);
19
+ process.exit(0);
20
+ }
21
+
22
+ if (subcommand === "reply") {
23
+ const specFile = args[1];
24
+ const threadId = args[2];
25
+ const text = args[3];
26
+ if (!specFile || !threadId || !text) {
27
+ console.error('Usage: revspec reply <file.md> <threadId> "<text>"');
28
+ process.exit(1);
29
+ }
30
+ const { runReply } = await import("../src/cli/reply");
31
+ runReply(specFile, threadId, text);
32
+ process.exit(0);
33
+ }
10
34
 
11
35
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
12
36
  console.log("Usage: revspec <file.md> [--tui|--nvim|--web]");
@@ -14,7 +38,8 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
14
38
  }
15
39
 
16
40
  if (args.includes("--version") || args.includes("-v")) {
17
- console.log("revspec 0.1.0");
41
+ const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
42
+ console.log(`revspec ${pkg.version}`);
18
43
  process.exit(0);
19
44
  }
20
45
 
@@ -31,70 +56,30 @@ if (!existsSync(specPath)) {
31
56
  process.exit(1);
32
57
  }
33
58
 
34
- // 2. Derive review/draft paths from spec filename
35
- // e.g. spec.md -> spec.review.json, spec.review.draft.json
59
+ // 2. Derive review/jsonl paths from spec filename
60
+ // e.g. spec.md -> spec.review.json, spec.review.live.jsonl
36
61
  const specDir = dirname(specPath);
37
62
  const specBase = basename(specPath, extname(specPath)); // e.g. "spec"
38
63
  const reviewPath = join(specDir, `${specBase}.review.json`);
64
+ const jsonlPath = join(specDir, `${specBase}.review.live.jsonl`);
39
65
  const draftPath = join(specDir, `${specBase}.review.draft.json`);
40
66
 
41
- // 3. Check for corrupted draft — if exists but invalid JSON, warn and delete
42
- if (existsSync(draftPath)) {
43
- let raw: string;
44
- try {
45
- raw = readFileSync(draftPath, "utf8");
46
- } catch {
47
- raw = "";
48
- }
49
- let parsedDraft: unknown = null;
50
- let parseError = false;
51
- try {
52
- parsedDraft = JSON.parse(raw);
53
- if (typeof parsedDraft !== "object" || parsedDraft === null) {
54
- parseError = true;
55
- }
56
- } catch {
57
- parseError = true;
58
- }
59
- if (parseError) {
60
- process.stderr.write(
61
- `Warning: Draft file is corrupted (invalid JSON), deleting: ${draftPath}\n`
62
- );
63
- unlinkSync(draftPath);
64
- }
65
- }
66
-
67
- // 4. Launch TUI (skip if REVSPEC_SKIP_TUI=1)
67
+ // 3. Launch TUI (skip if REVSPEC_SKIP_TUI=1)
68
68
  if (process.env.REVSPEC_SKIP_TUI !== "1") {
69
69
  await runTui(specPath, reviewPath, draftPath);
70
70
  }
71
71
 
72
- // 5. After TUI exits, read draft
73
- if (existsSync(draftPath)) {
74
- const draft = readDraftFile(draftPath);
75
-
76
- if (draft && draft.approved === true) {
77
- // Approved — delete draft, print APPROVED, exit 0
78
- unlinkSync(draftPath);
79
- process.stdout.write(`APPROVED: ${reviewPath}\n`);
80
- process.exit(0);
81
- }
82
-
83
- if (draft && draft.threads && draft.threads.length > 0) {
84
- // Has threads — merge into review file, delete draft
85
- const existingReview = readReviewFile(reviewPath);
86
- const merged = mergeDraftIntoReview(existingReview, draft, specPath);
87
- writeReviewFile(reviewPath, merged);
88
- unlinkSync(draftPath);
89
-
90
- // Output review path — draft had threads, so new comments were added
91
- process.stdout.write(`${reviewPath}\n`);
72
+ // 4. After TUI exits, check if approved via JSONL
73
+ if (existsSync(jsonlPath)) {
74
+ const { events } = readEventsFromOffset(jsonlPath, 0);
75
+ const wasApproved = events.some(e => e.type === "approve");
76
+ if (wasApproved && existsSync(reviewPath)) {
77
+ console.log(`APPROVED: ${reviewPath}`);
92
78
  process.exit(0);
93
79
  }
94
80
  }
95
81
 
96
- // 6. No draft (or draft had no threads and wasn't approved)
97
- // Check if existing review file has open/pending threads
82
+ // 5. Check if review file exists with open/pending threads
98
83
  const existingReview = readReviewFile(reviewPath);
99
84
  if (existingReview) {
100
85
  const hasOpenOrPending = existingReview.threads.some(