@joshuafolkken/kit 1.1387.0 → 1.1453.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 (278) 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-park.md +2 -0
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -2
  7. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  8. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  10. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  11. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  12. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  13. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  14. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  15. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  16. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  17. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  18. package/README.md +27 -4
  19. package/dist/hooks/audit-provision.js +2 -2
  20. package/dist/hooks/batch-guard.js +7 -7
  21. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
  23. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  24. package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
  25. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  26. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
  27. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  28. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
  29. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
  30. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
  31. package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
  32. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
  33. package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
  34. package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
  35. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
  36. package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
  37. package/dist/hooks/codex-hook-adapter.js +15 -15
  38. package/dist/hooks/duplicate-read-guard.js +8 -8
  39. package/dist/hooks/format-edited.js +5 -5
  40. package/dist/hooks/inputs.json +261 -201
  41. package/dist/hooks/investigation-guard.js +8 -8
  42. package/dist/hooks/pretool-guard.js +14 -14
  43. package/dist/hooks/session-lang.js +69 -16
  44. package/dist/hooks/stop-guard.js +13 -38
  45. package/dist/josh.js +95 -19
  46. package/package.json +4 -3
  47. package/prompts/collaboration-workflow/issue-template.md +10 -8
  48. package/prompts/review.md +30 -4
  49. package/scripts/agent/claude-agent-argv.ts +13 -1
  50. package/scripts/auth/auth-setup-cli.ts +101 -0
  51. package/scripts/auth/claude-token-output.ts +78 -0
  52. package/scripts/auth/claude-token.ts +68 -0
  53. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  54. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  55. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  56. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  57. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  58. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  59. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  60. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  61. package/scripts/backlog/backlog-drive-named.ts +4 -2
  62. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  63. package/scripts/backlog/backlog-drive.ts +38 -11
  64. package/scripts/backlog/backlog-overlap.ts +22 -5
  65. package/scripts/backlog/backlog-ready.ts +1 -3
  66. package/scripts/backlog/backlogrun-args.ts +13 -2
  67. package/scripts/backlog/backlogrun-cli.ts +91 -19
  68. package/scripts/backlog/backlogrun-launch.ts +24 -0
  69. package/scripts/backlog/backlogrun-replace.ts +166 -0
  70. package/scripts/backlog/backlogrun-stop.ts +178 -0
  71. package/scripts/behavior/behavior-cli.ts +6 -1
  72. package/scripts/doctor/doctor-io.ts +60 -9
  73. package/scripts/doctor/doctor.ts +20 -1
  74. package/scripts/document/document-byte-budget.ts +3 -3
  75. package/scripts/epic/epic-bundle-cli.ts +8 -8
  76. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  77. package/scripts/epic/epic-bundle.ts +39 -4
  78. package/scripts/exports/unused-members-cli.ts +2 -1
  79. package/scripts/followup/followup-checks-event.ts +82 -0
  80. package/scripts/followup/git-followup-finish.ts +16 -2
  81. package/scripts/followup/git-followup-workflow.ts +5 -2
  82. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  83. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  84. package/scripts/followup/git-pr-followup.ts +4 -1
  85. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  86. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  87. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  88. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  89. package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
  90. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  91. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  92. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  93. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  94. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  95. package/scripts/gate/gate-report.ts +70 -15
  96. package/scripts/gate/project-checks.ts +18 -4
  97. package/scripts/gate/scoped-green.ts +2 -1
  98. package/scripts/gate/verification-gate.ts +4 -3
  99. package/scripts/gh/gh-author-association.ts +14 -0
  100. package/scripts/gh/git-gh-conditional.ts +66 -0
  101. package/scripts/gh/git-gh-exec.ts +1 -1
  102. package/scripts/gh/git-gh-issue-read.ts +6 -3
  103. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  104. package/scripts/gh/git-gh-issue-write.ts +5 -1
  105. package/scripts/gh/git-gh-issue.ts +13 -1
  106. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  107. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  108. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  109. package/scripts/gh/git-pr-checks.ts +36 -2
  110. package/scripts/git/git-command.ts +16 -4
  111. package/scripts/git/git-schemas.ts +1 -0
  112. package/scripts/git/git-spawn-sync.ts +1 -1
  113. package/scripts/hooks/pretool-guard.ts +3 -1
  114. package/scripts/init/hook-command-rewrite.ts +31 -0
  115. package/scripts/init/init-actions.ts +7 -4
  116. package/scripts/init/init-ai-copy.ts +34 -11
  117. package/scripts/init/init-basic.ts +9 -4
  118. package/scripts/init/init-bootstrap.ts +6 -2
  119. package/scripts/init/init-dry-run-report.ts +150 -0
  120. package/scripts/init/init-dry-run.ts +189 -0
  121. package/scripts/init/init-install.ts +8 -1
  122. package/scripts/init/init-logic-toolchain.ts +76 -0
  123. package/scripts/init/init-logic.ts +2 -0
  124. package/scripts/init/init.ts +62 -42
  125. package/scripts/init/kit-setup-state.ts +11 -17
  126. package/scripts/init/own-formatter.ts +21 -0
  127. package/scripts/init/project-profile.ts +60 -12
  128. package/scripts/init/start-dry-run.ts +70 -0
  129. package/scripts/init/start-exec.ts +27 -1
  130. package/scripts/init/start-plan.ts +23 -3
  131. package/scripts/init/start-setup-pr.ts +13 -24
  132. package/scripts/init/start-steps.ts +62 -1
  133. package/scripts/init/start.ts +38 -17
  134. package/scripts/init/workflow-mark.ts +35 -0
  135. package/scripts/init/working-tree.ts +116 -0
  136. package/scripts/issue/issue-ack-cli.ts +84 -0
  137. package/scripts/issue/issue-auto-ok.ts +68 -99
  138. package/scripts/issue/issue-file-cli.ts +17 -17
  139. package/scripts/issue/issue-file.ts +1 -6
  140. package/scripts/issue/issue-labels.ts +46 -10
  141. package/scripts/issue/issue-release-cli.ts +69 -23
  142. package/scripts/issue/issue-release.ts +3 -1
  143. package/scripts/issue/issue-state-cli.ts +14 -4
  144. package/scripts/issue/issue-watch-diff.ts +152 -0
  145. package/scripts/issue/issue-watch-gate.ts +81 -0
  146. package/scripts/issue/issue-watch-hook.ts +54 -0
  147. package/scripts/issue/issue-watch.ts +240 -0
  148. package/scripts/issue/measure-rerun-cli.ts +3 -4
  149. package/scripts/josh/josh-command-map.ts +4 -0
  150. package/scripts/josh/josh-commands-ai.ts +11 -0
  151. package/scripts/josh/josh-commands-backlog.ts +2 -2
  152. package/scripts/josh/josh-commands-development.ts +1 -1
  153. package/scripts/josh/josh-commands-issue.ts +10 -3
  154. package/scripts/josh/josh-commands-lane.ts +11 -2
  155. package/scripts/josh/josh-commands-project.ts +26 -2
  156. package/scripts/josh/josh-verdict.ts +33 -4
  157. package/scripts/josh/process-identity.ts +32 -6
  158. package/scripts/lane/claude-lane-broker.ts +142 -0
  159. package/scripts/lane/lane-await.ts +6 -2
  160. package/scripts/lane/lane-broker-server.ts +69 -0
  161. package/scripts/lane/lane-cache.ts +37 -7
  162. package/scripts/lane/lane-capacity.ts +8 -6
  163. package/scripts/lane/lane-child-invocation.ts +1 -0
  164. package/scripts/lane/lane-cli.ts +15 -1
  165. package/scripts/lane/lane-close.ts +15 -6
  166. package/scripts/lane/lane-dispatch.ts +36 -10
  167. package/scripts/lane/lane-environment.ts +1 -1
  168. package/scripts/lane/lane-launch-cli.ts +13 -4
  169. package/scripts/lane/lane-ledger.ts +21 -3
  170. package/scripts/lane/lane-measure-cli.ts +10 -2
  171. package/scripts/lane/lane-open.ts +9 -1
  172. package/scripts/lane/lane-preserve.ts +49 -0
  173. package/scripts/lane/lane-reap.ts +20 -5
  174. package/scripts/lane/lane-registry.ts +6 -0
  175. package/scripts/lane/lane-restart-cli.ts +51 -0
  176. package/scripts/lane/lane-restart.ts +175 -0
  177. package/scripts/lane/lane-session-broker.ts +363 -0
  178. package/scripts/lane/lane-spent.ts +195 -0
  179. package/scripts/lane/lane-stage-stats.ts +27 -3
  180. package/scripts/lane/lane-stop-stats.ts +96 -0
  181. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  182. package/scripts/lane/process-tools.ts +31 -0
  183. package/scripts/lib/buffered-process.ts +36 -2
  184. package/scripts/lib/pnpm-reach.ts +27 -0
  185. package/scripts/lib/status-icons.ts +5 -1
  186. package/scripts/lint/lint-parallel.ts +19 -2
  187. package/scripts/observations/observation-ledger-home.ts +1 -0
  188. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  189. package/scripts/release/release-issue-close.ts +133 -0
  190. package/scripts/release/release-publish.ts +12 -1
  191. package/scripts/repo/repository-labels.ts +11 -1
  192. package/scripts/review/comment-ranges.ts +199 -0
  193. package/scripts/review/review-attest-cli.ts +21 -6
  194. package/scripts/review/review-brief-cli.ts +24 -7
  195. package/scripts/review/review-code-tree.ts +119 -0
  196. package/scripts/review/review-comment-round.ts +114 -0
  197. package/scripts/review/review-record-cli.ts +39 -17
  198. package/scripts/review/review-round2-cli.ts +16 -5
  199. package/scripts/review/review-round2.ts +30 -10
  200. package/scripts/review/review-stamps.ts +22 -1
  201. package/scripts/rules/implementation-cut.ts +3 -3
  202. package/scripts/rules/pre-gate-cut.ts +1 -1
  203. package/scripts/run/board/run-board-checks.ts +42 -0
  204. package/scripts/run/board/run-board-cli.ts +2 -0
  205. package/scripts/run/board/run-board-every.ts +3 -1
  206. package/scripts/run/board/run-board-fit.ts +16 -5
  207. package/scripts/run/board/run-board-fresh.ts +64 -0
  208. package/scripts/run/board/run-board-header.ts +129 -53
  209. package/scripts/run/board/run-board-labels.ts +47 -4
  210. package/scripts/run/board/run-board-lane-event.ts +66 -0
  211. package/scripts/run/board/run-board-layout.ts +57 -9
  212. package/scripts/run/board/run-board-now.ts +234 -0
  213. package/scripts/run/board/run-board-operation-read.ts +75 -0
  214. package/scripts/run/board/run-board-operation.ts +150 -0
  215. package/scripts/run/board/run-board-phase.ts +26 -1
  216. package/scripts/run/board/run-board-plan.ts +4 -6
  217. package/scripts/run/board/run-board-read.ts +49 -3
  218. package/scripts/run/board/run-board-reason.ts +124 -0
  219. package/scripts/run/board/run-board-render-checks.ts +55 -0
  220. package/scripts/run/board/run-board-render-legend.ts +12 -3
  221. package/scripts/run/board/run-board-render-slot.ts +2 -2
  222. package/scripts/run/board/run-board-render-tail.ts +128 -0
  223. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  224. package/scripts/run/board/run-board-render-title.ts +58 -0
  225. package/scripts/run/board/run-board-render.ts +96 -80
  226. package/scripts/run/board/run-board-row-yield.ts +32 -0
  227. package/scripts/run/board/run-board-setup.ts +61 -13
  228. package/scripts/run/board/run-board-state.ts +10 -0
  229. package/scripts/run/board/run-board-status.ts +66 -7
  230. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  231. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  232. package/scripts/run/board/run-board-thanks.ts +53 -0
  233. package/scripts/run/board/run-board-tick.ts +50 -21
  234. package/scripts/run/board/run-board-usual.ts +44 -0
  235. package/scripts/run/carry/run-carry-args.ts +9 -1
  236. package/scripts/run/carry/run-carry-cli.ts +2 -45
  237. package/scripts/run/carry/run-carry-end.ts +73 -0
  238. package/scripts/run/carry/run-carry.ts +8 -1
  239. package/scripts/run/entry/run-entry-cli.ts +5 -0
  240. package/scripts/run/event/run-event-checks.ts +54 -0
  241. package/scripts/run/event/run-event-render.ts +2 -0
  242. package/scripts/run/event/run-event-scope.ts +1 -0
  243. package/scripts/run/event/run-event-setup.ts +55 -11
  244. package/scripts/run/event/run-event-stream.ts +27 -11
  245. package/scripts/run/merge/run-merge-cli.ts +7 -4
  246. package/scripts/run/merge/run-merge-ship.ts +61 -0
  247. package/scripts/run/merge/run-merge-steps.ts +7 -14
  248. package/scripts/run/merge/run-merge-token.ts +16 -2
  249. package/scripts/run/run-headless.ts +1 -0
  250. package/scripts/run/run-step.ts +2 -0
  251. package/scripts/run/ship/run-ship-cli.ts +22 -2
  252. package/scripts/run/ship/run-ship-return.ts +31 -13
  253. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  254. package/scripts/run/ship/run-ship-stage.ts +1 -0
  255. package/scripts/run/wake/run-wake-await.ts +130 -0
  256. package/scripts/run/wake/run-wake-cli.ts +60 -71
  257. package/scripts/run/wake/run-wake-describe.ts +34 -2
  258. package/scripts/run/wake/run-wake-driver.ts +11 -4
  259. package/scripts/run/wake/run-wake-session.ts +20 -3
  260. package/scripts/run/wake/run-wake-stop.ts +37 -0
  261. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  262. package/scripts/sync/claude-settings-merge.ts +144 -0
  263. package/scripts/sync/sync-ai-files.ts +28 -3
  264. package/scripts/sync/sync.ts +15 -0
  265. package/scripts/test/project-test-script.ts +73 -0
  266. package/scripts/test/skip-marker.ts +14 -1
  267. package/scripts/test/test-related.ts +3 -2
  268. package/scripts/test/test-unit-guard.ts +78 -14
  269. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  270. package/scripts/uninstall/distributed-files.ts +92 -0
  271. package/scripts/uninstall/uninstall-plan.ts +119 -0
  272. package/scripts/uninstall/uninstall-report.ts +61 -0
  273. package/scripts/uninstall/uninstall.ts +206 -0
  274. package/templates/AGENTS.basic.md +4 -0
  275. package/templates/GEMINI.basic.md +6 -0
  276. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  277. package/scripts/lane/openai-review-broker.ts +0 -262
  278. package/scripts/run/board/run-board-track.ts +0 -22
