@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.
- package/.claude/skills/workflow-commands/backlogrun-progress.md +13 -12
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -1
- package/.claude/skills/workflow-commands/fullrun-steps.md +4 -2
- package/.claude/skills/workflow-commands/halfrun.md +3 -1
- package/.claude/skills/workflow-commands/kickoff.md +5 -3
- package/.claude/skills/workflow-commands/observation-filing.md +3 -1
- package/.claude/skills/workflow-commands/prerequisite.md +3 -1
- package/.claude/skills/workflow-commands/target-repository.md +1 -1
- package/.github/pull_request_template.md +12 -0
- package/.github/release.yml +3 -0
- package/.github/workflows/ci.yml +8 -0
- package/.github/workflows/production.yml +10 -1
- package/.github/workflows/publish.yml +72 -2
- package/CLAUDE.md +2 -0
- package/README.md +2 -1
- package/dist/CLAUDE.md +2 -0
- package/dist/CLAUDE.static.md +8 -0
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-264QY6KS.js → chunk-2PGRQ62K.js} +25 -10
- package/dist/hooks/chunk-3V37IID6.js +82 -0
- package/dist/hooks/{chunk-36UE53LS.js → chunk-7XQ6TK3R.js} +2 -2
- package/dist/hooks/{chunk-PRDCNGA6.js → chunk-BNFG2IFK.js} +4 -4
- package/dist/hooks/{chunk-VGYSA2NT.js → chunk-GQ3EVWHY.js} +22 -3
- package/dist/hooks/{chunk-Y5OYCU2F.js → chunk-I3JP3D4A.js} +3 -3
- package/dist/hooks/{chunk-6MQCXEEA.js → chunk-LCJZE42I.js} +25 -88
- package/dist/hooks/{chunk-ZBK4XRAS.js → chunk-NJZSO7BK.js} +3 -3
- package/dist/hooks/{chunk-SSX23SJ2.js → chunk-RKUAD3I6.js} +1 -0
- package/dist/hooks/{chunk-FRJKVSK3.js → chunk-ROULPSQC.js} +2 -2
- package/dist/hooks/{chunk-FNFOLFFV.js → chunk-SXPTRFPG.js} +2 -2
- package/dist/hooks/{chunk-FXAKJKTB.js → chunk-WPALEIRX.js} +1 -1
- package/dist/hooks/{chunk-4OXFU3MK.js → chunk-YO43HEPY.js} +634 -156
- package/dist/hooks/codex-hook-adapter.js +13 -12
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +4 -3
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -11
- package/dist/hooks/session-lang.js +2 -2
- package/dist/hooks/stop-guard.js +5 -5
- package/dist/josh.js +156 -20
- package/eslint/base.js +4 -1
- package/package.json +72 -18
- package/playwright.config.ts +9 -17
- package/ports/index.d.ts +1 -0
- package/ports/index.js +1 -0
- package/ports/web-server.d.ts +5 -0
- package/ports/web-server.js +73 -0
- package/prettier/index.js +3 -8
- package/prettier/static.d.ts +3 -0
- package/prettier/static.js +10 -0
- package/prompts/collaboration-workflow/issue-template.md +11 -4
- package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
- package/prompts/collaboration-workflow/wip-cap.md +2 -2
- package/prompts/review.md +2 -2
- package/prompts/testing-guide.md +1 -0
- package/scripts/agent/agent-role-profile.ts +11 -0
- package/scripts/build/build-claude-md.ts +6 -1
- package/scripts/ci/pr-classification.ts +114 -0
- package/scripts/document/document-byte-budget.ts +3 -2
- package/scripts/gate/gate-report.ts +2 -0
- package/scripts/gate/gate-steps.ts +50 -0
- package/scripts/gate/project-checks.ts +145 -0
- package/scripts/gate/verification-gate.ts +14 -43
- package/scripts/git/git-common-directory.ts +9 -1
- package/scripts/git/git-gh-pr-rollup.ts +29 -1
- package/scripts/git/git-gh-pr.ts +61 -1
- package/scripts/git/git-pr.ts +62 -13
- package/scripts/git/issue-labels.ts +9 -0
- package/scripts/init/distributed-paths.ts +9 -5
- package/scripts/init/hook-command-bootstrap.ts +40 -8
- package/scripts/init/hook-command-rewrite.ts +7 -3
- package/scripts/init/init-actions.ts +94 -7
- package/scripts/init/init-ai-copy.ts +107 -19
- package/scripts/init/init-logic-workspace.ts +38 -5
- package/scripts/init/init-logic.ts +1 -0
- package/scripts/init/init-static.ts +55 -0
- package/scripts/init/init.ts +97 -30
- package/scripts/init/project-profile-cli.ts +14 -0
- package/scripts/init/project-profile.ts +158 -0
- package/scripts/init/start.ts +30 -0
- package/scripts/issue/issue-bug-label.ts +13 -0
- package/scripts/issue/issue-classification.ts +74 -0
- package/scripts/issue/issue-lint-cli.ts +10 -3
- package/scripts/issue/markdown-section.ts +38 -4
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +27 -1
- package/scripts/josh/josh-command-map.ts +3 -0
- package/scripts/josh/josh-commands-development.ts +8 -15
- package/scripts/josh/josh-commands-project.ts +14 -1
- package/scripts/josh/josh-logic.ts +13 -0
- package/scripts/josh/run-github-prerequisite.ts +32 -0
- package/scripts/lane/lane-await.ts +8 -1
- package/scripts/lane/openai-lane-ship.ts +30 -0
- package/scripts/lane/openai-lane-supervisor.ts +35 -8
- package/scripts/lane/openai-review-broker.ts +260 -0
- package/scripts/lint/cspell-cached.ts +21 -0
- package/scripts/lint/format.ts +68 -0
- package/scripts/lint/lint-parallel.ts +30 -1
- package/scripts/observations/observations-flush.ts +1 -1
- package/scripts/package/production-dependency-graph.ts +68 -0
- package/scripts/package/verify-optional-eslint-install.ts +62 -0
- package/scripts/release/github-release-cli.ts +11 -0
- package/scripts/release/github-release.ts +220 -0
- package/scripts/release/publish-tag-cli.ts +8 -0
- package/scripts/release/publish-tag.ts +11 -0
- package/scripts/release/release-publish.ts +7 -2
- package/scripts/rules/delivered-rules.ts +2 -0
- package/scripts/rules/issue-bug-label.ts +85 -0
- package/scripts/rules/issue-filing-args.ts +50 -0
- package/scripts/run/run-merge-cli.ts +19 -0
- package/scripts/run/run-merge.ts +9 -1
- package/scripts/run/run-ship-cli.ts +20 -0
- package/scripts/run/run-ship-detach.ts +180 -14
- package/scripts/run/run-ship-review-steps.ts +45 -25
- package/scripts/run/run-step-cli.ts +11 -1
- package/scripts/run/run-step.ts +36 -8
- package/scripts/run/run-wake-session.ts +3 -1
- package/scripts/test/pilot-files.ts +0 -2
- package/scripts/test/skip-marker.ts +3 -0
- package/scripts/test/test-declared-changed.ts +2 -1
- package/scripts/test/test-declared-logic.ts +15 -10
- package/scripts/test/test-declared.ts +20 -8
- package/scripts/test/test-red-logic.ts +3 -3
- package/scripts/test/test-unit-guard.ts +24 -12
- package/templates/CLAUDE.static.md +8 -0
- package/templates/cursorrules.static +1 -0
- package/templates/pnpm-workspace.static.yaml +4 -0
- package/templates/prettierignore.static +12 -0
- 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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
219
|
+
console.info(` ⏭ skipped ${CLAUDE_MD_FILENAME} (already exists)`)
|
|
191
220
|
|
|
192
221
|
return true
|
|
193
222
|
}
|
|
194
223
|
|
|
195
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.
|
|
211
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
49
|
-
if (!
|
|
50
|
-
const existing_keys = new Set(extract_yaml_top_level_keys(
|
|
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
|
|
85
|
+
if (new_keys.length === 0) return cleaned
|
|
53
86
|
|
|
54
|
-
return append_user_blocks(
|
|
87
|
+
return append_user_blocks(cleaned, new_keys, template)
|
|
55
88
|
}
|
|
56
89
|
|
|
57
90
|
const init_logic_workspace = { merge_workspace_yaml }
|
|
@@ -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 }
|