@joshuafolkken/kit 1.883.0 → 1.921.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/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/dist/CLAUDE.md +2 -0
- package/dist/CLAUDE.static.md +8 -0
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-36UE53LS.js → chunk-3HZ5MJIW.js} +2 -2
- package/dist/hooks/chunk-3V37IID6.js +82 -0
- package/dist/hooks/{chunk-FRJKVSK3.js → chunk-77RLKZ5V.js} +2 -2
- package/dist/hooks/{chunk-VGYSA2NT.js → chunk-LC765SUA.js} +16 -2
- package/dist/hooks/{chunk-ZBK4XRAS.js → chunk-NWD47MW3.js} +3 -3
- package/dist/hooks/{chunk-4OXFU3MK.js → chunk-O2FBZPSW.js} +619 -156
- package/dist/hooks/{chunk-6MQCXEEA.js → chunk-OLTAGGHH.js} +25 -88
- package/dist/hooks/{chunk-FNFOLFFV.js → chunk-OYU6KPRA.js} +1 -1
- package/dist/hooks/{chunk-PRDCNGA6.js → chunk-UANRLZV7.js} +4 -4
- package/dist/hooks/{chunk-264QY6KS.js → chunk-UULARQ5Q.js} +23 -1
- package/dist/hooks/{chunk-Y5OYCU2F.js → chunk-XMR3BS35.js} +3 -3
- package/dist/hooks/codex-hook-adapter.js +11 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +3 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +10 -9
- package/dist/hooks/stop-guard.js +3 -3
- package/dist/josh.js +155 -12
- package/package.json +70 -19
- 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/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 +121 -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 +104 -19
- package/scripts/init/init-static.ts +52 -0
- package/scripts/init/init.ts +97 -30
- package/scripts/init/project-profile-cli.ts +14 -0
- package/scripts/init/project-profile.ts +149 -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 +6 -0
- 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/lint-parallel.ts +44 -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/propagate/propagate-run.ts +6 -0
- package/scripts/propagate/propagate-steps.ts +2 -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-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/scripts/version/version-check-arguments.ts +14 -0
- package/scripts/version/version-check.ts +10 -3
- package/templates/CLAUDE.static.md +8 -0
- package/templates/cursorrules.static +1 -0
- package/templates/prettierignore.static +12 -0
- package/templates/workflows/ci.yml +5 -0
package/scripts/git/git-pr.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pr_classification, type ReleaseClassification } from '#scripts/ci/pr-classification'
|
|
1
2
|
import { animation_helpers, type AnimationOptions } from './animation-helpers'
|
|
2
3
|
import { git_gh_command } from './git-gh-command'
|
|
3
4
|
import type { IssueInfo } from './git-issue'
|
|
@@ -12,7 +13,11 @@ import { pr_info_schema } from './schemas'
|
|
|
12
13
|
// in the same instant the pull request was created it can answer nothing, and `pr_get_url` folds
|
|
13
14
|
// "not there yet" and "the read failed" into the same `undefined`. The five-second sleep used to
|
|
14
15
|
// cover that gap incidentally.
|
|
15
|
-
async function create_pr(
|
|
16
|
+
async function create_pr(
|
|
17
|
+
title: string,
|
|
18
|
+
body: string,
|
|
19
|
+
label: ReleaseClassification,
|
|
20
|
+
): Promise<string | undefined> {
|
|
16
21
|
const config: AnimationOptions<string> = {
|
|
17
22
|
icon_selector: () => '✅',
|
|
18
23
|
error_message: 'Failed to create PR',
|
|
@@ -22,7 +27,7 @@ async function create_pr(title: string, body: string): Promise<string | undefine
|
|
|
22
27
|
try {
|
|
23
28
|
return await animation_helpers.execute_with_animation(
|
|
24
29
|
'Creating pull request...',
|
|
25
|
-
async () => await git_gh_command.pr_create(title, body),
|
|
30
|
+
async () => await git_gh_command.pr_create(title, body, label),
|
|
26
31
|
config,
|
|
27
32
|
)
|
|
28
33
|
} catch (error) {
|
|
@@ -72,8 +77,13 @@ async function report_open_pr(branch_name: string, created_url?: string): Promis
|
|
|
72
77
|
|
|
73
78
|
const PR_STATE_MERGED = 'MERGED'
|
|
74
79
|
|
|
75
|
-
async function create_and_report(
|
|
76
|
-
|
|
80
|
+
async function create_and_report(
|
|
81
|
+
title: string,
|
|
82
|
+
body: string,
|
|
83
|
+
branch_name: string,
|
|
84
|
+
label: ReleaseClassification,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const created_url = await create_pr(title, body, label)
|
|
77
87
|
|
|
78
88
|
await report_open_pr(branch_name, created_url)
|
|
79
89
|
}
|
|
@@ -113,7 +123,9 @@ async function report_existing_pr(
|
|
|
113
123
|
body: string,
|
|
114
124
|
branch_name: string,
|
|
115
125
|
should_replace_body: boolean,
|
|
126
|
+
label: ReleaseClassification,
|
|
116
127
|
): Promise<void> {
|
|
128
|
+
await git_gh_command.pr_ensure_classification(branch_name, label)
|
|
117
129
|
if (should_replace_body) await git_gh_command.pr_update_body(branch_name, body)
|
|
118
130
|
|
|
119
131
|
await report_open_pr(branch_name)
|
|
@@ -123,35 +135,36 @@ async function handle_existing_pr(
|
|
|
123
135
|
title: string,
|
|
124
136
|
body: string,
|
|
125
137
|
branch_name: string,
|
|
126
|
-
should_replace_body: boolean,
|
|
138
|
+
options: { label: ReleaseClassification; should_replace_body: boolean },
|
|
127
139
|
): Promise<void> {
|
|
128
140
|
const pr_state_result = await get_pr_state_safe(branch_name)
|
|
129
141
|
|
|
130
142
|
if (is_pr_state_merged(pr_state_result)) {
|
|
131
143
|
git_pr_messages.display_merged_pr_message()
|
|
132
|
-
await create_and_report(title, body, branch_name)
|
|
144
|
+
await create_and_report(title, body, branch_name, options.label)
|
|
133
145
|
|
|
134
146
|
return
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
await report_existing_pr(body, branch_name, should_replace_body)
|
|
149
|
+
await report_existing_pr(body, branch_name, options.should_replace_body, options.label)
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
async function create(
|
|
141
153
|
title: string,
|
|
142
154
|
body: string,
|
|
143
155
|
branch_name: string,
|
|
144
|
-
should_replace_body
|
|
156
|
+
options: { label: ReleaseClassification; should_replace_body?: boolean },
|
|
145
157
|
): Promise<void> {
|
|
158
|
+
const { label, should_replace_body = false } = options
|
|
146
159
|
const has_pr = await git_gh_command.pr_exists(branch_name)
|
|
147
160
|
|
|
148
161
|
if (!has_pr) {
|
|
149
|
-
await create_and_report(title, body, branch_name)
|
|
162
|
+
await create_and_report(title, body, branch_name, label)
|
|
150
163
|
|
|
151
164
|
return
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
await handle_existing_pr(title, body, branch_name, should_replace_body)
|
|
167
|
+
await handle_existing_pr(title, body, branch_name, { label, should_replace_body })
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
function build_title(issue_info: IssueInfo): string {
|
|
@@ -166,11 +179,47 @@ function build_body(issue_info: IssueInfo, extra_body?: string): string {
|
|
|
166
179
|
return `${closes}\n\n${extra_body}`
|
|
167
180
|
}
|
|
168
181
|
|
|
182
|
+
async function existing_pr_label(branch_name: string): Promise<ReleaseClassification | undefined> {
|
|
183
|
+
if (!(await git_gh_command.pr_exists(branch_name))) return undefined
|
|
184
|
+
if (is_pr_state_merged(await get_pr_state_safe(branch_name))) return undefined
|
|
185
|
+
|
|
186
|
+
return await git_gh_command.pr_get_classification(branch_name)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function create_for_issue(
|
|
190
|
+
issue_info: IssueInfo,
|
|
191
|
+
extra_body: string | undefined,
|
|
192
|
+
label: ReleaseClassification,
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
await create(
|
|
195
|
+
build_title(issue_info),
|
|
196
|
+
build_body(issue_info, extra_body),
|
|
197
|
+
issue_info.branch_name,
|
|
198
|
+
{
|
|
199
|
+
label,
|
|
200
|
+
should_replace_body: extra_body !== undefined,
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
169
205
|
async function create_with_issue_info(issue_info: IssueInfo, extra_body?: string): Promise<void> {
|
|
170
|
-
const
|
|
171
|
-
|
|
206
|
+
const existing_label = await existing_pr_label(issue_info.branch_name)
|
|
207
|
+
|
|
208
|
+
if (existing_label !== undefined) {
|
|
209
|
+
await create_for_issue(issue_info, extra_body, existing_label)
|
|
210
|
+
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const issue_json = await git_gh_command.issue_view_json(issue_info.number, 'labels,body')
|
|
215
|
+
|
|
216
|
+
if (issue_json === undefined) {
|
|
217
|
+
throw new Error(`Could not read issue #${issue_info.number} for release classification`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const label = pr_classification.select_issue_classification(issue_json)
|
|
172
221
|
|
|
173
|
-
await
|
|
222
|
+
await create_for_issue(issue_info, extra_body, label)
|
|
174
223
|
}
|
|
175
224
|
|
|
176
225
|
const git_pr = {
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// labeler and the next-issues display all key on these exact strings, and a drifted copy would
|
|
3
3
|
// fail silently — an epic filtered on the wrong name is simply never closed or never excluded.
|
|
4
4
|
const EPIC_LABEL = 'epic'
|
|
5
|
+
const BUG_LABEL = 'bug'
|
|
6
|
+
const ENHANCEMENT_LABEL = 'enhancement'
|
|
7
|
+
const BREAKING_CHANGE_LABEL = 'breaking-change'
|
|
5
8
|
const IN_PROGRESS_LABEL = 'in-progress'
|
|
6
9
|
// Parks a child that cannot advance without a person deciding something. `epic:next` is what reads
|
|
7
10
|
// it: a parked child is why a run reports "nothing left that time will fix" rather than waiting
|
|
@@ -237,6 +240,9 @@ const REVIEW_ROUND2_SKIPPED_LABEL = 'review-round2-skipped'
|
|
|
237
240
|
|
|
238
241
|
const ALL_LABELS: ReadonlySet<string> = new Set([
|
|
239
242
|
EPIC_LABEL,
|
|
243
|
+
BUG_LABEL,
|
|
244
|
+
ENHANCEMENT_LABEL,
|
|
245
|
+
BREAKING_CHANGE_LABEL,
|
|
240
246
|
IN_PROGRESS_LABEL,
|
|
241
247
|
NEEDS_DECISION_LABEL,
|
|
242
248
|
AUTO_OK_LABEL,
|
|
@@ -256,12 +262,15 @@ export {
|
|
|
256
262
|
ALL_LABELS,
|
|
257
263
|
ALREADY_DONE_LABEL,
|
|
258
264
|
AUTO_OK_LABEL,
|
|
265
|
+
BUG_LABEL,
|
|
266
|
+
BREAKING_CHANGE_LABEL,
|
|
259
267
|
depth_label_of,
|
|
260
268
|
DEPTH_0_LABEL,
|
|
261
269
|
DEPTH_1_LABEL,
|
|
262
270
|
DEPTH_2_LABEL,
|
|
263
271
|
DEPTH_LABEL_ORDER,
|
|
264
272
|
DEPTH_LABELS,
|
|
273
|
+
ENHANCEMENT_LABEL,
|
|
265
274
|
EPIC_LABEL,
|
|
266
275
|
FILING_ROUTE_LABELS,
|
|
267
276
|
has_any_label,
|
|
@@ -20,8 +20,10 @@ const GITHUB_REPO_TREE_BASE = 'https://github.com/joshuafolkken/kit/tree/main/'
|
|
|
20
20
|
// The consumer imports kit's published, already path-transformed rules. A tracked bootstrap note
|
|
21
21
|
// remains readable before installation, while the rule body stays in the package (kit#1878).
|
|
22
22
|
const CLAUDE_MD_IMPORT_LINE = '@node_modules/@joshuafolkken/kit/dist/CLAUDE.md'
|
|
23
|
-
const
|
|
23
|
+
const OLD_CLAUDE_MD_BOOTSTRAP =
|
|
24
24
|
'> Fresh checkout: if kit is not installed, run `pnpm install` first, then reread this file before doing any other work.'
|
|
25
|
+
const CLAUDE_MD_BOOTSTRAP =
|
|
26
|
+
'> Fresh checkout: before `pnpm install`, run `pnpm config get "@joshuafolkken:registry"` from the project root to check the effective registry, including user-level settings. If it points to GitHub Packages, [set up authentication](https://github.com/joshuafolkken/kit/blob/main/docs/authentication.md#1-get-a-token-from-the-gh-cli) first. Then run `pnpm install` and reread this file before doing any other work.'
|
|
25
27
|
|
|
26
28
|
// A span containing `*` is excluded: it is a **glob**, not a reference to a file a consumer can
|
|
27
29
|
// open. A distributed document that writes a directory set as `prompts/**` means "anything beneath
|
|
@@ -84,13 +86,15 @@ function transform_distributed_paths(content: string): string {
|
|
|
84
86
|
function ensure_claude_md_import(existing: string | undefined): string {
|
|
85
87
|
if (existing === undefined) return `${CLAUDE_MD_BOOTSTRAP}\n\n${CLAUDE_MD_IMPORT_LINE}\n`
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const updated = existing.replace(OLD_CLAUDE_MD_BOOTSTRAP, () => CLAUDE_MD_BOOTSTRAP)
|
|
90
|
+
|
|
91
|
+
if (updated.includes(CLAUDE_MD_IMPORT_LINE)) {
|
|
92
|
+
if (updated.includes(CLAUDE_MD_BOOTSTRAP)) return updated
|
|
89
93
|
|
|
90
|
-
return `${CLAUDE_MD_BOOTSTRAP}\n\n${
|
|
94
|
+
return `${CLAUDE_MD_BOOTSTRAP}\n\n${updated}`
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
return `${CLAUDE_MD_BOOTSTRAP}\n\n${CLAUDE_MD_IMPORT_LINE}\n\n${
|
|
97
|
+
return `${CLAUDE_MD_BOOTSTRAP}\n\n${CLAUDE_MD_IMPORT_LINE}\n\n${updated}`
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
const distributed_paths = {
|
|
@@ -10,6 +10,11 @@ interface HookResult {
|
|
|
10
10
|
stderr: string
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
interface CheckoutOptions {
|
|
14
|
+
git_overrides?: Record<string, string>
|
|
15
|
+
is_external_git?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
function create_partial_install(temporary_root: string): void {
|
|
14
19
|
const package_root = path.join(temporary_root, 'node_modules/@joshuafolkken/kit')
|
|
15
20
|
const hook_path = path.join(package_root, 'dist/hooks/pretool-guard.js')
|
|
@@ -19,25 +24,52 @@ function create_partial_install(temporary_root: string): void {
|
|
|
19
24
|
writeFileSync(hook_path, "process.stdout.write('guard ran')")
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
function
|
|
23
|
-
|
|
27
|
+
function execution_directory(temporary_root: string, is_nested: boolean): string {
|
|
28
|
+
if (!is_nested) return temporary_root
|
|
29
|
+
|
|
30
|
+
const nested_root = path.join(temporary_root, 'src')
|
|
31
|
+
|
|
32
|
+
mkdirSync(nested_root)
|
|
33
|
+
|
|
34
|
+
return nested_root
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function checkout_environment(temporary_root: string, options: CheckoutOptions): NodeJS.ProcessEnv {
|
|
38
|
+
const metadata_root = path.join(temporary_root, 'metadata')
|
|
24
39
|
const git_environment = {
|
|
25
40
|
...process.env,
|
|
26
41
|
...git_location_environment.location_free_environment(),
|
|
27
42
|
}
|
|
43
|
+
const git_arguments = options.is_external_git
|
|
44
|
+
? ['init', '--bare', '-q', metadata_root]
|
|
45
|
+
: ['init', '-q']
|
|
46
|
+
|
|
47
|
+
spawnSync('git', git_arguments, { cwd: temporary_root, env: git_environment })
|
|
48
|
+
|
|
49
|
+
if (options.is_external_git) {
|
|
50
|
+
return { ...git_environment, GIT_DIR: metadata_root, GIT_WORK_TREE: temporary_root }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { ...git_environment, ...options.git_overrides }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function run_in_temporary_checkout(
|
|
57
|
+
command: string,
|
|
58
|
+
is_partial_install = false,
|
|
59
|
+
is_nested = false,
|
|
60
|
+
options: CheckoutOptions = {},
|
|
61
|
+
): HookResult {
|
|
62
|
+
const temporary_root = mkdtempSync(path.join(tmpdir(), 'kit-hook-bootstrap-'))
|
|
28
63
|
|
|
29
64
|
try {
|
|
30
|
-
|
|
31
|
-
cwd: temporary_root,
|
|
32
|
-
env: git_environment,
|
|
33
|
-
})
|
|
65
|
+
const hook_environment = checkout_environment(temporary_root, options)
|
|
34
66
|
|
|
35
67
|
if (is_partial_install) create_partial_install(temporary_root)
|
|
36
68
|
|
|
37
69
|
const { status, stdout, stderr } = spawnSync('sh', ['-c', command], {
|
|
38
|
-
cwd: temporary_root,
|
|
70
|
+
cwd: execution_directory(temporary_root, is_nested),
|
|
39
71
|
encoding: 'utf8',
|
|
40
|
-
env:
|
|
72
|
+
env: hook_environment,
|
|
41
73
|
})
|
|
42
74
|
|
|
43
75
|
return { status, stdout, stderr }
|
|
@@ -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,31 @@ 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
|
+
]
|
|
32
|
+
const GITHUB_FILES = new Set([CODE_OF_CONDUCT, SECURITY_MD])
|
|
33
|
+
const STATIC_POINTER_MAPPING = { src: 'templates/cursorrules.static', dest: '.cursorrules' }
|
|
34
|
+
const PULL_REQUEST_TEMPLATE = '.github/pull_request_template.md'
|
|
35
|
+
const RELEASE_CONFIG = '.github/release.yml'
|
|
36
|
+
const STATIC_GITHUB_MAPPINGS = [
|
|
37
|
+
{ src: PULL_REQUEST_TEMPLATE, dest: PULL_REQUEST_TEMPLATE },
|
|
38
|
+
{ src: RELEASE_CONFIG, dest: RELEASE_CONFIG },
|
|
39
|
+
]
|
|
18
40
|
|
|
19
41
|
// Whether the existing file is a workflow that carries no header. A read failure answers "no": the
|
|
20
42
|
// point is to warn, and a warning is not worth failing a command that used to open nothing.
|
|
@@ -59,7 +81,7 @@ function did_skip_copy_if_absent(
|
|
|
59
81
|
label: string,
|
|
60
82
|
): boolean {
|
|
61
83
|
if (existsSync(destination_path)) {
|
|
62
|
-
console.info(` ⏭ skipped ${label} (already exists
|
|
84
|
+
console.info(` ⏭ skipped ${label} (already exists)`)
|
|
63
85
|
did_warn_unstamped_workflow(destination_path, label)
|
|
64
86
|
|
|
65
87
|
return true
|
|
@@ -89,8 +111,12 @@ function did_skip_workspace_yaml_copy(source_path: string, destination_path: str
|
|
|
89
111
|
return false
|
|
90
112
|
}
|
|
91
113
|
|
|
92
|
-
function did_skip_ai_file_copy(filename: string): boolean {
|
|
93
|
-
const source_path = package_path(
|
|
114
|
+
function did_skip_ai_file_copy(filename: string, shape?: ProjectShape): boolean {
|
|
115
|
+
const source_path = package_path(
|
|
116
|
+
filename === PRETTIER_IGNORE && shape?.profile === 'static'
|
|
117
|
+
? 'templates/prettierignore.static'
|
|
118
|
+
: filename,
|
|
119
|
+
)
|
|
94
120
|
const destination_path = path.join(PROJECT_ROOT, filename)
|
|
95
121
|
|
|
96
122
|
if (filename === WORKSPACE_YAML) {
|
|
@@ -183,16 +209,21 @@ function did_skip_ai_directory_copy(directory_name: string): boolean {
|
|
|
183
209
|
// AI file — leaves an existing one untouched so a consumer's additions are never disturbed. `josh sync`
|
|
184
210
|
// is what ensures the import line on an existing file (sync.ts). CLAUDE.md is no longer byte-copied
|
|
185
211
|
// (joshuafolkken/kit#1878), so it is handled here rather than through AI_COPY_FILES.
|
|
186
|
-
function did_skip_claude_md_import(): boolean {
|
|
212
|
+
function did_skip_claude_md_import(shape?: ProjectShape): boolean {
|
|
187
213
|
const destination_path = path.join(PROJECT_ROOT, CLAUDE_MD_FILENAME)
|
|
188
214
|
|
|
189
215
|
if (existsSync(destination_path)) {
|
|
190
|
-
console.info(` ⏭ skipped ${CLAUDE_MD_FILENAME} (already exists
|
|
216
|
+
console.info(` ⏭ skipped ${CLAUDE_MD_FILENAME} (already exists)`)
|
|
191
217
|
|
|
192
218
|
return true
|
|
193
219
|
}
|
|
194
220
|
|
|
195
|
-
|
|
221
|
+
const content =
|
|
222
|
+
shape?.profile === 'static'
|
|
223
|
+
? `> If kit is not installed, run \`pnpm install\` first.\n\n${STATIC_CLAUDE_IMPORT}\n`
|
|
224
|
+
: init_logic.ensure_claude_md_import(undefined)
|
|
225
|
+
|
|
226
|
+
writeFileSync(destination_path, content)
|
|
196
227
|
console.info(` ✔ created ${CLAUDE_MD_FILENAME}`)
|
|
197
228
|
|
|
198
229
|
return false
|
|
@@ -201,14 +232,69 @@ function did_skip_claude_md_import(): boolean {
|
|
|
201
232
|
// Returns the repository name resolved for the Sonar config, so `josh init` can reuse it for the
|
|
202
233
|
// security-updates report instead of spawning a second `gh repo view` (joshuafolkken/kit#805).
|
|
203
234
|
// 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
|
-
|
|
235
|
+
function should_copy_git_file(filename: string, shape?: ProjectShape): boolean {
|
|
236
|
+
if (shape?.has_git !== false) return true
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
filename !== GIT_ATTRIBUTES &&
|
|
240
|
+
!filename.startsWith('.claude/') &&
|
|
241
|
+
!filename.startsWith('.codex/')
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function should_copy_github_file(filename: string, shape?: ProjectShape): boolean {
|
|
246
|
+
if (shape?.has_github !== false) return true
|
|
247
|
+
|
|
248
|
+
return !GITHUB_FILES.has(filename) && !filename.startsWith('.github/')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function should_copy_static_file(filename: string, shape?: ProjectShape): boolean {
|
|
252
|
+
if (filename !== PRETTIER_IGNORE || shape?.profile !== 'static') return true
|
|
253
|
+
|
|
254
|
+
return shape.has_web
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function should_copy_ai_file(filename: string, shape?: ProjectShape): boolean {
|
|
258
|
+
return (
|
|
259
|
+
should_copy_git_file(filename, shape) &&
|
|
260
|
+
should_copy_github_file(filename, shape) &&
|
|
261
|
+
should_copy_static_file(filename, shape)
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function ai_files(shape?: ProjectShape): ReadonlyArray<string> {
|
|
266
|
+
const files = shape?.profile === 'static' ? STATIC_AI_FILES : init_logic.get_ai_copy_files()
|
|
267
|
+
|
|
268
|
+
return files.filter((filename) => should_copy_ai_file(filename, shape))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function ai_mapping_skips(shape?: ProjectShape): ReadonlyArray<boolean> {
|
|
272
|
+
const mappings =
|
|
273
|
+
shape?.profile === 'static'
|
|
274
|
+
? [STATIC_POINTER_MAPPING, ...(shape.has_github ? STATIC_GITHUB_MAPPINGS : [])]
|
|
275
|
+
: init_logic.get_ai_copy_file_mappings()
|
|
276
|
+
|
|
277
|
+
return mappings
|
|
278
|
+
.filter(({ dest }) => shape?.has_github !== false || !dest.startsWith('.github/'))
|
|
279
|
+
.map(({ src, dest }) => did_skip_ai_file_mapping(src, dest))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function repository_name(shape?: ProjectShape): string | undefined {
|
|
283
|
+
if (shape?.has_github === false || shape?.profile === 'static') return undefined
|
|
284
|
+
|
|
285
|
+
return gh_spawn.get_repo_name_with_owner()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function report_sync_hint(has_skips: boolean, shape?: ProjectShape): void {
|
|
289
|
+
if (!has_skips || shape?.profile === 'static') return
|
|
290
|
+
|
|
291
|
+
console.info('\n 💡 Run `josh sync` to overwrite skipped AI files with the latest version.')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function run_ai_copies(shape?: ProjectShape): string | undefined {
|
|
295
|
+
const did_skip_claude_md = did_skip_claude_md_import(shape)
|
|
296
|
+
const file_skips = ai_files(shape).map((filename) => did_skip_ai_file_copy(filename, shape))
|
|
297
|
+
const mapping_skips = ai_mapping_skips(shape)
|
|
212
298
|
const directory_skips = init_logic
|
|
213
299
|
.get_ai_copy_directories()
|
|
214
300
|
.map((directory_name) => did_skip_ai_directory_copy(directory_name))
|
|
@@ -216,13 +302,11 @@ function run_ai_copies(): string | undefined {
|
|
|
216
302
|
Boolean,
|
|
217
303
|
)
|
|
218
304
|
|
|
219
|
-
const name_with_owner =
|
|
305
|
+
const name_with_owner = repository_name(shape)
|
|
220
306
|
|
|
221
|
-
init_sonar.copy_sonar_with_template(name_with_owner)
|
|
307
|
+
if (shape?.profile !== 'static') init_sonar.copy_sonar_with_template(name_with_owner)
|
|
222
308
|
|
|
223
|
-
|
|
224
|
-
console.info('\n 💡 Run `josh sync` to overwrite skipped AI files with the latest version.')
|
|
225
|
-
}
|
|
309
|
+
report_sync_hint(has_skips, shape)
|
|
226
310
|
|
|
227
311
|
return name_with_owner
|
|
228
312
|
}
|
|
@@ -230,6 +314,7 @@ function run_ai_copies(): string | undefined {
|
|
|
230
314
|
const init_ai_copy = {
|
|
231
315
|
copy_ai_file,
|
|
232
316
|
run_ai_copies,
|
|
317
|
+
ai_files,
|
|
233
318
|
}
|
|
234
319
|
|
|
235
320
|
export { init_ai_copy }
|