@@ -0,0 +1,189 @@
1
+ import { createHash } from 'node:crypto'
2
+ import {
3
+ constants,
4
+ cpSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ rmSync,
10
+ statSync,
11
+ } from 'node:fs'
12
+ import os from 'node:os'
13
+ import path from 'node:path'
14
+ import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
15
+ import { resolve_tsx_runner } from '#scripts/josh/josh-logic'
16
+ import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
17
+ import { execaSync } from 'execa'
18
+ import { init_bootstrap } from './init-bootstrap'
19
+ import { init_dry_run_report, type FileChange } from './init-dry-run-report'
20
+ import { init_install } from './init-install'
21
+ import { PACKAGE_DIR, package_path } from './init-paths'
22
+ import { project_profile, type ProjectProfile } from './project-profile'
23
+ import { start_exec } from './start-exec'
24
+
25
+ // `josh init --dry-run` and `josh start --dry-run` answer "what would the setup change?" without
26
+ // a second copy of the setup's decisions: the real `josh init` runs on a scratch copy of the project,
27
+ // and the plan is the difference it left there. Every create, merge and skip is decided by the code
28
+ // that decides it for real, so the plan cannot drift from the run (#3830).
29
+
30
+ interface DryRunRequest {
31
+ root: string
32
+ profile: ProjectProfile | undefined
33
+ // Whether the setup meets a repository — `josh start` creates one before its setup runs.
34
+ has_git: boolean
35
+ is_install: boolean
36
+ }
37
+
38
+ interface DryRunSplit {
39
+ is_dry_run: boolean
40
+ rest: ReadonlyArray<string>
41
+ }
42
+
43
+ const DRY_RUN_FLAG = '--dry-run'
44
+ const INIT_SCRIPT = 'scripts/init/init.ts'
45
+ const SCRATCH_PREFIX = 'josh-init-dry-run-'
46
+ const DEFAULT_BRANCH = 'main'
47
+ const TITLE = 'josh init --dry-run — nothing was written. The setup would:'
48
+
49
+ function split_dry_run_flag(args: ReadonlyArray<string>): DryRunSplit {
50
+ return {
51
+ is_dry_run: args.includes(DRY_RUN_FLAG),
52
+ rest: args.filter((argument) => argument !== DRY_RUN_FLAG),
53
+ }
54
+ }
55
+
56
+ // A directory is copied unless the project scan skips it: an ignored one, or a link — followed, a
57
+ // link to the project or an ancestor would copy it into itself until the path is too long.
58
+ function is_copied_directory(source: string): boolean {
59
+ return (
60
+ !lstatSync(source).isSymbolicLink() &&
61
+ !project_profile.is_ignored_directory(path.basename(source))
62
+ )
63
+ }
64
+
65
+ // A link to a file is copied as the file it points to, so no write in the scratch copy can follow a
66
+ // link back into the project; a dangling one is left out, as the project scan leaves out what it
67
+ // cannot read. The root itself is always copied: `cpSync` filters it too, so a project in a
68
+ // directory named `build` or `dist` would otherwise be planned as an empty one.
69
+ function is_copied(root: string, source: string): boolean {
70
+ if (source === root) return true
71
+ if (!existsSync(source)) return false
72
+
73
+ return !statSync(source).isDirectory() || is_copied_directory(source)
74
+ }
75
+
76
+ // The scratch repository answers the setup's git questions as the project's does: one exists, and
77
+ // its origin is the same URL, so a GitHub project is planned as one.
78
+ function mirror_git(root: string, scratch: string): void {
79
+ start_exec.git_succeeds(['init', '--quiet', `--initial-branch=${DEFAULT_BRANCH}`], scratch)
80
+ const origin = git_spawn_sync.origin_url(root)
81
+
82
+ if (origin !== undefined) start_exec.git_succeeds(['remote', 'add', 'origin', origin], scratch)
83
+ }
84
+
85
+ function prepare_scratch(root: string, has_git: boolean): string {
86
+ const scratch = mkdtempSync(path.join(os.tmpdir(), SCRATCH_PREFIX))
87
+
88
+ // A copy-on-write clone where the file system supports one, so mirroring a large project costs
89
+ // next to nothing there; a plain copy elsewhere.
90
+ cpSync(root, scratch, {
91
+ recursive: true,
92
+ dereference: true,
93
+ mode: constants.COPYFILE_FICLONE,
94
+ filter: (source) => is_copied(root, source),
95
+ })
96
+ if (has_git) mirror_git(root, scratch)
97
+
98
+ return scratch
99
+ }
100
+
101
+ function digest_of(file: string): string {
102
+ return createHash('sha256').update(readFileSync(file)).digest('hex')
103
+ }
104
+
105
+ // A snapshot holds each file's digest, not its content, so a project with large files is compared
106
+ // without holding them in memory; only the files the setup changed are read in full.
107
+ function snapshot(root: string): Map<string, string> {
108
+ const files = project_profile.list_files(root)
109
+
110
+ return new Map(files.map((file) => [path.relative(root, file), digest_of(file)]))
111
+ }
112
+
113
+ function content_of(
114
+ root: string,
115
+ file: string,
116
+ digests: ReadonlyMap<string, string>,
117
+ ): string | undefined {
118
+ return digests.has(file) ? readFileSync(path.join(root, file), 'utf8') : undefined
119
+ }
120
+
121
+ // The project itself still holds what the scratch copy held before the setup ran.
122
+ function changes_since(
123
+ request: DryRunRequest,
124
+ scratch: string,
125
+ before: ReadonlyMap<string, string>,
126
+ ): Array<FileChange> {
127
+ const after = snapshot(scratch)
128
+
129
+ return init_dry_run_report.changed_files(before, after).map((change) => ({
130
+ path: change.path,
131
+ before: content_of(request.root, change.path, before),
132
+ after: content_of(scratch, change.path, after),
133
+ }))
134
+ }
135
+
136
+ // `--no-install` keeps the install out of the scratch copy, and the hand-off variable keeps this
137
+ // kit doing the setup itself — the scratch copy has no kit of its own to hand it to.
138
+ function run_init(scratch: string, profile: ProjectProfile | undefined): void {
139
+ const runner = resolve_tsx_runner()
140
+ const profile_args = profile === undefined ? [] : ['--profile', profile]
141
+ const args = [package_path(INIT_SCRIPT), init_install.NO_INSTALL_FLAG, ...profile_args]
142
+ const result = execaSync(runner.executable, [...runner.leading_arguments, ...args], {
143
+ cwd: scratch,
144
+ env: { [init_bootstrap.HANDOFF_ENV]: '1' },
145
+ all: true,
146
+ reject: false,
147
+ })
148
+
149
+ if (result.exitCode !== 0) throw new Error(`the simulated josh init failed:\n${result.all}`)
150
+ }
151
+
152
+ function simulate(request: DryRunRequest): Array<FileChange> {
153
+ const scratch = prepare_scratch(request.root, request.has_git)
154
+
155
+ try {
156
+ const before = snapshot(scratch)
157
+
158
+ run_init(scratch, request.profile)
159
+
160
+ return changes_since(request, scratch, before)
161
+ } finally {
162
+ rmSync(scratch, { recursive: true, force: true })
163
+ }
164
+ }
165
+
166
+ // The same two questions the real run asks: whether to install at all, and whether this kit is the
167
+ // project's own or hands the setup to the one it installs.
168
+ function dependency_lines(root: string, is_install: boolean): Array<string> {
169
+ if (!is_install) return [`skip the install and the format (${init_install.NO_INSTALL_FLAG})`]
170
+ const steps = `run ${init_install.INSTALL_STEP.label}, then ${init_install.FORMAT_STEP.label}`
171
+
172
+ if (init_bootstrap.is_project_kit(PACKAGE_DIR, root)) return [steps]
173
+
174
+ return [`install ${KIT_PACKAGE_NAME} into the project and hand the setup to its josh init`, steps]
175
+ }
176
+
177
+ function plan_report(request: DryRunRequest, title: string = TITLE): string {
178
+ const changes = simulate(request)
179
+
180
+ return init_dry_run_report.format_report(
181
+ title,
182
+ changes,
183
+ dependency_lines(request.root, request.is_install),
184
+ )
185
+ }
186
+
187
+ const init_dry_run = { DRY_RUN_FLAG, split_dry_run_flag, plan_report }
188
+ export { init_dry_run }
189
+ export type { DryRunRequest }
@@ -62,6 +62,13 @@ function run_post_init_steps(project_root: string): string | undefined {
62
62
  return undefined
63
63
  }
