@joshuafolkken/kit 1.957.0 → 1.978.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/background-commands.md +3 -0
- package/.claude/skills/workflow-commands/backlogrun-child.md +75 -120
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +78 -115
- package/.claude/skills/workflow-commands/backlogrun-progress.md +82 -146
- package/.claude/skills/workflow-commands/backlogrun-steps.md +173 -229
- package/.claude/skills/workflow-commands/backlogrun.md +3 -1
- package/.claude/skills/workflow-commands/followup.md +3 -0
- package/.claude/skills/workflow-commands/fullrun.md +4 -4
- package/.claude/skills/workflow-commands/halfrun.md +12 -7
- package/.claude/skills/workflow-commands/observation-filing.md +31 -25
- package/.claude/skills/workflow-commands/pre-gate-cut.md +96 -161
- package/.claude/skills/workflow-commands/working-tree-hold.md +16 -1
- package/.github/workflows/pr-classification.yml +12 -1
- package/CLAUDE.md +3 -3
- package/README.md +25 -21
- package/dist/CLAUDE.md +3 -3
- package/dist/hooks/batch-guard.js +4 -4
- package/dist/hooks/{chunk-XSL4SRMA.js → chunk-2K52BRWV.js} +2 -2
- package/dist/hooks/{chunk-M6K7EQDO.js → chunk-BNGGEY3Z.js} +38 -32
- package/dist/hooks/{chunk-KOCK5FXQ.js → chunk-BVMSPG3M.js} +3 -3
- package/dist/hooks/{chunk-3ON7HOYB.js → chunk-CRGJTD6E.js} +1 -1
- package/dist/hooks/{chunk-HJXQZ7V4.js → chunk-F6OM6SIG.js} +3 -3
- package/dist/hooks/{chunk-LDQDZYHX.js → chunk-KQG4776P.js} +1 -1
- package/dist/hooks/{chunk-244WYOEO.js → chunk-NXL3SBXY.js} +2 -2
- package/dist/hooks/{chunk-IJPBTDTB.js → chunk-Q7LE77CO.js} +88 -18
- package/dist/hooks/{chunk-BELI5XSS.js → chunk-ZPLXLJ7O.js} +15 -5
- package/dist/hooks/{chunk-GNOITLRR.js → chunk-ZYYSBNR6.js} +4 -4
- package/dist/hooks/codex-hook-adapter.js +10 -10
- package/dist/hooks/duplicate-read-guard.js +5 -5
- package/dist/hooks/format-edited.js +2 -2
- package/dist/hooks/investigation-guard.js +5 -5
- package/dist/hooks/pretool-guard.js +9 -9
- package/dist/hooks/stop-guard.js +206 -176
- package/dist/josh.js +8 -4
- package/package.json +4 -4
- package/prompts/collaboration-workflow/wip-cap.md +1 -1
- package/scripts/backlog/backlog-arrival.ts +11 -1
- package/scripts/backlog/backlog-budget.ts +25 -8
- package/scripts/backlog/backlog-next.ts +45 -2
- package/scripts/backlog/backlog-offer.ts +1 -0
- package/scripts/backlog/backlog-plan-cli.ts +66 -27
- package/scripts/backlog/backlog-plan.ts +30 -2
- package/scripts/backlog/backlog-stalled.ts +9 -0
- package/scripts/backlog/backlog-waves.ts +169 -0
- package/scripts/ci/pr-classification.ts +13 -12
- package/scripts/document/document-byte-budget.ts +1 -1
- package/scripts/document/entry-read-budget.ts +7 -5
- package/scripts/epic/epic-lane-offer.ts +69 -4
- package/scripts/epic/epic-next.ts +2 -2
- package/scripts/epic/epic-solo.ts +147 -0
- package/scripts/epic/epic-triage.ts +47 -0
- package/scripts/gate/hook-gate-reuse.ts +7 -8
- package/scripts/git/git-closes-keyword.ts +15 -0
- package/scripts/git/git-followup-flush.ts +5 -6
- package/scripts/git/git-followup-issue-close.ts +75 -0
- package/scripts/git/git-pr-followup-wrapup.ts +36 -6
- package/scripts/git/git-pr-followup.ts +9 -16
- package/scripts/git/git-staging.ts +33 -20
- package/scripts/git/issue-labels.ts +75 -5
- package/scripts/hooks/stop-guard.ts +1 -0
- package/scripts/init/init-bootstrap.ts +92 -0
- package/scripts/init/init-install.ts +67 -0
- package/scripts/init/init-logic-workspace.ts +1 -1
- package/scripts/init/init.ts +58 -13
- package/scripts/init/start-plan.ts +1 -1
- package/scripts/init/start-steps.ts +5 -28
- package/scripts/issue/issue-closing-pr.ts +93 -0
- package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +2 -1
- package/scripts/josh/josh-commands-project.ts +5 -1
- package/scripts/josh/josh-commands-workflow.ts +3 -3
- package/scripts/observations/observation-ledger-prepare.ts +31 -0
- package/scripts/observations/observation-ledger.ts +5 -5
- package/scripts/observations/observations-flush.ts +11 -30
- package/scripts/repo/repository-labels.ts +91 -0
- package/scripts/rules/implementation-cut.ts +49 -16
- package/scripts/rules/lane-background.ts +25 -5
- package/scripts/rules/stop-rules.ts +16 -4
- package/scripts/run/run-carry-cli.ts +21 -0
- package/scripts/run/run-cut-cli.ts +92 -110
- package/scripts/run/run-cut-report.ts +167 -0
- package/scripts/run/run-cut.ts +23 -7
- package/scripts/run/run-entry-cli.ts +70 -3
- package/scripts/run/run-halfrun-resume.ts +104 -0
- package/scripts/run/run-headless.ts +1 -0
- package/scripts/run/run-hold-cli.ts +57 -13
- package/scripts/run/run-hold.ts +26 -4
- package/scripts/run/run-merge-cli.ts +28 -14
- package/scripts/run/run-merge-steps.ts +38 -2
- package/scripts/run/run-merge.ts +41 -3
- package/scripts/run/run-prep-cli.ts +8 -2
- package/scripts/run/run-prep-locate.ts +165 -0
- package/scripts/run/run-prep.ts +12 -1
- package/scripts/run/run-step-cli.ts +16 -0
- package/scripts/run/run-step.ts +28 -4
- package/scripts/run/run-tail-cli.ts +4 -2
- package/scripts/run/run-wake-work.ts +4 -0
- package/scripts/safe-chain/ci-installer-pin.ts +8 -4
- package/scripts/security/security-audit.ts +12 -1
- package/scripts/sync/sync.ts +6 -0
- package/scripts/time-runtime/time-background.ts +11 -0
- package/scripts/time-runtime/time-transcript-line.ts +4 -0
- package/templates/pnpm-workspace.static.yaml +2 -0
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
observation_ledger,
|
|
3
3
|
OBSERVATION_LEDGER_PATHS,
|
|
4
4
|
} from '#scripts/observations/observation-ledger'
|
|
5
|
+
import { observation_ledger_prepare } from '#scripts/observations/observation-ledger-prepare'
|
|
5
6
|
import { git_command } from './git-command'
|
|
6
7
|
import { git_prompt } from './git-prompt'
|
|
7
8
|
import { git_status } from './git-status'
|
|
@@ -67,18 +68,26 @@ async function stage_untracked_files(files: ReadonlyArray<string>): Promise<void
|
|
|
67
68
|
for (const file of files) console.info(` + ${file}`)
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
// **The observation ledger
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
// **The observation ledger rides the run's own commit** (joshuafolkken/kit#2763). It used to be
|
|
72
|
+
// excluded here (joshuafolkken/kit#1756) and committed afterwards by `pnpm josh observations:flush` as
|
|
73
|
+
// a pull request of its own — a second branch, CI wait and merge behind every run that appended a
|
|
74
|
+
// line, 144 of them in one month. Staged with the run instead, the lines are reviewed and merged with
|
|
75
|
+
// the pull request whose run recorded them, while CI for that pull request is the only wait.
|
|
76
|
+
//
|
|
77
|
+
// **A line that breaks the grammar keeps the old exclusion** (joshuafolkken/kit#2123): the commit
|
|
78
|
+
// goes ahead without the ledger, and `pnpm josh observations:flush` — which refuses the same line and
|
|
79
|
+
// names it — is where it is repaired. The migration is run first, as the flush runs it, so a line
|
|
80
|
+
// still on the old path (joshuafolkken/kit#2724) is committed at the new one. A lane has no ledger
|
|
81
|
+
// line of its own to carry: its writers append to the primary checkout's.
|
|
82
|
+
async function ledger_exclusions(): Promise<ReadonlyArray<string>> {
|
|
83
|
+
const broken = await observation_ledger_prepare.prepare(await git_command.repository_root())
|
|
84
|
+
|
|
85
|
+
return broken.length > 0 ? OBSERVATION_LEDGER_PATHS : []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A migration claim is a copy mid-move, never the ledger itself, so it is never staged.
|
|
89
|
+
function is_stageable(file_path: string, excluded: ReadonlyArray<string>): boolean {
|
|
90
|
+
return !observation_ledger.is_migration_claim(file_path) && !excluded.includes(file_path)
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
// **Said out loud, because the alternative is an unexplained failure.** With the ledger the only
|
|
@@ -89,27 +98,31 @@ function is_stageable(file_path: string): boolean {
|
|
|
89
98
|
// **The paths are parsed rather than matched as substrings**: `docs/maintainers/observations.md.bak` contains the
|
|
90
99
|
// ledger's path, and a hint naming a file the exclusion never touched is a hint that teaches the
|
|
91
100
|
// reader to ignore it.
|
|
92
|
-
function report_excluded_paths(status_output: string): void {
|
|
93
|
-
const
|
|
101
|
+
function report_excluded_paths(status_output: string, excluded: ReadonlyArray<string>): void {
|
|
102
|
+
const left_out = status_output
|
|
94
103
|
.split('\n')
|
|
95
104
|
.filter((line) => line.trim().length > 0)
|
|
96
105
|
.map((line) => observation_ledger.status_path(line))
|
|
97
|
-
.filter((file_path) =>
|
|
106
|
+
.filter((file_path) => excluded.includes(file_path))
|
|
98
107
|
|
|
99
|
-
for (const file_path of
|
|
100
|
-
console.info(
|
|
108
|
+
for (const file_path of left_out) {
|
|
109
|
+
console.info(
|
|
110
|
+
`💡 ${file_path} is not staged: a ledger line breaks the grammar; \`pnpm josh observations:flush\` names it.`,
|
|
111
|
+
)
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
// The status is read after the migration, so an old-path ledger it moved is not staged by its old name.
|
|
104
116
|
async function stage_tracked_files(): Promise<void> {
|
|
117
|
+
const excluded = await ledger_exclusions()
|
|
105
118
|
const status_output = await git_command.status()
|
|
106
119
|
const untracked = git_status
|
|
107
120
|
.list_untracked_files(status_output)
|
|
108
|
-
.filter((file_path) => is_stageable(file_path))
|
|
121
|
+
.filter((file_path) => is_stageable(file_path, excluded))
|
|
109
122
|
|
|
110
|
-
await git_command.add_tracked(
|
|
123
|
+
await git_command.add_tracked(excluded)
|
|
111
124
|
console.info('💡 Auto-staged tracked modified files (git add -u).')
|
|
112
|
-
report_excluded_paths(status_output)
|
|
125
|
+
report_excluded_paths(status_output, excluded)
|
|
113
126
|
await stage_untracked_files(untracked)
|
|
114
127
|
}
|
|
115
128
|
|
|
@@ -10,6 +10,15 @@ const IN_PROGRESS_LABEL = 'in-progress'
|
|
|
10
10
|
// it: a parked child is why a run reports "nothing left that time will fix" rather than waiting
|
|
11
11
|
// forever (joshuafolkken/kit#860).
|
|
12
12
|
const NEEDS_DECISION_LABEL = 'needs-decision'
|
|
13
|
+
// Marks an issue whose defect reaches the verification path, so it runs with nothing beside it
|
|
14
|
+
// (joshuafolkken/kit#2776). A run applies it from the verification-path enumeration in `wip-cap.md`;
|
|
15
|
+
// `backlog:next` enforces it (`backlog-solo.ts`), so the rule no longer rests on a judgement at
|
|
16
|
+
// dispatch time.
|
|
17
|
+
const RUN_SOLO_LABEL = 'run:solo'
|
|
18
|
+
// The other half of the same judgement: an issue a run read and found safe to run beside others
|
|
19
|
+
// (joshuafolkken/kit#2779). Without it an issue lacking `run:solo` could mean "judged parallel" or
|
|
20
|
+
// "never judged", and `backlog:next` withholds the second (`epic-triage.ts`).
|
|
21
|
+
const RUN_LANE_LABEL = 'run:lane'
|
|
13
22
|
// Opts one issue outside any epic into unattended execution (joshuafolkken/kit#906). **Only a
|
|
14
23
|
// person applies it.** `epicrun #<E>` approves the merges inside `#<E>`; this label is the only way
|
|
15
24
|
// a person extends that approval past the epic's edge, so a label an AI could apply to itself would
|
|
@@ -177,24 +186,71 @@ const DEPTH_LABELS: ReadonlyArray<{
|
|
|
177
186
|
},
|
|
178
187
|
]
|
|
179
188
|
|
|
180
|
-
|
|
181
|
-
// applies with its intended color (joshuafolkken/kit#2197). The `epic` and `in-progress` metadata is
|
|
182
|
-
// the one the workflow documents create them with; the labels only a person applies are left out.
|
|
183
|
-
const WORKFLOW_LABELS: ReadonlyArray<{
|
|
189
|
+
interface LabelDefinition {
|
|
184
190
|
name: string
|
|
185
191
|
color: string
|
|
186
192
|
description: string
|
|
187
|
-
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// What `josh start` provisions on a new repository so the first `kickoff` finds every label a run
|
|
196
|
+
// applies with its intended color (joshuafolkken/kit#2197). The `epic` and `in-progress` metadata is
|
|
197
|
+
// the one the workflow documents create them with; the labels only a person applies are left out.
|
|
198
|
+
const WORKFLOW_LABELS: ReadonlyArray<LabelDefinition> = [
|
|
188
199
|
{
|
|
189
200
|
name: EPIC_LABEL,
|
|
190
201
|
color: '5319e7',
|
|
191
202
|
description: 'Tracks a batch of child issues from one split',
|
|
192
203
|
},
|
|
193
204
|
{ name: IN_PROGRESS_LABEL, color: '0075ca', description: 'Work is actively in progress' },
|
|
205
|
+
{
|
|
206
|
+
name: RUN_SOLO_LABEL,
|
|
207
|
+
color: 'b60205',
|
|
208
|
+
description: 'Runs alone in a backlogrun: nothing starts beside it',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: RUN_LANE_LABEL,
|
|
212
|
+
color: '0e8a16',
|
|
213
|
+
description: 'Judged safe to run beside others in a backlogrun',
|
|
214
|
+
},
|
|
194
215
|
...FILING_ROUTE_LABELS,
|
|
195
216
|
...DEPTH_LABELS,
|
|
196
217
|
]
|
|
197
218
|
|
|
219
|
+
// The pull request release classifications `pr-classification.yml` requires exactly one of
|
|
220
|
+
// (joshuafolkken/kit#2797). The check is distributed by `josh sync`, so the labels it asks for are
|
|
221
|
+
// provisioned from this same list — a repository that never had them would otherwise fail the check
|
|
222
|
+
// on every pull request that is not an `enhancement`.
|
|
223
|
+
const BUGFIX_LABEL = 'bugfix'
|
|
224
|
+
const OTHER_CHANGE_LABEL = 'other-change'
|
|
225
|
+
const IGNORE_FOR_RELEASE_LABEL = 'ignore-for-release'
|
|
226
|
+
const RELEASE_CLASSIFICATION_NAMES = [
|
|
227
|
+
BREAKING_CHANGE_LABEL,
|
|
228
|
+
ENHANCEMENT_LABEL,
|
|
229
|
+
BUGFIX_LABEL,
|
|
230
|
+
OTHER_CHANGE_LABEL,
|
|
231
|
+
IGNORE_FOR_RELEASE_LABEL,
|
|
232
|
+
] as const
|
|
233
|
+
type ReleaseClassification = (typeof RELEASE_CLASSIFICATION_NAMES)[number]
|
|
234
|
+
|
|
235
|
+
const RELEASE_CLASSIFICATION_METADATA: Readonly<
|
|
236
|
+
Record<ReleaseClassification, Omit<LabelDefinition, 'name'>>
|
|
237
|
+
> = {
|
|
238
|
+
[BREAKING_CHANGE_LABEL]: { color: 'd93f0b', description: 'Release: a breaking change (major)' },
|
|
239
|
+
[ENHANCEMENT_LABEL]: { color: '84b6eb', description: 'Release: a new feature (minor)' },
|
|
240
|
+
[BUGFIX_LABEL]: { color: 'd73a4a', description: 'Release: a bug fix (patch)' },
|
|
241
|
+
[OTHER_CHANGE_LABEL]: { color: 'c2e0c6', description: 'Release: any other change (patch)' },
|
|
242
|
+
[IGNORE_FOR_RELEASE_LABEL]: { color: 'cfd3d7', description: 'Release: left out of the notes' },
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const RELEASE_CLASSIFICATION_LABELS: ReadonlyArray<LabelDefinition> =
|
|
246
|
+
RELEASE_CLASSIFICATION_NAMES.map((name) => ({ name, ...RELEASE_CLASSIFICATION_METADATA[name] }))
|
|
247
|
+
|
|
248
|
+
// Every label kit provisions on a repository — `josh start` on a new one, `josh sync` on every run.
|
|
249
|
+
const REPOSITORY_LABELS: ReadonlyArray<LabelDefinition> = [
|
|
250
|
+
...WORKFLOW_LABELS,
|
|
251
|
+
...RELEASE_CLASSIFICATION_LABELS,
|
|
252
|
+
]
|
|
253
|
+
|
|
198
254
|
// The shape `gh issue list --json labels` returns; narrowed here so the predicate below takes any
|
|
199
255
|
// listing row without importing a schema.
|
|
200
256
|
interface LabelReference {
|
|
@@ -269,18 +325,25 @@ const ALL_LABELS: ReadonlySet<string> = new Set([
|
|
|
269
325
|
INTERRUPT_ROUTE_LABEL,
|
|
270
326
|
REVIEW_CAP_ROUTE_LABEL,
|
|
271
327
|
REVIEW_ROUND2_SKIPPED_LABEL,
|
|
328
|
+
RUN_LANE_LABEL,
|
|
329
|
+
RUN_SOLO_LABEL,
|
|
272
330
|
SPLIT_ROUTE_LABEL,
|
|
273
331
|
TIER_A_ROUTE_LABEL,
|
|
274
332
|
DEPTH_0_LABEL,
|
|
275
333
|
DEPTH_1_LABEL,
|
|
276
334
|
DEPTH_2_LABEL,
|
|
335
|
+
BUGFIX_LABEL,
|
|
336
|
+
OTHER_CHANGE_LABEL,
|
|
337
|
+
IGNORE_FOR_RELEASE_LABEL,
|
|
277
338
|
])
|
|
278
339
|
|
|
340
|
+
export type { LabelDefinition, ReleaseClassification }
|
|
279
341
|
export {
|
|
280
342
|
ALL_LABELS,
|
|
281
343
|
ALREADY_DONE_LABEL,
|
|
282
344
|
AUTO_OK_LABEL,
|
|
283
345
|
BUG_LABEL,
|
|
346
|
+
BUGFIX_LABEL,
|
|
284
347
|
BREAKING_CHANGE_LABEL,
|
|
285
348
|
depth_label_of,
|
|
286
349
|
DEPTH_0_LABEL,
|
|
@@ -293,14 +356,21 @@ export {
|
|
|
293
356
|
FILING_ROUTE_LABELS,
|
|
294
357
|
has_any_label,
|
|
295
358
|
has_label_name,
|
|
359
|
+
IGNORE_FOR_RELEASE_LABEL,
|
|
296
360
|
IN_PROGRESS_LABEL,
|
|
297
361
|
INTERRUPT_ROUTE_LABEL,
|
|
298
362
|
label_name_of,
|
|
299
363
|
NEEDS_DECISION_LABEL,
|
|
300
364
|
NEEDS_HUMAN_REVIEW_LABEL,
|
|
301
365
|
NOT_DIRECTLY_RUNNABLE_LABELS,
|
|
366
|
+
OTHER_CHANGE_LABEL,
|
|
367
|
+
RELEASE_CLASSIFICATION_LABELS,
|
|
368
|
+
RELEASE_CLASSIFICATION_NAMES,
|
|
369
|
+
REPOSITORY_LABELS,
|
|
302
370
|
REVIEW_CAP_ROUTE_LABEL,
|
|
303
371
|
REVIEW_ROUND2_SKIPPED_LABEL,
|
|
372
|
+
RUN_LANE_LABEL,
|
|
373
|
+
RUN_SOLO_LABEL,
|
|
304
374
|
SPLIT_ROUTE_LABEL,
|
|
305
375
|
TIER_A_ROUTE_LABEL,
|
|
306
376
|
WORKFLOW_LABELS,
|
|
@@ -78,6 +78,7 @@ async function build_context(
|
|
|
78
78
|
headless_refusals: stop_rules.count_headless_refusals(tail),
|
|
79
79
|
lane_child: lane_child_marker.is_child_of(process.cwd()),
|
|
80
80
|
background_pending: lane_background.pending_background_ids(tail).length > 0,
|
|
81
|
+
agent_pending: lane_background.pending_agent_ids(tail).length > 0,
|
|
81
82
|
session_lang: session_lang(),
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { init_install, type InstallStep } from './init-install'
|
|
5
|
+
import { init_logic_workspace } from './init-logic-workspace'
|
|
6
|
+
|
|
7
|
+
const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
|
|
8
|
+
const NODE_MODULES = 'node_modules'
|
|
9
|
+
const PACKAGE_JSON = 'package.json'
|
|
10
|
+
// The static template lists exactly the build scripts kit's own dependency tree carries, which is
|
|
11
|
+
// what installing kit alone needs approved (joshuafolkken/kit#2785).
|
|
12
|
+
const KIT_BUILDS_TEMPLATE = 'templates/pnpm-workspace.static.yaml'
|
|
13
|
+
const HANDOFF_FAILURE = 'josh init from the project-installed kit failed — see the output above'
|
|
14
|
+
// Set on the handed-off run, so a kit that still does not find itself installed in the project — pnpm
|
|
15
|
+
// put it somewhere else — stops instead of installing and handing off again without end.
|
|
16
|
+
const HANDOFF_ENV = 'JOSH_INIT_HANDED_OFF'
|
|
17
|
+
const REPEATED_HANDOFF = `the kit this project installed is not at ${NODE_MODULES}/${KIT_PACKAGE_NAME} — run \`pnpm exec josh init\` where it is`
|
|
18
|
+
|
|
19
|
+
const dependency_fields_schema = z.looseObject({
|
|
20
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
21
|
+
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function real_directory(directory: string): string | undefined {
|
|
25
|
+
return existsSync(directory) ? realpathSync(directory) : undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A `pnpm dlx` (or global) run executes whatever kit its cache held, which may be days behind the
|
|
29
|
+
// registry — and `init` pins the kit it runs as, so a stale cache used to leave its version and its
|
|
30
|
+
// templates in the project for good (joshuafolkken/kit#2794). Only the kit the project itself installs
|
|
31
|
+
// does the setup. Compared by real path, so a global virtual store shared with the project still
|
|
32
|
+
// reads as the project's own kit.
|
|
33
|
+
function is_project_kit(package_directory: string, project_root: string): boolean {
|
|
34
|
+
const installed = real_directory(path.join(project_root, NODE_MODULES, KIT_PACKAGE_NAME))
|
|
35
|
+
|
|
36
|
+
return installed !== undefined && installed === real_directory(package_directory)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function kit_build_flags(template: string): Array<string> {
|
|
40
|
+
return [...init_logic_workspace.template_build_values(template)]
|
|
41
|
+
.filter(([, value]) => value === 'true')
|
|
42
|
+
.map(([name]) => `--allow-build=${name}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function has_kit_dependency(project_root: string): boolean {
|
|
46
|
+
const manifest_path = path.join(project_root, PACKAGE_JSON)
|
|
47
|
+
if (!existsSync(manifest_path)) return false
|
|
48
|
+
const parsed: unknown = JSON.parse(readFileSync(manifest_path, 'utf8'))
|
|
49
|
+
const manifest = dependency_fields_schema.parse(parsed)
|
|
50
|
+
const names = Object.keys({ ...manifest.dependencies, ...manifest.devDependencies })
|
|
51
|
+
|
|
52
|
+
return names.includes(KIT_PACKAGE_NAME)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A project that already lists kit keeps the version it chose; otherwise pnpm picks it, under the
|
|
56
|
+
// project's own `minimumReleaseAge`, so the version installed is never the cache's decision.
|
|
57
|
+
function kit_install_step(has_kit: boolean, template: string): InstallStep {
|
|
58
|
+
if (has_kit) return { label: 'pnpm install', command: 'pnpm', args: ['install'] }
|
|
59
|
+
const args = ['add', '-D', ...kit_build_flags(template), KIT_PACKAGE_NAME]
|
|
60
|
+
|
|
61
|
+
return { label: `pnpm ${args.join(' ')}`, command: 'pnpm', args }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handoff_step(args: ReadonlyArray<string>): InstallStep {
|
|
65
|
+
const handoff_args = ['exec', 'josh', 'init', ...args]
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
label: `pnpm ${handoff_args.join(' ')}`,
|
|
69
|
+
command: 'pnpm',
|
|
70
|
+
args: handoff_args,
|
|
71
|
+
env: { [HANDOFF_ENV]: '1' },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Returns the failure to report, or undefined once the project-installed kit finished the setup.
|
|
76
|
+
function hand_off(
|
|
77
|
+
package_directory: string,
|
|
78
|
+
project_root: string,
|
|
79
|
+
args: ReadonlyArray<string>,
|
|
80
|
+
): string | undefined {
|
|
81
|
+
if (process.env[HANDOFF_ENV] !== undefined) return REPEATED_HANDOFF
|
|
82
|
+
console.info('\n🚀 Installing @joshuafolkken/kit into the project, then running its josh init')
|
|
83
|
+
const template = readFileSync(path.join(package_directory, KIT_BUILDS_TEMPLATE), 'utf8')
|
|
84
|
+
const install = kit_install_step(has_kit_dependency(project_root), template)
|
|
85
|
+
if (!init_install.did_step_succeed(install, project_root)) return `${install.label} failed`
|
|
86
|
+
if (!init_install.did_step_succeed(handoff_step(args), project_root)) return HANDOFF_FAILURE
|
|
87
|
+
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const init_bootstrap = { is_project_kit, kit_build_flags, hand_off }
|
|
92
|
+
export { init_bootstrap }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execaSync } from 'execa'
|
|
2
|
+
|
|
3
|
+
interface InstallStep {
|
|
4
|
+
label: string
|
|
5
|
+
command: string
|
|
6
|
+
args: ReadonlyArray<string>
|
|
7
|
+
env?: Readonly<Record<string, string>>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface InstallRequest {
|
|
11
|
+
is_install: boolean
|
|
12
|
+
rest: ReadonlyArray<string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const NO_INSTALL_FLAG = '--no-install'
|
|
16
|
+
const INSTALL_STEP: InstallStep = { label: 'pnpm install', command: 'pnpm', args: ['install'] }
|
|
17
|
+
const FORMAT_STEP: InstallStep = {
|
|
18
|
+
label: 'pnpm josh format',
|
|
19
|
+
command: 'pnpm',
|
|
20
|
+
args: ['exec', 'josh', 'format'],
|
|
21
|
+
}
|
|
22
|
+
const INSTALL_FAILURE = [
|
|
23
|
+
`${INSTALL_STEP.label} failed — fix the cause above,`,
|
|
24
|
+
`then run it manually: ${INSTALL_STEP.label} && ${FORMAT_STEP.label}`,
|
|
25
|
+
].join(' ')
|
|
26
|
+
const FORMAT_WARNING = [
|
|
27
|
+
` ⚠ ${FORMAT_STEP.label} left problems it could not fix in the project's own files —`,
|
|
28
|
+
`the setup itself succeeded; fix them, then run: ${FORMAT_STEP.label}`,
|
|
29
|
+
].join(' ')
|
|
30
|
+
|
|
31
|
+
// The flag is peeled off before the `--profile` pair is parsed, so the profile parser stays the one
|
|
32
|
+
// `josh start` shares.
|
|
33
|
+
function split_install_flag(args: ReadonlyArray<string>): InstallRequest {
|
|
34
|
+
return {
|
|
35
|
+
is_install: !args.includes(NO_INSTALL_FLAG),
|
|
36
|
+
rest: args.filter((argument) => argument !== NO_INSTALL_FLAG),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function did_step_succeed(step: InstallStep, project_root: string): boolean {
|
|
41
|
+
console.info(`\n$ ${step.label}`)
|
|
42
|
+
const result = execaSync(step.command, step.args, {
|
|
43
|
+
cwd: project_root,
|
|
44
|
+
env: step.env ?? {},
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
reject: false,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return result.exitCode === 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Returns the failure to report, or undefined once the install passed. A failed install stops
|
|
53
|
+
// there: `josh format` runs prettier and the kit itself, both of which the install provides. A failed
|
|
54
|
+
// format does not fail `init` — in a node project it also runs `eslint --fix`, which exits non-zero on
|
|
55
|
+
// the project's own unfixable errors, and those are the project's result, not the setup's (the
|
|
56
|
+
// reason `josh gate` is left out of `init` too).
|
|
57
|
+
function run_post_init_steps(project_root: string): string | undefined {
|
|
58
|
+
console.info('\nDependencies:')
|
|
59
|
+
if (!did_step_succeed(INSTALL_STEP, project_root)) return INSTALL_FAILURE
|
|
60
|
+
if (!did_step_succeed(FORMAT_STEP, project_root)) console.warn(FORMAT_WARNING)
|
|
61
|
+
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const init_install = { NO_INSTALL_FLAG, split_install_flag, did_step_succeed, run_post_init_steps }
|
|
66
|
+
export { init_install }
|
|
67
|
+
export type { InstallStep }
|
|
@@ -138,6 +138,6 @@ function merge_workspace_yaml(existing: string, template: string): string {
|
|
|
138
138
|
return append_user_blocks(cleaned, new_keys, template)
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
const init_logic_workspace = { merge_workspace_yaml }
|
|
141
|
+
const init_logic_workspace = { merge_workspace_yaml, template_build_values }
|
|
142
142
|
|
|
143
143
|
export { init_logic_workspace }
|
package/scripts/init/init.ts
CHANGED
|
@@ -15,11 +15,13 @@ import { execaSync } from 'execa'
|
|
|
15
15
|
import { z } from 'zod'
|
|
16
16
|
import { init_actions, PRETTIER_CONFIG_JS, type FileAction } from './init-actions'
|
|
17
17
|
import { init_ai_copy } from './init-ai-copy'
|
|
18
|
+
import { init_bootstrap } from './init-bootstrap'
|
|
19
|
+
import { init_install } from './init-install'
|
|
18
20
|
import { init_logic } from './init-logic'
|
|
19
21
|
import { PACKAGE_DIR, PROJECT_ROOT } from './init-paths'
|
|
20
22
|
import { init_static } from './init-static'
|
|
21
23
|
import { plugin_install_hint_module } from './plugin-install-hint'
|
|
22
|
-
import { project_profile, type ProjectShape } from './project-profile'
|
|
24
|
+
import { project_profile, type ProjectProfile, type ProjectShape } from './project-profile'
|
|
23
25
|
|
|
24
26
|
const PACKAGE_JSON = 'package.json'
|
|
25
27
|
const KIT_PACKAGE_NAME = '@joshuafolkken/kit'
|
|
@@ -28,6 +30,7 @@ const SAMPLE_INDENT_WIDTH = 4
|
|
|
28
30
|
const ARGUMENT_START_INDEX = 2
|
|
29
31
|
const SAMPLE_INDENT = ' '.repeat(SAMPLE_INDENT_WIDTH)
|
|
30
32
|
const INSTALL_HINT = '→ run `pnpm install` to install what package.json lists'
|
|
33
|
+
const INIT_USAGE = 'josh init [--profile static|node] [--no-install]'
|
|
31
34
|
|
|
32
35
|
function write_new_file(action: FileAction, destination_path: string): void {
|
|
33
36
|
mkdirSync(path.dirname(destination_path), { recursive: true })
|
|
@@ -218,14 +221,10 @@ function merge_project_package_json(shape: ProjectShape): void {
|
|
|
218
221
|
|
|
219
222
|
writeFileSync(package_json_path, merged)
|
|
220
223
|
console.info(` ✔ ${is_existing ? 'updated' : 'created'} package.json`)
|
|
221
|
-
// `init` writes the manifest but installs nothing, so a tool it just listed is absent until the
|
|
222
|
-
// user installs it — and `josh lint` then fails on it rather than skipping it
|
|
223
|
-
// (joshuafolkken/kit#2709). Saying so here is what tells the user the next step.
|
|
224
|
-
console.info(` ${INSTALL_HINT}`)
|
|
225
224
|
}
|
|
226
225
|
|
|
227
|
-
//
|
|
228
|
-
//
|
|
226
|
+
// Under `--no-install` a fresh project has no lefthook yet — added to devDependencies above — so the
|
|
227
|
+
// `prepare` script of the user's own `pnpm install` installs the hooks once it is.
|
|
229
228
|
function install_lefthook(project_root: string = PROJECT_ROOT): void {
|
|
230
229
|
console.info('\nLefthook:')
|
|
231
230
|
const bin = resolve_local_bin(project_root, LEFTHOOK_BIN)
|
|
@@ -285,8 +284,6 @@ function run_ai_file_actions(shape: ProjectShape): void {
|
|
|
285
284
|
// resolved, so `gh repo view` runs once; the position is the pre-existing one.
|
|
286
285
|
const name_with_owner = init_ai_copy.run_ai_copies(shape)
|
|
287
286
|
|
|
288
|
-
if (shape.has_git && shape.profile === 'node') install_lefthook()
|
|
289
|
-
|
|
290
287
|
if (shape.has_github) report_repository_settings(name_with_owner)
|
|
291
288
|
|
|
292
289
|
if (shape.profile === 'node') plugin_install_hint_module.report_plugin_install_hint()
|
|
@@ -302,19 +299,67 @@ function initialize_project(shape: ProjectShape): void {
|
|
|
302
299
|
|
|
303
300
|
run_ai_file_actions(shape)
|
|
304
301
|
if (shape.profile === 'node') project_config.sync_project_config(PROJECT_ROOT)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// A tool `init` just listed is absent until the user installs it — and `josh lint` then fails on it
|
|
305
|
+
// rather than skipping it (joshuafolkken/kit#2709). Saying so is what tells the user the next step.
|
|
306
|
+
function report_manual_install(shape: ProjectShape): void {
|
|
307
|
+
console.info(`\nDependencies:\n ${INSTALL_HINT}`)
|
|
308
|
+
if (shape.has_git && shape.profile === 'node') install_lefthook()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The install runs the `prepare` script, which installs the git hooks. A failure is thrown rather
|
|
312
|
+
// than printed, so `josh start` — which calls this entry point — stops before its first commit.
|
|
313
|
+
function finish_dependencies(shape: ProjectShape, is_install: boolean): void {
|
|
314
|
+
if (!is_install) {
|
|
315
|
+
report_manual_install(shape)
|
|
316
|
+
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const failure = init_install.run_post_init_steps(PROJECT_ROOT)
|
|
321
|
+
if (failure !== undefined) throw new Error(failure)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// A kit run from outside the project — `pnpm dlx`, a global install — sets nothing up itself: it
|
|
325
|
+
// installs the project's kit and hands the whole run to that one (joshuafolkken/kit#2794). Under
|
|
326
|
+
// `--no-install` nothing is installed, so the running kit does the setup as before.
|
|
327
|
+
function did_hand_off(args: ReadonlyArray<string>): boolean {
|
|
328
|
+
if (init_bootstrap.is_project_kit(PACKAGE_DIR, PROJECT_ROOT)) return false
|
|
329
|
+
const failure = init_bootstrap.hand_off(PACKAGE_DIR, PROJECT_ROOT, args)
|
|
330
|
+
if (failure !== undefined) throw new Error(failure)
|
|
331
|
+
|
|
332
|
+
return true
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function set_up_project(requested: ProjectProfile | undefined, is_install: boolean): void {
|
|
336
|
+
const shape = project_profile.inspect_project(PROJECT_ROOT, requested)
|
|
337
|
+
|
|
338
|
+
initialize_project(shape)
|
|
339
|
+
finish_dependencies(shape, is_install)
|
|
305
340
|
console.info('\n✅ Done.\n')
|
|
306
341
|
}
|
|
307
342
|
|
|
308
343
|
function main(args: ReadonlyArray<string> = []): void {
|
|
309
344
|
if (did_refuse_self_run(PACKAGE_DIR, PROJECT_ROOT)) return
|
|
310
|
-
const
|
|
311
|
-
|
|
345
|
+
const { is_install, rest } = init_install.split_install_flag(args)
|
|
346
|
+
// Validated before the hand-off, so a malformed invocation is refused before kit is installed.
|
|
347
|
+
const requested = project_profile.requested_profile(rest, INIT_USAGE)
|
|
312
348
|
|
|
313
|
-
|
|
349
|
+
if (!is_install || !did_hand_off(args)) set_up_project(requested, is_install)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function run_cli(args: ReadonlyArray<string>): void {
|
|
353
|
+
try {
|
|
354
|
+
main(args)
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error(`\n✖ ${error instanceof Error ? error.message : String(error)}\n`)
|
|
357
|
+
process.exitCode = 1
|
|
358
|
+
}
|
|
314
359
|
}
|
|
315
360
|
|
|
316
361
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
317
|
-
|
|
362
|
+
run_cli(process.argv.slice(ARGUMENT_START_INDEX))
|
|
318
363
|
}
|
|
319
364
|
|
|
320
365
|
const init = {
|
|
@@ -46,7 +46,7 @@ const STEP_LABELS: Readonly<Record<StartStep, string>> = {
|
|
|
46
46
|
initialize: 'Initialize kit (the same setup as josh init)',
|
|
47
47
|
commit: 'Commit every file as the initial commit',
|
|
48
48
|
repository: 'Create the GitHub repository and push main',
|
|
49
|
-
labels: 'Add the workflow labels the repository is missing',
|
|
49
|
+
labels: 'Add the workflow and release-classification labels the repository is missing',
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function refused(refusal: string): StepPlan {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { git_gh_exec } from '#scripts/git/git-gh-exec'
|
|
3
|
-
import {
|
|
3
|
+
import { repository_labels } from '#scripts/repo/repository-labels'
|
|
4
4
|
import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
|
|
5
5
|
import { execaSync } from 'execa'
|
|
6
6
|
import { main as init_main } from './init'
|
|
@@ -14,10 +14,7 @@ interface StepContext {
|
|
|
14
14
|
visibility: Visibility
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
type WorkflowLabel = (typeof WORKFLOW_LABELS)[number]
|
|
18
|
-
|
|
19
17
|
const DEFAULT_BRANCH = 'main'
|
|
20
|
-
const LABELS_ENDPOINT = 'repos/{owner}/{repo}/labels'
|
|
21
18
|
const GH_INSTALL_HINT =
|
|
22
19
|
'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/, run gh auth login, then run josh start again. Nothing was changed.'
|
|
23
20
|
const GH_AUTH_HINT =
|
|
@@ -100,29 +97,10 @@ function create_github_repository(context: StepContext): void {
|
|
|
100
97
|
run('gh', ['repo', 'create', name, visibility, ...source], context.root)
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return WORKFLOW_LABELS.filter((label) => !names.has(label.name))
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function create_label(label: WorkflowLabel, root: string): void {
|
|
112
|
-
const fields = [`name=${label.name}`, `color=${label.color}`, `description=${label.description}`]
|
|
113
|
-
|
|
114
|
-
run('gh', ['api', LABELS_ENDPOINT, ...fields.flatMap((field) => ['-f', field]), '--silent'], root)
|
|
115
|
-
console.info(` ✔ label ${label.name}`)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function add_missing_labels(context: StepContext): void {
|
|
119
|
-
const listing = execaSync('gh', ['api', '--paginate', LABELS_ENDPOINT, '--jq', '.[].name'], {
|
|
120
|
-
...git_gh_exec.direct_environment(),
|
|
121
|
-
cwd: context.root,
|
|
122
|
-
})
|
|
123
|
-
const existing = listing.stdout.split('\n').filter((name) => name.length > 0)
|
|
124
|
-
|
|
125
|
-
for (const label of missing_workflow_labels(existing)) create_label(label, context.root)
|
|
100
|
+
// The repository was created by the step before, so `gh` resolves the placeholder from the
|
|
101
|
+
// directory `josh start` runs in, which is the root.
|
|
102
|
+
function add_missing_labels(): void {
|
|
103
|
+
repository_labels.ensure_labels('{owner}/{repo}')
|
|
126
104
|
}
|
|
127
105
|
|
|
128
106
|
const STEP_ACTIONS: Readonly<Record<StartStep, (context: StepContext) => void>> = {
|
|
@@ -160,7 +138,6 @@ const start_steps = {
|
|
|
160
138
|
read_git_state,
|
|
161
139
|
github_cli_refusal,
|
|
162
140
|
self_run_refusal,
|
|
163
|
-
missing_workflow_labels,
|
|
164
141
|
run_steps,
|
|
165
142
|
}
|
|
166
143
|
export { start_steps }
|