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.
- package/README.md +17 -12
- package/dist/assets/{angular-html-CoQle5-T.js → angular-html-vBvPnd6a.js} +1 -1
- package/dist/assets/{angular-ts-BB_A1Lpy.js → angular-ts-CDzF1Z6a.js} +1 -1
- package/dist/assets/{apl-B7_4_FNq.js → apl-Bm9bxvP7.js} +1 -1
- package/dist/assets/{astro-CXVo4VE4.js → astro-RNrNWCYO.js} +1 -1
- package/dist/assets/{blade-BVX41dq7.js → blade-On-NBYUv.js} +1 -1
- package/dist/assets/{c-Dhyi8Nd2.js → c-hRmhFgD0.js} +1 -1
- package/dist/assets/{cobol-CU5oLeZj.js → cobol-CIqc3k2t.js} +1 -1
- package/dist/assets/{coffee-D_FU6gC_.js → coffee-Cd5EyvvS.js} +1 -1
- package/dist/assets/{cpp-CxoYLSi3.js → cpp-zXFAAp_S.js} +1 -1
- package/dist/assets/{crystal-BdgqBN3z.js → crystal-DRYY4cSw.js} +1 -1
- package/dist/assets/{css-wLk1vVwJ.js → css-C4op_MsD.js} +1 -1
- package/dist/assets/{edge-DMylWoGl.js → edge-d3rJkErf.js} +1 -1
- package/dist/assets/{elixir-Djmgto_Z.js → elixir-DPqlJhYM.js} +1 -1
- package/dist/assets/{elm-D7B-eBLA.js → elm-DgU37I8a.js} +1 -1
- package/dist/assets/{erb-g1sso1GW.js → erb-CZVB_ntC.js} +1 -1
- package/dist/assets/{git-rebase-DEAhxutZ.js → git-rebase-hUkltHZG.js} +1 -1
- package/dist/assets/{glimmer-js-CHvGFGMp.js → glimmer-js-5XOT9GQ8.js} +1 -1
- package/dist/assets/{glimmer-ts-DvhL705p.js → glimmer-ts-DT7D0Kfq.js} +1 -1
- package/dist/assets/{glsl-C8mS_t8I.js → glsl-Br4_Eh5O.js} +1 -1
- package/dist/assets/{graphql-DW2pnvqi.js → graphql--BmZ_AYP.js} +1 -1
- package/dist/assets/{hack-DYc1d3he.js → hack-CMdqGDoA.js} +1 -1
- package/dist/assets/{haml-_5OmzH31.js → haml-DLEOHGmg.js} +1 -1
- package/dist/assets/{handlebars-rN-CaXUf.js → handlebars-BfUftoV0.js} +1 -1
- package/dist/assets/{html-derivative-BovOubry.js → html-derivative-D5cZE6C4.js} +1 -1
- package/dist/assets/{html-DQnaJUjl.js → html-uOgx2iYC.js} +1 -1
- package/dist/assets/{http-BKTgwOYc.js → http-cW9c66J_.js} +1 -1
- package/dist/assets/{hurl-ChS3_IgD.js → hurl-BztURO2n.js} +1 -1
- package/dist/assets/{index-BLtRBc2n.css → index-C2p_1iAk.css} +1 -1
- package/dist/assets/{index-CEglkSs-.js → index-DZFM4-3L.js} +11 -11
- package/dist/assets/{java-Dgnyk--z.js → java-Cy4mnKaj.js} +1 -1
- package/dist/assets/{javascript-BfnrwkRN.js → javascript-_Z02t_9Q.js} +1 -1
- package/dist/assets/{jinja-CD2AIvOi.js → jinja-BppBm5gt.js} +1 -1
- package/dist/assets/{jison-DmKNw3Pl.js → jison-Dzns436M.js} +1 -1
- package/dist/assets/{json-CKB5a8aj.js → json-BYkfLOry.js} +1 -1
- package/dist/assets/{jsx-jJXrDRBM.js → jsx-DtAm8NUb.js} +1 -1
- package/dist/assets/{julia-G8vVBW-t.js → julia-CLIMRuh0.js} +1 -1
- package/dist/assets/{just-DMXLrLZL.js → just-DMg47an6.js} +1 -1
- package/dist/assets/{latex-DS3xuxld.js → latex-D05ztCOx.js} +1 -1
- package/dist/assets/{liquid-COQnD_C4.js → liquid-DpImQR4t.js} +1 -1
- package/dist/assets/{lua-BwMKwosd.js → lua--ZHUYQUU.js} +1 -1
- package/dist/assets/{marko-cQCK_WIk.js → marko-Dh3Xi08i.js} +1 -1
- package/dist/assets/{mdc-DQUjY13N.js → mdc-Br6Pt78u.js} +1 -1
- package/dist/assets/{nginx-C4Kv3DJi.js → nginx-CwSU6U6d.js} +1 -1
- package/dist/assets/{nim-ChzLzuYD.js → nim-CBbTwyeq.js} +1 -1
- package/dist/assets/{perl-z5xUiWAY.js → perl-D3jQu3LN.js} +1 -1
- package/dist/assets/{php-BHqm-Xeh.js → php-BMeXBzlU.js} +1 -1
- package/dist/assets/{pug-C9C_Llft.js → pug-BGdY1g5E.js} +1 -1
- package/dist/assets/{qml-dtpuwXJZ.js → qml-DsT5biza.js} +1 -1
- package/dist/assets/{r-BicQMs_c.js → r-CbS9PfN5.js} +1 -1
- package/dist/assets/{razor-D5h00YTZ.js → razor-Bwm8roHi.js} +1 -1
- package/dist/assets/{regexp-CTa8S86v.js → regexp-Bc_9DyAu.js} +1 -1
- package/dist/assets/{rst-Bg-b-7HK.js → rst-Cj1Aii91.js} +1 -1
- package/dist/assets/{ruby-BiWYm056.js → ruby-BPMtFZnc.js} +1 -1
- package/dist/assets/{sas-bJTdfgUz.js → sas-CoOau1t2.js} +1 -1
- package/dist/assets/{scss-0qkNyHr1.js → scss-CvfaI808.js} +1 -1
- package/dist/assets/{shellscript-3sDkDE9z.js → shellscript-B0C-TdLE.js} +1 -1
- package/dist/assets/{shellsession-D4EBhEmz.js → shellsession-UhmLiqcA.js} +1 -1
- package/dist/assets/{soy-DG9hUwLL.js → soy-D419u0RL.js} +1 -1
- package/dist/assets/{sql-B0UoKHkx.js → sql-Do9k0rRn.js} +1 -1
- package/dist/assets/{stata-CsQbuXQp.js → stata-x47J6BC4.js} +1 -1
- package/dist/assets/{surrealql-D63VLxos.js → surrealql-BERQ6pHu.js} +1 -1
- package/dist/assets/{svelte-BApdnAma.js → svelte-dRWkyqKW.js} +1 -1
- package/dist/assets/{templ-B64pN6Lz.js → templ-CTq-KoWN.js} +1 -1
- package/dist/assets/{tex-DR2UVIPF.js → tex-CdRGjyYI.js} +1 -1
- package/dist/assets/{ts-tags-IwhomvQv.js → ts-tags-CgaMqyVb.js} +1 -1
- package/dist/assets/{tsx-DROdxqzS.js → tsx-cpBQKcPG.js} +1 -1
- package/dist/assets/{twig-BtbUqpR4.js → twig-vbs_PM4-.js} +1 -1
- package/dist/assets/{typescript-CHpSjYZL.js → typescript-Cc--nwIo.js} +1 -1
- package/dist/assets/{vue-DCvEu_ug.js → vue-CgHwdBmB.js} +1 -1
- package/dist/assets/{vue-html-BgL4Rf-L.js → vue-html-BHfoMw47.js} +1 -1
- package/dist/assets/{vue-vine-CRNdhPTn.js → vue-vine-_lVRp_NK.js} +1 -1
- package/dist/assets/{xml-CkfPxZuz.js → xml-G4NVmTde.js} +1 -1
- package/dist/assets/{xsl-CteaoFSV.js → xsl-s0so2M1i.js} +1 -1
- package/dist/assets/{yaml-CUr3TDRN.js → yaml-CpsmW05t.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +49 -23
- package/scripts/access-token.mjs +21 -0
- package/scripts/build-diff-data.mjs +260 -39
- package/scripts/cache.mjs +301 -0
- package/scripts/cli-args.mjs +206 -57
- package/scripts/coding-agents.mjs +279 -162
- package/scripts/dev.mjs +218 -0
- package/scripts/doctor.mjs +265 -46
- package/scripts/generate-summaries.mjs +760 -260
- package/scripts/mock-agent.mjs +165 -0
- package/scripts/present.mjs +394 -85
- package/scripts/presenter-runtime.mjs +148 -0
- package/scripts/serve-built.mjs +343 -71
- package/scripts/summary-path.mjs +24 -7
- package/scripts/support-record.mjs +242 -0
package/scripts/dev.mjs
ADDED
|
@@ -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
|
+
}
|
package/scripts/doctor.mjs
CHANGED
|
@@ -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
|
|
25
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
60
|
-
return ` ${mark} ${label} ${version} (${dependency.path})`;
|
|
88
|
+
return ` ${mark} ${label} ${dependency.version || 'version unavailable'} (${dependency.path})`;
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
function
|
|
64
|
-
return
|
|
91
|
+
function stateLine(name, value) {
|
|
92
|
+
return ` ${name.padEnd(14)} ${value}`;
|
|
65
93
|
}
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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 (${
|
|
284
|
+
`Coding agents (${installedCountLabel(installedAgents)})`,
|
|
98
285
|
...agents.map(dependencyLine),
|
|
99
286
|
'',
|
|
100
|
-
'
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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:
|
|
117
|
-
|
|
334
|
+
text: reportText(reportOptions),
|
|
335
|
+
json,
|
|
336
|
+
ready: json.ready,
|
|
118
337
|
};
|
|
119
338
|
}
|