@jeiemgi/cckit 0.1.6

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 (191) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/AGENTS.md +101 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/LICENSE-MIT +21 -0
  5. package/README.md +143 -0
  6. package/SECURITY.md +22 -0
  7. package/bin/cckit +215 -0
  8. package/cckit.config.json +34 -0
  9. package/commands/kit-add.md +42 -0
  10. package/commands/kit-docs.md +45 -0
  11. package/commands/kit-doctor.md +52 -0
  12. package/commands/kit-export-project.md +58 -0
  13. package/commands/kit-export-training.md +49 -0
  14. package/commands/kit-init.md +126 -0
  15. package/commands/kit-routines.md +59 -0
  16. package/commands/kit-update.md +132 -0
  17. package/docs/kit-annotate/01-explainer.html +225 -0
  18. package/docs/kit-annotate/02-implementation-plan.html +196 -0
  19. package/docs/media/.onboarding-capture.cast +5 -0
  20. package/docs/media/README.md +43 -0
  21. package/docs/media/build-demo.sh +63 -0
  22. package/docs/media/build-kit-init.sh +51 -0
  23. package/docs/media/build-onboarding.sh +51 -0
  24. package/docs/media/kit-dry-run.cast +107 -0
  25. package/docs/media/kit-dry-run.gif +0 -0
  26. package/docs/media/kit-init.cast +56 -0
  27. package/docs/media/kit-init.gif +0 -0
  28. package/docs/media/kit-onboarding.cast +148 -0
  29. package/docs/media/kit-onboarding.gif +0 -0
  30. package/githooks/pre-commit +18 -0
  31. package/kit.config.schema.json +105 -0
  32. package/package.json +54 -0
  33. package/privacy-denylist.example +8 -0
  34. package/profiles/automation.json +36 -0
  35. package/profiles/content.json +41 -0
  36. package/profiles/minimal.json +31 -0
  37. package/profiles/research.json +37 -0
  38. package/profiles/software.json +32 -0
  39. package/scripts/annotate-setup.sh +149 -0
  40. package/scripts/autopilot.sh +50 -0
  41. package/scripts/capture-project-ids.sh +53 -0
  42. package/scripts/check.sh +66 -0
  43. package/scripts/contribute.sh +48 -0
  44. package/scripts/debug.sh +54 -0
  45. package/scripts/init-upgrade-test.sh +99 -0
  46. package/scripts/init.sh +827 -0
  47. package/scripts/install.sh +24 -0
  48. package/scripts/kit-add-test.sh +62 -0
  49. package/scripts/kit-add.sh +115 -0
  50. package/scripts/kit-adopt-test.sh +61 -0
  51. package/scripts/kit-adopt.sh +122 -0
  52. package/scripts/kit-bump-version.sh +79 -0
  53. package/scripts/kit-digest.sh +126 -0
  54. package/scripts/kit-doctor.sh +663 -0
  55. package/scripts/kit-export-project-test.sh +82 -0
  56. package/scripts/kit-export-project.sh +245 -0
  57. package/scripts/kit-export-training-test.sh +51 -0
  58. package/scripts/kit-export-training.sh +175 -0
  59. package/scripts/kit-migrate-test.sh +80 -0
  60. package/scripts/kit-migrate.sh +190 -0
  61. package/scripts/kit-onboard-test.sh +63 -0
  62. package/scripts/kit-onboard.sh +69 -0
  63. package/scripts/kit-promote-test.sh +54 -0
  64. package/scripts/kit-promote.sh +102 -0
  65. package/scripts/kit-remove-test.sh +61 -0
  66. package/scripts/kit-remove.sh +84 -0
  67. package/scripts/kit-routines.sh +322 -0
  68. package/scripts/kit-version-check.sh +91 -0
  69. package/scripts/kit-wire-test.sh +54 -0
  70. package/scripts/kit-wire.sh +132 -0
  71. package/scripts/knowledge-lint.sh +96 -0
  72. package/scripts/lib/cckit-output.sh +36 -0
  73. package/scripts/lib/effort-metrics.sh +452 -0
  74. package/scripts/lib/effort-ops-test.sh +83 -0
  75. package/scripts/lib/effort-ops.sh +132 -0
  76. package/scripts/lib/effort-plan.sh +104 -0
  77. package/scripts/lib/effort.sh +191 -0
  78. package/scripts/lib/engine-adapter.sh +92 -0
  79. package/scripts/lib/gh-log.sh +58 -0
  80. package/scripts/lib/gh-project.sh +212 -0
  81. package/scripts/lib/handoff.sh +35 -0
  82. package/scripts/lib/kit-cli-test.sh +42 -0
  83. package/scripts/lib/kit-cli.sh +32 -0
  84. package/scripts/lib/kit-config-resolve.sh +145 -0
  85. package/scripts/lib/kit-config.sh +88 -0
  86. package/scripts/lib/kit-engine-test.sh +107 -0
  87. package/scripts/lib/kit-events.sh +62 -0
  88. package/scripts/lib/kit-gc.sh +117 -0
  89. package/scripts/lib/kit-interview-test.sh +77 -0
  90. package/scripts/lib/kit-interview.sh +203 -0
  91. package/scripts/lib/kit-local.sh +79 -0
  92. package/scripts/lib/kit-manifest.sh +127 -0
  93. package/scripts/lib/kit-mode-test.sh +49 -0
  94. package/scripts/lib/kit-mode.sh +67 -0
  95. package/scripts/lib/kit-operate.sh +105 -0
  96. package/scripts/lib/kit-profile-test.sh +62 -0
  97. package/scripts/lib/kit-profile.sh +115 -0
  98. package/scripts/lib/kit-task-ops-test.sh +63 -0
  99. package/scripts/lib/kit-task-ops.sh +341 -0
  100. package/scripts/lib/pr-evidence.sh +173 -0
  101. package/scripts/lib/project-scan.sh +16 -0
  102. package/scripts/lib/react-detect.sh +78 -0
  103. package/scripts/lib/role-identity.sh +47 -0
  104. package/scripts/lib/secret-guard.sh +96 -0
  105. package/scripts/lib/toon.sh +35 -0
  106. package/scripts/lib/ui.sh +42 -0
  107. package/scripts/lib/version-bump.sh +59 -0
  108. package/scripts/lib/worktree-issue-test.sh +45 -0
  109. package/scripts/lib/worktree-issue.sh +73 -0
  110. package/scripts/lib/worktree-start.sh +280 -0
  111. package/scripts/orchestrate.sh +160 -0
  112. package/scripts/portable-test.sh +53 -0
  113. package/scripts/publish.sh +94 -0
  114. package/scripts/setup-labels.sh +25 -0
  115. package/scripts/setup-milestones.sh +17 -0
  116. package/scripts/showcase.sh +64 -0
  117. package/scripts/status.sh +44 -0
  118. package/scripts/task-sync.sh +59 -0
  119. package/scripts/test.sh +48 -0
  120. package/scripts/web-install.sh +22 -0
  121. package/skills/kit-annotate/SKILL.md +107 -0
  122. package/skills/kit-autopilot/SKILL.md +108 -0
  123. package/skills/kit-contribute/SKILL.md +134 -0
  124. package/skills/kit-customize/SKILL.md +134 -0
  125. package/skills/kit-dev/SKILL.md +67 -0
  126. package/skills/kit-digest/SKILL.md +41 -0
  127. package/skills/kit-effort-close/SKILL.md +156 -0
  128. package/skills/kit-effort-new/SKILL.md +173 -0
  129. package/skills/kit-effort-pr/SKILL.md +139 -0
  130. package/skills/kit-effort-start/SKILL.md +85 -0
  131. package/skills/kit-gc/SKILL.md +80 -0
  132. package/skills/kit-onboard/SKILL.md +50 -0
  133. package/skills/kit-security-sweep/SKILL.md +57 -0
  134. package/skills/kit-ship/SKILL.md +43 -0
  135. package/skills/kit-task-close/SKILL.md +66 -0
  136. package/skills/kit-task-new/SKILL.md +51 -0
  137. package/skills/kit-task-pr/SKILL.md +43 -0
  138. package/skills/kit-task-pr-auto/SKILL.md +27 -0
  139. package/skills/kit-task-pr-merge/SKILL.md +53 -0
  140. package/skills/kit-task-start/SKILL.md +76 -0
  141. package/skills/kit-task-sync/SKILL.md +37 -0
  142. package/templates/CLAUDE.md.tmpl +106 -0
  143. package/templates/agents/analyst.md +55 -0
  144. package/templates/agents/auto-dev.md +93 -0
  145. package/templates/agents/backend.md +59 -0
  146. package/templates/agents/designer.md +73 -0
  147. package/templates/agents/devops.md +57 -0
  148. package/templates/agents/editor.md +48 -0
  149. package/templates/agents/frontend.md +81 -0
  150. package/templates/agents/generalist.md +46 -0
  151. package/templates/agents/local-delegate.md +70 -0
  152. package/templates/agents/n8n.md +65 -0
  153. package/templates/agents/pm.md +69 -0
  154. package/templates/agents/qa.md +66 -0
  155. package/templates/agents/researcher.md +57 -0
  156. package/templates/agents/security.md +65 -0
  157. package/templates/agents/tech-lead.md +75 -0
  158. package/templates/hooks/guard-base-branch-commit.sh.tmpl +45 -0
  159. package/templates/hooks/kit-local-status.sh.tmpl +34 -0
  160. package/templates/hooks/kit_version_check.sh.tmpl +6 -0
  161. package/templates/hooks/mempal_followup.sh.tmpl +97 -0
  162. package/templates/hooks/mempal_precompact.sh.tmpl +4 -0
  163. package/templates/hooks/mempal_save.sh.tmpl +4 -0
  164. package/templates/hooks/mempal_session_start.sh.tmpl +8 -0
  165. package/templates/hooks/prepush_gate.sh.tmpl +36 -0
  166. package/templates/hooks/repo-hygiene.sh.tmpl +72 -0
  167. package/templates/kit.config.json.tmpl +32 -0
  168. package/templates/knowledge-INDEX.md.tmpl +12 -0
  169. package/templates/lib/kit-sigil.sh.tmpl +124 -0
  170. package/templates/rules/branch-naming.md +104 -0
  171. package/templates/rules/communication-style.md +22 -0
  172. package/templates/rules/delegation-brief.md +40 -0
  173. package/templates/rules/design-routing.md +35 -0
  174. package/templates/rules/effort-model.md +122 -0
  175. package/templates/rules/knowledge-base.md +41 -0
  176. package/templates/rules/mempalace.md +110 -0
  177. package/templates/rules/plan-output-format.md +58 -0
  178. package/templates/rules/react-annotate.md +69 -0
  179. package/templates/rules/risk-tiered-review.md +62 -0
  180. package/templates/rules/skill-gaps.md +48 -0
  181. package/templates/rules/task-management.md +42 -0
  182. package/templates/settings/settings.local.json.tmpl +27 -0
  183. package/templates/skills/NAMESPACED +13 -0
  184. package/templates/skills/copywriting/SKILL.md +252 -0
  185. package/templates/skills/copywriting/references/copy-frameworks.md +344 -0
  186. package/templates/skills/copywriting/references/natural-transitions.md +272 -0
  187. package/templates/skills/feature-build-refine/SKILL.md +367 -0
  188. package/templates/skills/karpathy-guidelines/SKILL.md +69 -0
  189. package/templates/skills/morning-briefing/SKILL.md +46 -0
  190. package/templates/skills/speckit/SKILL.md +239 -0
  191. package/templates/skills/supabase-patterns/SKILL.md +88 -0
