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