markdown-markdown 0.2.1 → 0.2.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.
Files changed (77) hide show
  1. package/AGENT.md +9 -0
  2. package/README.md +48 -5
  3. package/dist/cli.d.ts +8 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +449 -22
  6. package/dist/cli.js.map +1 -1
  7. package/dist/lib/document-title.d.ts +4 -0
  8. package/dist/lib/document-title.d.ts.map +1 -0
  9. package/dist/lib/document-title.js +18 -0
  10. package/dist/lib/document-title.js.map +1 -0
  11. package/dist/server/host.d.ts.map +1 -1
  12. package/dist/server/host.js +7 -0
  13. package/dist/server/host.js.map +1 -1
  14. package/dist/server/session-store.d.ts +4 -1
  15. package/dist/server/session-store.d.ts.map +1 -1
  16. package/dist/server/session-store.js +28 -0
  17. package/dist/server/session-store.js.map +1 -1
  18. package/dist/types.d.ts +10 -0
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/web/assets/{_baseUniq-nFqJWqJf.js → _baseUniq-BeWnGfR8.js} +1 -1
  21. package/dist/web/assets/{arc-Jg9CFoKt.js → arc-suYjphRL.js} +1 -1
  22. package/dist/web/assets/{architectureDiagram-Q4EWVU46-DYR5Lm08.js → architectureDiagram-Q4EWVU46-Dun0g_LP.js} +1 -1
  23. package/dist/web/assets/{blockDiagram-DXYQGD6D-30J7_LoM.js → blockDiagram-DXYQGD6D-B8KWi0ek.js} +1 -1
  24. package/dist/web/assets/{c4Diagram-AHTNJAMY-BClBvNDA.js → c4Diagram-AHTNJAMY-DH-kEaf-.js} +1 -1
  25. package/dist/web/assets/channel-eADqIRXc.js +1 -0
  26. package/dist/web/assets/{chunk-4BX2VUAB-Bgzv4fI5.js → chunk-4BX2VUAB-DZzS8Oum.js} +1 -1
  27. package/dist/web/assets/{chunk-4TB4RGXK-BKN4Lng_.js → chunk-4TB4RGXK-r3tH5W7U.js} +1 -1
  28. package/dist/web/assets/{chunk-55IACEB6-CXq43BG4.js → chunk-55IACEB6-Cpbpjr04.js} +1 -1
  29. package/dist/web/assets/{chunk-EDXVE4YY-DokDvDhv.js → chunk-EDXVE4YY-Bh5LY5hG.js} +1 -1
  30. package/dist/web/assets/{chunk-FMBD7UC4-BNN1454L.js → chunk-FMBD7UC4-BsyHJJbd.js} +1 -1
  31. package/dist/web/assets/{chunk-OYMX7WX6-CMHPjdY3.js → chunk-OYMX7WX6-c5e-q_wa.js} +1 -1
  32. package/dist/web/assets/{chunk-QZHKN3VN-D9Xdgwdu.js → chunk-QZHKN3VN-B247zH78.js} +1 -1
  33. package/dist/web/assets/{chunk-YZCP3GAM-CNVT9piY.js → chunk-YZCP3GAM-Cfm-rXjN.js} +1 -1
  34. package/dist/web/assets/classDiagram-6PBFFD2Q-DS-vDmh2.js +1 -0
  35. package/dist/web/assets/classDiagram-v2-HSJHXN6E-DS-vDmh2.js +1 -0
  36. package/dist/web/assets/clone-Wzjy1jSR.js +1 -0
  37. package/dist/web/assets/{cose-bilkent-S5V4N54A-Bhn6pAUV.js → cose-bilkent-S5V4N54A-DqxVLdHa.js} +1 -1
  38. package/dist/web/assets/{dagre-KV5264BT-C4y39Usr.js → dagre-KV5264BT-uESNxmxB.js} +1 -1
  39. package/dist/web/assets/{diagram-5BDNPKRD-rlvQhIY0.js → diagram-5BDNPKRD-BR2-mp52.js} +1 -1
  40. package/dist/web/assets/{diagram-G4DWMVQ6-CxlsuhBt.js → diagram-G4DWMVQ6-WKF0sInv.js} +1 -1
  41. package/dist/web/assets/{diagram-MMDJMWI5-aO59UQJW.js → diagram-MMDJMWI5-FmHCVuiV.js} +1 -1
  42. package/dist/web/assets/{diagram-TYMM5635-DboqCGxD.js → diagram-TYMM5635-DRfXSa3H.js} +1 -1
  43. package/dist/web/assets/{erDiagram-SMLLAGMA-BfTNK70Y.js → erDiagram-SMLLAGMA-CIaSeJrL.js} +1 -1
  44. package/dist/web/assets/{flowDiagram-DWJPFMVM-D5GqUELM.js → flowDiagram-DWJPFMVM-sxEQwCxD.js} +1 -1
  45. package/dist/web/assets/{ganttDiagram-T4ZO3ILL-0oluOY8-.js → ganttDiagram-T4ZO3ILL-CkcF9O8o.js} +1 -1
  46. package/dist/web/assets/{gitGraphDiagram-UUTBAWPF-DOvCDomy.js → gitGraphDiagram-UUTBAWPF-BMl-eEdv.js} +1 -1
  47. package/dist/web/assets/{graph-D6tF7y1b.js → graph-Dvb83lTL.js} +1 -1
  48. package/dist/web/assets/{index-CLPXS5Pa.js → index-CDiKEXNm.js} +125 -115
  49. package/dist/web/assets/index-D6JW8WNw.css +1 -0
  50. package/dist/web/assets/{infoDiagram-42DDH7IO-DzqzFgrP.js → infoDiagram-42DDH7IO-BErZMnZ_.js} +1 -1
  51. package/dist/web/assets/{ishikawaDiagram-UXIWVN3A-WJU0OO5B.js → ishikawaDiagram-UXIWVN3A-ByOhD_Q_.js} +1 -1
  52. package/dist/web/assets/{journeyDiagram-VCZTEJTY-Bo7zyWBb.js → journeyDiagram-VCZTEJTY-tvYu0tQA.js} +1 -1
  53. package/dist/web/assets/{kanban-definition-6JOO6SKY-DqDWBbmi.js → kanban-definition-6JOO6SKY-bOg9bXmu.js} +1 -1
  54. package/dist/web/assets/{layout-eCIhMhgM.js → layout-QKJyhC2Z.js} +1 -1
  55. package/dist/web/assets/{linear-DhP7p6ZM.js → linear-CIIOLDtW.js} +1 -1
  56. package/dist/web/assets/{min-CmgCLj3n.js → min-BN_T5qY9.js} +1 -1
  57. package/dist/web/assets/{mindmap-definition-QFDTVHPH-B7bz994e.js → mindmap-definition-QFDTVHPH-BtrZH91G.js} +1 -1
  58. package/dist/web/assets/{pieDiagram-DEJITSTG-DlKyyMAM.js → pieDiagram-DEJITSTG-Dnq_4keb.js} +1 -1
  59. package/dist/web/assets/{quadrantDiagram-34T5L4WZ-B3C_LSAl.js → quadrantDiagram-34T5L4WZ-H0k8Hqni.js} +1 -1
  60. package/dist/web/assets/{requirementDiagram-MS252O5E-D7q7Uv1-.js → requirementDiagram-MS252O5E-hB2GEO0i.js} +1 -1
  61. package/dist/web/assets/{sankeyDiagram-XADWPNL6-B3AHI7j9.js → sankeyDiagram-XADWPNL6-Dz1v5jHa.js} +1 -1
  62. package/dist/web/assets/{sequenceDiagram-FGHM5R23-C0ym1rK1.js → sequenceDiagram-FGHM5R23-DkjFesyC.js} +1 -1
  63. package/dist/web/assets/{stateDiagram-FHFEXIEX-_NYky71o.js → stateDiagram-FHFEXIEX-O3tFHR2C.js} +1 -1
  64. package/dist/web/assets/stateDiagram-v2-QKLJ7IA2-C1nL4I8b.js +1 -0
  65. package/dist/web/assets/{timeline-definition-GMOUNBTQ-BPub2z2y.js → timeline-definition-GMOUNBTQ-DfZTgHu_.js} +1 -1
  66. package/dist/web/assets/{vennDiagram-DHZGUBPP-B1ao3Y4V.js → vennDiagram-DHZGUBPP-lkZm2iF1.js} +1 -1
  67. package/dist/web/assets/{wardley-RL74JXVD-xqk6dIJX.js → wardley-RL74JXVD-BI3M_xu_.js} +1 -1
  68. package/dist/web/assets/{wardleyDiagram-NUSXRM2D-DHkUBrSw.js → wardleyDiagram-NUSXRM2D-BHWV3Mta.js} +1 -1
  69. package/dist/web/assets/{xychartDiagram-5P7HB3ND-D62ziF_o.js → xychartDiagram-5P7HB3ND-YYAt89Gd.js} +1 -1
  70. package/dist/web/index.html +2 -2
  71. package/package.json +1 -1
  72. package/dist/web/assets/channel-B-PozOMc.js +0 -1
  73. package/dist/web/assets/classDiagram-6PBFFD2Q-DZU9TAgg.js +0 -1
  74. package/dist/web/assets/classDiagram-v2-HSJHXN6E-DZU9TAgg.js +0 -1
  75. package/dist/web/assets/clone-BUwPWatq.js +0 -1
  76. package/dist/web/assets/index-4xbK2CF5.css +0 -1
  77. package/dist/web/assets/stateDiagram-v2-QKLJ7IA2-BMjcTPNs.js +0 -1
