@joshuafolkken/kit 1.1383.0 → 1.1435.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
  5. package/.claude/skills/workflow-commands/backlogrun.md +1 -0
  6. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  7. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  8. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  9. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  10. package/README.md +25 -0
  11. package/dist/hooks/audit-provision.js +2 -2
  12. package/dist/hooks/batch-guard.js +7 -7
  13. package/dist/hooks/{chunk-LOJTBU5V.js → chunk-2G5F2YCD.js} +3 -3
  14. package/dist/hooks/{chunk-4MGHBTOM.js → chunk-3LE6VLSR.js} +25 -8
  15. package/dist/hooks/{chunk-Y5R47QHU.js → chunk-4BHUPFKF.js} +1 -1
  16. package/dist/hooks/{chunk-KC6LUZZ2.js → chunk-5SJSSCJG.js} +1 -1
  17. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  18. package/dist/hooks/{chunk-RYP3TSUJ.js → chunk-7KMN5QB5.js} +3 -3
  19. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  20. package/dist/hooks/{chunk-YKUITFT7.js → chunk-EMB55BDF.js} +4 -4
  21. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  22. package/dist/hooks/{chunk-6MAW7BCM.js → chunk-JXQBK323.js} +2677 -190
  23. package/dist/hooks/{chunk-KCF2YE7O.js → chunk-MSYSUSY6.js} +5 -5
  24. package/dist/hooks/{chunk-LC56R53A.js → chunk-MU4LJFIV.js} +4 -4
  25. package/dist/hooks/{chunk-7VSZ2N4B.js → chunk-QL6QTPWN.js} +3 -3
  26. package/dist/hooks/{chunk-TT742SIO.js → chunk-XER6SSIB.js} +92 -19
  27. package/dist/hooks/{chunk-2X2ART5G.js → chunk-YW452ZAX.js} +500 -29
  28. package/dist/hooks/{chunk-6AC4C2IO.js → chunk-YZXYOAZ3.js} +187 -443
  29. package/dist/hooks/codex-hook-adapter.js +15 -15
  30. package/dist/hooks/duplicate-read-guard.js +8 -8
  31. package/dist/hooks/format-edited.js +5 -5
  32. package/dist/hooks/inputs.json +262 -201
  33. package/dist/hooks/investigation-guard.js +8 -8
  34. package/dist/hooks/pretool-guard.js +14 -14
  35. package/dist/hooks/session-lang.js +124 -15
  36. package/dist/hooks/stop-guard.js +98 -55
  37. package/dist/josh.js +63 -10
  38. package/package.json +4 -3
  39. package/prompts/review.md +22 -0
  40. package/scripts/agent/claude-agent-argv.ts +13 -1
  41. package/scripts/auth/auth-setup-cli.ts +101 -0
  42. package/scripts/auth/claude-token-output.ts +78 -0
  43. package/scripts/auth/claude-token.ts +68 -0
  44. package/scripts/backlog/backlog-drive-cli.ts +2 -4
  45. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  46. package/scripts/backlog/backlog-drive-launch.ts +5 -0
  47. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  48. package/scripts/backlog/backlog-drive-named.ts +4 -2
  49. package/scripts/backlog/backlog-drive-owner.ts +72 -5
  50. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  51. package/scripts/backlog/backlog-drive.ts +16 -8
  52. package/scripts/backlog/backlog-overlap.ts +22 -5
  53. package/scripts/backlog/backlog-ready.ts +1 -3
  54. package/scripts/backlog/backlogrun-args.ts +13 -2
  55. package/scripts/backlog/backlogrun-cli.ts +91 -19
  56. package/scripts/backlog/backlogrun-launch.ts +80 -5
  57. package/scripts/backlog/backlogrun-replace.ts +166 -0
  58. package/scripts/backlog/backlogrun-stop.ts +178 -0
  59. package/scripts/behavior/behavior-cli.ts +6 -1
  60. package/scripts/doctor/doctor-io.ts +60 -9
  61. package/scripts/doctor/doctor.ts +20 -1
  62. package/scripts/document/document-byte-budget.ts +3 -3
  63. package/scripts/epic/epic-bundle-cli.ts +8 -8
  64. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  65. package/scripts/epic/epic-bundle.ts +39 -4
  66. package/scripts/exports/unused-members-cli.ts +2 -1
  67. package/scripts/followup/followup-checks-event.ts +82 -0
  68. package/scripts/followup/git-followup-finish.ts +16 -2
  69. package/scripts/followup/git-followup-workflow.ts +5 -2
  70. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  71. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  72. package/scripts/followup/git-pr-followup.ts +4 -1
  73. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  74. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  75. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  76. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  77. package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
  78. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  79. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  80. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  81. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  82. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  83. package/scripts/gate/gate-report.ts +70 -15
  84. package/scripts/gate/project-checks.ts +13 -2
  85. package/scripts/gate/scoped-green.ts +2 -1
  86. package/scripts/gate/verification-gate.ts +4 -3
  87. package/scripts/gh/gh-author-association.ts +14 -0
  88. package/scripts/gh/git-gh-conditional.ts +66 -0
  89. package/scripts/gh/git-gh-exec.ts +1 -1
  90. package/scripts/gh/git-gh-issue-read.ts +6 -3
  91. package/scripts/gh/git-gh-issue-write.ts +5 -1
  92. package/scripts/gh/git-gh-issue.ts +4 -1
  93. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  94. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  95. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  96. package/scripts/gh/git-pr-checks.ts +36 -2
  97. package/scripts/git/git-command.ts +16 -4
  98. package/scripts/git/git-schemas.ts +1 -0
  99. package/scripts/git/git-spawn-sync.ts +1 -1
  100. package/scripts/hooks/pretool-guard.ts +3 -1
  101. package/scripts/init/hook-command-rewrite.ts +31 -0
  102. package/scripts/init/init-actions.ts +4 -1
  103. package/scripts/init/init-ai-copy.ts +33 -10
  104. package/scripts/init/init-basic.ts +8 -3
  105. package/scripts/init/init-logic-toolchain.ts +76 -0
  106. package/scripts/init/init-logic.ts +2 -0
  107. package/scripts/init/init.ts +5 -23
  108. package/scripts/init/project-profile.ts +36 -7
  109. package/scripts/init/start-exec.ts +27 -1
  110. package/scripts/init/start-plan.ts +30 -4
  111. package/scripts/init/start-steps.ts +77 -3
  112. package/scripts/init/start.ts +21 -13
  113. package/scripts/init/workflow-mark.ts +35 -0
  114. package/scripts/issue/issue-ack-cli.ts +84 -0
  115. package/scripts/issue/issue-labels.ts +40 -8
  116. package/scripts/issue/issue-state-cli.ts +14 -4
  117. package/scripts/issue/issue-watch-diff.ts +152 -0
  118. package/scripts/issue/issue-watch-gate.ts +81 -0
  119. package/scripts/issue/issue-watch-hook.ts +54 -0
  120. package/scripts/issue/issue-watch.ts +240 -0
  121. package/scripts/issue/measure-rerun-cli.ts +3 -4
  122. package/scripts/josh/josh-command-map.ts +4 -0
  123. package/scripts/josh/josh-commands-backlog.ts +2 -2
  124. package/scripts/josh/josh-commands-development.ts +1 -1
  125. package/scripts/josh/josh-commands-issue.ts +7 -0
  126. package/scripts/josh/josh-commands-lane.ts +11 -2
  127. package/scripts/josh/josh-commands-project.ts +25 -1
  128. package/scripts/josh/josh-verdict.ts +33 -4
  129. package/scripts/lane/claude-lane-broker.ts +142 -0
  130. package/scripts/lane/lane-await.ts +6 -2
  131. package/scripts/lane/lane-cache.ts +37 -7
  132. package/scripts/lane/lane-capacity.ts +8 -6
  133. package/scripts/lane/lane-child-invocation.ts +1 -0
  134. package/scripts/lane/lane-cli.ts +15 -1
  135. package/scripts/lane/lane-close.ts +15 -6
  136. package/scripts/lane/lane-dispatch.ts +36 -10
  137. package/scripts/lane/lane-environment.ts +1 -1
  138. package/scripts/lane/lane-launch-cli.ts +13 -4
  139. package/scripts/lane/lane-ledger.ts +21 -3
  140. package/scripts/lane/lane-measure-cli.ts +10 -2
  141. package/scripts/lane/lane-open.ts +9 -1
  142. package/scripts/lane/lane-preserve.ts +49 -0
  143. package/scripts/lane/lane-reap.ts +20 -5
  144. package/scripts/lane/lane-registry.ts +6 -0
  145. package/scripts/lane/lane-restart-cli.ts +51 -0
  146. package/scripts/lane/lane-restart.ts +175 -0
  147. package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
  148. package/scripts/lane/lane-spent.ts +195 -0
  149. package/scripts/lane/lane-stage-stats.ts +27 -3
  150. package/scripts/lane/lane-stop-stats.ts +96 -0
  151. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  152. package/scripts/lane/process-tools.ts +31 -0
  153. package/scripts/lib/buffered-process.ts +36 -2
  154. package/scripts/lib/pnpm-reach.ts +27 -0
  155. package/scripts/lib/status-icons.ts +5 -1
  156. package/scripts/lint/lint-parallel.ts +19 -2
  157. package/scripts/observations/observation-ledger-home.ts +1 -0
  158. package/scripts/review/review-attest-cli.ts +21 -6
  159. package/scripts/review/review-brief-cli.ts +2 -2
  160. package/scripts/review/review-comment-round.ts +114 -0
  161. package/scripts/review/review-record-cli.ts +39 -17
  162. package/scripts/rules/implementation-cut.ts +3 -3
  163. package/scripts/rules/pre-gate-cut.ts +1 -1
  164. package/scripts/run/board/run-board-checks.ts +42 -0
  165. package/scripts/run/board/run-board-cli.ts +2 -0
  166. package/scripts/run/board/run-board-every.ts +3 -1
  167. package/scripts/run/board/run-board-fit.ts +16 -5
  168. package/scripts/run/board/run-board-fresh.ts +64 -0
  169. package/scripts/run/board/run-board-header.ts +139 -37
  170. package/scripts/run/board/run-board-labels.ts +47 -4
  171. package/scripts/run/board/run-board-lane-event.ts +66 -0
  172. package/scripts/run/board/run-board-layout.ts +57 -9
  173. package/scripts/run/board/run-board-now.ts +234 -0
  174. package/scripts/run/board/run-board-operation-read.ts +75 -0
  175. package/scripts/run/board/run-board-operation.ts +150 -0
  176. package/scripts/run/board/run-board-phase.ts +26 -1
  177. package/scripts/run/board/run-board-plan.ts +4 -6
  178. package/scripts/run/board/run-board-read.ts +58 -5
  179. package/scripts/run/board/run-board-reason.ts +124 -0
  180. package/scripts/run/board/run-board-render-checks.ts +55 -0
  181. package/scripts/run/board/run-board-render-legend.ts +12 -3
  182. package/scripts/run/board/run-board-render-slot.ts +2 -2
  183. package/scripts/run/board/run-board-render-tail.ts +128 -0
  184. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  185. package/scripts/run/board/run-board-render-title.ts +58 -0
  186. package/scripts/run/board/run-board-render.ts +96 -80
  187. package/scripts/run/board/run-board-row-yield.ts +32 -0
  188. package/scripts/run/board/run-board-setup.ts +121 -0
  189. package/scripts/run/board/run-board-span.ts +49 -7
  190. package/scripts/run/board/run-board-state.ts +10 -0
  191. package/scripts/run/board/run-board-status.ts +76 -13
  192. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  193. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  194. package/scripts/run/board/run-board-thanks.ts +53 -0
  195. package/scripts/run/board/run-board-tick.ts +53 -16
  196. package/scripts/run/board/run-board-usual.ts +44 -0
  197. package/scripts/run/carry/run-carry-added.ts +1 -1
  198. package/scripts/run/carry/run-carry-args.ts +9 -1
  199. package/scripts/run/carry/run-carry-change.ts +1 -1
  200. package/scripts/run/carry/run-carry-cli.ts +39 -52
  201. package/scripts/run/carry/run-carry-end.ts +73 -0
  202. package/scripts/run/carry/run-carry-ended.ts +9 -1
  203. package/scripts/run/carry/run-carry.ts +24 -1
  204. package/scripts/run/entry/run-entry-cli.ts +5 -0
  205. package/scripts/run/event/run-event-checks.ts +54 -0
  206. package/scripts/run/event/run-event-render.ts +3 -0
  207. package/scripts/run/event/run-event-scope.ts +1 -0
  208. package/scripts/run/event/run-event-setup.ts +91 -0
  209. package/scripts/run/event/run-event-stream.ts +46 -9
  210. package/scripts/run/merge/run-merge-cli.ts +7 -4
  211. package/scripts/run/merge/run-merge-ship.ts +61 -0
  212. package/scripts/run/merge/run-merge-steps.ts +7 -14
  213. package/scripts/run/merge/run-merge-token.ts +16 -2
  214. package/scripts/run/run-headless.ts +1 -0
  215. package/scripts/run/run-step.ts +2 -0
  216. package/scripts/run/run-stranded-detect.ts +58 -12
  217. package/scripts/run/ship/run-ship-cli.ts +22 -2
  218. package/scripts/run/ship/run-ship-return.ts +31 -13
  219. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  220. package/scripts/run/ship/run-ship-stage.ts +1 -0
  221. package/scripts/run/wake/run-wake-await.ts +130 -0
  222. package/scripts/run/wake/run-wake-cli.ts +108 -74
  223. package/scripts/run/wake/run-wake-describe.ts +36 -2
  224. package/scripts/run/wake/run-wake-driver.ts +21 -5
  225. package/scripts/run/wake/run-wake-handoff.ts +1 -1
  226. package/scripts/run/wake/run-wake-session.ts +20 -3
  227. package/scripts/run/wake/run-wake-stop.ts +37 -0
  228. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  229. package/scripts/sync/claude-settings-merge.ts +144 -0
  230. package/scripts/sync/sync-ai-files.ts +28 -3
  231. package/scripts/sync/sync.ts +15 -0
  232. package/scripts/test/project-test-script.ts +73 -0
  233. package/scripts/test/skip-marker.ts +14 -1
  234. package/scripts/test/test-related.ts +3 -2
  235. package/scripts/test/test-unit-guard.ts +78 -14
  236. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  237. package/scripts/uninstall/distributed-files.ts +92 -0
  238. package/scripts/uninstall/uninstall-plan.ts +119 -0
  239. package/scripts/uninstall/uninstall-report.ts +61 -0
  240. package/scripts/uninstall/uninstall.ts +206 -0
  241. package/templates/AGENTS.basic.md +4 -0
  242. package/templates/GEMINI.basic.md +6 -0
  243. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  244. package/scripts/run/board/run-board-track.ts +0 -22
