super-review 0.0.0 → 0.0.2
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/README.md +106 -0
- package/dist/bin.mjs +17 -7
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
[](https://npmjs.com/package/super-review)
|
|
2
|
+
[](https://npmjs.com/package/super-review)
|
|
3
|
+
|
|
4
|
+
# super-review
|
|
5
|
+
|
|
6
|
+
Document an agent's changes as a guided review session in the [Super Review](https://github.com/ieedan/super-review) desktop app.
|
|
7
|
+
|
|
8
|
+
## Get Started
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npx super-review session save \
|
|
12
|
+
--key "<conversation id>" \
|
|
13
|
+
--name "What I changed" \
|
|
14
|
+
--description "A sentence or two of overview." \
|
|
15
|
+
--harness claude-code
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That captures a frozen snapshot of your working-tree changes as a reviewable session. Add a `--tour` to turn it into a walkthrough (see [Authoring a tour](#authoring-a-tour)).
|
|
19
|
+
|
|
20
|
+
Sessions are written into the repo under `.super-review/sessions/`, so they can be committed and travel with a branch or PR - a reviewer pulls the branch and walks the tour in the desktop app without having authored it.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
`super-review` is the companion CLI for the Super Review desktop app, meant to be driven by a coding agent (Claude Code, Cursor, Codex, etc.) after it finishes a task. The agent:
|
|
25
|
+
|
|
26
|
+
1. Makes its edits (they don't need to be committed - the diff is read from the current working tree).
|
|
27
|
+
2. Runs `session save` with a `--tour` describing what changed and why.
|
|
28
|
+
3. Re-runs the same command with the same `--key` to refresh the session as it iterates.
|
|
29
|
+
|
|
30
|
+
The result is an isolated, frozen review session: the diff as it was at save time, grouped into explained steps.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### `session save`
|
|
35
|
+
|
|
36
|
+
Capture the working tree's current changes as a reviewable session.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
super-review session save [options]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Option | Description |
|
|
43
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `--key <id>` | Stable upsert key (your harness conversation/run id). Re-running with the same key **updates** that session. |
|
|
45
|
+
| `--id <id>` | Target an existing session by its id (alternative to `--key`). |
|
|
46
|
+
| `--name <text>` | Session name. Required on first save unless a `--tour` supplies it. |
|
|
47
|
+
| `--description <text>` | What you changed. Required on first save unless a `--tour` supplies it. |
|
|
48
|
+
| `--harness <kind>` | One of `claude-code`, `cursor`, `codex`, `opencode`, `copilot`, `other` (default: `other`). Drives the card logo. |
|
|
49
|
+
| `--harness-label <t>` | Freeform harness name, used when `--harness other`. |
|
|
50
|
+
| `--harness-url <url>` | Deep link back to this run (resume URL or permalink). |
|
|
51
|
+
| `--tour <json>` | Inline JSON describing a guided tour (see below). Flags override the document's top-level `name`/`description`/`harness`. |
|
|
52
|
+
| `--cwd <path>` | Repo path (default: current directory). |
|
|
53
|
+
|
|
54
|
+
### `session clear`
|
|
55
|
+
|
|
56
|
+
Remove all sessions from the repo (`.super-review/sessions/`) - e.g. before merging a branch.
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
super-review session clear [--cwd <path>]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Authoring a tour
|
|
63
|
+
|
|
64
|
+
A tour groups the change into ordered **steps**, each with a title, a Markdown body, and the files it covers. Pass it as inline JSON to `--tour` - wrap it in single quotes so the shell keeps it as one argument:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
super-review session save --key "<conversation id>" --tour '{
|
|
68
|
+
"name": "Short title for the whole change",
|
|
69
|
+
"description": "One or two sentences of overview.",
|
|
70
|
+
"harness": "claude-code",
|
|
71
|
+
"harnessUrl": "<optional resume/permalink to this run>",
|
|
72
|
+
"steps": [
|
|
73
|
+
{
|
|
74
|
+
"title": "Data model",
|
|
75
|
+
"body": "What this group establishes and why. Markdown is supported - **bold**, lists, and `code` all render.",
|
|
76
|
+
"files": ["src/shared/types.ts", "src/main/store.ts"],
|
|
77
|
+
"callouts": [
|
|
78
|
+
{
|
|
79
|
+
"file": "src/shared/types.ts",
|
|
80
|
+
"startLine": 42,
|
|
81
|
+
"endLine": 48,
|
|
82
|
+
"side": "new",
|
|
83
|
+
"body": "The key bit: this field is what everything downstream keys off."
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"title": "UI",
|
|
89
|
+
"body": "How the new state is surfaced to the user.",
|
|
90
|
+
"files": ["src/components/Thing.svelte"]
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **Top-level `name` / `description` / `harness` / `harnessUrl`** are optional here - flags override them.
|
|
97
|
+
- **`steps[].files`** lists the files in reading order. Any changed file you omit still shows, grouped under **"Other changes"** at the end - nothing is hidden.
|
|
98
|
+
- **`steps[].callouts`** pin commentary to a line range inside one of the step's files, rendered as a note right at that spot in the diff:
|
|
99
|
+
- `file` - one of the step's files.
|
|
100
|
+
- `startLine`, `endLine` - 1-based inclusive range (omit `endLine` for a single line).
|
|
101
|
+
- `side` - `"new"` (added/current lines, the default) or `"old"` (the original).
|
|
102
|
+
- `body` - Markdown commentary.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
[MIT](https://github.com/ieedan/super-review/blob/main/LICENSE)
|
package/dist/bin.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as e,program as t}from"commander";import n from"node:path";import{simpleGit as r}from"simple-git";import{createHash as i,randomUUID as a}from"node:crypto";import{promises as o}from"node:fs";import{execFile as s}from"node:child_process";import{promisify as c}from"node:util";var l=`0.0.
|
|
3
|
-
`).filter(Boolean)){let e
|
|
4
|
-
`).filter(Boolean)){let
|
|
2
|
+
import{Command as e,program as t}from"commander";import n from"node:path";import{simpleGit as r}from"simple-git";import{createHash as i,randomUUID as a}from"node:crypto";import{promises as o}from"node:fs";import{execFile as s}from"node:child_process";import{promisify as c}from"node:util";var l=`0.0.2`,u=`Document an agent session for review with the super-review desktop app`;const d={png:`image/png`,jpg:`image/jpeg`,jpeg:`image/jpeg`,gif:`image/gif`,webp:`image/webp`,avif:`image/avif`,bmp:`image/bmp`,ico:`image/x-icon`,svg:`image/svg+xml`};function f(e){let t=e.lastIndexOf(`.`);return t===-1?``:e.slice(t+1).toLowerCase()}function p(e){return d[f(e)]??null}const m=c(s),h=2*1024*1024,g=10*1024*1024;function _(e){return i(`sha1`).update(n.resolve(e)).digest(`hex`).slice(0,12)}async function v(e){let t=r(e);try{return(await t.status()).current??null}catch{return null}}function y(e,t){return(e+t).trim().includes(`?`)?`untracked`:e===`R`||t===`R`?`renamed`:e===`C`||t===`C`?`copied`:e===`A`||t===`A`?`added`:e===`D`||t===`D`?`deleted`:e===`T`||t===`T`?`type-change`:`modified`}async function b(e,t){switch(t.kind){case`workingTree`:return{workingTree:!0};case`branch`:return{base:await S(e,t.base),head:t.head,workingTree:!1};case`pr`:return{base:`pr/${t.prNumber}/base`,head:`pr/${t.prNumber}/head`,workingTree:!1};case`session`:throw Error(`session context is not backed by git`);case`stash`:throw Error(`stash context is handled by dedicated branches`)}}async function x(e,t){try{return(await e.raw([`rev-parse`,`--verify`,`--quiet`,`${t}^{commit}`])).trim().length>0}catch{return!1}}async function S(e,t){if(t.startsWith(`origin/`))return t;let n=`origin/${t}`;return await x(e,n)?n:t}async function C(e,t){let n=r(e);if(t.kind===`stash`){let e=t.ref,r=[];if(!await x(n,e))return r;let i=await n.raw([`diff`,`--name-status`,`--find-renames`,`${e}^1`,e]).catch(()=>``),a=E(await n.raw([`diff`,`--numstat`,`${e}^1`,e]).catch(()=>``)),o=w(await n.raw([`diff`,`--raw`,`--find-renames`,`${e}^1`,e]).catch(()=>``));for(let e of i.split(`
|
|
3
|
+
`).filter(Boolean)){let t=e.split(` `),n=t[0],i=`modified`,s,c=t[1];n.startsWith(`A`)?i=`added`:n.startsWith(`D`)?i=`deleted`:n.startsWith(`M`)?i=`modified`:n.startsWith(`R`)?(i=`renamed`,s=t[1],c=t[2]):n.startsWith(`C`)?(i=`copied`,s=t[1],c=t[2]):n.startsWith(`T`)&&(i=`type-change`);let l=a.get(c)??{additions:0,deletions:0,binary:!1};r.push({path:c,oldPath:s,status:i,additions:l.additions,deletions:l.deletions,isBinary:l.binary,contentSig:o.get(c)})}if(await x(n,`${e}^3`)){let t=E(await n.raw([`diff`,`--numstat`,z,`${e}^3`]).catch(()=>``)),i=w(await n.raw([`diff`,`--raw`,z,`${e}^3`]).catch(()=>``));for(let[e,n]of t)r.push({path:e,status:`added`,additions:n.additions,deletions:n.deletions,isBinary:n.binary,contentSig:i.get(e)})}return r}let i=await b(n,t),a=[];if(i.workingTree){let[t,r,i]=await Promise.all([n.status(),n.raw([`diff`,`--numstat`,`HEAD`]).catch(()=>``),n.raw([`diff`,`--find-renames`,`HEAD`]).catch(()=>``)]),o=E(r),s=T(i),c=[];for(let e of t.files){let t=y(e.index,e.working_dir),n,r=e.path,i=e.path.match(/^(.+) -> (.+)$/);i&&(n=i[1],r=i[2]);let l=o.get(r)??{additions:0,deletions:0,binary:!1};a.push({path:r,oldPath:n,status:t,additions:l.additions,deletions:l.deletions,isBinary:l.binary,contentSig:s.get(r)}),!l.binary&&l.additions===0&&l.deletions===0&&(t===`untracked`||t===`added`)&&c.push({index:a.length-1,filePath:r})}return c.length>0&&await Promise.all(c.map(async({index:t,filePath:n})=>{let r=await O(e,n);r&&(a[t]={...a[t],additions:r.additions,isBinary:r.binary,contentSig:r.sig??a[t].contentSig})})),a}if(i.base&&i.head){let[e,t]=await Promise.all([x(n,i.base),x(n,i.head)]);if(!e||!t)return a;let[r,o,s]=await Promise.all([n.raw([`diff`,`--name-status`,`--find-renames`,`${i.base}...${i.head}`]),n.raw([`diff`,`--numstat`,`${i.base}...${i.head}`]),n.raw([`diff`,`--raw`,`--find-renames`,`${i.base}...${i.head}`]).catch(()=>``)]),c=E(o),l=w(s);for(let e of r.split(`
|
|
4
|
+
`).filter(Boolean)){let t=e.split(` `),n=t[0],r=`modified`,i,o=t[1];n.startsWith(`A`)?r=`added`:n.startsWith(`D`)?r=`deleted`:n.startsWith(`M`)?r=`modified`:n.startsWith(`R`)?(r=`renamed`,i=t[1],o=t[2]):n.startsWith(`C`)?(r=`copied`,i=t[1],o=t[2]):n.startsWith(`T`)&&(r=`type-change`);let s=c.get(o)??{additions:0,deletions:0,binary:!1};a.push({path:o,oldPath:i,status:r,additions:s.additions,deletions:s.deletions,isBinary:s.binary,contentSig:l.get(o)})}}return a}function w(e){let t=new Map;for(let n of e.split(`
|
|
5
|
+
`).filter(Boolean)){let e=n.indexOf(` `);if(e===-1)continue;let r=n.slice(0,e).split(` `)[3];if(!r||/^0+$/.test(r))continue;let i=n.slice(e+1).split(` `),a=i[i.length-1];t.set(a,r)}return t}function T(e){let t=new Map,n;for(let r of e.split(`
|
|
6
|
+
`))if(r.startsWith(`diff --git `)){let e=r.indexOf(` b/`);n=e===-1?void 0:r.slice(e+3)}else if(n&&r.startsWith(`index `)){let e=r.match(/^index [0-9a-f]+\.\.([0-9a-f]+)/);e&&!/^0+$/.test(e[1])&&t.set(n,e[1])}return t}function E(e){let t=new Map;for(let n of e.split(`
|
|
7
|
+
`).filter(Boolean)){let[e,r,...i]=n.split(` `),a=i.join(` `),o=e===`-`&&r===`-`;t.set(a,{additions:o?0:Number(e)||0,deletions:o?0:Number(r)||0,binary:o})}return t}async function D(e,t,n,r){try{let i=[`diff`,`--numstat`];return t&&n&&i.push(`${t}...${n}`),i.push(`--`,r),E(await e.raw(i)).get(r)??{additions:0,deletions:0,binary:!1}}catch{return{additions:0,deletions:0,binary:!1}}}async function O(e,t){try{let r=n.join(e,t),a=await o.stat(r);if(!a.isFile())return null;if(a.size===0)return{additions:0,binary:!1,sig:`empty`};if(a.size>h)return{additions:0,binary:!1,sig:`size:${a.size}`};let s=await o.readFile(r),c=i(`sha1`).update(s).digest(`hex`),l=Math.min(s.length,8192);for(let e=0;e<l;e++)if(s[e]===0)return{additions:0,binary:!0,sig:c};let u=0;for(let e=0;e<s.length;e++)s[e]===10&&u++;return s[s.length-1]!==10&&u++,{additions:u,binary:!1,sig:c}}catch{return null}}async function k(e,t,n){try{return await e.show([`${t}:${n}`])}catch{return``}}async function A(e,t){try{let r=await o.readFile(n.join(e,t));return r.byteLength>h?``:r.toString(`utf8`)}catch{return``}}async function j(e,t,n){try{let{stdout:r}=await m(`git`,[`show`,`${t}:${n}`],{cwd:e,encoding:`buffer`,maxBuffer:g});return r}catch{return null}}async function M(e,t){try{let r=await o.readFile(n.join(e,t));return r.byteLength>g?null:r}catch{return null}}function N(e,t){return`data:${t};base64,${e.toString(`base64`)}`}async function P(e,t,n,r,i){let a=r?await j(e,r,t):null,o=i?await j(e,i,t):await M(e,t);return{oldImage:a?N(a,n):void 0,newImage:o?N(o,n):void 0}}function F(e,t){let n=t.endsWith(`
|
|
5
8
|
`),r=n?t.slice(0,-1):t,i=r.length===0?[]:r.split(`
|
|
6
9
|
`);if(i.length===0)return``;let a=[`diff --git a/${e} b/${e}`,`new file mode 100644`,`--- /dev/null`,`+++ b/${e}`,`@@ -0,0 +1,${i.length} @@`,...i.map(e=>`+${e}`)];return n||a.push(`\`),a.join(`
|
|
7
10
|
`)+`
|
|
8
|
-
`}async function
|
|
9
|
-
`))}await
|
|
11
|
+
`}async function I(e,t,n){let i=r(e),a=await x(i,`${n}^3`)&&await i.raw([`cat-file`,`-e`,`${n}^3:${t}`]).then(()=>!0).catch(()=>!1),o=a?void 0:`${n}^1`,s=a?`${n}^3`:n,c=``,l=o?await k(i,o,t):``,u=await k(i,s,t),d=E(a?await i.raw([`diff`,`--numstat`,z,`${n}^3`,`--`,t]).catch(()=>``):await i.raw([`diff`,`--numstat`,`${n}^1`,n,`--`,t]).catch(()=>``)).get(t)??{additions:0,deletions:0,binary:!1},f=d.binary;a||(c=await i.raw([`diff`,`${n}^1`,n,`--`,t]).catch(()=>``));let m=p(t),g,_;m&&({oldImage:g,newImage:_}=await P(e,t,m,o,s));let v=l.length>0||g!==void 0,y=u.length>0||_!==void 0,b;b=y&&!v?`added`:v&&!y?`deleted`:`modified`;let S=l.length>h||u.length>h,C=f&&m!==null;return a&&!c&&b===`added`&&!f&&m===null&&!S&&u&&(c=F(t,u)),(S||C)&&(l=``,u=``),{file:{path:t,oldPath:void 0,status:b,additions:d.additions,deletions:d.deletions,isBinary:f},patch:c,oldContents:l,newContents:u,truncated:S,oldImage:g,newImage:_}}async function L(e,t,n){let i=r(e);if(n.kind===`stash`)return I(e,t,n.ref);let a=await b(i,n),o=``,s=``,c=``,l=!1,u=0,d=0,f,m=a.base;if(a.base&&a.head){let e=(await i.raw([`merge-base`,a.base,a.head]).catch(()=>``)).trim();e&&(m=e)}if(a.workingTree){o=await i.diff([`HEAD`,`--`,t]).catch(()=>``),o||=await i.diff([`--`,t]).catch(()=>``),s=await k(i,`HEAD`,t),c=await A(e,t);let n=await D(i,void 0,void 0,t);u=n.additions,d=n.deletions,l=n.binary}else if(a.base&&a.head){let[e,n,r,f]=await Promise.all([i.raw([`diff`,`${a.base}...${a.head}`,`--`,t]).catch(()=>``),k(i,m,t),k(i,a.head,t),D(i,a.base,a.head,t)]);o=e,s=n,c=r,u=f.additions,d=f.deletions,l=f.binary}let g=p(t),_,v;if(g){let n=a.workingTree?`HEAD`:m,r=a.workingTree?void 0:a.head;({oldImage:_,newImage:v}=await P(e,t,g,n,r))}let y=s.length>0||_!==void 0,x=c.length>0||v!==void 0;f=x&&!y?`added`:y&&!x?`deleted`:`modified`;let S=s.length>h||c.length>h,C=l&&g!==null;return a.workingTree&&!o&&f===`added`&&!l&&g===null&&!S&&c&&(o=F(t,c)),{file:{path:t,oldPath:void 0,status:f,additions:u,deletions:d,isBinary:l},patch:o,oldContents:S||C?``:s,newContents:S||C?``:c,truncated:S,oldImage:_,newImage:v}}async function R(e){try{let t=(await e.raw([`symbolic-ref`,`--short`,`refs/remotes/origin/HEAD`])).trim();if(t)return t.replace(/^origin\//,``)}catch{}for(let t of[`origin/main`,`origin/master`,`origin/trunk`,`main`,`master`,`trunk`])try{return await e.raw([`rev-parse`,`--verify`,`--quiet`,`${t}^{commit}`]),t.replace(/^origin\//,``)}catch{}}async function ee(e){return R(r(e))}const z=`4b825dc642cb6eb9a060e54bf8d69288fbee4904`;function B(e,t){let n=[];for(let r of e){let e=r.files.filter(e=>t.has(e));if(e.length===0)continue;let i=`step-${n.length+1}`,a=new Set(e),o=[];for(let e of r.callouts??[]){if(!a.has(e.file))continue;let t=Math.max(1,Math.floor(e.startLine)),n=Math.max(t,Math.floor(e.endLine??e.startLine));Number.isFinite(t)&&o.push({id:`${i}-c${o.length+1}`,file:e.file,startLine:t,endLine:n,side:e.side===`old`?`old`:`new`,body:e.body??``})}n.push({id:i,title:r.title,body:r.body??``,paths:e,callouts:o})}return n}async function V(e,t,n,i={kind:`workingTree`}){let o=_(e),s=r(e),c=i.kind===`branch`?i.base:`HEAD`,l=await s.revparse([c]).then(e=>e.trim(),()=>void 0)??await s.revparse([`HEAD`]).then(e=>e.trim(),()=>void 0),u=await v(e).catch(()=>null)??void 0,d=await C(e,i),f=[],p=0,m=0;for(let t of d){let n=await L(e,t.path,i);f.push({path:n.file.path,oldPath:n.file.oldPath??t.oldPath,status:n.file.status,additions:n.file.additions,deletions:n.file.deletions,isBinary:n.file.isBinary,patch:n.patch,oldContents:n.oldContents,newContents:n.newContents,truncated:n.truncated,oldImage:n.oldImage,newImage:n.newImage}),p+=n.file.additions,m+=n.file.deletions}let h=B(t.steps??n?.steps?.map(e=>({title:e.title,body:e.body,files:e.paths,callouts:e.callouts?.map(e=>({file:e.file,startLine:e.startLine,endLine:e.endLine,side:e.side,body:e.body}))}))??[],new Set(f.map(e=>e.path))),g=Date.now();return{id:n?.id??a(),repoId:o,key:t.key??n?.key,name:t.name??n?.name??`Untitled session`,description:t.description??n?.description??``,harness:t.harness??n?.harness??`other`,harnessLabel:t.harnessLabel??n?.harnessLabel,harnessUrl:t.harnessUrl??n?.harnessUrl,branch:u,baseRef:l,createdAt:n?.createdAt??g,updatedAt:g,fileCount:f.length,additions:p,deletions:m,stepCount:h.length,steps:h,files:f}}function H(e){return n.join(e,`.super-review`,`sessions`)}function U(e,t){return n.join(H(e),`${t}.json`)}function W(e){let{files:t,steps:n,...r}=e;return{...r,stepCount:n?.length??0}}async function G(e){try{let t=await o.readFile(e,`utf8`);return JSON.parse(t)}catch{return null}}async function K(e){let t=H(e),r;try{r=await o.readdir(t)}catch{return[]}return(await Promise.all(r.filter(e=>e.endsWith(`.json`)).map(e=>G(n.join(t,e))))).filter(e=>e!==null).map(W).sort((e,t)=>t.updatedAt-e.updatedAt)}async function q(e,t){return G(U(e,t))}async function J(e,t){let r;try{r=await o.readdir(H(e))}catch{return null}for(let i of r){if(!i.endsWith(`.json`))continue;let r=await G(n.join(H(e),i));if(r?.key===t)return r}return null}async function Y(e,t){let n=H(e);await o.mkdir(n,{recursive:!0}),await o.writeFile(U(e,t.id),JSON.stringify(t,null,2),`utf8`)}async function X(e){await o.rm(H(e),{recursive:!0,force:!0})}function Z(e){console.error(`error: ${e}`),process.exit(1)}async function Q(e){try{return(await r(e).revparse([`--show-toplevel`])).trim()}catch{Z(`not a git repository: ${e}`)}}const $=[`claude-code`,`cursor`,`codex`,`opencode`,`copilot`,`other`];function te(e){let t;try{t=JSON.parse(e)}catch{Z(`--tour is not valid JSON`)}(typeof t!=`object`||!t)&&Z(`--tour must be a JSON object`);let n=t;return n.steps!==void 0&&(Array.isArray(n.steps)||Z("--tour `steps` must be an array"),n.steps.forEach((e,t)=>{(!e||typeof e.title!=`string`||!e.title.trim())&&Z(`--tour step ${t+1} is missing a non-empty "title"`),(!Array.isArray(e.files)||e.files.length===0||!e.files.every(e=>typeof e==`string`))&&Z(`--tour step ${t+1} ("${e.title}") needs a non-empty "files" array of paths`),e.callouts!==void 0&&(Array.isArray(e.callouts)||Z(`--tour step ${t+1} ("${e.title}") "callouts" must be an array`),e.callouts.forEach((e,n)=>{let r=`step ${t+1} callout ${n+1}`;(!e||typeof e.file!=`string`)&&Z(`--tour ${r} needs a "file" string`),typeof e.startLine!=`number`&&Z(`--tour ${r} needs a numeric "startLine"`),(typeof e.body!=`string`||!e.body.trim())&&Z(`--tour ${r} needs a non-empty "body"`)}))})),n}async function ne(e){let t=e.tour?te(e.tour):null,r=e.harness??t?.harness;r&&!$.includes(r)&&Z(`invalid --harness "${r}". Expected one of: ${$.join(`, `)}`);let i=await Q(n.resolve(e.cwd??process.cwd())),a=null;e.id?(a=await q(i,e.id),a||Z(`no session with id "${e.id}" for this repo`)):e.key&&(a=await J(i,e.key));let o=e.name??t?.name,s=e.description??t?.description;!a&&(!o||!s)&&Z(`--name and --description are required when creating a new session`);let c={key:e.key,name:o,description:s,harness:r||void 0,harnessLabel:e.harnessLabel??t?.harnessLabel,harnessUrl:e.harnessUrl??t?.harnessUrl,steps:t?.steps},l=!!(e.committed||e.base||e.head),u;if(l){let t=e.base??await ee(i);t||Z(`could not determine a base branch to diff against; pass --base <ref>`),u={kind:`branch`,base:t,head:e.head??await v(i)??`HEAD`}}let d=await V(i,c,a,u);if(d.fileCount===0&&Z(l?`no committed changes on ${u.kind===`branch`?u.head:`HEAD`} vs ${u.kind===`branch`?u.base:`base`} to capture`:`no working-tree changes to capture`),t?.steps){let e=new Set(d.files.map(e=>e.path)),n=[...new Set(t.steps.flatMap(e=>[...e.files,...(e.callouts??[]).map(e=>e.file)]).filter(t=>!e.has(t)))];n.length>0&&console.error(`warning: ${n.length} tour path(s) not in the captured changes (dropped):\n`+n.map(e=>` - ${e}`).join(`
|
|
12
|
+
`))}await Y(i,d);let f=a?`updated`:`created`,p=d.stepCount>0?`, ${d.stepCount} tour step(s)`:``;console.log(`${f} session "${d.name}" (${d.id})\n ${d.fileCount} file(s), +${d.additions} −${d.deletions}${p}`)}const re=new e(`save`).description(`capture the working tree's current changes (or, with --committed, this branch's committed changes) as a reviewable session`).option(`--key <id>`,`Stable upsert key (your harness conversation/run id). Re-running with the same key UPDATES that session.`).option(`--id <id>`,`Target an existing session by its id (alternative to --key).`).option(`--name <text>`,`Session name (required on first save).`).option(`--description <text>`,`What you changed (required on first save).`).option(`--harness <kind>`,`One of: ${$.join(`, `)} (default: other).`).option(`--harness-label <text>`,`Freeform harness name (used when --harness other).`).option(`--harness-url <url>`,`Deep link back to this run (resume/permalink).`).option(`--committed`,`Capture this branch diffed against its base (auto-detected default branch) instead of the working tree, so already-committed changes can be documented.`).option(`--base <ref>`,`Base ref to diff against (implies --committed). Defaults to the auto-detected default branch.`).option(`--head <ref>`,`Head ref to diff (implies --committed). Defaults to the current branch.`).option(`--tour <json>`,`Inline JSON describing a guided tour: ordered steps that group related files with commentary, so the reviewer reads the change as a narrative. Flags override the document's top-level name/description/harness.`).option(`--cwd <path>`,`Repo path (default: current directory).`).addHelpText(`after`,`
|
|
10
13
|
Captures a frozen snapshot of the working tree's current changes so they can be
|
|
11
14
|
reviewed as an isolated session in the super-review desktop app.
|
|
12
15
|
|
|
@@ -20,7 +23,14 @@ reviewed as an isolated session in the super-review desktop app.
|
|
|
20
23
|
]
|
|
21
24
|
}
|
|
22
25
|
List files in reading order; any changed file you omit still shows, grouped
|
|
23
|
-
under "Other changes" at the end
|
|
26
|
+
under "Other changes" at the end.
|
|
27
|
+
|
|
28
|
+
Documenting committed changes:
|
|
29
|
+
By default the diff is the working tree (uncommitted edits). To document
|
|
30
|
+
changes you've already committed, pass --committed: it captures this branch
|
|
31
|
+
diffed against its base (auto-detected default branch, e.g. main). Override
|
|
32
|
+
with --base <ref> and/or --head <ref>. Passing --base or --head implies
|
|
33
|
+
--committed.`).action(ne);async function ie(e){let t=await Q(n.resolve(e.cwd??process.cwd())),r=(await K(t)).length;await X(t),console.log(r===0?`no sessions to clear`:`cleared ${r} session${r===1?``:`s`} from .super-review/sessions/`)}const ae=new e(`clear`).description(`remove all sessions from the repo (.super-review/sessions/)`).option(`--cwd <path>`,`Repo path (default: current directory).`).action(ie),oe=new e(`session`).description(`create and manage review sessions`).addHelpText(`after`,`
|
|
24
34
|
Sessions are stored in the repo under .super-review/sessions/, so they can be
|
|
25
35
|
committed and travel with a branch/PR. Run "session clear" to remove them all
|
|
26
|
-
(e.g. before merging) — git then sees the committed manifests as deleted.`).addCommand(
|
|
36
|
+
(e.g. before merging) — git then sees the committed manifests as deleted.`).addCommand(re).addCommand(ae);t.name(`super-review`).description(u).version(l).showHelpAfterError().addCommand(oe).parseAsync(process.argv).catch(e=>{Z(e instanceof Error?e.message:String(e))});export{};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-review",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Document an agent session for review with the super-review desktop app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@types/node": "^24.0.0",
|
|
41
41
|
"tsdown": "^0.22.1",
|
|
42
42
|
"typescript": "^5.7.3",
|
|
43
|
-
"@super-review/core": "0.
|
|
43
|
+
"@super-review/core": "0.1.1"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsdown",
|