move-doctor 0.2.0-dev.4820c3a → 0.2.0-dev.d0feceb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -30
  2. package/dist/cli.js +757 -649
  3. 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, readdir } from 'fs/promises';
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 = 25;
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) ?? 0;
182
- deductionByRule.set(diagnostic.ruleId, prior + weight);
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 ruleDeduction of deductionByRule.values()) {
186
- totalDeductions += Math.min(ruleDeduction, PER_RULE_DEDUCTION_CAP);
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((path14) => path14.module === "test_scenario");
2897
- const usesBegin = scenarioPaths.some((path14) => path14.member === "begin");
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((path14) => path14.member !== null && SCENARIO_API.has(path14.member));
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/utils/next-steps.ts
3204
- var POINTER = process.platform === "win32" && !process.env.WT_SESSION ? ">" : "\u203A";
3205
- var buildNextStep = (line) => ({ line });
3206
- var findHottestBucket = (result) => {
3207
- const buckets = /* @__PURE__ */ new Map();
3208
- const severityWeight = (severity) => {
3209
- if (severity === "error") {
3210
- return 8;
3211
- }
3212
- if (severity === "warning") {
3213
- return 3;
3214
- }
3215
- return 1;
3216
- };
3217
- for (const diagnostic of result.diagnostics) {
3218
- const weight = severityWeight(diagnostic.severity);
3219
- buckets.set(
3220
- diagnostic.bucket,
3221
- (buckets.get(diagnostic.bucket) ?? 0) + weight
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
- let best = null;
3225
- let bestWeight = 0;
3226
- for (const [bucket, weight] of buckets) {
3227
- if (weight > bestWeight) {
3228
- best = bucket;
3229
- bestWeight = weight;
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 best;
3267
+ return segments.map((seg) => seg.paint(seg.char.repeat(seg.cells))).join("");
3233
3268
  };
3234
- var buildNextSteps = (context) => {
3235
- const { result, hasInstalledSkill, hasSuiCli } = context;
3236
- const lines = [];
3237
- const steps = [];
3238
- if (result.diagnostics.length > 0) {
3239
- steps.push(
3240
- buildNextStep(
3241
- `${highlighter.muted(POINTER)} Run ${highlighter.accent("--verbose")} for file refs and fix hints.`
3242
- )
3243
- );
3244
- const hottest = findHottestBucket(result);
3245
- if (hottest) {
3246
- steps.push(
3247
- buildNextStep(
3248
- `${highlighter.muted(POINTER)} Focus on ${highlighter.bold(hottest)} first: ${highlighter.accent("move-doctor . --verbose")}`
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
- if (!hasInstalledSkill) {
3254
- steps.push(
3255
- buildNextStep(
3256
- `${highlighter.muted(POINTER)} Install the agent skill so your agent can auto-fix findings: ${highlighter.accent("npx move-doctor install")}`
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 (!hasSuiCli) {
3261
- steps.push(
3262
- buildNextStep(
3263
- `${highlighter.muted(POINTER)} Install the Sui CLI to enable compiler lints (W0*): ${highlighter.accent("https://docs.sui.io/guides/developer/getting-started/sui-install")}`
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
- steps.push(
3268
- buildNextStep(
3269
- `${highlighter.muted(POINTER)} Full rule catalog: ${highlighter.accent("https://move.doctor/docs/rules")}`
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 lines;
3365
+ return parts.join(" ");
3276
3366
  };
3277
-
3278
- // src/cli/render.ts
3279
- var POINTER2 = process.platform === "win32" && !process.env.WT_SESSION ? ">" : "\xB7";
3280
- var terminalWidth = () => {
3281
- const width = process.stdout.columns ?? 80;
3282
- return Math.max(40, Math.min(width, 80));
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 buildScoreBar = (score, width) => {
3285
- const filled = Math.round(score / PERFECT_SCORE * width);
3286
- const empty = width - filled;
3287
- return colorizeByScore("\u2588".repeat(filled), score) + highlighter.muted("\u2591".repeat(empty));
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 ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
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 width = Math.min(terminalWidth(), 74);
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
- top,
3421
+ buildTopBorder(width, color(glyph.cross), data.title),
3422
+ blank,
3423
+ row(scoreLine),
3344
3424
  blank,
3345
- rowLR(scoreLeft, title),
3346
- row(bar),
3425
+ row(compositionBar(counts, inner)),
3426
+ row(legend),
3347
3427
  blank,
3348
3428
  row(meta),
3349
- row(findings),
3350
- bottom
3429
+ buildBottomBorder(width)
3351
3430
  ].join("\n");
3352
3431
  };
3353
- var buildFindingsTldr = (f) => {
3354
- if (f.total === 0) {
3355
- return highlighter.ok("\u2713 no findings");
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 (f.warnings > 0) {
3365
- breakdown.push(
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
- if (f.info > 0) {
3370
- breakdown.push(highlighter.muted(`${f.info} info`));
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 `${head} ${highlighter.muted(POINTER2)} ${breakdown.join(highlighter.muted(" \xB7 "))}`;
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 "\u2717";
3454
+ return glyph.crossMark;
3377
3455
  }
3378
3456
  if (severity === "warning") {
3379
- return "\u26A0";
3457
+ return glyph.warn;
3380
3458
  }
3381
- return "\xB7";
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 ` ${icon} ${padded}${citation}${countBadge}`;
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
- lines.push(buildRuleHeaderLine(ruleId, ruleDiagnostics, ruleColumnWidth));
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
- const hint = `${POINTER2} ${first.fixHint}`;
3516
- lines.push(highlighter.muted(indentBlock(hint, " ")));
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(` ${buildLocation(diagnostic, rootDirectory)}`)
3511
+ highlighter.muted(` ${buildLocation(diagnostic, rootDirectory)}`)
3521
3512
  );
3522
3513
  }
3523
3514
  lines.push("");
3524
3515
  return lines;
3525
3516
  };
3526
- var buildVerboseSummary = (result, context) => {
3517
+ var buildBucketRuleDetail = (diagnostics, rootDirectory) => {
3527
3518
  const lines = [];
3528
- lines.push(cardFromContext(result, context));
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
- result.project.rootDirectory
3535
+ rootDirectory
3559
3536
  )
3560
3537
  );
3561
3538
  }
3562
3539
  }
3563
- if (!(result.compilerLintAvailable || context.hasSuiCli)) {
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
- highlighter.muted(
3566
- ` ${POINTER2} Sui CLI not found on PATH \u2014 compiler lints (W0*) skipped.`
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 POINTER3 = process.platform === "win32" && !process.env.WT_SESSION ? ">" : "\xB7";
3606
- var buildBar = (score, width) => {
3607
- const filled = Math.round(score / PERFECT_SCORE * width);
3608
- const empty = width - filled;
3609
- return colorizeByScore("\u2588".repeat(filled), score) + highlighter.muted("\u2591".repeat(empty));
3610
- };
3611
- var buildWorkspaceScoreHeader = (workspace, result, suiVersion, durationMs) => {
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 buildInfoCard({
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 buildPerPackageTable = (perPackage) => {
3646
- const lines = [];
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
- "Package".length
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, nameColWidth);
3788
+ const name = formatPackageName(entry.relativePath, labelWidth);
3657
3789
  if (entry.skipped) {
3658
3790
  lines.push(
3659
- ` ${highlighter.muted(entry.relativePath.padEnd(nameColWidth))} ${highlighter.muted("\u2014")} ${highlighter.muted("\xB7")} ${highlighter.muted("no changed files")}`
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 score = entry.score.score;
3664
- const scoreText = colorizeByScore(String(score).padStart(3), score);
3665
- const bar = buildBar(score, barWidth);
3666
- const label = colorizeByScore(scoreLabel(score), score);
3667
- const errors = entry.score.bySeverity.error;
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.join("\n");
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(POINTER3)} For details: ${highlighter.accent("npx move-doctor@latest --verbose")}`
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(POINTER3)} Install the agent skill: ${highlighter.accent("npx move-doctor install")}`
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(POINTER3)} Install the Sui CLI to enable compiler lints (W0*).`
3826
+ ` ${highlighter.muted(glyph.pointer)} Install the Sui CLI to enable compiler lints (W0*).`
3720
3827
  );
3721
3828
  }
3722
- lines.push(
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 buildVerboseDetails = (perPackage) => {
3729
- const sections = [];
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
- sections.push("");
3735
- sections.push(
3736
- ` ${highlighter.bold(entry.relativePath)} ${highlighter.muted(`(${entry.diagnostics.length} finding${entry.diagnostics.length === 1 ? "" : "s"})`)}`
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
- const ruleColWidth = Math.max(
3739
- ...entry.diagnostics.map((d) => d.ruleId.length),
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 sections.join("\n");
3846
+ return lines;
3775
3847
  };
3776
3848
  var renderWorkspaceText = (options) => {
3777
- const sections = [];
3778
- sections.push(
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
- sections.push(buildPerPackageTable(options.result.perPackage));
3787
- if (options.verbose) {
3788
- sections.push(buildVerboseDetails(options.result.perPackage));
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.1.0";
3889
+ var VERSION = "0.2.0-dev.d0feceb";
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")} ${highlighter.accent("move.doctor")}
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 detectSuiVersion = async (cwd) => {
3847
- const result = await runCommand2("sui", ["--version"], cwd);
3848
- if (!result || result.exitCode !== 0) {
3849
- return null;
3850
- }
3851
- const match = result.stdout.match(/sui\s+(\S+)/);
3852
- return match?.[1] ?? null;
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 readdir2(current, { withFileTypes: true });
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 POINTER4 = process.platform === "win32" && !process.env.WT_SESSION ? ">" : "\u203A";
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 answer = await prompts(
4192
- {
4193
- type: "select",
4194
- name: "value",
4195
- message: options.message,
4196
- choices: options.choices.map((choice) => ({
4197
- title: choice.title,
4198
- description: choice.description,
4199
- value: choice.value
4200
- })),
4201
- initial: options.initial ?? 0
4202
- },
4203
- {
4204
- onCancel: () => {
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 answer = await prompts(
4222
- {
4223
- type: "multiselect",
4224
- name: "values",
4225
- message: options.message,
4226
- hint: options.hint ?? "- Space to toggle. Enter to confirm.",
4227
- instructions: false,
4228
- min: options.min ?? 1,
4229
- choices: options.choices.map((choice) => ({
4230
- title: choice.title,
4231
- description: choice.description,
4232
- value: choice.value,
4233
- selected: choice.selected ?? false,
4234
- disabled: choice.disabled ?? false
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
- let currentText = initialText;
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 formatSkillSummary = (summary, projectRoot) => {
4396
- if (summary.installed.length === 0 && summary.failed.length === 0) {
4397
- return ` ${highlighter.muted("\xB7")} No agents detected on this machine \u2014 skill not installed anywhere.`;
4428
+ var skillSummaryHeadline = (summary) => {
4429
+ if (summary.installed.length === 0) {
4430
+ return "No agents detected \u2014 skill not installed";
4398
4431
  }
4399
- const lines = [];
4400
- if (summary.installed.length > 0) {
4401
- const agents = summary.installed.map((entry) => entry.agent).join(", ");
4402
- lines.push(
4403
- ` ${highlighter.ok("\u2713")} Skill installed for ${highlighter.bold(`${summary.installed.length} agent${summary.installed.length === 1 ? "" : "s"}`)} ${highlighter.muted(`(${agents})`)}`
4404
- );
4405
- const canonical = summary.installed.find(
4406
- (entry) => entry.mode === "symlink"
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
- lines.push(
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 = startSpinner("Detecting installed agents\u2026");
4479
+ const detectionSpinner = spinner();
4480
+ detectionSpinner.start("Detecting installed agents\u2026");
4452
4481
  const detected = await detectAgentsWithFallback();
4453
- detectionSpinner.succeed(
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
- process.stdout.write(
4459
- ` ${highlighter.muted("\xB7")} Selected ${highlighter.bold(`${agents.length}`)} of ${detected.length}: ${highlighter.muted(agents.join(", "))}
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 = startSpinner(
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
- process.stdout.write(`${formatSkillSummary(summary, projectRoot)}
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
- process.stdout.write(
4479
- ` ${highlighter.muted("\xB7")} Workflow already exists at ${highlighter.muted(path5.relative(projectRoot, workflowPath))}, left untouched.
4480
- `
4509
+ log.message(
4510
+ highlighter.muted(`Workflow already exists at ${rel}, left untouched.`)
4481
4511
  );
4482
4512
  } else {
4483
- process.stdout.write(
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 = startSpinner("Installing agent hooks\u2026");
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
- process.stdout.write(
4498
- ` ${highlighter.muted("\xB7")} No Claude Code / Cursor agents selected \u2014 agent hooks not installed.
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
- process.stdout.write(
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(POINTER5)} Run ${highlighter.accent("npx move-doctor install --yes")} in ${highlighter.muted(projectRoot)} to install the skill + CI workflow.
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
- process.stdout.write(
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
- process.stdout.write(
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
- process.platform === "win32" && !process.env.WT_SESSION ? ">" : "\u203A";
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 writeError = (message) => {
4680
- process.stderr.write(`${highlighter.error("\u2717")} ${message}
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("\u26A0")} ${highlighter.warn("warning")} ${highlighter.muted("\u2014 no .move source files found; score does not reflect a real codebase.")}
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.bySeverity.error > 0 ? 1 : 0;
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.bySeverity.error > 0 ? 1 : 0;
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
- const workflowInstalled = isWorkflowInstalledForWorkspace(workspace);
4800
- const shouldOfferSetup = !args.skipSetup && isInteractive() && !(skillInstalled && workflowInstalled) && !await hasDisabledSetupPrompt(workspace.rootDirectory);
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.bySeverity.error > 0 ? 1 : 0;
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.bySeverity.error > 0 ? 1 : 0;
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
- const workflowInstalled = isWorkflowInstalledForWorkspace(workspace);
4877
- const shouldOfferSetup = !args.skipSetup && isInteractive() && !(skillInstalled && workflowInstalled) && !await hasDisabledSetupPrompt(workspace.rootDirectory);
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
- const workspaceSui = await detectSuiVersion(workspace.rootDirectory);
4965
- const suiLabel = workspaceSui ? highlighter.muted(` \xB7 Sui ${workspaceSui.split("-")[0]}`) : "";
4966
- if (plan.focusPackage && workspace.isMonorepo) {
4967
- const focused = plan.focusPackage;
4968
- contextSpinner?.succeed(
4969
- `${highlighter.bold(focused.packageName)} ${highlighter.muted(`(1 of ${workspace.packages.length})`)} ${highlighter.muted(`edition ${focused.edition ?? "unset"}`)}${suiLabel}`
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
- } else if (workspace.isMonorepo) {
4972
- contextSpinner?.succeed(
4973
- `${highlighter.bold(path5.basename(workspace.rootDirectory))} ${highlighter.muted(`\xB7 ${workspace.packages.length} packages`)}${suiLabel}`
4974
- );
4975
- } else {
4976
- const only = workspace.packages[0];
4977
- contextSpinner?.succeed(
4978
- `${highlighter.bold(only.packageName)} ${highlighter.muted(`edition ${only.edition ?? "unset"}`)}${suiLabel}`
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) {