@workerstack/repo 0.0.0-stage → 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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Unified prereq doctor. Print-only install hints. Never runs installs. */
|
|
3
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
PACKAGED_SKILLS_REL,
|
|
10
|
+
WORKSPACE_SKILLS_REL,
|
|
11
|
+
findSkillContentConflicts,
|
|
12
|
+
formatSkillConflictWarn,
|
|
13
|
+
} from "./skill-conflicts.ts";
|
|
14
|
+
import { isRecord } from "./hook-input.ts";
|
|
15
|
+
import { INDEX_REL, sectionBody } from "../sync/maps.ts";
|
|
16
|
+
import { isSelf } from "../sync/stock.ts";
|
|
17
|
+
import { enabledSkills } from "../config.ts";
|
|
18
|
+
import { nestedHolds, nestedTmpDirs } from "./clean.ts";
|
|
19
|
+
import { redundantIgnoreLines } from "./gitignore-guard.ts";
|
|
20
|
+
import { canonicalPath, gitRevParse, resolveRoots } from "./workspace-root.ts";
|
|
21
|
+
import { WORKTREE_CMD, defaultBranch, listWorktrees, type WorktreeInfo } from "./worktree.ts";
|
|
22
|
+
import { readEnv } from "../env.ts";
|
|
23
|
+
import type { Env } from "../env.ts";
|
|
24
|
+
|
|
25
|
+
/** One doctor check's outcome; `extra` is the detail printed after the status. */
|
|
26
|
+
export interface DoctorStatus {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
extra: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A workspace leftover and the command that clears exactly it. */
|
|
32
|
+
export interface WorkspaceIssue {
|
|
33
|
+
kind: string;
|
|
34
|
+
path: string;
|
|
35
|
+
detail: string;
|
|
36
|
+
clear: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DoctorResult {
|
|
40
|
+
code: number;
|
|
41
|
+
stdout: string;
|
|
42
|
+
stderr: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ModelCli = "codex" | "grok" | "agy" | "cursor";
|
|
46
|
+
type ListCli = Exclude<ModelCli, "codex" | "agy">;
|
|
47
|
+
|
|
48
|
+
/** A model named in a generated agent file, with the CLI that has to offer it. */
|
|
49
|
+
interface ModelRef {
|
|
50
|
+
cli: ModelCli;
|
|
51
|
+
file: string;
|
|
52
|
+
model: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What a CLI offers (`ids`), or why it was skipped. */
|
|
56
|
+
interface Offered {
|
|
57
|
+
ids?: string[];
|
|
58
|
+
skip?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ListResult {
|
|
62
|
+
code: number | null;
|
|
63
|
+
stdout: string;
|
|
64
|
+
stderr: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type ListRunner = (bin: string, args: string[], opts: { signal: AbortSignal }) => Promise<ListResult>;
|
|
68
|
+
|
|
69
|
+
const errorText = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
70
|
+
|
|
71
|
+
export const CHROMIUM_BINS: string[] = [
|
|
72
|
+
"chromium",
|
|
73
|
+
"chromium-browser",
|
|
74
|
+
"google-chrome",
|
|
75
|
+
"google-chrome-stable",
|
|
76
|
+
"chrome",
|
|
77
|
+
"msedge",
|
|
78
|
+
"microsoft-edge",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export const LIGHTPANDA_REPO = "https://github.com/lightpanda-io/browser";
|
|
82
|
+
const RTK_CURL = "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh";
|
|
83
|
+
const RTK_DOCS = "https://www.rtk-ai.app/docs/getting-started/installation/";
|
|
84
|
+
const LP_INSTALL_SH = "curl -fsSL https://pkg.lightpanda.io/install.sh | bash";
|
|
85
|
+
const REQUIRED = ["bun", "git", "rtk", "lightpanda", "chromium"];
|
|
86
|
+
const MAPS_SECTIONS = ["skills", "refs"];
|
|
87
|
+
|
|
88
|
+
function winShim(bin: string): boolean {
|
|
89
|
+
return process.platform === "win32" && /\.(cmd|bat)$/i.test(bin);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function whichBin(
|
|
93
|
+
name: string,
|
|
94
|
+
pathEnv: string | undefined = readEnv().PATH,
|
|
95
|
+
platform: string = process.platform,
|
|
96
|
+
): string {
|
|
97
|
+
if (!name) return "";
|
|
98
|
+
const hostWin = process.platform === "win32";
|
|
99
|
+
const delim = (platform === "win32" || hostWin) && String(pathEnv || "").includes(";") ? ";" : path.delimiter;
|
|
100
|
+
const dirs = String(pathEnv || "").split(delim);
|
|
101
|
+
const names: string[] = [];
|
|
102
|
+
if ((hostWin || platform === "win32") && !path.extname(name)) {
|
|
103
|
+
if (hostWin) {
|
|
104
|
+
const pathext = String(readEnv().PATHEXT || ".EXE;.CMD;.BAT;.COM");
|
|
105
|
+
for (const raw of pathext.split(";")) {
|
|
106
|
+
if (!raw) continue;
|
|
107
|
+
names.push(name + raw.toLowerCase());
|
|
108
|
+
names.push(name + raw);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
names.push(`${name}.exe`);
|
|
112
|
+
names.push(name);
|
|
113
|
+
} else {
|
|
114
|
+
names.push(name);
|
|
115
|
+
}
|
|
116
|
+
const uniq = [...new Set(names)];
|
|
117
|
+
for (const dir of dirs) {
|
|
118
|
+
if (!dir) continue;
|
|
119
|
+
for (const n of uniq) {
|
|
120
|
+
const candidate = path.join(dir, n);
|
|
121
|
+
try {
|
|
122
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
123
|
+
return candidate;
|
|
124
|
+
} catch {
|
|
125
|
+
if (hostWin) {
|
|
126
|
+
try {
|
|
127
|
+
fs.accessSync(candidate, fs.constants.F_OK);
|
|
128
|
+
return candidate;
|
|
129
|
+
} catch {
|
|
130
|
+
/* try next */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function delimOf(pathEnv: string | undefined, platform: string): string {
|
|
140
|
+
return platform === "win32" && String(pathEnv || "").includes(";") ? ";" : path.delimiter;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withNmBin(cwd: string, pathEnv: string | undefined, platform: string): string {
|
|
144
|
+
const nm = path.join(cwd, "node_modules", ".bin");
|
|
145
|
+
const d = delimOf(pathEnv, platform);
|
|
146
|
+
return `${nm}${d}${pathEnv || ""}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Floor when no package.json beside the scripts declares `engines.bun` (init copies the scripts into consumers). */
|
|
150
|
+
export const BUN_MIN_FALLBACK = "1.4.4";
|
|
151
|
+
|
|
152
|
+
const BESIDE_PACKAGE_JSON = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
153
|
+
|
|
154
|
+
/** The `engines.bun` floor from the package.json two levels above this script, as `1.4.4`. */
|
|
155
|
+
export function bunMinimum(file: string = BESIDE_PACKAGE_JSON): string {
|
|
156
|
+
try {
|
|
157
|
+
const pkg: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
158
|
+
const range = isRecord(pkg) && isRecord(pkg.engines) ? pkg.engines.bun : undefined;
|
|
159
|
+
return /\d+(?:\.\d+){0,2}/.exec(String(range))?.[0] ?? BUN_MIN_FALLBACK;
|
|
160
|
+
} catch {
|
|
161
|
+
return BUN_MIN_FALLBACK;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** True when dotted version `have` (an optional leading `v`) is at least `min`; an unreadable `have` is not. */
|
|
166
|
+
export function versionAtLeast(have: string, min: string): boolean {
|
|
167
|
+
const a = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(have);
|
|
168
|
+
if (!a) return false;
|
|
169
|
+
const b = min.split(".").map(Number);
|
|
170
|
+
for (let i = 0; i < 3; i++) {
|
|
171
|
+
const x = Number(a[i + 1] ?? 0);
|
|
172
|
+
const y = b[i] ?? 0;
|
|
173
|
+
if (x !== y) return x > y;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readVersion(bin: string, flag: string): string {
|
|
179
|
+
try {
|
|
180
|
+
return String(
|
|
181
|
+
execFileSync(bin, [flag], {
|
|
182
|
+
encoding: "utf8",
|
|
183
|
+
timeout: 8000,
|
|
184
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
185
|
+
shell: winShim(bin),
|
|
186
|
+
}),
|
|
187
|
+
).trim();
|
|
188
|
+
} catch {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function lookupChromium(pathEnv: string | undefined, platform: string): string {
|
|
194
|
+
for (const n of CHROMIUM_BINS) {
|
|
195
|
+
const hit = whichBin(n, pathEnv, platform);
|
|
196
|
+
if (hit) return hit;
|
|
197
|
+
}
|
|
198
|
+
return "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function installHints(id: string, platform: string = process.platform): string[] {
|
|
202
|
+
if (id === "all") {
|
|
203
|
+
const keys = ["bun", "git", "rtk", "lightpanda", "chromium"];
|
|
204
|
+
return keys.flatMap((k) => installHints(k, platform));
|
|
205
|
+
}
|
|
206
|
+
if (id === "bun") {
|
|
207
|
+
if (platform === "win32") return ['powershell -c "irm bun.sh/install.ps1 | iex"', "https://bun.sh"];
|
|
208
|
+
return ["curl -fsSL https://bun.sh/install | bash", "https://bun.sh"];
|
|
209
|
+
}
|
|
210
|
+
if (id === "git") {
|
|
211
|
+
if (platform === "win32") return ["winget install Git.Git"];
|
|
212
|
+
if (platform === "darwin") return ["brew install git"];
|
|
213
|
+
return ["sudo apt install git"];
|
|
214
|
+
}
|
|
215
|
+
if (id === "rtk") {
|
|
216
|
+
if (platform === "win32") {
|
|
217
|
+
return ["rtk.exe on PATH (release zip) or WSL curl", RTK_DOCS];
|
|
218
|
+
}
|
|
219
|
+
return [RTK_CURL, "brew install rtk"];
|
|
220
|
+
}
|
|
221
|
+
if (id === "lightpanda") {
|
|
222
|
+
if (platform === "win32") {
|
|
223
|
+
return [
|
|
224
|
+
"Windows: native Lightpanda not required; WSL2 only",
|
|
225
|
+
"wsl --install",
|
|
226
|
+
`in WSL: ${LP_INSTALL_SH}`,
|
|
227
|
+
LIGHTPANDA_REPO,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
if (platform === "darwin") {
|
|
231
|
+
return [
|
|
232
|
+
LP_INSTALL_SH,
|
|
233
|
+
"brew tap lightpanda-io/browser",
|
|
234
|
+
"brew install lightpanda-io/browser/lightpanda",
|
|
235
|
+
LIGHTPANDA_REPO,
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
return [LP_INSTALL_SH, LIGHTPANDA_REPO];
|
|
239
|
+
}
|
|
240
|
+
if (id === "chromium") {
|
|
241
|
+
if (platform === "win32") {
|
|
242
|
+
return [
|
|
243
|
+
"winget install Google.Chrome",
|
|
244
|
+
"winget install Microsoft.Edge",
|
|
245
|
+
"add chrome.exe or msedge.exe to PATH or shim",
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
if (platform === "darwin") {
|
|
249
|
+
return ["brew install --cask chromium", "brew install --cask google-chrome"];
|
|
250
|
+
}
|
|
251
|
+
return ["sudo apt install chromium"];
|
|
252
|
+
}
|
|
253
|
+
if (id === "ffmpeg") {
|
|
254
|
+
if (platform === "win32") return ["winget install Gyan.FFmpeg"];
|
|
255
|
+
if (platform === "darwin") return ["brew install ffmpeg"];
|
|
256
|
+
return ["sudo apt install ffmpeg"];
|
|
257
|
+
}
|
|
258
|
+
if (id === "whisper-cli") {
|
|
259
|
+
if (platform === "win32") {
|
|
260
|
+
return ["whisper.cpp release: whisper-cli.exe on PATH", "https://github.com/ggml-org/whisper.cpp"];
|
|
261
|
+
}
|
|
262
|
+
if (platform === "darwin") return ["brew install whisper-cpp"];
|
|
263
|
+
return ["https://github.com/ggml-org/whisper.cpp"];
|
|
264
|
+
}
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isFile(abs: string): boolean {
|
|
269
|
+
try {
|
|
270
|
+
return fs.statSync(abs).isFile();
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function jevDoctorStatus({
|
|
277
|
+
env = readEnv(),
|
|
278
|
+
home = os.homedir(),
|
|
279
|
+
}: { env?: Env; home?: string } = {}): DoctorStatus {
|
|
280
|
+
if (String(env.TYPESAFE_API_KEY || "").trim()) return { ok: true, extra: "env" };
|
|
281
|
+
try {
|
|
282
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(home, ".workerstack", "jev.json"), "utf8"));
|
|
283
|
+
if (String(isRecord(parsed) && parsed.apiKey ? parsed.apiKey : "").trim()) {
|
|
284
|
+
return { ok: true, extra: "config" };
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
/* absent */
|
|
288
|
+
}
|
|
289
|
+
return { ok: false, extra: "" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function speakDoctorStatus({
|
|
293
|
+
env = readEnv(),
|
|
294
|
+
home = os.homedir(),
|
|
295
|
+
}: { env?: Env; home?: string } = {}): DoctorStatus {
|
|
296
|
+
const names: string[] = [];
|
|
297
|
+
if (String(env.ELEVENLABS_API_KEY || "").trim()) names.push("elevenlabs");
|
|
298
|
+
if (String(env.OPENAI_API_KEY || "").trim()) names.push("openai");
|
|
299
|
+
const hasFile = isFile(path.join(home, ".workerstack", "speak.json"));
|
|
300
|
+
if (!names.length && !hasFile) return { ok: false, extra: "" };
|
|
301
|
+
return { ok: true, extra: names.length ? names.join(",") : "config" };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const WORKSPACE_ACCOUNT_RE = /^[a-zA-Z0-9_-]{1,40}$/;
|
|
305
|
+
const WORKSPACE_KIND_ORDER = ["oauth", "tokens", "adc", "clasprc"];
|
|
306
|
+
|
|
307
|
+
export function workspaceDoctorStatus({ home = os.homedir() }: { home?: string } = {}): DoctorStatus {
|
|
308
|
+
const repo = path.join(home, ".workerstack");
|
|
309
|
+
const kinds = new Set<string>();
|
|
310
|
+
const scanAccountDir = (dir: string): void => {
|
|
311
|
+
if (isFile(path.join(dir, "google-oauth.json"))) kinds.add("oauth");
|
|
312
|
+
if (isFile(path.join(dir, "tokens.json"))) kinds.add("tokens");
|
|
313
|
+
};
|
|
314
|
+
const root = path.join(repo, "workspace");
|
|
315
|
+
try {
|
|
316
|
+
for (const ent of fs.readdirSync(root, { withFileTypes: true })) {
|
|
317
|
+
if (!ent.isDirectory() || !WORKSPACE_ACCOUNT_RE.test(ent.name)) continue;
|
|
318
|
+
scanAccountDir(path.join(root, ent.name));
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
/* none */
|
|
322
|
+
}
|
|
323
|
+
if (isFile(path.join(home, ".config", "gcloud", "application_default_credentials.json"))) {
|
|
324
|
+
kinds.add("adc");
|
|
325
|
+
}
|
|
326
|
+
if (isFile(path.join(home, ".clasprc.json"))) kinds.add("clasprc");
|
|
327
|
+
if (!kinds.size) return { ok: false, extra: "" };
|
|
328
|
+
return { ok: true, extra: WORKSPACE_KIND_ORDER.filter((k) => kinds.has(k)).join(",") };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function row(name: string, status: string, extra: string = ""): string {
|
|
332
|
+
const a = String(name).padEnd(18);
|
|
333
|
+
const b = String(status).padEnd(18);
|
|
334
|
+
const c = extra ? String(extra).trim() : "";
|
|
335
|
+
return c ? `${a} ${b} ${c}` : `${a} ${b}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export const GLOBALS_MANIFEST_REL = "agents/global-settings/manifest.json";
|
|
339
|
+
|
|
340
|
+
/** The package's own root, two levels above `cli/scripts/doctor.ts`: where the stock globals live. */
|
|
341
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
342
|
+
const GROK_GLOBALS_REL = "agents/global-settings/grok.toml";
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* One `globals` row from checkGlobals: ok, the first drift line, or the check error. Null when the
|
|
346
|
+
* repo ships no manifest. check-globals needs smol-toml, which setup.ts (it imports this file)
|
|
347
|
+
* must run without, so it loads here on demand instead of at import.
|
|
348
|
+
*/
|
|
349
|
+
export async function globalsRow(
|
|
350
|
+
cwd: string | undefined,
|
|
351
|
+
home: string | undefined,
|
|
352
|
+
sourceRoot: string = PACKAGE_ROOT,
|
|
353
|
+
): Promise<string | null> {
|
|
354
|
+
if (!fs.existsSync(path.join(sourceRoot, GLOBALS_MANIFEST_REL))) return null;
|
|
355
|
+
let mod: typeof import("./check-globals.ts");
|
|
356
|
+
try {
|
|
357
|
+
mod = await import("./check-globals.ts");
|
|
358
|
+
} catch (err) {
|
|
359
|
+
return row("globals", "unavailable", `${errorText(err).split("\n")[0]}; run bun install`);
|
|
360
|
+
}
|
|
361
|
+
const r = mod.checkGlobals({
|
|
362
|
+
sourceRoot,
|
|
363
|
+
...(home === undefined ? {} : { home }),
|
|
364
|
+
...(cwd === undefined ? {} : { workspaceRoot: cwd }),
|
|
365
|
+
});
|
|
366
|
+
if (r.ok) return row("globals", "ok");
|
|
367
|
+
return row(
|
|
368
|
+
"globals",
|
|
369
|
+
r.drift ? mod.driftLine(r.drift[0]!) : (r.error /* a failed check sets drift or error */ as string),
|
|
370
|
+
); // drift is only set when non-empty
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The `maps` row: null outside `@workerstack/repo`; otherwise ok when `references/INDEX.md` has both generated marker pairs. */
|
|
374
|
+
export function mapsRow(root: string): { line: string; ok: boolean } | null {
|
|
375
|
+
if (!isSelf(root)) return null;
|
|
376
|
+
const fix = "run bun repo sync";
|
|
377
|
+
let text: string;
|
|
378
|
+
try {
|
|
379
|
+
text = fs.readFileSync(path.join(root, INDEX_REL), "utf8");
|
|
380
|
+
} catch {
|
|
381
|
+
return { line: row("maps", "missing", `${INDEX_REL} is missing; ${fix}`), ok: false };
|
|
382
|
+
}
|
|
383
|
+
const absent = MAPS_SECTIONS.find((name) => sectionBody(text, name) == null);
|
|
384
|
+
if (absent) return { line: row("maps", "missing", `${INDEX_REL} has no repo:${absent} section; ${fix}`), ok: false };
|
|
385
|
+
return { line: row("maps", "ok", `${INDEX_REL} ${MAPS_SECTIONS.map((n) => `repo:${n}`).join(", ")}`), ok: true };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function gitOut(cwd: string, args: string[]): string | null {
|
|
389
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
390
|
+
return r.status === 0 ? r.stdout : null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function ignoreLines(file: string): string[] {
|
|
394
|
+
try {
|
|
395
|
+
return fs
|
|
396
|
+
.readFileSync(file, "utf8")
|
|
397
|
+
.split(/\r?\n/)
|
|
398
|
+
.map((l) => l.trimEnd());
|
|
399
|
+
} catch {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Days a worktree may sit unmerged before doctor lists it; the window `commit-push` uses for recent branches. */
|
|
405
|
+
const STALE_DAYS = 7;
|
|
406
|
+
const DAY_SECONDS = 86_400;
|
|
407
|
+
|
|
408
|
+
/** Whole days since the branch's last commit when it holds commits the default branch lacks, else null. */
|
|
409
|
+
function daysSinceUnmergedCommit(root: string, branch: string, target: string): number | null {
|
|
410
|
+
const ahead = Number(gitOut(root, ["rev-list", "--count", `${target}..refs/heads/${branch}`]));
|
|
411
|
+
const last = Number(gitOut(root, ["log", "-1", "--format=%ct", `refs/heads/${branch}`]));
|
|
412
|
+
return ahead > 0 && last > 0 ? Math.floor((Date.now() / 1000 - last) / DAY_SECONDS) : null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Paths in one list are distinct, so two never compare equal.
|
|
416
|
+
const byPath = (a: { path: string }, b: { path: string }): number => (a.path < b.path ? -1 : 1);
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Workspace leftovers (roadmap D9), each with the command that clears exactly it. Reads only; deletes nothing.
|
|
420
|
+
*/
|
|
421
|
+
export function workspaceIssues({ root, checkout }: { root: string; checkout: string }): WorkspaceIssue[] {
|
|
422
|
+
// resolveRoots already asked git about this dir; the memo saves a spawn.
|
|
423
|
+
const facts = gitRevParse(checkout);
|
|
424
|
+
if (!facts) return [];
|
|
425
|
+
const top = canonicalPath(path.resolve(facts.top));
|
|
426
|
+
const item = (kind: string, p: string, detail: string, clear: string): WorkspaceIssue => ({
|
|
427
|
+
kind,
|
|
428
|
+
path: p,
|
|
429
|
+
detail,
|
|
430
|
+
clear,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const stray = nestedTmpDirs(root).map((dir) => {
|
|
434
|
+
const holds = nestedHolds(dir);
|
|
435
|
+
return item("stray .tmp/repo", dir, holds ? `holds ${holds}` : "", `rm -r ${dir}`);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const listed = gitOut(top, ["ls-files", "-co", "--exclude-standard", "-z", "--", ":(glob)**/.gitignore"]) ?? "";
|
|
439
|
+
const redundant: WorkspaceIssue[] = [];
|
|
440
|
+
for (const rel of new Set(listed.split("\0").filter((r) => r && r !== ".gitignore"))) {
|
|
441
|
+
const file = path.join(top, ...rel.split("/"));
|
|
442
|
+
const lines = ignoreLines(file).filter((l) => l && !l.startsWith("#"));
|
|
443
|
+
const hits = lines.length ? redundantIgnoreLines(file, lines) : [];
|
|
444
|
+
if (!hits.length) continue;
|
|
445
|
+
const tracked = gitOut(top, ["ls-files", "--error-unmatch", "--", rel]) !== null;
|
|
446
|
+
const detail = hits.map((h) => `"${h.line}" ignored by ${h.rule}`).join(", ");
|
|
447
|
+
redundant.push(item("redundant .gitignore", file, detail, tracked ? `git -C ${top} rm -q ${rel}` : `rm ${file}`));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const rootIgnore = path.join(top, ".gitignore");
|
|
451
|
+
const lines = ignoreLines(rootIgnore);
|
|
452
|
+
const duplicate: WorkspaceIssue[] = [];
|
|
453
|
+
lines.forEach((line, i) => {
|
|
454
|
+
if (!line.endsWith("/") || line.startsWith("!") || line.length < 2) return;
|
|
455
|
+
const bare = lines.indexOf(line.slice(0, -1));
|
|
456
|
+
if (bare === -1) return;
|
|
457
|
+
duplicate.push(
|
|
458
|
+
item(
|
|
459
|
+
"duplicate .gitignore line",
|
|
460
|
+
`${rootIgnore}:${i + 1} "${line}"`,
|
|
461
|
+
`"${lines[bare]}" on line ${bare + 1} covers it`,
|
|
462
|
+
`delete line ${i + 1} of ${rootIgnore}`,
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const worktrees = listWorktrees(root);
|
|
468
|
+
const label = (w: WorktreeInfo): string => w.branch || "detached";
|
|
469
|
+
const missing = worktrees.filter((w) => w.state === "missing");
|
|
470
|
+
const outside = worktrees.filter((w) => w.state !== "missing" && !w.managed);
|
|
471
|
+
const merged = worktrees.filter((w) => w.state === "clean" && w.managed && w.merged);
|
|
472
|
+
const target = defaultBranch(root);
|
|
473
|
+
const stale: { w: WorktreeInfo; days: number }[] = [];
|
|
474
|
+
for (const w of worktrees) {
|
|
475
|
+
if (!w.managed || w.state === "missing" || w.merged || !w.branch) continue;
|
|
476
|
+
const days = daysSinceUnmergedCommit(root, w.branch, target);
|
|
477
|
+
if (days !== null && days >= STALE_DAYS) stale.push({ w, days });
|
|
478
|
+
}
|
|
479
|
+
return [
|
|
480
|
+
...stray.sort(byPath),
|
|
481
|
+
...redundant.sort(byPath),
|
|
482
|
+
...duplicate,
|
|
483
|
+
...missing.map((w) => item("missing worktree", w.path, label(w), `git -C ${root} worktree prune`)).sort(byPath),
|
|
484
|
+
...outside
|
|
485
|
+
.map((w) => item("worktree outside .worktrees", w.path, label(w), `git -C ${root} worktree remove ${w.path}`))
|
|
486
|
+
.sort(byPath),
|
|
487
|
+
...merged.map((w) => item("merged worktree", w.path, label(w), `${WORKTREE_CMD} prune`)).sort(byPath),
|
|
488
|
+
...stale
|
|
489
|
+
.map(({ w, days }) =>
|
|
490
|
+
item(
|
|
491
|
+
"stale worktree",
|
|
492
|
+
w.path,
|
|
493
|
+
`${w.branch}, last commit ${days} days ago`,
|
|
494
|
+
`${WORKTREE_CMD} finish ${path.basename(w.path)}`,
|
|
495
|
+
),
|
|
496
|
+
)
|
|
497
|
+
.sort(byPath),
|
|
498
|
+
];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function workspaceLines(items: WorkspaceIssue[]): string[] {
|
|
502
|
+
if (!items.length) return [row("workspace", "ok")];
|
|
503
|
+
return [
|
|
504
|
+
row("workspace", `${items.length} to clear`, "report only; nothing was deleted"),
|
|
505
|
+
...items.map((i) => `- ${i.kind} ${i.path}${i.detail ? ` (${i.detail})` : ""}: ${i.clear}`),
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** One overall wait for every CLI's model list. */
|
|
510
|
+
const MODEL_LIST_CAP_MS = 20_000;
|
|
511
|
+
const OFFERED_SHOWN = 6;
|
|
512
|
+
const MODEL_CLIS: ModelCli[] = ["codex", "grok", "agy", "cursor"];
|
|
513
|
+
|
|
514
|
+
const frontmatterModel = (text: string): string | undefined => {
|
|
515
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
516
|
+
return fm?.[1] === undefined ? undefined : /^model:[ \t]*["']?([^"'\r\n]+?)["']?[ \t]*$/m.exec(fm[1])?.[1];
|
|
517
|
+
};
|
|
518
|
+
const tomlModel = (text: string): string | undefined => /^model[ \t]*=[ \t]*"([^"\n]+)"/m.exec(text)?.[1];
|
|
519
|
+
/** Values of the `[subagents.models]` table, read without a TOML parser (setup.ts imports this file before smol-toml is installed). */
|
|
520
|
+
const subagentModels = (text: string): string[] => {
|
|
521
|
+
const at = text.search(/^\[subagents\.models\][ \t]*$/m);
|
|
522
|
+
if (at < 0) return [];
|
|
523
|
+
const table = text.slice(at).split(/\r?\n/).slice(1);
|
|
524
|
+
const body = table.slice(
|
|
525
|
+
0,
|
|
526
|
+
Math.max(
|
|
527
|
+
0,
|
|
528
|
+
table.findIndex((l) => l.startsWith("[")),
|
|
529
|
+
) || table.length,
|
|
530
|
+
);
|
|
531
|
+
return body.flatMap((l) => /^[\w-]+[ \t]*=[ \t]*"([^"]+)"/.exec(l)?.[1] ?? []);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Every model the checkout's generated agent files (plus the grok roles and global settings) name, per CLI.
|
|
536
|
+
* The offered set differs per CLI: codex reads a cache file, agy is a fixed set, grok and cursor run list commands.
|
|
537
|
+
* Claude agents use stable aliases and are not read.
|
|
538
|
+
*/
|
|
539
|
+
function agentModelRefs(checkout: string, sourceRoot: string): ModelRef[] {
|
|
540
|
+
const refs: ModelRef[] = [];
|
|
541
|
+
const readAt = (root: string, rel: string): string => {
|
|
542
|
+
try {
|
|
543
|
+
return fs.readFileSync(path.join(root, ...rel.split("/")), "utf8");
|
|
544
|
+
} catch {
|
|
545
|
+
return "";
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const read = (rel: string): string => readAt(checkout, rel);
|
|
549
|
+
const dirFiles = (rel: string, ext: string): string[] => {
|
|
550
|
+
try {
|
|
551
|
+
return fs
|
|
552
|
+
.readdirSync(path.join(checkout, ...rel.split("/")))
|
|
553
|
+
.filter((n) => n.endsWith(ext))
|
|
554
|
+
.sort()
|
|
555
|
+
.map((n) => `${rel}/${n}`);
|
|
556
|
+
} catch {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const add = (cli: ModelCli, files: string[], models: (text: string) => (string | undefined)[]): void => {
|
|
561
|
+
for (const file of files) for (const model of models(read(file))) if (model) refs.push({ cli, file, model });
|
|
562
|
+
};
|
|
563
|
+
add("codex", dirFiles(".codex/agents", ".toml"), (t) => [tomlModel(t)]);
|
|
564
|
+
add("grok", dirFiles(".grok/agents", ".md"), (t) => [frontmatterModel(t)]);
|
|
565
|
+
add("grok", dirFiles(".grok/roles", ".toml"), (t) => [tomlModel(t)]);
|
|
566
|
+
for (const model of subagentModels(readAt(sourceRoot, GROK_GLOBALS_REL)))
|
|
567
|
+
refs.push({ cli: "grok", file: GROK_GLOBALS_REL, model });
|
|
568
|
+
add("agy", dirFiles(".agents/agents", ".md"), (t) => [frontmatterModel(t)]);
|
|
569
|
+
// `inherit` runs the parent session's model, which the account can always use, so it names nothing to check.
|
|
570
|
+
add("cursor", dirFiles(".cursor/agents", ".md"), (t) => [frontmatterModel(t)].filter((m) => m !== "inherit"));
|
|
571
|
+
return refs;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const firstGroups = (out: string, re: RegExp): string[] => out.split(/\r?\n/).flatMap((l) => re.exec(l)?.[1] ?? []);
|
|
575
|
+
/**
|
|
576
|
+
* The only values agy 1.5.3 accepts for an agent's `model:` (its validator: "unsupported model: %q, must be one of 'inherit', 'flash', 'pro', 'flash_lite'").
|
|
577
|
+
* Any other value silently drops the agent; `agy models` lists session models, which an agent file cannot name.
|
|
578
|
+
*/
|
|
579
|
+
const AGY_AGENT_MODELS = ["inherit", "flash", "pro", "flash_lite"];
|
|
580
|
+
/** Each CLI's list command and how to read ids out of its output. */
|
|
581
|
+
const MODEL_LISTS: Record<ListCli, { bin: string; args: string[]; ids: (out: string) => string[] }> = {
|
|
582
|
+
grok: {
|
|
583
|
+
bin: "grok",
|
|
584
|
+
args: ["models"],
|
|
585
|
+
ids: (out) => firstGroups(out.slice(Math.max(0, out.indexOf("Available models:"))), /^\s+(?:\*\s+)?(\S+)/),
|
|
586
|
+
},
|
|
587
|
+
cursor: { bin: "cursor-agent", args: ["--list-models"], ids: (out) => firstGroups(out, /^(\S+) - /) },
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
/** Real list runner: resolves { code, stdout, stderr }; rejects with ENOENT when the CLI is not on PATH, or when the signal aborts it. */
|
|
591
|
+
const spawnList: ListRunner = (bin, args, { signal }) => {
|
|
592
|
+
return new Promise<ListResult>((resolve, reject) => {
|
|
593
|
+
let stdout = "";
|
|
594
|
+
let stderr = "";
|
|
595
|
+
const child = spawn(bin, args, { signal, stdio: ["ignore", "pipe", "pipe"], shell: winShim(bin) });
|
|
596
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
597
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
598
|
+
child.on("error", reject);
|
|
599
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
function codexOffered(home: string): Offered {
|
|
604
|
+
const file = path.join(home, ".codex", "models_cache.json");
|
|
605
|
+
let text: string;
|
|
606
|
+
try {
|
|
607
|
+
text = fs.readFileSync(file, "utf8");
|
|
608
|
+
} catch {
|
|
609
|
+
return { skip: `codex models cache missing at ${file}` };
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const parsed: unknown = JSON.parse(text);
|
|
613
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.models)) throw new Error("malformed");
|
|
614
|
+
const ids: string[] = [];
|
|
615
|
+
for (const m of parsed.models) {
|
|
616
|
+
if (!isRecord(m)) throw new Error("malformed");
|
|
617
|
+
if (typeof m.slug === "string" && m.slug) ids.push(m.slug);
|
|
618
|
+
}
|
|
619
|
+
if (ids.length) return { ids };
|
|
620
|
+
} catch {
|
|
621
|
+
/* malformed */
|
|
622
|
+
}
|
|
623
|
+
return { skip: `codex models cache malformed at ${file}` };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const CAPPED = Symbol("capped");
|
|
627
|
+
|
|
628
|
+
/** One CLI's offered ids, or why it is skipped. `capped` settles when the shared cap is over. */
|
|
629
|
+
async function listedModels(
|
|
630
|
+
cli: ListCli,
|
|
631
|
+
{
|
|
632
|
+
run,
|
|
633
|
+
signal,
|
|
634
|
+
capped,
|
|
635
|
+
capMs,
|
|
636
|
+
}: { run: ListRunner; signal: AbortSignal; capped: Promise<typeof CAPPED>; capMs: number },
|
|
637
|
+
): Promise<Offered> {
|
|
638
|
+
const { bin, args, ids } = MODEL_LISTS[cli];
|
|
639
|
+
try {
|
|
640
|
+
const r = await Promise.race([run(bin, args, { signal }), capped]);
|
|
641
|
+
if (r === CAPPED) return { skip: `${cli} models took over ${capMs / 1000} s` };
|
|
642
|
+
if (r.code !== 0) {
|
|
643
|
+
const first = r.stderr.split(/\r?\n/).find((l) => l.trim());
|
|
644
|
+
return { skip: `${cli} models failed: ${first ? first.trim() : `exit ${r.code}`}` };
|
|
645
|
+
}
|
|
646
|
+
const found = ids(r.stdout);
|
|
647
|
+
return found.length ? { ids: found } : { skip: `${cli} models failed: no models listed` };
|
|
648
|
+
} catch (err) {
|
|
649
|
+
if (isRecord(err) && err.code === "ENOENT") return { skip: `${cli} not installed` };
|
|
650
|
+
return { skip: `${cli} models failed: ${errorText(err).split("\n")[0]}` };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* The `agent models` row: every model an agent file names must be one its CLI still offers.
|
|
656
|
+
* codex is read from its models cache, agy is checked against a fixed set (never skipped), grok and cursor run list commands.
|
|
657
|
+
* Report only. A CLI that is missing, fails, or outlasts the one shared cap is skipped, and the row says so.
|
|
658
|
+
*/
|
|
659
|
+
export async function agentModelsLines({
|
|
660
|
+
cwd = process.cwd(),
|
|
661
|
+
env = readEnv(),
|
|
662
|
+
home = os.homedir(),
|
|
663
|
+
run = spawnList,
|
|
664
|
+
capMs = MODEL_LIST_CAP_MS,
|
|
665
|
+
sourceRoot = PACKAGE_ROOT,
|
|
666
|
+
}: {
|
|
667
|
+
cwd?: string;
|
|
668
|
+
env?: Env;
|
|
669
|
+
home?: string;
|
|
670
|
+
run?: ListRunner;
|
|
671
|
+
capMs?: number;
|
|
672
|
+
sourceRoot?: string;
|
|
673
|
+
} = {}): Promise<string[]> {
|
|
674
|
+
const refs = agentModelRefs(resolveRoots({}, { env, cwd }).checkout, sourceRoot);
|
|
675
|
+
const clis = MODEL_CLIS.filter((cli) => refs.some((r) => r.cli === cli));
|
|
676
|
+
const abort = new AbortController();
|
|
677
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
678
|
+
const capped = new Promise<typeof CAPPED>((resolve) => {
|
|
679
|
+
timer = setTimeout(() => {
|
|
680
|
+
resolve(CAPPED);
|
|
681
|
+
abort.abort();
|
|
682
|
+
}, capMs);
|
|
683
|
+
});
|
|
684
|
+
const ask = { run, signal: abort.signal, capped, capMs };
|
|
685
|
+
const lists = await Promise.all(
|
|
686
|
+
clis.map(async (cli): Promise<[ModelCli, Offered]> => [
|
|
687
|
+
cli,
|
|
688
|
+
cli === "codex" ? codexOffered(home) : cli === "agy" ? { ids: AGY_AGENT_MODELS } : await listedModels(cli, ask),
|
|
689
|
+
]),
|
|
690
|
+
);
|
|
691
|
+
clearTimeout(timer);
|
|
692
|
+
const offered = new Map(lists);
|
|
693
|
+
const skipped = lists.flatMap(([, l]) => l.skip ?? []);
|
|
694
|
+
const fixes = refs.flatMap(({ cli, file, model }) => {
|
|
695
|
+
const ids = offered.get(cli)!.ids; // every ref's cli is in clis
|
|
696
|
+
if (!ids || ids.includes(model)) return [];
|
|
697
|
+
return `- ${file}: model ${model} is not offered by ${cli}; offered: ${ids.slice(0, OFFERED_SHOWN).join(", ")}`;
|
|
698
|
+
});
|
|
699
|
+
const note = skipped.length ? `(skipped: ${skipped.join("; ")})` : "";
|
|
700
|
+
return fixes.length
|
|
701
|
+
? [row("agent models", `${fixes.length} to fix`, note), ...fixes]
|
|
702
|
+
: [row("agent models", "ok", note)];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function runDoctor({
|
|
706
|
+
cwd = process.cwd(),
|
|
707
|
+
pathEnv = readEnv().PATH,
|
|
708
|
+
platform = process.platform,
|
|
709
|
+
env = readEnv(),
|
|
710
|
+
home = os.homedir(),
|
|
711
|
+
extraLines = [],
|
|
712
|
+
globals = null,
|
|
713
|
+
skills = [],
|
|
714
|
+
}: {
|
|
715
|
+
cwd?: string;
|
|
716
|
+
pathEnv?: string | undefined;
|
|
717
|
+
platform?: string;
|
|
718
|
+
env?: Env;
|
|
719
|
+
home?: string;
|
|
720
|
+
extraLines?: string[];
|
|
721
|
+
/** The `globals` row from `globalsRow`, computed by the caller because it loads check-globals on demand. */
|
|
722
|
+
globals?: string | null;
|
|
723
|
+
/** The skill packs the repo enables (`agents.skills`); their tools get a row. */
|
|
724
|
+
skills?: readonly string[];
|
|
725
|
+
} = {}): DoctorResult {
|
|
726
|
+
const nmPath = withNmBin(cwd, pathEnv, platform);
|
|
727
|
+
const bunBin = whichBin("bun", pathEnv, platform);
|
|
728
|
+
const bunVer = bunBin ? readVersion(bunBin, "--version") : "";
|
|
729
|
+
const bunMin = bunMinimum();
|
|
730
|
+
const bunOk = Boolean(bunBin) && versionAtLeast(bunVer, bunMin);
|
|
731
|
+
const bun = bunOk ? bunBin : "";
|
|
732
|
+
const git = whichBin("git", pathEnv, platform);
|
|
733
|
+
const rtk = whichBin("rtk", nmPath, platform);
|
|
734
|
+
const lightpanda = whichBin("lightpanda", pathEnv, platform);
|
|
735
|
+
const chromium = lookupChromium(pathEnv, platform);
|
|
736
|
+
const gh = whichBin("gh", pathEnv, platform);
|
|
737
|
+
const clasp = whichBin("clasp", pathEnv, platform);
|
|
738
|
+
const ffmpeg = whichBin("ffmpeg", pathEnv, platform);
|
|
739
|
+
const whisperCli = whichBin("whisper-cli", pathEnv, platform) || whichBin("whisper-cpp", pathEnv, platform);
|
|
740
|
+
|
|
741
|
+
const found: Record<string, string> = {
|
|
742
|
+
bun,
|
|
743
|
+
git,
|
|
744
|
+
rtk,
|
|
745
|
+
lightpanda,
|
|
746
|
+
chromium,
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const lines: string[] = [];
|
|
750
|
+
if (bunBin && !bunOk) lines.push(row("bun", `need ≥${bunMin}`, `${bunVer || "unknown"} (${bunBin})`));
|
|
751
|
+
else lines.push(bunOk ? row("bun", "ok", `${bunVer} (${bunBin})`) : row("bun", "missing"));
|
|
752
|
+
lines.push(git ? row("git", "ok", git) : row("git", "missing"));
|
|
753
|
+
lines.push(rtk ? row("rtk", "ok", rtk) : row("rtk", "missing"));
|
|
754
|
+
lines.push(lightpanda ? row("lightpanda", "ok", lightpanda) : row("lightpanda", "missing"));
|
|
755
|
+
lines.push(chromium ? row("chromium", "ok", chromium) : row("chromium", "missing"));
|
|
756
|
+
const enabled = new Set<string>(skills);
|
|
757
|
+
lines.push(gh ? row("gh", "ok", gh) : row("gh", "missing (optional)"));
|
|
758
|
+
if (enabled.has("clasp")) {
|
|
759
|
+
lines.push(clasp ? row("clasp", "ok", clasp) : row("clasp", "missing (optional)"));
|
|
760
|
+
}
|
|
761
|
+
if (enabled.has("google-workspace")) {
|
|
762
|
+
const workspace = workspaceDoctorStatus({ home });
|
|
763
|
+
lines.push(
|
|
764
|
+
workspace.ok ? row("google-workspace", "ok", workspace.extra) : row("google-workspace", "missing (optional)"),
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
if (enabled.has("listen")) {
|
|
768
|
+
lines.push(ffmpeg ? row("ffmpeg", "ok", ffmpeg) : row("ffmpeg", "missing (optional)"));
|
|
769
|
+
lines.push(whisperCli ? row("whisper-cli", "ok", whisperCli) : row("whisper-cli", "missing (optional)"));
|
|
770
|
+
}
|
|
771
|
+
if (enabled.has("speak")) {
|
|
772
|
+
const speak = speakDoctorStatus({ env, home });
|
|
773
|
+
lines.push(speak.ok ? row("speak", "ok", speak.extra) : row("speak", "missing (optional)"));
|
|
774
|
+
}
|
|
775
|
+
const jev = jevDoctorStatus({ env, home });
|
|
776
|
+
lines.push(jev.ok ? row("jev", "ok", jev.extra) : row("jev", "missing (optional)"));
|
|
777
|
+
if (globals) lines.push(globals);
|
|
778
|
+
|
|
779
|
+
const missingRequired = REQUIRED.filter((k) => !found[k]);
|
|
780
|
+
if (missingRequired.length) {
|
|
781
|
+
lines.push("");
|
|
782
|
+
lines.push("install (print-only; not run):");
|
|
783
|
+
// No two tools share a hint line, so none repeats.
|
|
784
|
+
for (const k of missingRequired) lines.push(...installHints(k, platform));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const maps = mapsRow(cwd);
|
|
788
|
+
if (maps) lines.push(maps.line);
|
|
789
|
+
|
|
790
|
+
const skillConflicts = findSkillContentConflicts({
|
|
791
|
+
workspaceSkillsDir: path.join(cwd, WORKSPACE_SKILLS_REL),
|
|
792
|
+
packagedSkillsDir: path.join(cwd, PACKAGED_SKILLS_REL),
|
|
793
|
+
});
|
|
794
|
+
if (skillConflicts.length) {
|
|
795
|
+
lines.push("");
|
|
796
|
+
lines.push("skill conflicts (warn; re-init overwrites repo-owned names):");
|
|
797
|
+
for (const c of skillConflicts) {
|
|
798
|
+
lines.push(formatSkillConflictWarn(c.name));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
lines.push(...workspaceLines(workspaceIssues(resolveRoots({}, { env, cwd }))));
|
|
803
|
+
lines.push(...extraLines);
|
|
804
|
+
const stdout = `${lines.join("\n")}\n`;
|
|
805
|
+
return { code: missingRequired.length || maps?.ok === false ? 1 : 0, stdout, stderr: "" };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Load the consumer's root `scripts/lib/env.mjs` `loadEnv` when it exists (this script is `cli/scripts/doctor.ts`). */
|
|
809
|
+
export async function loadOptionalConsumerEnv(baseHref: string | URL = import.meta.url): Promise<boolean> {
|
|
810
|
+
const file = fileURLToPath(new URL("../../scripts/lib/env.mjs", baseHref));
|
|
811
|
+
if (!fs.existsSync(file)) return false;
|
|
812
|
+
const { loadEnv } = (await import(pathToFileURL(file).href)) /* a dynamic import has no static type */ as {
|
|
813
|
+
loadEnv?: unknown;
|
|
814
|
+
};
|
|
815
|
+
if (typeof loadEnv === "function") loadEnv();
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export async function main(): Promise<number> {
|
|
820
|
+
await loadOptionalConsumerEnv();
|
|
821
|
+
const r = runDoctor({
|
|
822
|
+
extraLines: await agentModelsLines(),
|
|
823
|
+
globals: await globalsRow(process.cwd(), os.homedir()),
|
|
824
|
+
skills: await enabledSkills(process.cwd()),
|
|
825
|
+
});
|
|
826
|
+
process.stdout.write(r.stdout); // runDoctor reports on stdout only
|
|
827
|
+
return r.code;
|
|
828
|
+
}
|