@joshuafolkken/kit 1.983.0 → 1.998.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 (94) hide show
  1. package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
  2. package/.claude/skills/workflow-commands/backlogrun-steps.md +1 -1
  3. package/.github/workflows/sonar-qube.yml +12 -2
  4. package/CLAUDE.md +2 -2
  5. package/README.md +30 -21
  6. package/dist/CLAUDE.md +2 -2
  7. package/dist/hooks/batch-guard.js +4 -4
  8. package/dist/hooks/{chunk-ZT65LR3U.js → chunk-4GDR2YTV.js} +4 -4
  9. package/dist/hooks/{chunk-ASUZGZFT.js → chunk-56LGMJJV.js} +2 -2
  10. package/dist/hooks/{chunk-LBXQHXMC.js → chunk-762BDXDU.js} +220 -68
  11. package/dist/hooks/{chunk-IM3KM25Y.js → chunk-7WAOP42A.js} +5 -3
  12. package/dist/hooks/{chunk-P3UX7SFN.js → chunk-A2H5I2VD.js} +2 -2
  13. package/dist/hooks/{chunk-ERXMJJOV.js → chunk-AWTFKKFW.js} +1 -1
  14. package/dist/hooks/{chunk-APVOACVT.js → chunk-GBJUERYK.js} +1 -1
  15. package/dist/hooks/{chunk-5JX6HXDX.js → chunk-KPZN42RT.js} +3 -3
  16. package/dist/hooks/{chunk-47SPMGOD.js → chunk-PAXMLGSS.js} +26 -7
  17. package/dist/hooks/{chunk-RFPHKU7C.js → chunk-WMIR5SNX.js} +3 -3
  18. package/dist/hooks/codex-hook-adapter.js +10 -10
  19. package/dist/hooks/duplicate-read-guard.js +5 -5
  20. package/dist/hooks/format-edited.js +2 -2
  21. package/dist/hooks/investigation-guard.js +5 -5
  22. package/dist/hooks/pretool-guard.js +9 -9
  23. package/dist/hooks/stop-guard.js +98 -26
  24. package/dist/josh.js +5 -5
  25. package/package.json +5 -1
  26. package/prettier/basic.d.ts +3 -0
  27. package/prettier/basic.js +10 -0
  28. package/prettier/index.js +2 -2
  29. package/prettier/static.js +3 -10
  30. package/prompts/sonar-hotspot-handling.md +6 -0
  31. package/prompts/testing-guide.md +1 -1
  32. package/scripts/agent/agent-diagnostics.ts +1 -0
  33. package/scripts/agent/agent-role-profile.ts +1 -1
  34. package/scripts/behavior/behavior-rules.ts +8 -1
  35. package/scripts/build/build-claude-md.ts +13 -4
  36. package/scripts/document/document-byte-budget.ts +1 -1
  37. package/scripts/gate/gate-steps.ts +1 -1
  38. package/scripts/gate/project-checks.ts +12 -11
  39. package/scripts/gate/type-check-command.ts +1 -1
  40. package/scripts/git/git-branch.ts +14 -3
  41. package/scripts/git/git-issue.ts +13 -5
  42. package/scripts/git/git-pr.ts +16 -10
  43. package/scripts/git/git-preflight.ts +138 -0
  44. package/scripts/git/telegram-notify.ts +21 -0
  45. package/scripts/hooks/stop-guard.ts +2 -0
  46. package/scripts/init/basic-path-migration.ts +24 -0
  47. package/scripts/init/distributed-paths.ts +22 -0
  48. package/scripts/init/init-actions.ts +27 -19
  49. package/scripts/init/init-ai-copy.ts +86 -28
  50. package/scripts/init/{init-static.ts → init-basic.ts} +6 -6
  51. package/scripts/init/init-bootstrap.ts +4 -3
  52. package/scripts/init/init-install.ts +1 -1
  53. package/scripts/init/init-logic-templates.ts +1 -1
  54. package/scripts/init/init-logic.ts +1 -1
  55. package/scripts/init/init.ts +41 -19
  56. package/scripts/init/kit-setup-state.ts +30 -0
  57. package/scripts/init/kit-written-paths.ts +33 -0
  58. package/scripts/init/project-profile-cli.ts +15 -1
  59. package/scripts/init/project-profile.ts +41 -12
  60. package/scripts/init/session-language-environment.ts +83 -0
  61. package/scripts/init/start-exec.ts +28 -0
  62. package/scripts/init/start-plan.ts +35 -13
  63. package/scripts/init/start-prompt.ts +22 -8
  64. package/scripts/init/start-setup-pr.ts +138 -0
  65. package/scripts/init/start-steps.ts +30 -30
  66. package/scripts/init/start.ts +7 -3
  67. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +3 -3
  68. package/scripts/josh/josh-commands-development.ts +3 -3
  69. package/scripts/josh/josh-commands-project.ts +2 -2
  70. package/scripts/lint/cspell-cached.ts +3 -3
  71. package/scripts/lint/format.ts +1 -1
  72. package/scripts/lint/lint-parallel.ts +2 -2
  73. package/scripts/rules/filing-cap.ts +1 -0
  74. package/scripts/rules/issue-citation.ts +30 -1
  75. package/scripts/rules/last-prompt.ts +79 -0
  76. package/scripts/rules/stop-rules.ts +5 -3
  77. package/scripts/safe-chain/ci-installer-pin.ts +19 -12
  78. package/scripts/safe-chain/project-config.ts +1 -1
  79. package/scripts/sync/sync-ai-files.ts +204 -0
  80. package/scripts/sync/sync.ts +48 -184
  81. package/scripts/test/josh-harness-environment.ts +1 -1
  82. package/scripts/test/pilot-files.ts +0 -1
  83. package/scripts/test/test-declared-changed.ts +1 -1
  84. package/scripts/test/test-declared-logic.ts +55 -14
  85. package/scripts/test/test-declared.ts +23 -11
  86. package/scripts/test/test-unit-guard.ts +6 -6
  87. package/scripts/time-runtime/time-shell.ts +36 -2
  88. package/scripts-ai/git-workflow.ts +10 -0
  89. package/templates/CLAUDE.basic.md +8 -0
  90. package/templates/workflows/ci.yml +27 -0
  91. /package/{templates/CLAUDE.static.md → dist/CLAUDE.basic.md} +0 -0
  92. /package/templates/{cursorrules.static → cursorrules.basic} +0 -0
  93. /package/templates/{pnpm-workspace.static.yaml → pnpm-workspace.basic.yaml} +0 -0
  94. /package/templates/{prettierignore.static → prettierignore.basic} +0 -0
