move-doctor 0.2.0 → 0.3.0-dev.e789f4b
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/CHANGELOG.md +24 -0
- package/README.md +3 -30
- package/dist/cli.js +757 -649
- package/package.json +2 -3
package/dist/cli.js
CHANGED
|
@@ -3,13 +3,13 @@ import * as path5 from 'path';
|
|
|
3
3
|
import { statSync, readdirSync, existsSync, readFileSync, mkdirSync, writeFileSync, chmodSync } from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { Parser, Language } from 'web-tree-sitter';
|
|
6
|
-
import { readFile, mkdir, writeFile, stat, mkdtemp, rm
|
|
6
|
+
import { readFile, mkdir, writeFile, readdir, stat, mkdtemp, rm } from 'fs/promises';
|
|
7
7
|
import { spawn } from 'child_process';
|
|
8
8
|
import pc from 'picocolors';
|
|
9
9
|
import * as os from 'os';
|
|
10
|
+
import { intro, outro, spinner, log, select as select$1, isCancel, multiselect as multiselect$1, cancel } from '@clack/prompts';
|
|
10
11
|
import { getSkillAgentConfig, detectInstalledSkillAgents, installSkillsFromSource } from 'agent-install';
|
|
11
12
|
import { createHash } from 'crypto';
|
|
12
|
-
import prompts from 'prompts';
|
|
13
13
|
|
|
14
14
|
var resolveGrammarWasmPath = () => {
|
|
15
15
|
const candidates = [
|
|
@@ -158,14 +158,25 @@ var scanMoveFiles = async (project, options) => {
|
|
|
158
158
|
return results.filter((file) => file !== null);
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
// ../core/dist/engine/score.js
|
|
161
|
+
// ../core/dist/engine/score.const.js
|
|
162
162
|
var SEVERITY_WEIGHTS = {
|
|
163
163
|
error: 8,
|
|
164
164
|
warning: 3,
|
|
165
165
|
info: 1
|
|
166
166
|
};
|
|
167
|
-
var PER_RULE_DEDUCTION_CAP =
|
|
167
|
+
var PER_RULE_DEDUCTION_CAP = {
|
|
168
|
+
error: 25,
|
|
169
|
+
warning: 25,
|
|
170
|
+
info: 5
|
|
171
|
+
};
|
|
172
|
+
var SEVERITY_RANK = {
|
|
173
|
+
error: 3,
|
|
174
|
+
warning: 2,
|
|
175
|
+
info: 1
|
|
176
|
+
};
|
|
168
177
|
var MAX_SCORE = 100;
|
|
178
|
+
|
|
179
|
+
// ../core/dist/engine/score.js
|
|
169
180
|
var computeScore = (diagnostics) => {
|
|
170
181
|
const bySeverity = {
|
|
171
182
|
error: 0,
|
|
@@ -178,12 +189,22 @@ var computeScore = (diagnostics) => {
|
|
|
178
189
|
bySeverity[diagnostic.severity] += 1;
|
|
179
190
|
byBucket[diagnostic.bucket] = (byBucket[diagnostic.bucket] ?? 0) + 1;
|
|
180
191
|
const weight = SEVERITY_WEIGHTS[diagnostic.severity];
|
|
181
|
-
const prior = deductionByRule.get(diagnostic.ruleId)
|
|
182
|
-
|
|
192
|
+
const prior = deductionByRule.get(diagnostic.ruleId);
|
|
193
|
+
if (prior) {
|
|
194
|
+
prior.weight += weight;
|
|
195
|
+
if (SEVERITY_RANK[diagnostic.severity] > SEVERITY_RANK[prior.severity]) {
|
|
196
|
+
prior.severity = diagnostic.severity;
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
deductionByRule.set(diagnostic.ruleId, {
|
|
200
|
+
weight,
|
|
201
|
+
severity: diagnostic.severity
|
|
202
|
+
});
|
|
203
|
+
}
|
|
183
204
|
}
|
|
184
205
|
let totalDeductions = 0;
|
|
185
|
-
for (const
|
|
186
|
-
totalDeductions += Math.min(
|
|
206
|
+
for (const { weight, severity } of deductionByRule.values()) {
|
|
207
|
+
totalDeductions += Math.min(weight, PER_RULE_DEDUCTION_CAP[severity]);
|
|
187
208
|
}
|
|
188
209
|
const score = Math.max(0, MAX_SCORE - totalDeductions);
|
|
189
210
|
return {
|
|
@@ -680,6 +701,7 @@ var highlighter = {
|
|
|
680
701
|
bgYellow: pc.bgYellow,
|
|
681
702
|
bgRed: pc.bgRed
|
|
682
703
|
};
|
|
704
|
+
var colorEnabled = () => pc.isColorSupported;
|
|
683
705
|
var PERFECT_SCORE = 100;
|
|
684
706
|
var SCORE_GOOD_THRESHOLD = 80;
|
|
685
707
|
var SCORE_OK_THRESHOLD = 50;
|
|
@@ -2893,12 +2915,12 @@ var unnecessaryTestScenario = defineAstRule({
|
|
|
2893
2915
|
continue;
|
|
2894
2916
|
}
|
|
2895
2917
|
const calls = collectNodesOfType(body, "call_expression");
|
|
2896
|
-
const scenarioPaths = calls.map((call) => callExpressionPath(call)).filter((
|
|
2897
|
-
const usesBegin = scenarioPaths.some((
|
|
2918
|
+
const scenarioPaths = calls.map((call) => callExpressionPath(call)).filter((path13) => path13.module === "test_scenario");
|
|
2919
|
+
const usesBegin = scenarioPaths.some((path13) => path13.member === "begin");
|
|
2898
2920
|
if (!usesBegin) {
|
|
2899
2921
|
continue;
|
|
2900
2922
|
}
|
|
2901
|
-
const usesApi = scenarioPaths.some((
|
|
2923
|
+
const usesApi = scenarioPaths.some((path13) => path13.member !== null && SCENARIO_API.has(path13.member));
|
|
2902
2924
|
if (usesApi) {
|
|
2903
2925
|
continue;
|
|
2904
2926
|
}
|
|
@@ -3060,51 +3082,6 @@ var resolveChangedFiles = async (rootDirectory, diff) => {
|
|
|
3060
3082
|
);
|
|
3061
3083
|
};
|
|
3062
3084
|
|
|
3063
|
-
// src/cli/skill-content.ts
|
|
3064
|
-
var SKILL_MD_CONTENT = `---
|
|
3065
|
-
name: move-doctor
|
|
3066
|
-
description: Use when finishing a Sui Move feature, fixing a bug, before committing Move code, or when the user types /movedoctor, asks to scan, triage, or clean up Move diagnostics. Covers Move Book conventions, Move 2024 idioms, ability mistakes, testing style, and security best practices.
|
|
3067
|
-
version: "0.1.0"
|
|
3068
|
-
---
|
|
3069
|
-
|
|
3070
|
-
# Move Doctor
|
|
3071
|
-
|
|
3072
|
-
Scans Sui Move codebases for convention, idiom, ability, testing, and security issues. Outputs a 0\u2013100 health score.
|
|
3073
|
-
|
|
3074
|
-
## After making Move code changes
|
|
3075
|
-
|
|
3076
|
-
Run \`npx move-doctor@latest --verbose --diff\` and check the score did not regress. If it dropped, fix the regressions before committing.
|
|
3077
|
-
|
|
3078
|
-
## For a full cleanup pass
|
|
3079
|
-
|
|
3080
|
-
Run \`npx move-doctor@latest --verbose\` (without \`--diff\`) to scan the whole codebase. Fix issues by severity \u2014 errors first, then warnings, then info.
|
|
3081
|
-
|
|
3082
|
-
## /movedoctor \u2014 full triage workflow
|
|
3083
|
-
|
|
3084
|
-
When the user types \`/movedoctor\`, says "run move doctor", or asks for a full triage / cleanup pass, fetch the canonical playbook and follow every step (fetching it means updates ship without a skill reinstall):
|
|
3085
|
-
|
|
3086
|
-
\`\`\`bash
|
|
3087
|
-
curl --fail --silent --show-error \\
|
|
3088
|
-
--header 'Cache-Control: no-cache' \\
|
|
3089
|
-
https://move.doctor/prompts/move-doctor-agent.md
|
|
3090
|
-
\`\`\`
|
|
3091
|
-
|
|
3092
|
-
It's a scan \u2192 group \u2192 triage \u2192 fix \u2192 re-score loop that edits the working tree directly (never commits, never opens PRs). For each finding, fetch the matching per-rule fix recipe on demand \u2014 \`<bucket>/<rule>\` comes straight from the diagnostic's rule id:
|
|
3093
|
-
|
|
3094
|
-
\`\`\`bash
|
|
3095
|
-
curl --fail --silent --show-error \\
|
|
3096
|
-
https://move.doctor/prompts/rules/<bucket>/<rule>.md
|
|
3097
|
-
\`\`\`
|
|
3098
|
-
|
|
3099
|
-
If the fetch fails (offline / site down), fall back to: run \`move-doctor --verbose\`, fix errors first (\`security/*\` and \`abilities/*\` findings are real vulnerabilities, not style), apply each finding's \`fixHint\` verbatim, and re-run until the score stops rising. Never suppress a rule unless you can explain why the surrounding code is a documented exception.
|
|
3100
|
-
|
|
3101
|
-
## Command
|
|
3102
|
-
|
|
3103
|
-
\`\`\`bash
|
|
3104
|
-
npx move-doctor@latest [path] --verbose --diff # --score for CI gating \xB7 --help for all flags
|
|
3105
|
-
\`\`\`
|
|
3106
|
-
`;
|
|
3107
|
-
|
|
3108
3085
|
// src/cli/parse-args.ts
|
|
3109
3086
|
var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
|
|
3110
3087
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-v", "--version"]);
|
|
@@ -3200,131 +3177,221 @@ var parseArgs = (argv2) => {
|
|
|
3200
3177
|
return args;
|
|
3201
3178
|
};
|
|
3202
3179
|
|
|
3203
|
-
// src/cli/
|
|
3204
|
-
var
|
|
3205
|
-
var
|
|
3206
|
-
var
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3180
|
+
// src/cli/constants.ts
|
|
3181
|
+
var BAR_WIDTH = 24;
|
|
3182
|
+
var MIN_TERM_WIDTH = 40;
|
|
3183
|
+
var MAX_TERM_WIDTH = 80;
|
|
3184
|
+
var MAX_CARD_WIDTH = 74;
|
|
3185
|
+
var HOMEPAGE = "https://move.doctor/";
|
|
3186
|
+
|
|
3187
|
+
// src/cli/utils/glyphs.ts
|
|
3188
|
+
var LEGACY_WIN = process.platform === "win32" && !process.env.WT_SESSION;
|
|
3189
|
+
var glyph = {
|
|
3190
|
+
/** Brand mark / "diagnosis" accent. */
|
|
3191
|
+
cross: LEGACY_WIN ? "+" : "\u271A",
|
|
3192
|
+
/** Filled status dot — health at a glance. */
|
|
3193
|
+
dot: LEGACY_WIN ? "*" : "\u25CF",
|
|
3194
|
+
/** Hollow status dot — info / neutral. */
|
|
3195
|
+
dotOpen: LEGACY_WIN ? "\xB7" : "\u25CB",
|
|
3196
|
+
check: LEGACY_WIN ? "\u221A" : "\u2713",
|
|
3197
|
+
warn: LEGACY_WIN ? "!" : "\u26A0",
|
|
3198
|
+
crossMark: LEGACY_WIN ? "x" : "\u2717",
|
|
3199
|
+
/** Action / next-step list marker. */
|
|
3200
|
+
pointer: LEGACY_WIN ? ">" : "\u203A",
|
|
3201
|
+
/** Inline separator between facts. */
|
|
3202
|
+
bullet: "\xB7"};
|
|
3203
|
+
|
|
3204
|
+
// src/cli/utils/meter.ts
|
|
3205
|
+
var ERROR_CELL = "\u2588";
|
|
3206
|
+
var WARNING_CELL = "\u2593";
|
|
3207
|
+
var INFO_CELL = "\u2592";
|
|
3208
|
+
var EMPTY_CELL = "\u2591";
|
|
3209
|
+
var makeSegments = (counts) => [
|
|
3210
|
+
{
|
|
3211
|
+
n: counts.errors,
|
|
3212
|
+
paint: highlighter.error,
|
|
3213
|
+
char: ERROR_CELL,
|
|
3214
|
+
cells: 0,
|
|
3215
|
+
frac: 0
|
|
3216
|
+
},
|
|
3217
|
+
{
|
|
3218
|
+
n: counts.warnings,
|
|
3219
|
+
paint: highlighter.warn,
|
|
3220
|
+
char: WARNING_CELL,
|
|
3221
|
+
cells: 0,
|
|
3222
|
+
frac: 0
|
|
3223
|
+
},
|
|
3224
|
+
{
|
|
3225
|
+
n: counts.info,
|
|
3226
|
+
paint: highlighter.muted,
|
|
3227
|
+
char: INFO_CELL,
|
|
3228
|
+
cells: 0,
|
|
3229
|
+
frac: 0
|
|
3223
3230
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3231
|
+
];
|
|
3232
|
+
var paintComposition = (counts, cells) => {
|
|
3233
|
+
if (cells <= 0) {
|
|
3234
|
+
return "";
|
|
3235
|
+
}
|
|
3236
|
+
const total = counts.errors + counts.warnings + counts.info;
|
|
3237
|
+
if (total === 0) {
|
|
3238
|
+
return highlighter.muted(EMPTY_CELL.repeat(cells));
|
|
3239
|
+
}
|
|
3240
|
+
const segments = makeSegments(counts);
|
|
3241
|
+
let used = 0;
|
|
3242
|
+
for (const seg of segments) {
|
|
3243
|
+
const exact = seg.n / total * cells;
|
|
3244
|
+
seg.cells = Math.floor(exact);
|
|
3245
|
+
seg.frac = exact - seg.cells;
|
|
3246
|
+
used += seg.cells;
|
|
3247
|
+
}
|
|
3248
|
+
const byFraction = [...segments].sort((a, b) => b.frac - a.frac);
|
|
3249
|
+
for (let k = 0; used < cells && byFraction.length > 0; k++) {
|
|
3250
|
+
const seg = byFraction[k % byFraction.length];
|
|
3251
|
+
if (seg) {
|
|
3252
|
+
seg.cells += 1;
|
|
3253
|
+
used += 1;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
for (const seg of segments) {
|
|
3257
|
+
if (seg.n > 0 && seg.cells === 0) {
|
|
3258
|
+
const donor = segments.reduce(
|
|
3259
|
+
(max, s) => s.cells > max.cells ? s : max
|
|
3260
|
+
);
|
|
3261
|
+
if (donor.cells > 1) {
|
|
3262
|
+
donor.cells -= 1;
|
|
3263
|
+
seg.cells += 1;
|
|
3264
|
+
}
|
|
3230
3265
|
}
|
|
3231
3266
|
}
|
|
3232
|
-
return
|
|
3267
|
+
return segments.map((seg) => seg.paint(seg.char.repeat(seg.cells))).join("");
|
|
3233
3268
|
};
|
|
3234
|
-
var
|
|
3235
|
-
const
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3269
|
+
var compositionBar = (counts, width) => {
|
|
3270
|
+
const total = counts.errors + counts.warnings + counts.info;
|
|
3271
|
+
if (total === 0) {
|
|
3272
|
+
return highlighter.ok(ERROR_CELL.repeat(width));
|
|
3273
|
+
}
|
|
3274
|
+
return paintComposition(counts, width);
|
|
3275
|
+
};
|
|
3276
|
+
var magnitudeBar = (counts, width, maxTotal) => {
|
|
3277
|
+
const total = counts.errors + counts.warnings + counts.info;
|
|
3278
|
+
if (total === 0) {
|
|
3279
|
+
return highlighter.muted(EMPTY_CELL.repeat(width));
|
|
3280
|
+
}
|
|
3281
|
+
const ratio = maxTotal > 0 ? total / maxTotal : 1;
|
|
3282
|
+
const filled = Math.min(width, Math.max(1, Math.round(ratio * width)));
|
|
3283
|
+
return paintComposition(counts, filled) + highlighter.muted(EMPTY_CELL.repeat(width - filled));
|
|
3284
|
+
};
|
|
3285
|
+
var SEVERITY_MARK = {
|
|
3286
|
+
error: ERROR_CELL,
|
|
3287
|
+
warning: WARNING_CELL,
|
|
3288
|
+
info: INFO_CELL
|
|
3289
|
+
};
|
|
3290
|
+
|
|
3291
|
+
// src/cli/utils/terminal.ts
|
|
3292
|
+
var supportsHyperlinks = () => process.stdout.isTTY === true && colorEnabled();
|
|
3293
|
+
var ESC = String.fromCharCode(27);
|
|
3294
|
+
var BEL = String.fromCharCode(7);
|
|
3295
|
+
var hyperlink = (text, url) => supportsHyperlinks() ? `${ESC}]8;;${url}${BEL}${text}${ESC}]8;;${BEL}` : text;
|
|
3296
|
+
|
|
3297
|
+
// src/cli/render-common.ts
|
|
3298
|
+
var ANSI_SGR = new RegExp(`${String.fromCharCode(27)}[[][0-9;]*m`, "g");
|
|
3299
|
+
var visibleLength = (text) => text.replace(ANSI_SGR, "").length;
|
|
3300
|
+
var truncatePlain = (text, max) => text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}\u2026`;
|
|
3301
|
+
var terminalWidth = () => {
|
|
3302
|
+
const width = process.stdout.columns ?? MAX_TERM_WIDTH;
|
|
3303
|
+
return Math.max(MIN_TERM_WIDTH, Math.min(width, MAX_TERM_WIDTH));
|
|
3304
|
+
};
|
|
3305
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
3306
|
+
var formatDurationShort = (durationMs) => durationMs < 1e3 ? `${durationMs}ms` : `${(durationMs / 1e3).toFixed(1)}s`;
|
|
3307
|
+
var countSeverities = (diagnostics) => {
|
|
3308
|
+
const counts = { errors: 0, warnings: 0, info: 0 };
|
|
3309
|
+
for (const diagnostic of diagnostics) {
|
|
3310
|
+
if (diagnostic.severity === "error") {
|
|
3311
|
+
counts.errors += 1;
|
|
3312
|
+
} else if (diagnostic.severity === "warning") {
|
|
3313
|
+
counts.warnings += 1;
|
|
3314
|
+
} else {
|
|
3315
|
+
counts.info += 1;
|
|
3251
3316
|
}
|
|
3252
3317
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3318
|
+
return counts;
|
|
3319
|
+
};
|
|
3320
|
+
var severityDot = (counts) => {
|
|
3321
|
+
if (counts.errors > 0) {
|
|
3322
|
+
return highlighter.error(glyph.dot);
|
|
3323
|
+
}
|
|
3324
|
+
if (counts.warnings > 0) {
|
|
3325
|
+
return highlighter.warn(glyph.dot);
|
|
3326
|
+
}
|
|
3327
|
+
return highlighter.muted(glyph.dotOpen);
|
|
3328
|
+
};
|
|
3329
|
+
var severitySummary = (counts) => {
|
|
3330
|
+
const parts = [];
|
|
3331
|
+
if (counts.errors > 0) {
|
|
3332
|
+
parts.push(highlighter.error(plural(counts.errors, "error")));
|
|
3333
|
+
}
|
|
3334
|
+
if (counts.warnings > 0) {
|
|
3335
|
+
parts.push(highlighter.warn(plural(counts.warnings, "warning")));
|
|
3336
|
+
}
|
|
3337
|
+
if (counts.info > 0) {
|
|
3338
|
+
parts.push(highlighter.muted(`${counts.info} info`));
|
|
3339
|
+
}
|
|
3340
|
+
return parts.join(highlighter.muted(" \xB7 "));
|
|
3341
|
+
};
|
|
3342
|
+
var barRow = (dot, label, bar, ...columns) => ` ${dot} ${label} ${bar} ${columns.join(" ")}`;
|
|
3343
|
+
var severityLegend = (counts, total) => {
|
|
3344
|
+
const pct = (n) => highlighter.muted(` ${Math.round(n / total * 100)}%`);
|
|
3345
|
+
const parts = [];
|
|
3346
|
+
if (counts.errors > 0) {
|
|
3347
|
+
parts.push(
|
|
3348
|
+
highlighter.error(
|
|
3349
|
+
`${SEVERITY_MARK.error} ${plural(counts.errors, "error")}`
|
|
3350
|
+
) + pct(counts.errors)
|
|
3258
3351
|
);
|
|
3259
3352
|
}
|
|
3260
|
-
if (
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
`${
|
|
3264
|
-
)
|
|
3353
|
+
if (counts.warnings > 0) {
|
|
3354
|
+
parts.push(
|
|
3355
|
+
highlighter.warn(
|
|
3356
|
+
`${SEVERITY_MARK.warning} ${plural(counts.warnings, "warning")}`
|
|
3357
|
+
) + pct(counts.warnings)
|
|
3265
3358
|
);
|
|
3266
3359
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
)
|
|
3271
|
-
);
|
|
3272
|
-
for (const step of steps) {
|
|
3273
|
-
lines.push(` ${step.line}`);
|
|
3360
|
+
if (counts.info > 0) {
|
|
3361
|
+
parts.push(
|
|
3362
|
+
highlighter.muted(`${SEVERITY_MARK.info} ${counts.info} info`) + pct(counts.info)
|
|
3363
|
+
);
|
|
3274
3364
|
}
|
|
3275
|
-
return
|
|
3365
|
+
return parts.join(" ");
|
|
3276
3366
|
};
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
const
|
|
3282
|
-
|
|
3367
|
+
var buildTopBorder = (width, cross, title) => {
|
|
3368
|
+
const muted = highlighter.muted;
|
|
3369
|
+
const label = "diagnosis";
|
|
3370
|
+
const fixed = 8 + label.length;
|
|
3371
|
+
const shownTitle = truncatePlain(title, Math.max(4, width - 2 - fixed - 1));
|
|
3372
|
+
const fill = Math.max(1, width - 2 - fixed - shownTitle.length);
|
|
3373
|
+
return ` ${muted("\u256D\u2500 ")}${cross}${muted(` ${label} ${"\u2500".repeat(fill)} `)}${highlighter.bold(shownTitle)}${muted(" \u2500\u256E")}`;
|
|
3283
3374
|
};
|
|
3284
|
-
var
|
|
3285
|
-
const
|
|
3286
|
-
const
|
|
3287
|
-
|
|
3375
|
+
var buildBottomBorder = (width) => {
|
|
3376
|
+
const muted = highlighter.muted;
|
|
3377
|
+
const label = "move.doctor";
|
|
3378
|
+
const fill = Math.max(0, width - 2 - (label.length + 3));
|
|
3379
|
+
const wordmark = highlighter.bold(
|
|
3380
|
+
highlighter.accent(hyperlink(label, HOMEPAGE))
|
|
3381
|
+
);
|
|
3382
|
+
return ` ${muted(`\u2570${"\u2500".repeat(fill)} `)}${wordmark}${muted(" \u2500\u256F")}`;
|
|
3288
3383
|
};
|
|
3289
|
-
var
|
|
3290
|
-
var stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
|
|
3291
|
-
var visibleLength = (text) => stripAnsi(text).length;
|
|
3292
|
-
var formatDurationShort = (durationMs) => durationMs < 1e3 ? `${durationMs}ms` : `${(durationMs / 1e3).toFixed(1)}s`;
|
|
3293
|
-
var truncatePlain = (text, max) => text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}\u2026`;
|
|
3294
|
-
var buildInfoCard = (data) => {
|
|
3384
|
+
var buildDiagnosisHeader = (data) => {
|
|
3295
3385
|
const { score } = data;
|
|
3296
|
-
const
|
|
3386
|
+
const color = (text) => colorizeByScore(text, score);
|
|
3387
|
+
const width = Math.min(terminalWidth(), MAX_CARD_WIDTH);
|
|
3297
3388
|
const inner = width - 4;
|
|
3298
3389
|
const muted = highlighter.muted;
|
|
3299
|
-
const colorScore = (text) => colorizeByScore(text, score);
|
|
3300
|
-
const top = ` ${muted(`\u256D${"\u2500".repeat(width - 2)}\u256E`)}`;
|
|
3301
|
-
const bottomLabel = "move.doctor";
|
|
3302
|
-
const bottomFill = Math.max(0, width - 2 - (1 + bottomLabel.length + 2));
|
|
3303
|
-
const bottom = ` ${muted(`\u2570${"\u2500".repeat(bottomFill)} `)}${highlighter.bold(highlighter.accent(bottomLabel))}${muted(" \u2500\u256F")}`;
|
|
3304
|
-
const blank = ` ${muted("\u2502")}${" ".repeat(width - 2)}${muted("\u2502")}`;
|
|
3305
|
-
const row = (content) => {
|
|
3306
|
-
const pad = Math.max(0, inner - visibleLength(content));
|
|
3307
|
-
return ` ${muted("\u2502")} ${content}${" ".repeat(pad)} ${muted("\u2502")}`;
|
|
3308
|
-
};
|
|
3309
|
-
const rowLR = (left, right) => {
|
|
3310
|
-
const pad = Math.max(1, inner - visibleLength(left) - visibleLength(right));
|
|
3311
|
-
return ` ${muted("\u2502")} ${left}${" ".repeat(pad)}${right} ${muted("\u2502")}`;
|
|
3312
|
-
};
|
|
3313
|
-
const scoreLeft = `${colorScore(highlighter.bold(String(score)))} ${muted(`/ ${PERFECT_SCORE}`)} ${colorScore(scoreLabel(score))}`;
|
|
3314
|
-
const scoreLeftWidth = visibleLength(scoreLeft);
|
|
3315
|
-
const title = highlighter.bold(
|
|
3316
|
-
truncatePlain(data.title, Math.max(8, inner - scoreLeftWidth - 2))
|
|
3317
|
-
);
|
|
3318
|
-
const bar = buildScoreBar(score, inner);
|
|
3319
3390
|
const metaParts = [];
|
|
3320
3391
|
if (data.packageCount !== void 0) {
|
|
3321
|
-
metaParts.push(
|
|
3322
|
-
`${data.packageCount} package${data.packageCount === 1 ? "" : "s"}`
|
|
3323
|
-
);
|
|
3392
|
+
metaParts.push(plural(data.packageCount, "package"));
|
|
3324
3393
|
}
|
|
3325
|
-
metaParts.push(
|
|
3326
|
-
`${data.moduleCount} module${data.moduleCount === 1 ? "" : "s"}`
|
|
3327
|
-
);
|
|
3394
|
+
metaParts.push(plural(data.moduleCount, "module"));
|
|
3328
3395
|
if (data.edition !== void 0) {
|
|
3329
3396
|
metaParts.push(`edition ${data.edition ?? "unset"}`);
|
|
3330
3397
|
}
|
|
@@ -3337,48 +3404,59 @@ var buildInfoCard = (data) => {
|
|
|
3337
3404
|
if (data.durationMs !== null) {
|
|
3338
3405
|
metaParts.push(`scanned in ${formatDurationShort(data.durationMs)}`);
|
|
3339
3406
|
}
|
|
3407
|
+
const counts = {
|
|
3408
|
+
errors: data.findings.errors,
|
|
3409
|
+
warnings: data.findings.warnings,
|
|
3410
|
+
info: data.findings.info
|
|
3411
|
+
};
|
|
3412
|
+
const blank = ` ${muted("\u2502")}${" ".repeat(width - 2)}${muted("\u2502")}`;
|
|
3413
|
+
const row = (content) => {
|
|
3414
|
+
const pad = Math.max(0, inner - visibleLength(content));
|
|
3415
|
+
return ` ${muted("\u2502")} ${content}${" ".repeat(pad)} ${muted("\u2502")}`;
|
|
3416
|
+
};
|
|
3417
|
+
const scoreLine = `${muted("score")} ${color(highlighter.bold(String(score)))} ${muted(`/ ${PERFECT_SCORE}`)} ${color(glyph.dot)} ${color(highlighter.bold(scoreLabel(score)))}`;
|
|
3418
|
+
const legend = data.findings.total === 0 ? `${highlighter.ok(glyph.check)} ${highlighter.ok("clean bill of health")}` : severityLegend(counts, data.findings.total);
|
|
3340
3419
|
const meta = muted(truncatePlain(metaParts.join(" \xB7 "), inner));
|
|
3341
|
-
const findings = buildFindingsTldr(data.findings);
|
|
3342
3420
|
return [
|
|
3343
|
-
|
|
3421
|
+
buildTopBorder(width, color(glyph.cross), data.title),
|
|
3422
|
+
blank,
|
|
3423
|
+
row(scoreLine),
|
|
3344
3424
|
blank,
|
|
3345
|
-
|
|
3346
|
-
row(
|
|
3425
|
+
row(compositionBar(counts, inner)),
|
|
3426
|
+
row(legend),
|
|
3347
3427
|
blank,
|
|
3348
3428
|
row(meta),
|
|
3349
|
-
|
|
3350
|
-
bottom
|
|
3429
|
+
buildBottomBorder(width)
|
|
3351
3430
|
].join("\n");
|
|
3352
3431
|
};
|
|
3353
|
-
var
|
|
3354
|
-
if (
|
|
3355
|
-
return
|
|
3356
|
-
}
|
|
3357
|
-
const head = f.total === 1 ? `${f.total} ${highlighter.bold("finding")}` : `${f.total} ${highlighter.bold("findings")}`;
|
|
3358
|
-
const breakdown = [];
|
|
3359
|
-
if (f.errors > 0) {
|
|
3360
|
-
breakdown.push(
|
|
3361
|
-
highlighter.error(`${f.errors} error${f.errors === 1 ? "" : "s"}`)
|
|
3362
|
-
);
|
|
3432
|
+
var severityRank = (group) => {
|
|
3433
|
+
if (group.diagnostics.some((d) => d.severity === "error")) {
|
|
3434
|
+
return 0;
|
|
3363
3435
|
}
|
|
3364
|
-
if (
|
|
3365
|
-
|
|
3366
|
-
highlighter.warn(`${f.warnings} warning${f.warnings === 1 ? "" : "s"}`)
|
|
3367
|
-
);
|
|
3436
|
+
if (group.diagnostics.some((d) => d.severity === "warning")) {
|
|
3437
|
+
return 1;
|
|
3368
3438
|
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3439
|
+
return 2;
|
|
3440
|
+
};
|
|
3441
|
+
var groupByBucket = (diagnostics) => {
|
|
3442
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
3443
|
+
for (const diagnostic of diagnostics) {
|
|
3444
|
+
const list = byBucket.get(diagnostic.bucket) ?? [];
|
|
3445
|
+
list.push(diagnostic);
|
|
3446
|
+
byBucket.set(diagnostic.bucket, list);
|
|
3371
3447
|
}
|
|
3372
|
-
return
|
|
3448
|
+
return [...byBucket.entries()].map(([bucket, list]) => ({ bucket, diagnostics: list })).sort(
|
|
3449
|
+
(a, b) => severityRank(a) - severityRank(b) || b.diagnostics.length - a.diagnostics.length
|
|
3450
|
+
);
|
|
3373
3451
|
};
|
|
3374
3452
|
var severityIcon = (severity) => {
|
|
3375
3453
|
if (severity === "error") {
|
|
3376
|
-
return
|
|
3454
|
+
return glyph.crossMark;
|
|
3377
3455
|
}
|
|
3378
3456
|
if (severity === "warning") {
|
|
3379
|
-
return
|
|
3457
|
+
return glyph.warn;
|
|
3380
3458
|
}
|
|
3381
|
-
return
|
|
3459
|
+
return glyph.bullet;
|
|
3382
3460
|
};
|
|
3383
3461
|
var colorizeBySeverity = (text, severity) => {
|
|
3384
3462
|
if (severity === "error") {
|
|
@@ -3389,26 +3467,6 @@ var colorizeBySeverity = (text, severity) => {
|
|
|
3389
3467
|
}
|
|
3390
3468
|
return highlighter.muted(text);
|
|
3391
3469
|
};
|
|
3392
|
-
var groupByBucket = (diagnostics) => {
|
|
3393
|
-
const byBucket = /* @__PURE__ */ new Map();
|
|
3394
|
-
for (const diagnostic of diagnostics) {
|
|
3395
|
-
const list = byBucket.get(diagnostic.bucket) ?? [];
|
|
3396
|
-
list.push(diagnostic);
|
|
3397
|
-
byBucket.set(diagnostic.bucket, list);
|
|
3398
|
-
}
|
|
3399
|
-
return [...byBucket.entries()].map(([bucket, list]) => ({ bucket, diagnostics: list })).sort(
|
|
3400
|
-
(a, b) => severityRank(a) - severityRank(b) || b.diagnostics.length - a.diagnostics.length
|
|
3401
|
-
);
|
|
3402
|
-
};
|
|
3403
|
-
var severityRank = (group) => {
|
|
3404
|
-
if (group.diagnostics.some((d) => d.severity === "error")) {
|
|
3405
|
-
return 0;
|
|
3406
|
-
}
|
|
3407
|
-
if (group.diagnostics.some((d) => d.severity === "warning")) {
|
|
3408
|
-
return 1;
|
|
3409
|
-
}
|
|
3410
|
-
return 2;
|
|
3411
|
-
};
|
|
3412
3470
|
var groupByRule = (diagnostics) => {
|
|
3413
3471
|
const byRule = /* @__PURE__ */ new Map();
|
|
3414
3472
|
for (const diagnostic of diagnostics) {
|
|
@@ -3418,29 +3476,6 @@ var groupByRule = (diagnostics) => {
|
|
|
3418
3476
|
}
|
|
3419
3477
|
return byRule;
|
|
3420
3478
|
};
|
|
3421
|
-
var buildBucketBreakdownLine = (group, columnWidth) => {
|
|
3422
|
-
const errors = group.diagnostics.filter((d) => d.severity === "error").length;
|
|
3423
|
-
const warnings = group.diagnostics.filter(
|
|
3424
|
-
(d) => d.severity === "warning"
|
|
3425
|
-
).length;
|
|
3426
|
-
const infos = group.diagnostics.filter((d) => d.severity === "info").length;
|
|
3427
|
-
const parts = [];
|
|
3428
|
-
if (errors > 0) {
|
|
3429
|
-
parts.push(
|
|
3430
|
-
highlighter.error(`${errors} ${errors === 1 ? "error" : "errors"}`)
|
|
3431
|
-
);
|
|
3432
|
-
}
|
|
3433
|
-
if (warnings > 0) {
|
|
3434
|
-
parts.push(
|
|
3435
|
-
highlighter.warn(`${warnings} ${warnings === 1 ? "warning" : "warnings"}`)
|
|
3436
|
-
);
|
|
3437
|
-
}
|
|
3438
|
-
if (infos > 0) {
|
|
3439
|
-
parts.push(highlighter.muted(`${infos} info`));
|
|
3440
|
-
}
|
|
3441
|
-
const bucketLabel = group.bucket.padEnd(columnWidth);
|
|
3442
|
-
return ` ${highlighter.bold(bucketLabel)} ${highlighter.muted(POINTER2)} ${parts.join(highlighter.muted(" \xB7 "))}`;
|
|
3443
|
-
};
|
|
3444
3479
|
var buildRuleHeaderLine = (ruleId, ruleDiagnostics, ruleColumnWidth) => {
|
|
3445
3480
|
const first = ruleDiagnostics[0];
|
|
3446
3481
|
const icon = colorizeBySeverity(severityIcon(first.severity), first.severity);
|
|
@@ -3451,55 +3486,7 @@ var buildRuleHeaderLine = (ruleId, ruleDiagnostics, ruleColumnWidth) => {
|
|
|
3451
3486
|
first.severity
|
|
3452
3487
|
);
|
|
3453
3488
|
const citation = first.citation ? ` ${highlighter.muted(first.citation)}` : "";
|
|
3454
|
-
return `
|
|
3455
|
-
};
|
|
3456
|
-
var cardFromContext = (result, context) => buildInfoCard({
|
|
3457
|
-
score: result.score.score,
|
|
3458
|
-
title: result.project.packageName,
|
|
3459
|
-
edition: result.project.edition,
|
|
3460
|
-
suiVersion: context.suiVersion,
|
|
3461
|
-
moduleCount: context.moduleCount,
|
|
3462
|
-
durationMs: context.durationMs,
|
|
3463
|
-
packageCount: context.packageCount,
|
|
3464
|
-
findings: {
|
|
3465
|
-
total: result.score.totalFindings,
|
|
3466
|
-
errors: result.score.bySeverity.error,
|
|
3467
|
-
warnings: result.score.bySeverity.warning,
|
|
3468
|
-
info: result.score.bySeverity.info
|
|
3469
|
-
}
|
|
3470
|
-
});
|
|
3471
|
-
var buildCompactSummary = (result, context) => {
|
|
3472
|
-
const lines = [];
|
|
3473
|
-
lines.push(cardFromContext(result, context));
|
|
3474
|
-
if (result.diagnostics.length > 0) {
|
|
3475
|
-
lines.push("");
|
|
3476
|
-
const bucketGroups = groupByBucket(result.diagnostics);
|
|
3477
|
-
const bucketColumnWidth = Math.max(
|
|
3478
|
-
...bucketGroups.map((g) => g.bucket.length),
|
|
3479
|
-
0
|
|
3480
|
-
);
|
|
3481
|
-
for (const group of bucketGroups) {
|
|
3482
|
-
lines.push(buildBucketBreakdownLine(group, bucketColumnWidth));
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
3485
|
-
if (!(result.compilerLintAvailable || context.hasSuiCli)) {
|
|
3486
|
-
lines.push("");
|
|
3487
|
-
lines.push(
|
|
3488
|
-
highlighter.muted(
|
|
3489
|
-
` ${POINTER2} Sui CLI not found on PATH \u2014 compiler lints (W0*) skipped.`
|
|
3490
|
-
)
|
|
3491
|
-
);
|
|
3492
|
-
}
|
|
3493
|
-
const nextSteps = buildNextSteps({
|
|
3494
|
-
result,
|
|
3495
|
-
hasInstalledSkill: context.hasInstalledSkill,
|
|
3496
|
-
hasSuiCli: context.hasSuiCli
|
|
3497
|
-
});
|
|
3498
|
-
if (nextSteps.length > 0) {
|
|
3499
|
-
lines.push("");
|
|
3500
|
-
lines.push(...nextSteps);
|
|
3501
|
-
}
|
|
3502
|
-
return lines.join("\n");
|
|
3489
|
+
return ` ${icon} ${padded}${citation}${countBadge}`;
|
|
3503
3490
|
};
|
|
3504
3491
|
var indentBlock = (text, prefix) => text.split("\n").map((line) => `${prefix}${line.trimStart()}`).join("\n");
|
|
3505
3492
|
var buildLocation = (diagnostic, rootDirectory) => {
|
|
@@ -3507,44 +3494,34 @@ var buildLocation = (diagnostic, rootDirectory) => {
|
|
|
3507
3494
|
return `${relativePath}:${diagnostic.line}:${diagnostic.column}`;
|
|
3508
3495
|
};
|
|
3509
3496
|
var buildVerboseRuleGroup = (ruleId, ruleDiagnostics, ruleColumnWidth, rootDirectory) => {
|
|
3510
|
-
const lines = [
|
|
3511
|
-
|
|
3497
|
+
const lines = [
|
|
3498
|
+
buildRuleHeaderLine(ruleId, ruleDiagnostics, ruleColumnWidth)
|
|
3499
|
+
];
|
|
3512
3500
|
const first = ruleDiagnostics[0];
|
|
3513
|
-
lines.push(highlighter.muted(indentBlock(first.message, "
|
|
3501
|
+
lines.push(highlighter.muted(indentBlock(first.message, " ")));
|
|
3514
3502
|
if (first.fixHint) {
|
|
3515
|
-
|
|
3516
|
-
|
|
3503
|
+
lines.push(
|
|
3504
|
+
highlighter.muted(
|
|
3505
|
+
indentBlock(`${glyph.pointer} ${first.fixHint}`, " ")
|
|
3506
|
+
)
|
|
3507
|
+
);
|
|
3517
3508
|
}
|
|
3518
3509
|
for (const diagnostic of ruleDiagnostics) {
|
|
3519
3510
|
lines.push(
|
|
3520
|
-
highlighter.muted(`
|
|
3511
|
+
highlighter.muted(` ${buildLocation(diagnostic, rootDirectory)}`)
|
|
3521
3512
|
);
|
|
3522
3513
|
}
|
|
3523
3514
|
lines.push("");
|
|
3524
3515
|
return lines;
|
|
3525
3516
|
};
|
|
3526
|
-
var
|
|
3517
|
+
var buildBucketRuleDetail = (diagnostics, rootDirectory) => {
|
|
3527
3518
|
const lines = [];
|
|
3528
|
-
|
|
3529
|
-
if (result.diagnostics.length === 0) {
|
|
3530
|
-
if (!(result.compilerLintAvailable || context.hasSuiCli)) {
|
|
3531
|
-
lines.push("");
|
|
3532
|
-
lines.push(
|
|
3533
|
-
highlighter.muted(
|
|
3534
|
-
` ${POINTER2} Sui CLI not found on PATH \u2014 compiler lints (W0*) skipped.`
|
|
3535
|
-
)
|
|
3536
|
-
);
|
|
3537
|
-
}
|
|
3538
|
-
return lines.join("\n");
|
|
3539
|
-
}
|
|
3540
|
-
lines.push("");
|
|
3541
|
-
for (const group of groupByBucket(result.diagnostics)) {
|
|
3542
|
-
const ruleGroups = groupByRule(group.diagnostics);
|
|
3543
|
-
const bucketColumnWidth = Math.max(
|
|
3544
|
-
...groupByBucket(result.diagnostics).map((g) => g.bucket.length)
|
|
3545
|
-
);
|
|
3546
|
-
lines.push(buildBucketBreakdownLine(group, bucketColumnWidth));
|
|
3519
|
+
for (const group of groupByBucket(diagnostics)) {
|
|
3547
3520
|
lines.push("");
|
|
3521
|
+
lines.push(
|
|
3522
|
+
` ${severityDot(countSeverities(group.diagnostics))} ${highlighter.bold(group.bucket)}`
|
|
3523
|
+
);
|
|
3524
|
+
const ruleGroups = groupByRule(group.diagnostics);
|
|
3548
3525
|
const ruleColumnWidth = Math.max(
|
|
3549
3526
|
...[...ruleGroups.keys()].map((ruleId) => ruleId.length),
|
|
3550
3527
|
0
|
|
@@ -3555,27 +3532,178 @@ var buildVerboseSummary = (result, context) => {
|
|
|
3555
3532
|
ruleId,
|
|
3556
3533
|
ruleDiagnostics,
|
|
3557
3534
|
ruleColumnWidth,
|
|
3558
|
-
|
|
3535
|
+
rootDirectory
|
|
3559
3536
|
)
|
|
3560
3537
|
);
|
|
3561
3538
|
}
|
|
3562
3539
|
}
|
|
3563
|
-
|
|
3540
|
+
return lines;
|
|
3541
|
+
};
|
|
3542
|
+
|
|
3543
|
+
// src/cli/utils/commands.ts
|
|
3544
|
+
var NPX = "npx move-doctor@latest";
|
|
3545
|
+
var CMD = {
|
|
3546
|
+
verbose: `${NPX} --verbose`,
|
|
3547
|
+
verboseHere: `${NPX} . --verbose`,
|
|
3548
|
+
install: `${NPX} install`};
|
|
3549
|
+
var SUI_INSTALL_URL = "https://docs.sui.io/guides/developer/getting-started/sui-install";
|
|
3550
|
+
|
|
3551
|
+
// src/cli/utils/next-steps.ts
|
|
3552
|
+
var buildNextStep = (line) => ({ line });
|
|
3553
|
+
var findHottestBucket = (result) => {
|
|
3554
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3555
|
+
const severityWeight = (severity) => {
|
|
3556
|
+
if (severity === "error") {
|
|
3557
|
+
return 8;
|
|
3558
|
+
}
|
|
3559
|
+
if (severity === "warning") {
|
|
3560
|
+
return 3;
|
|
3561
|
+
}
|
|
3562
|
+
return 1;
|
|
3563
|
+
};
|
|
3564
|
+
for (const diagnostic of result.diagnostics) {
|
|
3565
|
+
const weight = severityWeight(diagnostic.severity);
|
|
3566
|
+
buckets.set(
|
|
3567
|
+
diagnostic.bucket,
|
|
3568
|
+
(buckets.get(diagnostic.bucket) ?? 0) + weight
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
let best = null;
|
|
3572
|
+
let bestWeight = 0;
|
|
3573
|
+
for (const [bucket, weight] of buckets) {
|
|
3574
|
+
if (weight > bestWeight) {
|
|
3575
|
+
best = bucket;
|
|
3576
|
+
bestWeight = weight;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return best;
|
|
3580
|
+
};
|
|
3581
|
+
var buildNextSteps = (context) => {
|
|
3582
|
+
const { result, hasInstalledSkill, hasSuiCli } = context;
|
|
3583
|
+
const lines = [];
|
|
3584
|
+
const steps = [];
|
|
3585
|
+
if (result.diagnostics.length > 0) {
|
|
3586
|
+
steps.push(
|
|
3587
|
+
buildNextStep(
|
|
3588
|
+
`${highlighter.muted(glyph.pointer)} Run ${highlighter.accent("--verbose")} for file refs and fix hints.`
|
|
3589
|
+
)
|
|
3590
|
+
);
|
|
3591
|
+
const hottest = findHottestBucket(result);
|
|
3592
|
+
if (hottest) {
|
|
3593
|
+
steps.push(
|
|
3594
|
+
buildNextStep(
|
|
3595
|
+
`${highlighter.muted(glyph.pointer)} Focus on ${highlighter.bold(hottest)} first: ${highlighter.accent(CMD.verboseHere)}`
|
|
3596
|
+
)
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
if (!hasInstalledSkill) {
|
|
3601
|
+
steps.push(
|
|
3602
|
+
buildNextStep(
|
|
3603
|
+
`${highlighter.muted(glyph.pointer)} Install the agent skill so your agent can auto-fix findings: ${highlighter.accent(CMD.install)}`
|
|
3604
|
+
)
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
if (!hasSuiCli) {
|
|
3608
|
+
steps.push(
|
|
3609
|
+
buildNextStep(
|
|
3610
|
+
`${highlighter.muted(glyph.pointer)} Install the Sui CLI to enable compiler lints (W0*): ${highlighter.accent(SUI_INSTALL_URL)}`
|
|
3611
|
+
)
|
|
3612
|
+
);
|
|
3613
|
+
}
|
|
3614
|
+
for (const step of steps) {
|
|
3615
|
+
lines.push(` ${step.line}`);
|
|
3616
|
+
}
|
|
3617
|
+
return lines;
|
|
3618
|
+
};
|
|
3619
|
+
|
|
3620
|
+
// src/cli/render.ts
|
|
3621
|
+
var buildAreaBreakdown = (diagnostics) => {
|
|
3622
|
+
const groups = groupByBucket(diagnostics);
|
|
3623
|
+
const labelWidth = Math.max(...groups.map((g) => g.bucket.length), 0);
|
|
3624
|
+
const maxTotal = Math.max(...groups.map((g) => g.diagnostics.length), 1);
|
|
3625
|
+
const lines = ["", ` ${highlighter.bold("by area")}`];
|
|
3626
|
+
for (const group of groups) {
|
|
3627
|
+
const counts = countSeverities(group.diagnostics);
|
|
3628
|
+
const bar = magnitudeBar(counts, BAR_WIDTH, maxTotal);
|
|
3629
|
+
const count = highlighter.bold(
|
|
3630
|
+
String(group.diagnostics.length).padStart(3)
|
|
3631
|
+
);
|
|
3564
3632
|
lines.push(
|
|
3565
|
-
|
|
3566
|
-
|
|
3633
|
+
barRow(
|
|
3634
|
+
severityDot(counts),
|
|
3635
|
+
group.bucket.padEnd(labelWidth),
|
|
3636
|
+
bar,
|
|
3637
|
+
count,
|
|
3638
|
+
severitySummary(counts)
|
|
3567
3639
|
)
|
|
3568
3640
|
);
|
|
3569
|
-
lines.push("");
|
|
3570
3641
|
}
|
|
3642
|
+
return lines;
|
|
3643
|
+
};
|
|
3644
|
+
var buildFindingsDetail = (diagnostics, rootDirectory) => [
|
|
3645
|
+
"",
|
|
3646
|
+
` ${highlighter.bold("findings")}`,
|
|
3647
|
+
...buildBucketRuleDetail(diagnostics, rootDirectory)
|
|
3648
|
+
];
|
|
3649
|
+
var headerFromContext = (result, context) => buildDiagnosisHeader({
|
|
3650
|
+
score: result.score.score,
|
|
3651
|
+
title: result.project.packageName,
|
|
3652
|
+
edition: result.project.edition,
|
|
3653
|
+
suiVersion: context.suiVersion,
|
|
3654
|
+
moduleCount: context.moduleCount,
|
|
3655
|
+
durationMs: context.durationMs,
|
|
3656
|
+
packageCount: context.packageCount,
|
|
3657
|
+
findings: {
|
|
3658
|
+
total: result.score.totalFindings,
|
|
3659
|
+
errors: result.score.bySeverity.error,
|
|
3660
|
+
warnings: result.score.bySeverity.warning,
|
|
3661
|
+
info: result.score.bySeverity.info
|
|
3662
|
+
}
|
|
3663
|
+
});
|
|
3664
|
+
var suiMissingNote = () => highlighter.muted(
|
|
3665
|
+
` ${glyph.bullet} Sui CLI not found on PATH \u2014 compiler lints (W0*) skipped.`
|
|
3666
|
+
);
|
|
3667
|
+
var appendNextSteps = (lines, result, context) => {
|
|
3571
3668
|
const nextSteps = buildNextSteps({
|
|
3572
3669
|
result,
|
|
3573
3670
|
hasInstalledSkill: context.hasInstalledSkill,
|
|
3574
3671
|
hasSuiCli: context.hasSuiCli
|
|
3575
3672
|
});
|
|
3576
3673
|
if (nextSteps.length > 0) {
|
|
3674
|
+
lines.push("");
|
|
3577
3675
|
lines.push(...nextSteps);
|
|
3578
3676
|
}
|
|
3677
|
+
};
|
|
3678
|
+
var buildCompactSummary = (result, context) => {
|
|
3679
|
+
const lines = [headerFromContext(result, context)];
|
|
3680
|
+
if (result.diagnostics.length > 0) {
|
|
3681
|
+
lines.push(...buildAreaBreakdown(result.diagnostics));
|
|
3682
|
+
}
|
|
3683
|
+
if (!(result.compilerLintAvailable || context.hasSuiCli)) {
|
|
3684
|
+
lines.push("");
|
|
3685
|
+
lines.push(suiMissingNote());
|
|
3686
|
+
}
|
|
3687
|
+
appendNextSteps(lines, result, context);
|
|
3688
|
+
return lines.join("\n");
|
|
3689
|
+
};
|
|
3690
|
+
var buildVerboseSummary = (result, context) => {
|
|
3691
|
+
const lines = [headerFromContext(result, context)];
|
|
3692
|
+
if (result.diagnostics.length === 0) {
|
|
3693
|
+
if (!(result.compilerLintAvailable || context.hasSuiCli)) {
|
|
3694
|
+
lines.push("");
|
|
3695
|
+
lines.push(suiMissingNote());
|
|
3696
|
+
}
|
|
3697
|
+
return lines.join("\n").trimEnd();
|
|
3698
|
+
}
|
|
3699
|
+
lines.push(...buildAreaBreakdown(result.diagnostics));
|
|
3700
|
+
lines.push(
|
|
3701
|
+
...buildFindingsDetail(result.diagnostics, result.project.rootDirectory)
|
|
3702
|
+
);
|
|
3703
|
+
if (!(result.compilerLintAvailable || context.hasSuiCli)) {
|
|
3704
|
+
lines.push(suiMissingNote());
|
|
3705
|
+
}
|
|
3706
|
+
appendNextSteps(lines, result, context);
|
|
3579
3707
|
return lines.join("\n").trimEnd();
|
|
3580
3708
|
};
|
|
3581
3709
|
var renderText = (result, options) => {
|
|
@@ -3602,19 +3730,19 @@ var renderJson = (result) => JSON.stringify(
|
|
|
3602
3730
|
);
|
|
3603
3731
|
|
|
3604
3732
|
// src/cli/render-workspace.ts
|
|
3605
|
-
var
|
|
3606
|
-
var
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
};
|
|
3611
|
-
var
|
|
3733
|
+
var SCORE_COL = `${PERFECT_SCORE}/${PERFECT_SCORE}`.length;
|
|
3734
|
+
var countsOf = (entry) => ({
|
|
3735
|
+
errors: entry.score.bySeverity.error,
|
|
3736
|
+
warnings: entry.score.bySeverity.warning,
|
|
3737
|
+
info: entry.score.bySeverity.info
|
|
3738
|
+
});
|
|
3739
|
+
var buildWorkspaceHeader = (workspace, result, suiVersion, durationMs) => {
|
|
3612
3740
|
const workspaceName = workspace.rootDirectory.split(/[\\/]/).pop() ?? "workspace";
|
|
3613
3741
|
const moduleCount = result.perPackage.reduce(
|
|
3614
3742
|
(sum, entry) => sum + (entry.skipped ? 0 : entry.moduleCount),
|
|
3615
3743
|
0
|
|
3616
3744
|
);
|
|
3617
|
-
return
|
|
3745
|
+
return buildDiagnosisHeader({
|
|
3618
3746
|
score: result.aggregateScore.score,
|
|
3619
3747
|
title: workspaceName,
|
|
3620
3748
|
suiVersion,
|
|
@@ -3642,152 +3770,95 @@ var formatPackageName = (relativePath, width) => {
|
|
|
3642
3770
|
const leaf = relativePath.slice(sepIndex + 1);
|
|
3643
3771
|
return `${highlighter.muted(prefix)}${leaf}${pad}`;
|
|
3644
3772
|
};
|
|
3645
|
-
var
|
|
3646
|
-
const
|
|
3647
|
-
lines.push("");
|
|
3648
|
-
lines.push(` ${highlighter.bold("Per-package scores")}`);
|
|
3649
|
-
const nameColWidth = Math.max(
|
|
3773
|
+
var buildPackageBreakdown = (perPackage) => {
|
|
3774
|
+
const labelWidth = Math.max(
|
|
3650
3775
|
...perPackage.map((entry) => entry.relativePath.length),
|
|
3651
|
-
"
|
|
3776
|
+
"package".length
|
|
3777
|
+
);
|
|
3778
|
+
const maxTotal = Math.max(
|
|
3779
|
+
...perPackage.filter((entry) => !entry.skipped).map((entry) => entry.diagnostics.length),
|
|
3780
|
+
1
|
|
3652
3781
|
);
|
|
3653
|
-
const barWidth = 18;
|
|
3654
3782
|
const sorted = [...perPackage].sort((a, b) => a.score.score - b.score.score);
|
|
3783
|
+
const lines = [
|
|
3784
|
+
"",
|
|
3785
|
+
` ${highlighter.bold("by package")} ${highlighter.muted(`(score out of ${PERFECT_SCORE})`)}`
|
|
3786
|
+
];
|
|
3655
3787
|
for (const entry of sorted) {
|
|
3656
|
-
const name = formatPackageName(entry.relativePath,
|
|
3788
|
+
const name = formatPackageName(entry.relativePath, labelWidth);
|
|
3657
3789
|
if (entry.skipped) {
|
|
3658
3790
|
lines.push(
|
|
3659
|
-
|
|
3791
|
+
barRow(
|
|
3792
|
+
highlighter.muted(glyph.dotOpen),
|
|
3793
|
+
name,
|
|
3794
|
+
" ".repeat(BAR_WIDTH),
|
|
3795
|
+
highlighter.muted("\u2014".padStart(SCORE_COL)),
|
|
3796
|
+
highlighter.muted("skipped \xB7 no changed files")
|
|
3797
|
+
)
|
|
3660
3798
|
);
|
|
3661
3799
|
continue;
|
|
3662
3800
|
}
|
|
3663
|
-
const
|
|
3664
|
-
const
|
|
3665
|
-
const
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
const warnings = entry.score.bySeverity.warning;
|
|
3669
|
-
const infos = entry.score.bySeverity.info;
|
|
3670
|
-
const findings = [];
|
|
3671
|
-
if (errors > 0) {
|
|
3672
|
-
findings.push(highlighter.error(`${errors}e`));
|
|
3673
|
-
}
|
|
3674
|
-
if (warnings > 0) {
|
|
3675
|
-
findings.push(highlighter.warn(`${warnings}w`));
|
|
3676
|
-
}
|
|
3677
|
-
if (infos > 0) {
|
|
3678
|
-
findings.push(highlighter.muted(`${infos}i`));
|
|
3679
|
-
}
|
|
3680
|
-
const findingsCol = findings.length > 0 ? findings.join(" ") : highlighter.muted("clean");
|
|
3681
|
-
lines.push(
|
|
3682
|
-
` ${name} ${scoreText} ${bar} ${label.padEnd(10)} ${findingsCol}`
|
|
3801
|
+
const counts = countsOf(entry);
|
|
3802
|
+
const bar = magnitudeBar(counts, BAR_WIDTH, maxTotal);
|
|
3803
|
+
const score = colorizeByScore(
|
|
3804
|
+
`${entry.score.score}/${PERFECT_SCORE}`.padStart(SCORE_COL),
|
|
3805
|
+
entry.score.score
|
|
3683
3806
|
);
|
|
3807
|
+
const trailing = entry.diagnostics.length === 0 ? highlighter.ok("clean") : severitySummary(counts);
|
|
3808
|
+
lines.push(barRow(severityDot(counts), name, bar, score, trailing));
|
|
3684
3809
|
}
|
|
3685
|
-
return lines
|
|
3686
|
-
};
|
|
3687
|
-
var severityIcon2 = (severity) => {
|
|
3688
|
-
if (severity === "error") {
|
|
3689
|
-
return "\u2717";
|
|
3690
|
-
}
|
|
3691
|
-
if (severity === "warning") {
|
|
3692
|
-
return "\u26A0";
|
|
3693
|
-
}
|
|
3694
|
-
return "\xB7";
|
|
3695
|
-
};
|
|
3696
|
-
var colorizeBySeverity2 = (text, severity) => {
|
|
3697
|
-
if (severity === "error") {
|
|
3698
|
-
return highlighter.error(text);
|
|
3699
|
-
}
|
|
3700
|
-
if (severity === "warning") {
|
|
3701
|
-
return highlighter.warn(text);
|
|
3702
|
-
}
|
|
3703
|
-
return highlighter.muted(text);
|
|
3810
|
+
return lines;
|
|
3704
3811
|
};
|
|
3705
3812
|
var buildWorkspaceNextSteps = (options) => {
|
|
3706
3813
|
const lines = [];
|
|
3707
3814
|
if (!options.verbose) {
|
|
3708
3815
|
lines.push(
|
|
3709
|
-
` ${highlighter.muted(
|
|
3816
|
+
` ${highlighter.muted(glyph.pointer)} For per-finding detail: ${highlighter.accent(CMD.verbose)}`
|
|
3710
3817
|
);
|
|
3711
3818
|
}
|
|
3712
3819
|
if (!options.hasInstalledSkill) {
|
|
3713
3820
|
lines.push(
|
|
3714
|
-
` ${highlighter.muted(
|
|
3821
|
+
` ${highlighter.muted(glyph.pointer)} Install the agent skill: ${highlighter.accent(CMD.install)}`
|
|
3715
3822
|
);
|
|
3716
3823
|
}
|
|
3717
3824
|
if (!options.hasSuiCli) {
|
|
3718
3825
|
lines.push(
|
|
3719
|
-
` ${highlighter.muted(
|
|
3826
|
+
` ${highlighter.muted(glyph.pointer)} Install the Sui CLI to enable compiler lints (W0*).`
|
|
3720
3827
|
);
|
|
3721
3828
|
}
|
|
3722
|
-
lines.
|
|
3723
|
-
` ${highlighter.muted(POINTER3)} Full rule catalog: ${highlighter.accent("https://move.doctor/docs/rules")}`
|
|
3724
|
-
);
|
|
3725
|
-
return lines.length > 0 ? `
|
|
3726
|
-
${lines.join("\n")}` : "";
|
|
3829
|
+
return lines.length > 0 ? ["", ...lines] : [];
|
|
3727
3830
|
};
|
|
3728
|
-
var
|
|
3729
|
-
const
|
|
3831
|
+
var buildWorkspaceDetail = (perPackage) => {
|
|
3832
|
+
const lines = ["", ` ${highlighter.bold("findings")}`];
|
|
3730
3833
|
for (const entry of perPackage) {
|
|
3731
3834
|
if (entry.diagnostics.length === 0) {
|
|
3732
3835
|
continue;
|
|
3733
3836
|
}
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3837
|
+
const count = entry.diagnostics.length;
|
|
3838
|
+
lines.push("");
|
|
3839
|
+
lines.push(
|
|
3840
|
+
` ${severityDot(countsOf(entry))} ${highlighter.bold(entry.relativePath)} ${highlighter.muted(`(${count} finding${count === 1 ? "" : "s"})`)}`
|
|
3737
3841
|
);
|
|
3738
|
-
|
|
3739
|
-
...entry.diagnostics
|
|
3740
|
-
0
|
|
3842
|
+
lines.push(
|
|
3843
|
+
...buildBucketRuleDetail(entry.diagnostics, entry.project.rootDirectory)
|
|
3741
3844
|
);
|
|
3742
|
-
const byRule = /* @__PURE__ */ new Map();
|
|
3743
|
-
for (const diagnostic of entry.diagnostics) {
|
|
3744
|
-
const list = byRule.get(diagnostic.ruleId) ?? [];
|
|
3745
|
-
list.push(diagnostic);
|
|
3746
|
-
byRule.set(diagnostic.ruleId, list);
|
|
3747
|
-
}
|
|
3748
|
-
for (const [ruleId, diagnostics] of byRule) {
|
|
3749
|
-
const first = diagnostics[0];
|
|
3750
|
-
const icon = colorizeBySeverity2(
|
|
3751
|
-
severityIcon2(first.severity),
|
|
3752
|
-
first.severity
|
|
3753
|
-
);
|
|
3754
|
-
const ruleText = colorizeBySeverity2(
|
|
3755
|
-
ruleId.padEnd(ruleColWidth),
|
|
3756
|
-
first.severity
|
|
3757
|
-
);
|
|
3758
|
-
const count = diagnostics.length > 1 ? ` ${highlighter.muted(`\xD7${diagnostics.length}`)}` : "";
|
|
3759
|
-
sections.push(` ${icon} ${ruleText}${count}`);
|
|
3760
|
-
sections.push(highlighter.muted(` ${first.message}`));
|
|
3761
|
-
if (first.fixHint) {
|
|
3762
|
-
sections.push(highlighter.muted(` ${POINTER3} ${first.fixHint}`));
|
|
3763
|
-
}
|
|
3764
|
-
for (const diagnostic of diagnostics) {
|
|
3765
|
-
const rel = `${entry.relativePath}/${diagnostic.filePath.split(/[\\/]/).at(-1)}`;
|
|
3766
|
-
sections.push(
|
|
3767
|
-
highlighter.muted(
|
|
3768
|
-
` ${rel}:${diagnostic.line}:${diagnostic.column}`
|
|
3769
|
-
)
|
|
3770
|
-
);
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
3845
|
}
|
|
3774
|
-
return
|
|
3846
|
+
return lines;
|
|
3775
3847
|
};
|
|
3776
3848
|
var renderWorkspaceText = (options) => {
|
|
3777
|
-
const sections = [
|
|
3778
|
-
|
|
3779
|
-
buildWorkspaceScoreHeader(
|
|
3849
|
+
const sections = [
|
|
3850
|
+
buildWorkspaceHeader(
|
|
3780
3851
|
options.workspace,
|
|
3781
3852
|
options.result,
|
|
3782
3853
|
options.suiVersion,
|
|
3783
3854
|
options.durationMs ?? null
|
|
3784
|
-
)
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
if (options.verbose) {
|
|
3788
|
-
sections.push(
|
|
3855
|
+
),
|
|
3856
|
+
...buildPackageBreakdown(options.result.perPackage)
|
|
3857
|
+
];
|
|
3858
|
+
if (options.verbose && options.result.diagnostics.length > 0) {
|
|
3859
|
+
sections.push(...buildWorkspaceDetail(options.result.perPackage));
|
|
3789
3860
|
}
|
|
3790
|
-
sections.push(buildWorkspaceNextSteps(options));
|
|
3861
|
+
sections.push(...buildWorkspaceNextSteps(options));
|
|
3791
3862
|
return sections.join("\n");
|
|
3792
3863
|
};
|
|
3793
3864
|
var renderWorkspaceJson = (workspace, result) => JSON.stringify(
|
|
@@ -3815,11 +3886,12 @@ var renderWorkspaceJson = (workspace, result) => JSON.stringify(
|
|
|
3815
3886
|
var renderWorkspaceScoreOnly = (result) => String(result.aggregateScore.score);
|
|
3816
3887
|
|
|
3817
3888
|
// src/cli/utils/branded-header.ts
|
|
3818
|
-
var VERSION = "0.
|
|
3889
|
+
var VERSION = "0.3.0-dev.e789f4b";
|
|
3819
3890
|
var printBrandedHeader = () => {
|
|
3891
|
+
const wordmark = highlighter.accent(hyperlink("move.doctor", HOMEPAGE));
|
|
3820
3892
|
process.stdout.write(
|
|
3821
3893
|
`
|
|
3822
|
-
${highlighter.bold("move-doctor")} ${highlighter.muted(`v${VERSION}`)} ${highlighter.muted("\xB7")} ${
|
|
3894
|
+
${highlighter.accent(glyph.cross)} ${highlighter.bold("move-doctor")} ${highlighter.muted(`v${VERSION}`)} ${highlighter.muted("\xB7")} ${wordmark}
|
|
3823
3895
|
|
|
3824
3896
|
`
|
|
3825
3897
|
);
|
|
@@ -3843,13 +3915,17 @@ var runCommand2 = (command, args, cwd) => new Promise((resolve9) => {
|
|
|
3843
3915
|
resolve9(null);
|
|
3844
3916
|
}
|
|
3845
3917
|
});
|
|
3846
|
-
var
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3918
|
+
var suiVersionPromise;
|
|
3919
|
+
var detectSuiVersion = (cwd) => {
|
|
3920
|
+
suiVersionPromise ??= (async () => {
|
|
3921
|
+
const result = await runCommand2("sui", ["--version"], cwd);
|
|
3922
|
+
if (!result || result.exitCode !== 0) {
|
|
3923
|
+
return null;
|
|
3924
|
+
}
|
|
3925
|
+
const match = result.stdout.match(/sui\s+(\S+)/);
|
|
3926
|
+
return match?.[1] ?? null;
|
|
3927
|
+
})();
|
|
3928
|
+
return suiVersionPromise;
|
|
3853
3929
|
};
|
|
3854
3930
|
var detectGitChanges = async (cwd) => {
|
|
3855
3931
|
const result = await runCommand2("git", ["status", "--porcelain"], cwd);
|
|
@@ -3887,13 +3963,12 @@ var countSourceFiles = async (rootDirectory) => {
|
|
|
3887
3963
|
if (!existsSync(sourcesDirectory)) {
|
|
3888
3964
|
return 0;
|
|
3889
3965
|
}
|
|
3890
|
-
const { readdir: readdir2 } = await import('fs/promises');
|
|
3891
3966
|
let count = 0;
|
|
3892
3967
|
const queue = [sourcesDirectory];
|
|
3893
3968
|
while (queue.length > 0) {
|
|
3894
3969
|
const current = queue.shift();
|
|
3895
3970
|
try {
|
|
3896
|
-
const entries = await
|
|
3971
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
3897
3972
|
for (const entry of entries) {
|
|
3898
3973
|
if (entry.name.startsWith(".") || entry.name === "build") {
|
|
3899
3974
|
continue;
|
|
@@ -3937,6 +4012,46 @@ var detectContext = async (startDirectory) => {
|
|
|
3937
4012
|
sourceFileCount
|
|
3938
4013
|
};
|
|
3939
4014
|
};
|
|
4015
|
+
|
|
4016
|
+
// src/cli/skill-content.ts
|
|
4017
|
+
var SKILL_MD_CONTENT = `---
|
|
4018
|
+
name: move-doctor
|
|
4019
|
+
description: Use when finishing a Sui Move feature, fixing a bug, before committing Move code, or when the user types /movedoctor, asks to scan, triage, or clean up Move diagnostics. Covers Move Book conventions, Move 2024 idioms, ability mistakes, testing style, and security best practices.
|
|
4020
|
+
version: "0.1.0"
|
|
4021
|
+
---
|
|
4022
|
+
|
|
4023
|
+
# Move Doctor
|
|
4024
|
+
|
|
4025
|
+
Scans Sui Move codebases for convention, idiom, ability, testing, and security issues. Outputs a 0\u2013100 health score.
|
|
4026
|
+
|
|
4027
|
+
## After making Move code changes
|
|
4028
|
+
|
|
4029
|
+
Run \`npx move-doctor@latest --verbose --diff\` and check the score did not regress. If it dropped, fix the regressions before committing.
|
|
4030
|
+
|
|
4031
|
+
## For a full cleanup pass
|
|
4032
|
+
|
|
4033
|
+
Run \`npx move-doctor@latest --verbose\` (without \`--diff\`) to scan the whole codebase. Fix issues by severity \u2014 errors first, then warnings, then info.
|
|
4034
|
+
|
|
4035
|
+
## /movedoctor \u2014 full triage workflow
|
|
4036
|
+
|
|
4037
|
+
When the user types \`/movedoctor\`, says "run move doctor", or asks for a full triage / cleanup pass, fetch the canonical playbook and follow every step (fetching it means updates ship without a skill reinstall):
|
|
4038
|
+
|
|
4039
|
+
\`\`\`bash
|
|
4040
|
+
curl --fail --silent --show-error \\
|
|
4041
|
+
--header 'Cache-Control: no-cache' \\
|
|
4042
|
+
https://move.doctor/prompts/move-doctor-agent.md
|
|
4043
|
+
\`\`\`
|
|
4044
|
+
|
|
4045
|
+
It's a scan \u2192 triage \u2192 fix \u2192 re-score loop that edits the working tree directly (never commits, never opens PRs). Every finding in \`--json\` carries its own \`fixHint\` and \`citation\` \u2014 fix straight from those; there's nothing else to fetch.
|
|
4046
|
+
|
|
4047
|
+
If the fetch fails (offline / site down), fall back to: run \`move-doctor --verbose\`, fix errors first (\`security/*\` and \`abilities/*\` findings are real vulnerabilities, not style), apply each finding's \`fixHint\`, and re-run until the score stops rising. Never silence a finding unless you can explain why the surrounding code is a documented exception.
|
|
4048
|
+
|
|
4049
|
+
## Command
|
|
4050
|
+
|
|
4051
|
+
\`\`\`bash
|
|
4052
|
+
npx move-doctor@latest [path] --verbose --diff # --score for CI gating \xB7 --help for all flags
|
|
4053
|
+
\`\`\`
|
|
4054
|
+
`;
|
|
3940
4055
|
var AGENT_HOOK_TIMEOUT_SECONDS = 120;
|
|
3941
4056
|
var EXECUTABLE_MODE = 493;
|
|
3942
4057
|
var JSON_INDENT_SPACES = 2;
|
|
@@ -4183,134 +4298,52 @@ var disableSetupPrompt = async (projectRoot) => {
|
|
|
4183
4298
|
};
|
|
4184
4299
|
await writeConfig({ ...config, projects });
|
|
4185
4300
|
};
|
|
4186
|
-
var
|
|
4301
|
+
var abort = () => {
|
|
4302
|
+
cancel("Cancelled.");
|
|
4303
|
+
process.exit(130);
|
|
4304
|
+
};
|
|
4187
4305
|
var select = async (options) => {
|
|
4188
4306
|
if (!isInteractive()) {
|
|
4189
4307
|
return null;
|
|
4190
4308
|
}
|
|
4191
|
-
const
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
process.stderr.write(
|
|
4206
|
-
`
|
|
4207
|
-
${highlighter.muted(POINTER4)} Cancelled.
|
|
4208
|
-
|
|
4209
|
-
`
|
|
4210
|
-
);
|
|
4211
|
-
process.exit(130);
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
);
|
|
4215
|
-
return answer.value ?? null;
|
|
4309
|
+
const initialValue = options.choices[options.initial ?? 0]?.value;
|
|
4310
|
+
const result = await select$1({
|
|
4311
|
+
message: options.message,
|
|
4312
|
+
options: options.choices.map((choice) => ({
|
|
4313
|
+
value: choice.value,
|
|
4314
|
+
label: choice.title,
|
|
4315
|
+
...choice.description ? { hint: choice.description } : {}
|
|
4316
|
+
})),
|
|
4317
|
+
...initialValue === void 0 ? {} : { initialValue }
|
|
4318
|
+
});
|
|
4319
|
+
if (isCancel(result)) {
|
|
4320
|
+
abort();
|
|
4321
|
+
}
|
|
4322
|
+
return result;
|
|
4216
4323
|
};
|
|
4217
4324
|
var multiselect = async (options) => {
|
|
4218
4325
|
if (!isInteractive()) {
|
|
4219
4326
|
return null;
|
|
4220
4327
|
}
|
|
4221
|
-
const
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
}))
|
|
4236
|
-
},
|
|
4237
|
-
{
|
|
4238
|
-
onCancel: () => {
|
|
4239
|
-
process.stderr.write(
|
|
4240
|
-
`
|
|
4241
|
-
${highlighter.muted(POINTER4)} Cancelled.
|
|
4242
|
-
|
|
4243
|
-
`
|
|
4244
|
-
);
|
|
4245
|
-
process.exit(130);
|
|
4246
|
-
}
|
|
4247
|
-
}
|
|
4248
|
-
);
|
|
4249
|
-
return answer.values ?? null;
|
|
4250
|
-
};
|
|
4251
|
-
|
|
4252
|
-
// src/cli/utils/spinner.ts
|
|
4253
|
-
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4254
|
-
var FRAME_INTERVAL_MS = 80;
|
|
4255
|
-
var ERASE_LINE = "\r\x1B[K";
|
|
4256
|
-
var startSpinner = (initialText) => {
|
|
4257
|
-
if (!isInteractive()) {
|
|
4258
|
-
process.stderr.write(` ${highlighter.muted("\u2026")} ${initialText}
|
|
4259
|
-
`);
|
|
4260
|
-
return {
|
|
4261
|
-
update: () => {
|
|
4262
|
-
},
|
|
4263
|
-
succeed: (text) => process.stderr.write(` ${highlighter.ok("\u2713")} ${text}
|
|
4264
|
-
`),
|
|
4265
|
-
fail: (text) => process.stderr.write(` ${highlighter.error("\u2717")} ${text}
|
|
4266
|
-
`),
|
|
4267
|
-
stop: () => {
|
|
4268
|
-
}
|
|
4269
|
-
};
|
|
4328
|
+
const initialValues = options.choices.filter((choice) => choice.selected).map((choice) => choice.value);
|
|
4329
|
+
const result = await multiselect$1({
|
|
4330
|
+
message: options.message,
|
|
4331
|
+
options: options.choices.map((choice) => ({
|
|
4332
|
+
value: choice.value,
|
|
4333
|
+
label: choice.title,
|
|
4334
|
+
...choice.description ? { hint: choice.description } : {},
|
|
4335
|
+
disabled: choice.disabled ?? false
|
|
4336
|
+
})),
|
|
4337
|
+
initialValues,
|
|
4338
|
+
required: (options.min ?? 1) >= 1
|
|
4339
|
+
});
|
|
4340
|
+
if (isCancel(result)) {
|
|
4341
|
+
abort();
|
|
4270
4342
|
}
|
|
4271
|
-
|
|
4272
|
-
let frameIndex = 0;
|
|
4273
|
-
let finalized = false;
|
|
4274
|
-
const renderFrame = () => {
|
|
4275
|
-
if (finalized) {
|
|
4276
|
-
return;
|
|
4277
|
-
}
|
|
4278
|
-
const frame = FRAMES[frameIndex % FRAMES.length] ?? FRAMES[0];
|
|
4279
|
-
process.stderr.write(
|
|
4280
|
-
`${ERASE_LINE} ${highlighter.accent(frame)} ${currentText}`
|
|
4281
|
-
);
|
|
4282
|
-
frameIndex += 1;
|
|
4283
|
-
};
|
|
4284
|
-
renderFrame();
|
|
4285
|
-
const interval = setInterval(renderFrame, FRAME_INTERVAL_MS);
|
|
4286
|
-
const finalize = (icon, text) => {
|
|
4287
|
-
if (finalized) {
|
|
4288
|
-
return;
|
|
4289
|
-
}
|
|
4290
|
-
finalized = true;
|
|
4291
|
-
clearInterval(interval);
|
|
4292
|
-
process.stderr.write(`${ERASE_LINE} ${icon} ${text}
|
|
4293
|
-
`);
|
|
4294
|
-
};
|
|
4295
|
-
return {
|
|
4296
|
-
update: (text) => {
|
|
4297
|
-
currentText = text;
|
|
4298
|
-
},
|
|
4299
|
-
succeed: (text) => finalize(highlighter.ok("\u2713"), text),
|
|
4300
|
-
fail: (text) => finalize(highlighter.error("\u2717"), text),
|
|
4301
|
-
stop: () => {
|
|
4302
|
-
if (finalized) {
|
|
4303
|
-
return;
|
|
4304
|
-
}
|
|
4305
|
-
finalized = true;
|
|
4306
|
-
clearInterval(interval);
|
|
4307
|
-
process.stderr.write(ERASE_LINE);
|
|
4308
|
-
}
|
|
4309
|
-
};
|
|
4343
|
+
return result;
|
|
4310
4344
|
};
|
|
4311
4345
|
|
|
4312
4346
|
// src/cli/utils/install-wizard.ts
|
|
4313
|
-
var POINTER5 = process.platform === "win32" && !process.env.WT_SESSION ? ">" : "\u203A";
|
|
4314
4347
|
var SKILL_NAME = "move-doctor";
|
|
4315
4348
|
var FALLBACK_AGENT = "claude-code";
|
|
4316
4349
|
var GITHUB_ACTION_WORKFLOW = `name: move-doctor
|
|
@@ -4392,31 +4425,26 @@ var installWorkflow = async (projectRoot) => {
|
|
|
4392
4425
|
}
|
|
4393
4426
|
return { path: targetPath, existed };
|
|
4394
4427
|
};
|
|
4395
|
-
var
|
|
4396
|
-
if (summary.installed.length === 0
|
|
4397
|
-
return
|
|
4428
|
+
var skillSummaryHeadline = (summary) => {
|
|
4429
|
+
if (summary.installed.length === 0) {
|
|
4430
|
+
return "No agents detected \u2014 skill not installed";
|
|
4398
4431
|
}
|
|
4399
|
-
const
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4432
|
+
const agents = summary.installed.map((entry) => entry.agent).join(", ");
|
|
4433
|
+
const count = `${summary.installed.length} agent${summary.installed.length === 1 ? "" : "s"}`;
|
|
4434
|
+
return `Skill installed for ${highlighter.bold(count)} ${highlighter.muted(`(${agents})`)}`;
|
|
4435
|
+
};
|
|
4436
|
+
var emitSkillExtras = (summary, projectRoot) => {
|
|
4437
|
+
const canonical = summary.installed.find((entry) => entry.mode === "symlink");
|
|
4438
|
+
if (canonical) {
|
|
4439
|
+
log.message(
|
|
4440
|
+
highlighter.muted(
|
|
4441
|
+
`source: ${path5.relative(projectRoot, canonical.path) || canonical.path}`
|
|
4442
|
+
)
|
|
4407
4443
|
);
|
|
4408
|
-
if (canonical) {
|
|
4409
|
-
lines.push(
|
|
4410
|
-
` ${highlighter.muted(`source: ${path5.relative(projectRoot, canonical.path) || canonical.path}`)}`
|
|
4411
|
-
);
|
|
4412
|
-
}
|
|
4413
4444
|
}
|
|
4414
4445
|
for (const failure of summary.failed) {
|
|
4415
|
-
|
|
4416
|
-
` ${highlighter.warn("\u26A0")} ${failure.agent}: ${highlighter.muted(failure.error)}`
|
|
4417
|
-
);
|
|
4446
|
+
log.warn(`${failure.agent}: ${highlighter.muted(failure.error)}`);
|
|
4418
4447
|
}
|
|
4419
|
-
return lines.join("\n");
|
|
4420
4448
|
};
|
|
4421
4449
|
var detectAgentsWithFallback = async () => {
|
|
4422
4450
|
try {
|
|
@@ -4448,69 +4476,66 @@ var pickAgentsInteractively = async (detected) => {
|
|
|
4448
4476
|
return selection;
|
|
4449
4477
|
};
|
|
4450
4478
|
var resolveAgents = async (pick) => {
|
|
4451
|
-
const detectionSpinner =
|
|
4479
|
+
const detectionSpinner = spinner();
|
|
4480
|
+
detectionSpinner.start("Detecting installed agents\u2026");
|
|
4452
4481
|
const detected = await detectAgentsWithFallback();
|
|
4453
|
-
detectionSpinner.
|
|
4482
|
+
detectionSpinner.stop(
|
|
4454
4483
|
`Detected ${detected.length} agent${detected.length === 1 ? "" : "s"}: ${highlighter.muted(detected.join(", "))}`
|
|
4455
4484
|
);
|
|
4456
4485
|
const agents = pick ? await pickAgentsInteractively(detected) : detected;
|
|
4457
4486
|
if (agents.length !== detected.length) {
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
`
|
|
4487
|
+
log.message(
|
|
4488
|
+
highlighter.muted(
|
|
4489
|
+
`Selected ${agents.length} of ${detected.length}: ${agents.join(", ")}`
|
|
4490
|
+
)
|
|
4461
4491
|
);
|
|
4462
4492
|
}
|
|
4463
4493
|
return agents;
|
|
4464
4494
|
};
|
|
4465
4495
|
var applyTargets = async (projectRoot, targets) => {
|
|
4466
4496
|
if (targets.skill) {
|
|
4467
|
-
const installSpinner =
|
|
4497
|
+
const installSpinner = spinner();
|
|
4498
|
+
installSpinner.start(
|
|
4468
4499
|
`Installing skill to ${targets.agents.length} agent${targets.agents.length === 1 ? "" : "s"}\u2026`
|
|
4469
4500
|
);
|
|
4470
4501
|
const summary = await installSkillForAgents(projectRoot, targets.agents);
|
|
4471
|
-
installSpinner.stop();
|
|
4472
|
-
|
|
4473
|
-
`);
|
|
4502
|
+
installSpinner.stop(skillSummaryHeadline(summary));
|
|
4503
|
+
emitSkillExtras(summary, projectRoot);
|
|
4474
4504
|
}
|
|
4475
4505
|
if (targets.workflow) {
|
|
4476
4506
|
const { path: workflowPath, existed } = await installWorkflow(projectRoot);
|
|
4507
|
+
const rel = path5.relative(projectRoot, workflowPath);
|
|
4477
4508
|
if (existed) {
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
`
|
|
4509
|
+
log.message(
|
|
4510
|
+
highlighter.muted(`Workflow already exists at ${rel}, left untouched.`)
|
|
4481
4511
|
);
|
|
4482
4512
|
} else {
|
|
4483
|
-
|
|
4484
|
-
` ${highlighter.ok("\u2713")} GitHub workflow installed at ${highlighter.muted(path5.relative(projectRoot, workflowPath))}
|
|
4485
|
-
`
|
|
4486
|
-
);
|
|
4513
|
+
log.success(`GitHub workflow installed at ${highlighter.muted(rel)}`);
|
|
4487
4514
|
}
|
|
4488
4515
|
}
|
|
4489
4516
|
if (targets.agentHooks) {
|
|
4490
|
-
const hookSpinner =
|
|
4517
|
+
const hookSpinner = spinner();
|
|
4518
|
+
hookSpinner.start("Installing agent hooks\u2026");
|
|
4491
4519
|
const result = installMoveDoctorAgentHooks({
|
|
4492
4520
|
projectRoot,
|
|
4493
4521
|
agents: targets.agents
|
|
4494
4522
|
});
|
|
4495
|
-
hookSpinner.stop();
|
|
4496
4523
|
if (result.installedAgents.length === 0) {
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4524
|
+
hookSpinner.stop(
|
|
4525
|
+
highlighter.muted(
|
|
4526
|
+
"No Claude Code / Cursor agents \u2014 hooks not installed"
|
|
4527
|
+
)
|
|
4500
4528
|
);
|
|
4501
4529
|
} else {
|
|
4502
4530
|
const names = result.installedAgents.map((agent) => getSkillAgentConfig(agent).displayName).join(", ");
|
|
4503
|
-
|
|
4504
|
-
` ${highlighter.ok("\u2713")} Agent hooks installed for ${highlighter.bold(names)}
|
|
4505
|
-
`
|
|
4506
|
-
);
|
|
4531
|
+
hookSpinner.stop(`Agent hooks installed for ${highlighter.bold(names)}`);
|
|
4507
4532
|
}
|
|
4508
4533
|
}
|
|
4509
4534
|
};
|
|
4510
4535
|
var printNoninteractiveHint = (projectRoot) => {
|
|
4511
4536
|
process.stdout.write(
|
|
4512
4537
|
`
|
|
4513
|
-
${highlighter.muted(
|
|
4538
|
+
${highlighter.muted("\u203A")} Run ${highlighter.accent("npx move-doctor install --yes")} in ${highlighter.muted(projectRoot)} to install the skill + CI workflow.
|
|
4514
4539
|
`
|
|
4515
4540
|
);
|
|
4516
4541
|
};
|
|
@@ -4545,6 +4570,7 @@ var buildSetupChoices = (projectRoot, agents) => {
|
|
|
4545
4570
|
var runInstallWizard = async (options) => {
|
|
4546
4571
|
const { projectRoot, yes = false } = options;
|
|
4547
4572
|
if (yes || !isInteractive()) {
|
|
4573
|
+
intro(`\u271A ${highlighter.bold("move-doctor setup")}`);
|
|
4548
4574
|
const agents2 = await resolveAgents(false);
|
|
4549
4575
|
await applyTargets(projectRoot, {
|
|
4550
4576
|
agents: agents2,
|
|
@@ -4552,12 +4578,14 @@ var runInstallWizard = async (options) => {
|
|
|
4552
4578
|
workflow: true,
|
|
4553
4579
|
agentHooks: false
|
|
4554
4580
|
});
|
|
4581
|
+
outro("Skill + CI workflow installed.");
|
|
4555
4582
|
return;
|
|
4556
4583
|
}
|
|
4557
4584
|
if (await hasDisabledSetupPrompt(projectRoot)) {
|
|
4558
4585
|
printNoninteractiveHint(projectRoot);
|
|
4559
4586
|
return;
|
|
4560
4587
|
}
|
|
4588
|
+
intro(`\u271A ${highlighter.bold("move-doctor setup")}`);
|
|
4561
4589
|
const choice = await select({
|
|
4562
4590
|
message: "Set up move-doctor for this project?",
|
|
4563
4591
|
choices: [
|
|
@@ -4580,20 +4608,21 @@ var runInstallWizard = async (options) => {
|
|
|
4580
4608
|
initial: 0
|
|
4581
4609
|
});
|
|
4582
4610
|
if (choice === null || choice === "skip") {
|
|
4611
|
+
outro(
|
|
4612
|
+
`Skipped. Run ${highlighter.accent("npx move-doctor install")} anytime.`
|
|
4613
|
+
);
|
|
4583
4614
|
return;
|
|
4584
4615
|
}
|
|
4585
4616
|
if (choice === "never") {
|
|
4586
4617
|
await disableSetupPrompt(projectRoot);
|
|
4587
|
-
|
|
4588
|
-
`
|
|
4589
|
-
${highlighter.muted(POINTER5)} Got it. Run ${highlighter.accent("npx move-doctor install")} when you change your mind.
|
|
4590
|
-
`
|
|
4618
|
+
outro(
|
|
4619
|
+
`Got it. Run ${highlighter.accent("npx move-doctor install")} when you change your mind.`
|
|
4591
4620
|
);
|
|
4592
4621
|
return;
|
|
4593
4622
|
}
|
|
4594
|
-
process.stdout.write("\n");
|
|
4595
4623
|
const agents = await resolveAgents(true);
|
|
4596
4624
|
if (agents.length === 0) {
|
|
4625
|
+
outro("No agents selected \u2014 nothing installed.");
|
|
4597
4626
|
return;
|
|
4598
4627
|
}
|
|
4599
4628
|
const setupChoices = buildSetupChoices(projectRoot, agents);
|
|
@@ -4613,15 +4642,38 @@ var runInstallWizard = async (options) => {
|
|
|
4613
4642
|
workflow: !didSkipOptional && actions.includes("workflow"),
|
|
4614
4643
|
agentHooks: !didSkipOptional && actions.includes("agent-hooks")
|
|
4615
4644
|
});
|
|
4616
|
-
|
|
4617
|
-
`
|
|
4618
|
-
${highlighter.muted(POINTER5)} Done. Run ${highlighter.accent("npx move-doctor .")} again to see the agent in action.
|
|
4619
|
-
`
|
|
4645
|
+
outro(
|
|
4646
|
+
`Done. Run ${highlighter.accent("npx move-doctor .")} again to see the agent in action.`
|
|
4620
4647
|
);
|
|
4621
4648
|
};
|
|
4622
4649
|
|
|
4650
|
+
// src/cli/utils/output.ts
|
|
4651
|
+
var writeError = (message) => {
|
|
4652
|
+
process.stderr.write(`${highlighter.error(glyph.crossMark)} ${message}
|
|
4653
|
+
`);
|
|
4654
|
+
};
|
|
4655
|
+
var errorExitCode = (score) => score.bySeverity.error > 0 ? 1 : 0;
|
|
4656
|
+
|
|
4623
4657
|
// src/cli/utils/scope-prompt.ts
|
|
4624
|
-
|
|
4658
|
+
var promptPackageScope = async (focusName, totalPackages) => {
|
|
4659
|
+
const choice = await select({
|
|
4660
|
+
message: `You're inside ${highlighter.bold(focusName)} \u2014 1 of ${totalPackages} packages. What should I scan?`,
|
|
4661
|
+
choices: [
|
|
4662
|
+
{
|
|
4663
|
+
title: `Just ${focusName}`,
|
|
4664
|
+
value: "focus",
|
|
4665
|
+
description: "this package only \xB7 fast"
|
|
4666
|
+
},
|
|
4667
|
+
{
|
|
4668
|
+
title: `All ${totalPackages} packages`,
|
|
4669
|
+
value: "all",
|
|
4670
|
+
description: "the whole workspace"
|
|
4671
|
+
}
|
|
4672
|
+
],
|
|
4673
|
+
initial: 0
|
|
4674
|
+
});
|
|
4675
|
+
return choice ?? "focus";
|
|
4676
|
+
};
|
|
4625
4677
|
var resolveScope = async (options) => {
|
|
4626
4678
|
if (options.diffFlagPassed) {
|
|
4627
4679
|
return "diff";
|
|
@@ -4650,6 +4702,68 @@ var resolveScope = async (options) => {
|
|
|
4650
4702
|
return choice ?? "full";
|
|
4651
4703
|
};
|
|
4652
4704
|
|
|
4705
|
+
// src/cli/utils/spinner.ts
|
|
4706
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4707
|
+
var FRAME_INTERVAL_MS = 80;
|
|
4708
|
+
var ERASE_LINE = "\r\x1B[K";
|
|
4709
|
+
var startSpinner = (initialText) => {
|
|
4710
|
+
if (!isInteractive()) {
|
|
4711
|
+
process.stderr.write(` ${highlighter.muted("\u2026")} ${initialText}
|
|
4712
|
+
`);
|
|
4713
|
+
return {
|
|
4714
|
+
update: () => {
|
|
4715
|
+
},
|
|
4716
|
+
succeed: (text) => process.stderr.write(` ${highlighter.ok(glyph.check)} ${text}
|
|
4717
|
+
`),
|
|
4718
|
+
fail: (text) => process.stderr.write(
|
|
4719
|
+
` ${highlighter.error(glyph.crossMark)} ${text}
|
|
4720
|
+
`
|
|
4721
|
+
),
|
|
4722
|
+
stop: () => {
|
|
4723
|
+
}
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
let currentText = initialText;
|
|
4727
|
+
let frameIndex = 0;
|
|
4728
|
+
let finalized = false;
|
|
4729
|
+
const renderFrame = () => {
|
|
4730
|
+
if (finalized) {
|
|
4731
|
+
return;
|
|
4732
|
+
}
|
|
4733
|
+
const frame = FRAMES[frameIndex % FRAMES.length] ?? FRAMES[0];
|
|
4734
|
+
process.stderr.write(
|
|
4735
|
+
`${ERASE_LINE} ${highlighter.accent(frame)} ${currentText}`
|
|
4736
|
+
);
|
|
4737
|
+
frameIndex += 1;
|
|
4738
|
+
};
|
|
4739
|
+
renderFrame();
|
|
4740
|
+
const interval = setInterval(renderFrame, FRAME_INTERVAL_MS);
|
|
4741
|
+
const finalize = (icon, text) => {
|
|
4742
|
+
if (finalized) {
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
finalized = true;
|
|
4746
|
+
clearInterval(interval);
|
|
4747
|
+
process.stderr.write(`${ERASE_LINE} ${icon} ${text}
|
|
4748
|
+
`);
|
|
4749
|
+
};
|
|
4750
|
+
return {
|
|
4751
|
+
update: (text) => {
|
|
4752
|
+
currentText = text;
|
|
4753
|
+
},
|
|
4754
|
+
succeed: (text) => finalize(highlighter.ok(glyph.check), text),
|
|
4755
|
+
fail: (text) => finalize(highlighter.error(glyph.crossMark), text),
|
|
4756
|
+
stop: () => {
|
|
4757
|
+
if (finalized) {
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
finalized = true;
|
|
4761
|
+
clearInterval(interval);
|
|
4762
|
+
process.stderr.write(ERASE_LINE);
|
|
4763
|
+
}
|
|
4764
|
+
};
|
|
4765
|
+
};
|
|
4766
|
+
|
|
4653
4767
|
// src/cli/index.ts
|
|
4654
4768
|
var HELP_TEXT = `Move Doctor \u2014 A deterministic linter for Sui Move.
|
|
4655
4769
|
|
|
@@ -4676,9 +4790,16 @@ Setup flags:
|
|
|
4676
4790
|
-h, --help Show this help
|
|
4677
4791
|
-v, --version Show version
|
|
4678
4792
|
`;
|
|
4679
|
-
var
|
|
4680
|
-
|
|
4681
|
-
|
|
4793
|
+
var offerSetupIfNeeded = async (workspace, args, skillInstalled) => {
|
|
4794
|
+
const fullySetUp = skillInstalled && isWorkflowInstalledForWorkspace(workspace);
|
|
4795
|
+
const shouldOffer = !args.skipSetup && isInteractive() && !fullySetUp && !await hasDisabledSetupPrompt(workspace.rootDirectory);
|
|
4796
|
+
if (shouldOffer) {
|
|
4797
|
+
await runInstallWizard({
|
|
4798
|
+
projectRoot: workspace.rootDirectory,
|
|
4799
|
+
yes: false
|
|
4800
|
+
});
|
|
4801
|
+
process.stdout.write("\n");
|
|
4802
|
+
}
|
|
4682
4803
|
};
|
|
4683
4804
|
var planScan = (workspace, args) => {
|
|
4684
4805
|
if (args.packageFilter) {
|
|
@@ -4766,22 +4887,21 @@ var runSinglePackageScan = async (workspace, focus, args, showHeader) => {
|
|
|
4766
4887
|
}
|
|
4767
4888
|
scanSpinner?.stop();
|
|
4768
4889
|
const durationMs = Date.now() - scanStartedAt;
|
|
4769
|
-
changedFiles ? changedFiles.length : totalFileCount;
|
|
4770
4890
|
if (result.diagnostics.length === 0 && totalFileCount === 0 && !args.json && !args.scoreOnly) {
|
|
4771
4891
|
process.stderr.write(
|
|
4772
|
-
` ${highlighter.warn(
|
|
4892
|
+
` ${highlighter.warn(glyph.warn)} ${highlighter.warn("warning")} ${highlighter.muted("\u2014 no .move source files found; score does not reflect a real codebase.")}
|
|
4773
4893
|
`
|
|
4774
4894
|
);
|
|
4775
4895
|
}
|
|
4776
4896
|
if (args.json) {
|
|
4777
4897
|
process.stdout.write(`${renderJson(result)}
|
|
4778
4898
|
`);
|
|
4779
|
-
return result.score
|
|
4899
|
+
return errorExitCode(result.score);
|
|
4780
4900
|
}
|
|
4781
4901
|
if (args.scoreOnly) {
|
|
4782
4902
|
process.stdout.write(`${renderScoreOnly(result)}
|
|
4783
4903
|
`);
|
|
4784
|
-
return result.score
|
|
4904
|
+
return errorExitCode(result.score);
|
|
4785
4905
|
}
|
|
4786
4906
|
const skillInstalled = isSkillInstalledForWorkspace(workspace);
|
|
4787
4907
|
process.stdout.write(
|
|
@@ -4796,16 +4916,8 @@ ${renderText(result, {
|
|
|
4796
4916
|
|
|
4797
4917
|
`
|
|
4798
4918
|
);
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
if (shouldOfferSetup) {
|
|
4802
|
-
await runInstallWizard({
|
|
4803
|
-
projectRoot: workspace.rootDirectory,
|
|
4804
|
-
yes: false
|
|
4805
|
-
});
|
|
4806
|
-
process.stdout.write("\n");
|
|
4807
|
-
}
|
|
4808
|
-
return result.score.bySeverity.error > 0 ? 1 : 0;
|
|
4919
|
+
await offerSetupIfNeeded(workspace, args, skillInstalled);
|
|
4920
|
+
return errorExitCode(result.score);
|
|
4809
4921
|
};
|
|
4810
4922
|
var runWorkspaceScan = async (workspace, packagesToScan, args, showHeader) => {
|
|
4811
4923
|
const gitRoot = workspace.gitRootDirectory ?? await findGitRoot(workspace.rootDirectory);
|
|
@@ -4851,12 +4963,12 @@ var runWorkspaceScan = async (workspace, packagesToScan, args, showHeader) => {
|
|
|
4851
4963
|
if (args.json) {
|
|
4852
4964
|
process.stdout.write(`${renderWorkspaceJson(workspace, result)}
|
|
4853
4965
|
`);
|
|
4854
|
-
return result.aggregateScore
|
|
4966
|
+
return errorExitCode(result.aggregateScore);
|
|
4855
4967
|
}
|
|
4856
4968
|
if (args.scoreOnly) {
|
|
4857
4969
|
process.stdout.write(`${renderWorkspaceScoreOnly(result)}
|
|
4858
4970
|
`);
|
|
4859
|
-
return result.aggregateScore
|
|
4971
|
+
return errorExitCode(result.aggregateScore);
|
|
4860
4972
|
}
|
|
4861
4973
|
const skillInstalled = isSkillInstalledForWorkspace(workspace);
|
|
4862
4974
|
process.stdout.write(
|
|
@@ -4873,17 +4985,8 @@ ${renderWorkspaceText({
|
|
|
4873
4985
|
|
|
4874
4986
|
`
|
|
4875
4987
|
);
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
if (shouldOfferSetup) {
|
|
4879
|
-
await runInstallWizard({
|
|
4880
|
-
projectRoot: workspace.rootDirectory,
|
|
4881
|
-
yes: false,
|
|
4882
|
-
rootDisplayName: path5.basename(workspace.rootDirectory)
|
|
4883
|
-
});
|
|
4884
|
-
process.stdout.write("\n");
|
|
4885
|
-
}
|
|
4886
|
-
return result.aggregateScore.bySeverity.error > 0 ? 1 : 0;
|
|
4988
|
+
await offerSetupIfNeeded(workspace, args, skillInstalled);
|
|
4989
|
+
return errorExitCode(result.aggregateScore);
|
|
4887
4990
|
};
|
|
4888
4991
|
var runCli = async (argv2) => {
|
|
4889
4992
|
let args;
|
|
@@ -4961,21 +5064,26 @@ ${HELP_TEXT}`);
|
|
|
4961
5064
|
writeError(error.message);
|
|
4962
5065
|
return 2;
|
|
4963
5066
|
}
|
|
4964
|
-
|
|
4965
|
-
const
|
|
4966
|
-
if (plan.focusPackage && workspace.isMonorepo) {
|
|
4967
|
-
const
|
|
4968
|
-
|
|
4969
|
-
|
|
5067
|
+
contextSpinner?.stop();
|
|
5068
|
+
const canPromptScope = !(args.json || args.scoreOnly) && isInteractive();
|
|
5069
|
+
if (plan.focusPackage && workspace.isMonorepo && canPromptScope) {
|
|
5070
|
+
const scope = await promptPackageScope(
|
|
5071
|
+
plan.focusPackage.packageName,
|
|
5072
|
+
workspace.packages.length
|
|
4970
5073
|
);
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5074
|
+
if (scope === "all") {
|
|
5075
|
+
plan = {
|
|
5076
|
+
workspace,
|
|
5077
|
+
packagesToScan: workspace.packages,
|
|
5078
|
+
focusPackage: null
|
|
5079
|
+
};
|
|
5080
|
+
}
|
|
5081
|
+
} else if (plan.focusPackage && workspace.isMonorepo && showHeader) {
|
|
5082
|
+
process.stderr.write(
|
|
5083
|
+
highlighter.muted(
|
|
5084
|
+
` ${glyph.pointer} Scanning only ${plan.focusPackage.packageName} \u2014 use --all for the whole workspace.
|
|
5085
|
+
`
|
|
5086
|
+
)
|
|
4979
5087
|
);
|
|
4980
5088
|
}
|
|
4981
5089
|
if (plan.packagesToScan) {
|