@joshuafolkken/kit 1.1387.0 → 1.1435.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/epic-commands/SKILL.md +1 -1
- package/.claude/skills/epic-commands/epic-bundle.md +5 -0
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
- package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
- package/.claude/skills/workflow-commands/backlogrun.md +1 -0
- package/.claude/skills/workflow-commands/chain-rule.md +4 -2
- package/.claude/skills/workflow-commands/issue-comments.md +6 -0
- package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
- package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
- package/README.md +25 -0
- package/dist/hooks/audit-provision.js +2 -2
- package/dist/hooks/batch-guard.js +7 -7
- package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
- package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
- package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
- package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
- package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
- package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
- package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
- package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
- package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
- package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
- package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
- package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
- package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
- package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
- package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
- package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
- package/dist/hooks/codex-hook-adapter.js +15 -15
- package/dist/hooks/duplicate-read-guard.js +8 -8
- package/dist/hooks/format-edited.js +5 -5
- package/dist/hooks/inputs.json +288 -229
- package/dist/hooks/investigation-guard.js +8 -8
- package/dist/hooks/pretool-guard.js +14 -14
- package/dist/hooks/session-lang.js +69 -16
- package/dist/hooks/stop-guard.js +13 -38
- package/dist/josh.js +62 -9
- package/package.json +4 -3
- package/prompts/review.md +22 -0
- package/scripts/agent/claude-agent-argv.ts +13 -1
- package/scripts/auth/auth-setup-cli.ts +101 -0
- package/scripts/auth/claude-token-output.ts +78 -0
- package/scripts/auth/claude-token.ts +68 -0
- package/scripts/backlog/backlog-drive-epic.ts +24 -8
- package/scripts/backlog/backlog-drive-launch.ts +3 -0
- package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
- package/scripts/backlog/backlog-drive-named.ts +4 -2
- package/scripts/backlog/backlog-drive-unread.ts +56 -0
- package/scripts/backlog/backlog-drive.ts +16 -8
- package/scripts/backlog/backlog-overlap.ts +22 -5
- package/scripts/backlog/backlog-ready.ts +1 -3
- package/scripts/backlog/backlogrun-args.ts +13 -2
- package/scripts/backlog/backlogrun-cli.ts +91 -19
- package/scripts/backlog/backlogrun-launch.ts +24 -0
- package/scripts/backlog/backlogrun-replace.ts +166 -0
- package/scripts/backlog/backlogrun-stop.ts +178 -0
- package/scripts/behavior/behavior-cli.ts +6 -1
- package/scripts/doctor/doctor-io.ts +60 -9
- package/scripts/doctor/doctor.ts +20 -1
- package/scripts/document/document-byte-budget.ts +3 -3
- package/scripts/epic/epic-bundle-cli.ts +8 -8
- package/scripts/epic/epic-bundle-referenced.ts +2 -5
- package/scripts/epic/epic-bundle.ts +39 -4
- package/scripts/exports/unused-members-cli.ts +2 -1
- package/scripts/followup/followup-checks-event.ts +82 -0
- package/scripts/followup/git-followup-finish.ts +16 -2
- package/scripts/followup/git-followup-workflow.ts +5 -2
- package/scripts/followup/git-pr-followup-checks.ts +18 -4
- package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
- package/scripts/followup/git-pr-followup.ts +4 -1
- package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
- package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
- package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
- package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
- package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
- package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
- package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
- package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
- package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
- package/scripts/gate/gate-report.ts +70 -15
- package/scripts/gate/project-checks.ts +13 -2
- package/scripts/gate/scoped-green.ts +2 -1
- package/scripts/gate/verification-gate.ts +4 -3
- package/scripts/gh/gh-author-association.ts +14 -0
- package/scripts/gh/git-gh-conditional.ts +66 -0
- package/scripts/gh/git-gh-exec.ts +1 -1
- package/scripts/gh/git-gh-issue-read.ts +6 -3
- package/scripts/gh/git-gh-issue-write.ts +5 -1
- package/scripts/gh/git-gh-issue.ts +4 -1
- package/scripts/gh/git-pr-checks-eval.ts +13 -0
- package/scripts/gh/git-pr-checks-parse.ts +8 -1
- package/scripts/gh/git-pr-checks-watch.ts +11 -4
- package/scripts/gh/git-pr-checks.ts +36 -2
- package/scripts/git/git-command.ts +16 -4
- package/scripts/git/git-schemas.ts +1 -0
- package/scripts/git/git-spawn-sync.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +3 -1
- package/scripts/init/hook-command-rewrite.ts +31 -0
- package/scripts/init/init-actions.ts +4 -1
- package/scripts/init/init-ai-copy.ts +33 -10
- package/scripts/init/init-basic.ts +8 -3
- package/scripts/init/init-logic-toolchain.ts +76 -0
- package/scripts/init/init-logic.ts +2 -0
- package/scripts/init/init.ts +5 -23
- package/scripts/init/project-profile.ts +36 -7
- package/scripts/init/start-exec.ts +27 -1
- package/scripts/init/start-plan.ts +15 -0
- package/scripts/init/start-steps.ts +46 -0
- package/scripts/init/start.ts +1 -0
- package/scripts/init/workflow-mark.ts +35 -0
- package/scripts/issue/issue-ack-cli.ts +84 -0
- package/scripts/issue/issue-labels.ts +40 -8
- package/scripts/issue/issue-state-cli.ts +14 -4
- package/scripts/issue/issue-watch-diff.ts +152 -0
- package/scripts/issue/issue-watch-gate.ts +81 -0
- package/scripts/issue/issue-watch-hook.ts +54 -0
- package/scripts/issue/issue-watch.ts +240 -0
- package/scripts/issue/measure-rerun-cli.ts +3 -4
- package/scripts/josh/josh-command-map.ts +4 -0
- package/scripts/josh/josh-commands-backlog.ts +2 -2
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-issue.ts +7 -0
- package/scripts/josh/josh-commands-lane.ts +11 -2
- package/scripts/josh/josh-commands-project.ts +24 -0
- package/scripts/josh/josh-verdict.ts +33 -4
- package/scripts/lane/claude-lane-broker.ts +142 -0
- package/scripts/lane/lane-await.ts +6 -2
- package/scripts/lane/lane-cache.ts +37 -7
- package/scripts/lane/lane-capacity.ts +8 -6
- package/scripts/lane/lane-child-invocation.ts +1 -0
- package/scripts/lane/lane-cli.ts +15 -1
- package/scripts/lane/lane-close.ts +15 -6
- package/scripts/lane/lane-dispatch.ts +36 -10
- package/scripts/lane/lane-environment.ts +1 -1
- package/scripts/lane/lane-launch-cli.ts +13 -4
- package/scripts/lane/lane-ledger.ts +21 -3
- package/scripts/lane/lane-measure-cli.ts +10 -2
- package/scripts/lane/lane-open.ts +9 -1
- package/scripts/lane/lane-preserve.ts +49 -0
- package/scripts/lane/lane-reap.ts +20 -5
- package/scripts/lane/lane-registry.ts +6 -0
- package/scripts/lane/lane-restart-cli.ts +51 -0
- package/scripts/lane/lane-restart.ts +175 -0
- package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
- package/scripts/lane/lane-spent.ts +195 -0
- package/scripts/lane/lane-stage-stats.ts +27 -3
- package/scripts/lane/lane-stop-stats.ts +96 -0
- package/scripts/lane/openai-lane-supervisor.ts +2 -2
- package/scripts/lane/process-tools.ts +31 -0
- package/scripts/lib/buffered-process.ts +36 -2
- package/scripts/lib/pnpm-reach.ts +27 -0
- package/scripts/lib/status-icons.ts +5 -1
- package/scripts/lint/lint-parallel.ts +19 -2
- package/scripts/observations/observation-ledger-home.ts +1 -0
- package/scripts/review/review-attest-cli.ts +21 -6
- package/scripts/review/review-brief-cli.ts +2 -2
- package/scripts/review/review-comment-round.ts +114 -0
- package/scripts/review/review-record-cli.ts +39 -17
- package/scripts/rules/implementation-cut.ts +3 -3
- package/scripts/rules/pre-gate-cut.ts +1 -1
- package/scripts/run/board/run-board-checks.ts +42 -0
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-every.ts +3 -1
- package/scripts/run/board/run-board-fit.ts +16 -5
- package/scripts/run/board/run-board-fresh.ts +64 -0
- package/scripts/run/board/run-board-header.ts +129 -53
- package/scripts/run/board/run-board-labels.ts +47 -4
- package/scripts/run/board/run-board-lane-event.ts +66 -0
- package/scripts/run/board/run-board-layout.ts +57 -9
- package/scripts/run/board/run-board-now.ts +234 -0
- package/scripts/run/board/run-board-operation-read.ts +75 -0
- package/scripts/run/board/run-board-operation.ts +150 -0
- package/scripts/run/board/run-board-phase.ts +26 -1
- package/scripts/run/board/run-board-plan.ts +4 -6
- package/scripts/run/board/run-board-read.ts +49 -3
- package/scripts/run/board/run-board-reason.ts +124 -0
- package/scripts/run/board/run-board-render-checks.ts +55 -0
- package/scripts/run/board/run-board-render-legend.ts +12 -3
- package/scripts/run/board/run-board-render-slot.ts +2 -2
- package/scripts/run/board/run-board-render-tail.ts +128 -0
- package/scripts/run/board/run-board-render-thanks.ts +50 -0
- package/scripts/run/board/run-board-render-title.ts +58 -0
- package/scripts/run/board/run-board-render.ts +96 -80
- package/scripts/run/board/run-board-row-yield.ts +32 -0
- package/scripts/run/board/run-board-setup.ts +61 -13
- package/scripts/run/board/run-board-state.ts +10 -0
- package/scripts/run/board/run-board-status.ts +66 -7
- package/scripts/run/board/run-board-thanks-csv.ts +71 -0
- package/scripts/run/board/run-board-thanks-read.ts +112 -0
- package/scripts/run/board/run-board-thanks.ts +53 -0
- package/scripts/run/board/run-board-tick.ts +50 -21
- package/scripts/run/board/run-board-usual.ts +44 -0
- package/scripts/run/carry/run-carry-args.ts +9 -1
- package/scripts/run/carry/run-carry-cli.ts +2 -45
- package/scripts/run/carry/run-carry-end.ts +73 -0
- package/scripts/run/carry/run-carry.ts +8 -1
- package/scripts/run/entry/run-entry-cli.ts +5 -0
- package/scripts/run/event/run-event-checks.ts +54 -0
- package/scripts/run/event/run-event-render.ts +2 -0
- package/scripts/run/event/run-event-scope.ts +1 -0
- package/scripts/run/event/run-event-setup.ts +55 -11
- package/scripts/run/event/run-event-stream.ts +27 -11
- package/scripts/run/merge/run-merge-cli.ts +7 -4
- package/scripts/run/merge/run-merge-ship.ts +61 -0
- package/scripts/run/merge/run-merge-steps.ts +7 -14
- package/scripts/run/merge/run-merge-token.ts +16 -2
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-step.ts +2 -0
- package/scripts/run/ship/run-ship-cli.ts +22 -2
- package/scripts/run/ship/run-ship-return.ts +31 -13
- package/scripts/run/ship/run-ship-review-steps.ts +17 -4
- package/scripts/run/ship/run-ship-stage.ts +1 -0
- package/scripts/run/wake/run-wake-await.ts +130 -0
- package/scripts/run/wake/run-wake-cli.ts +60 -71
- package/scripts/run/wake/run-wake-describe.ts +34 -2
- package/scripts/run/wake/run-wake-driver.ts +11 -4
- package/scripts/run/wake/run-wake-session.ts +20 -3
- package/scripts/run/wake/run-wake-stop.ts +37 -0
- package/scripts/run/wake/run-wake-supervise.ts +44 -0
- package/scripts/sync/claude-settings-merge.ts +144 -0
- package/scripts/sync/sync-ai-files.ts +28 -3
- package/scripts/sync/sync.ts +15 -0
- package/scripts/test/project-test-script.ts +73 -0
- package/scripts/test/skip-marker.ts +14 -1
- package/scripts/test/test-related.ts +3 -2
- package/scripts/test/test-unit-guard.ts +78 -14
- package/scripts/time-runtime/time-reported-failure.ts +11 -3
- package/scripts/uninstall/distributed-files.ts +92 -0
- package/scripts/uninstall/uninstall-plan.ts +119 -0
- package/scripts/uninstall/uninstall-report.ts +61 -0
- package/scripts/uninstall/uninstall.ts +206 -0
- package/templates/AGENTS.basic.md +4 -0
- package/templates/GEMINI.basic.md +6 -0
- package/dist/hooks/chunk-6V5OPIUU.js +0 -13
- package/scripts/run/board/run-board-track.ts +0 -22
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { json_format } from '#scripts/config-merge/json-format'
|
|
2
|
+
import { hook_command_rewrite } from '#scripts/init/hook-command-rewrite'
|
|
3
|
+
|
|
4
|
+
// `josh sync` used to replace a project's `.claude/settings.json` with the distributed file, which
|
|
5
|
+
// deleted every hook and permission the project had added — and `.claude/settings.local.json` is
|
|
6
|
+
// personal, so a team had nowhere to keep a shared one. The merge replaces what kit ships and keeps
|
|
7
|
+
// the rest:
|
|
8
|
+
//
|
|
9
|
+
// - hooks: a handler whose command has one of kit's shapes is kit's — the current ones are replaced
|
|
10
|
+
// and a retired one is dropped; every other handler is the project's and stays.
|
|
11
|
+
// - permissions: kit's `allow` / `deny` entries are always present, so a removed `deny` comes back;
|
|
12
|
+
// the project's own entries follow them, less the ones kit has withdrawn.
|
|
13
|
+
// - any other key: kit's value where kit ships one, the project's where it does not. Objects merge
|
|
14
|
+
// entry by entry, so a project's own `env` variable or plugin survives beside kit's.
|
|
15
|
+
//
|
|
16
|
+
// kit's keys and entries come first and in kit's order, which is what makes a file with nothing of the
|
|
17
|
+
// project's own come out byte-identical to the distributed one, and a second merge change nothing.
|
|
18
|
+
|
|
19
|
+
type JsonObject = Record<string, unknown>
|
|
20
|
+
type EntryMerge = (key: string, kit: unknown, own: unknown) => unknown
|
|
21
|
+
|
|
22
|
+
const HOOKS_KEY = 'hooks'
|
|
23
|
+
const PERMISSIONS_KEY = 'permissions'
|
|
24
|
+
// An entry kit stops shipping would otherwise stay in every merged file as if the project had added
|
|
25
|
+
// it, so a permission kit withdraws could never be withdrawn. Withdrawing one means removing it from
|
|
26
|
+
// kit's settings file and naming it here.
|
|
27
|
+
const WITHDRAWN_KIT_PERMISSIONS: ReadonlySet<unknown> = new Set([
|
|
28
|
+
'Bash(git commit -a*)',
|
|
29
|
+
'Bash(git commit --all*)',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
function is_json_object(value: unknown): value is JsonObject {
|
|
33
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function as_array(value: unknown): Array<unknown> {
|
|
37
|
+
return Array.isArray(value) ? value : []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function merge_objects(kit: JsonObject, own: JsonObject, merge_entry: EntryMerge): JsonObject {
|
|
41
|
+
const merged: JsonObject = { ...kit, ...own }
|
|
42
|
+
|
|
43
|
+
for (const [key, value] of Object.entries(kit)) merged[key] = merge_entry(key, value, own[key])
|
|
44
|
+
|
|
45
|
+
return merged
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function merge_kit_value(kit: unknown, own: unknown): unknown {
|
|
49
|
+
if (!is_json_object(kit) || !is_json_object(own)) return kit
|
|
50
|
+
|
|
51
|
+
return merge_objects(kit, own, (_key, kit_entry, own_entry) =>
|
|
52
|
+
merge_kit_value(kit_entry, own_entry),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function merge_permission(kit: unknown, own: unknown): unknown {
|
|
57
|
+
if (!Array.isArray(kit) || !Array.isArray(own)) return merge_kit_value(kit, own)
|
|
58
|
+
|
|
59
|
+
return [...new Set([...as_array(kit), ...as_array(own)])]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function without_withdrawn(value: unknown): unknown {
|
|
63
|
+
if (!Array.isArray(value)) return value
|
|
64
|
+
|
|
65
|
+
return value.filter((entry) => !WITHDRAWN_KIT_PERMISSIONS.has(entry))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The withdrawn entries are taken out of every list first, so one goes even when kit no longer ships
|
|
69
|
+
// the list it sat in.
|
|
70
|
+
function merge_permissions(kit: unknown, own: unknown): unknown {
|
|
71
|
+
if (!is_json_object(kit) || !is_json_object(own)) return kit
|
|
72
|
+
const kept = Object.entries(own).map(([key, value]) => [key, without_withdrawn(value)] as const)
|
|
73
|
+
|
|
74
|
+
return merge_objects(kit, Object.fromEntries(kept), (_key, kit_entry, own_entry) =>
|
|
75
|
+
merge_permission(kit_entry, own_entry),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function is_kit_handler(handler: unknown): boolean {
|
|
80
|
+
if (!is_json_object(handler)) return false
|
|
81
|
+
const { command } = handler
|
|
82
|
+
|
|
83
|
+
return typeof command === 'string' && hook_command_rewrite.is_kit_hook_command(command)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A matcher group left with no handler of the project's own was kit's alone, and goes with them.
|
|
87
|
+
function without_kit_handlers(group: unknown): unknown {
|
|
88
|
+
if (!is_json_object(group)) return group
|
|
89
|
+
const handlers = as_array(group[HOOKS_KEY]).filter((handler) => !is_kit_handler(handler))
|
|
90
|
+
|
|
91
|
+
return handlers.length === 0 ? undefined : { ...group, [HOOKS_KEY]: handlers }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function own_groups(groups: unknown): Array<unknown> {
|
|
95
|
+
return as_array(groups)
|
|
96
|
+
.map((group) => without_kit_handlers(group))
|
|
97
|
+
.filter((group) => group !== undefined)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function own_hook_events(own: JsonObject): JsonObject {
|
|
101
|
+
const events = Object.entries(own).map(([event, groups]) => [event, own_groups(groups)] as const)
|
|
102
|
+
|
|
103
|
+
return Object.fromEntries(events.filter(([, groups]) => groups.length > 0))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function merge_hooks(kit: unknown, own: unknown): unknown {
|
|
107
|
+
if (!is_json_object(kit) || !is_json_object(own)) return kit
|
|
108
|
+
|
|
109
|
+
return merge_objects(kit, own_hook_events(own), (_event, kit_groups, groups) => [
|
|
110
|
+
...as_array(kit_groups),
|
|
111
|
+
...as_array(groups),
|
|
112
|
+
])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function merge_top_level(key: string, kit: unknown, own: unknown): unknown {
|
|
116
|
+
if (key === HOOKS_KEY) return merge_hooks(kit, own)
|
|
117
|
+
if (key === PERMISSIONS_KEY) return merge_permissions(kit, own)
|
|
118
|
+
|
|
119
|
+
return merge_kit_value(kit, own)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parse_object(content: string): JsonObject | undefined {
|
|
123
|
+
try {
|
|
124
|
+
const parsed: unknown = JSON.parse(content)
|
|
125
|
+
|
|
126
|
+
return is_json_object(parsed) ? parsed : undefined
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// A file that does not parse as a JSON object has nothing the merge can keep, so it is replaced with
|
|
133
|
+
// the distributed one, as every file was before the merge existed.
|
|
134
|
+
function merge_claude_settings(existing: string, distributed: string): string {
|
|
135
|
+
const own = parse_object(existing)
|
|
136
|
+
const kit = parse_object(distributed)
|
|
137
|
+
if (own === undefined || kit === undefined) return distributed
|
|
138
|
+
|
|
139
|
+
return json_format.format_json(merge_objects(kit, own, merge_top_level))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const claude_settings_merge = { merge_claude_settings, WITHDRAWN_KIT_PERMISSIONS }
|
|
143
|
+
|
|
144
|
+
export { claude_settings_merge }
|
|
@@ -8,6 +8,7 @@ import type { ProjectShape } from '#scripts/init/project-profile'
|
|
|
8
8
|
import { security_policy } from '#scripts/init/security-policy'
|
|
9
9
|
import { file_reader } from '#scripts/lib/read-file'
|
|
10
10
|
import { pack_hook } from '#scripts/safe-chain/pack-hook'
|
|
11
|
+
import { claude_settings_merge } from './claude-settings-merge'
|
|
11
12
|
import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
|
|
12
13
|
import { file_content } from './file-content'
|
|
13
14
|
import { REMOVED_SKILL_MANIFEST } from './removed-skill-manifest'
|
|
@@ -45,9 +46,32 @@ function should_skip_hook_file(filename: string): boolean {
|
|
|
45
46
|
return true
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
// The project's own hooks, permissions and keys survive: only kit's entries are replaced
|
|
50
|
+
// (`claude-settings-merge.ts`). `--force` is the replacement sync used to make, as it is for
|
|
51
|
+
// `pnpm-workspace.yaml`.
|
|
52
|
+
function sync_claude_settings(
|
|
53
|
+
source_path: string,
|
|
54
|
+
destination_path: string,
|
|
55
|
+
is_force: boolean,
|
|
56
|
+
): boolean {
|
|
57
|
+
const distributed = transform_copied_content(destination_path, readFileSync(source_path, 'utf8'))
|
|
58
|
+
const existing = is_force ? undefined : file_reader.read_optional(destination_path)
|
|
59
|
+
const content =
|
|
60
|
+
existing === undefined
|
|
61
|
+
? distributed
|
|
62
|
+
: claude_settings_merge.merge_claude_settings(existing, distributed)
|
|
63
|
+
|
|
64
|
+
return file_content.write_text_if_changed(destination_path, content)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sync_file(source: string, filename: string, is_force: boolean): void {
|
|
49
68
|
if (should_skip_hook_file(filename)) return
|
|
50
|
-
const
|
|
69
|
+
const source_path = path.join(PACKAGE_DIR, source)
|
|
70
|
+
const destination_path = path.join(PROJECT_ROOT, filename)
|
|
71
|
+
const did_change =
|
|
72
|
+
filename === CLAUDE_SETTINGS_FILE
|
|
73
|
+
? sync_claude_settings(source_path, destination_path, is_force)
|
|
74
|
+
: sync_ai_file(source_path, destination_path)
|
|
51
75
|
|
|
52
76
|
console.info(` ✔ ${did_change ? 'synced ' : 'unchanged'} ${filename}`)
|
|
53
77
|
}
|
|
@@ -96,7 +120,7 @@ function sync_ai_copy_file(filename: string, is_force: boolean, shape?: ProjectS
|
|
|
96
120
|
return
|
|
97
121
|
}
|
|
98
122
|
|
|
99
|
-
sync_file(source, filename)
|
|
123
|
+
sync_file(source, filename, is_force)
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
// The reason this directory was not synced, or nothing when it was. Both halves answer the same
|
|
@@ -203,6 +227,7 @@ const sync_ai_files = {
|
|
|
203
227
|
sync_ai_file,
|
|
204
228
|
sync_workspace_yaml,
|
|
205
229
|
sync_claude_md,
|
|
230
|
+
ensured_claude_md,
|
|
206
231
|
sync_directory,
|
|
207
232
|
sync_ai_copy_all,
|
|
208
233
|
}
|
package/scripts/sync/sync.ts
CHANGED
|
@@ -192,6 +192,19 @@ function sync_secretlint_development_deps(destination_path: string): void {
|
|
|
192
192
|
)
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// A tool `josh init` started adding after a consumer was initialized never reached it: `lefthook.yml`
|
|
196
|
+
// was synced, the lefthook that runs it was not. Only the tools the consumer resolves itself are
|
|
197
|
+
// added, and only where the manifest declares them nowhere; an existing range is left as it is.
|
|
198
|
+
function sync_consumer_resolved_development_deps(destination_path: string): void {
|
|
199
|
+
const has_git = project_profile.has_git(path.dirname(destination_path))
|
|
200
|
+
|
|
201
|
+
sync_package_json_with(
|
|
202
|
+
destination_path,
|
|
203
|
+
(existing) => init_logic.merge_consumer_resolved_development_deps(existing, has_git),
|
|
204
|
+
' ✔ synced toolchain devDependencies (run `pnpm install`)',
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
195
208
|
// `lefthook install` fails silently when `core.hooksPath` is set, leaving a consumer with no hooks
|
|
196
209
|
// and no message. `josh init` rewrites the clause it wrote so the failure
|
|
197
210
|
// is reported; this covers everyone already initialized, who never re-runs `josh init`.
|
|
@@ -219,6 +232,7 @@ function sync_package_json_migrations(destination_path: string): void {
|
|
|
219
232
|
sync_package_manager_version(destination_path)
|
|
220
233
|
sync_package_manager_on_fail(destination_path)
|
|
221
234
|
sync_secretlint_development_deps(destination_path)
|
|
235
|
+
sync_consumer_resolved_development_deps(destination_path)
|
|
222
236
|
sync_prepare_lefthook_warning(destination_path)
|
|
223
237
|
sync_safe_chain_preinstall(destination_path)
|
|
224
238
|
}
|
|
@@ -300,6 +314,7 @@ const sync = {
|
|
|
300
314
|
sync_package_manager_version,
|
|
301
315
|
sync_package_manager_on_fail,
|
|
302
316
|
sync_secretlint_development_deps,
|
|
317
|
+
sync_consumer_resolved_development_deps,
|
|
303
318
|
sync_prepare_lefthook_warning,
|
|
304
319
|
sync_safe_chain_preinstall,
|
|
305
320
|
sync_package_json_migrations,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { project_checks } from '#scripts/gate/project-checks'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { execa } from 'execa'
|
|
4
|
+
|
|
5
|
+
// **The unit check of a basic project that carries no vitest is the test script the project itself
|
|
6
|
+
// declares** — `scripts.test` in its `package.json`. A basic project is one of any language and any
|
|
7
|
+
// runner (`node --test`, jest, mocha, a wrapper around another language's), and its own script is the one statement of
|
|
8
|
+
// how its tests run that holds for all of them.
|
|
9
|
+
//
|
|
10
|
+
// Rejected: detecting `node --test` files and starting that runner here. Every further runner would
|
|
11
|
+
// need its own detection and its own invocation, and the project has already written both down.
|
|
12
|
+
const PNPM = 'pnpm'
|
|
13
|
+
const SCRIPT_NAME = 'test'
|
|
14
|
+
const FAIL_EXIT_CODE = 1
|
|
15
|
+
|
|
16
|
+
// What `npm init` and `pnpm init` write when no test command is given. It exits 1 by design, so
|
|
17
|
+
// running it would fail every freshly-initialized project for having declared nothing.
|
|
18
|
+
const INIT_PLACEHOLDER = 'echo "Error: no test specified" && exit 1'
|
|
19
|
+
|
|
20
|
+
// Set on the script this module spawns. A `scripts.test` that calls back into josh (`josh test:unit`,
|
|
21
|
+
// `josh gate`) reaches this check again with it set, and running the script from there would recurse
|
|
22
|
+
// without end.
|
|
23
|
+
const REENTRY_KEY = 'JOSH_PROJECT_TEST_SCRIPT'
|
|
24
|
+
const REENTRY_VALUE = '1'
|
|
25
|
+
|
|
26
|
+
type ProjectScriptState = 'declared' | 'absent' | 'reentrant'
|
|
27
|
+
|
|
28
|
+
const EMPTY_COMMANDS = new Set(['', INIT_PLACEHOLDER])
|
|
29
|
+
|
|
30
|
+
function field_of(value: unknown, key: string): unknown {
|
|
31
|
+
return json_value.is_record(value) ? value[key] : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function declared_script(project_directory: string): string | undefined {
|
|
35
|
+
const manifest: unknown = project_checks.read_manifest(project_directory)
|
|
36
|
+
const script = field_of(field_of(manifest, 'scripts'), SCRIPT_NAME)
|
|
37
|
+
if (typeof script !== 'string') return undefined
|
|
38
|
+
|
|
39
|
+
const command = script.trim()
|
|
40
|
+
|
|
41
|
+
return EMPTY_COMMANDS.has(command) ? undefined : command
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function state(
|
|
45
|
+
project_directory: string,
|
|
46
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
47
|
+
): ProjectScriptState {
|
|
48
|
+
if (declared_script(project_directory) === undefined) return 'absent'
|
|
49
|
+
|
|
50
|
+
return environment[REENTRY_KEY] === REENTRY_VALUE ? 'reentrant' : 'declared'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The notice is printed before the spawn, so a reader of the gate's output sees which command stood in
|
|
54
|
+
// for vitest rather than a bare runner log under the `test:unit` label.
|
|
55
|
+
async function run(project_directory: string, command_label: string): Promise<number> {
|
|
56
|
+
console.info(
|
|
57
|
+
`josh ${command_label}: vitest is not installed — running the project's own test script (${PNPM} run ${SCRIPT_NAME}).`,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const result = await execa(PNPM, ['run', SCRIPT_NAME], {
|
|
61
|
+
cwd: project_checks.project_root(project_directory),
|
|
62
|
+
env: { [REENTRY_KEY]: REENTRY_VALUE },
|
|
63
|
+
stdio: 'inherit',
|
|
64
|
+
reject: false,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return result.exitCode ?? FAIL_EXIT_CODE
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const project_test_script = { INIT_PLACEHOLDER, REENTRY_KEY, declared_script, state, run }
|
|
71
|
+
|
|
72
|
+
export type { ProjectScriptState }
|
|
73
|
+
export { project_test_script }
|
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
const SKIP_MARKER = '— skipping'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// **A check that ran and had nothing of the project's to look at says so with this, not with the skip
|
|
4
|
+
// marker.** `josh exports:unused` in a consumer and `josh behavior` with no transcript are out of scope
|
|
5
|
+
// by design rather than unverified, so they must not withhold the gate's green record the way a skip
|
|
6
|
+
// does — but neither checked anything, and the gate's verdict has to be able to tell them from a check
|
|
7
|
+
// that did. Shared for the reason the skip marker is: the notice and its reader cannot drift apart.
|
|
8
|
+
const NOTHING_TO_CHECK_MARKER = 'nothing to check'
|
|
9
|
+
|
|
10
|
+
// **A skip of a tool the project does not use carries this beside the skip marker.** A `basic`
|
|
11
|
+
// project with no ESLint configuration skips eslint, and the notice stays a visible skip the verdict
|
|
12
|
+
// counts — but nothing the project asked for went unverified, so it must not withhold the gate's
|
|
13
|
+
// green record the way a unit suite that did not run does.
|
|
14
|
+
const NOT_USED_MARKER = 'which this project does not use'
|
|
15
|
+
|
|
16
|
+
export { NOT_USED_MARKER, NOTHING_TO_CHECK_MARKER, SKIP_MARKER }
|
|
@@ -22,11 +22,12 @@ import { test_unit_guard } from './test-unit-guard'
|
|
|
22
22
|
// project with no unit suite the record would say the tests were green over a run in which none
|
|
23
23
|
// executed, and `josh review:brief` would stop refusing on the strength of it. **The guard's own
|
|
24
24
|
// decision is asked again rather than re-derived here**, so the two can never disagree; it is asked
|
|
25
|
-
// only when a record is otherwise due, since the question costs a project-wide glob.
|
|
25
|
+
// only when a record is otherwise due, since the question costs a project-wide glob. A basic project's
|
|
26
|
+
// own test script counts as a run: it executes the whole suite, which covers any narrowing.
|
|
26
27
|
function conclusive_tree(before: GateTree | undefined): GateTree | undefined {
|
|
27
28
|
if (before === undefined) return undefined
|
|
28
29
|
|
|
29
|
-
return test_unit_guard.
|
|
30
|
+
return test_unit_guard.is_running(process.cwd()) ? before : undefined
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async function run_related_tests(command_arguments: ReadonlyArray<string>): Promise<number> {
|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
import { PROJECT_ENVIRONMENT_KEYS } from '#ports'
|
|
6
6
|
import { project_checks } from '#scripts/gate/project-checks'
|
|
7
7
|
import { execa } from 'execa'
|
|
8
|
+
import { project_test_script, type ProjectScriptState } from './project-test-script'
|
|
8
9
|
import { SKIP_MARKER } from './skip-marker'
|
|
9
10
|
import { unit_worker_share } from './unit-worker-share'
|
|
10
11
|
|
|
@@ -35,29 +36,71 @@ const NODE_MODULES = 'node_modules'
|
|
|
35
36
|
// oversight.** The argument above rests on the package's presence being a declaration, and
|
|
36
37
|
// `@playwright/test` is not one: it is an optional peer dependency that vitest's browser mode also
|
|
37
38
|
// needs, so a project can legitimately have it installed with no `*.e2e.{ts,js}` file at all.
|
|
39
|
+
//
|
|
40
|
+
// **A basic project without vitest is not thereby a project without tests.** It may be of any
|
|
41
|
+
// language and any runner, and the `scripts.test` it declares is run in vitest's place
|
|
42
|
+
// (`project-test-script.ts`); only a basic project that declares none skips. A full project keeps the
|
|
43
|
+
// plain skip: its unit suite is vitest's, and `scripts.test` there is not this check's to run.
|
|
38
44
|
const SKIP_ACTION = 'skip-missing-package'
|
|
39
45
|
const SKIP_NO_TESTS_ACTION = 'skip-no-tests'
|
|
46
|
+
const SKIP_NO_SCRIPT_ACTION = 'skip-no-test-script'
|
|
47
|
+
const SKIP_REENTRANT_ACTION = 'skip-reentrant'
|
|
48
|
+
const RUN_SCRIPT_ACTION = 'run-project-script'
|
|
40
49
|
const FAIL_ACTION = 'fail-no-tests'
|
|
41
50
|
|
|
42
|
-
type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
type SkipAction =
|
|
52
|
+
| typeof SKIP_ACTION
|
|
53
|
+
| typeof SKIP_NO_TESTS_ACTION
|
|
54
|
+
| typeof SKIP_NO_SCRIPT_ACTION
|
|
55
|
+
| typeof SKIP_REENTRANT_ACTION
|
|
56
|
+
type GuardAction = 'run' | typeof RUN_SCRIPT_ACTION | SkipAction | typeof FAIL_ACTION
|
|
57
|
+
|
|
58
|
+
// What each skip did not run, said in full: a basic project that declares no test script was not
|
|
59
|
+
// skipped for lacking vitest alone, and the unit tests it skipped were never vitest's. The
|
|
60
|
+
// `SKIP_MARKER` between the two halves is the word the gate looks for to know a passing step did not
|
|
61
|
+
// actually run; the gate does not print a passing check's body, so without it a gate that skipped the
|
|
62
|
+
// whole unit suite would print the same lines as one that ran it.
|
|
63
|
+
const VITEST_SUBJECT = 'vitest unit tests'
|
|
64
|
+
const UNIT_SUBJECT = 'unit tests'
|
|
65
|
+
const SKIP_NOTICES: Record<SkipAction, { reason: string; subject: string }> = {
|
|
66
|
+
[SKIP_ACTION]: { reason: 'vitest is not installed', subject: VITEST_SUBJECT },
|
|
67
|
+
[SKIP_NO_TESTS_ACTION]: {
|
|
68
|
+
reason: 'no unit test files were found in this basic project',
|
|
69
|
+
subject: VITEST_SUBJECT,
|
|
70
|
+
},
|
|
71
|
+
[SKIP_NO_SCRIPT_ACTION]: {
|
|
72
|
+
reason: 'vitest is not installed and package.json declares no test script',
|
|
73
|
+
subject: UNIT_SUBJECT,
|
|
74
|
+
},
|
|
75
|
+
[SKIP_REENTRANT_ACTION]: {
|
|
76
|
+
reason: "the project's test script called back into this check",
|
|
77
|
+
subject: UNIT_SUBJECT,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
49
80
|
const FAIL_REASON =
|
|
50
81
|
'vitest is installed but no *.{test,spec}.{ts,js} test file was found — refusing to report a unit check that ran nothing'
|
|
51
82
|
|
|
83
|
+
const BASIC_MISSING_PACKAGE_ACTIONS: Record<ProjectScriptState, GuardAction> = {
|
|
84
|
+
declared: RUN_SCRIPT_ACTION,
|
|
85
|
+
absent: SKIP_NO_SCRIPT_ACTION,
|
|
86
|
+
reentrant: SKIP_REENTRANT_ACTION,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolve_installed_action(has_tests: boolean, is_basic: boolean): GuardAction {
|
|
90
|
+
if (has_tests) return 'run'
|
|
91
|
+
|
|
92
|
+
return is_basic ? SKIP_NO_TESTS_ACTION : FAIL_ACTION
|
|
93
|
+
}
|
|
94
|
+
|
|
52
95
|
function resolve_guard_action(
|
|
53
96
|
is_installed: boolean,
|
|
54
97
|
has_tests: boolean,
|
|
55
98
|
is_basic = false,
|
|
99
|
+
script_state: ProjectScriptState = 'absent',
|
|
56
100
|
): GuardAction {
|
|
57
|
-
if (
|
|
58
|
-
if (has_tests) return 'run'
|
|
101
|
+
if (is_installed) return resolve_installed_action(has_tests, is_basic)
|
|
59
102
|
|
|
60
|
-
return is_basic ?
|
|
103
|
+
return is_basic ? BASIC_MISSING_PACKAGE_ACTIONS[script_state] : SKIP_ACTION
|
|
61
104
|
}
|
|
62
105
|
|
|
63
106
|
function is_vitest_installed(project_directory: string): boolean {
|
|
@@ -84,15 +127,27 @@ function guard_action(project_directory: string): GuardAction {
|
|
|
84
127
|
is_vitest_installed(project_directory),
|
|
85
128
|
has_unit_tests(project_directory),
|
|
86
129
|
project_checks.is_basic(project_directory),
|
|
130
|
+
project_test_script.state(project_directory),
|
|
87
131
|
)
|
|
88
132
|
}
|
|
89
133
|
|
|
134
|
+
function is_skip_action(action: GuardAction): action is SkipAction {
|
|
135
|
+
return Object.hasOwn(SKIP_NOTICES, action)
|
|
136
|
+
}
|
|
137
|
+
|
|
90
138
|
// **A skip is the project having no unit suite, so no unit check can ever vouch for its tree.** The
|
|
91
139
|
// failing branch is not one: it is a broken suite, and it is left to fail where it is reported.
|
|
92
140
|
function is_skipping(project_directory: string): boolean {
|
|
141
|
+
return is_skip_action(guard_action(project_directory))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// **Whether a green exit vouches for the tree** — vitest ran, or the project's own script did. The
|
|
145
|
+
// scoped record is written on this answer, so a project `is_skipping` requires a record from is
|
|
146
|
+
// always one that can write it.
|
|
147
|
+
function is_running(project_directory: string): boolean {
|
|
93
148
|
const action = guard_action(project_directory)
|
|
94
149
|
|
|
95
|
-
return action ===
|
|
150
|
+
return action === 'run' || action === RUN_SCRIPT_ACTION
|
|
96
151
|
}
|
|
97
152
|
|
|
98
153
|
// A lane's port allocation belongs to its development and preview servers, not to unit fixtures.
|
|
@@ -146,16 +201,16 @@ const UNIT_COMMAND_LABEL = 'test:unit'
|
|
|
146
201
|
// they mean different things: one says the project has no unit suite yet, the other says it has one
|
|
147
202
|
// that could not be found. Only the first keeps `SKIP_MARKER`, so the
|
|
148
203
|
// gate's "passed without running" handling still means exactly what it did.
|
|
149
|
-
function report_no_run(action:
|
|
204
|
+
function report_no_run(action: SkipAction | typeof FAIL_ACTION, command_label: string): number {
|
|
150
205
|
if (action === FAIL_ACTION) {
|
|
151
206
|
console.error(`josh ${command_label}: ${FAIL_REASON}.`)
|
|
152
207
|
|
|
153
208
|
return FAIL_EXIT_CODE
|
|
154
209
|
}
|
|
155
210
|
|
|
156
|
-
const reason = action
|
|
211
|
+
const { reason, subject } = SKIP_NOTICES[action]
|
|
157
212
|
|
|
158
|
-
console.info(`josh ${command_label}: ${reason} ${SKIP_MARKER}
|
|
213
|
+
console.info(`josh ${command_label}: ${reason} ${SKIP_MARKER} ${subject}.`)
|
|
159
214
|
|
|
160
215
|
return 0
|
|
161
216
|
}
|
|
@@ -163,6 +218,10 @@ function report_no_run(action: Exclude<GuardAction, 'run'>, command_label: strin
|
|
|
163
218
|
// `announcement` is printed only on the branch that actually spawns vitest. A caller that wrote it
|
|
164
219
|
// itself would claim a run before this guard had decided there would be one — the same "a passing
|
|
165
220
|
// step did not actually run" ambiguity `SKIP_MARKER` exists for, reintroduced one layer up.
|
|
221
|
+
//
|
|
222
|
+
// **The project's own script takes neither the vitest arguments nor the announcement.** Both describe
|
|
223
|
+
// a vitest run — a filter, a narrowing to related files — and the script runs as the project declared
|
|
224
|
+
// it, whole; it prints its own notice saying so.
|
|
166
225
|
async function run_guarded_vitest(
|
|
167
226
|
project_directory: string,
|
|
168
227
|
vitest_arguments: ReadonlyArray<string>,
|
|
@@ -171,6 +230,10 @@ async function run_guarded_vitest(
|
|
|
171
230
|
): Promise<number> {
|
|
172
231
|
const action = guard_action(project_directory)
|
|
173
232
|
|
|
233
|
+
if (action === RUN_SCRIPT_ACTION) {
|
|
234
|
+
return await project_test_script.run(project_directory, command_label)
|
|
235
|
+
}
|
|
236
|
+
|
|
174
237
|
if (action !== 'run') return report_no_run(action, command_label)
|
|
175
238
|
if (announcement !== undefined) process.stdout.write(`${announcement}\n`)
|
|
176
239
|
|
|
@@ -195,6 +258,7 @@ const test_unit_guard = {
|
|
|
195
258
|
SKIP_MARKER,
|
|
196
259
|
guard_action,
|
|
197
260
|
is_skipping,
|
|
261
|
+
is_running,
|
|
198
262
|
resolve_guard_action,
|
|
199
263
|
is_vitest_installed,
|
|
200
264
|
has_unit_tests,
|
|
@@ -112,8 +112,16 @@ function is_failed_verdict(line: string): boolean {
|
|
|
112
112
|
return josh_verdict.read_verdict(line) === josh_verdict.FAILED_VERDICT
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
// **A gate that ran no check closes its region exactly as a passed one does.** Its `not verified`
|
|
116
|
+
// verdict is still the gate's own statement that no check failed, so a failure mark a skip notice
|
|
117
|
+
// forwarded in front of it is no more the gate's than one in front of `passed`.
|
|
118
|
+
const GREEN_VERDICTS: ReadonlySet<string | undefined> = new Set([
|
|
119
|
+
josh_verdict.PASSED_VERDICT,
|
|
120
|
+
josh_verdict.UNVERIFIED_VERDICT,
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
function is_green_verdict(line: string): boolean {
|
|
124
|
+
return GREEN_VERDICTS.has(josh_verdict.read_verdict(line))
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
// What one pass over the body has established. The two failure buckets are kept apart because a
|
|
@@ -165,7 +173,7 @@ function note_failure(scan: FailureScan): void {
|
|
|
165
173
|
// anything about the region: it is the gate's report about one check, and the run it belongs to states
|
|
166
174
|
// a failed verdict of its own.
|
|
167
175
|
function scan_line(scan: FailureScan, line: string): void {
|
|
168
|
-
if (
|
|
176
|
+
if (is_green_verdict(line)) {
|
|
169
177
|
close_region(scan)
|
|
170
178
|
|
|
171
179
|
return
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { init } from '#scripts/init/init'
|
|
3
|
+
import { init_actions } from '#scripts/init/init-actions'
|
|
4
|
+
import { CLAUDE_MD_FILENAME, init_ai_copy, WORKSPACE_YAML } from '#scripts/init/init-ai-copy'
|
|
5
|
+
import { init_basic } from '#scripts/init/init-basic'
|
|
6
|
+
import { package_path, PROJECT_ROOT } from '#scripts/init/init-paths'
|
|
7
|
+
import type { ProjectProfile, ProjectShape } from '#scripts/init/project-profile'
|
|
8
|
+
import { file_reader } from '#scripts/lib/read-file'
|
|
9
|
+
import { PACK_HOOK_FILE } from '#scripts/safe-chain/pack-hook'
|
|
10
|
+
import { sync_ai_files } from '#scripts/sync/sync-ai-files'
|
|
11
|
+
import { IGNORE_FILE, type DistributedFile } from './uninstall-plan'
|
|
12
|
+
|
|
13
|
+
// Every file the profile can write, whatever the project looks like today: a project that has since
|
|
14
|
+
// lost its last web file still holds the Prettier config kit wrote while it had one. Asking for the
|
|
15
|
+
// widest shape is safe because a file is only ever deleted while it equals what kit writes.
|
|
16
|
+
function widest_shape(profile: ProjectProfile): ProjectShape {
|
|
17
|
+
return {
|
|
18
|
+
profile,
|
|
19
|
+
reason: 'josh uninstall',
|
|
20
|
+
has_web: true,
|
|
21
|
+
has_typescript: true,
|
|
22
|
+
has_git: true,
|
|
23
|
+
has_github: true,
|
|
24
|
+
has_workflow: true,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Some content names the repository it lands in and cannot be produced where there is none. Such a
|
|
29
|
+
// file has no known distribution content, so it is kept.
|
|
30
|
+
function content_or_unknown(produce: () => string): string | undefined {
|
|
31
|
+
try {
|
|
32
|
+
return produce()
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function config_files(shape: ProjectShape): Array<DistributedFile> {
|
|
39
|
+
return init_actions.build_file_actions(shape).map((action) => ({
|
|
40
|
+
dest: action.dest,
|
|
41
|
+
distributed: content_or_unknown(() => action.create()),
|
|
42
|
+
}))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copied_file(filename: string, shape: ProjectShape): DistributedFile {
|
|
46
|
+
const source_path = package_path(init_ai_copy.ai_file_source(filename, shape))
|
|
47
|
+
const destination_path = path.join(PROJECT_ROOT, filename)
|
|
48
|
+
const read_content =
|
|
49
|
+
filename === WORKSPACE_YAML ? init_ai_copy.workspace_content : init_ai_copy.copied_content
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
dest: filename,
|
|
53
|
+
distributed: content_or_unknown(() => read_content(source_path, destination_path)),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mapped_file(source: string, destination: string): DistributedFile {
|
|
58
|
+
return {
|
|
59
|
+
dest: destination,
|
|
60
|
+
distributed: content_or_unknown(() =>
|
|
61
|
+
init_ai_copy.copied_content(package_path(source), path.join(PROJECT_ROOT, destination)),
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ai_files(shape: ProjectShape): Array<DistributedFile> {
|
|
67
|
+
return [
|
|
68
|
+
{ dest: CLAUDE_MD_FILENAME, distributed: sync_ai_files.ensured_claude_md(undefined, shape) },
|
|
69
|
+
...init_ai_copy.ai_files(shape).map((filename) => copied_file(filename, shape)),
|
|
70
|
+
...init_ai_copy.ai_file_mappings(shape).map(({ src, dest }) => mapped_file(src, dest)),
|
|
71
|
+
{ dest: PACK_HOOK_FILE, distributed: file_reader.read_optional(package_path(PACK_HOOK_FILE)) },
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The lists are `josh init`'s and `josh sync`'s own — the same builders, read for what they would
|
|
76
|
+
// write and not run — so a file added to either is one this command already knows about. The ignore
|
|
77
|
+
// file is left out: it is named for review and never compared, so it can never be removed.
|
|
78
|
+
function list(profile: ProjectProfile): Array<DistributedFile> {
|
|
79
|
+
const shape = widest_shape(profile)
|
|
80
|
+
|
|
81
|
+
return [...config_files(shape), ...ai_files(shape)].filter((file) => file.dest !== IGNORE_FILE)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// What kit's merge writes into the manifest it starts a project without one from, which names every
|
|
85
|
+
// entry it adds — `private` included, the one entry that start alone writes.
|
|
86
|
+
function kit_manifest(profile: ProjectProfile): string {
|
|
87
|
+
return init.merged_manifest(init_basic.initial_manifest(), widest_shape(profile))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const distributed_files = { list, kit_manifest }
|
|
91
|
+
|
|
92
|
+
export { distributed_files }
|