@@ -8,6 +8,8 @@ import {
8
8
  copy_directory_failure,
9
9
  directory_copy_blocker,
10
10
  } from '#scripts/sync/directory-copy-guard'
11
+ import { basic_path_migration } from './basic-path-migration'
12
+ import { distributed_paths } from './distributed-paths'
11
13
  import { transform_copied_content } from './init-copy-content'
12
14
  import { init_logic } from './init-logic'
13
15
  import { package_path, PROJECT_ROOT } from './init-paths'
@@ -16,12 +18,12 @@ import type { ProjectShape } from './project-profile'
16
18
 
17
19
  const WORKSPACE_YAML = 'pnpm-workspace.yaml'
18
20
  const CLAUDE_MD_FILENAME = 'CLAUDE.md'
19
- const STATIC_CLAUDE_IMPORT = '@node_modules/@joshuafolkken/kit/dist/CLAUDE.static.md'
21
+ const BASIC_CLAUDE_IMPORT = '@node_modules/@joshuafolkken/kit/dist/CLAUDE.basic.md'
20
22
  const PRETTIER_IGNORE = '.prettierignore'
21
23
  const GIT_ATTRIBUTES = '.gitattributes'
22
24
  const CODE_OF_CONDUCT = 'CODE_OF_CONDUCT.md'
23
25
  const SECURITY_MD = 'SECURITY.md'
