@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
@@ -3,6 +3,7 @@ import { json_value } from '#scripts/lib/json-value'
3
3
  import { z } from 'zod'
4
4
  import { init_logic } from './init-logic'
5
5
  import type { ProjectShape } from './project-profile'
6
+ import { workflow_mark } from './workflow-mark'
6
7
 
7
8
  const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
8
9
  const record_schema = z.record(z.string(), z.unknown())
@@ -16,13 +17,17 @@ function initial_manifest(): string {
16
17
  return json_format.format_json({ private: true })
17
18
  }
18
19
 
19
- function with_recorded_profile(content: string, profile: ProjectShape['profile']): string {
20
+ function with_recorded_profile(
21
+ content: string,
22
+ profile: ProjectShape['profile'],
23
+ has_workflow = false,
24
+ ): string {
20
25
  const parsed: unknown = JSON.parse(content)
21
26
  const manifest = record_schema.parse(parsed)
22
27
  const existing = manifest['josh']
23
28
  const josh = json_value.is_record(existing) ? existing : {}
24
29
 
25
- manifest['josh'] = { ...josh, profile }
30
+ manifest['josh'] = { ...josh, profile, ...workflow_mark.recorded_fields(has_workflow) }
26
31
 
27
32
  return json_format.format_json(manifest)
28
33
  }
@@ -32,7 +37,7 @@ function merge_basic_manifest(
32
37
  shape: ProjectShape,
33
38
  versions: BasicVersions,
34
39
  ): string {
35
- const with_profile = with_recorded_profile(content, shape.profile)
40
+ const with_profile = with_recorded_profile(content, shape.profile, shape.has_workflow)
36
41
  const with_script = init_logic.merge_package_scripts(with_profile, {
37
42
  preinstall: init_logic.SAFE_CHAIN_CMD,
38
43
  josh: 'josh',
@@ -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,
@@ -23,7 +23,6 @@ import { init_file_action } from './init-file-action'
23
23
  import { init_install } from './init-install'
24
24
  import { init_logic } from './init-logic'
25
25
  import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
26
- import { kit_development_versions } from './kit-development-versions'
27
26
  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'
@@ -71,27 +70,8 @@ function get_kit_self_dependency(): Record<string, string> {
71
70
  return { [KIT_PACKAGE_NAME]: version }
72
71
  }
73
72
 
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
-
73
+ // Each config `init` generates runs a CLI or imports types the consumer has to resolve itself, so
74
+ // the toolchain is added here: without it the first `josh gate` after `pnpm install` fails.
95
75
  function apply_dependency_merges(content: string, has_git = true): string {
96
76
  const migrated = init_logic.strip_managed_postinstall(content)
97
77
  const suggested = init_logic.get_suggested_scripts_for_content(migrated)
@@ -103,7 +83,7 @@ function apply_dependency_merges(content: string, has_git = true): string {
103
83
  )
104
84
  const with_toolchain = init_logic.merge_development_dependencies(
105
85
  merged,
106
- get_toolchain_development_dependencies(has_git),
86
+ init_logic.get_toolchain_development_dependencies(has_git),
107
87
  )
108
88
  const with_secretlint = init_logic.merge_secretlint_development_deps(with_toolchain)
109
89
 
@@ -147,6 +127,7 @@ function merged_manifest(existing: string, shape: ProjectShape): string {
147
127
  return init_basic.with_recorded_profile(
148
128
  apply_package_json_merges(existing, shape.has_git),
149
129
  shape.profile,
130
+ shape.has_workflow,
150
131
  )
151
132
  }
152
133
 
@@ -325,6 +306,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
325
306
  const init = {
326
307
  copy_ai_file: init_ai_copy.copy_ai_file,
327
308
  apply_package_json_merges,
309
+ merged_manifest,
328
310
  install_lefthook,
329
311
  resolve_kit_package_manager,
330
312
  }
@@ -4,6 +4,8 @@ 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 { workflow_mark } from './workflow-mark'
7
9
 
8
10
  type ProjectProfile = 'basic' | 'full'
9
11
 
@@ -17,6 +19,8 @@ interface ProjectShape extends ProfileResult {
17
19
  has_typescript: boolean
18
20
  has_git: boolean
19
21
  has_github: boolean
22
+ // Set up by `josh start` for the GitHub Issue workflow (`workflow-mark.ts`).
23
+ has_workflow: boolean
20
24
  }
21
25
 
22
26
  const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', '.svelte-kit'])
@@ -136,18 +140,27 @@ function scan_directory(directory: string, pending: Array<string>): Array<string
136
140
  .map((entry) => path.join(directory, entry.name)),
137
141
  )
138
142
 
139
- return entries.filter((entry) => entry.isFile()).map((entry) => path.extname(entry.name))
143
+ return entries.filter((entry) => entry.isFile()).map((entry) => path.join(directory, entry.name))
144
+ }
145
+
146
+ // A file kit wrote says nothing about the project: counted, the `.pnpmfile.mjs` of the first
147
+ // `josh init` would read as the user's Web file on the second and bring Prettier in
148
+ // (joshuafolkken/kit#3713).
149
+ function is_project_file(root: string, file: string): boolean {
150
+ const relative_path = path.relative(root, file).split(path.sep).join(path.posix.sep)
151
+
152
+ return !kit_written_paths.is_kit_written(relative_path)
140
153
  }
141
154
 
142
155
  function file_extensions(root: string): Array<string> {
143
156
  const pending = [root]
144
- const extensions: Array<string> = []
157
+ const files: Array<string> = []
145
158
 
146
159
  for (const directory of pending) {
147
- extensions.push(...scan_directory(directory, pending))
160
+ files.push(...scan_directory(directory, pending))
148
161
  }
149
162
 
150
- return extensions
163
+ return files.filter((file) => is_project_file(root, file)).map((file) => path.extname(file))
151
164
  }
152
165
 
153
166
  function scan_files(root: string): { has_web: boolean; has_typescript: boolean } {
@@ -164,17 +177,33 @@ function has_github_remote(root: string): boolean {
164
177
  return url !== undefined && repo_origin.parse_origin_url(url) !== undefined
165
178
  }
166
179
 
180
+ function has_git(root: string): boolean {
181
+ return existsSync(path.join(root, '.git'))
182
+ }
183
+
184
+ // The run is asked first, so a `josh start` whose setup has not written `package.json` yet reads it.
185
+ function has_workflow(root: string): boolean {
186
+ return workflow_mark.is_requested() || workflow_mark.is_recorded(read_manifest(root))
187
+ }
188
+
167
189
  function inspect_project(root: string, requested?: ProjectProfile): ProjectShape {
168
190
  const profile = resolve_profile(root, requested)
169
191
  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) }
192
+ const is_git = has_git(root)
193
+
194
+ return {
195
+ ...profile,
196
+ ...files,
197
+ has_git: is_git,
198
+ has_github: is_git && has_github_remote(root),
199
+ has_workflow: has_workflow(root),
200
+ }
173
201
  }
174
202
 
175
203
  const project_profile = {
176
204
  parse_profile,
177
205
  resolve_profile,
206
+ has_git,
178
207
  inspect_project,
179
208
  requested_profile,
180
209
  with_legacy_profile_names,
@@ -1,6 +1,6 @@
1
1
  import { git_gh_exec } from '#scripts/gh/git-gh-exec'
2
2
  import { git_location_environment } from '#scripts/git/git-location-environment'
3
- import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
3
+ import { git_spawn_sync, type GitSyncOptions } from '#scripts/git/git-spawn-sync'
4
4
  import { COMMAND_TIMEOUT_MS, SUITE_TIMEOUT_MS } from '#scripts/lib/timeouts'
5
5
  import { execaSync } from 'execa'
6
6
  import type { Visibility } from './start-plan'
@@ -16,6 +16,8 @@ import type { Visibility } from './start-plan'
16
16
  // project's own commit and push hooks, which may run its whole suite, so it takes the suite budget:
17
17
  // long enough for that, short enough that a push that never answers ends the run.
18
18
  const SUCCESS_EXIT_CODE = 0
19
+ const GIT_DIRECTORY = '.git'
20
+ const IDENTITY_VARIABLES = ['GIT_AUTHOR_IDENT', 'GIT_COMMITTER_IDENT']
19
21
 
20
22
  function root_environment(): Record<string, string | undefined> {
21
23
  return git_location_environment.location_free_environment()
@@ -29,6 +31,29 @@ function git_read(args: ReadonlyArray<string>, root: string): string | undefined
29
31
  return git_spawn_sync.read(args, { cwd: root, env: root_environment() })
30
32
  }
31
33
 
34
+ // Asked of git itself, so the answer is the one its commit would reach: config, the environment and
35
+ // the identity git can detect all count (#3754).
36
+ //
37
+ // **Only an answer from git counts as a missing identity.** A git that could not be started or never
38
+ // answered is a different failure, which the first step that runs git reports as what it is.
39
+ function is_identity_refused(variable: string, options: GitSyncOptions): boolean {
40
+ const { exit_code } = git_spawn_sync.run(['var', variable], options)
41
+
42
+ return exit_code !== undefined && exit_code !== SUCCESS_EXIT_CODE
43
+ }
44
+
45
+ // A plan that creates the repository asks from where it will be: an identity given per directory
46
+ // (`includeIf "gitdir:…"`) is matched against the git directory, which no `cwd` supplies before
47
+ // `git init` has run. The path stays relative to `root` so git resolves it as its commit will — by
48
+ // the path the user reached the directory through as well as the physical one, which is what a
49
+ // `gitdir:` under a symlinked parent is written against.
50
+ function lacks_git_identity(root: string, is_new_repository: boolean): boolean {
51
+ const location = is_new_repository ? { GIT_DIR: GIT_DIRECTORY } : {}
52
+ const options = { cwd: root, env: { ...root_environment(), ...location } }
53
+
54
+ return IDENTITY_VARIABLES.some((variable) => is_identity_refused(variable, options))
55
+ }
56
+
32
57
  function git_run(args: ReadonlyArray<string>, root: string): void {
33
58
  const options = { cwd: root, env: root_environment(), timeout: SUITE_TIMEOUT_MS }
34
59
  const { exit_code } = git_spawn_sync.run(args, { ...options, stdio: 'inherit' })
@@ -86,6 +111,7 @@ const start_exec = {
86
111
  git_succeeds,
87
112
  git_read,
88
113
  git_run,
114
+ lacks_git_identity,
89
115
  is_gh_installed,
90
116
  is_gh_signed_in,
91
117
  create_github_repository,
@@ -9,6 +9,7 @@ interface StartOptions {
9
9
  is_github: boolean
10
10
  visibility: Visibility
11
11
  init_command: string | undefined
12
+ push_preflight_command: string | undefined
12
13
  }
13
14
 
14
15
  type StartStep = 'git_init' | 'initialize' | 'commit' | 'repository' | 'labels' | 'setup_pr'
@@ -53,9 +54,13 @@ const STEP_LABELS: Readonly<Record<StartStep, string>> = {
53
54
  setup_pr: 'Open an Issue and a pull request that add the kit setup to main',
54
55
  }
55
56
 
56
- // The GitHub write steps `--yes` alone never consents to.
57
+ // The GitHub write steps `--yes` alone never consents to. Each one pushes — main, or the setup branch —
58
+ // so they are also the steps a caller's push preflight guards (#3702).
57
59
  const GITHUB_WRITE_STEPS: ReadonlySet<StartStep> = new Set(['repository', 'setup_pr'])
58
60
 
61
+ // The steps that create a commit, which git refuses without an author identity (#3754).
62
+ const COMMIT_STEPS: ReadonlySet<StartStep> = new Set(['commit', 'setup_pr'])
63
+
59
64
  function refused(refusal: string): StepPlan {
60
65
  return { steps: [], refusal }
61
66
  }
@@ -107,6 +112,14 @@ function plan_steps(state: GitState): StepPlan {
107
112
  return { steps: FULL_STEPS, refusal: undefined }
108
113
  }
109
114
 
115
+ function has_push(steps: ReadonlyArray<StartStep>): boolean {
116
+ return steps.some((step) => GITHUB_WRITE_STEPS.has(step))
117
+ }
118
+
119
+ function has_commit(steps: ReadonlyArray<StartStep>): boolean {
120
+ return steps.some((step) => COMMIT_STEPS.has(step))
121
+ }
122
+
110
123
  function interaction_of(options: StartOptions, is_tty: boolean): Verdict<Interaction> {
111
124
  if (options.is_yes) return { value: 'unattended', refusal: undefined }
112
125
  if (is_tty) return { value: 'interactive', refusal: undefined }
@@ -126,9 +139,7 @@ function github_consent(
126
139
  options: StartOptions,
127
140
  interaction: Interaction,
128
141
  ): Verdict<Consent> {
129
- const is_github_write = steps.some((step) => GITHUB_WRITE_STEPS.has(step))
130
-
131
- if (!is_github_write || options.is_github) return { value: 'granted', refusal: undefined }
142
+ if (!has_push(steps) || options.is_github) return { value: 'granted', refusal: undefined }
132
143
  if (interaction === 'interactive') return { value: 'ask', refusal: undefined }
133
144
 
134
145
  return {
@@ -158,6 +169,17 @@ function progress_report(
158
169
  return `josh start stopped at: ${labels[failed]}\nCompleted: ${done}\nCause: ${cause}`
159
170
  }
160
171
 
172
+ // What a run prints when the caller's push preflight fails: no step has run yet, so nothing changed.
173
+ function preflight_report(command: string, cause: string): string {
174
+ return `josh start stopped before its first step: the push preflight (${command}) failed. Nothing was changed.\nCause: ${cause}`
175
+ }
176
+
177
+ // What a run prints when the scanner the pre-push audit needs could not be put in place: every push
178
+ // the plan makes would be refused by that hook, so it stops while nothing has changed (#3755).
179
+ function scanner_report(cause: string): string {
180
+ return `josh start stopped before its first step: osv-scanner, which the pre-push audit needs, is not installed and could not be fetched. Nothing was changed.\nCause: ${cause}\nInstall osv-scanner (https://google.github.io/osv-scanner/) or restore the network, then run josh start again.`
181
+ }
182
+
161
183
  function plan_summary(
162
184
  steps: ReadonlyArray<StartStep>,
163
185
  labels: Readonly<Record<StartStep, string>> = STEP_LABELS,
@@ -169,9 +191,13 @@ function plan_summary(
169
191
 
170
192
  const start_plan = {
171
193
  plan_steps,
194
+ has_push,
195
+ has_commit,
172
196
  interaction_of,
173
197
  github_consent,
174
198
  progress_report,
199
+ preflight_report,
200
+ scanner_report,
175
201
  plan_summary,
176
202
  step_labels,
177
203
  STEP_LABELS,
@@ -2,6 +2,8 @@ import path from 'node:path'
2
2
  import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
3
3
  import { error_text } from '#scripts/lib/error-message'
4
4
  import { repository_labels } from '#scripts/repo/repository-labels'
5
+ import { security_audit } from '#scripts/security/security-audit'
6
+ import { security_audit_provision } from '#scripts/security/security-audit-provision'
5
7
  import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
6
8
  import { parseCommandString } from 'execa'
7
9
  import { main as init_main } from './init'
@@ -12,12 +14,14 @@ import type { ProjectProfile, ProjectShape } from './project-profile'
12
14
  import { start_exec } from './start-exec'
13
15
  import { start_plan, type GitState, type StartStep, type Visibility } from './start-plan'
14
16
  import { start_setup_pr } from './start-setup-pr'
17
+ import { workflow_mark } from './workflow-mark'
15
18
 
16
19
  interface StepContext {
17
20
  root: string
18
21
  profile: ProjectProfile
19
22
  visibility: Visibility
20
23
  init_command: string | undefined
24
+ push_preflight_command: string | undefined
21
25
  }
22
26
 
23
27
  interface RunContext extends StepContext {
@@ -28,6 +32,12 @@ const GH_INSTALL_HINT =
28
32
  'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/, run gh auth login, then run josh start again. Nothing was changed.'
29
33
  const GH_AUTH_HINT =
30
34
  'GitHub CLI is not signed in. Run gh auth login, then run josh start again. Nothing was changed.'
35
+ const GIT_IDENTITY_HINT =
36
+ 'Git has no author identity, which the commit josh start creates needs. Run git config --global user.name "Your Name" and git config --global user.email "you@example.com", then run josh start again. Nothing was changed.'
37
+ // Printed before the scanner is looked for: a first run downloads tens of megabytes, and a terminal
38
+ // that says nothing for minutes reads as a hang the user interrupts (#3755).
39
+ const SCANNER_NOTICE =
40
+ 'Checking for osv-scanner, which the pre-push audit needs. A first run downloads it, which can take a few minutes on a slow connection.'
31
41
 
32
42
  const { git_succeeds, git_read, run_local } = start_exec
33
43
 
@@ -62,6 +72,17 @@ function github_cli_refusal(root: string): string | undefined {
62
72
  return undefined
63
73
  }
64
74
 
75
+ // A machine Git was just installed on has no author identity, and the commit that needs one comes
76
+ // after the repository and kit's files are written. It is asked for before the first write, and only
77
+ // when the plan commits — a re-run with nothing left to commit is not stopped by it (#3754).
78
+ function git_identity_refusal(steps: ReadonlyArray<StartStep>, root: string): string | undefined {
79
+ if (!start_plan.has_commit(steps)) return undefined
80
+
81
+ return start_exec.lacks_git_identity(root, steps.includes('git_init'))
82
+ ? GIT_IDENTITY_HINT
83
+ : undefined
84
+ }
85
+
65
86
  // `josh init` refuses to run inside the kit source repository by printing and setting the exit code
66
87
  // rather than throwing, so its step would pass and the later steps would commit and publish the
67
88
  // source repository. The same check is raised before the first write instead.
@@ -69,6 +90,13 @@ function self_run_refusal(root: string): string | undefined {
69
90
  return self_sync_guard.self_sync_refusal(PACKAGE_DIR, root)
70
91
  }
71
92
 
93
+ // A caller's command line runs without a shell, split the way execa splits one.
94
+ function run_command_line(command_line: string, extra: ReadonlyArray<string>, root: string): void {
95
+ const [command = '', ...args] = parseCommandString(command_line)
96
+
97
+ run_local(command, [...args, ...extra], root)
98
+ }
99
+
72
100
  function create_git_repository(context: StepContext): void {
73
101
  initial_commit.create_repository(context.root)
74
102
  }
@@ -77,16 +105,17 @@ function create_git_repository(context: StepContext): void {
77
105
  // A toolkit that layers its files over kit's (`josh-app init`) names its own setup instead, so every
78
106
  // other step and guard stays here rather than in a copy of this command (#2872). That command runs
79
107
  // without a shell, is given the same profile, and a non-zero exit stops the run before the commit.
108
+ // Either one is told the project is set up for the GitHub Issue workflow (`workflow-mark.ts`).
80
109
  function initialize(context: StepContext): void {
110
+ workflow_mark.request()
111
+
81
112
  if (context.init_command === undefined) {
82
113
  init_main(['--profile', context.profile])
83
114
 
84
115
  return
85
116
  }
86
117
 
87
- const [command = '', ...args] = parseCommandString(context.init_command)
88
-
89
- run_local(command, [...args, '--profile', context.profile], context.root)
118
+ run_command_line(context.init_command, ['--profile', context.profile], context.root)
90
119
  }
91
120
 
92
121
  function commit_all(context: StepContext): void {
@@ -122,6 +151,47 @@ function baseline_of(
122
151
  return start_setup_pr.changed_paths(context.root)
123
152
  }
124
153
 
154
+ // A toolkit's own pre-push hook can need something kit knows nothing about (app-kit's needs a Docker
155
+ // daemon), and a push it refuses lands after `gh repo create` has made the repository. So the caller
156
+ // names a check that runs only when the plan pushes, before the first step: a run that does not push
157
+ // is never stopped by it, and one it stops has written nothing (#3702).
158
+ function run_push_preflight(steps: ReadonlyArray<StartStep>, context: StepContext): void {
159
+ const command_line = context.push_preflight_command
160
+
161
+ if (command_line === undefined || !start_plan.has_push(steps)) return
162
+
163
+ console.info(`\nPush preflight: ${command_line}`)
164
+
165
+ try {
166
+ run_command_line(command_line, [], context.root)
167
+ } catch (error) {
168
+ const cause = error_text.message_of(error)
169
+
170
+ throw new Error(start_plan.preflight_report(command_line, cause), { cause: error })
171
+ }
172
+ }
173
+
174
+ // The `full` profile installs kit's pre-push hook, whose audit refuses a push without osv-scanner. The
175
+ // scanner is otherwise fetched at an agent's session start, which a first `josh start` from a terminal
176
+ // has not had, so it is fetched here — forced, because a person asked for this run. A scanner that
177
+ // still cannot be found stops the run before the first step rather than at the push (#3755).
178
+ async function ensure_audit_scanner(
179
+ steps: ReadonlyArray<StartStep>,
180
+ context: StepContext,
181
+ ): Promise<void> {
182
+ if (context.profile !== 'full' || !start_plan.has_push(steps)) return
183
+
184
+ const { platform, arch } = process
185
+
186
+ console.info(`\n${SCANNER_NOTICE}`)
187
+ const outcome = await security_audit_provision.report(context.root, platform, arch, true)
188
+
189
+ console.info(outcome)
190
+ if (security_audit.resolve_scanner(context.root, platform) !== undefined) return
191
+
192
+ throw new Error(start_plan.scanner_report(outcome))
193
+ }
194
+
125
195
  type StepAction = (context: RunContext) => void | Promise<void>
126
196
 
127
197
  const STEP_ACTIONS: Readonly<Record<StartStep, StepAction>> = {
@@ -150,6 +220,9 @@ async function run_step(
150
220
 
151
221
  async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext): Promise<void> {
152
222
  const labels = start_plan.step_labels(context.init_command)
223
+
224
+ run_push_preflight(steps, context)
225
+ await ensure_audit_scanner(steps, context)
153
226
  const run_context: RunContext = { ...context, baseline: baseline_of(steps, context) }
154
227
 
155
228
  for (const [index, step] of steps.entries()) {
@@ -164,6 +237,7 @@ async function run_steps(steps: ReadonlyArray<StartStep>, context: StepContext):
164
237
  const start_steps = {
165
238
  read_git_state,
166
239
  github_cli_refusal,
240
+ git_identity_refusal,
167
241
  self_run_refusal,
168
242
  run_steps,
169
243
  }
@@ -15,8 +15,9 @@ import { start_steps } from './start-steps'
15
15
  const ARGUMENT_START_INDEX = 2
16
16
  const READY_MESSAGE = 'GitHub workflow ready. Use kickoff new in your assistant to plan an Issue.'
17
17
  const START_USAGE =
18
- 'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>]'
18
+ 'josh start [--profile basic|full] [--yes] [--github] [--public] [--init-command <command>] [--push-preflight-command <command>]'
19
19
  const INIT_COMMAND_FLAG = '--init-command'
20
+ const PUSH_PREFLIGHT_COMMAND_FLAG = '--push-preflight-command'
20
21
  const YES_FLAG = '--yes'
21
22
  const GITHUB_FLAG = '--github'
22
23
  const PUBLIC_FLAG = '--public'
@@ -28,34 +29,38 @@ interface Prepared {
28
29
  choices: Choices
29
30
  }
30
31
 
31
- interface InitCommandSplit {
32
- init_command: string | undefined
32
+ interface CommandSplit {
33
+ command: string | undefined
33
34
  rest: ReadonlyArray<string>
34
35
  }
35
36
 
36
37
  // A value that is blank or another flag means the command itself was left out, which would otherwise
37
- // swallow the next switch or hand the initialize step nothing to run.
38
+ // swallow the next switch or hand its step nothing to run.
38
39
  function is_command_value(value: string | undefined): value is string {
39
40
  return value !== undefined && value.trim() !== '' && !value.startsWith('-')
40
41
  }
41
42
 
42
43
  // The command arrives as the one argument after its flag and is removed together with it.
43
- function split_init_command(args: ReadonlyArray<string>): InitCommandSplit {
44
- const index = args.indexOf(INIT_COMMAND_FLAG)
44
+ function split_command(args: ReadonlyArray<string>, flag: string): CommandSplit {
45
+ const index = args.indexOf(flag)
45
46
 
46
- if (index === -1) return { init_command: undefined, rest: args }
47
+ if (index === -1) return { command: undefined, rest: args }
47
48
 
48
49
  const value = args[index + 1]
49
50
 
50
51
  if (!is_command_value(value)) throw new Error(`Usage: ${START_USAGE}`)
51
52
 
52
- return { init_command: value, rest: args.filter((_, at) => at !== index && at !== index + 1) }
53
+ return { command: value, rest: args.filter((_, at) => at !== index && at !== index + 1) }
53
54
  }
54
55
 
55
- // The switches and the initialize command are peeled off first, so what remains is exactly the
56
+ // The switches and the caller's commands are peeled off first, so what remains is exactly the
56
57
  // `--profile` pair `josh init` accepts — one parser for the profile in both commands.
57
58
  function parse_start_options(args: ReadonlyArray<string>): StartOptions {
58
- const { init_command, rest } = split_init_command(args)
59
+ const init = split_command(args, INIT_COMMAND_FLAG)
60
+ const { command: push_preflight_command, rest } = split_command(
61
+ init.rest,
62
+ PUSH_PREFLIGHT_COMMAND_FLAG,
63
+ )
59
64
  const profile_args = rest.filter((argument) => !SWITCHES.has(argument))
60
65
 
61
66
  return {
@@ -63,7 +68,8 @@ function parse_start_options(args: ReadonlyArray<string>): StartOptions {
63
68
  is_yes: rest.includes(YES_FLAG),
64
69
  is_github: rest.includes(GITHUB_FLAG),
65
70
  visibility: rest.includes(PUBLIC_FLAG) ? 'public' : 'private',
66
- init_command,
71
+ init_command: init.command,
72
+ push_preflight_command,
67
73
  }
68
74
  }
69
75
 
@@ -100,6 +106,7 @@ function prepare(args: ReadonlyArray<string>, is_tty: boolean, root: string): Pr
100
106
  const consent: Consent = settle(start_plan.github_consent(plan.steps, options, interaction))
101
107
 
102
108
  stop_on(start_steps.github_cli_refusal(root))
109
+ stop_on(start_steps.git_identity_refusal(plan.steps, root))
103
110
 
104
111
  return {
105
112
  steps: plan.steps,
@@ -112,12 +119,13 @@ async function main(args: ReadonlyArray<string>, is_tty: boolean): Promise<void>
112
119
  const { steps, interaction, choices } = prepare(args, is_tty, process.cwd())
113
120
 
114
121
  const { root, options } = choices
115
- const { visibility, init_command } = options
122
+ const { visibility, init_command, push_preflight_command } = options
116
123
 
117
124
  console.info(start_plan.plan_summary(steps, start_plan.step_labels(init_command)))
118
125
  const profile = await resolve_profile(choices, interaction)
126
+ const context = { root, profile, visibility, init_command, push_preflight_command }
119
127
 
120
- await start_steps.run_steps(steps, { root, profile, visibility, init_command })
128
+ await start_steps.run_steps(steps, context)
121
129
  console.info(`\n${READY_MESSAGE}`)
122
130
  }
123
131