@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,190 @@
1
+ #!/usr/bin/env bash
2
+ # kit-migrate.sh — reshuffle an OLD kit layout to the v2 one, idempotently, surviving /kit-update (#373).
3
+ #
4
+ # THE PROBLEM: early kits scaffolded skills as `task-*` (task-new/task-pr/...). v2 renamed them to
5
+ # `kit-task-*` (skillPrefix='kit-'). A project that initialized on the old layout still has the old
6
+ # dirs; a naive rename leaves three latent bugs:
7
+ # 1. /kit-update RESURRECTS the old `task-*` dirs ("add missing files") — the known incident.
8
+ # 2. The ownership manifest still points at the dead old paths -> remove can't leave clean.
9
+ # 3. CLAUDE.md / rules still say `/task-pr` -> the docs reference skills that no longer exist.
10
+ # kit-migrate fixes all three in one audited pass, and is a no-op on a project already on v2.
11
+ #
12
+ # WHAT IT DOES (per old->new pair in the migration map):
13
+ # a. BACKUP the project's .claude/ once (timestamped) before the first change.
14
+ # b. MOVE the file/dir content old->new (only if old exists and new doesn't — never clobber).
15
+ # c. RE-KEY any manifest entry old->new (so update/remove track the v2 path).
16
+ # d. REGISTER the rename in kit.config.json `upgrade.renamed{}` so /kit-update NEVER re-adds the
17
+ # old path (this is the resurrection guard, #334).
18
+ # e. CLAUDE.md / rules SURGERY: rewrite `/task-<verb>` references to `/kit-task-<verb>`.
19
+ # Idempotent: a second run finds the old paths already gone + already registered -> no-op.
20
+ # Honors KIT_DRY_RUN (preview, no writes) and KIT_ASSUME_YES (no confirm).
21
+ #
22
+ # Run:
23
+ # scripts/kit-migrate.sh # interactive
24
+ # scripts/kit-migrate.sh --dry-run # preview the reshuffle, write nothing
25
+ # KIT_ASSUME_YES=1 scripts/kit-migrate.sh # non-interactive (CI / batch)
26
+ # Requires: jq.
27
+
28
+ set -uo pipefail
29
+ _mig_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
30
+ # shellcheck source=/dev/null
31
+ . "$_mig_dir/lib/kit-operate.sh" # four-beat machine + kit-manifest.sh
32
+ # shellcheck source=/dev/null
33
+ . "$_mig_dir/lib/kit-cli.sh" # kit_is_main
34
+
35
+ # The old->v2 migration map: one "OLD_RELPATH|NEW_RELPATH" per line. Data-driven so the set of
36
+ # renames is auditable + extensible without touching the engine. Today: the task-* -> kit-task-*
37
+ # skill rename (skillPrefix='kit-'), plus task-gc -> kit-gc.
38
+ _kit_migrate_map() {
39
+ cat <<'MAP'
40
+ .claude/skills/task-new|.claude/skills/kit-task-new
41
+ .claude/skills/task-start|.claude/skills/kit-task-start
42
+ .claude/skills/task-pr|.claude/skills/kit-task-pr
43
+ .claude/skills/task-pr-merge|.claude/skills/kit-task-pr-merge
44
+ .claude/skills/task-pr-auto|.claude/skills/kit-task-pr-auto
45
+ .claude/skills/task-sync|.claude/skills/kit-task-sync
46
+ .claude/skills/task-close|.claude/skills/kit-task-close
47
+ .claude/skills/task-gc|.claude/skills/kit-gc
48
+ MAP
49
+ }
50
+
51
+ _kit_migrate_cfg() { printf '%s\n' ".claude/kit.config.json"; }
52
+
53
+ # Register OLD path in kit.config.json upgrade.renamed{} (-> NEW) so /kit-update skips re-adding it.
54
+ # Idempotent: writes only if the key is absent or points elsewhere. (#334 resurrection guard.)
55
+ _kit_migrate_register_rename() {
56
+ local old="$1" new="$2" cfg cur tmp
57
+ cfg="$(_kit_migrate_cfg)"
58
+ [ -f "$cfg" ] || { _kit_op_say " (no $cfg — skip rename registry for $old)"; return 0; }
59
+ cur="$(jq -r --arg k "$old" '.upgrade.renamed[$k] // empty' "$cfg" 2>/dev/null || true)"
60
+ [ "$cur" = "$new" ] && return 0 # already registered
61
+ if _kit_op_dry; then _kit_op_say " [dry-run] would register upgrade.renamed[$old]=$new in $cfg"; return 0; fi
62
+ tmp="$(mktemp)" || return 1
63
+ jq --arg k "$old" --arg v "$new" \
64
+ '.upgrade = (.upgrade // {}) | .upgrade.renamed = (.upgrade.renamed // {}) | .upgrade.renamed[$k] = $v' \
65
+ "$cfg" > "$tmp" && mv "$tmp" "$cfg" || { rm -f "$tmp"; return 1; }
66
+ _kit_op_say " registered upgrade.renamed[$old] -> $new"
67
+ }
68
+
69
+ # Re-key a manifest entry old->new, preserving tier/op (so update/remove track the v2 path).
70
+ _kit_migrate_rekey_manifest() {
71
+ local old="$1" new="$2" mf entry tier op tmp
72
+ mf="$(kit_manifest_path)"; [ -f "$mf" ] || return 0
73
+ entry="$(jq -ec --arg p "$old" '.entries[$p] // empty' "$mf" 2>/dev/null || true)"
74
+ [ -n "$entry" ] || return 0
75
+ tier="$(printf '%s' "$entry" | jq -r '.tier // "A"')"
76
+ op="$(printf '%s' "$entry" | jq -r '.op // "wire"')"
77
+ if _kit_op_dry; then _kit_op_say " [dry-run] would re-key manifest $old -> $new"; return 0; fi
78
+ # record the new path at its (now moved) content, then drop the old entry
79
+ kit_manifest_record "$new" "$tier" "$op" >/dev/null 2>&1 || true
80
+ kit_manifest_remove_entry "$old" || true
81
+ }
82
+
83
+ _kit_migrate_backup_done=""
84
+ # Backup .claude/ once per run, before the first mutation. Skipped under dry-run.
85
+ _kit_migrate_backup() {
86
+ [ -n "$_kit_migrate_backup_done" ] && return 0
87
+ _kit_migrate_backup_done=1
88
+ _kit_op_dry && { _kit_op_say " [dry-run] would back up .claude/ before migrating"; return 0; }
89
+ [ -d .claude ] || return 0
90
+ local bk=".claude.bak.$(date -u +%Y%m%d%H%M%S 2>/dev/null || echo migrate)"
91
+ cp -R .claude "$bk" 2>/dev/null && _kit_op_say " backed up .claude/ -> $bk" || _kit_op_say " (backup skipped — cp failed, continuing)"
92
+ }
93
+
94
+ # CLAUDE.md + rules surgery: rewrite /task-<verb> -> /kit-task-<verb>, task-gc -> kit-gc.
95
+ # Operates on CLAUDE.md and .claude/rules/*.md. Idempotent (already-migrated text has no /task-).
96
+ _kit_migrate_doc_surgery() {
97
+ local f changed=0 tmp files=()
98
+ [ -f CLAUDE.md ] && files+=(CLAUDE.md)
99
+ if [ -d .claude/rules ]; then
100
+ while IFS= read -r f; do [ -n "$f" ] && files+=("$f"); done < <(find .claude/rules -maxdepth 1 -type f -name '*.md' 2>/dev/null)
101
+ fi
102
+ for f in ${files[@]+"${files[@]}"}; do
103
+ [ -f "$f" ] || continue
104
+ # match /task-<verb> not already preceded by 'kit-' ; and the bare task-gc skill name.
105
+ if grep -Eq '/(task-(new|start|pr|pr-merge|pr-auto|sync|close)|task-gc)\b' "$f" 2>/dev/null; then
106
+ if _kit_op_dry; then _kit_op_say " [dry-run] would rewrite /task-* -> /kit-task-* in $f"; changed=1; continue; fi
107
+ tmp="$(mktemp)" || return 1
108
+ sed -E 's#/task-(new|start|pr-merge|pr-auto|pr|sync|close)#/kit-task-\1#g; s#/task-gc#/kit-gc#g' "$f" > "$tmp" \
109
+ && mv "$tmp" "$f" && { _kit_op_say " rewrote skill refs in $f"; changed=1; } || rm -f "$tmp"
110
+ fi
111
+ done
112
+ [ "$changed" -eq 0 ] && _kit_op_say " (docs reference current skill names — no rewrite needed)"
113
+ return 0
114
+ }
115
+
116
+ # kit_migrate_all — run the full reshuffle. Returns 0 (a project already on v2 is success / no-op).
117
+ kit_migrate_all() {
118
+ command -v jq >/dev/null 2>&1 || { echo "kit-migrate: jq required" >&2; return 1; }
119
+
120
+ # First pass: what WOULD move? (so we can show a plan + confirm before any write)
121
+ local arr=() line old new pending=0
122
+ while IFS= read -r line; do [ -n "$line" ] && arr+=("$line"); done < <(_kit_migrate_map)
123
+ for line in ${arr[@]+"${arr[@]}"}; do
124
+ old="${line%%|*}"; new="${line##*|}"
125
+ [ -e "$old" ] && [ ! -e "$new" ] && pending=$((pending+1))
126
+ done
127
+
128
+ if [ "$pending" -eq 0 ]; then
129
+ _kit_op_say "kit-migrate: layout already on v2 (no old task-* paths to move)."
130
+ # Still ensure the renames are registered + docs are clean (idempotent backfill for partial runs).
131
+ for line in ${arr[@]+"${arr[@]}"}; do
132
+ old="${line%%|*}"; new="${line##*|}"
133
+ [ ! -e "$old" ] && _kit_migrate_register_rename "$old" "$new"
134
+ done
135
+ _kit_migrate_doc_surgery
136
+ return 0
137
+ fi
138
+
139
+ _kit_op_say "kit-migrate: $pending old-layout path(s) to reshuffle to v2:"
140
+ for line in ${arr[@]+"${arr[@]}"}; do
141
+ old="${line%%|*}"; new="${line##*|}"
142
+ [ -e "$old" ] && [ ! -e "$new" ] && _kit_op_say " ~ $old -> $new"
143
+ done
144
+
145
+ if [ -z "${KIT_ASSUME_YES:-}" ] && [ -z "${KIT_DRY_RUN:-}" ]; then
146
+ printf 'kit-migrate: reshuffle %s path(s) (a .claude/ backup is taken first)? [y/N] ' "$pending" >&2
147
+ local _c=""; IFS= read -r _c </dev/tty 2>/dev/null || IFS= read -r _c 2>/dev/null || _c=""
148
+ case "$_c" in y|Y|yes|YES) ;; *) _kit_op_say "kit-migrate: aborted (nothing moved)."; return 0;; esac
149
+ fi
150
+
151
+ _kit_migrate_backup
152
+
153
+ local moved=0
154
+ for line in ${arr[@]+"${arr[@]}"}; do
155
+ old="${line%%|*}"; new="${line##*|}"
156
+ if [ -e "$old" ] && [ ! -e "$new" ]; then
157
+ if _kit_op_dry; then
158
+ _kit_op_say " [dry-run] would move $old -> $new"
159
+ else
160
+ mkdir -p "$(dirname "$new")" && mv "$old" "$new" && { _kit_op_say " moved $old -> $new"; moved=$((moved+1)); }
161
+ # re-key the manifest for the moved file(s). For a dir, re-key each tracked child.
162
+ if [ -d "$new" ]; then
163
+ local child rel
164
+ while IFS= read -r child; do
165
+ rel="${child#./}"
166
+ _kit_migrate_rekey_manifest "${old}/${rel#"$new/"}" "$rel" 2>/dev/null || true
167
+ done < <(find "$new" -type f 2>/dev/null)
168
+ else
169
+ _kit_migrate_rekey_manifest "$old" "$new"
170
+ fi
171
+ fi
172
+ fi
173
+ # Register the rename either way (so update never resurrects $old) — idempotent.
174
+ _kit_migrate_register_rename "$old" "$new"
175
+ done
176
+
177
+ _kit_migrate_doc_surgery
178
+ _kit_op_say "kit-migrate: done ($moved moved). Old paths registered as renamed — /kit-update will not resurrect them."
179
+ }
180
+
181
+ # CLI (direct execution only)
182
+ if kit_is_main; then
183
+ while [ $# -gt 0 ]; do case "$1" in
184
+ --dry-run) export KIT_DRY_RUN=1; shift;;
185
+ --yes|-y) export KIT_ASSUME_YES=1; shift;;
186
+ -h|--help) echo "usage: kit-migrate.sh [--dry-run] [--yes] (env: KIT_ASSUME_YES, KIT_DRY_RUN)"; exit 0;;
187
+ *) echo "unknown arg: $1" >&2; exit 2;;
188
+ esac; done
189
+ kit_migrate_all
190
+ fi
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # kit-onboard-test.sh — self-test for kit-onboard persistence (#372). Runs under bash AND zsh.
3
+ # Run: bash scripts/kit-onboard-test.sh
4
+ dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
5
+
6
+ if [ -n "${KIT_OB_TEST_INNER:-}" ]; then
7
+ set -u
8
+ fail=0
9
+ t() { if [ "$2" != "$3" ]; then echo "FAIL($KIT_OB_TEST_INNER): $1 -> got '[$2]' want '[$3]'"; fail=1; else echo "ok($KIT_OB_TEST_INNER): $1"; fi; }
10
+ OB="$dir/kit-onboard.sh"
11
+
12
+ export KIT_PROFILE_HOME="$(mktemp -d)"
13
+ export KIT_ASSUME_YES=1 KIT_VERSION=9.9.9 KIT_PROFILE_USER=tester
14
+ work="$(mktemp -d)"; trap 'rm -rf "$KIT_PROFILE_HOME" "$work"' EXIT
15
+
16
+ # status: none before global interview
17
+ "$OB" status >/dev/null 2>&1; t "status before rc" "$?" "1"
18
+
19
+ # global: writes profile, preserves schema/user, applies answers
20
+ printf '%s\n' '{"name":"Ada","language":"es","mode":"enforced","plans_format":"markdown","memory":"on"}' > "$work/g.json"
21
+ "$OB" global --answers "$work/g.json" >/dev/null 2>&1; t "global rc" "$?" "0"
22
+ prof="$KIT_PROFILE_HOME/kit.profile.tester.json"
23
+ t "profile created" "$([ -f "$prof" ] && echo yes)" "yes"
24
+ t "schema kept" "$(jq -r .schema "$prof")" "1"
25
+ t "user kept" "$(jq -r .user "$prof")" "tester"
26
+ t "name applied" "$(jq -r .name "$prof")" "Ada"
27
+ t "nested default" "$(jq -r .defaults.mode "$prof")" "enforced"
28
+ t "memory applied" "$(jq -r .defaults.memory "$prof")" "on"
29
+ "$OB" status >/dev/null 2>&1; t "status after rc" "$?" "0"
30
+
31
+ # global is mergeable (a 2nd partial run keeps prior fields)
32
+ printf '%s\n' '{"language":"en"}' > "$work/g2.json"
33
+ "$OB" global --answers "$work/g2.json" >/dev/null 2>&1
34
+ t "merge keeps name" "$(jq -r .name "$prof")" "Ada"
35
+ t "merge updates lang" "$(jq -r .language "$prof")" "en"
36
+
37
+ # project: writes ./.claude/kit.config.json via operate (manifest-tracked)
38
+ mkdir -p "$work/proj"
39
+ printf '%s\n' '{"name":"My App","owner":"Ada","language":"es","mode":"guided","software":"no"}' > "$work/p.json"
40
+ "$OB" project --answers "$work/p.json" --dir "$work/proj" >/dev/null 2>&1; t "project rc" "$?" "0"
41
+ cfg="$work/proj/.claude/kit.config.json"
42
+ t "config created" "$([ -f "$cfg" ] && echo yes)" "yes"
43
+ t "project.name" "$(jq -r .project.name "$cfg")" "My App"
44
+ t "project.mode" "$(jq -r .mode "$cfg")" "guided"
45
+ t "manifest tracks" "$(jq -r '.entries["'.claude/kit.config.json'"]|has("hash")' "$work/proj/.claude/kit.manifest.json")" "true"
46
+
47
+ # project idempotent (byte-identical)
48
+ before="$(cat "$cfg")"; "$OB" project --answers "$work/p.json" --dir "$work/proj" >/dev/null 2>&1
49
+ t "project idempotent" "$(cat "$cfg")" "$before"
50
+
51
+ [ "$fail" -eq 0 ] && echo "ALL OK($KIT_OB_TEST_INNER)"
52
+ exit "$fail"
53
+ fi
54
+
55
+ rc=0; ran=0
56
+ for sh in bash zsh; do
57
+ command -v "$sh" >/dev/null 2>&1 || continue
58
+ ran=$((ran+1)); echo "--- $sh ---"
59
+ rcflag=""; [ "$sh" = "zsh" ] && rcflag="--no-rcs"
60
+ KIT_OB_TEST_INNER="$sh" PATH="$PATH" "$sh" $rcflag "$0" || rc=1
61
+ done
62
+ [ "$ran" -eq 0 ] && { echo "no shell"; exit 1; }
63
+ exit "$rc"
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # kit-onboard.sh — persist two-tier interview answers (D11, #372). The ASKING is the kit-onboard
3
+ # skill (AskUserQuestion, Tier A). This script is the deterministic persistence half:
4
+ #
5
+ # global answers -> ~/.claude/kit.profile.<user>.json (the once-in-a-lifetime profile, D7)
6
+ # project answers -> ./.claude/kit.config.json (manifest-tracked via kit-operate)
7
+ #
8
+ # Split this way the persistence is unit-testable under bash AND zsh; the skill only renders.
9
+ #
10
+ # Run:
11
+ # scripts/kit-onboard.sh global --answers FILE [--user U]
12
+ # scripts/kit-onboard.sh project --answers FILE [--dir DIR]
13
+ # scripts/kit-onboard.sh status # has the global profile been created?
14
+ # Env: KIT_ASSUME_YES / KIT_DRY_RUN (project tier, via kit-operate). Requires: jq.
15
+
16
+ set -uo pipefail
17
+ _ob_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
18
+ # shellcheck source=/dev/null
19
+ . "$_ob_dir/lib/kit-interview.sh" # catalog/apply (pulls kit-cli + kit-profile)
20
+ # shellcheck source=/dev/null
21
+ . "$_ob_dir/lib/kit-operate.sh" # four-beat machine + manifest (project tier)
22
+
23
+ # kit_onboard_global <answersfile> [user]
24
+ # Ensure the profile exists (schema + user + slug), then merge the global-tier answers onto it.
25
+ kit_onboard_global() {
26
+ command -v jq >/dev/null 2>&1 || kit_die "jq required"
27
+ local answers="$1" user="${2:-}"
28
+ [ -f "$answers" ] || kit_die "answers file missing: $answers"
29
+ kit_profile_init "$user" || kit_die "could not init profile"
30
+ local p merged tmp; p="$(kit_profile_path "$user")"
31
+ merged="$(kit_interview_apply global "$answers" "$p")" || kit_die "deriving global profile failed"
32
+ tmp="$(mktemp)"; printf '%s\n' "$merged" > "$tmp" && mv "$tmp" "$p" || { rm -f "$tmp"; kit_die "write failed"; }
33
+ kit_say "global profile updated: $p"
34
+ }
35
+
36
+ # kit_onboard_project <answersfile> [targetdir]
37
+ # Merge the project-tier answers onto ./.claude/kit.config.json through kit-operate (manifest).
38
+ kit_onboard_project() {
39
+ command -v jq >/dev/null 2>&1 || kit_die "jq required"
40
+ local answers="$1" target="${2:-$PWD}"
41
+ [ -f "$answers" ] || kit_die "answers file missing: $answers"
42
+ target="$(cd "$target" 2>/dev/null && pwd)" || kit_die "no such dir: $target"
43
+ cd "$target" || kit_die "cannot enter $target"
44
+ local cfg=".claude/kit.config.json" base merged
45
+ base=""; [ -f "$cfg" ] && base="$cfg"
46
+ merged="$(kit_interview_apply project "$answers" "$base")" || kit_die "deriving project config failed"
47
+ kit_say "kit onboard project -> $target/$cfg"
48
+ printf '%s\n' "$merged" | kit_op_write_content "$cfg" A onboard
49
+ case $? in 0|10) return 0;; *) kit_die "failed to write $cfg";; esac
50
+ }
51
+
52
+ # CLI
53
+ if kit_is_main; then
54
+ _sub="${1:-}"; shift 2>/dev/null || true
55
+ _answers=""; _user=""; _dir="$PWD"
56
+ while [ $# -gt 0 ]; do case "$1" in
57
+ --answers) _answers="$2"; shift 2;;
58
+ --user) _user="$2"; shift 2;;
59
+ --dir) _dir="$2"; shift 2;;
60
+ -h|--help) echo "usage: kit-onboard.sh global|project|status [--answers F] [--user U] [--dir D]"; exit 0;;
61
+ *) kit_die "unknown arg: $1";;
62
+ esac; done
63
+ case "$_sub" in
64
+ global) kit_onboard_global "$_answers" "$_user";;
65
+ project) kit_onboard_project "$_answers" "$_dir";;
66
+ status) if kit_profile_exists "$_user"; then echo "profile: $(kit_profile_path "$_user") (exists)"; else echo "profile: none yet — run the global interview"; exit 1; fi;;
67
+ *) kit_die "usage: kit-onboard.sh global|project|status ...";;
68
+ esac
69
+ fi
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # kit-promote-test.sh — self-test for kit-promote (#374). Runs under bash AND zsh.
3
+ # Run: bash scripts/kit-promote-test.sh
4
+ PLUGIN_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
5
+
6
+ if [ -n "${KIT_PROMOTE_TEST_INNER:-}" ]; then
7
+ set -u
8
+ fail=0
9
+ t() { if [ "$2" != "$3" ]; then echo "FAIL($KIT_PROMOTE_TEST_INNER): $1 -> got '[$2]' want '[$3]'"; fail=1; else echo "ok($KIT_PROMOTE_TEST_INNER): $1"; fi; }
10
+
11
+ ws="$(mktemp -d)"; trap 'rm -rf "$ws"' EXIT
12
+ printf '{"workspace":{"name":"w"}}\n' > "$ws/kit.workspace.json"
13
+ mkdir -p "$ws/proj-a/.claude/skills/brand" "$ws/proj-b/.claude/skills/brand" "$ws/proj-a/.claude/skills/solo"
14
+ printf 'SHARED VOICE\n' > "$ws/proj-a/.claude/skills/brand/SKILL.md"
15
+ printf 'SHARED VOICE\n' > "$ws/proj-b/.claude/skills/brand/SKILL.md" # identical -> dup candidate
16
+ printf 'only here\n' > "$ws/proj-a/.claude/skills/solo/SKILL.md"
17
+
18
+ . "$PLUGIN_DIR/scripts/kit-promote.sh"
19
+ . "$PLUGIN_DIR/scripts/lib/kit-manifest.sh"
20
+
21
+ # workspace root detection from inside a project
22
+ t "ws root from project" "$(cd "$ws/proj-a" && kit_workspace_root | sed "s#$ws#WS#")" "WS"
23
+ t "ws root from nested" "$(cd "$ws/proj-a/.claude/skills" && kit_workspace_root | sed "s#$ws#WS#")" "WS"
24
+
25
+ # promote brand/SKILL.md from proj-a up to the commons (assume-yes skips the above-project prompt)
26
+ ( cd "$ws/proj-a" && export KIT_ASSUME_YES=1 && kit_promote "skills/brand/SKILL.md" >/dev/null 2>&1 )
27
+ t "copied to commons" "$([ -f "$ws/.claude/skills/brand/SKILL.md" ] && echo yes || echo no)" "yes"
28
+ t "commons content" "$(cat "$ws/.claude/skills/brand/SKILL.md")" "SHARED VOICE"
29
+ t "commons manifest" "$(cd "$ws" && KIT_MANIFEST=.claude/kit.manifest.json kit_manifest_verify .claude/skills/brand/SKILL.md)" "intact"
30
+ t "project mirror tracked" "$(cd "$ws/proj-a" && kit_manifest_verify .claude/skills/brand/SKILL.md)" "intact"
31
+
32
+ # dup detection across the two projects: brand/SKILL.md is identical -> candidate; solo is not
33
+ cands="$(kit_promote_candidates "$ws/proj-a" "$ws/proj-b" 2>/dev/null)"
34
+ t "candidate found" "$(printf '%s' "$cands" | grep -c 'skills/brand/SKILL.md')" "1"
35
+ t "no false candidate" "$(printf '%s' "$cands" | grep -c 'skills/solo/SKILL.md')" "0"
36
+
37
+ # dry-run promote writes nothing new
38
+ mkdir -p "$ws/proj-b/.claude/skills/new"; printf 'x\n' > "$ws/proj-b/.claude/skills/new/SKILL.md"
39
+ ( cd "$ws/proj-b" && KIT_DRY_RUN=1 kit_promote "skills/new/SKILL.md" >/dev/null 2>&1 )
40
+ t "dry-run no commons write" "$([ -f "$ws/.claude/skills/new/SKILL.md" ] && echo yes || echo no)" "no"
41
+
42
+ [ "$fail" -eq 0 ] && echo "ALL OK($KIT_PROMOTE_TEST_INNER)"
43
+ exit "$fail"
44
+ fi
45
+
46
+ rc=0; ran=0
47
+ for sh in bash zsh; do
48
+ command -v "$sh" >/dev/null 2>&1 || continue
49
+ ran=$((ran+1)); echo "--- $sh ---"
50
+ rcflag=""; [ "$sh" = "zsh" ] && rcflag="--no-rcs"
51
+ KIT_PROMOTE_TEST_INNER="$sh" PATH="$PATH" PLUGIN_DIR="$PLUGIN_DIR" "$sh" $rcflag "$0" || rc=1
52
+ done
53
+ [ "$ran" -eq 0 ] && { echo "no shell"; exit 1; }
54
+ exit "$rc"
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # kit-promote.sh — promote a project-local skill/agent to the workspace COMMONS so sibling
3
+ # projects can share it (#374). Born-local, shareable by ascent.
4
+ #
5
+ # MECHANICS (decided in the v2 plan, F6):
6
+ # - The commons is `<workspace>/.claude/` (workspace root = nearest ancestor with kit.workspace.json).
7
+ # - Promotion COPIES the file up to the commons (not symlink): Cowork ignores symlinks pointing
8
+ # outside a plugin dir, and a copy works on every surface. Hash in the manifest detects drift.
9
+ # - Writing ABOVE the current project ALWAYS asks permission (D10) — the commons is shared ground.
10
+ # - The promoted copy is recorded in BOTH manifests (workspace + project) so kit-wire/remove and
11
+ # duplicate-detection can reason about it. The project copy becomes a tracked mirror: editing it
12
+ # locally is drift the kit will flag (edit at the workspace level instead).
13
+ #
14
+ # Run: scripts/kit-promote.sh <relpath-under-.claude> # e.g. skills/brand-voice/SKILL.md
15
+ # scripts/kit-promote.sh --candidates <dir1> <dir2> ... # list exact-dup files across projects
16
+ # Env: KIT_ASSUME_YES (skip the above-project permission prompt), KIT_DRY_RUN.
17
+ # Requires: jq.
18
+
19
+ set -uo pipefail
20
+ _pr_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
21
+ # shellcheck source=/dev/null
22
+ . "$_pr_dir/lib/kit-operate.sh" # manifest + operate
23
+ # shellcheck source=/dev/null
24
+ . "$_pr_dir/lib/kit-cli.sh" # kit_is_main, kit_say/die
25
+
26
+ # kit_workspace_root [startdir] -> path of nearest ancestor holding kit.workspace.json (empty if none)
27
+ kit_workspace_root() {
28
+ local d; d="$(cd "${1:-$PWD}" 2>/dev/null && pwd)" || return 1
29
+ while [ -n "$d" ]; do
30
+ [ -f "$d/kit.workspace.json" ] && { printf '%s\n' "$d"; return 0; }
31
+ [ "$d" = "/" ] && break
32
+ d="$(dirname "$d")"
33
+ done
34
+ return 1
35
+ }
36
+
37
+ # kit_promote <relpath> — relpath is under the project's .claude/ (e.g. skills/foo/SKILL.md).
38
+ # Copies <proj>/.claude/<relpath> -> <ws>/.claude/<relpath>, records both sides.
39
+ kit_promote() {
40
+ command -v jq >/dev/null 2>&1 || { kit_die "jq required"; }
41
+ local rel="$1"
42
+ [ -n "$rel" ] || { kit_die "usage: kit-promote.sh <relpath-under-.claude>"; }
43
+ local proj_file=".claude/$rel"
44
+ [ -f "$proj_file" ] || { kit_die "not found: $proj_file"; }
45
+
46
+ local ws; ws="$(kit_workspace_root)" || { kit_die "no workspace root (kit.workspace.json) found above $PWD — promotion needs a workspace."; }
47
+ local proj; proj="$(pwd)"
48
+ if [ "$ws" = "$proj" ]; then kit_die "already at the workspace root — nothing to promote up."; fi
49
+
50
+ local dest="$ws/.claude/$rel"
51
+ kit_say "promote: $proj_file"
52
+ kit_say " -> $dest (workspace commons)"
53
+
54
+ # Writing ABOVE the project always asks (D10), unless KIT_ASSUME_YES.
55
+ if [ -z "${KIT_ASSUME_YES:-}" ] && [ -z "${KIT_DRY_RUN:-}" ]; then
56
+ printf 'Write to the workspace commons (above this project)? [y/N] ' >&2
57
+ local c=""; IFS= read -r c </dev/tty 2>/dev/null || IFS= read -r c 2>/dev/null || c=""
58
+ case "$c" in y|Y|yes|YES) ;; *) kit_say "aborted."; return 0;; esac
59
+ fi
60
+ if [ -n "${KIT_DRY_RUN:-}" ]; then kit_say " [dry-run] would copy to commons + record both manifests"; return 10; fi
61
+
62
+ mkdir -p "$(dirname "$dest")"
63
+ cp -- "$proj_file" "$dest"
64
+ # record in the WORKSPACE manifest (commons owner)
65
+ ( cd "$ws" && KIT_MANIFEST=".claude/kit.manifest.json" kit_manifest_record ".claude/$rel" A promote >/dev/null 2>&1 ) || true
66
+ # record the project copy as a promoted mirror (so drift can be detected; edit upstream instead)
67
+ kit_manifest_record "$proj_file" A promote-mirror >/dev/null 2>&1 || true
68
+ kit_say " promoted. Edit it at the workspace level; the project copy is a tracked mirror."
69
+ }
70
+
71
+ # kit_promote_candidates <dir>... — find files that are byte-identical across 2+ given project
72
+ # .claude/ trees (exact-dup = a promotion candidate). Prints "<count>\t<relpath>" for dups.
73
+ kit_promote_candidates() {
74
+ command -v jq >/dev/null 2>&1 || { kit_die "jq required"; }
75
+ [ "$#" -ge 2 ] || { kit_die "usage: --candidates <dir1> <dir2> [..]"; }
76
+ local d f rel h
77
+ # emit "hash<TAB>relpath" for every skill/agent file under each dir's .claude/{skills,agents}
78
+ { for d in "$@"; do
79
+ [ -d "$d/.claude" ] || continue
80
+ while IFS= read -r f; do
81
+ [ -f "$f" ] || continue
82
+ rel="${f#"$d"/.claude/}"
83
+ h="$(kit_manifest_hash "$f")"
84
+ printf '%s\t%s\n' "$h" "$rel"
85
+ done <<EOF
86
+ $(find "$d/.claude/skills" "$d/.claude/agents" -type f 2>/dev/null)
87
+ EOF
88
+ done
89
+ } | LC_ALL=C sort | awk -F'\t' '
90
+ { if ($1==ph) { cnt++ } else { if (cnt>1) print cnt"\t"prel; ph=$1; prel=$2; cnt=1 } }
91
+ END { if (cnt>1) print cnt"\t"prel }'
92
+ }
93
+
94
+ # CLI
95
+ if kit_is_main; then
96
+ case "${1:-}" in
97
+ --candidates) shift; kit_promote_candidates "$@";;
98
+ -h|--help) echo "usage: kit-promote.sh <relpath-under-.claude> | --candidates <dir>...";;
99
+ "") kit_die "usage: kit-promote.sh <relpath-under-.claude> | --candidates <dir>...";;
100
+ *) kit_promote "$1";;
101
+ esac
102
+ fi
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # kit-remove-test.sh — self-test for kit-remove (#373). Runs under bash AND zsh.
3
+ # Run: bash scripts/kit-remove-test.sh
4
+ PLUGIN_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
5
+
6
+ if [ -n "${KIT_REMOVE_TEST_INNER:-}" ]; then
7
+ set -u
8
+ fail=0
9
+ t() { if [ "$2" != "$3" ]; then echo "FAIL($KIT_REMOVE_TEST_INNER): $1 -> got '[$2]' want '[$3]'"; fail=1; else echo "ok($KIT_REMOVE_TEST_INNER): $1"; fi; }
10
+
11
+ proj="$(mktemp -d)"; trap 'rm -rf "$proj"' EXIT
12
+ cd "$proj"
13
+ export KIT_ASSUME_YES=1
14
+ . "$PLUGIN_DIR/scripts/kit-remove.sh"
15
+ . "$PLUGIN_DIR/scripts/lib/kit-manifest.sh"
16
+
17
+ # set up a project the kit "installed": two tracked files + one user-modified + one untracked
18
+ mkdir -p .claude/hooks knowledge
19
+ printf 'a\n' > .claude/statusline.sh; kit_manifest_record .claude/statusline.sh B wire 1 2026-01-01T00:00:00Z
20
+ printf 'b\n' > .claude/hooks/guard.sh; kit_manifest_record .claude/hooks/guard.sh B wire 1 2026-01-01T00:00:00Z
21
+ printf 'c\n' > .claude/settings.json; kit_manifest_record .claude/settings.json B wire 1 2026-01-01T00:00:00Z
22
+ printf 'USER EDITED\n' > .claude/settings.json # now modified vs manifest
23
+ printf 'my notes\n' > knowledge/doc.md # NOT tracked — must survive
24
+
25
+ t "before: statusline tracked" "$(kit_manifest_verify .claude/statusline.sh)" "intact"
26
+ t "before: settings modified" "$(kit_manifest_verify .claude/settings.json)" "modified"
27
+
28
+ # remove with --yes: intact files deleted, modified kept (assume-yes still declines modified),
29
+ # untracked knowledge/ untouched.
30
+ printf 'n\n' | kit_remove_all >/dev/null 2>&1 # answer 'n' to the modified-file prompt
31
+
32
+ t "intact deleted (statusline)" "$([ -f .claude/statusline.sh ] && echo yes || echo no)" "no"
33
+ t "intact deleted (guard)" "$([ -f .claude/hooks/guard.sh ] && echo yes || echo no)" "no"
34
+ t "modified KEPT (settings)" "$([ -f .claude/settings.json ] && echo yes || echo no)" "yes"
35
+ t "modified content intact" "$(cat .claude/settings.json)" "USER EDITED"
36
+ t "untracked knowledge SAFE" "$([ -f knowledge/doc.md ] && echo yes || echo no)" "yes"
37
+ # manifest kept because one modified entry remained
38
+ t "manifest remains" "$([ -f .claude/kit.manifest.json ] && echo yes || echo no)" "yes"
39
+ t "only modified entry left" "$(kit_manifest_list | tr '\n' ',')" ".claude/settings.json,"
40
+
41
+ # dry-run never deletes
42
+ proj2="$(mktemp -d)"; cd "$proj2"
43
+ printf 'x\n' > .claude/f.sh 2>/dev/null || { mkdir -p .claude; printf 'x\n' > .claude/f.sh; }
44
+ kit_manifest_record .claude/f.sh B wire 1 2026-01-01T00:00:00Z
45
+ KIT_DRY_RUN=1 kit_remove_all >/dev/null 2>&1
46
+ t "dry-run keeps file" "$([ -f .claude/f.sh ] && echo yes || echo no)" "yes"
47
+ t "dry-run keeps manifest" "$([ -f .claude/kit.manifest.json ] && echo yes || echo no)" "yes"
48
+
49
+ [ "$fail" -eq 0 ] && echo "ALL OK($KIT_REMOVE_TEST_INNER)"
50
+ exit "$fail"
51
+ fi
52
+
53
+ rc=0; ran=0
54
+ for sh in bash zsh; do
55
+ command -v "$sh" >/dev/null 2>&1 || continue
56
+ ran=$((ran+1)); echo "--- $sh ---"
57
+ rcflag=""; [ "$sh" = "zsh" ] && rcflag="--no-rcs"
58
+ KIT_REMOVE_TEST_INNER="$sh" PATH="$PATH" PLUGIN_DIR="$PLUGIN_DIR" "$sh" $rcflag "$0" || rc=1
59
+ done
60
+ [ "$ran" -eq 0 ] && { echo "no shell"; exit 1; }
61
+ exit "$rc"
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # kit-remove.sh — uninstall the kit from a project, by MANIFEST, never by guesswork (#373).
3
+ #
4
+ # "You can leave clean" is the trust feature: removal walks the F0 ownership manifest in reverse
5
+ # and deletes only what the kit installed, applying the conffiles rule (D12):
6
+ # intact -> delete (kit wrote it, user never touched it)
7
+ # modified -> ASK (user edited it; default keep)
8
+ # missing -> just drop the entry
9
+ # untracked -> never touched (can't happen via the manifest, but kit_op_remove refuses anyway)
10
+ # It NEVER deletes knowledge/, plans/, drafts/, the user's CLAUDE.md, or anything not in the
11
+ # manifest. Honors KIT_DRY_RUN; KIT_ASSUME_YES applies non-interactively (still asks on modified).
12
+ #
13
+ # Run: scripts/kit-remove.sh # interactive
14
+ # scripts/kit-remove.sh --dry-run # preview only
15
+ # scripts/kit-remove.sh --yes # non-interactive (modified files still prompt)
16
+ # Requires: jq.
17
+
18
+ set -uo pipefail
19
+ _rm_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
20
+ # shellcheck source=/dev/null
21
+ . "$_rm_dir/lib/kit-operate.sh" # brings kit-manifest.sh too
22
+ # shellcheck source=/dev/null
23
+ . "$_rm_dir/lib/kit-cli.sh" # kit_is_main
24
+
25
+ # kit_remove_all — remove every manifest entry (reverse order so nested files go before dirs),
26
+ # then drop the manifest + prune now-empty .claude/kit dirs. Returns count kept (modified/declined).
27
+ kit_remove_all() {
28
+ command -v jq >/dev/null 2>&1 || { echo "kit-remove: jq required" >&2; return 1; }
29
+ local mf; mf="$(kit_manifest_path)"
30
+ if [ ! -f "$mf" ]; then echo "kit-remove: no manifest ($mf) — nothing kit-managed here." >&2; return 0; fi
31
+
32
+ # snapshot the tracked paths, reverse-sorted (longest/deepest first)
33
+ local paths; paths="$(kit_manifest_list | LC_ALL=C sort -r)"
34
+ if [ -z "$paths" ]; then echo "kit-remove: manifest is empty." >&2; rm -f "$mf"; return 0; fi
35
+
36
+ # One top-level confirm for the whole uninstall (skipped under --yes / --dry-run). Per-file the
37
+ # machine still protects user-modified files individually.
38
+ local n; n="$(printf '%s\n' "$paths" | grep -c .)"
39
+ if [ -z "${KIT_ASSUME_YES:-}" ] && [ -z "${KIT_DRY_RUN:-}" ]; then
40
+ printf 'kit-remove: about to remove %s kit-managed file(s). Continue? [y/N] ' "$n" >&2
41
+ local _c=""; IFS= read -r _c </dev/tty 2>/dev/null || IFS= read -r _c 2>/dev/null || _c=""
42
+ case "$_c" in y|Y|yes|YES) ;; *) echo "kit-remove: aborted." >&2; return 0;; esac
43
+ fi
44
+
45
+ # Collect into an array FIRST, then iterate — so a per-file prompt's `read` inside the loop
46
+ # can't steal the next path off the loop's stdin (classic while-read pitfall).
47
+ local arr=() line
48
+ while IFS= read -r line; do [ -n "$line" ] && arr+=("$line"); done <<EOF
49
+ $paths
50
+ EOF
51
+ local total=0 removed=0 kept=0 p rc
52
+ for p in ${arr[@]+"${arr[@]}"}; do
53
+ total=$((total+1))
54
+ kit_op_remove "$p"; rc=$?
55
+ if [ "$rc" -eq 0 ]; then removed=$((removed+1)); else kept=$((kept+1)); fi
56
+ done
57
+
58
+ _kit_op_say "kit-remove: $removed removed, $kept kept (modified/declined), of $total tracked."
59
+ # If every entry is gone, remove the (now-empty) manifest too.
60
+ if [ -z "$(kit_manifest_list)" ]; then
61
+ if _kit_op_dry; then _kit_op_say " [dry-run] would delete empty manifest $mf"
62
+ else rm -f "$mf"; _kit_op_say " deleted empty manifest $mf"; fi
63
+ else
64
+ _kit_op_say " manifest kept ($(kit_manifest_list | grep -c . ) entr(y/ies) remain — modified files you chose to keep)."
65
+ fi
66
+ # Remove any kit-managed cron entries for this project (fail-soft — missing crontab is fine).
67
+ local rt_script; rt_script="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)/kit-routines.sh"
68
+ if [ -f "$rt_script" ] && command -v crontab >/dev/null 2>&1; then
69
+ bash "$rt_script" remove-all || true
70
+ fi
71
+ return 0
72
+ }
73
+
74
+ # CLI (direct execution only; zsh-safe guard)
75
+ # CLI (direct execution only)
76
+ if kit_is_main; then
77
+ while [ $# -gt 0 ]; do case "$1" in
78
+ --dry-run) export KIT_DRY_RUN=1; shift;;
79
+ --yes|-y) export KIT_ASSUME_YES=1; shift;;
80
+ -h|--help) echo "usage: kit-remove.sh [--dry-run] [--yes]"; exit 0;;
81
+ *) echo "unknown arg: $1" >&2; exit 2;;
82
+ esac; done
83
+ kit_remove_all
84
+ fi