@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
|
+
import { run_liveness } from '#scripts/run/run-liveness'
|
|
3
4
|
import { lane_child_invocation } from './lane-child-invocation'
|
|
4
5
|
import { lane_handoff } from './lane-handoff'
|
|
5
6
|
|
|
@@ -22,15 +23,20 @@ const MS_PER_SECOND = 1000
|
|
|
22
23
|
const ISSUE_PATTERN = /^[1-9]\d*$/u
|
|
23
24
|
const PROCESS_FOUND = 0
|
|
24
25
|
|
|
26
|
+
// `is_settled` is read once, when the wait starts: a child that ended between two `lane:await` calls
|
|
27
|
+
// never appears in the second one, and only its settled issue tells it apart from one not yet
|
|
28
|
+
// launched (joshuafolkken/kit#3133).
|
|
25
29
|
interface AwaitState {
|
|
26
30
|
appeared: boolean
|
|
27
31
|
disappeared_at: number | undefined
|
|
28
32
|
first_polled_at: number | undefined
|
|
33
|
+
is_settled?: boolean
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
// Injectable for testing -- the CLI never overrides these.
|
|
32
37
|
interface AwaitOptions {
|
|
33
38
|
is_running?: (issue: string) => boolean
|
|
39
|
+
is_settled?: (issue: string) => Promise<boolean>
|
|
34
40
|
poll_ms?: number
|
|
35
41
|
reconfirm_ms?: number
|
|
36
42
|
never_appeared_timeout_ms?: number
|
|
@@ -43,6 +49,7 @@ interface CheckConfig {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
interface RunConfig extends CheckConfig {
|
|
52
|
+
is_settled: (issue: string) => Promise<boolean>
|
|
46
53
|
poll_ms: number
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -55,23 +62,42 @@ function is_process_running_default(issue: string): boolean {
|
|
|
55
62
|
return lane_handoff.is_ship_running(issue, process.cwd())
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
// The same positive evidence `run:liveness` answers `settled` on: the issue closed, or it was parked.
|
|
66
|
+
async function is_settled_default(issue: string): Promise<boolean> {
|
|
67
|
+
return (await run_liveness.read_child_settled(issue)) === true
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
function
|
|
63
|
-
return
|
|
70
|
+
function make_initial_state(is_settled: boolean): AwaitState {
|
|
71
|
+
return { appeared: false, disappeared_at: undefined, first_polled_at: undefined, is_settled }
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
function
|
|
74
|
+
async function make_states(
|
|
75
|
+
issues: ReadonlyArray<string>,
|
|
76
|
+
is_settled: (issue: string) => Promise<boolean>,
|
|
77
|
+
): Promise<Map<string, AwaitState>> {
|
|
78
|
+
const settled = await Promise.all(issues.map(async (issue) => await is_settled(issue)))
|
|
79
|
+
|
|
80
|
+
return new Map(issues.map((issue, index) => [issue, make_initial_state(settled[index] === true)]))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolve_timing(
|
|
84
|
+
options: AwaitOptions,
|
|
85
|
+
): Pick<RunConfig, 'never_appeared_timeout_ms' | 'poll_ms' | 'reconfirm_ms'> {
|
|
67
86
|
return {
|
|
68
|
-
is_running: options.is_running ?? is_process_running_default,
|
|
69
87
|
poll_ms: options.poll_ms ?? DEFAULT_POLL_MS,
|
|
70
88
|
reconfirm_ms: options.reconfirm_ms ?? RECONFIRM_MS,
|
|
71
89
|
never_appeared_timeout_ms: options.never_appeared_timeout_ms ?? NEVER_APPEARED_TIMEOUT_MS,
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
function resolve_options(options: AwaitOptions): RunConfig {
|
|
94
|
+
return {
|
|
95
|
+
is_running: options.is_running ?? is_process_running_default,
|
|
96
|
+
is_settled: options.is_settled ?? is_settled_default,
|
|
97
|
+
...resolve_timing(options),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
75
101
|
function update_liveness(state: AwaitState, is_live: boolean): void {
|
|
76
102
|
if (!is_live) return
|
|
77
103
|
|
|
@@ -79,13 +105,16 @@ function update_liveness(state: AwaitState, is_live: boolean): void {
|
|
|
79
105
|
state.disappeared_at = undefined
|
|
80
106
|
}
|
|
81
107
|
|
|
82
|
-
//
|
|
108
|
+
// Returns the issue when it had already settled before the wait started — it ended and will never
|
|
109
|
+
// appear. Otherwise throws if it never appeared within the timeout window, and records the first-seen time.
|
|
83
110
|
function check_not_appeared(
|
|
84
111
|
issue: string,
|
|
85
112
|
state: AwaitState,
|
|
86
113
|
now_ms: number,
|
|
87
114
|
timeout_ms: number,
|
|
88
|
-
):
|
|
115
|
+
): string | undefined {
|
|
116
|
+
if (state.is_settled === true) return issue
|
|
117
|
+
|
|
89
118
|
state.first_polled_at ??= now_ms
|
|
90
119
|
const elapsed_ms = now_ms - state.first_polled_at
|
|
91
120
|
|
|
@@ -94,10 +123,13 @@ function check_not_appeared(
|
|
|
94
123
|
`lane child ${issue} never appeared within ${String(timeout_ms / MS_PER_SECOND)}s`,
|
|
95
124
|
)
|
|
96
125
|
}
|
|
126
|
+
|
|
127
|
+
return undefined
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
// Returns the issue that has confirmed-completed this tick, `undefined` if none yet.
|
|
100
|
-
// The `appeared` guard prevents a process that never showed up from being declared done
|
|
131
|
+
// The `appeared` guard prevents a process that never showed up from being declared done, unless its
|
|
132
|
+
// issue had already settled when the wait started.
|
|
101
133
|
function check_issue(
|
|
102
134
|
issue: string,
|
|
103
135
|
state: AwaitState,
|
|
@@ -111,9 +143,7 @@ function check_issue(
|
|
|
111
143
|
if (is_live) return undefined
|
|
112
144
|
|
|
113
145
|
if (!state.appeared) {
|
|
114
|
-
check_not_appeared(issue, state, now_ms, config.never_appeared_timeout_ms)
|
|
115
|
-
|
|
116
|
-
return undefined
|
|
146
|
+
return check_not_appeared(issue, state, now_ms, config.never_appeared_timeout_ms)
|
|
117
147
|
}
|
|
118
148
|
|
|
119
149
|
if (state.disappeared_at === undefined) {
|
|
@@ -144,7 +174,7 @@ async function wait_for_any(
|
|
|
144
174
|
options: AwaitOptions = {},
|
|
145
175
|
): Promise<string> {
|
|
146
176
|
const config = resolve_options(options)
|
|
147
|
-
const states = make_states(issues)
|
|
177
|
+
const states = await make_states(issues, config.is_settled)
|
|
148
178
|
|
|
149
179
|
for (;;) {
|
|
150
180
|
const completed = check_any(states, Date.now(), config)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, rmSync } from 'node:fs'
|
|
2
2
|
import { git_command } from '#scripts/git/git-command'
|
|
3
3
|
import { git_worktree } from '#scripts/git/git-worktree'
|
|
4
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
4
5
|
import { lane_paths } from './lane-paths'
|
|
5
6
|
import { lane_reap } from './lane-reap'
|
|
6
7
|
import { lane_registry, type LaneInfo } from './lane-registry'
|
|
@@ -147,7 +148,9 @@ async function try_close(issue: string): Promise<boolean> {
|
|
|
147
148
|
const outcome = await close_lane(issue)
|
|
148
149
|
|
|
149
150
|
return outcome.kind !== 'incomplete'
|
|
150
|
-
} catch {
|
|
151
|
+
} catch (error) {
|
|
152
|
+
error_text.trace_swallowed('lane_close.try_close', error)
|
|
153
|
+
|
|
151
154
|
return false
|
|
152
155
|
}
|
|
153
156
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { josh_command } from '#scripts/josh/josh-run'
|
|
4
|
+
import { cli_flags } from '#scripts/lib/cli-flags'
|
|
5
5
|
import { INSTALL_TIMEOUT_MS } from '#scripts/lib/timeouts'
|
|
6
6
|
import { execa } from 'execa'
|
|
7
7
|
|
|
@@ -35,21 +35,15 @@ interface LaunchContext {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function read_context(argv: ReadonlyArray<string>): LaunchContext | undefined {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (issue === undefined || rest.length > 0 || !ISSUE_PATTERN.test(issue)) return undefined
|
|
48
|
-
|
|
49
|
-
return { issue, stash: parsed.values.stash }
|
|
50
|
-
} catch {
|
|
51
|
-
return undefined
|
|
52
|
-
}
|
|
38
|
+
const parsed = cli_flags.arguments_of(argv, OPTIONS)
|
|
39
|
+
|
|
40
|
+
if (parsed === undefined) return undefined
|
|
41
|
+
|
|
42
|
+
const [issue, ...rest] = parsed.positionals
|
|
43
|
+
|
|
44
|
+
if (issue === undefined || rest.length > 0 || !ISSUE_PATTERN.test(issue)) return undefined
|
|
45
|
+
|
|
46
|
+
return { issue, stash: parsed.values.stash }
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
// The pop brought in the `pnpm-lock.yaml` `josh latest` rewrote, so this one lane installs a second
|
|
@@ -6,6 +6,7 @@ import { agent_argv } from '#scripts/agent/agent-argv'
|
|
|
6
6
|
import { agent_role_profile } from '#scripts/agent/agent-role-profile'
|
|
7
7
|
import { git_common_directory } from '#scripts/git/git-common-directory'
|
|
8
8
|
import { stamp_file } from '#scripts/josh/stamp-file'
|
|
9
|
+
import { error_text } from '#scripts/lib/error-message'
|
|
9
10
|
import { detached_launch } from '#scripts/run/detached-launch'
|
|
10
11
|
import { run_ship_review } from '#scripts/run/run-ship-review'
|
|
11
12
|
import { z } from 'zod'
|
|
@@ -102,7 +103,9 @@ function write_response(target: string, body: z.infer<typeof response_schema>):
|
|
|
102
103
|
async function launched(lane: LaneInfo, round: ReviewRound): Promise<boolean> {
|
|
103
104
|
try {
|
|
104
105
|
return await launch_review(lane, round)
|
|
105
|
-
} catch {
|
|
106
|
+
} catch (error) {
|
|
107
|
+
error_text.trace_swallowed('openai_review_broker.launched', error)
|
|
108
|
+
|
|
106
109
|
return false
|
|
107
110
|
}
|
|
108
111
|
}
|
package/scripts/lib/cli-flags.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseArgs, type ParseArgsConfig } from 'node:util'
|
|
1
|
+
import { parseArgs, type ParseArgsConfig, type ParseArgsOptionsConfig } from 'node:util'
|
|
2
2
|
|
|
3
3
|
// Argument reading for the commands under `scripts/`.
|
|
4
4
|
//
|
|
@@ -32,6 +32,35 @@ function parse_or_undefined<T extends ParseArgsConfig>(
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// The two shapes every command reads `argv` in: flags alone, or flags beside positionals. Both are
|
|
36
|
+
// strict, so an unknown flag is `undefined` rather than a silently ignored typo; the commands had each
|
|
37
|
+
// carried a one-line wrapper spelling these out (joshuafolkken/kit#3072).
|
|
38
|
+
type FlagsOnly<T extends ParseArgsOptionsConfig> = ReturnType<
|
|
39
|
+
typeof parseArgs<{ args: Array<string>; options: T }>
|
|
40
|
+
>
|
|
41
|
+
type WithPositionals<T extends ParseArgsOptionsConfig> = ReturnType<
|
|
42
|
+
typeof parseArgs<{ args: Array<string>; options: T; allowPositionals: true }>
|
|
43
|
+
>
|
|
44
|
+
|
|
45
|
+
function values_of<T extends ParseArgsOptionsConfig>(
|
|
46
|
+
argv: ReadonlyArray<string>,
|
|
47
|
+
options: T,
|
|
48
|
+
): FlagsOnly<T>['values'] | undefined {
|
|
49
|
+
return parse_or_undefined({ args: [...argv], options })?.values
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function arguments_of<T extends ParseArgsOptionsConfig>(
|
|
53
|
+
argv: ReadonlyArray<string>,
|
|
54
|
+
options: T,
|
|
55
|
+
): WithPositionals<T> | undefined {
|
|
56
|
+
return parse_or_undefined({ args: [...argv], options, allowPositionals: true })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A string option's value, or `undefined` where the flag was absent or parsed as anything else.
|
|
60
|
+
function string_of(value: unknown): string | undefined {
|
|
61
|
+
return typeof value === 'string' ? value : undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cli_flags = { refuse_unknown_flags, parse_or_undefined, values_of, arguments_of, string_of }
|
|
36
65
|
|
|
37
66
|
export { cli_flags }
|
|
@@ -4,6 +4,17 @@ function message_of(error: unknown): string {
|
|
|
4
4
|
return error instanceof Error ? error.message : String(error)
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const DEBUG_ENV_KEY = 'JOSH_DEBUG'
|
|
8
|
+
|
|
9
|
+
// A catch that folds a failure into a state verdict — "dirty", "not merged", "not written" — leaves no
|
|
10
|
+
// trace of why by default, so a wrong verdict cannot be followed back to its cause (joshuafolkken/kit#3067).
|
|
11
|
+
// `JOSH_DEBUG` set to any non-blank value writes the swallowed reason to stderr; unset, nothing changes.
|
|
12
|
+
function trace_swallowed(where: string, error: unknown): void {
|
|
13
|
+
if ((process.env[DEBUG_ENV_KEY] ?? '').trim() === '') return
|
|
14
|
+
|
|
15
|
+
process.stderr.write(`josh debug: ${where}: ${message_of(error)}\n`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const error_text = { message_of, trace_swallowed }
|
|
8
19
|
|
|
9
20
|
export { error_text }
|
|
@@ -40,10 +40,6 @@ const LS_FILES_FLAGS: ReadonlyArray<string> = [
|
|
|
40
40
|
'-z',
|
|
41
41
|
]
|
|
42
42
|
const NUL = '\0'
|
|
43
|
-
// `--show-toplevel` prints the absolute repository (or worktree) root from anywhere inside it, so the
|
|
44
|
-
// root-relative names `--full-name` returns resolve to the right absolute paths wherever the command
|
|
45
|
-
// was invoked.
|
|
46
|
-
const SHOW_TOPLEVEL: ReadonlyArray<string> = ['rev-parse', '--show-toplevel']
|
|
47
43
|
|
|
48
44
|
function is_lint_target(relative_path: string): boolean {
|
|
49
45
|
return LINT_EXTENSIONS.has(path.extname(relative_path))
|
|
@@ -53,12 +49,6 @@ function split_names(output: string): ReadonlyArray<string> {
|
|
|
53
49
|
return output.split(NUL).filter((name) => name.length > 0)
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
// `git_spawn.read` is the one place ordinary git commands are spawned from, so this reuses it rather
|
|
57
|
-
// than growing a second spawn site (joshuafolkken/kit#1640).
|
|
58
|
-
async function repo_root(): Promise<string> {
|
|
59
|
-
return await git_spawn.read([...SHOW_TOPLEVEL])
|
|
60
|
-
}
|
|
61
|
-
|
|
62
52
|
// The names resolve against the repository `root`, never the process cwd: `--full-name` prints them
|
|
63
53
|
// relative to the root wherever the command runs, so resolving against cwd would double the prefix
|
|
64
54
|
// and drop every file when the command is invoked from a subdirectory.
|
|
@@ -73,7 +63,6 @@ async function lint_target_files(root: string): Promise<ReadonlyArray<string>> {
|
|
|
73
63
|
const line_targets = {
|
|
74
64
|
is_lint_target,
|
|
75
65
|
lint_target_files,
|
|
76
|
-
repo_root,
|
|
77
66
|
split_names,
|
|
78
67
|
}
|
|
79
68
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { git_command } from '#scripts/git/git-command'
|
|
4
5
|
import { line_budget, type FileBudget, type LineBudget } from './line-budget'
|
|
5
6
|
import { line_targets } from './line-targets'
|
|
6
7
|
|
|
@@ -122,10 +123,11 @@ function render(budgets: ReadonlyArray<FileBudget>, project_root: string): strin
|
|
|
122
123
|
return [header(budgets), ...rows_for(budgets, project_root)].join('\n')
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
// The scan resolves the repository root first — `budgets_for` and the row display both key off it
|
|
126
|
-
//
|
|
126
|
+
// The scan resolves the repository root first — `budgets_for` and the row display both key off it, and
|
|
127
|
+
// `git ls-files --full-name` names files relative to it — so a run started from a subdirectory reports
|
|
128
|
+
// the same files as one started from the root.
|
|
127
129
|
async function run_scan(): Promise<number> {
|
|
128
|
-
const root = await
|
|
130
|
+
const root = await git_command.repository_root()
|
|
129
131
|
const budgets = await scan_budgets(root)
|
|
130
132
|
|
|
131
133
|
process.stdout.write(`${render(budgets, root)}\n`)
|
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { git_location_environment } from '#scripts/git/git-location-environment'
|
|
4
4
|
import { ls_remote_branch_arguments } from '#scripts/git/git-ls-remote'
|
|
5
|
-
import {
|
|
5
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
6
6
|
|
|
7
7
|
// The git probes propagation needs before it writes anything into a working tree.
|
|
8
8
|
//
|
|
@@ -16,7 +16,6 @@ const GIT_TIMEOUT_MS = 10_000
|
|
|
16
16
|
// `rev-parse` and tight for an ssh handshake, and a probe that times out answers "no" — which for
|
|
17
17
|
// the two below would report a push that never reached origin as a push nobody attempted.
|
|
18
18
|
const REMOTE_TIMEOUT_MS = 60_000
|
|
19
|
-
const SUCCESS_EXIT_CODE = 0
|
|
20
19
|
const DEFAULT_BRANCH_REF = 'refs/remotes/origin/HEAD'
|
|
21
20
|
const FALLBACK_DEFAULT_BRANCH = 'main'
|
|
22
21
|
const ORIGIN_PREFIX = 'refs/remotes/origin/'
|
|
@@ -54,15 +53,11 @@ function run_git(
|
|
|
54
53
|
args: ReadonlyArray<string>,
|
|
55
54
|
timeout_ms: number = GIT_TIMEOUT_MS,
|
|
56
55
|
): string | undefined {
|
|
57
|
-
|
|
56
|
+
return git_spawn_sync.read(args, {
|
|
58
57
|
cwd: repository_path,
|
|
59
|
-
reject: false,
|
|
60
58
|
timeout: timeout_ms,
|
|
61
|
-
env: {
|
|
62
|
-
extendEnv: true,
|
|
59
|
+
env: { ...git_spawn_sync.C_LOCALE, ...git_location_environment.location_free_environment() },
|
|
63
60
|
})
|
|
64
|
-
|
|
65
|
-
return result.exitCode === SUCCESS_EXIT_CODE ? result.stdout.trim() : undefined
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
// The repository's default branch, from the remote's own HEAD. Falls back to `main` when the ref is
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { git_command } from '#scripts/git/git-command'
|
|
1
2
|
import { line_targets } from '#scripts/lines/line-targets'
|
|
2
3
|
import { import_graph } from './import-graph'
|
|
3
4
|
import { refactor_lint, type CategoryResult } from './refactor-lint'
|
|
@@ -97,7 +98,7 @@ async function scan(root: string): Promise<ScanResult> {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
async function run_scan(): Promise<number> {
|
|
100
|
-
const root = await
|
|
101
|
+
const root = await git_command.repository_root()
|
|
101
102
|
const result = await scan(root)
|
|
102
103
|
|
|
103
104
|
process.stdout.write(`${render_result(result)}\n`)
|
|
@@ -99,25 +99,33 @@ function is_github_tarball_line(line: string): boolean {
|
|
|
99
99
|
return line.trimStart().startsWith('tarball: https://npm.pkg.github.com/')
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// Integrities are keyed by `name@version`, so two scoped packages sharing a version never collide.
|
|
103
|
+
function package_id(key: string): string {
|
|
104
|
+
const name = fix_gh_packages_logic.package_path_from_key(key)
|
|
105
|
+
|
|
106
|
+
return `${name}@${fix_gh_packages_logic.package_version_from_key(key)}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function clean_scoped_line(
|
|
103
110
|
line: string,
|
|
104
|
-
|
|
111
|
+
id: string,
|
|
105
112
|
integrities: ReadonlyMap<string, IntegrityPair>,
|
|
106
113
|
): string | undefined {
|
|
107
|
-
if (!
|
|
114
|
+
if (!id) return line
|
|
108
115
|
if (is_github_tarball_line(line)) return undefined
|
|
109
116
|
const cleaned = remove_flow_tarball(line)
|
|
110
|
-
const pair = integrities.get(
|
|
117
|
+
const pair = integrities.get(id)
|
|
111
118
|
if (pair === undefined) return cleaned
|
|
112
119
|
|
|
113
120
|
return cleaned.split(`integrity: ${pair[0]}`).join(`integrity: ${pair[1]}`)
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
function
|
|
117
|
-
if (!is_package_entry(line)) return
|
|
118
|
-
const prefix =
|
|
123
|
+
function next_scoped_id(line: string, id: string): string {
|
|
124
|
+
if (!is_package_entry(line)) return id
|
|
125
|
+
const prefix = ` '${SCOPE}/`
|
|
126
|
+
if (!line.startsWith(prefix)) return ''
|
|
119
127
|
|
|
120
|
-
return
|
|
128
|
+
return package_id(line.slice(" '".length).split("'", 1)[0] ?? '')
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
function integrity_pair(
|
|
@@ -135,12 +143,12 @@ function lock_integrities(
|
|
|
135
143
|
): Map<string, IntegrityPair> {
|
|
136
144
|
const packages = fix_gh_packages_logic.parse_lockfile_packages(lockfile)
|
|
137
145
|
const pairs = new Map<string, IntegrityPair>()
|
|
138
|
-
const
|
|
146
|
+
const scoped_entries = Object.entries(packages).filter(([key]) => key.startsWith(`${SCOPE}/`))
|
|
139
147
|
|
|
140
|
-
for (const [key, entry] of
|
|
141
|
-
const
|
|
142
|
-
const pair = integrity_pair(entry.resolution?.integrity, npm_integrities.get(
|
|
143
|
-
if (pair !== undefined) pairs.set(
|
|
148
|
+
for (const [key, entry] of scoped_entries) {
|
|
149
|
+
const id = package_id(key)
|
|
150
|
+
const pair = integrity_pair(entry.resolution?.integrity, npm_integrities.get(id))
|
|
151
|
+
if (pair !== undefined) pairs.set(id, pair)
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
return pairs
|
|
@@ -150,17 +158,17 @@ function push_if_defined(lines: Array<string>, cleaned: string | undefined): voi
|
|
|
150
158
|
if (cleaned !== undefined) lines.push(cleaned)
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
function
|
|
161
|
+
function rewrite_scoped_lockfile(
|
|
154
162
|
lockfile: string,
|
|
155
163
|
npm_integrities: ReadonlyMap<string, string>,
|
|
156
164
|
): string {
|
|
157
|
-
let
|
|
165
|
+
let id = ''
|
|
158
166
|
const lines: Array<string> = []
|
|
159
167
|
const integrities = lock_integrities(lockfile, npm_integrities)
|
|
160
168
|
|
|
161
169
|
for (const line of lockfile.split('\n')) {
|
|
162
|
-
|
|
163
|
-
const cleaned =
|
|
170
|
+
id = next_scoped_id(line, id)
|
|
171
|
+
const cleaned = clean_scoped_line(line, id, integrities)
|
|
164
172
|
|
|
165
173
|
push_if_defined(lines, cleaned)
|
|
166
174
|
}
|
|
@@ -206,7 +214,8 @@ function previous_registry(project_npmrc: string, user_npmrc: string): string {
|
|
|
206
214
|
|
|
207
215
|
function plan(project_npmrc: string, user_npmrc: string, lockfile: string): MigrationPlan {
|
|
208
216
|
const packages = scoped_packages(lockfile)
|
|
209
|
-
|
|
217
|
+
// Every scoped package may migrate; `migrate` blocks any whose locked version is not on public npm.
|
|
218
|
+
const blocked: Array<string> = []
|
|
210
219
|
if (has_duplicate_mapping(project_npmrc, user_npmrc)) blocked.push('duplicate registry mappings')
|
|
211
220
|
const unsupported = unsupported_registry(project_npmrc)
|
|
212
221
|
if (unsupported !== undefined) blocked.push(unsupported)
|
|
@@ -220,7 +229,7 @@ const migrate_logic = {
|
|
|
220
229
|
plan,
|
|
221
230
|
github_tarballs,
|
|
222
231
|
is_github_tarball,
|
|
223
|
-
|
|
232
|
+
rewrite_scoped_lockfile,
|
|
224
233
|
scoped_versions,
|
|
225
234
|
}
|
|
226
235
|
|
|
@@ -115,8 +115,9 @@ async function verify_public_versions(
|
|
|
115
115
|
|
|
116
116
|
for (const [index, [name, version]] of versions.entries()) {
|
|
117
117
|
const integrity = probed[index]
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const id = `${name}@${version}`
|
|
119
|
+
if (integrity === undefined) missing.push(id)
|
|
120
|
+
else integrities.set(id, integrity)
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
return { missing, integrities }
|
|
@@ -183,7 +184,7 @@ async function verify_resolution(
|
|
|
183
184
|
const old_tarballs = migrate_logic.github_tarballs(lockfile)
|
|
184
185
|
if (old_tarballs.length > 0) throw new Error(`GitHub tarballs remain: ${old_tarballs.join(', ')}`)
|
|
185
186
|
|
|
186
|
-
if (migrate_logic.
|
|
187
|
+
if (migrate_logic.rewrite_scoped_lockfile(lockfile, integrities) !== lockfile) {
|
|
187
188
|
throw new Error('lockfile integrity does not match public npm')
|
|
188
189
|
}
|
|
189
190
|
}
|
|
@@ -197,7 +198,7 @@ async function apply(
|
|
|
197
198
|
writeFileSync(context.npmrc_path, context.plan.content)
|
|
198
199
|
writeFileSync(
|
|
199
200
|
context.lockfile_path,
|
|
200
|
-
migrate_logic.
|
|
201
|
+
migrate_logic.rewrite_scoped_lockfile(context.original_lockfile, integrities),
|
|
201
202
|
)
|
|
202
203
|
await verify_resolution(context, dependencies, integrities)
|
|
203
204
|
} catch (error) {
|
|
@@ -215,7 +216,7 @@ function is_already_migrated(
|
|
|
215
216
|
): boolean {
|
|
216
217
|
return (
|
|
217
218
|
context.plan.content === context.original_npmrc &&
|
|
218
|
-
migrate_logic.
|
|
219
|
+
migrate_logic.rewrite_scoped_lockfile(context.original_lockfile, integrities) ===
|
|
219
220
|
context.original_lockfile
|
|
220
221
|
)
|
|
221
222
|
}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
1
|
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
|
|
3
3
|
import { github_release } from './github-release'
|
|
4
4
|
import { github_release_environment } from './github-release-environment'
|
|
5
5
|
|
|
6
|
+
// A release published without the tag list would pick its previous tag from nothing, so a failed
|
|
7
|
+
// `git tag` stops the run rather than reading as "no tags".
|
|
8
|
+
function local_tags(): Array<string> {
|
|
9
|
+
const output = git_spawn_sync.read(['tag', '--list'])
|
|
10
|
+
if (output === undefined) throw new Error('git tag --list failed')
|
|
11
|
+
|
|
12
|
+
return output.split('\n')
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
async function main(): Promise<void> {
|
|
7
16
|
const { token, tag, settings } = github_release_environment.read_release_input(process.env)
|
|
8
|
-
const tags = execFileSync('git', ['tag', '--list'], { encoding: 'utf8' }).trim().split('\n')
|
|
9
17
|
|
|
10
|
-
console.info(await github_release.publish(fetch, token, tag, { ...settings, tags }))
|
|
18
|
+
console.info(await github_release.publish(fetch, token, tag, { ...settings, tags: local_tags() }))
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
if (process.argv[1] === fileURLToPath(import.meta.url)) await main()
|
|
22
|
+
|
|
23
|
+
const github_release_cli = { local_tags }
|
|
24
|
+
export { github_release_cli }
|
|
@@ -2,6 +2,7 @@ import { git_gh_command } from '#scripts/gh/git-gh-command'
|
|
|
2
2
|
import { git_pr_checks } from '#scripts/gh/git-pr-checks'
|
|
3
3
|
import { git_command } from '#scripts/git/git-command'
|
|
4
4
|
import { git_remote_branch } from '#scripts/git/git-remote-branch'
|
|
5
|
+
import { repository_lock } from '#scripts/git/repository-lock'
|
|
5
6
|
import { write_version } from '#scripts/version/bump-version'
|
|
6
7
|
import { version_targets } from '#scripts/version/version-targets'
|
|
7
8
|
import type { ReleasePlan } from './release-plan'
|
|
@@ -20,6 +21,9 @@ const { PACKAGE_JSON } = version_targets
|
|
|
20
21
|
const SUCCESS_EXIT_CODE = 0
|
|
21
22
|
const FAILURE_EXIT_CODE = 1
|
|
22
23
|
const IGNORE_FOR_RELEASE_LABEL = 'ignore-for-release'
|
|
24
|
+
const RELEASE_LOCK_PREFIX = 'josh-release-lock-'
|
|
25
|
+
// A release waits on CI for minutes, so a second run refuses rather than queueing behind it.
|
|
26
|
+
const RELEASE_LOCK_MAX_WAIT_MS = 0
|
|
23
27
|
|
|
24
28
|
function branch_name_for(version: string): string {
|
|
25
29
|
return `release/v${version}`
|
|
@@ -87,6 +91,14 @@ async function refuse_existing_branch(branch_name: string): Promise<void> {
|
|
|
87
91
|
throw new Error(existing_branch_message(branch_name))
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
// **The leftovers go before the guard asks** (joshuafolkken/kit#3058). A run cut off before its
|
|
95
|
+
// `finally`, or a `worktree add` that failed after creating its branch, leaves a local
|
|
96
|
+
// `release/v<version>` that the guard would otherwise refuse as a release already opened.
|
|
97
|
+
async function clear_the_way(branch_name: string): Promise<void> {
|
|
98
|
+
await release_worktree.clear_leftover()
|
|
99
|
+
await refuse_existing_branch(branch_name)
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
// **No `checkout -b` and no branch guard here** (joshuafolkken/kit#2411). The work tree is already
|
|
91
103
|
// sitting on the release branch — `release_worktree.create` cut it that way — and the guard ran
|
|
92
104
|
// before that creation, because `worktree_add` makes the branch local and `branch_exists` would then
|
|
@@ -121,10 +133,10 @@ async function merge_and_tag(plan: ReleasePlan, branch_name: string): Promise<nu
|
|
|
121
133
|
// branch guard runs before the tree exists, `process.chdir` points the version write and the git
|
|
122
134
|
// commands at the tree, and the `finally` returns to the root and removes the tree — whichever way
|
|
123
135
|
// the wait ended, so a failed tag watch cleans up rather than leaving debris behind.
|
|
124
|
-
async function
|
|
136
|
+
async function publish_locked(plan: ReleasePlan): Promise<number> {
|
|
125
137
|
const branch_name = branch_name_for(plan.next_version)
|
|
126
138
|
|
|
127
|
-
await
|
|
139
|
+
await clear_the_way(branch_name)
|
|
128
140
|
|
|
129
141
|
const directory = await release_worktree.create(branch_name)
|
|
130
142
|
const previous_cwd = process.cwd()
|
|
@@ -140,6 +152,28 @@ async function publish(plan: ReleasePlan): Promise<number> {
|
|
|
140
152
|
}
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
function release_running_message(target: string): string {
|
|
156
|
+
return `Another \`pnpm josh release\` is running in this repository (lock \`${target}\`); wait for it to finish, then run it again.`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// **One release at a time per repository, held for the whole run** (joshuafolkken/kit#3058).
|
|
160
|
+
// `clear_leftover` removes an unpushed, commit-less release branch, which is also what a live run
|
|
161
|
+
// looks like while it installs dependencies — so it may only run once no other release is alive. The
|
|
162
|
+
// lock answers that: a run cut off before its `finally` leaves a record whose owner is gone, which
|
|
163
|
+
// `repository_lock` clears on the next claim, while a live owner makes this run refuse at once.
|
|
164
|
+
async function publish(plan: ReleasePlan): Promise<number> {
|
|
165
|
+
const target = repository_lock.lock_path(RELEASE_LOCK_PREFIX)
|
|
166
|
+
const exit_code = await repository_lock.with_lock(
|
|
167
|
+
async () => await publish_locked(plan),
|
|
168
|
+
target,
|
|
169
|
+
RELEASE_LOCK_MAX_WAIT_MS,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if (exit_code === undefined) throw new Error(release_running_message(target))
|
|
173
|
+
|
|
174
|
+
return exit_code
|
|
175
|
+
}
|
|
176
|
+
|
|
143
177
|
const release_publish = {
|
|
144
178
|
branch_name_for,
|
|
145
179
|
commit_message,
|