@joshuafolkken/kit 1.887.0 → 1.929.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 (127) hide show
  1. package/.claude/skills/workflow-commands/backlogrun-progress.md +13 -12
  2. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -1
  3. package/.claude/skills/workflow-commands/fullrun-steps.md +4 -2
  4. package/.claude/skills/workflow-commands/halfrun.md +3 -1
  5. package/.claude/skills/workflow-commands/kickoff.md +5 -3
  6. package/.claude/skills/workflow-commands/observation-filing.md +3 -1
  7. package/.claude/skills/workflow-commands/prerequisite.md +3 -1
  8. package/.claude/skills/workflow-commands/target-repository.md +1 -1
  9. package/.github/pull_request_template.md +12 -0
  10. package/.github/release.yml +3 -0
  11. package/.github/workflows/ci.yml +8 -0
  12. package/.github/workflows/production.yml +10 -1
  13. package/.github/workflows/publish.yml +72 -2
  14. package/CLAUDE.md +2 -0
  15. package/README.md +2 -1
  16. package/dist/CLAUDE.md +2 -0
  17. package/dist/CLAUDE.static.md +8 -0
  18. package/dist/hooks/batch-guard.js +6 -6
  19. package/dist/hooks/{chunk-264QY6KS.js → chunk-2PGRQ62K.js} +25 -10
  20. package/dist/hooks/chunk-3V37IID6.js +82 -0
  21. package/dist/hooks/{chunk-36UE53LS.js → chunk-7XQ6TK3R.js} +2 -2
  22. package/dist/hooks/{chunk-PRDCNGA6.js → chunk-BNFG2IFK.js} +4 -4
  23. package/dist/hooks/{chunk-VGYSA2NT.js → chunk-GQ3EVWHY.js} +22 -3
  24. package/dist/hooks/{chunk-Y5OYCU2F.js → chunk-I3JP3D4A.js} +3 -3
  25. package/dist/hooks/{chunk-6MQCXEEA.js → chunk-LCJZE42I.js} +25 -88
  26. package/dist/hooks/{chunk-ZBK4XRAS.js → chunk-NJZSO7BK.js} +3 -3
  27. package/dist/hooks/{chunk-SSX23SJ2.js → chunk-RKUAD3I6.js} +1 -0
  28. package/dist/hooks/{chunk-FRJKVSK3.js → chunk-ROULPSQC.js} +2 -2
  29. package/dist/hooks/{chunk-FNFOLFFV.js → chunk-SXPTRFPG.js} +2 -2
  30. package/dist/hooks/{chunk-FXAKJKTB.js → chunk-WPALEIRX.js} +1 -1
  31. package/dist/hooks/{chunk-4OXFU3MK.js → chunk-YO43HEPY.js} +634 -156
  32. package/dist/hooks/codex-hook-adapter.js +13 -12
  33. package/dist/hooks/duplicate-read-guard.js +7 -7
  34. package/dist/hooks/format-edited.js +4 -3
  35. package/dist/hooks/investigation-guard.js +7 -7
  36. package/dist/hooks/pretool-guard.js +12 -11
  37. package/dist/hooks/session-lang.js +2 -2
  38. package/dist/hooks/stop-guard.js +5 -5
  39. package/dist/josh.js +156 -20
  40. package/eslint/base.js +4 -1
  41. package/package.json +72 -18
  42. package/playwright.config.ts +9 -17
  43. package/ports/index.d.ts +1 -0
  44. package/ports/index.js +1 -0
  45. package/ports/web-server.d.ts +5 -0
  46. package/ports/web-server.js +73 -0
  47. package/prettier/index.js +3 -8
  48. package/prettier/static.d.ts +3 -0
  49. package/prettier/static.js +10 -0
  50. package/prompts/collaboration-workflow/issue-template.md +11 -4
  51. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  52. package/prompts/collaboration-workflow/wip-cap.md +2 -2
  53. package/prompts/review.md +2 -2
  54. package/prompts/testing-guide.md +1 -0
  55. package/scripts/agent/agent-role-profile.ts +11 -0
  56. package/scripts/build/build-claude-md.ts +6 -1
  57. package/scripts/ci/pr-classification.ts +114 -0
  58. package/scripts/document/document-byte-budget.ts +3 -2
  59. package/scripts/gate/gate-report.ts +2 -0
  60. package/scripts/gate/gate-steps.ts +50 -0
  61. package/scripts/gate/project-checks.ts +145 -0
  62. package/scripts/gate/verification-gate.ts +14 -43
  63. package/scripts/git/git-common-directory.ts +9 -1
  64. package/scripts/git/git-gh-pr-rollup.ts +29 -1
  65. package/scripts/git/git-gh-pr.ts +61 -1
  66. package/scripts/git/git-pr.ts +62 -13
  67. package/scripts/git/issue-labels.ts +9 -0
  68. package/scripts/init/distributed-paths.ts +9 -5
  69. package/scripts/init/hook-command-bootstrap.ts +40 -8
  70. package/scripts/init/hook-command-rewrite.ts +7 -3
  71. package/scripts/init/init-actions.ts +94 -7
  72. package/scripts/init/init-ai-copy.ts +107 -19
  73. package/scripts/init/init-logic-workspace.ts +38 -5
  74. package/scripts/init/init-logic.ts +1 -0
  75. package/scripts/init/init-static.ts +55 -0
  76. package/scripts/init/init.ts +97 -30
  77. package/scripts/init/project-profile-cli.ts +14 -0
  78. package/scripts/init/project-profile.ts +158 -0
  79. package/scripts/init/start.ts +30 -0
  80. package/scripts/issue/issue-bug-label.ts +13 -0
  81. package/scripts/issue/issue-classification.ts +74 -0
  82. package/scripts/issue/issue-lint-cli.ts +10 -3
  83. package/scripts/issue/markdown-section.ts +38 -4
  84. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +27 -1
  85. package/scripts/josh/josh-command-map.ts +3 -0
  86. package/scripts/josh/josh-commands-development.ts +8 -15
  87. package/scripts/josh/josh-commands-project.ts +14 -1
  88. package/scripts/josh/josh-logic.ts +13 -0
  89. package/scripts/josh/run-github-prerequisite.ts +32 -0
  90. package/scripts/lane/lane-await.ts +8 -1
  91. package/scripts/lane/openai-lane-ship.ts +30 -0
  92. package/scripts/lane/openai-lane-supervisor.ts +35 -8
  93. package/scripts/lane/openai-review-broker.ts +260 -0
  94. package/scripts/lint/cspell-cached.ts +21 -0
  95. package/scripts/lint/format.ts +68 -0
  96. package/scripts/lint/lint-parallel.ts +30 -1
  97. package/scripts/observations/observations-flush.ts +1 -1
  98. package/scripts/package/production-dependency-graph.ts +68 -0
  99. package/scripts/package/verify-optional-eslint-install.ts +62 -0
  100. package/scripts/release/github-release-cli.ts +11 -0
  101. package/scripts/release/github-release.ts +220 -0
  102. package/scripts/release/publish-tag-cli.ts +8 -0
  103. package/scripts/release/publish-tag.ts +11 -0
  104. package/scripts/release/release-publish.ts +7 -2
  105. package/scripts/rules/delivered-rules.ts +2 -0
  106. package/scripts/rules/issue-bug-label.ts +85 -0
  107. package/scripts/rules/issue-filing-args.ts +50 -0
  108. package/scripts/run/run-merge-cli.ts +19 -0
  109. package/scripts/run/run-merge.ts +9 -1
  110. package/scripts/run/run-ship-cli.ts +20 -0
  111. package/scripts/run/run-ship-detach.ts +180 -14
  112. package/scripts/run/run-ship-review-steps.ts +45 -25
  113. package/scripts/run/run-step-cli.ts +11 -1
  114. package/scripts/run/run-step.ts +36 -8
  115. package/scripts/run/run-wake-session.ts +3 -1
  116. package/scripts/test/pilot-files.ts +0 -2
  117. package/scripts/test/skip-marker.ts +3 -0
  118. package/scripts/test/test-declared-changed.ts +2 -1
  119. package/scripts/test/test-declared-logic.ts +15 -10
  120. package/scripts/test/test-declared.ts +20 -8
  121. package/scripts/test/test-red-logic.ts +3 -3
  122. package/scripts/test/test-unit-guard.ts +24 -12
  123. package/templates/CLAUDE.static.md +8 -0
  124. package/templates/cursorrules.static +1 -0
  125. package/templates/pnpm-workspace.static.yaml +4 -0
  126. package/templates/prettierignore.static +12 -0
  127. package/templates/workflows/ci.yml +5 -0