@@ -0,0 +1,119 @@
1
+ import { json_format } from '#scripts/config-merge/json-format'
2
+ import { json_value } from '#scripts/lib/json-value'
3
+ import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
4
+
5
+ // One file kit writes into a project, with the content it writes there. `distributed` is absent when
6
+ // that content cannot be worked out here — a file whose text depends on the repository it lands in,
7
+ // read where there is no such repository — and such a file is never one this command may delete.
8
+ interface DistributedFile {
9
+ dest: string
10
+ distributed: string | undefined
11
+ }
12
+
13
+ interface FilePlan {
14
+ remove: Array<string>
15
+ keep: Array<string>
16
+ }
17
+
18
+ type ReadFile = (destination: string) => string | undefined
19
+
20
+ // Never deleted, even while it is still what kit wrote: without it the `.env` and `node_modules` it
21
+ // kept out of Git turn into untracked files, and the next `git add -A` stages them.
22
+ const IGNORE_FILE = '.gitignore'
23
+ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies']
24
+ const JOSH_FIELD = 'josh'
25
+ const PROFILE_FIELD = 'profile'
26
+ // `strip_kit` takes kit's own entry back out of this field, so it is not left for a person to review.
27
+ const HANDLED_FIELDS: ReadonlySet<string> = new Set([JOSH_FIELD])
28
+
29
+ // A file is deleted only while it is still, byte for byte, what kit writes: anything else holds the
30
+ // project's own lines — an edit, or a file kit merged its entries into — and kit kept no record of
31
+ // which lines those are.
32
+ function classify_files(files: ReadonlyArray<DistributedFile>, read_file: ReadFile): FilePlan {
33
+ const present = files
34
+ .map((file) => ({ ...file, existing: read_file(file.dest) }))
35
+ .filter((file) => file.existing !== undefined)
36
+ const untouched = new Set(
37
+ present.filter((file) => file.existing === file.distributed).map((file) => file.dest),
38
+ )
39
+ const destinations = [...new Set(present.map((file) => file.dest))]
40
+
41
+ return {
42
+ remove: destinations.filter((destination) => untouched.has(destination)),
43
+ keep: destinations.filter((destination) => !untouched.has(destination)),
44
+ }
45
+ }
46
+
47
+ function parse_manifest(manifest_text: string): Record<string, unknown> {
48
+ const parsed: unknown = JSON.parse(manifest_text)
49
+
50
+ if (!json_value.is_record(parsed)) throw new Error('package.json must contain an object')
51
+
52
+ return parsed
53
+ }
54
+
55
+ function without_key(record: Record<string, unknown>, key: string): Record<string, unknown> {
56
+ return Object.fromEntries(Object.entries(record).filter(([name]) => name !== key))
57
+ }
58
+
59
+ // The field with one key taken out of the object it holds — and the field itself once that leaves
60
+ // the object empty, so a manifest kit created is not left carrying `"devDependencies": {}`.
61
+ function without_nested_key(
62
+ manifest: Record<string, unknown>,
63
+ field: string,
64
+ key: string,
65
+ ): Record<string, unknown> {
66
+ const value = manifest[field]
67
+ if (!json_value.is_record(value) || !Object.hasOwn(value, key)) return manifest
68
+ const rest = without_key(value, key)
69
+
70
+ return Object.keys(rest).length === 0
71
+ ? without_key(manifest, field)
72
+ : { ...manifest, [field]: rest }
73
+ }
74
+
75
+ // The manifest without kit's dependency entry and its profile record — the two entries that are
76
+ // kit's alone, so removing them cannot take anything of the project's.
77
+ function strip_kit(manifest_text: string): string {
78
+ let manifest = without_nested_key(parse_manifest(manifest_text), JOSH_FIELD, PROFILE_FIELD)
79
+
80
+ for (const field of DEPENDENCY_FIELDS) {
81
+ manifest = without_nested_key(manifest, field, KIT_PACKAGE_NAME)
82
+ }
83
+
84
+ return json_format.format_package_json(manifest)
85
+ }
86
+
87
+ // Whether the manifest still holds an entry `strip_kit` takes out — asked of the parsed manifest, so
88
+ // a file indented another way is not mistaken for one that has something to remove.
89
+ function has_kit_entries(manifest_text: string): boolean {
90
+ const unchanged = json_format.format_package_json(parse_manifest(manifest_text))
91
+
92
+ return strip_kit(manifest_text) !== unchanged
93
+ }
94
+
95
+ function shared_entries(field: string, kit_value: unknown, project_value: unknown): Array<string> {
96
+ if (project_value === undefined) return []
97
+ if (!json_value.is_record(kit_value) || !json_value.is_record(project_value)) return [field]
98
+
99
+ return Object.keys(kit_value)
100
+ .filter((key) => key !== KIT_PACKAGE_NAME && Object.hasOwn(project_value, key))
101
+ .map((key) => `${field}.${key}`)
102
+ }
103
+
104
+ // The manifest entries kit merges in that the project still holds, named `scripts.preinstall` style.
105
+ // `kit_manifest_text` is what kit's own merge writes into an empty manifest, so the names come from
106
+ // the merge and not from a second list of them. They are reported, never removed: the merge kept no
107
+ // record of the value it found, so a script named `josh` may be kit's or the project's own.
108
+ function manifest_review(manifest_text: string, kit_manifest_text: string): Array<string> {
109
+ const manifest = parse_manifest(manifest_text)
110
+
111
+ return Object.entries(parse_manifest(kit_manifest_text))
112
+ .filter(([field]) => !HANDLED_FIELDS.has(field))
113
+ .flatMap(([field, kit_value]) => shared_entries(field, kit_value, manifest[field]))
114
+ }
115
+
116
+ const uninstall_plan = { classify_files, strip_kit, has_kit_entries, manifest_review }
117
+
118
+ export { uninstall_plan, IGNORE_FILE }
119
+ export type { DistributedFile, FilePlan }
@@ -0,0 +1,61 @@
1
+ import type { FilePlan } from './uninstall-plan'
2
+
3
+ interface UninstallPlan {
4
+ files: FilePlan
5
+ // Whether kit's dependency entry or profile record is still in `package.json` to take out.
6
+ has_manifest_entries: boolean
7
+ // Whether lefthook is installed here with a config kit wrote or merged into.
8
+ has_git_hooks: boolean
9
+ review: Array<string>
10
+ }
11
+
12
+ const NOTHING = ' (nothing)'
13
+ // The review comes first: the install runs whatever `scripts.prepare` is still in the manifest, and
14
+ // the one kit merged in installs lefthook's hooks again.
15
+ const NEXT_STEPS = [
16
+ 'First clear "Review by hand" — a `scripts.prepare` left there reinstalls the git hooks.',
17
+ 'Run `pnpm install` — it drops the package from node_modules and pnpm-lock.yaml.',
18
+ 'Delete node_modules and pnpm-lock.yaml yourself if kit was the only dependency.',
19
+ 'Labels, Issues and pull requests `josh start` created on GitHub are not touched.',
20
+ ]
21
+
22
+ function section(title: string, lines: ReadonlyArray<string>): string {
23
+ const body = lines.length === 0 ? [NOTHING] : lines.map((line) => ` ${line}`)
24
+
25
+ return [`${title}:`, ...body].join('\n')
26
+ }
27
+
28
+ function removal_lines(plan: UninstallPlan): Array<string> {
29
+ const lines = [...plan.files.remove]
30
+
31
+ if (plan.has_manifest_entries) {
32
+ lines.push('package.json — the @joshuafolkken/kit dependency and the josh.profile record')
33
+ }
34
+
35
+ if (plan.has_git_hooks) lines.push('git hooks installed by lefthook (lefthook uninstall)')
36
+
37
+ return lines
38
+ }
39
+
40
+ // Printed before anything is deleted, so the three lists are the whole of what the command will do
41
+ // and of what it leaves for a person: nothing outside the first one is ever written to.
42
+ function describe(plan: UninstallPlan): string {
43
+ return [
44
+ section('Remove (still exactly what kit wrote)', removal_lines(plan)),
45
+ section('Keep (differs from what kit writes — remove kit lines by hand)', plan.files.keep),
46
+ section('Review by hand (kit cannot tell these from your own)', plan.review),
47
+ ].join('\n\n')
48
+ }
49
+
50
+ function next_steps(): string {
51
+ return section('Next', NEXT_STEPS)
52
+ }
53
+
54
+ function is_empty(plan: UninstallPlan): boolean {
55
+ return removal_lines(plan).length === 0
56
+ }
57
+
58
+ const uninstall_report = { describe, next_steps, is_empty }
59
+
60
+ export { uninstall_report }
61
+ export type { UninstallPlan }
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env tsx
2
+ import { existsSync, readdirSync, rmdirSync, rmSync, writeFileSync } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { stdin as input, stdout as output } from 'node:process'
5
+ import { createInterface } from 'node:readline/promises'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { resolve_local_bin } from '#scripts/build/local-bin'
8
+ import { ask_yes_no_simple } from '#scripts/git/git-prompt'
9
+ import { LEFTHOOK_PATH } from '#scripts/init/init-actions'
10
+ import { init_logic_sonar } from '#scripts/init/init-logic-sonar'
11
+ import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
12
+ import { project_profile, type ProjectProfile } from '#scripts/init/project-profile'
13
+ import { error_text } from '#scripts/lib/error-message'
14
+ import { file_reader } from '#scripts/lib/read-file'
15
+ import { PROJECT_CONFIG_FILE } from '#scripts/safe-chain/project-config'
16
+ import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
17
+ import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
18
+ import { execaSync } from 'execa'
19
+ import { distributed_files } from './distributed-files'
20
+ import { IGNORE_FILE, uninstall_plan, type FilePlan } from './uninstall-plan'
21
+ import { uninstall_report, type UninstallPlan } from './uninstall-report'
22
+
23
+ const PACKAGE_JSON = 'package.json'
24
+ const LEFTHOOK_BIN = 'lefthook'
25
+ const ARGUMENT_START_INDEX = 2
26
+ const YES_FLAG = '--yes'
27
+ const UNINSTALL_USAGE = 'josh uninstall [--profile basic|full] [--yes]'
28
+ const QUESTION = 'Remove the files and entries listed under "Remove"? (y/n): '
29
+ const NOT_CONFIRMED = 'Nothing was removed.'
30
+ const NO_TERMINAL = `${NOT_CONFIRMED} This is a preview: there is no terminal to confirm on, so pass --yes to remove what is listed.`
31
+ // Written from the repository's own name and settings, so there is no distributed content to compare
32
+ // them with: they are named for review and never deleted.
33
+ const REVIEW_ONLY_FILES = [init_logic_sonar.get_sonar_template_destination(), PROJECT_CONFIG_FILE]
34
+ const IGNORE_FILE_REVIEW = `${IGNORE_FILE} — delete it only once nothing it ignores (.env, node_modules) is left`
35
+
36
+ interface Options {
37
+ is_yes: boolean
38
+ profile: ProjectProfile
39
+ }
40
+
41
+ function project_path(relative_path: string): string {
42
+ return path.join(PROJECT_ROOT, relative_path)
43
+ }
44
+
45
+ function read_project_file(relative_path: string): string | undefined {
46
+ return file_reader.read_if_readable(project_path(relative_path))
47
+ }
48
+
49
+ function parse_options(args: ReadonlyArray<string>): Options {
50
+ const rest = args.filter((argument) => argument !== YES_FLAG)
51
+ const requested = project_profile.requested_profile(rest, UNINSTALL_USAGE)
52
+
53
+ return {
54
+ is_yes: args.includes(YES_FLAG),
55
+ profile: project_profile.resolve_profile(PROJECT_ROOT, requested).profile,
56
+ }
57
+ }
58
+
59
+ // A lefthook config the profile distributes that still names the package — the one kit wrote, or the
60
+ // project's own with kit's preset merged in. One that names it nowhere is the project's alone.
61
+ function has_kit_lefthook_config(files: FilePlan): boolean {
62
+ if (![...files.remove, ...files.keep].includes(LEFTHOOK_PATH)) return false
63
+
64
+ return read_project_file(LEFTHOOK_PATH)?.includes(KIT_PACKAGE_NAME) === true
65
+ }
66
+
67
+ // Hooks are taken out only where the lefthook config is one kit wrote or merged into: once kit is
68
+ // gone that config extends a preset that no longer exists, and a hook left in `.git/hooks` would
69
+ // fail every commit on it.
70
+ function has_git_hooks(files: FilePlan): boolean {
71
+ return (
72
+ has_kit_lefthook_config(files) &&
73
+ existsSync(project_path('.git')) &&
74
+ existsSync(resolve_local_bin(PROJECT_ROOT, LEFTHOOK_BIN))
75
+ )
76
+ }
77
+
78
+ function review_entries(manifest: string | undefined, profile: ProjectProfile): Array<string> {
79
+ const merged =
80
+ manifest === undefined
81
+ ? []
82
+ : uninstall_plan.manifest_review(manifest, distributed_files.kit_manifest(profile))
83
+
84
+ return [
85
+ ...merged.map((entry) => `${PACKAGE_JSON} ${entry}`),
86
+ ...REVIEW_ONLY_FILES.filter((file) => existsSync(project_path(file))),
87
+ ...(existsSync(project_path(IGNORE_FILE)) ? [IGNORE_FILE_REVIEW] : []),
88
+ ]
89
+ }
90
+
91
+ function build_plan(profile: ProjectProfile): UninstallPlan {
92
+ const files = uninstall_plan.classify_files(distributed_files.list(profile), read_project_file)
93
+ const manifest = read_project_file(PACKAGE_JSON)
94
+
95
+ return {
96
+ files,
97
+ has_manifest_entries: manifest !== undefined && uninstall_plan.has_kit_entries(manifest),
98
+ has_git_hooks: has_git_hooks(files),
99
+ review: review_entries(manifest, profile),
100
+ }
101
+ }
102
+
103
+ async function is_confirmed(is_yes: boolean): Promise<boolean> {
104
+ if (is_yes) return true
105
+
106
+ if (!process.stdin.isTTY) {
107
+ console.info(`\n${NO_TERMINAL}`)
108
+
109
+ return false
110
+ }
111
+
112
+ const prompt = createInterface({ input, output })
113
+
114
+ try {
115
+ const is_accepted = await ask_yes_no_simple(prompt, `\n${QUESTION}`)
116
+ if (!is_accepted) console.info(NOT_CONFIRMED)
117
+
118
+ return is_accepted
119
+ } finally {
120
+ prompt.close()
121
+ }
122
+ }
123
+
124
+ // Run before the config is deleted, while lefthook can still read the project it is leaving.
125
+ function remove_git_hooks(): void {
126
+ const bin = resolve_local_bin(PROJECT_ROOT, LEFTHOOK_BIN)
127
+ const result = execaSync(bin, ['uninstall'], {
128
+ cwd: PROJECT_ROOT,
129
+ stdio: 'inherit',
130
+ reject: false,
131
+ })
132
+
133
+ if (result.exitCode === 0) console.info(' ✔ removed git hooks')
134
+ else console.warn(' ⚠ lefthook uninstall failed — run it manually: lefthook uninstall')
135
+ }
136
+
137
+ function is_empty_directory(directory: string): boolean {
138
+ return existsSync(directory) && readdirSync(directory).length === 0
139
+ }
140
+
141
+ // A directory kit created for its own file — `.vscode`, `.github` — goes with the last file in it,
142
+ // and one that still holds anything of the project's stays.
143
+ function remove_empty_parents(relative_path: string): void {
144
+ let directory = path.dirname(relative_path)
145
+
146
+ while (directory !== '.' && is_empty_directory(project_path(directory))) {
147
+ rmdirSync(project_path(directory))
148
+ directory = path.dirname(directory)
149
+ }
150
+ }
151
+
152
+ function remove_file(relative_path: string): void {
153
+ rmSync(project_path(relative_path))
154
+ remove_empty_parents(relative_path)
155
+ console.info(` ✔ removed ${relative_path}`)
156
+ }
157
+
158
+ function strip_manifest(): void {
159
+ const manifest = read_project_file(PACKAGE_JSON)
160
+ if (manifest === undefined) return
161
+
162
+ writeFileSync(project_path(PACKAGE_JSON), uninstall_plan.strip_kit(manifest))
163
+ console.info(` ✔ updated ${PACKAGE_JSON}`)
164
+ }
165
+
166
+ function execute(plan: UninstallPlan): void {
167
+ console.info('\nRemoving:')
168
+ if (plan.has_git_hooks) remove_git_hooks()
169
+ for (const file of plan.files.remove) remove_file(file)
170
+ if (plan.has_manifest_entries) strip_manifest()
171
+ console.info(`\n${uninstall_report.next_steps()}\n\n✅ Done.\n`)
172
+ }
173
+
174
+ // The refusal `josh init` and `josh sync` make, for the opposite damage: in kit's own repository
175
+ // every distributed file is the source, and each of them equals what kit distributes.
176
+ async function main(args: ReadonlyArray<string> = []): Promise<void> {
177
+ if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
178
+ const options = parse_options(args)
179
+ const plan = build_plan(options.profile)
180
+
181
+ console.info(`\n🧹 Uninstalling @joshuafolkken/kit (profile: ${options.profile})\n`)
182
+ console.info(uninstall_report.describe(plan))
183
+
184
+ if (uninstall_report.is_empty(plan)) {
185
+ console.info('\nNothing of kit is left to remove.\n')
186
+
187
+ return
188
+ }
189
+
190
+ if (await is_confirmed(options.is_yes)) execute(plan)
191
+ }
192
+
193
+ async function run_cli(args: ReadonlyArray<string>): Promise<void> {
194
+ try {
195
+ await main(args)
196
+ } catch (error) {
197
+ console.error(`\n✖ ${error_text.message_of(error)}\n`)
198
+ process.exitCode = 1
199
+ }
200
+ }
201
+
202
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
203
+ await run_cli(process.argv.slice(ARGUMENT_START_INDEX))
204
+ }
205
+
206
+ export { main }
@@ -0,0 +1,4 @@
1
+ # Agent Instructions
2
+
3
+ > **All rules for this project live in [`CLAUDE.md`](./CLAUDE.md). Read it in full before doing
4
+ > any work here, and follow every rule in it.** This file exists only to point at it.
@@ -0,0 +1,6 @@
1
+ # Agent Instructions
2
+
3
+ > **All rules for this project live in [`CLAUDE.md`](./CLAUDE.md). Read it in full before doing
4
+ > any work here, and follow every rule in it.** This file exists only to point at it.
5
+
6
+ @CLAUDE.md
@@ -1,13 +0,0 @@
1
- // scripts/josh/session-language.ts
2
- var ENV_KEY = "JOSH_SESSION_LANG";
3
- var DEFAULT_SESSION_LANG = "ja";
4
- function resolve_session_lang() {
5
- const value = process.env[ENV_KEY]?.trim();
6
- if (value) return { lang: value, is_default: false };
7
- return { lang: DEFAULT_SESSION_LANG, is_default: true };
8
- }
9
- var session_language = { DEFAULT_SESSION_LANG, ENV_KEY, resolve_session_lang };
10
-
11
- export {
12
- session_language
13
- };
@@ -1,22 +0,0 @@
1
- import { run_board_labels } from './run-board-labels'
2
- import type { Phase } from './run-board-phase'
3
- import type { ItemState } from './run-board-status'
4
-
5
- // A row's phases as a track: the icon of every phase it has passed, left to right, so how far a child
6
- // has got and what it is doing now are one mark. **The track is a history**: it grows as the child
7
- // goes and draws nothing ahead, since a ship that stops sends the child back and no fixed frame says
8
- // how many rounds are left.
9
-
10
- const { PHASE_ICONS, STATE_ICONS } = run_board_labels
11
-
12
- // A settled row's track ends on the row's own state icon rather than a fixed ✅, so a child that
13
- // stopped or parked never reads as having succeeded.
14
- function track_of(track: ReadonlyArray<Phase>, state: ItemState = 'running'): string {
15
- const end = state === 'running' ? '' : STATE_ICONS[state]
16
-
17
- return `${track.map((phase) => PHASE_ICONS[phase]).join('')}${end}`
18
- }
19
-
20
- const run_board_track = { track_of }
21
-
22
- export { run_board_track }