bun-doctor 0.0.1
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/LICENSE +21 -0
- package/README.md +63 -0
- package/action.yml +52 -0
- package/bin/bun-doctor.js +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +243 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +129 -0
- package/dist/index.mjs +2 -0
- package/dist/scan-BVcJTreL.mjs +749 -0
- package/dist/scan-BVcJTreL.mjs.map +1 -0
- package/docs/compat-db.md +30 -0
- package/docs/rule-spec.md +57 -0
- package/package.json +61 -0
- package/skills/bun-doctor/SKILL.md +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Bun Doctor
|
|
2
|
+
|
|
3
|
+
Diagnose Node-to-Bun migration readiness for JavaScript and TypeScript projects.
|
|
4
|
+
|
|
5
|
+
`bun-doctor` answers one question:
|
|
6
|
+
|
|
7
|
+
> Can this repo safely move to Bun, and what exact changes get it there?
|
|
8
|
+
|
|
9
|
+
It scans package manager state, lockfiles, Bun config, TypeScript config, CI workflows, dependency risk, and a first pass of Bun-specific code risks. The output is a 0-100 Bun Readiness score grouped into Blockers, Risks, Migration work, and Bun wins.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y bun-doctor@latest .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Local development from this repo:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun install
|
|
21
|
+
bun run build
|
|
22
|
+
node dist/cli.mjs . --verbose
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## CLI
|
|
26
|
+
|
|
27
|
+
```txt
|
|
28
|
+
Usage: bun-doctor [directory] [options]
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--json output a structured JSON report
|
|
32
|
+
--score output only the numeric score
|
|
33
|
+
--verbose show every diagnostic
|
|
34
|
+
--no-package skip package/config/dependency checks
|
|
35
|
+
--no-code skip source code checks
|
|
36
|
+
--fail-on <level> exit non-zero on blocker, risk, migration, or none
|
|
37
|
+
-v, --version print version
|
|
38
|
+
-h, --help print help
|
|
39
|
+
|
|
40
|
+
Commands:
|
|
41
|
+
bun-doctor install install the bun-doctor agent skill
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Scoring
|
|
45
|
+
|
|
46
|
+
The score starts at 100 and subtracts for unique triggered rules:
|
|
47
|
+
|
|
48
|
+
- Blocker: 12 points
|
|
49
|
+
- Risk: 5 points
|
|
50
|
+
- Migration work: 2 points
|
|
51
|
+
- Bun win: 0 points
|
|
52
|
+
|
|
53
|
+
Bun wins are shown as opportunities but do not lower readiness.
|
|
54
|
+
|
|
55
|
+
## Rule Sources
|
|
56
|
+
|
|
57
|
+
Every diagnostic has at least one verifiable source: Bun docs, compatibility docs, or an issue/test link in the compatibility database. No source means no rule.
|
|
58
|
+
|
|
59
|
+
## Roadmap
|
|
60
|
+
|
|
61
|
+
- MVP: CLI, JSON, score, config/package rules, code-risk scans, compatibility DB v0.
|
|
62
|
+
- v1.0: public eval harness that verifies compatibility DB entries across Bun versions and platforms.
|
|
63
|
+
- Later: editor/linter plugin once rules prove useful in real migrations.
|
package/action.yml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: "Bun Doctor"
|
|
2
|
+
description: "Scan JavaScript and TypeScript projects for Bun migration readiness"
|
|
3
|
+
branding:
|
|
4
|
+
icon: "activity"
|
|
5
|
+
color: "orange"
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
directory:
|
|
9
|
+
description: "Project directory to scan"
|
|
10
|
+
default: "."
|
|
11
|
+
verbose:
|
|
12
|
+
description: "Show all diagnostics"
|
|
13
|
+
default: "true"
|
|
14
|
+
fail-on:
|
|
15
|
+
description: "Exit with error code on diagnostics: blocker, risk, migration, none"
|
|
16
|
+
default: "blocker"
|
|
17
|
+
node-version:
|
|
18
|
+
description: "Node.js version to use for running bun-doctor"
|
|
19
|
+
default: "22"
|
|
20
|
+
|
|
21
|
+
outputs:
|
|
22
|
+
score:
|
|
23
|
+
description: "Bun Readiness score (0-100)"
|
|
24
|
+
value: ${{ steps.score.outputs.score }}
|
|
25
|
+
|
|
26
|
+
runs:
|
|
27
|
+
using: "composite"
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: ${{ inputs.node-version }}
|
|
32
|
+
|
|
33
|
+
- shell: bash
|
|
34
|
+
env:
|
|
35
|
+
INPUT_DIRECTORY: ${{ inputs.directory }}
|
|
36
|
+
INPUT_VERBOSE: ${{ inputs.verbose }}
|
|
37
|
+
INPUT_FAIL_ON: ${{ inputs.fail-on }}
|
|
38
|
+
run: |
|
|
39
|
+
FLAGS=("--fail-on" "$INPUT_FAIL_ON")
|
|
40
|
+
if [ "$INPUT_VERBOSE" = "true" ]; then FLAGS+=("--verbose"); fi
|
|
41
|
+
npx -y bun-doctor@latest "$INPUT_DIRECTORY" "${FLAGS[@]}"
|
|
42
|
+
|
|
43
|
+
- id: score
|
|
44
|
+
if: always()
|
|
45
|
+
shell: bash
|
|
46
|
+
env:
|
|
47
|
+
INPUT_DIRECTORY: ${{ inputs.directory }}
|
|
48
|
+
run: |
|
|
49
|
+
SCORE=$(npx -y bun-doctor@latest "$INPUT_DIRECTORY" --score --fail-on none 2>/dev/null | tail -1 | tr -d '[:space:]') || true
|
|
50
|
+
if [[ -n "$SCORE" && "$SCORE" =~ ^[0-9]+$ ]]; then
|
|
51
|
+
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
|
|
52
|
+
fi
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as VERSION, i as toRelativePath, t as scan } from "./scan-BVcJTreL.mjs";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
//#region src/install-skill.ts
|
|
7
|
+
const SKILL_CONTENT = `---
|
|
8
|
+
name: bun-doctor
|
|
9
|
+
description: Use after making dependency, CI, package manager, test runner, or Node runtime changes in a project that uses or is migrating to Bun. Checks Bun readiness and migration risk.
|
|
10
|
+
version: "1.0.0"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Bun Doctor
|
|
14
|
+
|
|
15
|
+
Run \`npx -y bun-doctor@latest . --verbose\` after Bun migration changes and fix blockers before switching CI or runtime.
|
|
16
|
+
`;
|
|
17
|
+
const installSkill = (options) => {
|
|
18
|
+
const skillDirectory = path.join(options.directory, ".agents", "skills", "bun-doctor");
|
|
19
|
+
const skillPath = path.join(skillDirectory, "SKILL.md");
|
|
20
|
+
if (options.dryRun) return skillPath;
|
|
21
|
+
fs.mkdirSync(skillDirectory, { recursive: true });
|
|
22
|
+
fs.writeFileSync(skillPath, SKILL_CONTENT, "utf8");
|
|
23
|
+
return skillPath;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/report.ts
|
|
27
|
+
const CATEGORY_ORDER = [
|
|
28
|
+
"Blockers",
|
|
29
|
+
"Risks",
|
|
30
|
+
"Migration work",
|
|
31
|
+
"Bun wins"
|
|
32
|
+
];
|
|
33
|
+
const LEVEL_SYMBOL = {
|
|
34
|
+
blocker: "x",
|
|
35
|
+
risk: "!",
|
|
36
|
+
migration: "~",
|
|
37
|
+
win: "+"
|
|
38
|
+
};
|
|
39
|
+
const groupByCategory = (diagnostics) => {
|
|
40
|
+
const groups = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const diagnostic of diagnostics) {
|
|
42
|
+
const existing = groups.get(diagnostic.category) ?? [];
|
|
43
|
+
existing.push(diagnostic);
|
|
44
|
+
groups.set(diagnostic.category, existing);
|
|
45
|
+
}
|
|
46
|
+
return groups;
|
|
47
|
+
};
|
|
48
|
+
const formatLocation = (diagnostic, rootDirectory) => {
|
|
49
|
+
const relativePath = toRelativePath(path.resolve(diagnostic.filePath), rootDirectory);
|
|
50
|
+
return diagnostic.line > 0 ? `${relativePath}:${diagnostic.line}` : relativePath;
|
|
51
|
+
};
|
|
52
|
+
const formatTextReport = (result, verbose) => {
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push(`bun-doctor`);
|
|
55
|
+
lines.push(`Project: ${result.project.packageName}`);
|
|
56
|
+
lines.push(`Bun Readiness: ${result.score.score}/100 (${result.score.label})`);
|
|
57
|
+
lines.push(`Findings: ${result.summary.blockers} blockers, ${result.summary.risks} risks, ${result.summary.migrations} migration, ${result.summary.wins} wins`);
|
|
58
|
+
lines.push("");
|
|
59
|
+
if (result.diagnostics.length === 0) {
|
|
60
|
+
lines.push("No Bun migration findings.");
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
const groups = groupByCategory(result.diagnostics);
|
|
64
|
+
for (const category of CATEGORY_ORDER) {
|
|
65
|
+
const diagnostics = groups.get(category) ?? [];
|
|
66
|
+
if (diagnostics.length === 0) continue;
|
|
67
|
+
lines.push(`${category} (${diagnostics.length})`);
|
|
68
|
+
const shownDiagnostics = verbose ? diagnostics : diagnostics.slice(0, 3);
|
|
69
|
+
for (const diagnostic of shownDiagnostics) {
|
|
70
|
+
lines.push(` ${LEVEL_SYMBOL[diagnostic.level]} ${diagnostic.title} [${diagnostic.ruleId}]`);
|
|
71
|
+
lines.push(` ${diagnostic.message}`);
|
|
72
|
+
if (diagnostic.help) lines.push(` ${diagnostic.help}`);
|
|
73
|
+
lines.push(` ${formatLocation(diagnostic, result.project.rootDirectory)}`);
|
|
74
|
+
lines.push(` Source: ${diagnostic.sources[0]}`);
|
|
75
|
+
}
|
|
76
|
+
if (!verbose && diagnostics.length > shownDiagnostics.length) lines.push(` ... ${diagnostics.length - shownDiagnostics.length} more. Re-run with --verbose.`);
|
|
77
|
+
lines.push("");
|
|
78
|
+
}
|
|
79
|
+
return lines.join("\n").trimEnd();
|
|
80
|
+
};
|
|
81
|
+
const toJsonReport = (result) => ({
|
|
82
|
+
schemaVersion: 1,
|
|
83
|
+
ok: true,
|
|
84
|
+
score: result.score,
|
|
85
|
+
summary: result.summary,
|
|
86
|
+
project: {
|
|
87
|
+
name: result.project.packageName,
|
|
88
|
+
rootDirectory: result.project.rootDirectory,
|
|
89
|
+
packageJsonPath: result.project.packageJsonPath,
|
|
90
|
+
lockfiles: result.project.lockfiles,
|
|
91
|
+
legacyLockfiles: result.project.legacyLockfiles
|
|
92
|
+
},
|
|
93
|
+
diagnostics: result.diagnostics
|
|
94
|
+
});
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/cli.ts
|
|
97
|
+
const VALID_FAIL_ON_LEVELS = new Set([
|
|
98
|
+
"blocker",
|
|
99
|
+
"risk",
|
|
100
|
+
"migration",
|
|
101
|
+
"win",
|
|
102
|
+
"none"
|
|
103
|
+
]);
|
|
104
|
+
const HELP_TEXT = `Usage: bun-doctor [directory] [options]
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--json output a structured JSON report
|
|
108
|
+
--score output only the numeric score
|
|
109
|
+
--verbose show every diagnostic
|
|
110
|
+
--no-package skip package/config/dependency checks
|
|
111
|
+
--no-code skip source code checks
|
|
112
|
+
--fail-on <level> exit non-zero on blocker, risk, migration, or none
|
|
113
|
+
-v, --version print version
|
|
114
|
+
-h, --help print help
|
|
115
|
+
|
|
116
|
+
Commands:
|
|
117
|
+
bun-doctor install [directory] [--dry-run]
|
|
118
|
+
`;
|
|
119
|
+
const shouldFail = (levels, failOn) => {
|
|
120
|
+
if (failOn === "none") return false;
|
|
121
|
+
if (failOn === "win") return levels.size > 0;
|
|
122
|
+
if (failOn === "migration") return levels.has("blocker") || levels.has("risk") || levels.has("migration");
|
|
123
|
+
if (failOn === "risk") return levels.has("blocker") || levels.has("risk");
|
|
124
|
+
return levels.has("blocker");
|
|
125
|
+
};
|
|
126
|
+
const parseFailOn = (value) => {
|
|
127
|
+
if (!value) return "blocker";
|
|
128
|
+
if (VALID_FAIL_ON_LEVELS.has(value)) return value;
|
|
129
|
+
throw new Error(`Invalid --fail-on value: ${value}. Expected blocker, risk, migration, win, or none.`);
|
|
130
|
+
};
|
|
131
|
+
const parseCli = (argv) => {
|
|
132
|
+
const parsed = parseArgs({
|
|
133
|
+
args: argv,
|
|
134
|
+
allowPositionals: true,
|
|
135
|
+
options: {
|
|
136
|
+
json: {
|
|
137
|
+
type: "boolean",
|
|
138
|
+
default: false
|
|
139
|
+
},
|
|
140
|
+
score: {
|
|
141
|
+
type: "boolean",
|
|
142
|
+
default: false
|
|
143
|
+
},
|
|
144
|
+
verbose: {
|
|
145
|
+
type: "boolean",
|
|
146
|
+
default: false
|
|
147
|
+
},
|
|
148
|
+
"no-package": {
|
|
149
|
+
type: "boolean",
|
|
150
|
+
default: false
|
|
151
|
+
},
|
|
152
|
+
"no-code": {
|
|
153
|
+
type: "boolean",
|
|
154
|
+
default: false
|
|
155
|
+
},
|
|
156
|
+
"fail-on": {
|
|
157
|
+
type: "string",
|
|
158
|
+
default: "blocker"
|
|
159
|
+
},
|
|
160
|
+
version: {
|
|
161
|
+
type: "boolean",
|
|
162
|
+
short: "v",
|
|
163
|
+
default: false
|
|
164
|
+
},
|
|
165
|
+
help: {
|
|
166
|
+
type: "boolean",
|
|
167
|
+
short: "h",
|
|
168
|
+
default: false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (parsed.values.help) {
|
|
173
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
if (parsed.values.version) {
|
|
177
|
+
process.stdout.write(`${VERSION}\n`);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
directory: path.resolve(parsed.positionals[0] ?? "."),
|
|
182
|
+
flags: {
|
|
183
|
+
json: Boolean(parsed.values.json),
|
|
184
|
+
score: Boolean(parsed.values.score),
|
|
185
|
+
verbose: Boolean(parsed.values.verbose),
|
|
186
|
+
packageChecks: !parsed.values["no-package"],
|
|
187
|
+
codeChecks: !parsed.values["no-code"],
|
|
188
|
+
failOn: parseFailOn(parsed.values["fail-on"])
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
const runInstallCommand = (argv) => {
|
|
193
|
+
const parsed = parseArgs({
|
|
194
|
+
args: argv,
|
|
195
|
+
allowPositionals: true,
|
|
196
|
+
options: {
|
|
197
|
+
"dry-run": {
|
|
198
|
+
type: "boolean",
|
|
199
|
+
default: false
|
|
200
|
+
},
|
|
201
|
+
help: {
|
|
202
|
+
type: "boolean",
|
|
203
|
+
short: "h",
|
|
204
|
+
default: false
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
if (parsed.values.help) {
|
|
209
|
+
process.stdout.write("Usage: bun-doctor install [directory] [--dry-run]\n");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const skillPath = installSkill({
|
|
213
|
+
directory: path.resolve(parsed.positionals[0] ?? "."),
|
|
214
|
+
dryRun: Boolean(parsed.values["dry-run"])
|
|
215
|
+
});
|
|
216
|
+
const action = parsed.values["dry-run"] ? "Would install" : "Installed";
|
|
217
|
+
process.stdout.write(`${action} bun-doctor skill at ${skillPath}\n`);
|
|
218
|
+
};
|
|
219
|
+
const main = async () => {
|
|
220
|
+
const argv = process.argv.slice(2);
|
|
221
|
+
if (argv[0] === "install") {
|
|
222
|
+
runInstallCommand(argv.slice(1));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const { directory, flags } = parseCli(argv);
|
|
226
|
+
const result = await scan(directory, {
|
|
227
|
+
packageChecks: flags.packageChecks,
|
|
228
|
+
codeChecks: flags.codeChecks
|
|
229
|
+
});
|
|
230
|
+
if (flags.score) process.stdout.write(`${result.score.score}\n`);
|
|
231
|
+
else if (flags.json) process.stdout.write(`${JSON.stringify(toJsonReport(result), null, 2)}\n`);
|
|
232
|
+
else process.stdout.write(`${formatTextReport(result, flags.verbose)}\n`);
|
|
233
|
+
if (shouldFail(new Set(result.diagnostics.map((diagnostic) => diagnostic.level)), flags.failOn)) process.exitCode = 1;
|
|
234
|
+
};
|
|
235
|
+
main().catch((error) => {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
process.stderr.write(`bun-doctor: ${message}\n`);
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
});
|
|
240
|
+
//#endregion
|
|
241
|
+
export {};
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/install-skill.ts","../src/report.ts","../src/cli.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\ninterface InstallSkillOptions {\n directory: string;\n dryRun?: boolean;\n}\n\nconst SKILL_CONTENT = `---\nname: bun-doctor\ndescription: Use after making dependency, CI, package manager, test runner, or Node runtime changes in a project that uses or is migrating to Bun. Checks Bun readiness and migration risk.\nversion: \"1.0.0\"\n---\n\n# Bun Doctor\n\nRun \\`npx -y bun-doctor@latest . --verbose\\` after Bun migration changes and fix blockers before switching CI or runtime.\n`;\n\nexport const installSkill = (options: InstallSkillOptions): string => {\n const skillDirectory = path.join(options.directory, \".agents\", \"skills\", \"bun-doctor\");\n const skillPath = path.join(skillDirectory, \"SKILL.md\");\n\n if (options.dryRun) return skillPath;\n\n fs.mkdirSync(skillDirectory, { recursive: true });\n fs.writeFileSync(skillPath, SKILL_CONTENT, \"utf8\");\n return skillPath;\n};\n","import path from \"node:path\";\nimport type { Diagnostic, FindingCategory, ScanResult } from \"./types.js\";\nimport { toRelativePath } from \"./utils.js\";\n\nconst CATEGORY_ORDER: FindingCategory[] = [\"Blockers\", \"Risks\", \"Migration work\", \"Bun wins\"];\n\nconst LEVEL_SYMBOL: Record<Diagnostic[\"level\"], string> = {\n blocker: \"x\",\n risk: \"!\",\n migration: \"~\",\n win: \"+\",\n};\n\nconst groupByCategory = (diagnostics: Diagnostic[]): Map<FindingCategory, Diagnostic[]> => {\n const groups = new Map<FindingCategory, Diagnostic[]>();\n for (const diagnostic of diagnostics) {\n const existing = groups.get(diagnostic.category) ?? [];\n existing.push(diagnostic);\n groups.set(diagnostic.category, existing);\n }\n return groups;\n};\n\nconst formatLocation = (diagnostic: Diagnostic, rootDirectory: string): string => {\n const relativePath = toRelativePath(path.resolve(diagnostic.filePath), rootDirectory);\n return diagnostic.line > 0 ? `${relativePath}:${diagnostic.line}` : relativePath;\n};\n\nexport const formatTextReport = (result: ScanResult, verbose: boolean): string => {\n const lines: string[] = [];\n lines.push(`bun-doctor`);\n lines.push(`Project: ${result.project.packageName}`);\n lines.push(`Bun Readiness: ${result.score.score}/100 (${result.score.label})`);\n lines.push(\n `Findings: ${result.summary.blockers} blockers, ${result.summary.risks} risks, ${result.summary.migrations} migration, ${result.summary.wins} wins`,\n );\n lines.push(\"\");\n\n if (result.diagnostics.length === 0) {\n lines.push(\"No Bun migration findings.\");\n return lines.join(\"\\n\");\n }\n\n const groups = groupByCategory(result.diagnostics);\n for (const category of CATEGORY_ORDER) {\n const diagnostics = groups.get(category) ?? [];\n if (diagnostics.length === 0) continue;\n lines.push(`${category} (${diagnostics.length})`);\n const shownDiagnostics = verbose ? diagnostics : diagnostics.slice(0, 3);\n for (const diagnostic of shownDiagnostics) {\n lines.push(` ${LEVEL_SYMBOL[diagnostic.level]} ${diagnostic.title} [${diagnostic.ruleId}]`);\n lines.push(` ${diagnostic.message}`);\n if (diagnostic.help) lines.push(` ${diagnostic.help}`);\n lines.push(` ${formatLocation(diagnostic, result.project.rootDirectory)}`);\n lines.push(` Source: ${diagnostic.sources[0]}`);\n }\n if (!verbose && diagnostics.length > shownDiagnostics.length) {\n lines.push(` ... ${diagnostics.length - shownDiagnostics.length} more. Re-run with --verbose.`);\n }\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n};\n\nexport const toJsonReport = (result: ScanResult): object => ({\n schemaVersion: 1,\n ok: true,\n score: result.score,\n summary: result.summary,\n project: {\n name: result.project.packageName,\n rootDirectory: result.project.rootDirectory,\n packageJsonPath: result.project.packageJsonPath,\n lockfiles: result.project.lockfiles,\n legacyLockfiles: result.project.legacyLockfiles,\n },\n diagnostics: result.diagnostics,\n});\n","#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport path from \"node:path\";\nimport { VERSION } from \"./constants.js\";\nimport { installSkill } from \"./install-skill.js\";\nimport { formatTextReport, toJsonReport } from \"./report.js\";\nimport { scan } from \"./scan.js\";\nimport type { FailOnLevel, ScanOptions } from \"./types.js\";\n\ninterface CliFlags {\n json: boolean;\n score: boolean;\n verbose: boolean;\n packageChecks: boolean;\n codeChecks: boolean;\n failOn: FailOnLevel;\n}\n\nconst VALID_FAIL_ON_LEVELS = new Set<FailOnLevel>([\"blocker\", \"risk\", \"migration\", \"win\", \"none\"]);\n\nconst HELP_TEXT = `Usage: bun-doctor [directory] [options]\n\nOptions:\n --json output a structured JSON report\n --score output only the numeric score\n --verbose show every diagnostic\n --no-package skip package/config/dependency checks\n --no-code skip source code checks\n --fail-on <level> exit non-zero on blocker, risk, migration, or none\n -v, --version print version\n -h, --help print help\n\nCommands:\n bun-doctor install [directory] [--dry-run]\n`;\n\nconst shouldFail = (levels: Set<string>, failOn: FailOnLevel): boolean => {\n if (failOn === \"none\") return false;\n if (failOn === \"win\") return levels.size > 0;\n if (failOn === \"migration\") return levels.has(\"blocker\") || levels.has(\"risk\") || levels.has(\"migration\");\n if (failOn === \"risk\") return levels.has(\"blocker\") || levels.has(\"risk\");\n return levels.has(\"blocker\");\n};\n\nconst parseFailOn = (value: string | undefined): FailOnLevel => {\n if (!value) return \"blocker\";\n if (VALID_FAIL_ON_LEVELS.has(value as FailOnLevel)) return value as FailOnLevel;\n throw new Error(`Invalid --fail-on value: ${value}. Expected blocker, risk, migration, win, or none.`);\n};\n\nconst parseCli = (argv: string[]): { directory: string; flags: CliFlags } => {\n const parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n json: { type: \"boolean\", default: false },\n score: { type: \"boolean\", default: false },\n verbose: { type: \"boolean\", default: false },\n \"no-package\": { type: \"boolean\", default: false },\n \"no-code\": { type: \"boolean\", default: false },\n \"fail-on\": { type: \"string\", default: \"blocker\" },\n version: { type: \"boolean\", short: \"v\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n\n if (parsed.values.help) {\n process.stdout.write(`${HELP_TEXT}\\n`);\n process.exit(0);\n }\n\n if (parsed.values.version) {\n process.stdout.write(`${VERSION}\\n`);\n process.exit(0);\n }\n\n return {\n directory: path.resolve(parsed.positionals[0] ?? \".\"),\n flags: {\n json: Boolean(parsed.values.json),\n score: Boolean(parsed.values.score),\n verbose: Boolean(parsed.values.verbose),\n packageChecks: !parsed.values[\"no-package\"],\n codeChecks: !parsed.values[\"no-code\"],\n failOn: parseFailOn(parsed.values[\"fail-on\"]),\n },\n };\n};\n\nconst runInstallCommand = (argv: string[]): void => {\n const parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n \"dry-run\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n });\n\n if (parsed.values.help) {\n process.stdout.write(\"Usage: bun-doctor install [directory] [--dry-run]\\n\");\n return;\n }\n\n const directory = path.resolve(parsed.positionals[0] ?? \".\");\n const skillPath = installSkill({ directory, dryRun: Boolean(parsed.values[\"dry-run\"]) });\n const action = parsed.values[\"dry-run\"] ? \"Would install\" : \"Installed\";\n process.stdout.write(`${action} bun-doctor skill at ${skillPath}\\n`);\n};\n\nconst main = async (): Promise<void> => {\n const argv = process.argv.slice(2);\n if (argv[0] === \"install\") {\n runInstallCommand(argv.slice(1));\n return;\n }\n\n const { directory, flags } = parseCli(argv);\n const options: ScanOptions = {\n packageChecks: flags.packageChecks,\n codeChecks: flags.codeChecks,\n };\n const result = await scan(directory, options);\n\n if (flags.score) {\n process.stdout.write(`${result.score.score}\\n`);\n } else if (flags.json) {\n process.stdout.write(`${JSON.stringify(toJsonReport(result), null, 2)}\\n`);\n } else {\n process.stdout.write(`${formatTextReport(result, flags.verbose)}\\n`);\n }\n\n const levels = new Set(result.diagnostics.map((diagnostic) => diagnostic.level));\n if (shouldFail(levels, flags.failOn)) {\n process.exitCode = 1;\n }\n};\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`bun-doctor: ${message}\\n`);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type FindingLevel = "blocker" | "risk" | "migration" | "win";
|
|
3
|
+
type FindingCategory = "Blockers" | "Risks" | "Migration work" | "Bun wins";
|
|
4
|
+
type FailOnLevel = FindingLevel | "none";
|
|
5
|
+
interface PackageJson {
|
|
6
|
+
name?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
packageManager?: string;
|
|
9
|
+
scripts?: Record<string, string>;
|
|
10
|
+
dependencies?: Record<string, string>;
|
|
11
|
+
devDependencies?: Record<string, string>;
|
|
12
|
+
peerDependencies?: Record<string, string>;
|
|
13
|
+
optionalDependencies?: Record<string, string>;
|
|
14
|
+
trustedDependencies?: string[];
|
|
15
|
+
workspaces?: string[] | {
|
|
16
|
+
packages?: string[];
|
|
17
|
+
catalog?: unknown;
|
|
18
|
+
catalogs?: unknown;
|
|
19
|
+
};
|
|
20
|
+
catalog?: unknown;
|
|
21
|
+
catalogs?: unknown;
|
|
22
|
+
bunDoctor?: BunDoctorConfig;
|
|
23
|
+
}
|
|
24
|
+
interface BunDoctorConfig {
|
|
25
|
+
ignore?: {
|
|
26
|
+
rules?: string[];
|
|
27
|
+
files?: string[];
|
|
28
|
+
};
|
|
29
|
+
package?: boolean;
|
|
30
|
+
code?: boolean;
|
|
31
|
+
}
|
|
32
|
+
interface CompatEntry {
|
|
33
|
+
packageName: string;
|
|
34
|
+
severity: FindingLevel;
|
|
35
|
+
affectedRanges: string[];
|
|
36
|
+
bunVersions: string[];
|
|
37
|
+
platforms: Array<"darwin" | "linux" | "win32" | "all">;
|
|
38
|
+
confidence: "high" | "medium" | "low";
|
|
39
|
+
reason: string;
|
|
40
|
+
sources: string[];
|
|
41
|
+
lastVerified: string;
|
|
42
|
+
replacement?: string;
|
|
43
|
+
workaround?: string;
|
|
44
|
+
migrationHint?: string;
|
|
45
|
+
requiresTrustedDependency?: boolean;
|
|
46
|
+
}
|
|
47
|
+
interface SourceFile {
|
|
48
|
+
filePath: string;
|
|
49
|
+
content: string;
|
|
50
|
+
}
|
|
51
|
+
interface WorkflowFile {
|
|
52
|
+
filePath: string;
|
|
53
|
+
content: string;
|
|
54
|
+
}
|
|
55
|
+
interface BunfigInfo {
|
|
56
|
+
filePath: string;
|
|
57
|
+
content: string;
|
|
58
|
+
installIgnoreScripts?: boolean;
|
|
59
|
+
installFrozenLockfile?: boolean;
|
|
60
|
+
installAuto?: string;
|
|
61
|
+
installSecurityScanner?: string;
|
|
62
|
+
}
|
|
63
|
+
interface ProjectInfo {
|
|
64
|
+
rootDirectory: string;
|
|
65
|
+
packageJsonPath: string;
|
|
66
|
+
packageJson: PackageJson;
|
|
67
|
+
packageName: string;
|
|
68
|
+
dependencies: Record<string, string>;
|
|
69
|
+
trustedDependencies: Set<string>;
|
|
70
|
+
packageManifests: PackageManifest[];
|
|
71
|
+
lockfiles: string[];
|
|
72
|
+
legacyLockfiles: string[];
|
|
73
|
+
bunfig: BunfigInfo | null;
|
|
74
|
+
tsconfigPath: string | null;
|
|
75
|
+
tsconfig: Record<string, unknown> | null;
|
|
76
|
+
workflows: WorkflowFile[];
|
|
77
|
+
sourceFiles: SourceFile[];
|
|
78
|
+
pnpmWorkspacePath: string | null;
|
|
79
|
+
}
|
|
80
|
+
interface PackageManifest {
|
|
81
|
+
packageJsonPath: string;
|
|
82
|
+
packageJson: PackageJson;
|
|
83
|
+
packageName: string;
|
|
84
|
+
dependencies: Record<string, string>;
|
|
85
|
+
trustedDependencies: Set<string>;
|
|
86
|
+
}
|
|
87
|
+
interface Diagnostic {
|
|
88
|
+
ruleId: string;
|
|
89
|
+
title: string;
|
|
90
|
+
level: FindingLevel;
|
|
91
|
+
category: FindingCategory;
|
|
92
|
+
message: string;
|
|
93
|
+
filePath: string;
|
|
94
|
+
line: number;
|
|
95
|
+
sources: string[];
|
|
96
|
+
help?: string;
|
|
97
|
+
packageName?: string;
|
|
98
|
+
}
|
|
99
|
+
interface ScoreResult {
|
|
100
|
+
score: number;
|
|
101
|
+
label: "Ready" | "Close" | "Risky" | "Blocked";
|
|
102
|
+
}
|
|
103
|
+
interface ScanSummary {
|
|
104
|
+
blockers: number;
|
|
105
|
+
risks: number;
|
|
106
|
+
migrations: number;
|
|
107
|
+
wins: number;
|
|
108
|
+
}
|
|
109
|
+
interface ScanOptions {
|
|
110
|
+
packageChecks?: boolean;
|
|
111
|
+
codeChecks?: boolean;
|
|
112
|
+
configOverride?: BunDoctorConfig | null;
|
|
113
|
+
}
|
|
114
|
+
interface ScanResult {
|
|
115
|
+
project: ProjectInfo;
|
|
116
|
+
diagnostics: Diagnostic[];
|
|
117
|
+
score: ScoreResult;
|
|
118
|
+
summary: ScanSummary;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/scan.d.ts
|
|
122
|
+
declare const scan: (directory: string, options?: ScanOptions) => Promise<ScanResult>;
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/score.d.ts
|
|
125
|
+
declare const calculateScore: (diagnostics: Diagnostic[]) => ScoreResult;
|
|
126
|
+
declare const summarizeDiagnostics: (diagnostics: Diagnostic[]) => ScanSummary;
|
|
127
|
+
//#endregion
|
|
128
|
+
export { type BunDoctorConfig, type CompatEntry, type Diagnostic, type FailOnLevel, type FindingCategory, type FindingLevel, type ScanOptions, type ScanResult, calculateScore, scan, summarizeDiagnostics };
|
|
129
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED