@joshuafolkken/kit 1.1094.0 → 1.1128.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 (149) hide show
  1. package/.claude/skills/workflow-commands/SKILL.md +1 -1
  2. package/.claude/skills/workflow-commands/background-commands.md +36 -85
  3. package/.claude/skills/workflow-commands/backlogrun-progress.md +29 -32
  4. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -2
  5. package/.claude/skills/workflow-commands/fullrun.md +5 -4
  6. package/.claude/skills/workflow-commands/halfrun.md +5 -4
  7. package/.claude/skills/workflow-commands/latest-gate.md +7 -15
  8. package/.claude/skills/workflow-commands/pre-gate-cut.md +4 -8
  9. package/.claude/skills/workflow-commands/split-assessment.md +4 -18
  10. package/.github/actions/setup-node/action.yml +33 -0
  11. package/.github/actions/setup-pnpm/action.yml +4 -4
  12. package/.github/workflows/auto-tag.yml +2 -3
  13. package/.github/workflows/ci.yml +7 -8
  14. package/.github/workflows/dependabot-auto-merge.yml +1 -0
  15. package/.github/workflows/pr-classification.yml +1 -3
  16. package/.github/workflows/production.yml +1 -0
  17. package/.github/workflows/publish.yml +10 -9
  18. package/.github/workflows/sonar-qube.yml +17 -13
  19. package/.pnpmfile.mjs +29 -0
  20. package/AGENTS.md +4 -11
  21. package/CLAUDE.md +24 -17
  22. package/GEMINI.md +4 -13
  23. package/README.md +8 -3
  24. package/SECURITY.md +1 -0
  25. package/cspell/index.yaml +4 -0
  26. package/dist/CLAUDE.basic.md +6 -6
  27. package/dist/CLAUDE.md +24 -17
  28. package/dist/CLAUDE.static.md +6 -6
  29. package/dist/config-merge/index.js +33 -9
  30. package/dist/hooks/batch-guard.js +4 -4
  31. package/dist/hooks/{chunk-NKOG3XAV.js → chunk-4JZDP5X5.js} +2 -2
  32. package/dist/hooks/{chunk-3C4YCCEF.js → chunk-5SD2YM7Z.js} +4 -4
  33. package/dist/hooks/{chunk-OIZUHPJI.js → chunk-77J7IEQM.js} +1 -1
  34. package/dist/hooks/{chunk-KHWHKJOM.js → chunk-7HVB7SZG.js} +3 -3
  35. package/dist/hooks/{chunk-UD24EAIL.js → chunk-AIOIOMPX.js} +1875 -104
  36. package/dist/hooks/{chunk-M2XDCUKD.js → chunk-M7QKEKWO.js} +88 -1783
  37. package/dist/hooks/{chunk-E54KGFEQ.js → chunk-MKVGBQPA.js} +1 -1
  38. package/dist/hooks/{chunk-7ZPFAVR7.js → chunk-NXYMNPUQ.js} +3 -3
  39. package/dist/hooks/{chunk-O4LL4HTI.js → chunk-QVLT52OV.js} +2 -2
  40. package/dist/hooks/{chunk-IL27YTPH.js → chunk-XYUIQOW3.js} +2 -2
  41. package/dist/hooks/codex-hook-adapter.js +12 -47
  42. package/dist/hooks/duplicate-read-guard.js +5 -5
  43. package/dist/hooks/format-edited.js +2 -2
  44. package/dist/hooks/inputs.json +180 -179
  45. package/dist/hooks/investigation-guard.js +5 -5
  46. package/dist/hooks/pretool-guard.js +9 -9
  47. package/dist/hooks/stop-guard.js +17 -12
  48. package/dist/josh.js +79 -30
  49. package/dist/version/index.js +36 -20
  50. package/eslint/rules/unicorn.js +3 -2
  51. package/package.json +18 -2
  52. package/pnpm-workspace.yaml +5 -7
  53. package/prompts/collaboration-workflow/glossary.md +17 -0
  54. package/prompts/collaboration-workflow/issue-template.md +1 -1
  55. package/prompts/collaboration-workflow/operating-rules.md +1 -1
  56. package/prompts/collaboration-workflow/principles.md +9 -0
  57. package/prompts/collaboration-workflow.md +1 -0
  58. package/scripts/backlog/backlog-arrival.ts +30 -10
  59. package/scripts/backlog/backlog-budget-cli.ts +15 -16
  60. package/scripts/backlog/backlog-offer-cli.ts +6 -14
  61. package/scripts/backlog/backlog-plan.ts +29 -18
  62. package/scripts/backlog/backlog-ready.ts +20 -7
  63. package/scripts/config-merge/parse-jsonc.ts +33 -3
  64. package/scripts/config-merge/patch-json-key.ts +13 -6
  65. package/scripts/cost-runtime/cost-cli.ts +3 -7
  66. package/scripts/doctor/doctor-consumer.ts +3 -5
  67. package/scripts/doctor/doctor-io.ts +9 -13
  68. package/scripts/document/backlogrun-parent-read-set.ts +12 -1
  69. package/scripts/document/document-byte-budget.ts +1 -0
  70. package/scripts/document/entry-read-budget.ts +13 -7
  71. package/scripts/document/entry-read-set.ts +32 -8
  72. package/scripts/document/lane-child-read-set.ts +7 -0
  73. package/scripts/document/read-set-point-of-use.ts +8 -3
  74. package/scripts/document/read-set-trim.ts +3 -1
  75. package/scripts/epic/epic-audit.ts +3 -18
  76. package/scripts/gh/git-gh-issue-list.ts +12 -11
  77. package/scripts/gh/git-gh-issue-write.ts +4 -1
  78. package/scripts/gh/git-gh-pr-auto-merge.ts +4 -1
  79. package/scripts/git/git-command.ts +6 -1
  80. package/scripts/git/git-spawn-sync.ts +65 -0
  81. package/scripts/git/git-spawn.ts +5 -4
  82. package/scripts/git/stash/stash-pop-args.ts +1 -6
  83. package/scripts/init/init-file-action.ts +54 -0
  84. package/scripts/init/init-logic-migrate.ts +35 -17
  85. package/scripts/init/init-logic-secretlint.ts +1 -1
  86. package/scripts/init/init-logic.ts +6 -22
  87. package/scripts/init/init.ts +11 -68
  88. package/scripts/init/kit-written-paths.ts +2 -0
  89. package/scripts/init/project-profile.ts +3 -6
  90. package/scripts/init/start-steps.ts +2 -1
  91. package/scripts/issue/defect-rate-cli.ts +61 -20
  92. package/scripts/issue/issue-backlinks.ts +7 -14
  93. package/scripts/issue/issue-cite.ts +12 -6
  94. package/scripts/issue/issue-file.ts +5 -12
  95. package/scripts/issue/markdown-section.ts +21 -6
  96. package/scripts/issue/measure-rerun-cli.ts +73 -14
  97. package/scripts/josh/josh-commands-project.ts +1 -1
  98. package/scripts/josh/josh-commands-workflow.ts +1 -1
  99. package/scripts/josh/run-github-prerequisite.ts +3 -6
  100. package/scripts/lane/lane-await.ts +43 -13
  101. package/scripts/lane/lane-close.ts +4 -1
  102. package/scripts/lane/lane-launch-cli.ts +10 -16
  103. package/scripts/lane/openai-review-broker.ts +4 -1
  104. package/scripts/lib/cli-flags.ts +31 -2
  105. package/scripts/lib/error-message.ts +12 -1
  106. package/scripts/lines/line-targets.ts +0 -11
  107. package/scripts/lines/lines-command.ts +5 -3
  108. package/scripts/propagate/propagate-git.ts +3 -8
  109. package/scripts/refactor/refactor-scan.ts +2 -1
  110. package/scripts/registry-migration/migrate-logic.ts +28 -19
  111. package/scripts/registry-migration/migrate.ts +6 -5
  112. package/scripts/release/github-release-cli.ts +14 -3
  113. package/scripts/release/release-publish.ts +36 -2
  114. package/scripts/release/release-worktree.ts +97 -2
  115. package/scripts/review/review-checkout.ts +3 -2
  116. package/scripts/review/review-record-cli.ts +9 -16
  117. package/scripts/rules/early-heartbeat.ts +4 -3
  118. package/scripts/rules/lane-park.ts +9 -0
  119. package/scripts/rules/stop-rules.ts +7 -4
  120. package/scripts/run/run-carry-args.ts +8 -12
  121. package/scripts/run/run-cut-args.ts +1 -6
  122. package/scripts/run/run-ending-cli.ts +1 -5
  123. package/scripts/run/run-entry-cli.ts +40 -9
  124. package/scripts/run/run-entry-stop.ts +104 -0
  125. package/scripts/run/run-event-stream-emit.ts +7 -2
  126. package/scripts/run/run-hold.ts +4 -1
  127. package/scripts/run/run-liveness-cli.ts +1 -5
  128. package/scripts/run/run-merge-cli.ts +2 -10
  129. package/scripts/run/run-parent-cut-hook.ts +4 -1
  130. package/scripts/run/run-prep-cli.ts +4 -1
  131. package/scripts/run/run-progress-args.ts +20 -4
  132. package/scripts/run/run-progress-cli.ts +31 -29
  133. package/scripts/run/run-progress-clock.ts +4 -1
  134. package/scripts/run/run-ship-cli.ts +8 -16
  135. package/scripts/run/run-ship-probe.ts +4 -1
  136. package/scripts/run/run-status-cli.ts +2 -15
  137. package/scripts/run/run-tidy-lanes.ts +4 -1
  138. package/scripts/run/run-wake-cli.ts +2 -10
  139. package/scripts/run/run-watcher-guard.ts +9 -5
  140. package/scripts/safe-chain/pack-hook.ts +92 -0
  141. package/scripts/sync/sync-ai-files.ts +4 -0
  142. package/scripts/test/test-declared-changed.ts +13 -16
  143. package/scripts/test/test-declared.ts +3 -7
  144. package/scripts/version/version-commands.ts +11 -1
  145. package/templates/CLAUDE.basic.md +6 -6
  146. package/templates/workflows/ci.yml +16 -82
  147. package/templates/workflows/dependabot-auto-merge.yml +1 -0
  148. package/templates/workflows/github-release.yml +1 -3
  149. package/tsconfig.sonar.json +4 -22