@@ -1,3 +1,4 @@
1
+ import { GIT_LOCATION_VARIABLES } from '#scripts/git/git-location-environment'
1
2
  import { claude_plugin_config } from './claude-plugin-config'
2
3
  import { hook_launch } from './hook-launch'
3
4
 
@@ -15,7 +16,7 @@ import { hook_launch } from './hook-launch'
15
16
 
16
17
  const PNPM_JOSH_PREFIX = 'pnpm josh '
17
18
  const CODEX_ADAPTER_SOURCE = 'pnpm exec tsx scripts/hooks/codex-hook-adapter.ts'
18
- // Relative to the consumer's project root, which is the working directory a Claude Code hook runs in.
19
+ // Relative to the consumer's project root, which hook commands select before using this path.
19
20
  // The path is the plugin marketplace's `node_modules` location plus the built entry `bin.josh` names.
20
21
  const BUNDLE_INVOCATION = `node ${claude_plugin_config.MARKETPLACE_PATH}/dist/josh.js `
21
22
  // The per-hook bundle directory a fresh clone builds, and where it lives inside the installed package.
@@ -26,7 +27,8 @@ const MISSING_INSTALL_NOTICE = 'kit hooks inactive: run pnpm install, then rerea
26
27
  const MISSING_INSTALL_GUARD = `if [ ! -f ${CONSUMER_PACKAGE_MANIFEST} ]; then echo '${MISSING_INSTALL_NOTICE}' >&2; exit 0; fi; `
