plan-review 0.1.1 → 1.0.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.
@@ -0,0 +1,133 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, existsSync, } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ // ── Internal helpers (not exported) ────────────────────────────────
6
+ function pathHash(planPath) {
7
+ const abs = resolve(planPath);
8
+ const hash = createHash('sha256').update(abs).digest('hex');
9
+ return hash.slice(0, 16);
10
+ }
11
+ function sessionFilePath(planPath) {
12
+ return join(getSessionDir(), pathHash(planPath) + '.json');
13
+ }
14
+ // ── Exported functions ─────────────────────────────────────────────
15
+ export function getSessionDir() {
16
+ const dir = join(homedir(), '.plan-review', 'sessions');
17
+ mkdirSync(dir, { recursive: true });
18
+ return dir;
19
+ }
20
+ export function computeContentHash(content) {
21
+ const hex = createHash('sha256').update(content).digest('hex');
22
+ return `sha256:${hex}`;
23
+ }
24
+ export function saveSession(planPath, contentHash, comments, activeSection) {
25
+ try {
26
+ const data = {
27
+ version: 1,
28
+ planPath,
29
+ contentHash,
30
+ comments,
31
+ activeSection,
32
+ lastModified: new Date().toISOString(),
33
+ };
34
+ const filePath = sessionFilePath(planPath);
35
+ writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
36
+ }
37
+ catch (err) {
38
+ console.warn(`[plan-review] Failed to save session: ${err.message}`);
39
+ }
40
+ }
41
+ export function loadSession(planPath, currentContentHash) {
42
+ const filePath = sessionFilePath(planPath);
43
+ if (!existsSync(filePath)) {
44
+ return null;
45
+ }
46
+ let raw;
47
+ try {
48
+ raw = readFileSync(filePath, 'utf-8');
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ let data;
54
+ try {
55
+ data = JSON.parse(raw);
56
+ }
57
+ catch {
58
+ console.warn(`[plan-review] Corrupt session file, removing: ${filePath}`);
59
+ try {
60
+ unlinkSync(filePath);
61
+ }
62
+ catch {
63
+ // ignore
64
+ }
65
+ return null;
66
+ }
67
+ // Restore Date objects in comment timestamps
68
+ const comments = data.comments.map((c) => ({
69
+ ...c,
70
+ timestamp: new Date(c.timestamp),
71
+ }));
72
+ return {
73
+ comments,
74
+ activeSection: data.activeSection,
75
+ stale: data.contentHash !== currentContentHash,
76
+ };
77
+ }
78
+ export function clearSession(planPath) {
79
+ const filePath = sessionFilePath(planPath);
80
+ try {
81
+ unlinkSync(filePath);
82
+ }
83
+ catch {
84
+ // No error if missing
85
+ }
86
+ }
87
+ export function listSessions() {
88
+ const dir = getSessionDir();
89
+ let files;
90
+ try {
91
+ files = readdirSync(dir);
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ const results = [];
97
+ for (const file of files) {
98
+ if (!file.endsWith('.json'))
99
+ continue;
100
+ const filePath = join(dir, file);
101
+ let data;
102
+ try {
103
+ const raw = readFileSync(filePath, 'utf-8');
104
+ data = JSON.parse(raw);
105
+ }
106
+ catch {
107
+ console.warn(`[plan-review] Skipping corrupt session file: ${filePath}`);
108
+ continue;
109
+ }
110
+ let stale;
111
+ if (!existsSync(data.planPath)) {
112
+ stale = null;
113
+ }
114
+ else {
115
+ try {
116
+ const currentContent = readFileSync(data.planPath, 'utf-8');
117
+ const currentHash = computeContentHash(currentContent);
118
+ stale = currentHash !== data.contentHash;
119
+ }
120
+ catch {
121
+ stale = null;
122
+ }
123
+ }
124
+ results.push({
125
+ planPath: data.planPath,
126
+ commentCount: data.comments.length,
127
+ lastModified: data.lastModified,
128
+ stale,
129
+ });
130
+ }
131
+ return results;
132
+ }
133
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { PlanDocument, ReviewComment } from './types.js';
2
+ export interface Transport {
3
+ sendDocument(doc: PlanDocument): void;
4
+ onReviewSubmit(handler: (comments: ReviewComment[]) => void): void;
5
+ start(port: number): Promise<{
6
+ url: string;
7
+ }>;
8
+ stop(): Promise<void>;
9
+ }
10
+ export declare class HttpTransport implements Transport {
11
+ private doc;
12
+ private submitHandler;
13
+ private sessionSaveHandler;
14
+ private server;
15
+ sendDocument(doc: PlanDocument): void;
16
+ onReviewSubmit(handler: (comments: ReviewComment[]) => void): void;
17
+ onSessionSave(handler: (comments: ReviewComment[], activeSection: string | null) => void): void;
18
+ start(port: number): Promise<{
19
+ url: string;
20
+ }>;
21
+ stop(): Promise<void>;
22
+ }
@@ -0,0 +1,35 @@
1
+ import { createReviewServer, startServer, stopServer } from './server/server.js';
2
+ import { getAssetHtml } from './server/assets.js';
3
+ export class HttpTransport {
4
+ doc = null;
5
+ submitHandler = null;
6
+ sessionSaveHandler = null;
7
+ server = null;
8
+ sendDocument(doc) {
9
+ this.doc = doc;
10
+ }
11
+ onReviewSubmit(handler) {
12
+ this.submitHandler = handler;
13
+ }
14
+ onSessionSave(handler) {
15
+ this.sessionSaveHandler = handler;
16
+ }
17
+ async start(port) {
18
+ if (!this.doc)
19
+ throw new Error('No document set');
20
+ this.server = createReviewServer({
21
+ getDocument: () => this.doc,
22
+ onSubmit: (comments) => this.submitHandler?.(comments),
23
+ getAssetHtml: () => getAssetHtml(),
24
+ onSessionSave: (comments, activeSection) => this.sessionSaveHandler?.(comments, activeSection),
25
+ });
26
+ return startServer(this.server, port);
27
+ }
28
+ async stop() {
29
+ if (this.server && this.server.listening) {
30
+ await stopServer(this.server);
31
+ this.server = null;
32
+ }
33
+ }
34
+ }
35
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AASlD,MAAM,OAAO,aAAa;IAChB,GAAG,GAAwB,IAAI,CAAC;IAChC,aAAa,GAAiD,IAAI,CAAC;IACnE,kBAAkB,GAA+E,IAAI,CAAC;IACtG,MAAM,GAAkB,IAAI,CAAC;IAErC,YAAY,CAAC,GAAiB;QAC5B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,cAAc,CAAC,OAA4C;QACzD,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;IAC/B,CAAC;IAED,aAAa,CAAC,OAA0E;QACtF,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,IAAI,CAAC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAElD,IAAI,CAAC,MAAM,GAAG,kBAAkB,CAAC;YAC/B,WAAW,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAI;YAC5B,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,QAAQ,CAAC;YACtD,YAAY,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;YAClC,aAAa,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC;SAC/F,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC9B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC;CACF"}
package/dist/types.d.ts CHANGED
@@ -18,10 +18,17 @@ export interface Section {
18
18
  relatedFiles?: string[];
19
19
  verification?: string;
20
20
  }
21
+ export interface LineAnchor {
22
+ type: 'lines';
23
+ startLine: number;
24
+ endLine: number;
25
+ lineTexts: string[];
26
+ }
21
27
  export interface ReviewComment {
22
28
  sectionId: string;
23
29
  text: string;
24
30
  timestamp: Date;
31
+ anchor?: LineAnchor;
25
32
  }
26
33
  export type OutputTarget = 'stdout' | 'clipboard' | 'file' | 'claude';
27
34
  export type SplitStrategy = 'heading' | 'separator' | 'auto';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plan-review",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "description": "Interactive CLI for reviewing AI-generated markdown plans",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,10 +8,13 @@
8
8
  "plan-review": "dist/index.js"
9
9
  },
10
10
  "files": [
11
- "dist"
11
+ "dist",
12
+ "skills"
12
13
  ],
13
14
  "scripts": {
14
- "build": "tsc",
15
+ "build": "tsc && tsc --project tsconfig.browser.json --noEmit && node scripts/build-browser.js",
16
+ "build:browser": "node scripts/build-browser.js",
17
+ "dev:browser": "node scripts/build-browser.js --watch",
15
18
  "dev": "tsx src/index.ts",
16
19
  "test": "vitest run",
17
20
  "test:watch": "vitest",
@@ -32,8 +35,12 @@
32
35
  "marked-terminal": "^7.3.0"
33
36
  },
34
37
  "devDependencies": {
38
+ "@testing-library/preact": "^3.2.4",
39
+ "preact": "^10.29.1",
35
40
  "@types/node": "^25.6.0",
36
41
  "@vitest/coverage-v8": "^4.1.4",
42
+ "esbuild": "^0.28.0",
43
+ "jsdom": "^29.0.2",
37
44
  "tsx": "^4.21.0",
38
45
  "typescript": "^6.0.2",
39
46
  "vitest": "^4.1.4"
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: plan-review
3
+ description: Use when the user asks to review a plan, start plan review, or says "I want to review this plan". Triggers on plan review requests for markdown implementation plans, specs, or design docs. Builds and runs the plan-review browser UI, feeds review output back into the conversation.
4
+ ---
5
+
6
+ # Plan Review
7
+
8
+ Launch the plan-review browser UI for interactive review of markdown plans, then feed the structured review output back into this session.
9
+
10
+ ## Prerequisites
11
+
12
+ The `plan-review` CLI must be installed or available at `~/desenv/personal/plan-review/`. If not built, run `npm run build` first.
13
+
14
+ ## Process
15
+
16
+ 1. **Identify the plan file.** If the user specified a file, use it. If not, look for the most recent file matching `docs/superpowers/plans/*.md` in the current working directory. If multiple candidates exist, ask which one.
17
+
18
+ 2. **Build if needed.** Check if `dist/index.js` exists in the plan-review project. If not:
19
+ ```bash
20
+ cd ~/desenv/personal/plan-review && npm run build
21
+ ```
22
+
23
+ 3. **Run the review.** Launch in browser mode with stdout output:
24
+ ```bash
25
+ node ~/desenv/personal/plan-review/dist/index.js <plan-file> --browser -o stdout
26
+ ```
27
+ This opens the browser review UI. The command blocks until the user submits their review, then prints structured review output to stdout.
28
+
29
+ 4. **Read the output.** The review output is structured markdown with the user's comments anchored to specific sections. Read it and present a summary to the user.
30
+
31
+ 5. **Act on feedback.** Ask the user what they want to do with the review:
32
+ - Address the comments (modify the plan or code)
33
+ - Save the review to a file
34
+ - Continue discussion about specific comments
35
+
36
+ ## Important
37
+
38
+ - The `--browser` flag opens a three-panel review UI (TOC + content + comments).
39
+ - The `-o stdout` flag ensures the review output comes back to this session.
40
+ - The command will block until the user clicks "Submit Review" in the browser.
41
+ - If the user has an existing session for this plan, they'll be prompted to resume.
42
+ - Use `--fresh` flag if the user explicitly wants to start a clean review.