@@ -420,16 +420,40 @@ function point_of_use_cost(
420
420
  return { file, heading: '', cost: cost_of(read_document(root, file)), is_resolved: true }
421
421
  }
422
422
 
423
- function point_of_use_costs(root: string, entry: string): Array<SectionCost> {
424
- const found = document_section.section(read_document(root, SKILL_FILE), TABLE_SECTION)
425
- const references = all_matches(found?.text ?? '', SECTION_REFERENCE).map((match) =>
426
- to_reference(match),
427
- )
423
+ // **What an entry's path names: its own manifests, and the §1 trigger rows that name the entry**
424
+ // (joshuafolkken/kit#3078). `SKILL.md`'s other text is left out for the reason `sections_for` gives —
425
+ // §2 names every document, so counting it would charge a `kickoff` for `followup.md`. One hop only:
426
+ // the point-of-use documents cite one another, and following them would reach the whole set again.
427
+ // `also` is what a role reaches beside its base entry's path (`read-set-trim.ts`).
428
+ function cited_by(
429
+ root: string,
430
+ entry: string,
431
+ table_text: string,
432
+ also: ReadonlySet<string>,
433
+ ): Set<string> {
434
+ const manifests = files_for(root, entry).filter((file) => file !== SKILL_FILE)
435
+ const rows = table_text
436
+ .split('\n')
437
+ .filter((line) => line.startsWith('|') && line.includes(`\`${entry}\``))
438
+ const texts = [...manifests.map((file) => read_document(root, file)), ...rows]
439
+
440
+ return new Set([...also, ...texts.flatMap((text) => names_in(text))])
441
+ }
442
+
443
+ function point_of_use_costs(
444
+ root: string,
445
+ entry: string,
446
+ also: ReadonlySet<string>,
447
+ ): Array<SectionCost> {
448
+ const table_text =
449
+ document_section.section(read_document(root, SKILL_FILE), TABLE_SECTION)?.text ?? ''
450
+ const references = all_matches(table_text, SECTION_REFERENCE).map((match) => to_reference(match))
451
+ const cited = cited_by(root, entry, table_text, also)
428
452
 
429
- return point_of_use_files(entry).map((file) => point_of_use_cost(root, file, references))
453
+ return point_of_use_files(entry, cited).map((file) => point_of_use_cost(root, file, references))
430
454
  }
431
455
 
432
- function costed(root: string, entry: string): ReadSetCost {
456
+ function costed(root: string, entry: string, also: ReadonlySet<string> = new Set()): ReadSetCost {
433
457
  const { files, sections } = read_set(root, entry)
434
458
  const own_costs = file_costs(root, files)
435
459
  const section_costs = sections.map((reference) => section_cost(root, reference))
@@ -439,7 +463,7 @@ function costed(root: string, entry: string): ReadSetCost {
439
463
  entry,
440
464
  files: own_costs,
441
465
  sections: section_costs,
442
- point_of_use: point_of_use_costs(root, entry),
466
+ point_of_use: point_of_use_costs(root, entry, also),
443
467
  bash_output_cap: bash_output_cap_reader.bash_output_cap(root),
444
468
  whole: total([own, referenced_cost(root, sections)]),
445
469
  scoped: total([own, scoped_cost(root, sections)]),
@@ -64,17 +64,24 @@ const SKIPPED_POINT_OF_USE: ReadonlySet<string> = new Set([
64
64
  'retrospective.md',
65
65
  ])
66
66
 
67
+ // **A child that parks reads `backlogrun-park.md`** (`SKILL.md` §1, "A lane child that parks"), a
68
+ // document a standalone `fullrun`'s path never names — so the child's reach adds it to its base
69
+ // entry's (joshuafolkken/kit#3078).
70
+ const REACHED_POINT_OF_USE: ReadonlySet<string> = new Set(['backlogrun-park.md'])
71
+
67
72
  function costed(root: string): ReadSetCost {
68
73
  return read_set_trim.costed(root, {
69
74
  base_entry: FULLRUN,
70
75
  label: LANE_CHILD,
71
76
  unused_skill_sections: UNUSED_SKILL_SECTIONS,
72
77
  skipped_point_of_use: SKIPPED_POINT_OF_USE,
78
+ reached_point_of_use: REACHED_POINT_OF_USE,
73
79
  })
74
80
  }
75
81
 
76
82
  const lane_child_read_set = {
77
83
  LANE_CHILD,
84
+ REACHED_POINT_OF_USE,
78
85
  SKIPPED_POINT_OF_USE,
79
86
  UNUSED_SKILL_SECTIONS,
80
87
  costed,
@@ -75,9 +75,14 @@ function is_point_of_use(entry: string, file: string): boolean {
75
75
  return (POINT_OF_USE_BY_ENTRY.get(entry) ?? NO_PER_ENTRY_FILES).has(file)
76
76
  }
77
77
 
78
- // The global point-of-use files plus the ones this entry names in its own row.
79
- function point_of_use_files(entry: string): Array<string> {
80
- return [...POINT_OF_USE_FILES, ...(POINT_OF_USE_BY_ENTRY.get(entry) ?? NO_PER_ENTRY_FILES)]
78
+ // **Only the global files the entry reaches, plus the ones this entry names in its own row**
79
+ // (joshuafolkken/kit#3078). `cited` is every document the entry's path names; a global file outside it
80
+ // is one this entry never opens — a `kickoff` never issues `followup` — so charging it made every
81
+ // entry's total the same figure and the per-entry ratchet measured nothing.
82
+ function point_of_use_files(entry: string, cited: ReadonlySet<string>): Array<string> {
83
+ const reached = [...POINT_OF_USE_FILES].filter((file) => cited.has(file))
84
+
85
+ return [...reached, ...(POINT_OF_USE_BY_ENTRY.get(entry) ?? NO_PER_ENTRY_FILES)]
81
86
  }
82
87
 
83
88
  const read_set_point_of_use = {
@@ -24,6 +24,8 @@ interface TrimSpec {
24
24
  unused_skill_sections: ReadonlyArray<string>
25
25
  // The point-of-use documents this role's parent owns; empty when the role owns them all.
26
26
  skipped_point_of_use: ReadonlySet<string>
27
+ // The point-of-use documents this role reaches that its base entry's path does not name.
28
+ reached_point_of_use: ReadonlySet<string>
27
29
  }
28
30
 
29
31
  function subtract(left: Cost, right: Cost): Cost {
@@ -59,7 +61,7 @@ function reduce_skill(file: FileCost, saving: Cost): FileCost {
59
61
  // **`whole` and `scoped` both carry the own-files cost identically**, so the `SKILL.md` saving lands
60
62
  // on each by the same amount — there is no need to re-derive the referenced-section split.
61
63
  function costed(root: string, spec: TrimSpec): ReadSetCost {
62
- const base = entry_read_set.costed(root, spec.base_entry)
64
+ const base = entry_read_set.costed(root, spec.base_entry, spec.reached_point_of_use)
63
65
  const saving = unused_skill_cost(root, spec.unused_skill_sections)
64
66
 
65
67
  return {
@@ -1,3 +1,4 @@
1
+ import { markdown_section } from '#scripts/issue/markdown-section'
1
2
  import { epic_graph, type EpicChild, type IssueReference } from './epic-graph'
2
3
 
3
4
  // Reading an epic's children against each other.
@@ -43,7 +44,6 @@ const ACCEPTANCE_HEADINGS: ReadonlySet<string> = new Set([
43
44
  '## 受け入れ条件',
44
45
  '## Acceptance criteria',
45
46
  ])
46
- const HEADING_PREFIX = '## '
47
47
 
48
48
  type FindingLevel = 'error' | 'warning'
49
49
 
@@ -169,26 +169,11 @@ function is_acceptance_heading(line: string): boolean {
169
169
  return ACCEPTANCE_HEADINGS.has(line.trim())
170
170
  }
171
171
 
172
- function is_heading(line: string): boolean {
173
- return line.trimStart().startsWith(HEADING_PREFIX)
174
- }
175
-
176
- // The lines under the acceptance-criteria heading, up to the next `##`. Shaped like the other
177
- // section readers in this repository, for the same reason: a regex spanning the block backtracks.
178
- function section_lines(lines: ReadonlyArray<string>): Array<string> {
179
- const start = lines.findIndex((line) => is_acceptance_heading(line))
180
- if (start === -1) return []
181
- const rest = lines.slice(start + 1)
182
- const end = rest.findIndex((line) => is_heading(line))
183
-
184
- return end === -1 ? rest : rest.slice(0, end)
185
- }
186
-
187
- // The acceptance-criteria section of a child's body, or an empty string.
172
+ // The acceptance-criteria section of a child's body, up to the next unfenced `##`, or an empty string.
188
173
  function acceptance_section(body: string | undefined): string {
189
174
  if (body === undefined) return ''
190
175
 
191
- return section_lines(body.split('\n')).join('\n')
176
+ return markdown_section.section_lines_matching(body, is_acceptance_heading).join('\n')
192
177
  }
193
178
 
194
179
  // Everything `node` depends on, directly or through a chain. Walked iteratively so a graph that
@@ -1,3 +1,4 @@
1
+ import { bounded_pool } from '#scripts/lib/bounded-pool'
1
2
  import { git_gh_api_path } from './git-gh-api-path'
2
3
  import { git_gh_exec } from './git-gh-exec'
3
4
  import { read_blocked_by } from './git-gh-issue-read'
@@ -198,21 +199,21 @@ async function fetch_selected(request: IssueListRequest): Promise<SelectedRows>
198
199
  // issue GitHub itself reports as unblocked costs no request. A backlog with no declared blockers
199
200
  // therefore costs exactly what `gh` cost — one listing — rather than one request per row.
200
201
  //
201
- // Sequential rather than a burst: the only caller that asks for the field is the `auto-ok` pickup,
202
- // whose listing is already narrowed by a label, and a rate limit reached in parallel would fail rows
203
- // that a paced pass reads.
202
+ // Bounded rather than sequential (joshuafolkken/kit#3103): the pass was paced one request at a time,
203
+ // which made every `backlog:plan` wait about 0.7 s per opted-in issue declaring a blocker. The width
204
+ // is the one `epic:bundle` already reads the same endpoint at, so the burst stays bounded while a
205
+ // rate-limited read still fails the whole listing rather than a row (`bounded-pool.ts`).
206
+ const RELATION_CONCURRENCY = 8
207
+
204
208
  async function read_relations(
205
209
  rows: ReadonlyArray<RestIssue>,
206
210
  repo?: string,
207
211
  ): Promise<Array<BlockedBy>> {
208
- const relations: Array<BlockedBy> = []
209
-
210
- for (const row of rows) {
211
- // eslint-disable-next-line no-await-in-loop -- paced reads, so a rate limit does not fail rows
212
- relations.push(await read_blocked_by(String(row.number), row, repo))
213
- }
214
-
215
- return relations
212
+ return await bounded_pool.bounded_map(
213
+ rows,
214
+ RELATION_CONCURRENCY,
215
+ async (row) => await read_blocked_by(String(row.number), row, repo),
216
+ )
216
217
  }
217
218
 
218
219
  // The listing, plus what it could not cover. `json` is `undefined` — never `'[]'` — when the listing
@@ -1,3 +1,4 @@
1
+ import { error_text } from '#scripts/lib/error-message'
1
2
  import { git_gh_api_path } from './git-gh-api-path'
2
3
  import { git_gh_exec, type GhApiRequest } from './git-gh-exec'
3
4
 
@@ -43,7 +44,9 @@ async function did_write_succeed(write: () => Promise<unknown>): Promise<boolean
43
44
  await write()
44
45
 
45
46
  return true
46
- } catch {
47
+ } catch (error) {
48
+ error_text.trace_swallowed('git_gh_issue_write.did_write_succeed', error)
49
+
47
50
  return false
48
51
  }
49
52
  }
@@ -1,3 +1,4 @@
1
+ import { error_text } from '#scripts/lib/error-message'
1
2
  import { git_gh_api_path } from './git-gh-api-path'
2
3
  import { git_gh_exec } from './git-gh-exec'
3
4
  import { read_pull, require_pr_number } from './git-gh-pr-read'
@@ -40,7 +41,9 @@ async function pr_enable_auto_merge(branch_name: string): Promise<void> {
40
41
  async function pr_is_merged(branch_name: string): Promise<boolean> {
41
42
  try {
42
43
  return git_gh_pr_rest.is_merged(await read_pull(await require_pr_number(branch_name)))
43
- } catch {
44
+ } catch (error) {
45
+ error_text.trace_swallowed('git_gh_pr_auto_merge.pr_is_merged', error)
46
+
44
47
  return false
45
48
  }
46
49
  }
@@ -1,6 +1,8 @@
1
1
  import { PORCELAIN_FLAG, UNTRACKED_FILES_FLAG } from './constants'
2
+ import { create_spawn_error } from './git-execa-error'
2
3
  import { git_push_transport } from './git-push-transport'
3
4
  import { git_spawn } from './git-spawn'
5
+ import { git_spawn_sync } from './git-spawn-sync'
4
6
 
5
7
  async function branch(): Promise<string> {
6
8
  return await git_spawn.read(['rev-parse', '--abbrev-ref', 'HEAD'])
@@ -31,7 +33,10 @@ async function status(directory?: string): Promise<string> {
31
33
  // onto the working directory resolves to nothing, and a digest map built that way collapses to
32
34
  // "every file absent" — which compares *equal* to another such map (joshuafolkken/kit#1241).
33
35
  async function repository_root(): Promise<string> {
34
- return await git_spawn.read(['rev-parse', '--show-toplevel'])
36
+ const result = git_spawn_sync.toplevel()
37
+ if (result.exit_code !== 0) throw create_spawn_error('rev-parse', result.exit_code)
38
+
39
+ return result.stdout.trim()
35
40
  }
36
41
 
37
42
  // The commit this checkout is sitting on, as opposed to `change_base_commit`'s commit a change is
@@ -0,0 +1,65 @@
1
+ import { read_spawn_stderr, read_spawn_stdout } from '#scripts/lib/spawn-exit'
2
+ import { execaSync } from 'execa'
3
+ import { git_utilities } from './constants'
4
+
5
+ // The one place a synchronous git command is spawned from (joshuafolkken/kit#3065) — the sync
6
+ // counterpart of `git-spawn.ts`. Every caller here wants an answer rather than an exception, so a
7
+ // non-zero exit is returned as a result, and every call is bounded: a git that never answers is an
8
+ // unknown the caller already handles, never a hang.
9
+
10
+ const GIT_TIMEOUT_MS = 10_000
11
+ const SUCCESS_EXIT_CODE = 0
12
+ // git translates its messages, so a caller matching stderr would fail under any non-English locale.
13
+ const C_LOCALE: Record<string, string> = { LC_ALL: 'C', LANGUAGE: 'C' }
14
+
15
+ interface GitSyncOptions {
16
+ cwd?: string
17
+ timeout?: number
18
+ env?: Record<string, string | undefined>
19
+ }
20
+
21
+ interface GitSyncResult {
22
+ exit_code: number | undefined
23
+ stdout: string
24
+ stderr: string
25
+ }
26
+
27
+ function run(args: ReadonlyArray<string>, options: GitSyncOptions = {}): GitSyncResult {
28
+ // execa runs the binary directly with an argument array and no `shell` option; the git command and
29
+ // args are internally controlled, never untrusted input. tssecurity:S8705 is a false positive here.
30
+ const result = execaSync(git_utilities.get_git_command_for_spawn(), args, {
31
+ ...options,
32
+ extendEnv: true,
33
+ reject: false,
34
+ timeout: options.timeout ?? GIT_TIMEOUT_MS,
35
+ }) // NOSONAR
36
+
37
+ return {
38
+ exit_code: result.exitCode,
39
+ stdout: read_spawn_stdout(result),
40
+ stderr: read_spawn_stderr(result),
41
+ }
42
+ }
43
+
44
+ // The trimmed output of a command that succeeded, or undefined for any failure.
45
+ function read(args: ReadonlyArray<string>, options: GitSyncOptions = {}): string | undefined {
46
+ const result = run(args, options)
47
+
48
+ return result.exit_code === SUCCESS_EXIT_CODE ? result.stdout.trim() : undefined
49
+ }
50
+
51
+ // The configured `origin` URL, or undefined when the repository has none.
52
+ function origin_url(cwd: string): string | undefined {
53
+ return read(['config', '--get', 'remote.origin.url'], { cwd })
54
+ }
55
+
56
+ // The absolute repository (or worktree) root, asked under the C locale so a caller can tell "not a
57
+ // repository" apart from every other failure by its stderr.
58
+ function toplevel(timeout: number = GIT_TIMEOUT_MS): GitSyncResult {
59
+ return run(['rev-parse', '--show-toplevel'], { env: C_LOCALE, timeout })
60
+ }
61
+
62
+ const git_spawn_sync = { GIT_TIMEOUT_MS, C_LOCALE, run, read, origin_url, toplevel }
63
+
64
+ export { git_spawn_sync }
65
+ export type { GitSyncResult }
@@ -18,10 +18,11 @@ import { git_ssh_keepalive } from './git-ssh-keepalive'
18
18
  // **`git-push-transport.ts` is the one deliberate exception among the command modules** and is not a
19
19
  // second helper of this kind: a push needs a transport-fault retry that no other command has, so it
20
20
  // spawns git itself. The timeout it carries is shared here by `read_remote` / `with_output_remote`. Those two files are the spawn sites of the ordinary command
21
- // modules — the claim stops there. Other parts of this package spawn git for their own purposes
22
- // (`scripts/run/run-progress-clock.ts`, `scripts/propagate/propagate-git.ts`,
23
- // `scripts/doctor/doctor-io.ts`, `scripts/git/git-fixture-workspace.ts`), and an audit of how the git
24
- // binary is resolved has to read those too.
21
+ // modules — the claim stops there. A synchronous caller goes through `git-spawn-sync.ts`, the same
22
+ // binary resolution with a result instead of an exception (joshuafolkken/kit#3065). Other parts of
23
+ // this package spawn git for their own purposes (`scripts/run/run-progress-clock.ts`,
24
+ // `scripts/git/git-fixture-workspace.ts`), and an audit of how the git binary is resolved has to read
25
+ // those too.
25
26
 
26
27
  async function read(arguments_: Array<string>): Promise<string> {
27
28
  const git_cmd = git_utilities.get_git_command_for_spawn()
@@ -21,12 +21,7 @@ interface Request {
21
21
  }
22
22
 
23
23
  function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
24
- return cli_flags.parse_or_undefined({
25
- args: [...argv],
26
- options: OPTIONS,
27
- strict: true,
28
- allowPositionals: true,
29
- })
24
+ return cli_flags.arguments_of(argv, OPTIONS)
30
25
  }
31
26
 
32
27
  // The message is the sole positional and must be a non-empty string: an empty message matches every
@@ -0,0 +1,54 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { FileAction } from './init-actions'
4
+
5
+ const SAMPLE_INDENT_WIDTH = 4
6
+ const SAMPLE_INDENT = ' '.repeat(SAMPLE_INDENT_WIDTH)
7
+
8
+ function write_new_file(action: FileAction, destination_path: string): void {
9
+ mkdirSync(path.dirname(destination_path), { recursive: true })
10
+ writeFileSync(destination_path, action.create())
11
+ console.info(` ✔ created ${action.dest}`)
12
+ }
13
+
14
+ function show_sample(action: FileAction): void {
15
+ console.info(` ⚠ exists ${action.dest} — add manually:`)
16
+ console.info('')
17
+ console.info(action.create().replaceAll(/^/gmu, () => SAMPLE_INDENT))
18
+ }
19
+
20
+ // A file that already holds the sample needs nothing added, so only one that differs gets it shown —
21
+ // for a create-only file (no merge) as for a merge that only migrates a kit-written line (#3069).
22
+ function report_unchanged(action: FileAction, existing: string): void {
23
+ const is_sample_missing = existing !== action.create()
24
+ const should_show_sample =
25
+ action.merge === undefined || action.should_show_sample_when_unchanged === true
26
+
27
+ if (is_sample_missing && should_show_sample) show_sample(action)
28
+ else console.info(` ✔ unchanged ${action.dest}`)
29
+ }
30
+
31
+ function merge_existing_file(action: FileAction, destination_path: string): void {
32
+ const existing = readFileSync(destination_path, 'utf8')
33
+ const merged = action.merge === undefined ? existing : action.merge(existing)
34
+
35
+ if (merged === existing) {
36
+ report_unchanged(action, existing)
37
+
38
+ return
39
+ }
40
+
41
+ writeFileSync(destination_path, merged)
42
+ console.info(` ✔ updated ${action.dest}`)
43
+ }
44
+
45
+ function execute_file_action(action: FileAction, project_root: string): void {
46
+ const destination_path = path.join(project_root, action.dest)
47
+
48
+ if (existsSync(destination_path)) merge_existing_file(action, destination_path)
49
+ else write_new_file(action, destination_path)
50
+ }
51
+
52
+ const init_file_action = { execute_file_action }
53
+
54
+ export { init_file_action }
@@ -4,6 +4,27 @@ const PREINSTALL_KEY = 'preinstall'
4
4
  const JF_PREFIX = 'jf-'
5
5
  const JOSH_PREFIX = 'josh '
6
6
 
7
+ // The plain values kit wrote before its scripts became josh subcommands (#80). `check` is left out:
8
+ // its value is the one `sv create` writes itself, so it cannot be told apart from the project's own.
9
+ const PRE_JOSH_SCRIPT_VALUES: ReadonlyMap<string, string> = new Map([
10
+ ['lint', 'pnpm lint:prettier && pnpm lint:eslint'],
11
+ ['lint:prettier', 'prettier --check .'],
12
+ ['lint:eslint', 'eslint . --cache --cache-strategy content'],
13
+ ['format', 'pnpm format:prettier && pnpm format:eslint'],
14
+ ['format:prettier', 'prettier --write .'],
15
+ ['format:eslint', 'eslint . --fix --cache --cache-strategy content'],
16
+ ['cspell', 'cspell lint --no-must-find-files --no-progress "**/*.{ts,js,md,yaml,yml,json}"'],
17
+ ['cspell:dot', 'pnpm cspell . --dot'],
18
+ ['test:unit', 'vitest run'],
19
+ ['lefthook:install', 'lefthook install'],
20
+ ['lefthook:uninstall', 'lefthook uninstall'],
21
+ ['lefthook:commit', 'lefthook run pre-commit'],
22
+ ['lefthook:push', 'lefthook run pre-push'],
23
+ ['main:sync', 'git checkout main && git pull'],
24
+ ['main:merge', 'git pull origin main'],
25
+ ['check:ci', 'svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --threshold error'],
26
+ ])
27
+
7
28
  const RETIRED_MANAGED_SCRIPTS = new Set<string>([
8
29
  'git',
9
30
  'git:followup',
@@ -18,30 +39,15 @@ const RETIRED_MANAGED_SCRIPTS = new Set<string>([
18
39
  'version:patch',
19
40
  'version:current',
20
41
  'overrides:check',
21
- 'lint',
22
- 'lint:prettier',
23
- 'lint:eslint',
24
- 'format',
25
- 'format:prettier',
26
- 'format:eslint',
27
- 'cspell',
28
- 'cspell:dot',
29
- 'test:unit',
30
42
  'test:e2e',
31
43
  'test',
32
- 'lefthook:install',
33
- 'lefthook:uninstall',
34
- 'lefthook:commit',
35
- 'lefthook:push',
36
- 'main:sync',
37
- 'main:merge',
38
44
  'latest',
39
45
  'latest:corepack',
40
46
  'latest:update',
41
47
  'check',
42
- 'check:ci',
43
48
  'check:svelte',
44
49
  'check:svelte:ci',
50
+ ...PRE_JOSH_SCRIPT_VALUES.keys(),
45
51
  ])
46
52
 
47
53
  function migrate_jf_value(value: string): string {
@@ -62,9 +68,21 @@ function apply_jf_migrations(scripts: Record<string, string>): Record<string, st
62
68
  )
63
69
  }
64
70
 
71
+ function is_kit_written(key: string, value: string): boolean {
72
+ if (value.startsWith(JOSH_PREFIX) || value.startsWith(JF_PREFIX)) return true
73
+
74
+ return PRE_JOSH_SCRIPT_VALUES.get(key) === value
75
+ }
76
+
77
+ // A retired name alone does not make a script kit's: `sv create` writes its own `check`, which must
78
+ // survive `josh init` (joshuafolkken/kit#3069). Only a value kit itself wrote is retired.
79
+ function is_retired_script(key: string, value: string): boolean {
80
+ return RETIRED_MANAGED_SCRIPTS.has(key) && is_kit_written(key, value)
81
+ }
82
+
65
83
  function remove_retired_scripts(scripts: Record<string, string>): Record<string, string> {
66
84
  return Object.fromEntries(
67
- Object.entries(scripts).filter(([key]) => !RETIRED_MANAGED_SCRIPTS.has(key)),
85
+ Object.entries(scripts).filter(([key, value]) => !is_retired_script(key, value)),
68
86
  )
69
87
  }
70
88
 
@@ -5,7 +5,7 @@ const SECRETLINT_RULE_PRESET = '@secretlint/secretlint-rule-preset-recommend'
5
5
 
6
6
  // secretlint resolves both its CLI and every rule package from the project it runs in, not
7
7
  // transitively through the kit — the same constraint that forces the prettier plugins into
8
- // consumer devDependencies (see PRETTIER_PLUGIN_DEV_DEPS in init-logic.ts). Omitting either
8
+ // consumer devDependencies (as kit peers, see init.ts). Omitting either
9
9
  // entry makes the pre-commit hook fail with "Cannot find module". Versions mirror the kit's
10
10
  // own devDependencies.
11
11
  const SECRETLINT_DEV_DEPS: Record<string, string> = {
@@ -92,10 +92,13 @@ const AI_COPY_FILES: ReadonlyArray<string> = [
92
92
  '.github/workflows/production.yml',
93
93
  '.github/workflows/sonar-qube.yml',
94
94
  '.github/workflows/pr-classification.yml',
95
- // Called by `pr-classification.yml` as `uses: ./.github/actions/setup-pnpm`; without it the
96
- // consumer's job cannot resolve the action and the Release classification check fails
97
- // (joshuafolkken/kit#3013).
95
+ // Called by `ci.yml` and `pr-classification.yml` as `uses: ./.github/actions/setup-pnpm`; without
96
+ // it the consumer's jobs cannot resolve the action and their checks fail (joshuafolkken/kit#3013,
97
+ // joshuafolkken/kit#3095).
98
98
  '.github/actions/setup-pnpm/action.yml',
99
+ // The one place the workflows' Node.js version is written, called by every distributed workflow
100
+ // that sets Node.js up (joshuafolkken/kit#3084).
101
+ '.github/actions/setup-node/action.yml',
99
102
  '.github/pull_request_template.md',
100
103
  '.github/release.yml',
101
104
  '.github/dependabot.yml',
@@ -220,20 +223,6 @@ const SUGGESTED_SCRIPTS_COMMON: Record<string, string> = {
220
223
  josh: 'josh',
221
224
  }
222
225
 
223
- // prettier resolves its `plugins[]` from the consumer project, not transitively through the kit,
224
- // so every package that uses the kit prettier preset must declare these as devDependencies. The
225
- // preset references all three unconditionally (prettier/index.js → plugins), hence all three are
226
- // added regardless of project — omitting any breaks `prettier`/`josh lint` with a
227
- // "Cannot find package" error. Versions mirror the kit's own devDependencies.
228
- const SORT_IMPORTS_PLUGIN_KEY = '@ianvs/prettier-plugin-sort-imports'
229
- const PRETTIER_SVELTE_PLUGIN_KEY = 'prettier-plugin-svelte'
230
- const PRETTIER_TAILWIND_PLUGIN_KEY = 'prettier-plugin-tailwindcss'
231
- const PRETTIER_PLUGIN_DEV_DEPS: Record<string, string> = {
232
- [SORT_IMPORTS_PLUGIN_KEY]: '^4.7.1',
233
- [PRETTIER_SVELTE_PLUGIN_KEY]: '^4.1.1',
234
- [PRETTIER_TAILWIND_PLUGIN_KEY]: '^0.8.0',
235
- }
236
-
237
226
  // format_json rather than JSON.stringify: it lays arrays out the way prettier does — inline while
238
227
  // they fit within printWidth, one element per line once they do not — so the file kit writes is
239
228
  // `prettier --check`-clean in the consumer whatever the entry count happens to be (#660).
@@ -433,10 +422,6 @@ function strip_managed_postinstall(content: string): string {
433
422
  )
434
423
  }
435
424
 
436
- function merge_prettier_plugin_development_deps(content: string): string {
437
- return init_logic_json_merge.merge_development_dependencies(content, PRETTIER_PLUGIN_DEV_DEPS)
438
- }
439
-
440
425
  const init_logic = {
441
426
  ...init_logic_templates,
442
427
  ...init_logic_workspace,
@@ -454,7 +439,6 @@ const init_logic = {
454
439
  generate_npmrc,
455
440
  merge_npmrc,
456
441
  merge_gitignore,
457
- merge_prettier_plugin_development_deps,
458
442
  get_tsconfig_extends_entry,
459
443
  get_tsconfig_preset_filename,
460
444
  get_lefthook_extends_value,