package/AGENT.md CHANGED
@@ -24,6 +24,15 @@ For agent-facing usage guidance, use the separate [`markdown-markdown-skill`](ht
24
24
  - Run the test suite with `npm test`.
25
25
  - Publish releases through GitHub Actions on `v*` tags; avoid local `npm publish` except for bootstrap or debugging.
26
26
 
27
+ ## Async Review CLI
28
+
29
+ - Prefer the async commands for agent workflows: `markdown-markdown review create`, `review wait`, `review refresh`, and `review close`.
30
+ - `review create` starts the single active review session and opens the browser.
31
+ - `review wait` is the source of truth for user intent. It returns either `finish_review`, `continue_review`, or an `abandoned` result.
32
+ - If `review wait` returns `continue_review`, edit the files in the current workspace context, then run `review refresh` to start the next round.
33
+ - If `review wait` returns `abandoned`, stop and ask the user whether to continue without review or start a fresh review session.
34
+ - The browser UI stays readable during `awaiting_refresh`, but annotation edits are intentionally locked until the next round is ready.
35
+
27
36
  ## Release Workflow
28
37
 
29
38
  - Treat `main` as the source of truth.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Local Markdown review and annotation CLI.
4
4
 
5
- Point it at a single Markdown file or a directory of Markdown files, make annotations in the browser, then submit once you are done. The command exits and prints a JSON payload that can be handed to an AI system or another automation step.
5
+ Point it at a single Markdown file or a directory of Markdown files, review them in the browser, then hand the structured result back to an AI system or another automation step.
6
6
 
7
7
  ## Companion Skill
8
8
 
@@ -34,6 +34,47 @@ The workflow uses GitHub Actions trusted publishing, so no npm token needs to be
34
34
 
35
35
  ## Use
36
36
 
37
+ ### Async review loop
38
+
39
+ The primary agent-facing flow is asynchronous:
40
+
41
+ ```bash
42
+ npx markdown-markdown review create --browser system ./docs/spec.md
43
+ ```
44
+
45
+ `review create` starts a single active review session, opens the browser UI, and prints a JSON payload with:
46
+
47
+ - `sessionId`
48
+ - `reviewUrl`
49
+ - `controlUrl`
50
+ - `publicUrl`
51
+ - `round`
52
+ - `phase`
53
+
54
+ Then the agent waits for user action:
55
+
56
+ ```bash
57
+ npx markdown-markdown review wait
58
+ ```
59
+
60
+ When the user clicks `结束 review`, `review wait` returns a `finish_review` payload.
61
+
62
+ When the user clicks `吸收修改后再给我看`, `review wait` returns a `continue_review` payload. After the agent edits files, it should request the next round:
63
+
64
+ ```bash
65
+ npx markdown-markdown review refresh
66
+ ```
67
+
68
+ When the session is done, clean it up explicitly:
69
+
70
+ ```bash
71
+ npx markdown-markdown review close
72
+ ```
73
+
74
+ The browser UI keeps the document readable during `continue_review`, but it locks annotation edits until the next round is refreshed. The browser tab title also follows the current file name and review status, which helps when you have multiple tabs open.
75
+
76
+ ### Legacy one-shot mode
77
+
37
78
  ```bash
38
79
  npx markdown-markdown ./docs
39
80
  ```
@@ -75,21 +116,23 @@ The CLI will:
75
116
  - use a public Cloudflare tunnel when `cloudflared` is installed, unless you pass `--no-cloudflare`
76
117
  - fail fast with a clear install message when you pass `--cloudflare` but `cloudflared` is missing
77
118
  - let you annotate headings, paragraphs, blockquotes, lists, tables, code blocks, images, and horizontal rules
78
- - submit the annotations and close the browser
79
- - print the final JSON result to stdout
119
+ - support two review actions in the browser: `结束 review` and `吸收修改后再给我看`
120
+ - print the structured review result to stdout
80
121
  - optionally write the final JSON to `--output`
81
122
  - optionally write running/completed/failed status snapshots to `--status-file`
82
123
 
83
124
  ## Output
84
125
 
85
- The submitted JSON includes:
126
+ The review result JSON includes:
86
127
 
87
128
  - `rootPath`
88
129
  - `mode`
89
130
  - `files`
90
131
  - `annotations`
91
132
  - `prompt`
92
- - `submittedAt`
133
+ - `submittedAt` or `abandonedAt`
134
+ - `action` when the user submits (`finish_review` or `continue_review`)
135
+ - `round`
93
136
 
94
137
  The `prompt` field is a ready-to-send Markdown summary of the requested changes, grouped by file with short anchor text and line ranges so an AI can locate the target block without extra token cost.
95
138
 
package/dist/cli.d.ts CHANGED
@@ -8,6 +8,14 @@ export interface ParsedCliArgs {
8
8
  statusPath: string | null;
9
9
  helpRequested: boolean;
10
10
  }
11
+ export type ReviewCommandName = 'create' | 'wait' | 'refresh' | 'close';
12
+ export type CliInvocation = (ParsedCliArgs & {
13
+ kind: 'legacy';
14
+ }) | (ParsedCliArgs & {
15
+ kind: 'review';
16
+ reviewCommand: ReviewCommandName;
17
+ });
11
18
  export declare function parseCliArgs(argv: string[]): ParsedCliArgs;
19
+ export declare function parseCliInvocation(argv: string[]): CliInvocation;
12
20
  export declare function isDirectExecutionEntry(argv1: string | undefined, moduleUrl: string): boolean;
13
21
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;CACxB;AA0BD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAsI1D;AAmMD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAS5F"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAuBA,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EAIf,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAExE,MAAM,MAAM,aAAa,GACrB,CAAC,aAAa,GAAG;IACf,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC,GACF,CAAC,aAAa,GAAG;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,aAAa,EAAE,iBAAiB,CAAC;CAClC,CAAC,CAAC;AAqFP,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAsI1D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CA+BhE;AA6oBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAS5F"}
package/dist/cli.js CHANGED
@@ -1,14 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, realpathSync } from 'node:fs';
3
- import { stat } from 'node:fs/promises';
4
2
  import { randomUUID } from 'node:crypto';