64
64
 
65
- const init_install = { NO_INSTALL_FLAG, split_install_flag, did_step_succeed, run_post_init_steps }
65
+ const init_install = {
66
+ NO_INSTALL_FLAG,
67
+ INSTALL_STEP,
68
+ FORMAT_STEP,
69
+ split_install_flag,
70
+ did_step_succeed,
71
+ run_post_init_steps,
72
+ }
66
73
  export { init_install }
67
74
  export type { InstallStep }
@@ -0,0 +1,76 @@
1
+ import { parse_jsonc } from '#scripts/config-merge/parse-jsonc'
2
+ import { json_value } from '#scripts/lib/json-value'
3
+ import { init_logic_json_merge } from './init-logic-json-merge'
4
+ import { kit_development_versions } from './kit-development-versions'
5
+
6
+ // The tools a consumer has to resolve itself, as opposed to the ones only kit's own ESLint config
7
+ // imports. Those are optional kit peers, which pnpm does not install unlisted, so a consumer still
8
+ // needs them — `josh init` writes them, `josh sync` leaves them to it (#3711). A tool belongs here
9
+ // when the consumer runs its bin from the project root (eslint, prettier, tsc, playwright, cspell,
10
+ // lefthook), when a config the consumer owns imports it (`playwright.config.ts` → @playwright/test,
11
+ // @types/node), or when it is named by a string Prettier resolves from the consumer (the preset's
12
+ // plugins). `josh init` and `josh sync` both read this one list, so a tool added here reaches a fresh
13
+ // project and an already-initialized one alike.
14
+ const CONSUMER_RESOLVED_TOOLS = [
15
+ 'eslint',
16
+ 'prettier',
17
+ 'typescript',
18
+ '@playwright/test',
19
+ '@ianvs/prettier-plugin-sort-imports',
20
+ 'prettier-plugin-svelte',
21
+ 'prettier-plugin-tailwindcss',
22
+ 'cspell',
23
+ '@types/node',
24
+ ]
25
+ // Only a project with Git gets `lefthook.yml`, so only that one needs the bin.
26
+ const LEFTHOOK_DEVELOPMENT_DEPENDENCY = 'lefthook'
27
+ const DECLARING_FIELDS = [
28
+ 'dependencies',
29
+ 'devDependencies',
30
+ 'peerDependencies',
31
+ 'optionalDependencies',
32
+ ]
33
+
34
+ function consumer_resolved_names(has_git: boolean): ReadonlyArray<string> {
35
+ return has_git
36
+ ? [...CONSUMER_RESOLVED_TOOLS, LEFTHOOK_DEVELOPMENT_DEPENDENCY]
37
+ : CONSUMER_RESOLVED_TOOLS
38
+ }
39
+
40
+ // What `josh init` adds: every kit peer — the ones only kit's own configs import included — and the
41
+ // tools above. Versions mirror kit's own devDependencies, so the consumer runs the toolchain kit is
42
+ // verified with.
43
+ function get_toolchain_development_dependencies(has_git: boolean): Record<string, string> {
44
+ return kit_development_versions.versions_of([
45
+ ...kit_development_versions.peer_names(),
46
+ ...consumer_resolved_names(has_git),
47
+ ])
48
+ }
49
+
50
+ function is_declared(manifest: Record<string, unknown>, name: string): boolean {
51
+ return DECLARING_FIELDS.some((field) => {
52
+ const dependencies = manifest[field]
53
+
54
+ return json_value.is_record(dependencies) && Object.hasOwn(dependencies, name)
55
+ })
56
+ }
57
+
58
+ // What `josh sync` adds to an already-initialized consumer: only the tools above, and only the ones
59
+ // the manifest declares nowhere — a tool kept under `dependencies` is present, not missing.
60
+ function merge_consumer_resolved_development_deps(content: string, has_git: boolean): string {
61
+ const manifest = parse_jsonc(content)
62
+ const missing = consumer_resolved_names(has_git).filter((name) => !is_declared(manifest, name))
63
+
64
+ return init_logic_json_merge.merge_development_dependencies(
65
+ content,
66
+ kit_development_versions.versions_of(missing),
67
+ )
68
+ }
69
+
70
+ const init_logic_toolchain = {
71
+ consumer_resolved_names,
72
+ get_toolchain_development_dependencies,
73
+ merge_consumer_resolved_development_deps,
74
+ }
75
+
76
+ export { init_logic_toolchain }
@@ -7,6 +7,7 @@ import { init_logic_json_merge } from './init-logic-json-merge'
7
7
  import { init_logic_secretlint } from './init-logic-secretlint'