24
- const STATIC_AI_FILES = [
26
+ const BASIC_AI_FILES = [
25
27
  'AGENTS.md',
26
28
  'GEMINI.md',
27
29
  PRETTIER_IGNORE,
@@ -30,16 +32,16 @@ const STATIC_AI_FILES = [
30
32
  SECURITY_MD,
31
33
  WORKSPACE_YAML,
32
34
  ]
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',
35
+ // A basic project gets its own template for these, not kit's development copy (joshuafolkken/kit#2693).
36
+ const BASIC_SOURCES: Readonly<Record<string, string>> = {
37
+ [PRETTIER_IGNORE]: 'templates/prettierignore.basic',
38
+ [WORKSPACE_YAML]: 'templates/pnpm-workspace.basic.yaml',
37
39
  }
38
40
  const GITHUB_FILES = new Set([CODE_OF_CONDUCT, SECURITY_MD])
39
- const STATIC_POINTER_MAPPING = { src: 'templates/cursorrules.static', dest: '.cursorrules' }
41
+ const BASIC_POINTER_MAPPING = { src: 'templates/cursorrules.basic', dest: '.cursorrules' }
40
42
  const PULL_REQUEST_TEMPLATE = '.github/pull_request_template.md'
41
43
  const RELEASE_CONFIG = '.github/release.yml'
42
- const STATIC_GITHUB_MAPPINGS = [
44
+ const BASIC_GITHUB_MAPPINGS = [
43
45
  { src: PULL_REQUEST_TEMPLATE, dest: PULL_REQUEST_TEMPLATE },
44
46
  { src: RELEASE_CONFIG, dest: RELEASE_CONFIG },
45
47
  ]
@@ -117,9 +119,16 @@ function did_skip_workspace_yaml_copy(source_path: string, destination_path: str
117
119
  return false
118
120
  }
119
121
 
122
+ // The package file an AI file is copied from: a basic project gets its own template for a few of
123
+ // them, every other file is copied from the path it is written to.
124
+ function ai_file_source(filename: string, shape?: ProjectShape): string {
125
+ const basic_source = shape?.profile === 'basic' ? BASIC_SOURCES[filename] : undefined
126
+
127
+ return basic_source ?? filename
128
+ }
129
+
120
130
  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)
131
+ const source_path = package_path(ai_file_source(filename, shape))
123
132
  const destination_path = path.join(PROJECT_ROOT, filename)
124
133
 
125
134
  if (filename === WORKSPACE_YAML) {
@@ -212,19 +221,61 @@ function did_skip_ai_directory_copy(directory_name: string): boolean {
212
221
  // AI file — leaves an existing one untouched so a consumer's additions are never disturbed. `josh sync`
213
222
  // is what ensures the import line on an existing file (sync.ts). CLAUDE.md is no longer byte-copied
214
223
  // (joshuafolkken/kit#1878), so it is handled here rather than through AI_COPY_FILES.
224
+ //
225
+ // The one edit made to an existing file is the rules import written before joshuafolkken/kit#2829,
226
+ // moved onto the basic path — only that kit-written line, never the consumer's additions.
227
+ // An unreadable file is left as it is and reported as skipped, as it was before the migration.
228
+ function read_if_readable(destination_path: string): string | undefined {
229
+ try {
230
+ return readFileSync(destination_path, 'utf8')
231
+ } catch {
232
+ return undefined
233
+ }
234
+ }
235
+
236
+ function report_existing_claude_md(destination_path: string): void {
237
+ const existing = read_if_readable(destination_path) ?? ''
238
+ const migrated = basic_path_migration.migrate_basic_paths(existing)
239
+
240
+ if (migrated === existing) {
241
+ console.info(` ⏭ skipped ${CLAUDE_MD_FILENAME} (already exists)`)
242
+
243
+ return
244
+ }
245
+
246
+ writeFileSync(destination_path, migrated)
247
+ console.info(` ✔ updated ${CLAUDE_MD_FILENAME} (rules import moved to the basic path)`)
248
+ }
249
+
250
+ function basic_claude_md(): string {
251
+ return `> If kit is not installed, run \`pnpm install\` first.\n\n${BASIC_CLAUDE_IMPORT}\n`
252
+ }
253
+
254
+ // A basic CLAUDE.md imports the basic rules alone: the full bootstrap and import are removed, a
255
+ // pre-rename path is moved, and the basic header is restored when no basic import is left
256
+ // (joshuafolkken/kit#2827). The project's own lines stay as they are.
257
+ function ensure_basic_claude_md(existing: string | undefined): string {
258
+ if (existing === undefined) return basic_claude_md()
259
+ const kept = basic_path_migration.migrate_basic_paths(
260
+ distributed_paths.remove_claude_md_import(existing),
261
+ )
262
+
263
+ if (kept.includes(BASIC_CLAUDE_IMPORT)) return kept
264
+
265
+ return kept === '' ? basic_claude_md() : `${basic_claude_md()}\n${kept}`
266
+ }
267
+
215
268
  function did_skip_claude_md_import(shape?: ProjectShape): boolean {
216
269
  const destination_path = path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME)
217
270
 
218
271
  if (existsSync(destination_path)) {
219
- console.info(` ⏭ skipped ${CLAUDE_MD_FILENAME} (already exists)`)
272
+ report_existing_claude_md(destination_path)
220
273
 
221
274
  return true
222
275
  }
223
276
 
224
277
  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)
278
+ shape?.profile === 'basic' ? basic_claude_md() : init_logic.ensure_claude_md_import(undefined)
228
279
 
229
280
  writeFileSync(destination_path, content)
230
281
  console.info(` ✔ created ${CLAUDE_MD_FILENAME}`)
@@ -251,8 +302,8 @@ function should_copy_github_file(filename: string, shape?: ProjectShape): boolea
251
302
  return !GITHUB_FILES.has(filename) && !filename.startsWith('.github/')
252
303
  }
253
304
 
254
- function should_copy_static_file(filename: string, shape?: ProjectShape): boolean {
255
- if (filename !== PRETTIER_IGNORE || shape?.profile !== 'static') return true
305
+ function should_copy_basic_file(filename: string, shape?: ProjectShape): boolean {
306
+ if (filename !== PRETTIER_IGNORE || shape?.profile !== 'basic') return true
256
307
 
257
308
  return shape.has_web
258
309
  }
@@ -261,35 +312,39 @@ function should_copy_ai_file(filename: string, shape?: ProjectShape): boolean {
261
312
  return (
262
313
  should_copy_git_file(filename, shape) &&
263
314
  should_copy_github_file(filename, shape) &&
264
- should_copy_static_file(filename, shape)
315
+ should_copy_basic_file(filename, shape)
265
316
  )
266
317
  }
267
318
 
268
319
  function ai_files(shape?: ProjectShape): ReadonlyArray<string> {
269
- const files = shape?.profile === 'static' ? STATIC_AI_FILES : init_logic.get_ai_copy_files()
320
+ const files = shape?.profile === 'basic' ? BASIC_AI_FILES : init_logic.get_ai_copy_files()
270
321
 
271
322
  return files.filter((filename) => should_copy_ai_file(filename, shape))
272
323
  }
273
324
 
274
- function ai_mapping_skips(shape?: ProjectShape): ReadonlyArray<boolean> {
325
+ type FileMappings = ReturnType<typeof init_logic.get_ai_copy_file_mappings>
326
+
327
+ function ai_file_mappings(shape?: ProjectShape): FileMappings {
275
328
  const mappings =
276
- shape?.profile === 'static'
277
- ? [STATIC_POINTER_MAPPING, ...(shape.has_github ? STATIC_GITHUB_MAPPINGS : [])]
329
+ shape?.profile === 'basic'
330
+ ? [BASIC_POINTER_MAPPING, ...(shape.has_github ? BASIC_GITHUB_MAPPINGS : [])]
278
331
  : init_logic.get_ai_copy_file_mappings()
279
332
 
280
- return mappings
281
- .filter(({ dest }) => shape?.has_github !== false || !dest.startsWith('.github/'))
282
- .map(({ src, dest }) => did_skip_ai_file_mapping(src, dest))
333
+ return mappings.filter(({ dest }) => shape?.has_github !== false || !dest.startsWith('.github/'))
334
+ }
335
+
336
+ function ai_mapping_skips(shape?: ProjectShape): ReadonlyArray<boolean> {
337
+ return ai_file_mappings(shape).map(({ src, dest }) => did_skip_ai_file_mapping(src, dest))
283
338
  }
284
339
 
285
340
  function repository_name(shape?: ProjectShape): string | undefined {
286
- if (shape?.has_github === false || shape?.profile === 'static') return undefined
341
+ if (shape?.has_github === false || shape?.profile === 'basic') return undefined
287
342
 
288
343
  return gh_spawn.get_repo_name_with_owner()
289
344
  }
290
345
 
291
346
  function report_sync_hint(has_skips: boolean, shape?: ProjectShape): void {
292
- if (!has_skips || shape?.profile === 'static') return
347
+ if (!has_skips || shape?.profile === 'basic') return
293
348
 
294
349
  console.info('\n 💡 Run `josh sync` to overwrite skipped AI files with the latest version.')
295
350
  }
@@ -307,7 +362,7 @@ function run_ai_copies(shape?: ProjectShape): string | undefined {
307
362
 
308
363
  const name_with_owner = repository_name(shape)
309
364
 
310
- if (shape?.profile !== 'static') init_sonar.copy_sonar_with_template(name_with_owner)
365
+ if (shape?.profile !== 'basic') init_sonar.copy_sonar_with_template(name_with_owner)
311
366
 
312
367
  report_sync_hint(has_skips, shape)
313
368
 
@@ -318,6 +373,9 @@ const init_ai_copy = {
318
373
  copy_ai_file,
319
374
  run_ai_copies,
320
375
  ai_files,
376
+ ai_file_source,
377
+ ai_file_mappings,
378
+ ensure_basic_claude_md,
321
379
  }
322
380
 
323
- export { init_ai_copy }
381
+ export { init_ai_copy, CLAUDE_MD_FILENAME }
@@ -10,7 +10,7 @@ function is_record(value: unknown): value is Record<string, unknown> {
10
10
  return typeof value === 'object' && value !== null && !Array.isArray(value)
11
11
  }
12
12
 
13
- interface StaticVersions {
13
+ interface BasicVersions {
14
14
  kit: string
15
15
  prettier: string
16
16
  }
@@ -30,10 +30,10 @@ function with_recorded_profile(content: string, profile: ProjectShape['profile']
30
30
  return json_format.format_json(manifest)
31
31
  }
32
32
 
33
- function merge_static_manifest(
33
+ function merge_basic_manifest(
34
34
  content: string,
35
35
  shape: ProjectShape,
36
- versions: StaticVersions,
36
+ versions: BasicVersions,
37
37
  ): string {
38
38
  const with_profile = with_recorded_profile(content, shape.profile)
39
39
  const with_script = init_logic.merge_package_scripts(with_profile, {
@@ -50,6 +50,6 @@ function merge_static_manifest(
50
50
  return init_logic.sort_package_json_keys(with_prettier)
51
51
  }
52
52
 
53
- const init_static = { initial_manifest, with_recorded_profile, merge_static_manifest }
54
- export { init_static }
55
- export type { StaticVersions }
53
+ const init_basic = { initial_manifest, with_recorded_profile, merge_basic_manifest }
54
+ export { init_basic }
55
+ export type { BasicVersions }
@@ -3,13 +3,14 @@ import path from 'node:path'
3
3
  import { z } from 'zod'
4
4
  import { init_install, type InstallStep } from './init-install'
5
5
  import { init_logic_workspace } from './init-logic-workspace'
6
+ import { project_profile } from './project-profile'
6
7
 
7
8
  const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
8
9
  const NODE_MODULES = 'node_modules'
9
10
  const PACKAGE_JSON = 'package.json'
10
- // The static template lists exactly the build scripts kit's own dependency tree carries, which is
11
+ // The basic template lists exactly the build scripts kit's own dependency tree carries, which is
11
12
  // what installing kit alone needs approved (joshuafolkken/kit#2785).
12
- const KIT_BUILDS_TEMPLATE = 'templates/pnpm-workspace.static.yaml'
13
+ const KIT_BUILDS_TEMPLATE = 'templates/pnpm-workspace.basic.yaml'
13
14
  const HANDOFF_FAILURE = 'josh init from the project-installed kit failed — see the output above'
14
15
  // Set on the handed-off run, so a kit that still does not find itself installed in the project — pnpm
15
16
  // put it somewhere else — stops instead of installing and handing off again without end.
@@ -62,7 +63,7 @@ function kit_install_step(has_kit: boolean, template: string): InstallStep {
62
63
  }
63
64
 
64
65
  function handoff_step(args: ReadonlyArray<string>): InstallStep {
65
- const handoff_args = ['exec', 'josh', 'init', ...args]
66
+ const handoff_args = ['exec', 'josh', 'init', ...project_profile.with_legacy_profile_names(args)]
66
67
 
67
68
  return {
68
69
  label: `pnpm ${handoff_args.join(' ')}`,
@@ -51,7 +51,7 @@ function did_step_succeed(step: InstallStep, project_root: string): boolean {
51
51
 
52
52
  // Returns the failure to report, or undefined once the install passed. A failed install stops
53
53
  // there: `josh format` runs prettier and the kit itself, both of which the install provides. A failed
54
- // format does not fail `init` — in a node project it also runs `eslint --fix`, which exits non-zero on
54
+ // format does not fail `init` — in a full project it also runs `eslint --fix`, which exits non-zero on
55
55
  // the project's own unfixable errors, and those are the project's result, not the setup's (the
56
56
  // reason `josh gate` is left out of `init` too).
57
57
  function run_post_init_steps(project_root: string): string | undefined {
@@ -132,7 +132,7 @@ ${stylesheet_line}}
132
132
  }
133
133
 
134
134
  // A named stylesheet is kept only while it exists: kit ≤1.929 wrote the SvelteKit default into every
135
- // node project, and keeping that line would leave `josh sync` unable to repair the aborting format.
135
+ // full project, and keeping that line would leave `josh sync` unable to repair the aborting format.
136
136
  function existing_stylesheet(existing: string, project_root: string): string | undefined {
137
137
  const named = TAILWIND_STYLESHEET_PATTERN.exec(existing)?.[1]
138
138
 
@@ -230,7 +230,7 @@ const PRETTIER_PLUGIN_DEV_DEPS: Record<string, string> = {
230
230
  // format_json rather than JSON.stringify: it lays arrays out the way prettier does — inline while
231
231
  // they fit within printWidth, one element per line once they do not — so the file kit writes is
232
232
  // `prettier --check`-clean in the consumer whatever the entry count happens to be (#660).
233
- // TypeScript 6 defaults `types` to `[]`, so a node project sees no `process` global — not even in the
233
+ // TypeScript 6 defaults `types` to `[]`, so a full project sees no `process` global — not even in the
234
234
  // generated `playwright.config.ts` — until the Node types are named (joshuafolkken/kit#2710).
235
235
  function generate_tsconfig(): string {
236
236
  return json_format.format_json({
@@ -15,11 +15,12 @@ import { execaSync } from 'execa'
15
15
  import { z } from 'zod'
16
16
  import { init_actions, PRETTIER_CONFIG_JS, type FileAction } from './init-actions'
17
17
  import { init_ai_copy } from './init-ai-copy'
18
+ import { init_basic } from './init-basic'
18
19
  import { init_bootstrap } from './init-bootstrap'
19
20
  import { init_install } from './init-install'
20
21
  import { init_logic } from './init-logic'
21
22
  import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
22
- import { init_static } from './init-static'
23
+ import { kit_setup_state } from './kit-setup-state'
23
24
  import { plugin_install_hint_module } from './plugin-install-hint'
24
25
  import { project_profile, type ProjectProfile, type ProjectShape } from './project-profile'
25
26
 
@@ -30,7 +31,7 @@ const SAMPLE_INDENT_WIDTH = 4
30
31
  const ARGUMENT_START_INDEX = 2
31
32
  const SAMPLE_INDENT = ' '.repeat(SAMPLE_INDENT_WIDTH)
32
33
  const INSTALL_HINT = '→ run `pnpm install` to install what package.json lists'
33
- const INIT_USAGE = 'josh init [--profile static|node] [--no-install]'
34
+ const INIT_USAGE = 'josh init [--profile basic|full] [--no-install]'
34
35
 
35
36
  function write_new_file(action: FileAction, destination_path: string): void {
36
37
  mkdirSync(path.dirname(destination_path), { recursive: true })
@@ -44,22 +45,27 @@ function show_sample(action: FileAction): void {
44
45
  console.info(action.create().replaceAll(/^/gmu, () => SAMPLE_INDENT))
45
46
  }
46
47
 
48
+ function report_unchanged(action: FileAction): void {
49
+ if (action.should_show_sample_when_unchanged === true) show_sample(action)
50
+ else console.info(` ✔ unchanged ${action.dest}`)
51
+ }
52
+
47
53
  function merge_existing_file(
48
54
  merge_function: (existing: string) => string,
49
55
  destination_path: string,
50
- destination: string,
56
+ action: FileAction,
51
57
  ): void {
52
58
  const existing = readFileSync(destination_path, 'utf8')
53
59
  const merged = merge_function(existing)
54
60
 
55
61
  if (merged === existing) {
56
- console.info(` ✔ unchanged ${destination}`)
62
+ report_unchanged(action)
57
63
 
58
64
  return
59
65
  }
60
66
 
61
67
  writeFileSync(destination_path, merged)
62
- console.info(` ✔ updated ${destination}`)
68
+ console.info(` ✔ updated ${action.dest}`)
63
69
  }
64
70
 
65
71
  function execute_file_action(action: FileAction): void {
@@ -78,7 +84,7 @@ function execute_file_action(action: FileAction): void {
78
84
  return
79
85
  }
80
86
 
81
- merge_existing_file(action.merge, destination_path, action.dest)
87
+ merge_existing_file(action.merge, destination_path, action)
82
88
  }
83
89
 
84
90
  const engine_pin_schema = z.looseObject({ name: z.string(), version: z.string() })
@@ -181,7 +187,7 @@ function apply_package_json_merges(content: string, has_git = true): string {
181
187
  return package_manager_version.align_development_engines_version(sorted)
182
188
  }
183
189
 
184
- function get_static_versions(): { kit: string; prettier: string } {
190
+ function get_basic_versions(): { kit: string; prettier: string } {
185
191
  const manifest = z
186
192
  .object({
187
193
  version: z.string(),
@@ -195,11 +201,11 @@ function get_static_versions(): { kit: string; prettier: string } {
195
201
  }
196
202
 
197
203
  function merged_manifest(existing: string, shape: ProjectShape): string {
198
- if (shape.profile === 'static') {
199
- return init_static.merge_static_manifest(existing, shape, get_static_versions())
204
+ if (shape.profile === 'basic') {
205
+ return init_basic.merge_basic_manifest(existing, shape, get_basic_versions())
200
206
  }
201
207
 
202
- return init_static.with_recorded_profile(
208
+ return init_basic.with_recorded_profile(
203
209
  apply_package_json_merges(existing, shape.has_git),
204
210
  shape.profile,
205
211
  )
@@ -210,7 +216,7 @@ function merge_project_package_json(shape: ProjectShape): void {
210
216
  const is_existing = existsSync(package_json_path)
211
217
  const existing = is_existing
212
218
  ? readFileSync(package_json_path, 'utf8')
213
- : init_static.initial_manifest()
219
+ : init_basic.initial_manifest()
214
220
  const merged = merged_manifest(existing, shape)
215
221
 
216
222
  if (is_existing && merged === existing) {
@@ -246,7 +252,7 @@ function run_config_file_actions(shape: ProjectShape): void {
246
252
  console.info('Config files:')
247
253
 
248
254
  if (
249
- shape.profile === 'node' &&
255
+ shape.profile === 'full' &&
250
256
  sync.migrate_prettierrc(path.join(PROJECT_ROOT, PRETTIER_CONFIG_JS))
251
257
  ) {
252
258
  console.info(' ✔ migrated .prettierrc → prettier.config.js')
@@ -286,7 +292,7 @@ function run_ai_file_actions(shape: ProjectShape): void {
286
292
 
287
293
  if (shape.has_github) report_repository_settings(name_with_owner)
288
294
 
289
- if (shape.profile === 'node') plugin_install_hint_module.report_plugin_install_hint()
295
+ if (shape.profile === 'full') plugin_install_hint_module.report_plugin_install_hint()
290
296
  }
291
297
 
292
298
  function initialize_project(shape: ProjectShape): void {
@@ -298,14 +304,14 @@ function initialize_project(shape: ProjectShape): void {
298
304
  merge_project_package_json(shape)
299
305
 
300
306
  run_ai_file_actions(shape)
301
- if (shape.profile === 'node') project_config.sync_project_config(PROJECT_ROOT)
307
+ if (shape.profile === 'full') project_config.sync_project_config(PROJECT_ROOT)
302
308
  }
303
309
 
304
310
  // A tool `init` just listed is absent until the user installs it — and `josh lint` then fails on it
305
311
  // rather than skipping it (joshuafolkken/kit#2709). Saying so is what tells the user the next step.
306
312
  function report_manual_install(shape: ProjectShape): void {
307
313
  console.info(`\nDependencies:\n ${INSTALL_HINT}`)
308
- if (shape.has_git && shape.profile === 'node') install_lefthook()
314
+ if (shape.has_git && shape.profile === 'full') install_lefthook()
309
315
  }
310
316
 
311
317
  // The install runs the `prepare` script, which installs the git hooks. A failure is thrown rather
@@ -340,18 +346,34 @@ function set_up_project(requested: ProjectProfile | undefined, is_install: boole
340
346
  console.info('\n✅ Done.\n')
341
347
  }
342
348
 
343
- function main(args: ReadonlyArray<string> = []): void {
344
- if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
349
+ // True when this process set the project up — false when it refused, or handed the run to the
350
+ // project's own kit, which then speaks for itself.
351
+ function main(args: ReadonlyArray<string> = []): boolean {
352
+ if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return false
345
353
  const { is_install, rest } = init_install.split_install_flag(args)
346
354
  // Validated before the hand-off, so a malformed invocation is refused before kit is installed.
347
355
  const requested = project_profile.requested_profile(rest, INIT_USAGE)
348
356
 
349
- if (!is_install || !did_hand_off(args)) set_up_project(requested, is_install)
357
+ const is_set_up_here = !is_install || !did_hand_off(args)
358
+
359
+ if (is_set_up_here) set_up_project(requested, is_install)
360
+
361
+ return is_set_up_here
362
+ }
363
+
364
+ // Only the command a person typed points onward to `josh start`: `josh start` calls `main` itself and
365
+ // is already the next step (joshuafolkken/kit#2816).
366
+ // A setup that failed after writing, such as a failed install, sets the exit code and points nowhere.
367
+ function print_start_hint(): void {
368
+ if (process.exitCode !== undefined && process.exitCode !== 0) return
369
+ const hint = kit_setup_state.start_hint(PROJECT_ROOT)
370
+
371
+ if (hint !== undefined) console.info(`${hint}\n`)
350
372
  }
351
373
 
352
374
  function run_cli(args: ReadonlyArray<string>): void {
353
375
  try {
354
- main(args)
376
+ if (main(args)) print_start_hint()
355
377
  } catch (error) {
356
378
  console.error(`\n✖ ${error instanceof Error ? error.message : String(error)}\n`)
357
379
  process.exitCode = 1
@@ -0,0 +1,30 @@
1
+ import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
2
+ import { start_exec } from './start-exec'
3
+
4
+ const START_HINT =
5
+ 'To use the GitHub Issue workflow, run pnpm exec josh start next: it puts this setup on GitHub, through a pull request when main already has commits.'
6
+
7
+ // Whether the commit checked out records kit in its manifest — the sign that kit's setup has already
8
+ // reached the branch, so `josh start` has no setup pull request to open (joshuafolkken/kit#2816).
9
+ function is_kit_committed(root: string): boolean {
10
+ // `./` resolves the path from `root` rather than from the repository's top level.
11
+ const manifest = start_exec.read_output('git', ['show', 'HEAD:./package.json'], root)
12
+
13
+ return manifest?.includes(`"${KIT_PACKAGE_NAME}"`) ?? false
14
+ }
15
+
16
+ function is_git_work_tree(root: string): boolean {
17
+ return start_exec.succeeds('git', ['rev-parse', '--is-inside-work-tree'], root)
18
+ }
19
+
20
+ // The line `josh init` ends with in a Git repository whose checkout does not hold kit yet: the setup is
21
+ // still uncommitted, and `josh start` is what carries it to GitHub.
22
+ function start_hint(root: string): string | undefined {
23
+ if (!is_git_work_tree(root) || is_kit_committed(root)) return undefined
24
+
25
+ return START_HINT
26
+ }
27
+
28
+ const kit_setup_state = { is_kit_committed, start_hint }
29
+
30
+ export { kit_setup_state }
@@ -0,0 +1,33 @@
1
+ import { PROJECT_CONFIG_FILE } from '#scripts/safe-chain/project-config'
2
+ import { managed_config_scope } from '#scripts/sync/managed-config-scope'
3
+ import { BASIC_PRETTIER_CONFIG } from './init-actions'
4
+ import { CLAUDE_MD_FILENAME } from './init-ai-copy'
5
+
6
+ const MANIFEST_FILE = 'package.json'
7
+ const LOCKFILE = 'pnpm-lock.yaml'
8
+
9
+ // The paths `josh init` writes beyond what `managed_config_scope` enumerates. They stay out of that
10
+ // scope on purpose: it also answers the pull-request gate on distributed config, which leaves
11
+ // CLAUDE.md out by design. The manifest and lockfile are written by the install `josh init` runs.
12
+ const SETUP_ONLY_PATHS: ReadonlySet<string> = new Set([
13
+ CLAUDE_MD_FILENAME,
14
+ BASIC_PRETTIER_CONFIG,
15
+ PROJECT_CONFIG_FILE,
16
+ MANIFEST_FILE,
17
+ LOCKFILE,
18
+ ])
19
+
20
+ // Whether a changed path is one kit's setup wrote — what the setup pull request of `josh start`
21
+ // carries, so a file the user keeps beside it (notes, an editor's own directory) never rides along
22
+ // (joshuafolkken/kit#2816).
23
+ function is_kit_written(file_path: string): boolean {
24
+ return SETUP_ONLY_PATHS.has(file_path) || managed_config_scope.has_managed_path([file_path])
25
+ }
26
+
27
+ function select(paths: ReadonlyArray<string>): Array<string> {
28
+ return paths.filter((file_path) => is_kit_written(file_path))
29
+ }
30
+
31
+ const kit_written_paths = { is_kit_written, select }
32
+
33
+ export { kit_written_paths }
@@ -3,12 +3,26 @@ import { fileURLToPath } from 'node:url'
3
3
  import { project_profile } from './project-profile'
4
4
 
5
5
  const ARGUMENT_START_INDEX = 2
6
+ // A `ci.yml` synced before joshuafolkken/kit#2829 matches `*'profile: static ('*` on this output to
7
+ // hand a basic project to `josh gate`. A consumer can upgrade kit without re-syncing that workflow,
8
+ // so a basic project keeps printing the old phrase on a line of its own until every workflow reads
9
+ // the new name.
10
+ const LEGACY_BASIC_LINE = 'compat: profile: static (the name before kit#2829)'
11
+
12
+ function describe(result: { profile: string; reason: string }): string {
13
+ const line = `profile: ${result.profile} (${result.reason})`
14
+
15
+ return result.profile === 'basic' ? `${line}\n${LEGACY_BASIC_LINE}` : line
16
+ }
6
17
 
7
18
  function main(): void {
8
19
  const requested = project_profile.requested_profile(process.argv.slice(ARGUMENT_START_INDEX))
9
20
  const result = project_profile.resolve_profile(process.cwd(), requested)
10
21
 
11
- console.info(`profile: ${result.profile} (${result.reason})`)
22
+ console.info(describe(result))
12
23
  }
13
24
 
14
25
  if (process.argv[1] === fileURLToPath(import.meta.url)) main()
26
+
27
+ const project_profile_cli = { describe }
28
+ export { project_profile_cli }