3
+ import { existsSync, realpathSync } from 'node:fs';
4
+ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
5
6
  import path from 'node:path';
7
+ import { spawn, spawnSync } from 'node:child_process';
6
8
  import { fileURLToPath } from 'node:url';
7
9
  import { buildCompletedStatus, buildFailedStatus, buildRunningStatus, writeJsonFile } from './lib/lifecycle.js';
8
10
  import { discoverMarkdownFiles } from './server/session.js';
9
11
  import { launchBrowser } from './server/browser.js';
10
12
  import { hasCloudflaredExecutable, launchCloudflaredTunnel } from './server/tunnel.js';
11
13
  import { startReviewRuntime } from './server/runtime.js';
14
+ import { startReviewHost } from './server/host.js';
15
+ import { closeSession, getActiveSession, readActiveSessionRuntime, readSessionResult, readSessionRuntime, writeSessionRuntime, } from './server/session-store.js';
16
+ function buildCurrentCliExecArgs() {
17
+ const cliEntry = fileURLToPath(import.meta.url);
18
+ if (cliEntry.endsWith('.ts')) {
19
+ return ['--import', 'tsx', cliEntry];
20
+ }
21
+ return [cliEntry];
22
+ }
12
23
  function resolveRealpath(candidate) {
13
24
  try {
14
25
  return realpathSync(candidate);
@@ -17,7 +28,7 @@ function resolveRealpath(candidate) {
17
28
  return path.resolve(candidate);
18
29
  }
19
30
  }
20
- function printUsage() {
31
+ function printLegacyUsage() {
21
32
  console.error([
22
33
  'Usage: markdown-markdown [options] <file-or-directory>',
23
34
  '',
@@ -28,6 +39,29 @@ function printUsage() {
28
39
  ' --no-cloudflare Force localhost only',
29
40
  ' --output <file> Write the final review JSON to a file',
30
41
  ' --status-file <file> Write running/completed status updates to a file',
42
+ '',
43
+ 'Async review commands:',
44
+ ' markdown-markdown review create [options] <file-or-directory>',
45
+ ' markdown-markdown review wait',
46
+ ' markdown-markdown review refresh',
47
+ ' markdown-markdown review close',
48
+ ].join('\n'));
49
+ }
50
+ function printReviewUsage() {
51
+ console.error([
52
+ 'Usage: markdown-markdown review <create|wait|refresh|close> [options]',
53
+ '',
54
+ 'Commands:',
55
+ ' create <file-or-directory> Start an async review session and open the browser UI',
56
+ ' wait Block until the active review session returns a result',
57
+ ' refresh Reload files for the next review round after continue_review',
58
+ ' close End the active review session and clean up the daemon',
59
+ '',
60
+ 'Options for review create:',
61
+ ' --browser app|system Browser launch mode (default: app)',
62
+ ' --cloudflare Require a Cloudflare tunnel',
63
+ ' --cloudflare=auto|required|off',
64
+ ' --no-cloudflare Force localhost only',
31
65
  ].join('\n'));
32
66
  }
33
67
  export function parseCliArgs(argv) {
@@ -94,7 +128,7 @@ export function parseCliArgs(argv) {
94
128
  outputPath = value;
95
129
  continue;
96
130
  }
97
- if (parsingOptions && (arg === '--status-file')) {
131
+ if (parsingOptions && arg === '--status-file') {
98
132
  const nextValue = argv[index + 1];
99
133
  if (!nextValue) {
100
134
  throw new Error(`Missing value for ${arg}.`);
@@ -141,6 +175,33 @@ export function parseCliArgs(argv) {
141
175
  helpRequested: false,
142
176
  };
143
177
  }
178
+ export function parseCliInvocation(argv) {
179
+ if (argv[0] === 'review') {
180
+ const reviewCommand = argv[1];
181
+ if (!reviewCommand || !['create', 'wait', 'refresh', 'close'].includes(reviewCommand)) {
182
+ throw new Error(`Unknown review command: ${reviewCommand ?? '(missing)'}`);
183
+ }
184
+ const parsed = parseCliArgs(argv.slice(2));
185
+ if (reviewCommand === 'create' && !parsed.targetArg && !parsed.helpRequested) {
186
+ throw new Error('review create requires a file or directory target.');
187
+ }
188
+ if (reviewCommand !== 'create' && parsed.targetArg) {
189
+ throw new Error(`review ${reviewCommand} does not accept a target path.`);
190
+ }
191
+ if (parsed.outputPath || parsed.statusPath) {
192
+ throw new Error('review create/wait/refresh/close do not support --output or --status-file. Use stdout and the async commands instead.');
193
+ }
194
+ return {
195
+ ...parsed,
196
+ kind: 'review',
197
+ reviewCommand,
198
+ };
199
+ }
200
+ return {
201
+ ...parseCliArgs(argv),
202
+ kind: 'legacy',
203
+ };
204
+ }
144
205
  function resolveStaticDir() {
145
206
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
146
207
  const candidates = [
@@ -155,6 +216,35 @@ function resolveStaticDir() {
155
216
  }
156
217
  return null;
157
218
  }
219
+ async function readJsonFile(filePath) {
220
+ try {
221
+ const content = await readFile(filePath, 'utf8');
222
+ return JSON.parse(content);
223
+ }
224
+ catch (error) {
225
+ if (error.code === 'ENOENT') {
226
+ return null;
227
+ }
228
+ throw error;
229
+ }
230
+ }
231
+ function sleep(milliseconds) {
232
+ return new Promise((resolve) => {
233
+ setTimeout(resolve, milliseconds);
234
+ });
235
+ }
236
+ function isPidAlive(pid) {
237
+ if (!pid || pid <= 0) {
238
+ return false;
239
+ }
240
+ try {
241
+ process.kill(pid, 0);
242
+ return true;
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ }
158
248
  async function writeLifecycleArtifact(filePath, value, label) {
159
249
  if (!filePath) {
160
250
  return;
@@ -166,6 +256,75 @@ async function writeLifecycleArtifact(filePath, value, label) {
166
256
  console.error(`Failed to write ${label} at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
167
257
  }
168
258
  }
259
+ async function resolveTargetContext(targetArg) {
260
+ const targetPath = path.resolve(process.cwd(), targetArg);
261
+ const targetStats = await stat(targetPath).catch(() => null);
262
+ if (!targetStats) {
263
+ throw new Error(`Target path does not exist: ${targetPath}`);
264
+ }
265
+ const mode = targetStats.isFile() ? 'file' : 'directory';
266
+ const files = await discoverMarkdownFiles(targetPath);
267
+ if (files.length === 0) {
268
+ throw new Error(`No markdown files found under: ${targetPath}`);
269
+ }
270
+ return { files, mode, targetPath };
271
+ }
272
+ async function ensureNoConflictingActiveSession() {
273
+ const activeSession = await getActiveSession();
274
+ if (!activeSession) {
275
+ return;
276
+ }
277
+ const runtime = await readActiveSessionRuntime();
278
+ if (runtime && !isPidAlive(runtime.hostPid)) {
279
+ await closeSession(activeSession.sessionId);
280
+ return;
281
+ }
282
+ const location = runtime?.reviewUrl ?? runtime?.controlUrl;
283
+ throw new Error(`An active review session already exists: ${activeSession.sessionId}${location ? ` (${location})` : ''}. Close it before creating a new one.`);
284
+ }
285
+ async function waitForDaemonReady(readyFile, errorFile, childPid, timeoutMs = 20_000) {
286
+ const deadline = Date.now() + timeoutMs;
287
+ while (Date.now() < deadline) {
288
+ const errorPayload = await readJsonFile(errorFile);
289
+ if (errorPayload?.message) {
290
+ throw new Error(errorPayload.message);
291
+ }
292
+ const readyPayload = await readJsonFile(readyFile);
293
+ if (readyPayload) {
294
+ return readyPayload;
295
+ }
296
+ if (!isPidAlive(childPid)) {
297
+ throw new Error('Review daemon stopped before it reported a ready state.');
298
+ }
299
+ await sleep(100);
300
+ }
301
+ throw new Error('Timed out waiting for the async review daemon to start.');
302
+ }
303
+ function killProcessTree(pid) {
304
+ if (process.platform === 'win32') {
305
+ spawnSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' });
306
+ return;
307
+ }
308
+ try {
309
+ process.kill(pid, 'SIGTERM');
310
+ }
311
+ catch {
312
+ // Ignore kill failures; close flow will fall back to clearing session state.
313
+ }
314
+ }
315
+ async function requestControl(runtime, endpoint) {
316
+ const response = await fetch(`${runtime.controlUrl}${endpoint}`, {
317
+ method: 'POST',
318
+ headers: {
319
+ 'Content-Type': 'application/json',
320
+ },
321
+ });
322
+ if (!response.ok) {
323
+ const body = await response.text();
324
+ throw new Error(body || `Control request failed: ${response.status}`);
325
+ }
326
+ return (await response.json());
327
+ }
169
328
  async function runReviewSession(parsedArgs, targetPath, mode, files, staticDir) {
170
329
  const resolvedOutputPath = parsedArgs.outputPath ? path.resolve(process.cwd(), parsedArgs.outputPath) : null;
171
330
  const resolvedStatusPath = parsedArgs.statusPath ? path.resolve(process.cwd(), parsedArgs.statusPath) : null;
@@ -256,39 +415,307 @@ async function runReviewSession(parsedArgs, targetPath, mode, files, staticDir)
256
415
  await shutdown();
257
416
  }
258
417
  }
418
+ function parseInternalReviewHostArgs(argv) {
419
+ const values = new Map();
420
+ for (let index = 0; index < argv.length; index += 2) {
421
+ const key = argv[index];
422
+ const value = argv[index + 1];
423
+ if (!key?.startsWith('--') || !value) {
424
+ throw new Error(`Malformed internal review host arguments near: ${key ?? '(missing)'}`);
425
+ }
426
+ values.set(key.slice(2), value);
427
+ }
428
+ const mode = values.get('mode');
429
+ const browserMode = values.get('browser');
430
+ const cloudflareMode = values.get('cloudflare');
431
+ if (mode !== 'file' && mode !== 'directory') {
432
+ throw new Error(`Unsupported internal mode: ${mode ?? '(missing)'}`);
433
+ }
434
+ if (browserMode !== 'app' && browserMode !== 'system') {
435
+ throw new Error(`Unsupported internal browser mode: ${browserMode ?? '(missing)'}`);
436
+ }
437
+ if (cloudflareMode !== 'auto' && cloudflareMode !== 'required' && cloudflareMode !== 'off') {
438
+ throw new Error(`Unsupported internal cloudflare mode: ${cloudflareMode ?? '(missing)'}`);
439
+ }
440
+ const sessionId = values.get('session-id');
441
+ const rootPath = values.get('root-path');
442
+ const staticDir = values.get('static-dir');
443
+ const readyFile = values.get('ready-file');
444
+ const errorFile = values.get('error-file');
445
+ if (!sessionId || !rootPath || !staticDir || !readyFile || !errorFile) {
446
+ throw new Error('Missing required internal review host arguments.');
447
+ }
448
+ return {
449
+ sessionId,
450
+ rootPath,
451
+ mode,
452
+ staticDir,
453
+ browserMode,
454
+ cloudflareMode,
455
+ readyFile,
456
+ errorFile,
457
+ };
458
+ }
459
+ async function writeInternalResult(filePath, payload) {
460
+ await mkdir(path.dirname(filePath), { recursive: true });
461
+ await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
462
+ }
463
+ async function runInternalReviewHost(args) {
464
+ let browser = null;
465
+ let tunnel = null;
466
+ let host = null;
467
+ let shuttingDown = false;
468
+ let shutdownResolver = null;
469
+ const shutdown = async () => {
470
+ if (shuttingDown) {
471
+ return;
472
+ }
473
+ shuttingDown = true;
474
+ await browser?.close();
475
+ await tunnel?.close();
476
+ await host?.close();
477
+ shutdownResolver?.();
478
+ };
479
+ try {
480
+ const files = await discoverMarkdownFiles(args.rootPath);
481
+ host = await startReviewHost({
482
+ files,
483
+ mode: args.mode,
484
+ rootPath: args.rootPath,
485
+ sessionId: args.sessionId,
486
+ staticDir: args.staticDir,
487
+ });
488
+ if (args.cloudflareMode === 'required' && !hasCloudflaredExecutable()) {
489
+ throw new Error('Cloudflare tunnel was requested, but cloudflared is not installed. Install cloudflared or rerun with --no-cloudflare.');
490
+ }
491
+ if (args.cloudflareMode !== 'off') {
492
+ tunnel = await launchCloudflaredTunnel(host.url);
493
+ if (args.cloudflareMode === 'required' && !tunnel) {
494
+ throw new Error('Cloudflare tunnel was requested, but cloudflared could not start. Install cloudflared or rerun with --no-cloudflare.');
495
+ }
496
+ }
497
+ const launchUrl = tunnel?.publicUrl ?? host.url;
498
+ browser =
499
+ args.browserMode === 'system'
500
+ ? await launchBrowser(launchUrl, { executableCandidates: [] })
501
+ : await launchBrowser(launchUrl);
502
+ if (browser.process) {
503
+ browser.process.once('exit', () => {
504
+ void host?.notifyBrowserClosed();
505
+ });
506
+ }
507
+ await writeSessionRuntime(host.sessionId, {
508
+ browserMode: args.browserMode,
509
+ cloudflareMode: args.cloudflareMode,
510
+ controlUrl: host.url,
511
+ hostPid: process.pid,
512
+ publicUrl: tunnel?.publicUrl ?? null,
513
+ reviewUrl: launchUrl,
514
+ startedAt: new Date().toISOString(),
515
+ });
516
+ await writeInternalResult(args.readyFile, {
517
+ browserMode: args.browserMode,
518
+ cloudflareMode: args.cloudflareMode,
519
+ controlUrl: host.url,
520
+ mode: args.mode,
521
+ phase: 'waiting_for_review',
522
+ publicUrl: tunnel?.publicUrl ?? null,
523
+ reviewUrl: launchUrl,
524
+ rootPath: args.rootPath,
525
+ round: 1,
526
+ sessionId: host.sessionId,
527
+ });
528
+ host.result.then(async (result) => {
529
+ if ('action' in result && result.action === 'finish_review') {
530
+ await browser?.close();
531
+ }
532
+ }).catch(() => {
533
+ // Errors are surfaced to control commands and stderr; keep the daemon alive for inspection.
534
+ });
535
+ process.once('SIGINT', () => {
536
+ void shutdown();
537
+ });
538
+ process.once('SIGTERM', () => {
539
+ void shutdown();
540
+ });
541
+ process.once('uncaughtException', (error) => {
542
+ void writeInternalResult(args.errorFile, {
543
+ message: error instanceof Error ? error.message : String(error),
544
+ }).finally(() => shutdown());
545
+ });
546
+ await new Promise((resolve) => {
547
+ shutdownResolver = resolve;
548
+ });
549
+ }
550
+ catch (error) {
551
+ await writeInternalResult(args.errorFile, {
552
+ message: error instanceof Error ? error.message : String(error),
553
+ });
554
+ await shutdown();
555
+ throw error;
556
+ }
557
+ }
558
+ async function runReviewCreate(parsed) {
559
+ await ensureNoConflictingActiveSession();
560
+ const staticDir = resolveStaticDir();
561
+ if (!staticDir) {
562
+ throw new Error('Build output not found. Run the package build before starting the CLI.');
563
+ }
564
+ const { mode, targetPath } = await resolveTargetContext(parsed.targetArg ?? '');
565
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'markdown-markdown-daemon-'));
566
+ const readyFile = path.join(tempDir, 'ready.json');
567
+ const errorFile = path.join(tempDir, 'error.json');
568
+ const sessionId = randomUUID();
569
+ const child = spawn(process.execPath, [
570
+ ...buildCurrentCliExecArgs(),
571
+ '__internal-review-host',
572
+ '--session-id',
573
+ sessionId,
574
+ '--root-path',
575
+ targetPath,
576
+ '--mode',
577
+ mode,
578
+ '--browser',
579
+ parsed.browserMode,
580
+ '--cloudflare',
581
+ parsed.cloudflareMode,
582
+ '--static-dir',
583
+ staticDir,
584
+ '--ready-file',
585
+ readyFile,
586
+ '--error-file',
587
+ errorFile,
588
+ ], {
589
+ detached: true,
590
+ stdio: 'ignore',
591
+ });
592
+ child.unref();
593
+ try {
594
+ const ready = await waitForDaemonReady(readyFile, errorFile, child.pid ?? 0);
595
+ process.stdout.write(`${JSON.stringify(ready, null, 2)}\n`);
596
+ }
597
+ finally {
598
+ await rm(tempDir, { force: true, recursive: true });
599
+ }
600
+ }
601
+ async function runReviewWait() {
602
+ const activeSession = await getActiveSession();
603
+ if (!activeSession) {
604
+ throw new Error('No active review session is running.');
605
+ }
606
+ while (true) {
607
+ const result = await readSessionResult(activeSession.sessionId);
608
+ if (result) {
609
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
610
+ return;
611
+ }
612
+ const runtime = await readSessionRuntime(activeSession.sessionId);
613
+ if (runtime && !isPidAlive(runtime.hostPid)) {
614
+ throw new Error('The active review daemon stopped before producing a result.');
615
+ }
616
+ await sleep(500);
617
+ }
618
+ }
619
+ async function requireActiveRuntime() {
620
+ const activeSession = await getActiveSession();
621
+ if (!activeSession) {
622
+ throw new Error('No active review session is running.');
623
+ }
624
+ const runtime = await readSessionRuntime(activeSession.sessionId);
625
+ if (!runtime) {
626
+ throw new Error(`Runtime metadata for active review session ${activeSession.sessionId} is missing.`);
627
+ }
628
+ return {
629
+ runtime,
630
+ sessionId: activeSession.sessionId,
631
+ };
632
+ }
633
+ async function runReviewRefresh() {
634
+ const { runtime } = await requireActiveRuntime();
635
+ const refreshed = await requestControl(runtime, '/api/control/refresh');
636
+ process.stdout.write(`${JSON.stringify(refreshed, null, 2)}\n`);
637
+ }
638
+ async function runReviewClose() {
639
+ const activeSession = await getActiveSession();
640
+ if (!activeSession) {
641
+ process.stdout.write(`${JSON.stringify({ closed: false, reason: 'no_active_session' }, null, 2)}\n`);
642
+ return;
643
+ }
644
+ const runtime = await readSessionRuntime(activeSession.sessionId);
645
+ if (runtime) {
646
+ try {
647
+ await requestControl(runtime, '/api/control/close');
648
+ }
649
+ catch {
650
+ // Fall through to the process kill and state cleanup path.
651
+ }
652
+ if (isPidAlive(runtime.hostPid)) {
653
+ killProcessTree(runtime.hostPid);
654
+ }
655
+ }
656
+ for (let attempt = 0; attempt < 20; attempt += 1) {
657
+ const current = await getActiveSession();
658
+ if (!current) {
659
+ process.stdout.write(`${JSON.stringify({ closed: true, sessionId: activeSession.sessionId }, null, 2)}\n`);
660
+ return;
661
+ }
662
+ await sleep(100);
663
+ }
664
+ await closeSession(activeSession.sessionId);
665
+ process.stdout.write(`${JSON.stringify({ closed: true, forced: true, sessionId: activeSession.sessionId }, null, 2)}\n`);
666
+ }
259
667
  async function main() {
260
- let parsedArgs;
668
+ if (process.argv[2] === '__internal-review-host') {
669
+ const args = parseInternalReviewHostArgs(process.argv.slice(3));
670
+ await runInternalReviewHost(args);
671
+ return;
672
+ }
673
+ let invocation;
261
674
  try {
262
- parsedArgs = parseCliArgs(process.argv.slice(2));
675
+ invocation = parseCliInvocation(process.argv.slice(2));
263
676
  }
264
677
  catch (error) {
265
678
  console.error(error instanceof Error ? error.message : String(error));
266
- printUsage();
679
+ printLegacyUsage();
267
680
  process.exit(1);
268
681
  }
269
- if (parsedArgs.helpRequested) {
270
- printUsage();
682
+ if (invocation.helpRequested) {
683
+ if (invocation.kind === 'review') {
684
+ printReviewUsage();
685
+ }
686
+ else {
687
+ printLegacyUsage();
688
+ }
271
689
  process.exit(0);
272
690
  }
273
- if (!parsedArgs.targetArg) {
274
- printUsage();
275
- process.exit(1);
276
- }
277
- const targetPath = path.resolve(process.cwd(), parsedArgs.targetArg);
278
- const targetStats = await stat(targetPath).catch(() => null);
279
- if (!targetStats) {
280
- throw new Error(`Target path does not exist: ${targetPath}`);
691
+ if (invocation.kind === 'review') {
692
+ switch (invocation.reviewCommand) {
693
+ case 'create':
694
+ await runReviewCreate(invocation);
695
+ return;
696
+ case 'wait':
697
+ await runReviewWait();
698
+ return;
699
+ case 'refresh':
700
+ await runReviewRefresh();
701
+ return;
702
+ case 'close':
703
+ await runReviewClose();
704
+ return;
705
+ default:
706
+ throw new Error(`Unsupported review command: ${String(invocation.reviewCommand)}`);
707
+ }
281
708
  }
282
- const mode = targetStats.isFile() ? 'file' : 'directory';
283
- const files = await discoverMarkdownFiles(targetPath);
284
- if (files.length === 0) {
285
- throw new Error(`No markdown files found under: ${targetPath}`);
709
+ if (!invocation.targetArg) {
710
+ printLegacyUsage();
711
+ process.exit(1);
286
712
  }
713
+ const { files, mode, targetPath } = await resolveTargetContext(invocation.targetArg);
287
714
  const staticDir = resolveStaticDir();
288
715
  if (!staticDir) {
289
716
  throw new Error('Build output not found. Run the package build before starting the CLI.');
290
717
  }
291
- await runReviewSession(parsedArgs, targetPath, mode, files, staticDir);
718
+ await runReviewSession(invocation, targetPath, mode, files, staticDir);
292
719
  }
293
720
  export function isDirectExecutionEntry(argv1, moduleUrl) {
294
721
  if (!argv1) {