8
8
  import { init_logic_sonar } from './init-logic-sonar'
9
9
  import { init_logic_templates } from './init-logic-templates'
10
+ import { init_logic_toolchain } from './init-logic-toolchain'
10
11
  import { init_logic_workspace } from './init-logic-workspace'
11
12
  import { init_logic_yaml_merge } from './init-logic-yaml-merge'
12
13
 
@@ -417,6 +418,7 @@ const init_logic = {
417
418
  ...init_logic_sonar,
418
419
  ...init_logic_json_merge,
419
420
  ...init_logic_secretlint,
421
+ ...init_logic_toolchain,
420
422
  ...init_logic_yaml_merge,
421
423
  ...init_logic_deploy_vps,
422
424
  ...distributed_paths,
@@ -19,21 +19,27 @@ import { init_actions, PRETTIER_CONFIG_JS } from './init-actions'
19
19
  import { init_ai_copy } from './init-ai-copy'
20
20
  import { init_basic } from './init-basic'
21
21
  import { init_bootstrap } from './init-bootstrap'
22
+ import { init_dry_run } from './init-dry-run'
22
23
  import { init_file_action } from './init-file-action'
23
24
  import { init_install } from './init-install'
24
25
  import { init_logic } from './init-logic'
25
26
  import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
26
- import { kit_development_versions } from './kit-development-versions'
27
- import { kit_setup_state } from './kit-setup-state'
28
27
  import { package_manager_pin } from './package-manager-pin'
29
28
  import { project_profile, type ProjectProfile, type ProjectShape } from './project-profile'
29
+ import { working_tree } from './working-tree'
30
30
 
31
31
  const PACKAGE_JSON = 'package.json'
32
32
  const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
33
33
  const LEFTHOOK_BIN = 'lefthook'
34
34
  const ARGUMENT_START_INDEX = 2
35
35
  const INSTALL_HINT = '→ run `pnpm install` to install what package.json lists'
36
- const INIT_USAGE = 'josh init [--profile basic|full] [--no-install]'
36
+ const INIT_USAGE = 'josh init [--profile basic|full] [--no-install] [--dry-run]'
37
+
38
+ interface InitRequest {
39
+ is_dry_run: boolean
40
+ is_install: boolean
41
+ requested: ProjectProfile | undefined
42
+ }
37
43
 
38
44
  const engine_pin_schema = z.looseObject({ name: z.string(), version: z.string() })
39
45
 
@@ -71,27 +77,8 @@ function get_kit_self_dependency(): Record<string, string> {
71
77
  return { [KIT_PACKAGE_NAME]: version }
72
78
  }
73
79
 
74
- // Each config `init` generates runs a CLI or imports types the consumer has to resolve itself:
75
- // `cspell.config.yaml` → cspell, `playwright.config.ts` → @types/node, `lefthook.yml` → lefthook.
76
- // Prettier, its preset plugins and @playwright/test come in as peers. Without them the first
77
- // `josh gate` after `pnpm install` fails.
78
- const TOOL_DEVELOPMENT_DEPENDENCIES = ['cspell', '@types/node']
79
- const LEFTHOOK_DEVELOPMENT_DEPENDENCY = 'lefthook'
80
-
81
- function tool_dependency_names(has_git: boolean): ReadonlyArray<string> {
82
- return has_git
83
- ? [...TOOL_DEVELOPMENT_DEPENDENCIES, LEFTHOOK_DEVELOPMENT_DEPENDENCY]
84
- : TOOL_DEVELOPMENT_DEPENDENCIES
85
- }
86
-
87
- // Versions mirror kit's own devDependencies, so the consumer runs the toolchain kit is verified with.
88
- function get_toolchain_development_dependencies(has_git: boolean): Record<string, string> {
89
- return kit_development_versions.versions_of([
90
- ...kit_development_versions.peer_names(),
91
- ...tool_dependency_names(has_git),
92
- ])
93
- }
94
-
80
+ // Each config `init` generates runs a CLI or imports types the consumer has to resolve itself, so
81
+ // the toolchain is added here: without it the first `josh gate` after `pnpm install` fails.
95
82
  function apply_dependency_merges(content: string, has_git = true): string {
96
83
  const migrated = init_logic.strip_managed_postinstall(content)
97
84
  const suggested = init_logic.get_suggested_scripts_for_content(migrated)
@@ -103,7 +90,7 @@ function apply_dependency_merges(content: string, has_git = true): string {
103
90
  )
104
91
  const with_toolchain = init_logic.merge_development_dependencies(
105
92
  merged,
106
- get_toolchain_development_dependencies(has_git),
93
+ init_logic.get_toolchain_development_dependencies(has_git),
107
94
  )
108
95
  const with_secretlint = init_logic.merge_secretlint_development_deps(with_toolchain)
109
96
 
@@ -147,6 +134,7 @@ function merged_manifest(existing: string, shape: ProjectShape): string {
147
134
  return init_basic.with_recorded_profile(
148
135
  apply_package_json_merges(existing, shape.has_git),
149
136
  shape.profile,
137
+ shape.has_workflow,
150
138
  )
151
139
  }
152
140
 
@@ -284,14 +272,21 @@ function set_up_project(requested: ProjectProfile | undefined, is_install: boole
284
272
  console.info('\n✅ Done.\n')
285
273
  }
286
274
 
287
- // True when this process set the project up — false when it refused, or handed the run to the
288
- // project's own kit, which then speaks for itself.
289
- function main(args: ReadonlyArray<string> = []): boolean {
290
- if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return false
291
- const { is_install, rest } = init_install.split_install_flag(args)
292
- // Validated before the hand-off, so a malformed invocation is refused before kit is installed.
293
- const requested = project_profile.requested_profile(rest, INIT_USAGE)
275
+ // A dry run sets nothing up, so it answers false and points nowhere onward.
276
+ function print_dry_run({ requested, is_install }: InitRequest): false {
277
+ const has_git = project_profile.has_git(PROJECT_ROOT)
278
+
279
+ console.info(
280
+ init_dry_run.plan_report({ root: PROJECT_ROOT, profile: requested, has_git, is_install }),
281
+ )
282
+
283
+ return false
284
+ }
294
285
 
286
+ function did_set_up_here(
287
+ { requested, is_install }: InitRequest,
288
+ args: ReadonlyArray<string>,
289
+ ): boolean {
295
290
  const is_set_up_here = !is_install || !did_hand_off(args)
296
291
 
297
292
  if (is_set_up_here) set_up_project(requested, is_install)
@@ -299,19 +294,43 @@ function main(args: ReadonlyArray<string> = []): boolean {
299
294
  return is_set_up_here
300
295
  }
301
296
 
302
- // Only the command a person typed points onward to `josh start`: `josh start` calls `main` itself and
303
- // is already the next step.
304
- // A setup that failed after writing, such as a failed install, sets the exit code and points nowhere.
305
- function print_start_hint(): void {
306
- if (process.exitCode !== undefined && process.exitCode !== 0) return
307
- const hint = kit_setup_state.start_hint(PROJECT_ROOT)
297
+ // The flags are peeled off first, so what remains is exactly the `--profile` pair.
298
+ // Validated before the hand-off, so a malformed invocation is refused before kit is installed.
299
+ function parse_init_args(args: ReadonlyArray<string>): InitRequest {
300
+ const { is_dry_run, rest: run_args } = init_dry_run.split_dry_run_flag(args)
301
+ const { is_install, rest } = init_install.split_install_flag(run_args)
302
+
303
+ return { is_dry_run, is_install, requested: project_profile.requested_profile(rest, INIT_USAGE) }
304
+ }
308
305
 
309
- if (hint !== undefined) console.info(`${hint}\n`)
306
+ // True when this process set the project up — false when it refused, or handed the run to the
307
+ // project's own kit, which then speaks for itself.
308
+ function main(args: ReadonlyArray<string> = []): boolean {
309
+ if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return false
310
+ const request = parse_init_args(args)
311
+
312
+ return request.is_dry_run ? print_dry_run(request) : did_set_up_here(request, args)
313
+ }
314
+
315
+ // Only the command a person typed is refused, before anything is written or installed. `josh start`
316
+ // calls `main` itself and decides about its own tree (#3835); a handed-off run follows the check
317
+ // its caller already passed, on a tree the kit install it just made has changed.
318
+ function did_refuse_dirty_tree(): boolean {
319
+ const refusal = init_bootstrap.is_handed_off()
320
+ ? undefined
321
+ : working_tree.dirty_tree_refusal(PROJECT_ROOT, 'josh init')
322
+
323
+ if (refusal === undefined) return false
324
+ console.error(`\n${refusal}\n`)
325
+ process.exitCode = 1
326
+
327
+ return true
310
328
  }
311
329
 
312
330
  function run_cli(args: ReadonlyArray<string>): void {
313
331
  try {
314
- if (main(args)) print_start_hint()
332
+ if (did_refuse_dirty_tree()) return
333
+ main(args)
315
334
  } catch (error) {
316
335
  console.error(`\n✖ ${error_text.message_of(error)}\n`)
317
336
  process.exitCode = 1
@@ -325,10 +344,11 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
325
344
  const init = {
326
345
  copy_ai_file: init_ai_copy.copy_ai_file,
327
346
  apply_package_json_merges,
347
+ merged_manifest,
328
348
  install_lefthook,
329
349
  resolve_kit_package_manager,
330
350
  }
331
351
 
332
352
  // `init` is the entry point; `main` is exported for its own unit test, the way `sync.ts` exports the
333
- // `main()` its self-run guard lives in.
334
- export { init, main }
353
+ // `main()` its self-run guard lives in, and `run_cli` for the refusal only the typed command makes.
354
+ export { init, main, run_cli }
@@ -1,30 +1,24 @@
1
+ import { json_value } from '#scripts/lib/json-value'
1
2
  import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
2
3
  import { start_exec } from './start-exec'
4
+ import { workflow_mark } from './workflow-mark'
3
5
 
4
- const START_HINT =
5
- 'To use the GitHub Issue workflow, run pnpm exec josh start next: it puts this setup on GitHub, through a pull request when main already has commits.'
6
-
7
- // Whether the commit checked out records kit in its manifest — the sign that kit's setup has already
8
- // reached the branch, so `josh start` has no setup pull request to open.
6
+ // Whether the commit checked out holds `josh start`'s setup — its manifest lists kit and records the
7
+ // GitHub workflow mark — the sign that the setup has already reached the branch, so `josh start` has
8
+ // no setup pull request to open. Kit alone is not enough: a committed `josh init` setup lacks the mark
9
+ // and, on `basic`, `.claude/settings.json`, which `josh start`'s setup then writes and only a setup
10
+ // pull request carries to `main` (#3835).
9
11
  function is_kit_committed(root: string): boolean {
10
12
  // `./` resolves the path from `root` rather than from the repository's top level.
11
13
  const manifest = start_exec.git_read(['show', 'HEAD:./package.json'], root)
12
14
 
13
- return manifest?.includes(`"${KIT_PACKAGE_NAME}"`) ?? false
14
- }
15
-
16
- function is_git_work_tree(root: string): boolean {
17
- return start_exec.git_succeeds(['rev-parse', '--is-inside-work-tree'], root)
18
- }
15
+ if (manifest?.includes(`"${KIT_PACKAGE_NAME}"`) !== true) return false
19
16
 
20
- // The line `josh init` ends with in a Git repository whose checkout does not hold kit yet: the setup is
21
- // still uncommitted, and `josh start` is what carries it to GitHub.
22
- function start_hint(root: string): string | undefined {
23
- if (!is_git_work_tree(root) || is_kit_committed(root)) return undefined
17
+ const parsed = json_value.parse_or_undefined(manifest)
24
18
 
25
- return START_HINT
19
+ return workflow_mark.is_recorded(json_value.is_record(parsed) ? parsed : undefined)
26
20
  }
27
21
 
28
- const kit_setup_state = { is_kit_committed, start_hint }
22
+ const kit_setup_state = { is_kit_committed }
29
23
 
30
24
  export { kit_setup_state }
@@ -0,0 +1,21 @@
1
+ import { existsSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ // The configuration files of a formatter other than Prettier. A `basic` project holding one already
5
+ // formats its own files, so `josh init` gives it no Prettier and `josh format` / `josh gate` skip it
6
+ // (joshuafolkken/kit#3832).
7
+ const OWN_FORMATTER_CONFIGS = [
8
+ 'biome.json',
9
+ 'biome.jsonc',
10
+ 'dprint.json',
11
+ 'dprint.jsonc',
12
+ '.dprint.json',
13
+ '.dprint.jsonc',
14
+ ]
15
+
16
+ function find_config(root: string): string | undefined {
17
+ return OWN_FORMATTER_CONFIGS.find((name) => existsSync(path.join(root, name)))
18
+ }
19
+
20
+ const own_formatter = { find_config }
21
+ export { own_formatter }
@@ -4,6 +4,9 @@ import { repo_origin } from '#scripts/discovery/repo-origin'
4
4
  import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
5
5
  import { json_value } from '#scripts/lib/json-value'
6
6
  import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
7
+ import { kit_written_paths } from './kit-written-paths'
8
+ import { own_formatter } from './own-formatter'
9
+ import { workflow_mark } from './workflow-mark'
7
10
 
8
11
  type ProjectProfile = 'basic' | 'full'
9
12
 
@@ -13,10 +16,13 @@ interface ProfileResult {
13
16
  }
14
17
 
15
18
  interface ProjectShape extends ProfileResult {
16
- has_web: boolean
19
+ // Web files, and no formatter of the project's own (`own-formatter.ts`).
20
+ should_use_prettier: boolean
17
21
  has_typescript: boolean
18
22
  has_git: boolean
19
23
  has_github: boolean
24
+ // Set up by `josh start` for the GitHub Issue workflow (`workflow-mark.ts`).
25
+ has_workflow: boolean
20
26
  }
21
27
 
22
28
  const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', '.svelte-kit'])
@@ -123,8 +129,12 @@ function resolve_profile(root: string, requested?: ProjectProfile): ProfileResul
123
129
  return inferred_profile(manifest)
124
130
  }
125
131
 
132
+ function is_ignored_directory(name: string): boolean {
133
+ return IGNORED_DIRECTORIES.has(name)
134
+ }
135
+
126
136
  function should_visit(entry: Dirent): boolean {
127
- return entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)
137
+ return entry.isDirectory() && !is_ignored_directory(entry.name)
128
138
  }
129
139
 
130
140
  function scan_directory(directory: string, pending: Array<string>): Array<string> {
@@ -136,26 +146,46 @@ function scan_directory(directory: string, pending: Array<string>): Array<string
136
146
  .map((entry) => path.join(directory, entry.name)),
137
147
  )
138
148
 
139
- return entries.filter((entry) => entry.isFile()).map((entry) => path.extname(entry.name))
149
+ return entries.filter((entry) => entry.isFile()).map((entry) => path.join(directory, entry.name))
140
150
  }
141
151
 
142
- function file_extensions(root: string): Array<string> {
152
+ // A file kit wrote says nothing about the project: counted, the `.pnpmfile.mjs` of the first
153
+ // `josh init` would read as the user's Web file on the second and bring Prettier in
154
+ // (joshuafolkken/kit#3713).
155
+ function is_project_file(root: string, file: string): boolean {
156
+ const relative_path = path.relative(root, file).split(path.sep).join(path.posix.sep)
157
+
158
+ return !kit_written_paths.is_kit_written(relative_path)
159
+ }
160
+
161
+ // Every file under `root` outside the directories no setup reads — the one walk the project scan and
162
+ // the `josh init --dry-run` snapshot share.
163
+ function list_files(root: string): Array<string> {
143
164
  const pending = [root]
144
- const extensions: Array<string> = []
165
+ const files: Array<string> = []
145
166
 
146
167
  for (const directory of pending) {
147
- extensions.push(...scan_directory(directory, pending))
168
+ files.push(...scan_directory(directory, pending))
148
169
  }
149
170
 
150
- return extensions
171
+ return files
151
172
  }
152
173
 
153
- function scan_files(root: string): { has_web: boolean; has_typescript: boolean } {
174
+ function file_extensions(root: string): Array<string> {
175
+ return list_files(root)
176
+ .filter((file) => is_project_file(root, file))
177
+ .map((file) => path.extname(file))
178
+ }
179
+
180
+ // A project that formats with its own tool gets no Prettier even with Web files: two formatters
181
+ // would rewrite each other's output (joshuafolkken/kit#3832).
182
+ function scan_files(root: string): { should_use_prettier: boolean; has_typescript: boolean } {
154
183
  const extensions = file_extensions(root)
155
184
  const has_web = extensions.some((extension) => WEB_EXTENSIONS.has(extension))
185
+ const should_use_prettier = has_web && own_formatter.find_config(root) === undefined
156
186
  const has_typescript = extensions.some((extension) => TYPESCRIPT_EXTENSIONS.has(extension))
157
187
 
158
- return { has_web, has_typescript }
188
+ return { should_use_prettier, has_typescript }
159
189
  }
160
190
 
161
191
  function has_github_remote(root: string): boolean {
@@ -164,18 +194,36 @@ function has_github_remote(root: string): boolean {
164
194
  return url !== undefined && repo_origin.parse_origin_url(url) !== undefined
165
195
  }
166
196
 
197
+ function has_git(root: string): boolean {
198
+ return existsSync(path.join(root, '.git'))
199
+ }
200
+
201
+ // The run is asked first, so a `josh start` whose setup has not written `package.json` yet reads it.
202
+ function has_workflow(root: string): boolean {
203
+ return workflow_mark.is_requested() || workflow_mark.is_recorded(read_manifest(root))
204
+ }
205
+
167
206
  function inspect_project(root: string, requested?: ProjectProfile): ProjectShape {
168
207
  const profile = resolve_profile(root, requested)
169
208
  const files = scan_files(root)
170
- const has_git = existsSync(path.join(root, '.git'))
171
-
172
- return { ...profile, ...files, has_git, has_github: has_git && has_github_remote(root) }
209
+ const is_git = has_git(root)
210
+
211
+ return {
212
+ ...profile,
213
+ ...files,
214
+ has_git: is_git,
215
+ has_github: is_git && has_github_remote(root),
216
+ has_workflow: has_workflow(root),
217
+ }
173
218
  }
174
219
 
175
220
  const project_profile = {
176
221
  parse_profile,
177
222
  resolve_profile,
223
+ has_git,
178
224
  inspect_project,
225
+ is_ignored_directory,
226
+ list_files,
179
227
  requested_profile,
180
228
  with_legacy_profile_names,
181
229
  }