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 +19 -0
- package/README.md +6 -2
- package/bin/revspec.ts +41 -56
- package/docs/superpowers/plans/2026-03-14-live-ai-integration.md +1877 -0
- package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.md +518 -0
- package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.review.json +65 -0
- package/package.json +1 -1
- package/scripts/release.sh +12 -35
- package/src/cli/reply.ts +53 -0
- package/src/cli/watch.ts +378 -0
- package/src/protocol/live-events.ts +215 -0
- package/src/protocol/live-merge.ts +66 -0
- package/src/protocol/types.ts +2 -1
- package/src/state/review-state.ts +47 -5
- package/src/tui/app.ts +167 -71
- package/src/tui/comment-input.ts +265 -96
- package/src/tui/help.ts +4 -2
- package/src/tui/live-watcher.ts +40 -0
- package/src/tui/pager.ts +18 -8
- package/src/tui/status-bar.ts +17 -6
- package/src/tui/theme.ts +4 -4
- package/test/cli-reply.test.ts +140 -0
- package/test/cli-watch.test.ts +216 -0
- package/test/cli.test.ts +49 -40
- package/test/e2e-live.test.ts +171 -0
- package/test/live-interaction.test.ts +398 -0
- package/test/protocol/live-events.test.ts +509 -0
- package/test/protocol/live-merge.test.ts +167 -0
- package/test/protocol/merge.test.ts +5 -5
- package/test/protocol/read.test.ts +2 -2
- package/test/protocol/types.test.ts +2 -2
- package/test/protocol/write.test.ts +2 -2
- package/test/state/review-state.test.ts +88 -15
- package/test/tui/pager.test.ts +21 -46
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
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
3
|
import { resolve, basename, extname, dirname, join } from "path";
|
|
4
|
-
import { readReviewFile
|
|
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
|
-
|
|
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/
|
|
35
|
-
// e.g. spec.md -> spec.review.json, spec.review.
|
|
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.
|
|
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
|
-
//
|
|
73
|
-
if (existsSync(
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
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
|
-
//
|
|
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(
|