skills-doctor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/bin/skills-doctor.js +13 -0
  5. package/dist/cli/commands/scan.d.ts +29 -0
  6. package/dist/cli/commands/scan.d.ts.map +1 -0
  7. package/dist/cli/commands/scan.js +234 -0
  8. package/dist/cli/index.d.ts +4 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +32 -0
  11. package/dist/cli/utils/cli-logger.d.ts +8 -0
  12. package/dist/cli/utils/cli-logger.d.ts.map +1 -0
  13. package/dist/cli/utils/cli-logger.js +14 -0
  14. package/dist/cli/utils/handle-error.d.ts +6 -0
  15. package/dist/cli/utils/handle-error.d.ts.map +1 -0
  16. package/dist/cli/utils/handle-error.js +24 -0
  17. package/dist/cli/utils/handoff-to-agent.d.ts +21 -0
  18. package/dist/cli/utils/handoff-to-agent.d.ts.map +1 -0
  19. package/dist/cli/utils/handoff-to-agent.js +79 -0
  20. package/dist/cli/utils/is-command-available.d.ts +7 -0
  21. package/dist/cli/utils/is-command-available.d.ts.map +1 -0
  22. package/dist/cli/utils/is-command-available.js +33 -0
  23. package/dist/cli/utils/json-mode.d.ts +25 -0
  24. package/dist/cli/utils/json-mode.d.ts.map +1 -0
  25. package/dist/cli/utils/json-mode.js +47 -0
  26. package/dist/cli/utils/launch-agent.d.ts +33 -0
  27. package/dist/cli/utils/launch-agent.d.ts.map +1 -0
  28. package/dist/cli/utils/launch-agent.js +105 -0
  29. package/dist/cli/utils/prompts.d.ts +17 -0
  30. package/dist/cli/utils/prompts.d.ts.map +1 -0
  31. package/dist/cli/utils/prompts.js +25 -0
  32. package/dist/cli/utils/render-score-header.d.ts +18 -0
  33. package/dist/cli/utils/render-score-header.d.ts.map +1 -0
  34. package/dist/cli/utils/render-score-header.js +148 -0
  35. package/dist/cli/utils/run-command.d.ts +8 -0
  36. package/dist/cli/utils/run-command.d.ts.map +1 -0
  37. package/dist/cli/utils/run-command.js +29 -0
  38. package/dist/cli/utils/should-skip-prompts.d.ts +8 -0
  39. package/dist/cli/utils/should-skip-prompts.d.ts.map +1 -0
  40. package/dist/cli/utils/should-skip-prompts.js +24 -0
  41. package/dist/cli/utils/spinner.d.ts +8 -0
  42. package/dist/cli/utils/spinner.d.ts.map +1 -0
  43. package/dist/cli/utils/spinner.js +13 -0
  44. package/dist/domain/build-handoff-prompt.d.ts +9 -0
  45. package/dist/domain/build-handoff-prompt.d.ts.map +1 -0
  46. package/dist/domain/build-handoff-prompt.js +67 -0
  47. package/dist/domain/build-report.d.ts +38 -0
  48. package/dist/domain/build-report.d.ts.map +1 -0
  49. package/dist/domain/build-report.js +39 -0
  50. package/dist/domain/calculate-score.d.ts +13 -0
  51. package/dist/domain/calculate-score.d.ts.map +1 -0
  52. package/dist/domain/calculate-score.js +38 -0
  53. package/dist/domain/compare-findings.d.ts +10 -0
  54. package/dist/domain/compare-findings.d.ts.map +1 -0
  55. package/dist/domain/compare-findings.js +50 -0
  56. package/dist/domain/discover-skill-roots.d.ts +16 -0
  57. package/dist/domain/discover-skill-roots.d.ts.map +1 -0
  58. package/dist/domain/discover-skill-roots.js +66 -0
  59. package/dist/domain/parse-skill.d.ts +3 -0
  60. package/dist/domain/parse-skill.d.ts.map +1 -0
  61. package/dist/domain/parse-skill.js +53 -0
  62. package/dist/domain/rules/quality.d.ts +3 -0
  63. package/dist/domain/rules/quality.d.ts.map +1 -0
  64. package/dist/domain/rules/quality.js +292 -0
  65. package/dist/domain/rules/structural.d.ts +7 -0
  66. package/dist/domain/rules/structural.d.ts.map +1 -0
  67. package/dist/domain/rules/structural.js +253 -0
  68. package/dist/domain/scan-skills.d.ts +6 -0
  69. package/dist/domain/scan-skills.d.ts.map +1 -0
  70. package/dist/domain/scan-skills.js +49 -0
  71. package/dist/domain/summarize-findings.d.ts +20 -0
  72. package/dist/domain/summarize-findings.d.ts.map +1 -0
  73. package/dist/domain/summarize-findings.js +32 -0
  74. package/dist/domain/types.d.ts +62 -0
  75. package/dist/domain/types.d.ts.map +1 -0
  76. package/dist/domain/types.js +1 -0
  77. package/dist/domain/write-findings-directory.d.ts +16 -0
  78. package/dist/domain/write-findings-directory.d.ts.map +1 -0
  79. package/dist/domain/write-findings-directory.js +78 -0
  80. package/dist/index.d.ts +16 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +13 -0
  83. package/package.json +53 -0
  84. package/scripts/extract-release-notes.mjs +40 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-06-15