@@ -0,0 +1,663 @@
1
+ #!/usr/bin/env bash
2
+ # kit-doctor — onboarding preflight: deps + brew bootstrap + gh auth/scopes + SSH guided
3
+ #
4
+ # Usage:
5
+ # scripts/kit-doctor.sh # detect + auto-install + report
6
+ # scripts/kit-doctor.sh --dry-run # report only — no installs, no auth changes
7
+ # scripts/kit-doctor.sh --no-install # check and auth only — skip package installs
8
+ # scripts/kit-doctor.sh --dismiss-local # silence the "local layer down" session notice
9
+ # # until the next x.y kit update
10
+ #
11
+ # Tiers:
12
+ # Tier 0 Homebrew (macOS bootstrap — requires sudo password)
13
+ # Tier 1 git, gh, jq, perl — hard deps, auto-install via brew
14
+ # Tier 2 node + pnpm, vercel, turbo — project-specific, auto-install if applicable
15
+ # Local mlx-lm via uv tool install + mlx_lm.server auto-start (when .local.enabled)
16
+ # Auth gh auth status + scope:project, git config name/email
17
+ # SSH optional guided flow (ed25519, pbcopy, link to github settings)
18
+ set -euo pipefail
19
+
20
+ # Own directory — so we can find sibling scripts (kit-export-project.sh) regardless of cwd.
21
+ _export_dir_doctor="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
22
+
23
+ # ---- flags ------------------------------------------------------------------
24
+ DRY_RUN=0; NO_INSTALL=0; DISMISS_LOCAL=0
25
+ for _a in "$@"; do
26
+ case "$_a" in
27
+ --dry-run) DRY_RUN=1 ;;
28
+ --no-install) NO_INSTALL=1 ;;
29
+ --dismiss-local) DISMISS_LOCAL=1 ;;
30
+ -h|--help)
31
+ sed -n '2,12p' "$0" | grep '^#' | sed 's/^# \?//'
32
+ exit 0 ;;
33
+ esac
34
+ done
35
+
36
+ # ---- --dismiss-local: record the dismiss and exit (no preflight run) ---------
37
+ # Writes .local.dismissed = current kitVersion to kit.config.json; the SessionStart
38
+ # notice stays silent until the kit's x.y core moves past it (issue #313).
39
+ if [[ $DISMISS_LOCAL -eq 1 ]]; then
40
+ _kitcfg="${TARGET:-$PWD}/.claude/kit.config.json"
41
+ if ! command -v jq >/dev/null 2>&1 || [[ ! -f "$_kitcfg" ]]; then
42
+ echo "kit-doctor: cannot dismiss — need jq + $_kitcfg" >&2
43
+ exit 1
44
+ fi
45
+ _kv="$(jq -r '.kitVersion // "0.0.0"' "$_kitcfg")"
46
+ _tmp="$(mktemp)"
47
+ jq --arg v "$_kv" '.local.dismissed = $v' "$_kitcfg" > "$_tmp" && mv "$_tmp" "$_kitcfg"
48
+ echo "local layer notice dismissed (kit $_kv) — reappears on the next x.y kit update"
49
+ echo "re-enable earlier: remove .local.dismissed from .claude/kit.config.json"
50
+ exit 0
51
+ fi
52
+
53
+ # ---- color/unicode detection (matches kit-sigil.sh pattern) -----------------
54
+ _loc="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
55
+ if [ "${KIT_ASCII:-}" = "1" ]; then KIT_UNICODE=0
56
+ elif printf '%s' "$_loc" | grep -qiE 'utf-?8'; then KIT_UNICODE=1
57
+ else KIT_UNICODE=0; fi
58
+
59
+ if [ "${FORCE_COLOR:-}" = "1" ]; then KIT_COLOR=1
60
+ elif [ -t 1 ] && [ -z "${NO_COLOR+x}" ]; then KIT_COLOR=1
61
+ else KIT_COLOR=0; fi
62
+
63
+ if [ "$KIT_COLOR" = 1 ]; then
64
+ C_AZAFRAN=$'\033[38;2;201;122;44m'
65
+ C_SUCCESS=$'\033[38;2;21;128;61m'
66
+ C_FAIL=$'\033[38;2;192;50;43m'
67
+ C_WARN=$'\033[38;2;180;83;9m'
68
+ C_DIM=$'\033[2m'
69
+ C_BOLD=$'\033[1m'
70
+ C_RESET=$'\033[0m'
71
+ else
72
+ C_AZAFRAN=''; C_SUCCESS=''; C_FAIL=''; C_WARN=''; C_DIM=''; C_BOLD=''; C_RESET=''
73
+ fi
74
+
75
+ if [ "$KIT_UNICODE" = 1 ]; then
76
+ MARK_OK="${C_SUCCESS}✓${C_RESET}"
77
+ MARK_FAIL="${C_FAIL}✗${C_RESET}"
78
+ MARK_WARN="${C_WARN}!${C_RESET}"
79
+ MARK_SKIP="${C_DIM}-${C_RESET}"
80
+ MARK_SEED="${C_AZAFRAN}⡶${C_RESET}"
81
+ else
82
+ MARK_OK="ok"; MARK_FAIL="FAIL"; MARK_WARN="warn"; MARK_SKIP="-"; MARK_SEED="o"
83
+ fi
84
+
85
+ # ---- report accumulators ----------------------------------------------------
86
+ CHECKS_OK=0; CHECKS_FAIL=0; CHECKS_WARN=0
87
+ ACTIONS_TAKEN=() # things the doctor installed/fixed
88
+ ACTIONS_NEEDED=() # things the user must do manually
89
+
90
+ row() { # <mark> <label> <detail>
91
+ printf ' %-3s %-28s %s\n' "$1" "$2" "${3:-}"
92
+ }
93
+
94
+ # ---- macOS detection --------------------------------------------------------
95
+ IS_MACOS=0
96
+ [[ "$(uname -s 2>/dev/null)" == "Darwin" ]] && IS_MACOS=1
97
+
98
+ # ---- helpers ----------------------------------------------------------------
99
+ has_cmd() { command -v "$1" >/dev/null 2>&1; }
100
+
101
+ # brew_install <pkg> [<cmd-to-check>] — install via brew unless dry-run/no-install
102
+ # Returns 0 if the command is now available, 1 if not.
103
+ brew_install() {
104
+ local pkg="$1" cmd="${2:-$1}"
105
+ if [[ $DRY_RUN -eq 1 || $NO_INSTALL -eq 1 ]]; then
106
+ return 1
107
+ fi
108
+ if ! has_cmd brew; then return 1; fi
109
+ brew install "$pkg" >/dev/null 2>&1 && has_cmd "$cmd"
110
+ }
111
+
112
+ # ============================================================================
113
+ # BANNER
114
+ # ============================================================================
115
+ printf '\n'
116
+ printf ' %s%s%s kit-doctor\n' "$MARK_SEED" "$C_BOLD" " / claude-kit${C_RESET}"
117
+ printf ' %s\n' "${C_DIM}onboarding preflight${C_RESET}${DRY_RUN:+${C_WARN} [dry-run]${C_RESET}}${NO_INSTALL:+${C_DIM} [no-install]${C_RESET}}"
118
+ printf '\n'
119
+
120
+ # ============================================================================
121
+ # TIER 0 — Homebrew bootstrap (macOS only)
122
+ # ============================================================================
123
+ printf ' %s%sTier 0 — bootstrap%s\n' "$C_BOLD" "" "$C_RESET"
124
+
125
+ if [[ $IS_MACOS -eq 0 ]]; then
126
+ row "$MARK_SKIP" "Homebrew" "not macOS — skipping"
127
+ elif has_cmd brew; then
128
+ row "$MARK_OK" "Homebrew" "$(brew --version 2>/dev/null | head -1)"
129
+ CHECKS_OK=$((CHECKS_OK + 1))
130
+ else
131
+ if [[ $DRY_RUN -eq 1 || $NO_INSTALL -eq 1 ]]; then
132
+ row "$MARK_FAIL" "Homebrew" "missing — install: https://brew.sh"
133
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
134
+ ACTIONS_NEEDED+=("Install Homebrew: https://brew.sh")
135
+ else
136
+ printf '\n'
137
+ printf ' %s Homebrew not found — running the official installer (will ask for sudo password)...\n' "$MARK_WARN"
138
+ printf ' %sInstaller URL: https://brew.sh%s\n\n' "$C_DIM" "$C_RESET"
139
+ if /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; then
140
+ # Add brew to PATH for the rest of this session (Apple Silicon path)
141
+ [[ -f /opt/homebrew/bin/brew ]] && eval "$(/opt/homebrew/bin/brew shellenv)" 2>/dev/null || true
142
+ [[ -f /usr/local/bin/brew ]] && eval "$(/usr/local/bin/brew shellenv)" 2>/dev/null || true
143
+ row "$MARK_OK" "Homebrew" "installed $(brew --version 2>/dev/null | head -1)"
144
+ CHECKS_OK=$((CHECKS_OK + 1))
145
+ ACTIONS_TAKEN+=("Installed Homebrew")
146
+ else
147
+ row "$MARK_FAIL" "Homebrew" "install failed — see https://brew.sh"
148
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
149
+ ACTIONS_NEEDED+=("Manually install Homebrew: https://brew.sh")
150
+ fi
151
+ fi
152
+ fi
153
+ printf '\n'
154
+
155
+ # ============================================================================
156
+ # TIER 1 — hard deps
157
+ # ============================================================================
158
+ printf ' %s%sTier 1 — hard deps%s\n' "$C_BOLD" "" "$C_RESET"
159
+
160
+ # ---- git -------------------------------------------------------------------
161
+ if has_cmd git; then
162
+ _gitver="$(git --version 2>/dev/null | awk '{print $3}')"
163
+ row "$MARK_OK" "git" "$_gitver"
164
+ CHECKS_OK=$((CHECKS_OK + 1))
165
+ elif brew_install git; then
166
+ row "$MARK_OK" "git" "installed $(git --version 2>/dev/null | awk '{print $3}')"
167
+ CHECKS_OK=$((CHECKS_OK + 1))
168
+ ACTIONS_TAKEN+=("Installed git via brew")
169
+ else
170
+ row "$MARK_FAIL" "git" "missing — brew install git"
171
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
172
+ ACTIONS_NEEDED+=("Install git: brew install git")
173
+ fi
174
+
175
+ # ---- gh (min version 2.29 for Projects v2) ----------------------------------
176
+ GH_OK=0
177
+ if has_cmd gh; then
178
+ _ghver="$(gh --version 2>/dev/null | head -1 | awk '{print $3}')"
179
+ # Check for Projects v2 support (gh >= 2.29.0)
180
+ _ghmaj="${_ghver%%.*}"
181
+ _ghmin="${_ghver#*.}"; _ghmin="${_ghmin%%.*}"
182
+ if [[ "${_ghmaj:-0}" -gt 2 ]] || \
183
+ [[ "${_ghmaj:-0}" -eq 2 && "${_ghmin:-0}" -ge 29 ]]; then
184
+ row "$MARK_OK" "gh" "$_ghver (Projects v2 ok)"
185
+ CHECKS_OK=$((CHECKS_OK + 1))
186
+ GH_OK=1
187
+ else
188
+ row "$MARK_WARN" "gh" "$_ghver — upgrade needed (>=2.29 for Projects v2)"
189
+ CHECKS_WARN=$((CHECKS_WARN + 1))
190
+ if [[ $DRY_RUN -eq 0 && $NO_INSTALL -eq 0 ]] && has_cmd brew; then
191
+ brew upgrade gh >/dev/null 2>&1 && GH_OK=1 && ACTIONS_TAKEN+=("Upgraded gh")
192
+ fi
193
+ [[ $GH_OK -eq 0 ]] && ACTIONS_NEEDED+=("Upgrade gh: brew upgrade gh")
194
+ fi
195
+ elif brew_install gh; then
196
+ _ghver="$(gh --version 2>/dev/null | head -1 | awk '{print $3}')"
197
+ row "$MARK_OK" "gh" "installed $\_ghver"
198
+ CHECKS_OK=$((CHECKS_OK + 1))
199
+ GH_OK=1
200
+ ACTIONS_TAKEN+=("Installed gh via brew")
201
+ else
202
+ row "$MARK_FAIL" "gh" "missing — brew install gh"
203
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
204
+ ACTIONS_NEEDED+=("Install gh: brew install gh")
205
+ fi
206
+
207
+ # ---- jq -------------------------------------------------------------------
208
+ if has_cmd jq; then
209
+ row "$MARK_OK" "jq" "$(jq --version 2>/dev/null)"
210
+ CHECKS_OK=$((CHECKS_OK + 1))
211
+ elif brew_install jq; then
212
+ row "$MARK_OK" "jq" "installed $(jq --version 2>/dev/null)"
213
+ CHECKS_OK=$((CHECKS_OK + 1))
214
+ ACTIONS_TAKEN+=("Installed jq via brew")
215
+ else
216
+ row "$MARK_FAIL" "jq" "missing — brew install jq"
217
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
218
+ ACTIONS_NEEDED+=("Install jq: brew install jq")
219
+ fi
220
+
221
+ # ---- perl -----------------------------------------------------------------
222
+ if has_cmd perl; then
223
+ row "$MARK_OK" "perl" "$(perl --version 2>/dev/null | head -2 | tail -1 | tr -d '()' | awk '{print $2}')"
224
+ CHECKS_OK=$((CHECKS_OK + 1))
225
+ elif brew_install perl; then
226
+ row "$MARK_OK" "perl" "installed"
227
+ CHECKS_OK=$((CHECKS_OK + 1))
228
+ ACTIONS_TAKEN+=("Installed perl via brew")
229
+ else
230
+ row "$MARK_FAIL" "perl" "missing — brew install perl"
231
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
232
+ ACTIONS_NEEDED+=("Install perl: brew install perl")
233
+ fi
234
+ printf '\n'
235
+
236
+ # ============================================================================
237
+ # TIER 2 — project deps (context-aware)
238
+ # ============================================================================
239
+ printf ' %s%sTier 2 — project deps%s\n' "$C_BOLD" "" "$C_RESET"
240
+
241
+ # ---- node + pnpm (check package.json for pinned version) -------------------
242
+ _pkg_json="${TARGET:-$PWD}/package.json"
243
+ _pkg_mgr=""
244
+ [[ -f "$_pkg_json" ]] && _pkg_mgr="$(jq -r '.packageManager // ""' "$_pkg_json" 2>/dev/null || true)"
245
+
246
+ if has_cmd node; then
247
+ row "$MARK_OK" "node" "$(node --version 2>/dev/null)"
248
+ CHECKS_OK=$((CHECKS_OK + 1))
249
+ elif brew_install node; then
250
+ row "$MARK_OK" "node" "installed $(node --version 2>/dev/null)"
251
+ CHECKS_OK=$((CHECKS_OK + 1))
252
+ ACTIONS_TAKEN+=("Installed node via brew")
253
+ else
254
+ row "$MARK_FAIL" "node" "missing — brew install node"
255
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
256
+ ACTIONS_NEEDED+=("Install node: brew install node")
257
+ fi
258
+
259
+ if has_cmd pnpm; then
260
+ row "$MARK_OK" "pnpm" "$(pnpm --version 2>/dev/null)"
261
+ CHECKS_OK=$((CHECKS_OK + 1))
262
+ elif [[ -n "$_pkg_mgr" && "$_pkg_mgr" == pnpm* ]]; then
263
+ # Pinned in package.json — enable via corepack
264
+ if has_cmd corepack || has_cmd node; then
265
+ if [[ $DRY_RUN -eq 0 && $NO_INSTALL -eq 0 ]]; then
266
+ corepack enable pnpm 2>/dev/null && \
267
+ corepack prepare "$_pkg_mgr" --activate 2>/dev/null && \
268
+ ACTIONS_TAKEN+=("Enabled pnpm via corepack ($_pkg_mgr)") || true
269
+ fi
270
+ if has_cmd pnpm; then
271
+ row "$MARK_OK" "pnpm" "$(pnpm --version 2>/dev/null) (via corepack)"
272
+ CHECKS_OK=$((CHECKS_OK + 1))
273
+ else
274
+ row "$MARK_FAIL" "pnpm" "missing — corepack enable pnpm"
275
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
276
+ ACTIONS_NEEDED+=("Enable pnpm: corepack enable pnpm && corepack prepare $_pkg_mgr --activate")
277
+ fi
278
+ else
279
+ row "$MARK_FAIL" "pnpm" "missing — npm install -g pnpm"
280
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
281
+ ACTIONS_NEEDED+=("Install pnpm: npm install -g pnpm")
282
+ fi
283
+ elif brew_install pnpm; then
284
+ row "$MARK_OK" "pnpm" "installed $(pnpm --version 2>/dev/null)"
285
+ CHECKS_OK=$((CHECKS_OK + 1))
286
+ ACTIONS_TAKEN+=("Installed pnpm via brew")
287
+ else
288
+ row "$MARK_WARN" "pnpm" "not found (needed for turbo monorepo)"
289
+ CHECKS_WARN=$((CHECKS_WARN + 1))
290
+ ACTIONS_NEEDED+=("Install pnpm: brew install pnpm")
291
+ fi
292
+
293
+ # ---- node_modules (check if pnpm install needed) ----------------------------
294
+ _nm="${TARGET:-$PWD}/node_modules"
295
+ if [[ -d "$_nm" ]]; then
296
+ row "$MARK_OK" "node_modules" "present"
297
+ CHECKS_OK=$((CHECKS_OK + 1))
298
+ elif [[ -f "$_pkg_json" ]]; then
299
+ if [[ $DRY_RUN -eq 0 && $NO_INSTALL -eq 0 ]] && has_cmd pnpm; then
300
+ printf '\n %s node_modules missing — running pnpm install...\n' "$MARK_WARN"
301
+ if pnpm install --frozen-lockfile 2>/dev/null || pnpm install; then
302
+ row "$MARK_OK" "node_modules" "installed via pnpm install"
303
+ CHECKS_OK=$((CHECKS_OK + 1))
304
+ ACTIONS_TAKEN+=("Ran pnpm install")
305
+ else
306
+ row "$MARK_FAIL" "node_modules" "pnpm install failed — run manually"
307
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
308
+ ACTIONS_NEEDED+=("Run pnpm install")
309
+ fi
310
+ else
311
+ row "$MARK_WARN" "node_modules" "missing — run pnpm install"
312
+ CHECKS_WARN=$((CHECKS_WARN + 1))
313
+ ACTIONS_NEEDED+=("Run pnpm install")
314
+ fi
315
+ else
316
+ row "$MARK_SKIP" "node_modules" "no package.json found — skipping"
317
+ fi
318
+
319
+ # ---- vercel (only if .vercel/ linked or VERCEL env) -------------------------
320
+ _has_vercel_link=0
321
+ [[ -d "${TARGET:-$PWD}/.vercel" ]] && _has_vercel_link=1
322
+
323
+ if [[ $_has_vercel_link -eq 1 ]] || [[ -n "${VERCEL:-}" ]]; then
324
+ if has_cmd vercel; then
325
+ row "$MARK_OK" "vercel" "$(vercel --version 2>/dev/null | head -1)"
326
+ CHECKS_OK=$((CHECKS_OK + 1))
327
+ elif brew_install vercel vercel; then
328
+ row "$MARK_OK" "vercel" "installed $(vercel --version 2>/dev/null | head -1)"
329
+ CHECKS_OK=$((CHECKS_OK + 1))
330
+ ACTIONS_TAKEN+=("Installed vercel CLI via brew")
331
+ elif [[ $DRY_RUN -eq 0 && $NO_INSTALL -eq 0 ]] && has_cmd npm; then
332
+ npm install -g vercel >/dev/null 2>&1 && \
333
+ ACTIONS_TAKEN+=("Installed vercel CLI via npm") || true
334
+ if has_cmd vercel; then
335
+ row "$MARK_OK" "vercel" "installed $(vercel --version 2>/dev/null | head -1)"
336
+ CHECKS_OK=$((CHECKS_OK + 1))
337
+ else
338
+ row "$MARK_FAIL" "vercel" "missing — npm install -g vercel"
339
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
340
+ ACTIONS_NEEDED+=("Install vercel: npm install -g vercel")
341
+ fi
342
+ else
343
+ row "$MARK_FAIL" "vercel" "missing — npm install -g vercel"
344
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
345
+ ACTIONS_NEEDED+=("Install vercel: npm install -g vercel")
346
+ fi
347
+ else
348
+ row "$MARK_SKIP" "vercel" "no .vercel/ project link — skipping"
349
+ fi
350
+
351
+ # turbo + playwright: dev deps only — never global; check node_modules
352
+ if [[ -d "$_nm" ]]; then
353
+ if [[ -x "$_nm/.bin/turbo" ]]; then
354
+ row "$MARK_OK" "turbo" "dev dep present"
355
+ CHECKS_OK=$((CHECKS_OK + 1))
356
+ else
357
+ row "$MARK_SKIP" "turbo" "not in node_modules (run pnpm install)"
358
+ fi
359
+ if [[ -x "$_nm/.bin/playwright" ]]; then
360
+ row "$MARK_OK" "playwright" "dev dep present"
361
+ CHECKS_OK=$((CHECKS_OK + 1))
362
+ else
363
+ row "$MARK_SKIP" "playwright" "not in node_modules (run pnpm install)"
364
+ fi
365
+ fi
366
+ printf '\n'
367
+
368
+ # ============================================================================
369
+ # LOCAL MODEL LAYER (opt-in — only checked when .local.enabled is true)
370
+ # ============================================================================
371
+ # Same gating principle as vercel: silence when the project didn't opt in.
372
+ # Install path (#313): `uv tool install mlx-lm` — isolated tool venv, PEP 668-safe
373
+ # (Homebrew python is externally-managed; plain pip/pip3 fails there). Fallback
374
+ # pipx; NEVER suggest plain pip. When mlx-lm is present but the server is down,
375
+ # the doctor starts it in background + health-checks the port. --dry-run stays
376
+ # 100% read-only: it reports what it would do, installs and starts nothing.
377
+ _kitcfg="${TARGET:-$PWD}/.claude/kit.config.json"
378
+ _local_on="false"
379
+ [[ -f "$_kitcfg" ]] && _local_on="$(jq -r '.local.enabled // false' "$_kitcfg" 2>/dev/null || echo false)"
380
+ if [[ "$_local_on" == "true" ]]; then
381
+ printf ' %s%sLocal model layer (mlx_lm.server)%s\n' "$C_BOLD" "" "$C_RESET"
382
+ _lport="$(jq -r '.local.port // 8080' "$_kitcfg" 2>/dev/null || echo 8080)"
383
+ _lmodel="$(jq -r '.local.model // "mlx-community/Qwen3-8B-4bit"' "$_kitcfg" 2>/dev/null || echo "mlx-community/Qwen3-8B-4bit")"
384
+
385
+ # Apple silicon — MLX requirement; gate install/start on it
386
+ _mlx_capable=0
387
+ if [[ $IS_MACOS -eq 1 && "$(uname -m 2>/dev/null)" == "arm64" ]]; then
388
+ row "$MARK_OK" "apple silicon" "$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo arm64)"
389
+ CHECKS_OK=$((CHECKS_OK + 1))
390
+ _mlx_capable=1
391
+ else
392
+ row "$MARK_WARN" "apple silicon" "MLX needs an Apple silicon Mac — local layer will stay dormant here"
393
+ CHECKS_WARN=$((CHECKS_WARN + 1))
394
+ fi
395
+
396
+ # mlx-lm binary — uv/pipx install to ~/.local/bin, which may not be on PATH yet
397
+ _mlx_find() {
398
+ if has_cmd mlx_lm.server; then command -v mlx_lm.server
399
+ elif [[ -x "$HOME/.local/bin/mlx_lm.server" ]]; then echo "$HOME/.local/bin/mlx_lm.server"
400
+ fi
401
+ return 0 # never trip set -e via the $(...) assignment
402
+ }
403
+ _mlx_bin="$(_mlx_find)"
404
+
405
+ if [[ -n "$_mlx_bin" ]]; then
406
+ row "$MARK_OK" "mlx-lm" "installed ($_mlx_bin)"
407
+ CHECKS_OK=$((CHECKS_OK + 1))
408
+ elif [[ $_mlx_capable -eq 0 ]]; then
409
+ row "$MARK_SKIP" "mlx-lm" "not Apple silicon — skipping install"
410
+ elif [[ $DRY_RUN -eq 1 || $NO_INSTALL -eq 1 ]]; then
411
+ row "$MARK_FAIL" "mlx-lm" "missing — uv tool install mlx-lm"
412
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
413
+ ACTIONS_NEEDED+=("Install the local model runtime: uv tool install mlx-lm")
414
+ else
415
+ # ensure an installer: uv preferred (brew-installable), pipx as fallback
416
+ if ! has_cmd uv && ! has_cmd pipx; then
417
+ brew_install uv uv && ACTIONS_TAKEN+=("Installed uv via brew") || true
418
+ fi
419
+ if has_cmd uv; then
420
+ printf ' %s mlx-lm missing — installing via uv tool install (isolated venv)...\n' "$MARK_WARN"
421
+ uv tool install mlx-lm >/dev/null 2>&1 || true
422
+ _mlx_installer="uv tool install"
423
+ elif has_cmd pipx; then
424
+ printf ' %s mlx-lm missing — installing via pipx (isolated venv)...\n' "$MARK_WARN"
425
+ pipx install mlx-lm >/dev/null 2>&1 || true
426
+ _mlx_installer="pipx"
427
+ fi
428
+ _mlx_bin="$(_mlx_find)"
429
+ if [[ -n "$_mlx_bin" ]]; then
430
+ row "$MARK_OK" "mlx-lm" "installed via ${_mlx_installer:-uv} ($_mlx_bin)"
431
+ CHECKS_OK=$((CHECKS_OK + 1))
432
+ ACTIONS_TAKEN+=("Installed mlx-lm via ${_mlx_installer:-uv}")
433
+ else
434
+ row "$MARK_FAIL" "mlx-lm" "install failed — uv tool install mlx-lm"
435
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
436
+ ACTIONS_NEEDED+=("Install the local model runtime: uv tool install mlx-lm")
437
+ fi
438
+ fi
439
+
440
+ # server alive on the configured port — auto-start when down + installed
441
+ _lurl="http://127.0.0.1:${_lport}/v1/models"
442
+ if curl -sf -m 1 "$_lurl" >/dev/null 2>&1; then
443
+ row "$MARK_OK" "server" "alive @ :${_lport} (${_lmodel##*/}) — NL chores run at \$0"
444
+ CHECKS_OK=$((CHECKS_OK + 1))
445
+ elif [[ -z "$_mlx_bin" ]]; then
446
+ row "$MARK_WARN" "server" "not running — install mlx-lm first, then re-run kit-doctor"
447
+ CHECKS_WARN=$((CHECKS_WARN + 1))
448
+ ACTIONS_NEEDED+=("Start the local model server: mlx_lm.server --model ${_lmodel} --port ${_lport}")
449
+ elif [[ $DRY_RUN -eq 1 ]]; then
450
+ row "$MARK_WARN" "server" "not running — would start mlx_lm.server in background (first run downloads ~4.5 GB)"
451
+ CHECKS_WARN=$((CHECKS_WARN + 1))
452
+ ACTIONS_NEEDED+=("Start the local model server: mlx_lm.server --model ${_lmodel} --port ${_lport}")
453
+ else
454
+ _llog="$HOME/.claude/kit-local-server.log"
455
+ mkdir -p "$HOME/.claude" 2>/dev/null || _llog="${TMPDIR:-/tmp}/kit-local-server.log"
456
+ # first-run warning: model not in the HF cache yet → server stays "loading" for a while
457
+ _hfdir="$HOME/.cache/huggingface/hub/models--${_lmodel//\//--}"
458
+ [[ -d "$_hfdir" ]] || printf ' %s first run: the model downloads now (~4.5 GB) — the server may take several minutes to come up\n' "$MARK_WARN"
459
+ printf ' %s server down — starting mlx_lm.server in background (log: %s)...\n' "$MARK_WARN" "$_llog"
460
+ nohup "$_mlx_bin" --model "$_lmodel" --port "$_lport" >>"$_llog" 2>&1 &
461
+ _mlx_pid=$!
462
+ _lup=0
463
+ for _i in $(seq 1 30); do
464
+ if curl -sf -m 1 "$_lurl" >/dev/null 2>&1; then _lup=1; break; fi
465
+ kill -0 "$_mlx_pid" 2>/dev/null || break
466
+ sleep 1
467
+ done
468
+ if [[ $_lup -eq 1 ]]; then
469
+ row "$MARK_OK" "server" "started @ :${_lport} (${_lmodel##*/}) — NL chores run at \$0"
470
+ CHECKS_OK=$((CHECKS_OK + 1))
471
+ ACTIONS_TAKEN+=("Started mlx_lm.server @ :${_lport} (pid $_mlx_pid)")
472
+ elif kill -0 "$_mlx_pid" 2>/dev/null; then
473
+ row "$MARK_WARN" "server" "starting (pid $_mlx_pid) — model still downloading/loading; check: curl :${_lport}/v1/models"
474
+ CHECKS_WARN=$((CHECKS_WARN + 1))
475
+ ACTIONS_TAKEN+=("Started mlx_lm.server (pid $_mlx_pid) — still loading, log: $_llog")
476
+ else
477
+ row "$MARK_FAIL" "server" "failed to start — see $_llog"
478
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
479
+ ACTIONS_NEEDED+=("Start the local model server manually: mlx_lm.server --model ${_lmodel} --port ${_lport} (log: $_llog)")
480
+ fi
481
+ fi
482
+ printf '\n'
483
+ fi
484
+
485
+ # ============================================================================
486
+ # AUTH + CONFIG
487
+ # ============================================================================
488
+ printf ' %s%sAuth + config%s\n' "$C_BOLD" "" "$C_RESET"
489
+
490
+ # ---- gh auth status ---------------------------------------------------------
491
+ GH_AUTHED=0
492
+ if ! has_cmd gh; then
493
+ row "$MARK_SKIP" "gh auth" "gh not found — install first"
494
+ else
495
+ if gh auth status >/dev/null 2>&1; then
496
+ _ghuser="$(gh api user --jq .login 2>/dev/null || echo "unknown")"
497
+ row "$MARK_OK" "gh auth" "logged in as @$_ghuser"
498
+ CHECKS_OK=$((CHECKS_OK + 1))
499
+ GH_AUTHED=1
500
+ else
501
+ if [[ $DRY_RUN -eq 1 ]]; then
502
+ row "$MARK_FAIL" "gh auth" "not authenticated — gh auth login"
503
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
504
+ ACTIONS_NEEDED+=("Authenticate: gh auth login")
505
+ else
506
+ printf '\n %s gh not authenticated — launching gh auth login (device flow)...\n\n' "$MARK_WARN"
507
+ if gh auth login --git-credential-helper; then
508
+ _ghuser="$(gh api user --jq .login 2>/dev/null || echo "unknown")"
509
+ row "$MARK_OK" "gh auth" "logged in as @$_ghuser"
510
+ CHECKS_OK=$((CHECKS_OK + 1))
511
+ GH_AUTHED=1
512
+ ACTIONS_TAKEN+=("Authenticated gh (device flow)")
513
+ else
514
+ row "$MARK_FAIL" "gh auth" "login failed — run: gh auth login"
515
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
516
+ ACTIONS_NEEDED+=("Authenticate: gh auth login")
517
+ fi
518
+ fi
519
+ fi
520
+ fi
521
+
522
+ # ---- gh scope: project (Projects v2 silently fails without it) --------------
523
+ GH_SCOPE_PROJECT=0
524
+ if [[ $GH_AUTHED -eq 1 ]] && has_cmd gh; then
525
+ _token_scopes="$(gh auth status 2>&1 | grep -i 'token scopes\|scopes:' | head -1 || true)"
526
+ if echo "$_token_scopes" | grep -qi 'project'; then
527
+ row "$MARK_OK" "gh scope:project" "present"
528
+ CHECKS_OK=$((CHECKS_OK + 1))
529
+ GH_SCOPE_PROJECT=1
530
+ else
531
+ if [[ $DRY_RUN -eq 1 ]]; then
532
+ row "$MARK_FAIL" "gh scope:project" "missing — gh auth refresh -s project"
533
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
534
+ ACTIONS_NEEDED+=("Add scope: gh auth refresh -s project")
535
+ else
536
+ printf '\n %s scope \"project\" missing — running gh auth refresh (will open browser)...\n\n' "$MARK_WARN"
537
+ if gh auth refresh -s project; then
538
+ row "$MARK_OK" "gh scope:project" "added"
539
+ CHECKS_OK=$((CHECKS_OK + 1))
540
+ GH_SCOPE_PROJECT=1
541
+ ACTIONS_TAKEN+=("Added gh scope:project via auth refresh")
542
+ else
543
+ row "$MARK_FAIL" "gh scope:project" "refresh failed — gh auth refresh -s project"
544
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
545
+ ACTIONS_NEEDED+=("Add scope: gh auth refresh -s project")
546
+ fi
547
+ fi
548
+ fi
549
+ elif [[ $GH_AUTHED -eq 0 ]]; then
550
+ row "$MARK_SKIP" "gh scope:project" "gh not authed — re-run after login"
551
+ fi
552
+
553
+ # ---- git config user.name + user.email -------------------------------------
554
+ _git_name="$(git config --global user.name 2>/dev/null || true)"
555
+ if [[ -n "$_git_name" ]]; then
556
+ row "$MARK_OK" "git user.name" "$_git_name"
557
+ CHECKS_OK=$((CHECKS_OK + 1))
558
+ else
559
+ row "$MARK_FAIL" "git user.name" "not set — git config --global user.name \"Your Name\""
560
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
561
+ ACTIONS_NEEDED+=("Set git identity: git config --global user.name \"Your Name\"")
562
+ fi
563
+
564
+ _git_email="$(git config --global user.email 2>/dev/null || true)"
565
+ if [[ -n "$_git_email" ]]; then
566
+ row "$MARK_OK" "git user.email" "$_git_email"
567
+ CHECKS_OK=$((CHECKS_OK + 1))
568
+ else
569
+ row "$MARK_FAIL" "git user.email" "not set — git config --global user.email \"you@example.com\""
570
+ CHECKS_FAIL=$((CHECKS_FAIL + 1))
571
+ ACTIONS_NEEDED+=("Set git email: git config --global user.email \"you@example.com\"")
572
+ fi
573
+ printf '\n'
574
+
575
+ # ============================================================================
576
+ # SSH (optional guided flow)
577
+ # ============================================================================
578
+ printf ' %s%sSSH (optional — GitHub HTTPS auth already configured by gh)%s\n' "$C_BOLD" "" "$C_RESET"
579
+
580
+ _ssh_result=0
581
+ ssh -T git@github.com -o ConnectTimeout=5 -o BatchMode=yes 2>&1 | grep -q "successfully authenticated" && _ssh_result=1 || true
582
+
583
+ if [[ $_ssh_result -eq 1 ]]; then
584
+ row "$MARK_OK" "SSH to github.com" "key already accepted"
585
+ CHECKS_OK=$((CHECKS_OK + 1))
586
+ else
587
+ row "$MARK_SKIP" "SSH to github.com" "no key — HTTPS is used by default (gh auth sets it up)"
588
+
589
+ # Show guided SSH setup only if user explicitly wants SSH
590
+ if [[ "${KIT_DOCTOR_SSH:-}" == "1" && $DRY_RUN -eq 0 ]]; then
591
+ _ssh_key="$HOME/.ssh/id_ed25519_kit"
592
+ if [[ ! -f "$_ssh_key" ]]; then
593
+ printf '\n Generating ed25519 SSH key at %s...\n' "$_ssh_key"
594
+ ssh-keygen -t ed25519 -f "$_ssh_key" -C "claude-kit onboarding" -N "" 2>/dev/null
595
+ ACTIONS_TAKEN+=("Generated SSH key $_ssh_key")
596
+ fi
597
+ if has_cmd pbcopy; then
598
+ pbcopy < "${_ssh_key}.pub"
599
+ printf ' Public key copied to clipboard.\n'
600
+ else
601
+ cat "${_ssh_key}.pub"
602
+ fi
603
+ printf '\n Paste the public key at: https://github.com/settings/ssh/new\n'
604
+ ACTIONS_NEEDED+=("Add SSH public key: https://github.com/settings/ssh/new")
605
+ else
606
+ printf ' %sTip: set KIT_DOCTOR_SSH=1 and re-run to generate an ed25519 key.%s\n' "$C_DIM" "$C_RESET"
607
+ fi
608
+ fi
609
+ printf '\n'
610
+
611
+ # ============================================================================
612
+ # SURFACE PORTABILITY (connection test — terminal / Cowork / claude.ai, #376)
613
+ # ============================================================================
614
+ # A kit project should run on all three Claude surfaces with no hand edits. Terminal + Cowork read
615
+ # CLAUDE.md + .claude/ natively; claude.ai needs the kit-export-project transform. This test asks
616
+ # the same question kit-export-project --verify does: is the PORTABLE surface self-sufficient — does
617
+ # every file CLAUDE.md leans on carry tier-A (portable) semantics, not a tier-B CLI-only shim that
618
+ # would silently no-op off-terminal? Read-only; gated on a kit project (CLAUDE.md present).
619
+ _kit_target="${TARGET:-$PWD}"
620
+ _export_script="$_export_dir_doctor/kit-export-project.sh"
621
+ if [[ -f "$_kit_target/CLAUDE.md" && -f "$_export_script" ]]; then
622
+ printf ' %s%sSurface portability (terminal / Cowork / claude.ai)%s\n' "$C_BOLD" "" "$C_RESET"
623
+ if ( cd "$_kit_target" && bash "$_export_script" --verify ) >/dev/null 2>&1; then
624
+ row "$MARK_OK" "portable surface" "tier-A self-sufficient — runs in terminal/Cowork/claude.ai unchanged"
625
+ CHECKS_OK=$((CHECKS_OK + 1))
626
+ else
627
+ row "$MARK_WARN" "portable surface" "defect(s) — run: kit-export-project.sh --verify"
628
+ CHECKS_WARN=$((CHECKS_WARN + 1))
629
+ ACTIONS_NEEDED+=("Inspect portability: scripts/kit-export-project.sh --verify (then export for claude.ai with no args)")
630
+ fi
631
+ printf '\n'
632
+ fi
633
+
634
+ # ============================================================================
635
+ # SUMMARY
636
+ # ============================================================================
637
+ printf ' %s%sSummary%s\n' "$C_BOLD" "" "$C_RESET"
638
+ printf ' %-4s %s checked ok\n' "" "$CHECKS_OK"
639
+ [[ $CHECKS_WARN -gt 0 ]] && printf ' %-4s %s warnings\n' "" "$CHECKS_WARN"
640
+ [[ $CHECKS_FAIL -gt 0 ]] && printf ' %-4s %s failed\n' "" "$CHECKS_FAIL"
641
+
642
+ if [[ ${#ACTIONS_TAKEN[@]} -gt 0 ]]; then
643
+ printf '\n %s%sInstalled / fixed:%s\n' "$C_BOLD" "" "$C_RESET"
644
+ for _a in "${ACTIONS_TAKEN[@]}"; do
645
+ printf ' %s %s\n' "$MARK_OK" "$_a"
646
+ done
647
+ fi
648
+
649
+ if [[ ${#ACTIONS_NEEDED[@]} -gt 0 ]]; then
650
+ printf '\n %s%sAction required:%s\n' "$C_BOLD" "" "$C_RESET"
651
+ for _a in "${ACTIONS_NEEDED[@]}"; do
652
+ printf ' %s %s\n' "$MARK_FAIL" "$_a"
653
+ done
654
+ fi
655
+ printf '\n'
656
+
657
+ # ---- onboarding page -------------------------------------------------------
658
+ _admin_url="${CCKIT_ADMIN_URL:-http://localhost:3001}"
659
+ printf ' %sOnboarding guide (web): %s/onboarding%s\n' "$C_DIM" "$_admin_url" "$C_RESET"
660
+ printf '\n'
661
+
662
+ # Exit non-zero when critical checks failed
663
+ [[ $CHECKS_FAIL -gt 0 ]] && exit 1 || exit 0