27
28
  const CODEX_ADAPTER_BUNDLE = `node ${CONSUMER_HOOK_BUNDLE_DIR}codex-hook-adapter.js`
28
29
  const CODEX_HOOKS_DESTINATION = '.codex/hooks.json'
29
- const CODEX_ROOT_PREFIX = String.raw`cd \"$(git rev-parse --show-toplevel)\" && `
30
+ const GIT_UNSET_OPTIONS = GIT_LOCATION_VARIABLES.map((name) => `-u ${name}`).join(' ')
31
+ const PROJECT_ROOT_PREFIX = String.raw`if project_root=\"$(env ${GIT_UNSET_OPTIONS} git rev-parse --show-toplevel 2>/dev/null)\"; then unset ${GIT_LOCATION_VARIABLES.join(' ')}; else project_root=\"$(git rev-parse --show-toplevel)\" || exit 1; fi; cd \"$project_root\" && `
30
32
  // Capture the value of every `"command"` field, escapes and all, so the rewrites below touch command
31
33
  // values alone — never an echo reminder's prose or a `"description"` that merely mentions the string.
32
34
  const COMMAND_FIELD = /("command":\s*")((?:[^"\\]|\\.)*)(")/gu
@@ -61,7 +63,9 @@ function rewrite_command_value(value: string): string {
61
63
  function rewrite_hook_commands(content: string, is_codex = false): string {
62
64
  return content.replaceAll(COMMAND_FIELD, (_match, open: string, value: string, close: string) => {
63
65
  const rewritten = rewrite_command_value(value)
64
- const prefix = is_codex && !rewritten.startsWith(CODEX_ROOT_PREFIX) ? CODEX_ROOT_PREFIX : ''
66
+ const should_select_root = is_codex || rewritten.includes(claude_plugin_config.MARKETPLACE_PATH)
67
+ const prefix =
68
+ should_select_root && !rewritten.startsWith(PROJECT_ROOT_PREFIX) ? PROJECT_ROOT_PREFIX : ''
65
69
 
66
70
  return `${open}${prefix}${rewritten}${close}`
67
71
  })
@@ -1,10 +1,17 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { json_format } from '#scripts/config-merge/json-format'
3
4
  import { string_array_schema, vscode_settings_schema } from '#scripts/lib/schemas'
4
5
  import { init_logic } from './init-logic'
5
6
  import { package_path, PROJECT_ROOT } from './init-paths'
7
+ import type { ProjectShape } from './project-profile'
6
8
 
7
9
  const PRETTIER_CONFIG_JS = 'prettier.config.js'
10
+ const STATIC_PRETTIER_CONFIG = 'prettier.config.mjs'
11
+ const VSCODE_EXTENSIONS_PATH = '.vscode/extensions.json'
12
+ const VSCODE_SETTINGS_PATH = '.vscode/settings.json'
13
+ const TSCONFIG_PATH = 'tsconfig.json'
14
+ const LEFTHOOK_PATH = 'lefthook.yml'
8
15
 
9
16
  interface FileAction {
10
17
  dest: string
@@ -38,13 +45,13 @@ function build_vscode_actions(): ReadonlyArray<FileAction> {
38
45
 
39
46
  return [
40
47
  {
41
- dest: '.vscode/extensions.json',
48
+ dest: VSCODE_EXTENSIONS_PATH,
42
49
  create: () => read_package_file(extensions_path),
43
50
  merge: (existing) =>
44
51
  init_logic.merge_json_array_field(existing, 'recommendations', recommendations),
45
52
  },
46
53
  {
47
- dest: '.vscode/settings.json',
54
+ dest: VSCODE_SETTINGS_PATH,
48
55
  create: () =>
49
56
  init_logic.strip_kit_only_vscode_settings_content(read_package_file(settings_path)),
50
57
  merge: (existing) => init_logic.merge_json_object(existing, settings_data),
@@ -52,6 +59,64 @@ function build_vscode_actions(): ReadonlyArray<FileAction> {
52
59
  ]
53
60
  }
54
61
 
62
+ const STATIC_EXTENSIONS = [
63
+ 'streetsidesoftware.code-spell-checker',
64
+ 'pkief.material-icon-theme',
65
+ 'usernamehw.errorlens',
66
+ 'anthropic.claude-code',
67
+ 'github.github-vscode-theme',
68
+ ]
69
+ const PRETTIER_EXTENSION = 'esbenp.prettier-vscode'
70
+ const DEFAULT_FORMATTER = 'editor.defaultFormatter'
71
+ const FORMAT_ON_SAVE = 'editor.formatOnSave'
72
+ const STATIC_LANGUAGES = ['[html]', '[css]', '[javascript]']
73
+
74
+ function static_settings(): Record<string, Record<string, unknown>> {
75
+ const formatter = { [DEFAULT_FORMATTER]: PRETTIER_EXTENSION, [FORMAT_ON_SAVE]: true }
76
+
77
+ return Object.fromEntries(STATIC_LANGUAGES.map((language) => [language, formatter]))
78
+ }
79
+
80
+ function build_static_settings_action(): FileAction {
81
+ const settings = static_settings()
82
+
83
+ return build_action(
84
+ VSCODE_SETTINGS_PATH,
85
+ () => json_format.format_json(settings),
86
+ (existing) => init_logic.merge_json_object(existing, settings),
87
+ )
88
+ }
89
+
90
+ function build_static_vscode_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
91
+ const recommendations = shape.has_web
92
+ ? [...STATIC_EXTENSIONS, PRETTIER_EXTENSION]
93
+ : STATIC_EXTENSIONS
94
+ const actions: Array<FileAction> = [
95
+ build_action(
96
+ VSCODE_EXTENSIONS_PATH,
97
+ () => json_format.format_json({ recommendations }),
98
+ (existing) => init_logic.merge_json_array_field(existing, 'recommendations', recommendations),
99
+ ),
100
+ ]
101
+
102
+ if (shape.has_web) actions.push(build_static_settings_action())
103
+
104
+ return actions
105
+ }
106
+
107
+ function build_static_tsconfig_action(): FileAction {
108
+ return build_action(
109
+ TSCONFIG_PATH,
110
+ () => json_format.format_json({ extends: init_logic.get_tsconfig_extends_entry() }),
111
+ (existing) =>
112
+ init_logic.merge_tsconfig_extends(
113
+ existing,
114
+ init_logic.get_tsconfig_extends_entry(),
115
+ PROJECT_ROOT,
116
+ ),
117
+ )
118
+ }
119
+
55
120
  // Create-only: the rule list is a project-owned allowlist once it exists, so a consumer
56
121
  // that added custom rules or disabled a noisy one must not have that overwritten.
57
122
  function build_secretlint_action(): FileAction {
@@ -65,7 +130,7 @@ function build_secretlint_action(): FileAction {
65
130
  // migrate a retired `*.jsonc` preset path only once its renamed target is installed (#681).
66
131
  function build_tsconfig_action(): FileAction {
67
132
  return build_action(
68
- 'tsconfig.json',
133
+ TSCONFIG_PATH,
69
134
  () => init_logic.generate_tsconfig(),
70
135
  (existing) =>
71
136
  init_logic.merge_tsconfig_exclude(
@@ -89,7 +154,7 @@ function build_config_file_actions(): ReadonlyArray<FileAction> {
89
154
  (existing) => init_logic.merge_cspell_import(existing, init_logic.get_cspell_import_value()),
90
155
  ),
91
156
  build_action(
92
- 'lefthook.yml',
157
+ LEFTHOOK_PATH,
93
158
  () => init_logic.generate_lefthook_config(),
94
159
  (existing) => init_logic.merge_lefthook_extends(existing, lefthook_extends),
95
160
  ),
@@ -122,9 +187,29 @@ function build_gitignore_action(): FileAction {
122
187
  )
123
188
  }
124
189
 
125
- function build_file_actions(): ReadonlyArray<FileAction> {
190
+ function build_static_actions(shape: ProjectShape): ReadonlyArray<FileAction> {
191
+ const actions: Array<FileAction> = []
192
+ if (shape.has_git) actions.push(build_gitignore_action())
193
+ if (shape.has_typescript) actions.push(build_static_tsconfig_action())
194
+
195
+ if (shape.has_web) {
196
+ actions.push({
197
+ dest: STATIC_PRETTIER_CONFIG,
198
+ create: () =>
199
+ "import { config } from '@joshuafolkken/kit/prettier/static'\n\nexport default config\n",
200
+ })
201
+ }
202
+
203
+ actions.push(...build_static_vscode_actions(shape))
204
+
205
+ return actions
206
+ }
207
+
208
+ function build_file_actions(shape?: ProjectShape): ReadonlyArray<FileAction> {
209
+ if (shape?.profile === 'static') return build_static_actions(shape)
210
+
126
211
  return [
127
- build_gitignore_action(),
212
+ ...(shape?.has_git === false ? [] : [build_gitignore_action()]),
128
213
  build_action(
129
214
  '.npmrc',
130
215
  () => init_logic.generate_npmrc(),
@@ -137,7 +222,9 @@ function build_file_actions(): ReadonlyArray<FileAction> {
137
222
  (existing) => init_logic.merge_prettier_config(existing),
138
223
  ),
139
224
  build_playwright_action(),
140
- ...build_config_file_actions(),
225
+ ...build_config_file_actions().filter(
226
+ (action) => shape?.has_git !== false || action.dest !== LEFTHOOK_PATH,
227
+ ),
141
228
  ]
142
229
  }
143
230
 
@@ -12,9 +12,37 @@ import { transform_copied_content } from './init-copy-content'
12
12
  import { init_logic } from './init-logic'
13
13
  import { package_path, PROJECT_ROOT } from './init-paths'
14
14
  import { init_sonar } from './init-sonar'
15
+ import type { ProjectShape } from './project-profile'
15
16
 
16
17
  const WORKSPACE_YAML = 'pnpm-workspace.yaml'
17
18
  const CLAUDE_MD_FILENAME = 'CLAUDE.md'
19
+ const STATIC_CLAUDE_IMPORT = '@node_modules/@joshuafolkken/kit/dist/CLAUDE.static.md'
20
+ const PRETTIER_IGNORE = '.prettierignore'
21
+ const GIT_ATTRIBUTES = '.gitattributes'
22
+ const CODE_OF_CONDUCT = 'CODE_OF_CONDUCT.md'
23
+ const SECURITY_MD = 'SECURITY.md'
24
+ const STATIC_AI_FILES = [
25
+ 'AGENTS.md',
26
+ 'GEMINI.md',
27
+ PRETTIER_IGNORE,
28
+ GIT_ATTRIBUTES,
29
+ CODE_OF_CONDUCT,
30
+ SECURITY_MD,
31
+ WORKSPACE_YAML,
32
+ ]
33
+ // A static project gets its own template for these, not kit's development copy (joshuafolkken/kit#2693).
34
+ const STATIC_SOURCES: Readonly<Record<string, string>> = {
35
+ [PRETTIER_IGNORE]: 'templates/prettierignore.static',
36
+ [WORKSPACE_YAML]: 'templates/pnpm-workspace.static.yaml',
37
+ }
38
+ const GITHUB_FILES = new Set([CODE_OF_CONDUCT, SECURITY_MD])
39
+ const STATIC_POINTER_MAPPING = { src: 'templates/cursorrules.static', dest: '.cursorrules' }
40
+ const PULL_REQUEST_TEMPLATE = '.github/pull_request_template.md'
41
+ const RELEASE_CONFIG = '.github/release.yml'
42
+ const STATIC_GITHUB_MAPPINGS = [
43
+ { src: PULL_REQUEST_TEMPLATE, dest: PULL_REQUEST_TEMPLATE },
44
+ { src: RELEASE_CONFIG, dest: RELEASE_CONFIG },
45
+ ]
18
46
 
19
47
  // Whether the existing file is a workflow that carries no header. A read failure answers "no": the
20
48
  // point is to warn, and a warning is not worth failing a command that used to open nothing.
@@ -59,7 +87,7 @@ function did_skip_copy_if_absent(
59
87
  label: string,
60
88
  ): boolean {
61
89
  if (existsSync(destination_path)) {
62
- console.info(` ⏭ skipped ${label} (already exists — run josh sync to update)`)
90
+ console.info(` ⏭ skipped ${label} (already exists)`)
63
91
  did_warn_unstamped_workflow(destination_path, label)
64
92
 
65
93
  return true
@@ -89,8 +117,9 @@ function did_skip_workspace_yaml_copy(source_path: string, destination_path: str
89
117
  return false
90
118
  }
91
119
 
92
- function did_skip_ai_file_copy(filename: string): boolean {
93
- const source_path = package_path(filename)
120
+ function did_skip_ai_file_copy(filename: string, shape?: ProjectShape): boolean {
121
+ const static_source = shape?.profile === 'static' ? STATIC_SOURCES[filename] : undefined
122
+ const source_path = package_path(static_source ?? filename)
94
123
  const destination_path = path.join(PROJECT_ROOT, filename)
95
124
 
96
125
  if (filename === WORKSPACE_YAML) {
@@ -183,16 +212,21 @@ function did_skip_ai_directory_copy(directory_name: string): boolean {
183
212
  // AI file — leaves an existing one untouched so a consumer's additions are never disturbed. `josh sync`
184
213
  // is what ensures the import line on an existing file (sync.ts). CLAUDE.md is no longer byte-copied
185
214
  // (joshuafolkken/kit#1878), so it is handled here rather than through AI_COPY_FILES.
186
- function did_skip_claude_md_import(): boolean {
215
+ function did_skip_claude_md_import(shape?: ProjectShape): boolean {
187
216
  const destination_path = path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME)
188
217
 
189
218
  if (existsSync(destination_path)) {
190
- console.info(` ⏭ skipped ${CLAUDE_MD_FILENAME} (already exists — run josh sync to update)`)
219
+ console.info(` ⏭ skipped ${CLAUDE_MD_FILENAME} (already exists)`)
191
220
 
192
221
  return true
193
222
  }
194
223
 
195
- writeFileSync(destination_path, init_logic.ensure_claude_md_import(undefined))
224
+ const content =
225
+ shape?.profile === 'static'
226
+ ? `> If kit is not installed, run \`pnpm install\` first.\n\n${STATIC_CLAUDE_IMPORT}\n`
227
+ : init_logic.ensure_claude_md_import(undefined)
228
+
229
+ writeFileSync(destination_path, content)
196
230
  console.info(` ✔ created ${CLAUDE_MD_FILENAME}`)
197
231
 
198
232
  return false
@@ -201,14 +235,69 @@ function did_skip_claude_md_import(): boolean {
201
235
  // Returns the repository name resolved for the Sonar config, so `josh init` can reuse it for the
202
236
  // security-updates report instead of spawning a second `gh repo view` (joshuafolkken/kit#805).
203
237
  // Resolving it here rather than in the caller keeps every AI-file write ahead of the network call.
204
- function run_ai_copies(): string | undefined {
205
- const did_skip_claude_md = did_skip_claude_md_import()
206
- const file_skips = init_logic
207
- .get_ai_copy_files()
208
- .map((filename) => did_skip_ai_file_copy(filename))
209
- const mapping_skips = init_logic
210
- .get_ai_copy_file_mappings()
211
- .map(({ src: source, dest: destination }) => did_skip_ai_file_mapping(source, destination))
238
+ function should_copy_git_file(filename: string, shape?: ProjectShape): boolean {
239
+ if (shape?.has_git !== false) return true
240
+
241
+ return (
242
+ filename !== GIT_ATTRIBUTES &&
243
+ !filename.startsWith('.claude/') &&
244
+ !filename.startsWith('.codex/')
245
+ )
246
+ }
247
+
248
+ function should_copy_github_file(filename: string, shape?: ProjectShape): boolean {
249
+ if (shape?.has_github !== false) return true
250
+
251
+ return !GITHUB_FILES.has(filename) && !filename.startsWith('.github/')
252
+ }
253
+
254
+ function should_copy_static_file(filename: string, shape?: ProjectShape): boolean {
255
+ if (filename !== PRETTIER_IGNORE || shape?.profile !== 'static') return true
256
+
257
+ return shape.has_web
258
+ }
259
+
260
+ function should_copy_ai_file(filename: string, shape?: ProjectShape): boolean {
261
+ return (
262
+ should_copy_git_file(filename, shape) &&
263
+ should_copy_github_file(filename, shape) &&
264
+ should_copy_static_file(filename, shape)
265
+ )
266
+ }
267
+
268
+ function ai_files(shape?: ProjectShape): ReadonlyArray<string> {
269
+ const files = shape?.profile === 'static' ? STATIC_AI_FILES : init_logic.get_ai_copy_files()
270
+
271
+ return files.filter((filename) => should_copy_ai_file(filename, shape))
272
+ }
273
+
274
+ function ai_mapping_skips(shape?: ProjectShape): ReadonlyArray<boolean> {
275
+ const mappings =
276
+ shape?.profile === 'static'
277
+ ? [STATIC_POINTER_MAPPING, ...(shape.has_github ? STATIC_GITHUB_MAPPINGS : [])]
278
+ : init_logic.get_ai_copy_file_mappings()
279
+
280
+ return mappings
281
+ .filter(({ dest }) => shape?.has_github !== false || !dest.startsWith('.github/'))
282
+ .map(({ src, dest }) => did_skip_ai_file_mapping(src, dest))
283
+ }
284
+
285
+ function repository_name(shape?: ProjectShape): string | undefined {
286
+ if (shape?.has_github === false || shape?.profile === 'static') return undefined
287
+
288
+ return gh_spawn.get_repo_name_with_owner()
289
+ }
290
+
291
+ function report_sync_hint(has_skips: boolean, shape?: ProjectShape): void {
292
+ if (!has_skips || shape?.profile === 'static') return
293
+
294
+ console.info('\n 💡 Run `josh sync` to overwrite skipped AI files with the latest version.')
295
+ }
296
+
297
+ function run_ai_copies(shape?: ProjectShape): string | undefined {
298
+ const did_skip_claude_md = did_skip_claude_md_import(shape)
299
+ const file_skips = ai_files(shape).map((filename) => did_skip_ai_file_copy(filename, shape))
300
+ const mapping_skips = ai_mapping_skips(shape)
212
301
  const directory_skips = init_logic
213
302
  .get_ai_copy_directories()
214
303
  .map((directory_name) => did_skip_ai_directory_copy(directory_name))
@@ -216,13 +305,11 @@ function run_ai_copies(): string | undefined {
216
305
  Boolean,
217
306
  )
218
307
 
219
- const name_with_owner = gh_spawn.get_repo_name_with_owner()
308
+ const name_with_owner = repository_name(shape)
220
309
 
221
- init_sonar.copy_sonar_with_template(name_with_owner)
310
+ if (shape?.profile !== 'static') init_sonar.copy_sonar_with_template(name_with_owner)
222
311
 
223
- if (has_skips) {
224
- console.info('\n 💡 Run `josh sync` to overwrite skipped AI files with the latest version.')
225
- }
312
+ report_sync_hint(has_skips, shape)
226
313
 
227
314
  return name_with_owner
228
315
  }
@@ -230,6 +317,7 @@ function run_ai_copies(): string | undefined {
230
317
  const init_ai_copy = {
231
318
  copy_ai_file,
232
319
  run_ai_copies,
320
+ ai_files,
233
321
  }
234
322
 
235
323
  export { init_ai_copy }
@@ -42,16 +42,49 @@ function remove_deprecated_yaml_keys(content: string): string {
42
42
  return `${result.trimEnd()}\n`
43
43
  }
44
44
 
45
+ // pnpm 11+ writes this placeholder into `allowBuilds` for every build script it ignored, and fails
46
+ // every later install until it is answered. `pnpm add -D @joshuafolkken/kit` before `josh init`
47
+ // leaves one for esbuild, and the key-level merge below keeps an existing `allowBuilds` whole, so a
48
+ // placeholder the template decides is answered here (joshuafolkken/kit#2693).
49
+ const BUILD_PLACEHOLDER_PATTERN = /^([ \t]+)([^\s:#][^:#]*): set this to true or false$/gmu
50
+ const TEMPLATE_BUILD_PATTERN = /^[ \t]+(?<key>[^\s:#][^:#]*):[ \t]*(?<value>true|false)\b/u
51
+
52
+ function unquote(key: string): string {
53
+ return key.trim().replaceAll(/^['"]|['"]$/gu, '')
54
+ }
55
+
56
+ function template_build_values(template: string): Map<string, string> {
57
+ const lines = extract_yaml_block(template, 'allowBuilds').split('\n')
58
+
59
+ return new Map(
60
+ lines.flatMap((line) => {
61
+ const { key, value } = TEMPLATE_BUILD_PATTERN.exec(line)?.groups ?? {}
62
+
63
+ return key && value ? [[unquote(key), value] as const] : []
64
+ }),
65
+ )
66
+ }
67
+
68
+ function answer_build_placeholders(existing: string, template: string): string {
69
+ const values = template_build_values(template)
70
+
71
+ return existing.replaceAll(BUILD_PLACEHOLDER_PATTERN, (line, indent: string, key: string) => {
72
+ const value = values.get(unquote(key))
73
+
74
+ return value === undefined ? line : `${indent}${key}: ${value}`
75
+ })
76
+ }
77
+
45
78
  function merge_workspace_yaml(existing: string, template: string): string {
46
79
  if (!existing.trim()) return template
47
80
  const normalized = existing.endsWith('\n') ? existing : `${existing}\n`
48
- const without_deprecated = remove_deprecated_yaml_keys(normalized)
49
- if (!without_deprecated.trim()) return template
50
- const existing_keys = new Set(extract_yaml_top_level_keys(without_deprecated))
81
+ const cleaned = answer_build_placeholders(remove_deprecated_yaml_keys(normalized), template)
82
+ if (!cleaned.trim()) return template
83
+ const existing_keys = new Set(extract_yaml_top_level_keys(cleaned))
51
84
  const new_keys = extract_yaml_top_level_keys(template).filter((k) => !existing_keys.has(k))
52
- if (new_keys.length === 0) return without_deprecated
85
+ if (new_keys.length === 0) return cleaned
53
86
 
54
- return append_user_blocks(without_deprecated, new_keys, template)
87
+ return append_user_blocks(cleaned, new_keys, template)
55
88
  }
56
89
 
57
90
  const init_logic_workspace = { merge_workspace_yaml }
@@ -451,6 +451,7 @@ const init_logic = {
451
451
  strip_kit_only_vscode_settings,
452
452
  strip_kit_only_vscode_settings_content,
453
453
  VSCODE_EXTENSIONS_FILENAME,
454
+ SAFE_CHAIN_CMD,
454
455
  GUARDED_LEFTHOOK_CMD,
455
456
  upgrade_prepare_lefthook_warning,
456
457
  get_npmrc_lines,
@@ -0,0 +1,55 @@
1
+ import { json_format } from '#scripts/config-merge/json-format'
2
+ import { z } from 'zod'
3
+ import { init_logic } from './init-logic'
4
+ import type { ProjectShape } from './project-profile'
5
+
6
+ const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
7
+ const record_schema = z.record(z.string(), z.unknown())
8
+
9
+ function is_record(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
11
+ }
12
+
13
+ interface StaticVersions {
14
+ kit: string
15
+ prettier: string
16
+ }
17
+
18
+ function initial_manifest(): string {
19
+ return json_format.format_json({ private: true })
20
+ }
21
+
22
+ function with_recorded_profile(content: string, profile: ProjectShape['profile']): string {
23
+ const parsed: unknown = JSON.parse(content)
24
+ const manifest = record_schema.parse(parsed)
25
+ const existing = manifest['josh']
26
+ const josh = is_record(existing) ? existing : {}
27
+
28
+ manifest['josh'] = { ...josh, profile }
29
+
30
+ return json_format.format_json(manifest)
31
+ }
32
+
33
+ function merge_static_manifest(
34
+ content: string,
35
+ shape: ProjectShape,
36
+ versions: StaticVersions,
37
+ ): string {
38
+ const with_profile = with_recorded_profile(content, shape.profile)
39
+ const with_script = init_logic.merge_package_scripts(with_profile, {
40
+ preinstall: init_logic.SAFE_CHAIN_CMD,
41
+ josh: 'josh',
42
+ })
43
+ const with_kit = init_logic.merge_development_dependencies(with_script, {
44
+ [KIT_PACKAGE_NAME]: versions.kit,
45
+ })
46
+ const with_prettier = shape.has_web
47
+ ? init_logic.merge_development_dependencies(with_kit, { prettier: versions.prettier })
48
+ : with_kit
49
+
50
+ return init_logic.sort_package_json_keys(with_prettier)
51
+ }
52
+
53
+ const init_static = { initial_manifest, with_recorded_profile, merge_static_manifest }
54
+ export { init_static }
55
+ export type { StaticVersions }