diffsplain 0.5.0 → 0.6.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.
Files changed (91) hide show
  1. package/README.md +17 -12
  2. package/dist/assets/{angular-html-CoQle5-T.js → angular-html-vBvPnd6a.js} +1 -1
  3. package/dist/assets/{angular-ts-BB_A1Lpy.js → angular-ts-CDzF1Z6a.js} +1 -1
  4. package/dist/assets/{apl-B7_4_FNq.js → apl-Bm9bxvP7.js} +1 -1
  5. package/dist/assets/{astro-CXVo4VE4.js → astro-RNrNWCYO.js} +1 -1
  6. package/dist/assets/{blade-BVX41dq7.js → blade-On-NBYUv.js} +1 -1
  7. package/dist/assets/{c-Dhyi8Nd2.js → c-hRmhFgD0.js} +1 -1
  8. package/dist/assets/{cobol-CU5oLeZj.js → cobol-CIqc3k2t.js} +1 -1
  9. package/dist/assets/{coffee-D_FU6gC_.js → coffee-Cd5EyvvS.js} +1 -1
  10. package/dist/assets/{cpp-CxoYLSi3.js → cpp-zXFAAp_S.js} +1 -1
  11. package/dist/assets/{crystal-BdgqBN3z.js → crystal-DRYY4cSw.js} +1 -1
  12. package/dist/assets/{css-wLk1vVwJ.js → css-C4op_MsD.js} +1 -1
  13. package/dist/assets/{edge-DMylWoGl.js → edge-d3rJkErf.js} +1 -1
  14. package/dist/assets/{elixir-Djmgto_Z.js → elixir-DPqlJhYM.js} +1 -1
  15. package/dist/assets/{elm-D7B-eBLA.js → elm-DgU37I8a.js} +1 -1
  16. package/dist/assets/{erb-g1sso1GW.js → erb-CZVB_ntC.js} +1 -1
  17. package/dist/assets/{git-rebase-DEAhxutZ.js → git-rebase-hUkltHZG.js} +1 -1
  18. package/dist/assets/{glimmer-js-CHvGFGMp.js → glimmer-js-5XOT9GQ8.js} +1 -1
  19. package/dist/assets/{glimmer-ts-DvhL705p.js → glimmer-ts-DT7D0Kfq.js} +1 -1
  20. package/dist/assets/{glsl-C8mS_t8I.js → glsl-Br4_Eh5O.js} +1 -1
  21. package/dist/assets/{graphql-DW2pnvqi.js → graphql--BmZ_AYP.js} +1 -1
  22. package/dist/assets/{hack-DYc1d3he.js → hack-CMdqGDoA.js} +1 -1
  23. package/dist/assets/{haml-_5OmzH31.js → haml-DLEOHGmg.js} +1 -1
  24. package/dist/assets/{handlebars-rN-CaXUf.js → handlebars-BfUftoV0.js} +1 -1
  25. package/dist/assets/{html-derivative-BovOubry.js → html-derivative-D5cZE6C4.js} +1 -1
  26. package/dist/assets/{html-DQnaJUjl.js → html-uOgx2iYC.js} +1 -1
  27. package/dist/assets/{http-BKTgwOYc.js → http-cW9c66J_.js} +1 -1
  28. package/dist/assets/{hurl-ChS3_IgD.js → hurl-BztURO2n.js} +1 -1
  29. package/dist/assets/{index-BLtRBc2n.css → index-C2p_1iAk.css} +1 -1
  30. package/dist/assets/{index-CEglkSs-.js → index-DZFM4-3L.js} +11 -11
  31. package/dist/assets/{java-Dgnyk--z.js → java-Cy4mnKaj.js} +1 -1
  32. package/dist/assets/{javascript-BfnrwkRN.js → javascript-_Z02t_9Q.js} +1 -1
  33. package/dist/assets/{jinja-CD2AIvOi.js → jinja-BppBm5gt.js} +1 -1
  34. package/dist/assets/{jison-DmKNw3Pl.js → jison-Dzns436M.js} +1 -1
  35. package/dist/assets/{json-CKB5a8aj.js → json-BYkfLOry.js} +1 -1
  36. package/dist/assets/{jsx-jJXrDRBM.js → jsx-DtAm8NUb.js} +1 -1
  37. package/dist/assets/{julia-G8vVBW-t.js → julia-CLIMRuh0.js} +1 -1
  38. package/dist/assets/{just-DMXLrLZL.js → just-DMg47an6.js} +1 -1
  39. package/dist/assets/{latex-DS3xuxld.js → latex-D05ztCOx.js} +1 -1
  40. package/dist/assets/{liquid-COQnD_C4.js → liquid-DpImQR4t.js} +1 -1
  41. package/dist/assets/{lua-BwMKwosd.js → lua--ZHUYQUU.js} +1 -1
  42. package/dist/assets/{marko-cQCK_WIk.js → marko-Dh3Xi08i.js} +1 -1
  43. package/dist/assets/{mdc-DQUjY13N.js → mdc-Br6Pt78u.js} +1 -1
  44. package/dist/assets/{nginx-C4Kv3DJi.js → nginx-CwSU6U6d.js} +1 -1
  45. package/dist/assets/{nim-ChzLzuYD.js → nim-CBbTwyeq.js} +1 -1
  46. package/dist/assets/{perl-z5xUiWAY.js → perl-D3jQu3LN.js} +1 -1
  47. package/dist/assets/{php-BHqm-Xeh.js → php-BMeXBzlU.js} +1 -1
  48. package/dist/assets/{pug-C9C_Llft.js → pug-BGdY1g5E.js} +1 -1
  49. package/dist/assets/{qml-dtpuwXJZ.js → qml-DsT5biza.js} +1 -1
  50. package/dist/assets/{r-BicQMs_c.js → r-CbS9PfN5.js} +1 -1
  51. package/dist/assets/{razor-D5h00YTZ.js → razor-Bwm8roHi.js} +1 -1
  52. package/dist/assets/{regexp-CTa8S86v.js → regexp-Bc_9DyAu.js} +1 -1
  53. package/dist/assets/{rst-Bg-b-7HK.js → rst-Cj1Aii91.js} +1 -1
  54. package/dist/assets/{ruby-BiWYm056.js → ruby-BPMtFZnc.js} +1 -1
  55. package/dist/assets/{sas-bJTdfgUz.js → sas-CoOau1t2.js} +1 -1
  56. package/dist/assets/{scss-0qkNyHr1.js → scss-CvfaI808.js} +1 -1
  57. package/dist/assets/{shellscript-3sDkDE9z.js → shellscript-B0C-TdLE.js} +1 -1
  58. package/dist/assets/{shellsession-D4EBhEmz.js → shellsession-UhmLiqcA.js} +1 -1
  59. package/dist/assets/{soy-DG9hUwLL.js → soy-D419u0RL.js} +1 -1
  60. package/dist/assets/{sql-B0UoKHkx.js → sql-Do9k0rRn.js} +1 -1
  61. package/dist/assets/{stata-CsQbuXQp.js → stata-x47J6BC4.js} +1 -1
  62. package/dist/assets/{surrealql-D63VLxos.js → surrealql-BERQ6pHu.js} +1 -1
  63. package/dist/assets/{svelte-BApdnAma.js → svelte-dRWkyqKW.js} +1 -1
  64. package/dist/assets/{templ-B64pN6Lz.js → templ-CTq-KoWN.js} +1 -1
  65. package/dist/assets/{tex-DR2UVIPF.js → tex-CdRGjyYI.js} +1 -1
  66. package/dist/assets/{ts-tags-IwhomvQv.js → ts-tags-CgaMqyVb.js} +1 -1
  67. package/dist/assets/{tsx-DROdxqzS.js → tsx-cpBQKcPG.js} +1 -1
  68. package/dist/assets/{twig-BtbUqpR4.js → twig-vbs_PM4-.js} +1 -1
  69. package/dist/assets/{typescript-CHpSjYZL.js → typescript-Cc--nwIo.js} +1 -1
  70. package/dist/assets/{vue-DCvEu_ug.js → vue-CgHwdBmB.js} +1 -1
  71. package/dist/assets/{vue-html-BgL4Rf-L.js → vue-html-BHfoMw47.js} +1 -1
  72. package/dist/assets/{vue-vine-CRNdhPTn.js → vue-vine-_lVRp_NK.js} +1 -1
  73. package/dist/assets/{xml-CkfPxZuz.js → xml-G4NVmTde.js} +1 -1
  74. package/dist/assets/{xsl-CteaoFSV.js → xsl-s0so2M1i.js} +1 -1
  75. package/dist/assets/{yaml-CUr3TDRN.js → yaml-CpsmW05t.js} +1 -1
  76. package/dist/index.html +2 -2
  77. package/package.json +49 -23
  78. package/scripts/access-token.mjs +21 -0
  79. package/scripts/build-diff-data.mjs +260 -39
  80. package/scripts/cache.mjs +301 -0
  81. package/scripts/cli-args.mjs +206 -57
  82. package/scripts/coding-agents.mjs +279 -162
  83. package/scripts/dev.mjs +218 -0
  84. package/scripts/doctor.mjs +265 -46
  85. package/scripts/generate-summaries.mjs +760 -260
  86. package/scripts/mock-agent.mjs +165 -0
  87. package/scripts/present.mjs +394 -85
  88. package/scripts/presenter-runtime.mjs +148 -0
  89. package/scripts/serve-built.mjs +343 -71
  90. package/scripts/summary-path.mjs +24 -7
  91. package/scripts/support-record.mjs +242 -0
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { once } from "node:events";
5
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { createInterface } from "node:readline";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { stripVTControlCharacters } from "node:util";
11
+
12
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
13
+
14
+ function childStatus(code, signal) {
15
+ if (code !== null) return code;
16
+ return signal || "unknown";
17
+ }
18
+
19
+ function childExitCode(code) {
20
+ return code === null ? 1 : code;
21
+ }
22
+
23
+ // fallow-ignore-next-line complexity -- this validates the full dev-only command surface.
24
+ export function parseDevelopmentArgs(rawArgs, callerDirectory = process.cwd()) {
25
+ const [mode, ...args] = rawArgs;
26
+ if (!["live", "mock"].includes(mode)) {
27
+ throw new Error('Use "live" or "mock".');
28
+ }
29
+ const values = new Map();
30
+ for (let index = 0; index < args.length; index += 1) {
31
+ const name = args[index];
32
+ if (!["--repo", "--fixture", "--port", "--delay"].includes(name)) {
33
+ throw new Error(`Unknown option: ${name}`);
34
+ }
35
+ const value = args[index + 1];
36
+ if (!value || value.startsWith("--")) throw new Error(`${name} needs a value`);
37
+ if (values.has(name)) throw new Error(`${name} was passed more than once`);
38
+ values.set(name, value);
39
+ index += 1;
40
+ }
41
+ if (mode === "live" && values.has("--fixture")) {
42
+ throw new Error("--fixture is only available in mock mode");
43
+ }
44
+ if (mode === "mock" && values.has("--repo")) {
45
+ throw new Error("--repo is only available in live mode");
46
+ }
47
+ const port = values.get("--port") || "2299";
48
+ if (!/^\d+$/.test(port) || Number(port) > 65_535) {
49
+ throw new Error("--port must be a number from 0 to 65535");
50
+ }
51
+ const delay = values.get("--delay") || "750";
52
+ if (!/^\d+$/.test(delay) || Number(delay) < 10 || Number(delay) > 60_000) {
53
+ throw new Error("--delay must be a number from 10 to 60000");
54
+ }
55
+ return {
56
+ mode,
57
+ port,
58
+ delay,
59
+ repo: resolve(callerDirectory, values.get("--repo") || callerDirectory),
60
+ fixture: resolve(
61
+ callerDirectory,
62
+ values.get("--fixture") || resolve(root, "public/demo-diff-data.json"),
63
+ ),
64
+ };
65
+ }
66
+
67
+ export function developmentCommands(options, output) {
68
+ const clientReady = join(dirname(output), "client-ready");
69
+ const watcher =
70
+ options.mode === "live"
71
+ ? {
72
+ command: process.execPath,
73
+ args: [
74
+ resolve(root, "scripts/build-diff-data.mjs"),
75
+ "--repo",
76
+ options.repo,
77
+ "--worktree",
78
+ "--watch",
79
+ "--watch-content",
80
+ "--output",
81
+ output,
82
+ ],
83
+ }
84
+ : {
85
+ command: process.execPath,
86
+ args: [
87
+ resolve(root, "scripts/mock-agent.mjs"),
88
+ "--fixture",
89
+ options.fixture,
90
+ "--delay",
91
+ options.delay,
92
+ "--start-file",
93
+ join(dirname(output), "page-ready"),
94
+ "--advance-file",
95
+ clientReady,
96
+ "--output",
97
+ output,
98
+ ],
99
+ };
100
+ return {
101
+ watcher,
102
+ vite: {
103
+ command: process.execPath,
104
+ args: [
105
+ resolve(root, "node_modules/vite/bin/vite.js"),
106
+ "--host",
107
+ "127.0.0.1",
108
+ "--port",
109
+ options.port,
110
+ ],
111
+ env: {
112
+ DIFFSPLAIN_LIVE_OUTPUT: output,
113
+ ...(options.mode === "mock"
114
+ ? { DIFFSPLAIN_LIVE_ACK: clientReady }
115
+ : {}),
116
+ },
117
+ },
118
+ };
119
+ }
120
+
121
+ export function isDevelopmentEntry(argument, moduleUrl) {
122
+ return Boolean(argument) && pathToFileURL(resolve(argument)).href === moduleUrl;
123
+ }
124
+
125
+ export function viteServerUrl(output) {
126
+ return stripVTControlCharacters(output).match(
127
+ /Local:\s+(http:\/\/[^\s]+)/,
128
+ )?.[1];
129
+ }
130
+
131
+ // fallow-ignore-next-line complexity -- this supervisor owns both child lifecycles.
132
+ async function main() {
133
+ const options = parseDevelopmentArgs(process.argv.slice(2));
134
+ if (!existsSync(options.mode === "live" ? options.repo : options.fixture)) {
135
+ throw new Error(
136
+ options.mode === "live"
137
+ ? `Repo does not exist: ${options.repo}`
138
+ : `Fixture does not exist: ${options.fixture}`,
139
+ );
140
+ }
141
+ const runtimeDirectory = mkdtempSync(join(tmpdir(), "diffsplain-dev-"));
142
+ const output = join(runtimeDirectory, "diff-data.json");
143
+ const { watcher, vite } = developmentCommands(options, output);
144
+ let closing = false;
145
+ let pageReady = false;
146
+ const children = new Set();
147
+ const stop = async (code = 0) => {
148
+ if (closing) return;
149
+ closing = true;
150
+ for (const child of children) {
151
+ if (!child.killed) child.kill("SIGTERM");
152
+ }
153
+ await Promise.all(
154
+ [...children].map((child) =>
155
+ child.exitCode === null ? once(child, "exit") : Promise.resolve(),
156
+ ),
157
+ );
158
+ rmSync(runtimeDirectory, { recursive: true, force: true });
159
+ process.exitCode = code;
160
+ };
161
+ process.on("SIGINT", () => void stop());
162
+ process.on("SIGTERM", () => void stop());
163
+
164
+ const watchChild = spawn(watcher.command, watcher.args, {
165
+ cwd: root,
166
+ stdio: ["ignore", "pipe", "pipe"],
167
+ });
168
+ children.add(watchChild);
169
+ const viteChild = spawn(vite.command, vite.args, {
170
+ cwd: root,
171
+ env: { ...process.env, ...vite.env },
172
+ stdio: ["ignore", "pipe", "pipe"],
173
+ });
174
+ children.add(viteChild);
175
+ for (const [name, child] of [
176
+ ["watcher", watchChild],
177
+ ["Vite", viteChild],
178
+ ]) {
179
+ child.on("error", (error) => {
180
+ if (!closing) {
181
+ console.error(`Could not start ${name}: ${error.message}`);
182
+ void stop(1);
183
+ }
184
+ });
185
+ child.on("exit", (code, signal) => {
186
+ children.delete(child);
187
+ if (!closing) {
188
+ console.error(`${name} stopped (${childStatus(code, signal)}).`);
189
+ void stop(childExitCode(code));
190
+ }
191
+ });
192
+ }
193
+ function relayWatcherLine(line) {
194
+ if (line) console.log(`[${options.mode}] ${line}`);
195
+ }
196
+ for (const stream of [watchChild.stdout, watchChild.stderr]) {
197
+ createInterface({ input: stream }).on("line", relayWatcherLine);
198
+ }
199
+ function relayViteLine(line) {
200
+ const url = viteServerUrl(line);
201
+ if (url && !pageReady) {
202
+ pageReady = true;
203
+ writeFileSync(join(runtimeDirectory, "page-ready"), "ready\n");
204
+ console.log(`Diffsplain ${options.mode} ready: ${url}`);
205
+ }
206
+ if (line) console.log(`[vite] ${line}`);
207
+ }
208
+ for (const stream of [viteChild.stdout, viteChild.stderr]) {
209
+ createInterface({ input: stream }).on("line", relayViteLine);
210
+ }
211
+ }
212
+
213
+ if (isDevelopmentEntry(process.argv[1], import.meta.url)) {
214
+ main().catch((error) => {
215
+ console.error(error instanceof Error ? error.message : String(error));
216
+ process.exitCode = 1;
217
+ });
218
+ }
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import {
3
+ agentDisabledReason,
3
4
  codingAgentBinary,
4
5
  codingAgents,
5
6
  findCommand,
@@ -12,6 +13,7 @@ const agentLabels = {
12
13
  cursor: 'Cursor',
13
14
  opencode: 'OpenCode',
14
15
  };
16
+ const minimumNodeVersion = [22, 13, 0];
15
17
 
16
18
  function firstLine(value) {
17
19
  return value
@@ -21,99 +23,316 @@ function firstLine(value) {
21
23
  .find(Boolean);
22
24
  }
23
25
 
24
- function commandVersion(command) {
25
- const result = spawnSync(command, ['--version'], {
26
+ function commandResult(command, args, env) {
27
+ return spawnSync(command, args, {
26
28
  encoding: 'utf8',
29
+ env,
27
30
  timeout: 5_000,
28
31
  windowsHide: true,
29
32
  });
33
+ }
34
+
35
+ function commandVersion(command, env) {
36
+ const result = commandResult(command, ['--version'], env);
30
37
  if (result.error || result.status !== 0) return undefined;
31
38
  return firstLine(`${result.stdout || ''}\n${result.stderr || ''}`);
32
39
  }
33
40
 
34
- async function inspectDependency(
35
- label,
36
- command,
37
- {
38
- env,
39
- platform,
40
- },
41
- ) {
41
+ function commandSucceeds(command, args, env) {
42
+ const result = commandResult(command, args, env);
43
+ return !result.error && result.status === 0;
44
+ }
45
+
46
+ function parseNodeVersion(version) {
47
+ const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)/);
48
+ if (!match) return undefined;
49
+ return match.slice(1).map(Number);
50
+ }
51
+
52
+ function supportsNodeVersion(version) {
53
+ const current = parseNodeVersion(version);
54
+ if (!current) return false;
55
+ for (const [index, part] of current.entries()) {
56
+ if (part !== minimumNodeVersion[index]) {
57
+ return part > minimumNodeVersion[index];
58
+ }
59
+ }
60
+ return true;
61
+ }
62
+
63
+ async function inspectDependency(label, command, { env, platform }) {
42
64
  const path = await findCommand(command, { env, platform });
43
- if (!path) return { label, command, installed: false };
65
+ if (!path) {
66
+ return { label, command, installed: false, compatible: 'not-checked' };
67
+ }
68
+ const version = commandVersion(path, env);
44
69
  return {
45
70
  label,
46
71
  command,
47
72
  installed: true,
48
73
  path,
49
- version: commandVersion(path),
74
+ version,
75
+ compatible: version ? 'not-verified' : 'unknown',
50
76
  };
51
77
  }
52
78
 
53
79
  function dependencyLine(dependency) {
54
80
  const label = dependency.label.padEnd(9);
81
+ if (dependency.disabled) {
82
+ return ` ! ${label} disabled (${dependency.disabled})`;
83
+ }
55
84
  if (!dependency.installed) {
56
85
  return ` ✗ ${label} not found (${dependency.command})`;
57
86
  }
58
87
  const mark = dependency.version ? '✓' : '!';
59
- const version = dependency.version || 'version unavailable';
60
- return ` ${mark} ${label} ${version} (${dependency.path})`;
88
+ return ` ${mark} ${label} ${dependency.version || 'version unavailable'} (${dependency.path})`;
61
89
  }
62
90
 
63
- function joinedAgentNames(agents) {
64
- return agents.map((agent) => agent.label).join(', ');
91
+ function stateLine(name, value) {
92
+ return ` ${name.padEnd(14)} ${value}`;
65
93
  }
66
94
 
67
- export async function doctorReport({
68
- env = process.env,
69
- platform = process.platform,
70
- architecture = process.arch,
71
- nodeVersion = process.version,
72
- nodePath = process.execPath,
73
- } = {}) {
95
+ function capabilityLines(name, capability) {
96
+ return [
97
+ ` ${name}`,
98
+ stateLine(
99
+ 'installed',
100
+ typeof capability.installed === 'boolean'
101
+ ? yesNo(capability.installed)
102
+ : capability.installed,
103
+ ),
104
+ stateLine('compatible', capability.compatible),
105
+ stateLine('authenticated', capability.authenticated),
106
+ stateLine('smoke test', capability.smokeTest),
107
+ ];
108
+ }
109
+
110
+ function yesNo(value) {
111
+ return value ? 'yes' : 'no';
112
+ }
113
+
114
+ function localSmokeTest(dependency, deep, env) {
115
+ if (!deep) return 'not-run';
116
+ if (!dependency.installed) return 'not-run';
117
+ return commandSucceeds(dependency.path, ['--help'], env)
118
+ ? 'passed (local command only)'
119
+ : 'failed (local command only)';
120
+ }
121
+
122
+ function providerCapability(agent, deep, env) {
123
+ return {
124
+ installed: agent.installed,
125
+ compatible: agent.compatible,
126
+ authenticated: 'not-checked',
127
+ smokeTest: localSmokeTest(agent, deep, env),
128
+ };
129
+ }
130
+
131
+ async function inspectDependencies(env, platform) {
74
132
  const [git, gh, ...agents] = await Promise.all([
75
133
  inspectDependency('Git', 'git', { env, platform }),
76
134
  inspectDependency('gh', 'gh', { env, platform }),
77
- ...codingAgents.map((agent) =>
78
- inspectDependency(
135
+ ...codingAgents.map((agent) => {
136
+ const disabled = agentDisabledReason(agent);
137
+ if (disabled) {
138
+ return Promise.resolve({
139
+ label: agentLabels[agent],
140
+ command: codingAgentBinary(agent, { env }),
141
+ installed: false,
142
+ compatible: 'no',
143
+ disabled,
144
+ });
145
+ }
146
+ return inspectDependency(
79
147
  agentLabels[agent],
80
148
  codingAgentBinary(agent, { env }),
81
149
  { env, platform },
82
- ),
83
- ),
150
+ );
151
+ }),
84
152
  ]);
85
- const installedAgents = agents.filter((agent) => agent.installed);
86
- const agentCount = installedAgents.length
153
+ return { git, gh, agents };
154
+ }
155
+
156
+ function coreInstalled(git, nodePath) {
157
+ return git.installed && Boolean(nodePath);
158
+ }
159
+
160
+ function coreReady(git, nodeSupported) {
161
+ return git.installed && Boolean(git.version) && nodeSupported;
162
+ }
163
+
164
+ function coreReviewCapability(git, nodeSupported, nodePath, deep, env) {
165
+ const ready = coreReady(git, nodeSupported);
166
+ return {
167
+ installed: coreInstalled(git, nodePath),
168
+ compatible: yesNo(ready),
169
+ authenticated: 'not-required',
170
+ smokeTest: localSmokeTest(git, deep, env),
171
+ ready,
172
+ };
173
+ }
174
+
175
+ function agentNoteCapabilities(agents, deep, env) {
176
+ return Object.fromEntries(
177
+ codingAgents.map((agent, index) => [
178
+ agent,
179
+ providerCapability(agents[index], deep, env),
180
+ ]),
181
+ );
182
+ }
183
+
184
+ function authenticationState(gh, env) {
185
+ if (!gh.installed) return 'not-checked';
186
+ return commandSucceeds(gh.path, ['auth', 'status', '--active'], env)
187
+ ? 'passed'
188
+ : 'failed';
189
+ }
190
+
191
+ function pullRequestCapability(gh, deep, env) {
192
+ return {
193
+ installed: gh.installed,
194
+ compatible: gh.compatible,
195
+ authenticated: authenticationState(gh, env),
196
+ smokeTest: localSmokeTest(gh, deep, env),
197
+ };
198
+ }
199
+
200
+ function machineReport({
201
+ deep,
202
+ platform,
203
+ architecture,
204
+ nodeVersion,
205
+ nodePath,
206
+ nodeSupported,
207
+ git,
208
+ gh,
209
+ agents,
210
+ capabilities,
211
+ }) {
212
+ return {
213
+ schemaVersion: 1,
214
+ deep,
215
+ platform: { name: platform, architecture },
216
+ dependencies: {
217
+ node: {
218
+ installed: true,
219
+ version: nodeVersion,
220
+ path: nodePath,
221
+ compatible: nodeSupported ? 'yes' : 'no',
222
+ },
223
+ git,
224
+ gh,
225
+ agents: Object.fromEntries(
226
+ codingAgents.map((agent, index) => [agent, agents[index]]),
227
+ ),
228
+ },
229
+ capabilities,
230
+ ready: capabilities.coreReview.ready,
231
+ };
232
+ }
233
+
234
+ function installedCountLabel(installedAgents) {
235
+ return installedAgents.length
87
236
  ? `${installedAgents.length} installed`
88
237
  : 'none installed';
238
+ }
239
+
240
+ function agentCapabilityLines(agent, capabilities) {
241
+ return capabilityLines(
242
+ `Agent notes: ${agentLabels[agent]}`,
243
+ capabilities.agentNotes[agent],
244
+ );
245
+ }
246
+
247
+ function appendDoctorNotes(lines, installedAgents, deep) {
248
+ if (!installedAgents.length) {
249
+ lines.push(' No agent is required for a plain local review; use --no-agent.');
250
+ }
251
+ if (!deep) {
252
+ lines.push(
253
+ ' Smoke tests were not run. Use doctor --deep for local command checks.',
254
+ );
255
+ }
256
+ }
257
+
258
+ function reportText({
259
+ deep,
260
+ platform,
261
+ architecture,
262
+ nodeVersion,
263
+ nodePath,
264
+ git,
265
+ gh,
266
+ agents,
267
+ capabilities,
268
+ }) {
269
+ const installedAgents = agents.filter((agent) => agent.installed);
89
270
  const lines = [
90
271
  'Diffsplain doctor',
91
272
  '',
92
273
  'Dependencies',
93
- ` ✓ ${'Node'.padEnd(9)} ${nodeVersion} (${nodePath})`,
274
+ dependencyLine({
275
+ label: 'Node',
276
+ command: 'node',
277
+ installed: true,
278
+ path: nodePath,
279
+ version: nodeVersion,
280
+ }),
94
281
  dependencyLine(git),
95
282
  dependencyLine(gh),
96
283
  '',
97
- `Coding agents (${agentCount})`,
284
+ `Coding agents (${installedCountLabel(installedAgents)})`,
98
285
  ...agents.map(dependencyLine),
99
286
  '',
100
- 'Status',
101
- git.installed
102
- ? ' ✓ Git reviews are ready.'
103
- : ' ✗ Git is not installed.',
104
- installedAgents.length
105
- ? ` ✓ Agent notes are ready with ${joinedAgentNames(installedAgents)}.`
106
- : ' ✗ No supported coding agent is installed.',
107
- gh.installed
108
- ? 'Pull request lookup is ready with gh.'
109
- : ' ✗ gh is not installed; pull request lookup is unavailable.',
287
+ 'Capabilities',
288
+ ...capabilityLines('Plain local review', {
289
+ ...capabilities.coreReview,
290
+ installed: yesNo(capabilities.coreReview.installed),
291
+ }),
292
+ ...codingAgents.flatMap((agent) =>
293
+ agentCapabilityLines(agent, capabilities),
294
+ ),
295
+ ...capabilityLines('Pull request lookup', {
296
+ ...capabilities.pullRequestLookup,
297
+ installed: yesNo(capabilities.pullRequestLookup.installed),
298
+ }),
110
299
  ` Platform: ${platform} ${architecture}`,
111
300
  ];
112
- if (!installedAgents.length) {
113
- lines.push(' Use --no-agent to run without agent notes.');
114
- }
301
+ appendDoctorNotes(lines, installedAgents, deep);
302
+ return lines.join('\n');
303
+ }
304
+
305
+ export async function doctorReport({
306
+ env = process.env,
307
+ platform = process.platform,
308
+ architecture = process.arch,
309
+ nodeVersion = process.version,
310
+ nodePath = process.execPath,
311
+ deep = false,
312
+ } = {}) {
313
+ const { git, gh, agents } = await inspectDependencies(env, platform);
314
+ const nodeSupported = supportsNodeVersion(nodeVersion);
315
+ const capabilities = {
316
+ coreReview: coreReviewCapability(git, nodeSupported, nodePath, deep, env),
317
+ agentNotes: agentNoteCapabilities(agents, deep, env),
318
+ pullRequestLookup: pullRequestCapability(gh, deep, env),
319
+ };
320
+ const reportOptions = {
321
+ deep,
322
+ platform,
323
+ architecture,
324
+ nodeVersion,
325
+ nodePath,
326
+ nodeSupported,
327
+ git,
328
+ gh,
329
+ agents,
330
+ capabilities,
331
+ };
332
+ const json = machineReport(reportOptions);
115
333
  return {
116
- text: lines.join('\n'),
117
- ready: git.installed && installedAgents.length > 0,
334
+ text: reportText(reportOptions),
335
+ json,
336
+ ready: json.ready,
118
337
  };
119
338
  }