10
+
11
+ ### Added
12
+
13
+ - Initial Skills Doctor product requirements, skill quality specification, reusable CLI architecture specification, and incremental implementation plan.
14
+ - Bun-managed TypeScript CLI scaffold with Biome, Vitest, typecheck, build, verify, and package dry-run quality gates.
15
+ - GitHub Actions CI and tag-driven Bun release workflow.
16
+ - Release-note extraction from `CHANGELOG.md` for GitHub Releases created by CI.
17
+ - Project-local Claude and Codex/agents skill root discovery, `SKILL.md` scanning, and YAML frontmatter parsing.
18
+ - Structural Agent Skills validation for required frontmatter, naming, description length, optional fields, missing `SKILL.md`, and unsupported fields.
19
+ - Deterministic quality rules for weak descriptions, generic bodies, progressive-disclosure issues, missing resources, script guidance, missing evals, and divergent cross-ecosystem skills.
20
+ - Scan report construction, human summary rendering, JSON-mode helpers, and blocking-finding exit-code decisions.
21
+ - Interactive scan CLI with Commander, root selection prompts, non-interactive prompt skipping, spinner adapter, and findings review output.
22
+ - Claude and Codex repair-agent detection, prompt-based agent selection, command execution helpers, and launch preview construction.
23
+ - Findings-driven repair handoff prompts with local report directories, subset selection, and prompt/report file output.
24
+ - Local repair-agent launch flow with explicit confirmation, inherited-terminal launcher support, post-handoff re-scan, and fixed/remaining/new finding summaries.
25
+ - Domain-focused public API facade plus fixture coverage for valid, malformed, weak, missing-resource, script, and cross-ecosystem skill scans.
26
+ - README, MIT license, release checklist, and finalized initial release notes.
27
+ - Local score calculation for scan reports using distinct violated rules, with React Doctor-style labels in human, JSON, and repair-report output.
28
+ - React Doctor-style score header in human CLI output with a face, proportional bar, terminal-width clamping, score-threshold colors, and TTY animation.
29
+ - Concise human scan summary and streamlined review menu focused on fixing skills first.
30
+
31
+ ### Fixed
32
+
33
+ - Avoid Node unsettled top-level-await warnings while the interactive CLI is waiting for prompts.
34
+ - Discover global `~/.claude/skills` and `~/.agents/skills` roots and prompt for local, global/root, or both when both scopes exist.
35
+ - Run tests under `CI=true` during local verification and isolate interactive CLI tests from ambient CI environment variables.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Metelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Skills Doctor
2
+
3
+ Skills Doctor is a local-first CLI for auditing Agent Skills in project and
4
+ user-level skill roots. It scans `.claude/skills/`, `.agents/skills/`, or both,
5
+ checks each `SKILL.md` against the local skill quality specification derived
6
+ from the Agent Skills standards at <https://agentskills.io/home>, and can hand a
7
+ findings-specific repair prompt to `claude` or `codex`.
8
+
9
+ ## Install And Run
10
+
11
+ From a repository that contains project-local skills, or from anywhere when you
12
+ want to scan global user-level skills:
13
+
14
+ ```bash
15
+ bunx skills-doctor@latest
16
+ ```
17
+
18
+ For local development in this repo:
19
+
20
+ ```bash
21
+ bun install --frozen-lockfile
22
+ bun run verify
23
+ bun run dev
24
+ ```
25
+
26
+ You can also run the built binary through package managers after publishing:
27
+
28
+ ```bash
29
+ skills-doctor
30
+ ```
31
+
32
+ ## What It Scans
33
+
34
+ Skills Doctor detects these project-local roots relative to the directory you
35
+ run it from:
36
+
37
+ - `.claude/skills/`
38
+ - `.agents/skills/`
39
+
40
+ It also detects these global user-level roots:
41
+
42
+ - `~/.claude/skills/`
43
+ - `~/.agents/skills/`
44
+
45
+ When local and global roots both exist, the interactive CLI first asks whether
46
+ to scan local project skills, global/root skills, or both. When both Claude and
47
+ Codex/agents roots exist in the selected scope, it asks whether to scan Claude,
48
+ Codex/agents, or both. When no known root exists, it prompts for a custom skills
49
+ directory. Non-interactive runs use conservative defaults and fail with a clear
50
+ user error when a required choice cannot be made.
51
+
52
+ ## What It Checks
53
+
54
+ The scanner validates skills against `docs/SKILLS_SPEC.md`, which consolidates
55
+ the Agent Skills standards from <https://agentskills.io/home>, including:
56
+
57
+ - required YAML frontmatter and valid `name`/`description` fields
58
+ - trigger-oriented descriptions
59
+ - non-generic skill bodies with concrete workflow structure
60
+ - progressive disclosure for large or referenced material
61
+ - referenced `references/`, `scripts/`, and `assets/` files
62
+ - script guidance that is non-interactive and reproducible
63
+ - eval guidance for non-trivial skills
64
+ - divergent same-name skills across Claude and Codex/agents roots
65
+
66
+ Findings are grouped as blocking errors, warnings, and advisory improvements.
67
+ The human summary opens with a score header showing a face, `0` to `100` score,
68
+ label, and proportional terminal bar. The score starts at 100 and deducts 1.5
69
+ points for each distinct error rule and 0.75 points for each distinct warning
70
+ rule; repeated findings from the same rule do not increase the penalty. Advisory
71
+ findings are counted in the report but do not affect the score. Score labels are
72
+ `Great` for 75 or higher, `Needs work` for 50 through 74, and `Critical` below
73
+ 50.
74
+
75
+ ## Interactive Repair Flow
76
+
77
+ When findings exist, the CLI can:
78
+
79
+ 1. Show a concise score, skill count, and issue count.
80
+ 2. Let you choose a repair subset: errors, errors plus warnings, all findings,
81
+ or selected skills.
82
+ 3. Detect local `claude` and `codex` executables.
83
+ 4. Write a full report under `.skills-doctor/reports/<timestamp>/`.
84
+ 5. Generate a compact `handoff-prompt.md` tailored to the selected findings.
85
+ 6. Preview the launch command.
86
+ 7. Ask for explicit confirmation before handing the terminal to the selected
87
+ agent.
88
+ 8. Re-scan the same roots after the agent exits and report fixed, remaining,
89
+ and new findings.
90
+
91
+ Launch mappings:
92
+
93
+ - Claude: `claude --dangerously-skip-permissions <prompt>`
94
+ - Codex: `codex --yolo <prompt>`
95
+
96
+ Skills Doctor does not edit skill files during the scan phase. Repairs are made
97
+ only by the local agent after you confirm the handoff.
98
+
99
+ ## JSON Mode
100
+
101
+ Use JSON output for automation:
102
+
103
+ ```bash
104
+ skills-doctor --json
105
+ skills-doctor --json --json-compact
106
+ skills-doctor --yes --json
107
+ ```
108
+
109
+ JSON mode writes one machine-readable report to stdout and suppresses prompts
110
+ and spinners. Human logs and expected errors stay out of stdout.
111
+
112
+ ## Exit Codes
113
+
114
+ - `0`: no blocking errors remain in the final scan.
115
+ - `1`: blocking errors remain, no readable skills root was available, or another
116
+ expected user error occurred.
117
+
118
+ ## Privacy
119
+
120
+ Skills Doctor reads local skill files and writes local report files. It does not
121
+ upload skill contents or call a hosted model. Content leaves the process only if
122
+ you explicitly launch a local agent CLI such as `claude` or `codex`, and that
123
+ agent then follows its own configuration.
124
+
125
+ ## Release Checklist
126
+
127
+ Before tagging a release:
128
+
129
+ ```bash
130
+ bun run verify
131
+ bun run pack:dry-run
132
+ node scripts/extract-release-notes.mjs 0.1.0
133
+ ```
134
+
135
+ Then:
136
+
137
+ 1. Update `package.json` version.
138
+ 2. Move changelog entries from `Unreleased` into `## [x.y.z] - YYYY-MM-DD`.
139
+ 3. Commit the release prep.
140
+ 4. Tag `v<x.y.z>`.
141
+ 5. Push the tag to trigger the release workflow.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ import module from "node:module";
4
+
5
+ if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
6
+ try {
7
+ module.enableCompileCache();
8
+ } catch {
9
+ // Compile cache is an optimization; startup must not depend on it.
10
+ }
11
+ }
12
+
13
+ await import("../dist/cli/index.js");
@@ -0,0 +1,29 @@
1
+ import type { ScanReport } from "../../domain/build-report.js";
2
+ import type { AgentAvailabilityProbe, RepairAgentLauncher } from "../utils/launch-agent.js";
3
+ import { type PromptAdapter } from "../utils/prompts.js";
4
+ import { type SpinnerFactory } from "../utils/spinner.js";
5
+ export type ScanFlags = {
6
+ readonly json?: boolean;
7
+ readonly jsonCompact?: boolean;
8
+ readonly yes?: boolean;
9
+ };
10
+ export type ScanActionOptions = {
11
+ readonly cwd?: string;
12
+ readonly homeDir?: string | undefined;
13
+ readonly env?: NodeJS.ProcessEnv;
14
+ readonly stdinIsTty?: boolean;
15
+ readonly prompts?: PromptAdapter;
16
+ readonly writeStdout?: (message: string) => void;
17
+ readonly writeStderr?: (message: string) => void;
18
+ readonly stdoutIsTty?: boolean;
19
+ readonly terminalColumns?: number | undefined;
20
+ readonly animateScoreHeader?: boolean;
21
+ readonly spinner?: SpinnerFactory;
22
+ readonly version?: string;
23
+ readonly isRepairAgentAvailable?: AgentAvailabilityProbe;
24
+ readonly repairReportOutputRoot?: string;
25
+ readonly repairReportTimestamp?: string;
26
+ readonly launchAgent?: RepairAgentLauncher;
27
+ };
28
+ export declare const scanAction: (directory: string, flags: ScanFlags, options?: ScanActionOptions) => Promise<ScanReport>;
29
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/scan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAU/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM5F,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGhF,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEzE,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IACzD,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC;CAC5C,CAAC;AAMF,eAAO,MAAM,UAAU,GACrB,WAAW,MAAM,EACjB,OAAO,SAAS,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,UAAU,CAyFpB,CAAC"}
@@ -0,0 +1,234 @@
1
+ import path from "node:path";
2
+ import { buildScanReport } from "../../domain/build-report.js";
3
+ import { compareFindings, renderPostHandoffSummary } from "../../domain/compare-findings.js";
4
+ import { discoverSkillRoots } from "../../domain/discover-skill-roots.js";
5
+ import { scanSkillRoots } from "../../domain/scan-skills.js";
6
+ import { renderHumanSummary, resolveScanExitCode } from "../../domain/summarize-findings.js";
7
+ import { CliInputError } from "../utils/handle-error.js";
8
+ import { prepareRepairHandoff } from "../utils/handoff-to-agent.js";
9
+ import { enableJsonMode, writeJsonReport } from "../utils/json-mode.js";
10
+ import { chooseRepairAgent, formatRepairAgentPreview, launchRepairAgent, } from "../utils/launch-agent.js";
11
+ import { inquirerPromptAdapter } from "../utils/prompts.js";
12
+ import { printScoreHeader } from "../utils/render-score-header.js";
13
+ import { shouldSkipPrompts } from "../utils/should-skip-prompts.js";
14
+ import { createSpinner } from "../utils/spinner.js";
15
+ export const scanAction = async (directory, flags, options = {}) => {
16
+ const cwd = path.resolve(options.cwd ?? process.cwd(), directory);
17
+ const prompts = options.prompts ?? inquirerPromptAdapter;
18
+ const writeStdout = options.writeStdout ?? ((message) => process.stdout.write(message));
19
+ const writeStderr = options.writeStderr ?? ((message) => process.stderr.write(message));
20
+ const stdoutIsTty = options.stdoutIsTty ?? process.stdout.isTTY === true;
21
+ const skipPrompts = shouldSkipPrompts({
22
+ yes: Boolean(flags.yes),
23
+ json: Boolean(flags.json),
24
+ env: options.env,
25
+ stdinIsTty: options.stdinIsTty ?? process.stdin.isTTY,
26
+ });
27
+ const spinner = options.spinner ??
28
+ createSpinner({
29
+ enabled: !skipPrompts && !flags.json,
30
+ write: (message) => writeStderr(`${message}\n`),
31
+ });
32
+ if (flags.json) {
33
+ enableJsonMode({ compact: Boolean(flags.jsonCompact), directory: cwd });
34
+ }
35
+ const discovered = await spinner.run("Finding local skill roots...", () => discoverSkillRoots({ cwd, homeDir: options.homeDir }));
36
+ let roots = discovered.roots;
37
+ if (roots.length === 0) {
38
+ if (skipPrompts) {
39
+ throw new CliInputError("No .claude/skills or .agents/skills root was found. Re-run interactively or add a supported skills root.");
40
+ }
41
+ const customRoot = await prompts.input("Skills directory path", ".");
42
+ const custom = await discoverSkillRoots({
43
+ cwd,
44
+ homeDir: options.homeDir,
45
+ customRoots: [{ rootPath: customRoot, ecosystem: "custom" }],
46
+ });
47
+ roots = custom.roots;
48
+ }
49
+ else if (!skipPrompts) {
50
+ roots = await selectRootScopes(roots, prompts);
51
+ roots = await selectRoots(roots, prompts);
52
+ }
53
+ if (roots.length === 0) {
54
+ throw new CliInputError("No readable skills root was selected.");
55
+ }
56
+ const scan = await spinner.run("Scanning skills...", () => scanSkillRoots({ roots }));
57
+ const report = buildScanReport({
58
+ version: options.version ?? "0.0.0",
59
+ directory: cwd,
60
+ elapsedMilliseconds: 0,
61
+ scan,
62
+ });
63
+ let finalReport = report;
64
+ if (flags.json) {
65
+ writeJsonReport(report, writeStdout);
66
+ }
67
+ else {
68
+ await printScoreHeader({
69
+ score: report.score,
70
+ write: writeStdout,
71
+ color: stdoutIsTty,
72
+ columns: options.terminalColumns ?? process.stdout.columns,
73
+ animate: options.animateScoreHeader ?? (!skipPrompts && stdoutIsTty),
74
+ });
75
+ writeStdout(renderHumanSummary(report, { includeScore: false }));
76
+ if (!skipPrompts && report.findingCount > 0) {
77
+ finalReport =
78
+ (await reviewFindings(report, {
79
+ cwd,
80
+ roots,
81
+ version: options.version ?? "0.0.0",
82
+ spinner,
83
+ prompts,
84
+ write: writeStdout,
85
+ isRepairAgentAvailable: options.isRepairAgentAvailable,
86
+ repairReportOutputRoot: options.repairReportOutputRoot,
87
+ repairReportTimestamp: options.repairReportTimestamp,
88
+ launchAgent: options.launchAgent ?? launchRepairAgent,
89
+ })) ?? report;
90
+ }
91
+ }
92
+ process.exitCode = resolveScanExitCode(finalReport);
93
+ return finalReport;
94
+ };
95
+ const selectRoots = async (roots, prompts) => {
96
+ const hasClaude = roots.some((root) => root.ecosystem === "claude");
97
+ const hasCodex = roots.some((root) => root.ecosystem === "codex");
98
+ if (!hasClaude || !hasCodex)
99
+ return roots;
100
+ const selection = await prompts.select("Choose skills folder to scan", [
101
+ { name: "Both", value: "all" },
102
+ { name: "Claude (.claude/skills)", value: "claude" },
103
+ { name: "Codex/agents (.agents/skills)", value: "codex" },
104
+ ]);
105
+ if (selection === "all")
106
+ return roots;
107
+ return roots.filter((root) => root.ecosystem === selection);
108
+ };
109
+ const selectRootScopes = async (roots, prompts) => {
110
+ const hasLocal = roots.some((root) => root.source === "local");
111
+ const hasGlobal = roots.some((root) => root.source === "global");
112
+ if (!hasLocal || !hasGlobal)
113
+ return roots;
114
+ const selection = await prompts.select("Choose skills scope to scan", [
115
+ { name: "Both local project and global/root skills", value: "all" },
116
+ { name: "Local project skills (./.claude/skills, ./.agents/skills)", value: "local" },
117
+ { name: "Global/root skills (~/.claude/skills, ~/.agents/skills)", value: "global" },
118
+ ]);
119
+ if (selection === "all")
120
+ return roots;
121
+ return roots.filter((root) => root.source === selection);
122
+ };
123
+ const reviewFindings = async (report, input) => {
124
+ const { prompts, write } = input;
125
+ const action = await prompts.select("Next step", [
126
+ { name: "Fix skills with Claude or Codex", value: "repair" },
127
+ ...(report.errorCount > 0 ? [{ name: "View errors", value: "errors" }] : []),
128
+ { name: "View all findings", value: "all" },
129
+ { name: "Exit", value: "exit" },
130
+ ]);
131
+ if (action === "exit")
132
+ return;
133
+ if (action === "repair") {
134
+ return runRepairAgentFlow(report, input);
135
+ }
136
+ const selectedFindings = action === "errors"
137
+ ? report.findings.filter((finding) => finding.severity === "error")
138
+ : report.findings;
139
+ if (action === "by-skill") {
140
+ write(renderFindingsBySkill(selectedFindings));
141
+ return;
142
+ }
143
+ write(renderFindings(selectedFindings));
144
+ return undefined;
145
+ };
146
+ const runRepairAgentFlow = async (report, input) => {
147
+ try {
148
+ const agent = await chooseRepairAgent({
149
+ prompts: input.prompts,
150
+ isAvailable: input.isRepairAgentAvailable,
151
+ });
152
+ if (agent === undefined) {
153
+ input.write("Repair handoff cancelled.\n");
154
+ return undefined;
155
+ }
156
+ const handoff = await prepareRepairHandoff({
157
+ report,
158
+ prompts: input.prompts,
159
+ outputRoot: input.repairReportOutputRoot,
160
+ timestamp: input.repairReportTimestamp,
161
+ });
162
+ input.write(`Selected ${agent.displayName}.\n`);
163
+ input.write(`Launch preview: ${formatRepairAgentPreview(agent.id)}\n`);
164
+ if (handoff.reportDirectory !== undefined) {
165
+ input.write(`Report directory: ${handoff.reportDirectory}\n`);
166
+ }
167
+ if (handoff.promptPath !== undefined) {
168
+ input.write(`Repair prompt: ${handoff.promptPath}\n`);
169
+ }
170
+ else {
171
+ input.write(`Repair prompt:\n${handoff.prompt}\n`);
172
+ }
173
+ if (handoff.reportWriteError !== undefined) {
174
+ input.write(`Report write failed: ${handoff.reportWriteError.message}\n`);
175
+ }
176
+ const shouldLaunch = await input.prompts.confirm(`Launch ${agent.displayName} now?`, false);
177
+ if (!shouldLaunch) {
178
+ input.write("Agent launch cancelled.\n");
179
+ return undefined;
180
+ }
181
+ let exitCode;
182
+ try {
183
+ exitCode = await input.launchAgent(agent.id, handoff.prompt, input.cwd);
184
+ }
185
+ catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ input.write(`Agent launch failed: ${message}\n`);
188
+ return undefined;
189
+ }
190
+ if (exitCode !== 0) {
191
+ input.write(`Agent exited with code ${exitCode}.\n`);
192
+ }
193
+ const nextScan = await input.spinner.run("Re-scanning skills...", () => scanSkillRoots({ roots: input.roots }));
194
+ const nextReport = buildScanReport({
195
+ version: input.version,
196
+ directory: input.cwd,
197
+ elapsedMilliseconds: 0,
198
+ scan: nextScan,
199
+ handoffRequested: true,
200
+ });
201
+ const comparison = compareFindings(report.findings, nextReport.findings);
202
+ input.write(renderPostHandoffSummary(comparison, nextReport));
203
+ if (nextReport.findingCount > 0 &&
204
+ (await input.prompts.confirm("Run another repair pass?", false))) {
205
+ return (await reviewFindings(nextReport, input)) ?? nextReport;
206
+ }
207
+ return nextReport;
208
+ }
209
+ catch (error) {
210
+ if (error instanceof CliInputError) {
211
+ input.write(`${error.message}\n`);
212
+ return undefined;
213
+ }
214
+ throw error;
215
+ }
216
+ };
217
+ const renderFindings = (findings) => `${findings
218
+ .map((finding) => `[${finding.severity}] ${finding.ruleId} ${finding.skillName ?? finding.skillPath}\n${finding.message}\nSuggestion: ${finding.suggestion}`)
219
+ .join("\n\n")}\n`;
220
+ const renderFindingsBySkill = (findings) => {
221
+ const groups = new Map();
222
+ for (const finding of findings) {
223
+ const key = finding.skillName ?? finding.skillPath;
224
+ groups.set(key, [...(groups.get(key) ?? []), finding]);
225
+ }
226
+ return [...groups.entries()]
227
+ .map(([skillName, skillFindings]) => {
228
+ const lines = [`${skillName}:`];
229
+ lines.push(...skillFindings.map((finding) => `- [${finding.severity}] ${finding.ruleId}`));
230
+ return lines.join("\n");
231
+ })
232
+ .join("\n\n")
233
+ .concat("\n");
234
+ };
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ export declare const buildProgram: () => Command;
3
+ export declare const main: (argv?: readonly string[]) => Promise<void>;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,eAAO,MAAM,YAAY,QAAO,OAmB/B,CAAC;AAEF,eAAO,MAAM,IAAI,GAAU,OAAM,SAAS,MAAM,EAAiB,KAAG,OAAO,CAAC,IAAI,CAY/E,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { Command } from "commander";
2
+ import { scanAction } from "./commands/scan.js";
3
+ import { handleCliError } from "./utils/handle-error.js";
4
+ export const buildProgram = () => {
5
+ const program = new Command()
6
+ .name("skills-doctor")
7
+ .description("Scan Agent Skills and report quality issues.")
8
+ .version("0.0.0", "-v, --version", "display the version number")
9
+ .argument("[directory]", "directory to scan from", ".")
10
+ .option("--json", "output one machine-readable JSON report")
11
+ .option("--json-compact", "with --json, omit indentation")
12
+ .option("-y, --yes", "skip prompts and use conservative defaults")
13
+ .action(async (directory, flags) => {
14
+ await scanAction(directory, flags);
15
+ });
16
+ return program;
17
+ };
18
+ export const main = async (argv = process.argv) => {
19
+ process.on("SIGINT", () => process.exit(130));
20
+ process.on("SIGTERM", () => process.exit(143));
21
+ process.stdout.on("error", (error) => {
22
+ if (error.code === "EPIPE")
23
+ process.exit(0);
24
+ });
25
+ try {
26
+ await buildProgram().parseAsync([...argv]);
27
+ }
28
+ finally {
29
+ process.stdin.unref?.();
30
+ }
31
+ };
32
+ main().catch(handleCliError);
@@ -0,0 +1,8 @@
1
+ export type CliLogger = {
2
+ readonly log: (message: string) => void;
3
+ readonly warn: (message: string) => void;
4
+ readonly error: (message: string) => void;
5
+ readonly break: () => void;
6
+ };
7
+ export declare const cliLogger: CliLogger;
8
+ //# sourceMappingURL=cli-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-logger.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/cli-logger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,SAavB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export const cliLogger = {
2
+ log: (message) => {
3
+ process.stderr.write(`${message}\n`);
4
+ },
5
+ warn: (message) => {
6
+ process.stderr.write(`${message}\n`);
7
+ },
8
+ error: (message) => {
9
+ process.stderr.write(`${message}\n`);
10
+ },
11
+ break: () => {
12
+ process.stderr.write("\n");
13
+ },
14
+ };
@@ -0,0 +1,6 @@
1
+ export declare class CliInputError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare const isExpectedUserError: (error: unknown) => error is CliInputError;
5
+ export declare const handleCliError: (error: unknown) => void;
6
+ //# sourceMappingURL=handle-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handle-error.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/handle-error.ts"],"names":[],"mappings":"AAGA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,eAAO,MAAM,mBAAmB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,aAC9B,CAAC;AAEjC,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,IAgB/C,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { cliLogger } from "./cli-logger.js";
2
+ import { isJsonModeActive, writeJsonErrorReport } from "./json-mode.js";
3
+ export class CliInputError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "CliInputError";
7
+ }
8
+ }
9
+ export const isExpectedUserError = (error) => error instanceof CliInputError;
10
+ export const handleCliError = (error) => {
11
+ if (isJsonModeActive()) {
12
+ writeJsonErrorReport(error);
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ if (isExpectedUserError(error)) {
17
+ cliLogger.error(error.message);
18
+ process.exitCode = 1;
19
+ return;
20
+ }
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ cliLogger.error(`Unexpected error: ${message}`);
23
+ process.exitCode = 1;
24
+ };
@@ -0,0 +1,21 @@
1
+ import type { ScanReport } from "../../domain/build-report.js";
2
+ import type { Finding } from "../../domain/types.js";
3
+ import { writeFindingsDirectory } from "../../domain/write-findings-directory.js";
4
+ import type { PromptAdapter } from "./prompts.js";
5
+ export type RepairFindingSubset = "errors" | "errors-and-warnings" | "all" | "selected-skills";
6
+ export type PreparedRepairHandoff = {
7
+ readonly findings: readonly Finding[];
8
+ readonly prompt: string;
9
+ readonly reportDirectory?: string | undefined;
10
+ readonly promptPath?: string | undefined;
11
+ readonly reportWriteError?: Error | undefined;
12
+ };
13
+ export type PrepareRepairHandoffInput = {
14
+ readonly report: ScanReport;
15
+ readonly prompts: PromptAdapter;
16
+ readonly outputRoot?: string | undefined;
17
+ readonly timestamp?: string | undefined;
18
+ readonly writeDirectory?: typeof writeFindingsDirectory | undefined;
19
+ };
20
+ export declare const prepareRepairHandoff: (input: PrepareRepairHandoffInput) => Promise<PreparedRepairHandoff>;
21
+ //# sourceMappingURL=handoff-to-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-to-agent.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/handoff-to-agent.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAKrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AAElF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,qBAAqB,GAAG,KAAK,GAAG,iBAAiB,CAAC;AAE/F,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,sBAAsB,GAAG,SAAS,CAAC;CACrE,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,OAAO,yBAAyB,KAC/B,OAAO,CAAC,qBAAqB,CAgC/B,CAAC"}