@joshuafolkken/kit 1.929.0 → 1.947.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.
Files changed (78) hide show
  1. package/.claude/skills/workflow-commands/background-commands.md +6 -0
  2. package/.claude/skills/workflow-commands/backlogrun-child.md +1 -1
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +4 -3
  4. package/.claude/skills/workflow-commands/observation-filing.md +3 -3
  5. package/.claude/skills/workflow-commands/prerequisite.md +1 -1
  6. package/.github/workflows/ci.yml +26 -8
  7. package/.github/workflows/pr-classification.yml +76 -0
  8. package/README.md +47 -22
  9. package/dist/hooks/batch-guard.js +4 -4
  10. package/dist/hooks/{chunk-GQ3EVWHY.js → chunk-3FWV6CFC.js} +113 -19
  11. package/dist/hooks/{chunk-2PGRQ62K.js → chunk-EYFJ74MJ.js} +11 -6
  12. package/dist/hooks/{chunk-YO43HEPY.js → chunk-GIX5SAAB.js} +237 -235
  13. package/dist/hooks/{chunk-LCJZE42I.js → chunk-IA7WC4NS.js} +1 -1
  14. package/dist/hooks/{chunk-BNFG2IFK.js → chunk-IBFC25IX.js} +4 -4
  15. package/dist/hooks/{chunk-I3JP3D4A.js → chunk-M4U4YNRE.js} +3 -3
  16. package/dist/hooks/{chunk-NJZSO7BK.js → chunk-NKVLBB65.js} +3 -3
  17. package/dist/hooks/{chunk-SXPTRFPG.js → chunk-O3PEDF3M.js} +1 -1
  18. package/dist/hooks/{chunk-7XQ6TK3R.js → chunk-OM5VDM6F.js} +2 -2
  19. package/dist/hooks/{chunk-ROULPSQC.js → chunk-V7RH56HU.js} +2 -2
  20. package/dist/hooks/codex-hook-adapter.js +10 -10
  21. package/dist/hooks/duplicate-read-guard.js +5 -5
  22. package/dist/hooks/format-edited.js +2 -2
  23. package/dist/hooks/investigation-guard.js +5 -5
  24. package/dist/hooks/pretool-guard.js +9 -9
  25. package/dist/hooks/stop-guard.js +9 -3
  26. package/dist/josh.js +11 -6
  27. package/package.json +13 -1
  28. package/prompts/collaboration-workflow/issue-template.md +1 -1
  29. package/scripts/ci/pr-classification.ts +27 -6
  30. package/scripts/document/document-byte-budget.ts +1 -1
  31. package/scripts/gate/hook-gate-reuse.ts +1 -1
  32. package/scripts/gate/project-checks.ts +45 -15
  33. package/scripts/gate/type-check-command.ts +43 -0
  34. package/scripts/git/constants.ts +4 -0
  35. package/scripts/git/git-command.ts +11 -5
  36. package/scripts/git/git-gh-pr-rollup.ts +56 -16
  37. package/scripts/git/git-staging.ts +6 -4
  38. package/scripts/git/git-stash.ts +73 -1
  39. package/scripts/git/stash-orphans.ts +24 -2
  40. package/scripts/git/stash-sweep-lock.ts +141 -0
  41. package/scripts/hooks/stop-guard.ts +2 -0
  42. package/scripts/init/init-actions.ts +3 -2
  43. package/scripts/init/init-logic-migrate.ts +10 -1
  44. package/scripts/init/init-logic-templates.ts +27 -7
  45. package/scripts/init/init-logic-workspace.ts +52 -1
  46. package/scripts/init/init-logic.ts +6 -1
  47. package/scripts/init/init.ts +60 -19
  48. package/scripts/issue/issue-merged.ts +75 -0
  49. package/scripts/issue/measure-rerun-cli.ts +2 -2
  50. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +10 -0
  51. package/scripts/josh/josh-command-map.ts +1 -0
  52. package/scripts/josh/josh-command-types.ts +0 -3
  53. package/scripts/josh/josh-commands-ai.ts +6 -0
  54. package/scripts/josh/josh-commands-development.ts +5 -5
  55. package/scripts/lint/lint-parallel.ts +19 -18
  56. package/scripts/observations/observation-ledger-home.ts +22 -2
  57. package/scripts/observations/observation-ledger-line.ts +11 -0
  58. package/scripts/observations/observation-ledger-migrate.ts +146 -0
  59. package/scripts/observations/observation-ledger.ts +51 -3
  60. package/scripts/observations/observations-flush.ts +38 -7
  61. package/scripts/review/review-finding-ledger.ts +1 -1
  62. package/scripts/review/review-record-cli.ts +1 -7
  63. package/scripts/rules/delivered-rules.ts +10 -0
  64. package/scripts/rules/lane-background.ts +111 -0
  65. package/scripts/rules/stop-rules.ts +15 -2
  66. package/scripts/run/run-carry-flush.ts +2 -7
  67. package/scripts/run/run-hold-cli.ts +10 -2
  68. package/scripts/run/run-tidy-cli.ts +50 -0
  69. package/scripts/run/run-tidy-lanes.ts +110 -0
  70. package/scripts/run/run-tidy-stashes.ts +160 -0
  71. package/scripts/run/run-tidy.ts +101 -0
  72. package/scripts/safe-chain/ci-installer-pin.ts +90 -0
  73. package/scripts/safe-chain/preinstall-command.ts +39 -0
  74. package/scripts/sync/sync.ts +5 -2
  75. package/scripts/test/pilot-files.ts +0 -1
  76. package/scripts/version/preinstall-version-update.ts +2 -0
  77. package/templates/workflows/ci.yml +21 -7
  78. package/tsconfig/base.json +4 -1
@@ -0,0 +1,160 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { git_stash, type StashEntry } from '#scripts/git/git-stash'
3
+ import { stash_orphans } from '#scripts/git/stash-orphans'
4
+ import { stash_sweep_lock } from '#scripts/git/stash-sweep-lock'
5
+ import { bounded_pool } from '#scripts/lib/bounded-pool'
6
+ import { observation_ledger } from '#scripts/observations/observation-ledger'
7
+ import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
8
+ import { run_tidy, type Outcome, type Verdict } from './run-tidy'
9
+ import type { IsMerged } from './run-tidy-lanes'
10
+
11
+ // The stash half of `josh run:tidy` (joshuafolkken/kit#2701): each entry whose named issues are all
12
+ // merged is dropped, and one that touches the observation ledger has its lines carried into the ledger
13
+ // first. The ledger is append-only and the digest counts repeats across it, so an entry's lines are
14
+ // never discarded with the entry.
15
+
16
+ // The same width `run-carry-stash.ts` reads issue states at.
17
+ const READ_CONCURRENCY = 8
18
+ const GONE_REASON = 'already dropped by another run'
19
+ const BUSY_REASON = 'another run held the stash sweep lock'
20
+
21
+ async function read_ledger(ledger_path: string): Promise<string> {
22
+ try {
23
+ return await readFile(ledger_path, 'utf8')
24
+ } catch {
25
+ return ''
26
+ }
27
+ }
28
+
29
+ // An entry cut before joshuafolkken/kit#2724 holds its lines at the ledger's old path, so the lines of
30
+ // every ledger path it touches are carried to the current ledger.
31
+ async function added_ledger_lines(
32
+ hash: string,
33
+ ledger_paths: ReadonlyArray<string>,
34
+ ): Promise<Array<string>> {
35
+ const added: Array<string> = []
36
+
37
+ for (const ledger_path of ledger_paths) {
38
+ const lines = await git_stash.added_lines(hash, ledger_path)
39
+
40
+ added.push(...lines)
41
+ }
42
+
43
+ return added
44
+ }
45
+
46
+ // How many ledger lines went into the primary checkout's ledger before the drop.
47
+ async function carry_ledger(hash: string): Promise<number> {
48
+ const changed = await git_stash.changed_paths(hash)
49
+ const ledger_paths = changed.filter((file_path) => observation_ledger.is_ledger_path(file_path))
50
+
51
+ if (ledger_paths.length === 0) return 0
52
+
53
+ const ledger_path = observation_ledger_home.ledger_path()
54
+ const added = await added_ledger_lines(hash, ledger_paths)
55
+ const carried = run_tidy.ledger_carry(added, await read_ledger(ledger_path))
56
+
57
+ await observation_ledger_home.append(ledger_path, carried)
58
+
59
+ return carried.length
60
+ }
61
+
62
+ function carried_note(count: number): string {
63
+ return count === 0 ? '' : ` (${String(count)} observation line(s) carried to the ledger)`
64
+ }
65
+
66
+ async function drop(entry: StashEntry): Promise<Outcome> {
67
+ const carried = await carry_ledger(entry.selector)
68
+ const target = `stash "${entry.subject}"${carried_note(carried)}`
69
+ const is_dropped = await git_stash.drop_by_hash(entry.selector)
70
+
71
+ return { target, verdict: is_dropped ? run_tidy.CLEAN : run_tidy.keep(GONE_REASON) }
72
+ }
73
+
74
+ async function merged_set(
75
+ issues: ReadonlyArray<string>,
76
+ is_merged: IsMerged,
77
+ ): Promise<Set<string>> {
78
+ const answers = await bounded_pool.bounded_map(
79
+ issues,
80
+ READ_CONCURRENCY,
81
+ async (issue) => await is_merged(issue),
82
+ )
83
+
84
+ return new Set(issues.filter((_issue, index) => answers[index] === true))
85
+ }
86
+
87
+ function entry_verdict(entry: StashEntry, merged: ReadonlySet<string>): Verdict | undefined {
88
+ const issues = stash_orphans.issues_named(entry.subject)
89
+
90
+ return run_tidy.stash_verdict({ subject: entry.subject, issues }, merged)
91
+ }
92
+
93
+ async function tidy_entry(
94
+ entry: StashEntry,
95
+ merged: ReadonlySet<string>,
96
+ ): Promise<Outcome | undefined> {
97
+ const verdict = entry_verdict(entry, merged)
98
+
99
+ if (verdict === undefined) return undefined
100
+
101
+ if (verdict.kind === 'keep') return { target: `stash "${entry.subject}"`, verdict }
102
+
103
+ try {
104
+ return await drop(entry)
105
+ } catch (error) {
106
+ return { target: `stash "${entry.subject}"`, verdict: run_tidy.keep(String(error)) }
107
+ }
108
+ }
109
+
110
+ async function tidy_entries(
111
+ entries: ReadonlyArray<StashEntry>,
112
+ merged: ReadonlySet<string>,
113
+ ): Promise<Array<Outcome>> {
114
+ const outcomes: Array<Outcome> = []
115
+
116
+ for (const entry of entries) {
117
+ const outcome = await tidy_entry(entry, merged)
118
+
119
+ if (outcome !== undefined) outcomes.push(outcome)
120
+ }
121
+
122
+ return outcomes
123
+ }
124
+
125
+ // The report a sweep that never got the lock gives: every entry it would have dropped is kept.
126
+ function busy_outcome(entry: StashEntry, merged: ReadonlySet<string>): Outcome | undefined {
127
+ const verdict = entry_verdict(entry, merged)
128
+
129
+ if (verdict === undefined) return undefined
130
+
131
+ return {
132
+ target: `stash "${entry.subject}"`,
133
+ verdict: verdict.kind === 'keep' ? verdict : run_tidy.keep(BUSY_REASON),
134
+ }
135
+ }
136
+
137
+ function busy_outcomes(
138
+ entries: ReadonlyArray<StashEntry>,
139
+ merged: ReadonlySet<string>,
140
+ ): Array<Outcome> {
141
+ return entries
142
+ .map((entry) => busy_outcome(entry, merged))
143
+ .filter((outcome): outcome is Outcome => outcome !== undefined)
144
+ }
145
+
146
+ // Sequential drops under the repository-wide sweep lock: each resolves its position from its hash just
147
+ // before it goes, which holds only while no other sweep is shifting the stack or appending the same
148
+ // ledger lines. The GitHub reads stay outside the lock — they are the slow part and mutate nothing.
149
+ async function tidy_stashes(is_merged: IsMerged): Promise<Array<Outcome>> {
150
+ const entries = await git_stash.list_by_hash()
151
+ const issues = [...new Set(entries.flatMap((entry) => stash_orphans.issues_named(entry.subject)))]
152
+ const merged = await merged_set(issues, is_merged)
153
+ const outcomes = await stash_sweep_lock.with_lock(async () => await tidy_entries(entries, merged))
154
+
155
+ return outcomes ?? busy_outcomes(entries, merged)
156
+ }
157
+
158
+ const run_tidy_stashes = { tidy_stashes }
159
+
160
+ export { run_tidy_stashes }
@@ -0,0 +1,101 @@
1
+ import { observation_ledger_line } from '#scripts/observations/observation-ledger-line'
2
+
3
+ // The decisions behind `josh run:tidy` (joshuafolkken/kit#2701), kept apart from the git and GitHub
4
+ // reads so each can be pinned without either. A run start sweeps what merged work left behind — the
5
+ // lanes holding a seat after their branch merged, and the stashes of issues that are all merged — and
6
+ // every rule here is written so that doubt keeps the thing: a lane with any uncommitted change or any
7
+ // commit no remote has, and a stash naming any issue that is not merged or naming none at all, stay.
8
+
9
+ const LINE_SEPARATOR = '\n'
10
+ const CLEANED_HEADER = 'run:tidy — cleaned:'
11
+ const KEPT_HEADER = 'run:tidy — kept:'
12
+ const CHANGES_REASON = 'uncommitted changes'
13
+ const UNPUSHED_REASON = 'commits no remote has'
14
+ const RUNNING_REASON = 'held by a live run'
15
+
16
+ interface LaneFacts {
17
+ issue: string
18
+ is_merged: boolean
19
+ has_changes: boolean
20
+ has_unpushed: boolean
21
+ is_running: boolean
22
+ }
23
+
24
+ interface StashFacts {
25
+ subject: string
26
+ issues: ReadonlyArray<string>
27
+ }
28
+
29
+ type Verdict = { kind: 'clean' } | { kind: 'keep'; reason: string }
30
+
31
+ interface Outcome {
32
+ target: string
33
+ verdict: Verdict
34
+ }
35
+
36
+ const CLEAN: Verdict = { kind: 'clean' }
37
+
38
+ function keep(reason: string): Verdict {
39
+ return { kind: 'keep', reason }
40
+ }
41
+
42
+ // A lane whose issue is still open is some run's, so it is left out of the report entirely rather than
43
+ // listed as kept — the report names what merged work left behind, not every lane.
44
+ function lane_verdict(facts: LaneFacts): Verdict | undefined {
45
+ if (!facts.is_merged) return undefined
46
+
47
+ if (facts.has_changes) return keep(CHANGES_REASON)
48
+
49
+ if (facts.has_unpushed) return keep(UNPUSHED_REASON)
50
+
51
+ return facts.is_running ? keep(RUNNING_REASON) : CLEAN
52
+ }
53
+
54
+ function unmerged_reason(unmerged: ReadonlyArray<string>): string {
55
+ const named = unmerged.map((issue) => `#${issue}`).join(', ')
56
+
57
+ return `${named} not merged`
58
+ }
59
+
60
+ // Like a lane, a stash naming no merged issue is left out of the report: one naming no issue at all,
61
+ // or only open ones, is not merged work's residue, and listing it at every run start would bury the
62
+ // entries that are.
63
+ function stash_verdict(facts: StashFacts, merged: ReadonlySet<string>): Verdict | undefined {
64
+ const unmerged = facts.issues.filter((issue) => !merged.has(issue))
65
+
66
+ if (unmerged.length === facts.issues.length) return undefined
67
+
68
+ return unmerged.length === 0 ? CLEAN : keep(unmerged_reason(unmerged))
69
+ }
70
+
71
+ // The ledger lines a stash would carry away with it, less those the ledger already holds — the same
72
+ // line appended twice would read as a recurrence to the observation digest, which counts repeats.
73
+ function ledger_carry(added: ReadonlyArray<string>, ledger: string): Array<string> {
74
+ const entries = added.filter((line) => observation_ledger_line.is_ledger_entry_line(line))
75
+
76
+ return observation_ledger_line.missing_lines([...new Set(entries)], ledger)
77
+ }
78
+
79
+ function format_outcome(outcome: Outcome): string {
80
+ const suffix = outcome.verdict.kind === 'keep' ? ` — ${outcome.verdict.reason}` : ''
81
+
82
+ return ` ${outcome.target}${suffix}`
83
+ }
84
+
85
+ function section(header: string, outcomes: ReadonlyArray<Outcome>): Array<string> {
86
+ return outcomes.length === 0 ? [] : [header, ...outcomes.map((item) => format_outcome(item))]
87
+ }
88
+
89
+ // `undefined` when the sweep found nothing merged, so an ordinary run start prints nothing.
90
+ function format_report(outcomes: ReadonlyArray<Outcome>): string | undefined {
91
+ const cleaned = outcomes.filter((outcome) => outcome.verdict.kind === 'clean')
92
+ const kept = outcomes.filter((outcome) => outcome.verdict.kind === 'keep')
93
+ const lines = [...section(CLEANED_HEADER, cleaned), ...section(KEPT_HEADER, kept)]
94
+
95
+ return lines.length === 0 ? undefined : lines.join(LINE_SEPARATOR)
96
+ }
97
+
98
+ const run_tidy = { CLEAN, format_report, keep, lane_verdict, ledger_carry, stash_verdict }
99
+
100
+ export type { LaneFacts, Outcome, StashFacts, Verdict }
101
+ export { run_tidy }
@@ -0,0 +1,90 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { execaSync } from 'execa'
4
+
5
+ // The workflows that install safe-chain on the runner. Both carry the release and the installer's
6
+ // SHA-256 as workflow-level env, so one pin per file moves every "Setup safe-chain" step at once
7
+ // (joshuafolkken/kit#2711). A consumer has only the first, and `josh sync` rewrites it from the
8
+ // template, so moving it anywhere but kit is harmless and short-lived.
9
+ const WORKFLOW_PATHS = ['.github/workflows/ci.yml', 'templates/workflows/ci.yml']
10
+ // `[ \t]` rather than `\s`: under the `m` flag `\s*` would run across line ends and swallow the
11
+ // blank line that follows the env block.
12
+ const VERSION_RE = /^(?<key>[ \t]*SAFE_CHAIN_INSTALLER_VERSION:[ \t]*)(?<value>\S+)[ \t]*$/mu
13
+ const SHA256_RE = /^(?<key>[ \t]*SAFE_CHAIN_INSTALLER_SHA256:[ \t]*)(?<value>\S+)[ \t]*$/mu
14
+ const VALUE_GROUP = 'value'
15
+ const DOWNLOAD_TIMEOUT_MS = 30_000
16
+
17
+ interface InstallerPin {
18
+ version: string
19
+ sha256: string
20
+ }
21
+
22
+ function installer_url(version: string): string {
23
+ return `https://github.com/AikidoSec/safe-chain/releases/download/${version}/install-safe-chain.sh`
24
+ }
25
+
26
+ function extract_pinned_version(content: string): string | undefined {
27
+ return VERSION_RE.exec(content)?.groups?.[VALUE_GROUP]
28
+ }
29
+
30
+ function rewrite_pin(content: string, pin: InstallerPin): string {
31
+ return content
32
+ .replace(VERSION_RE, (_match, key: string) => `${key}${pin.version}`)
33
+ .replace(SHA256_RE, (_match, key: string) => `${key}${pin.sha256}`)
34
+ }
35
+
36
+ function fetch_installer_sha256(version: string): string | undefined {
37
+ // `--proto =https` keeps the release redirect from being followed onto plain HTTP.
38
+ const result = execaSync('curl', ['--proto', '=https', '-fsSL', installer_url(version)], {
39
+ encoding: 'buffer',
40
+ // The hash is of the file as published; execa would otherwise drop its trailing newline.
41
+ stripFinalNewline: false,
42
+ reject: false,
43
+ timeout: DOWNLOAD_TIMEOUT_MS,
44
+ })
45
+ if (result.exitCode !== 0 || result.stdout.length === 0) return undefined
46
+
47
+ return createHash('sha256').update(result.stdout).digest('hex')
48
+ }
49
+
50
+ // The workflows whose pin lags `latest` — a missing file or one without the pin is not a target.
51
+ function stale_workflows(latest: string, workflow_paths: ReadonlyArray<string>): Array<string> {
52
+ return workflow_paths.filter((workflow_path) => {
53
+ if (!existsSync(workflow_path)) return false
54
+ const current = extract_pinned_version(readFileSync(workflow_path, 'utf8'))
55
+
56
+ return current !== undefined && current !== latest
57
+ })
58
+ }
59
+
60
+ function write_pin(workflow_path: string, pin: InstallerPin): void {
61
+ console.info(`\n↑ Updating safe-chain installer in ${workflow_path} → ${pin.version}`)
62
+ writeFileSync(workflow_path, rewrite_pin(readFileSync(workflow_path, 'utf8'), pin), 'utf8')
63
+ }
64
+
65
+ function sync(latest: string, workflow_paths: ReadonlyArray<string> = WORKFLOW_PATHS): void {
66
+ const targets = stale_workflows(latest, workflow_paths)
67
+ if (targets.length === 0) return
68
+
69
+ const sha256 = fetch_installer_sha256(latest)
70
+
71
+ if (sha256 === undefined) {
72
+ console.warn(`\n⚠ Could not download the safe-chain ${latest} installer; CI pin left as is`)
73
+
74
+ return
75
+ }
76
+
77
+ for (const workflow_path of targets) write_pin(workflow_path, { version: latest, sha256 })
78
+ }
79
+
80
+ const ci_installer_pin = {
81
+ WORKFLOW_PATHS,
82
+ installer_url,
83
+ extract_pinned_version,
84
+ rewrite_pin,
85
+ fetch_installer_sha256,
86
+ sync,
87
+ }
88
+
89
+ export { ci_installer_pin }
90
+ export type { InstallerPin }
@@ -0,0 +1,39 @@
1
+ // `setup-ci` only writes shims and adds them to PATH on a CI runner, so a local `pnpm install` is
2
+ // scanned only when the developer has enabled safe-chain's own shell integration. That integration
3
+ // runs pnpm behind safe-chain's registry proxy and hands `GLOBAL_AGENT_HTTP_PROXY` to it, which a bare
4
+ // pnpm never sets — the one signal a lifecycle script can read (joshuafolkken/kit#2707).
5
+ const SETUP_CI_CMD = 'pnpm dlx @aikidosec/safe-chain setup-ci'
6
+ const LEGACY_PREINSTALL_RE = /^pnpm dlx @aikidosec\/safe-chain(?:@\S+)? setup-ci$/u
7
+ const INTEGRATION_HINT =
8
+ 'Warning: safe-chain is not scanning this install because its shell integration is not active.' +
9
+ ' Install safe-chain (https://github.com/AikidoSec/safe-chain#installation) or run: safe-chain setup' +
10
+ ' - then restart your terminal.'
11
+ // `preinstall` runs before any dependency is installed, kit included, so the check is plain Node with
12
+ // no imports. It never fails the install and stays silent on CI, where the runner is set up instead.
13
+ // The source avoids double quotes, `$` and backticks so the shell passes it to Node verbatim.
14
+ const LOCAL_INTEGRATION_CHECK_SOURCE = `if(!process.env.CI&&!process.env.GLOBAL_AGENT_HTTP_PROXY)console.warn('${INTEGRATION_HINT}')`
15
+ const LOCAL_INTEGRATION_CHECK_CMD = `node -e "${LOCAL_INTEGRATION_CHECK_SOURCE}"`
16
+
17
+ function with_local_check(setup_command: string): string {
18
+ return `${setup_command} && ${LOCAL_INTEGRATION_CHECK_CMD}`
19
+ }
20
+
21
+ // Upgrade a `preinstall` kit wrote before the check existed, pinned version included; any other
22
+ // value is the consumer's own and is left exactly as it is.
23
+ function migrate_preinstall(value: string): string {
24
+ if (!LEGACY_PREINSTALL_RE.test(value)) return value
25
+
26
+ return with_local_check(value)
27
+ }
28
+
29
+ const SAFE_CHAIN_CMD = with_local_check(SETUP_CI_CMD)
30
+
31
+ const safe_chain_preinstall = {
32
+ SAFE_CHAIN_CMD,
33
+ LOCAL_INTEGRATION_CHECK_CMD,
34
+ LOCAL_INTEGRATION_CHECK_SOURCE,
35
+ INTEGRATION_HINT,
36
+ migrate_preinstall,
37
+ }
38
+
39
+ export { safe_chain_preinstall }
@@ -135,7 +135,10 @@ function did_migrate_prettierrc(destination_path: string): boolean {
135
135
 
136
136
  const existing = readFileSync(legacy_path, 'utf8')
137
137
 
138
- writeFileSync(destination_path, init_logic.merge_prettier_config(existing))
138
+ writeFileSync(
139
+ destination_path,
140
+ init_logic.merge_prettier_config(existing, path.dirname(destination_path)),
141
+ )
139
142
  rmSync(legacy_path)
140
143
 
141
144
  return true
@@ -143,7 +146,7 @@ function did_migrate_prettierrc(destination_path: string): boolean {
143
146
 
144
147
  function write_merged_prettier_config(destination_path: string): void {
145
148
  const existing = readFileSync(destination_path, 'utf8')
146
- const merged = init_logic.merge_prettier_config(existing)
149
+ const merged = init_logic.merge_prettier_config(existing, path.dirname(destination_path))
147
150
 
148
151
  if (merged === existing) {
149
152
  console.info(' ✔ unchanged prettier.config.js')
@@ -212,7 +212,6 @@ const PILOT_FILES: ReadonlyArray<string> = [
212
212
  'scripts/init/init-logic-migrate.test.ts',
213
213
  'scripts/init/init-logic-secretlint.test.ts',
214
214
  'scripts/init/init-logic-sonar.test.ts',
215
- 'scripts/init/init-logic-templates.test.ts',
216
215
  'scripts/init/init-logic-workspace.test.ts',
217
216
  'scripts/init/init-logic.cspell.test.ts',
218
217
  'scripts/init/init-logic.merge.test.ts',
@@ -1,4 +1,5 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs'
2
+ import { ci_installer_pin } from '#scripts/safe-chain/ci-installer-pin'
2
3
  import { execaSync } from 'execa'
3
4
 
4
5
  const SAFE_CHAIN_PKG = '@aikidosec/safe-chain'
@@ -72,6 +73,7 @@ function sync(package_json_path: string): void {
72
73
  }
73
74
 
74
75
  write_if_updated(package_json_path, info.content, info.current_version, latest)
76
+ ci_installer_pin.sync(latest)
75
77
  }
76
78
 
77
79
  const preinstall_version_update = {
@@ -4,9 +4,13 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
 
7
+ # No `labeled` / `unlabeled`: a label added right after the pull request opens started a second
8
+ # run on the same commit, the concurrency block below cancelled one of the two, and a cancelled
9
+ # run left in the newer check suite blocked the merge (joshuafolkken/kit#2712). The one check that
10
+ # reads labels runs in `pr-classification.yml` instead.
7
11
  pull_request:
8
12
  branches: [main]
9
- types: [opened, synchronize, reopened, labeled, unlabeled]
13
+ types: [opened, synchronize, reopened]
10
14
 
11
15
  # A push run is grouped by the commit it carries, so a later push to the default branch never
12
16
  # cancels an earlier one. Without the sha every such push shares one group — `github.ref` is
@@ -25,6 +29,12 @@ concurrency:
25
29
  # list (e.g. `chromium firefox webkit`) instead of editing this file, which josh sync overwrites.
26
30
  env:
27
31
  JOSH_PLAYWRIGHT_BROWSERS: ${{ vars.JOSH_PLAYWRIGHT_BROWSERS || 'chromium' }}
32
+ # The safe-chain release every "Setup safe-chain" step installs, and the SHA-256 of that release's
33
+ # install-safe-chain.sh. The installer puts the safe-chain binary under ~/.safe-chain/bin and
34
+ # adds it to PATH beside the shims; `pnpm dlx … setup-ci` left only the shims, which then let
35
+ # every install through without a scan (joshuafolkken/kit#2711). `josh latest` moves both together.
36
+ SAFE_CHAIN_INSTALLER_VERSION: 1.5.20
37
+ SAFE_CHAIN_INSTALLER_SHA256: 0ad25efe15d1fa56105157a454d647223e78eb0c53d1f85e3d10afcd722e7bfd
28
38
 
29
39
  jobs:
30
40
  playwright-image:
@@ -179,7 +189,11 @@ jobs:
179
189
  ${{ runner.os }}-cspell-cache-
180
190
 
181
191
  - name: Setup safe-chain
182
- run: pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci
192
+ run: |
193
+ installer="$RUNNER_TEMP/install-safe-chain.sh"
194
+ curl --proto '=https' -fsSL "https://github.com/AikidoSec/safe-chain/releases/download/$SAFE_CHAIN_INSTALLER_VERSION/install-safe-chain.sh" -o "$installer"
195
+ echo "$SAFE_CHAIN_INSTALLER_SHA256 $installer" | sha256sum -c -
196
+ sh "$installer" --ci
183
197
 
184
198
  - name: Install dependencies
185
199
  env:
@@ -221,10 +235,6 @@ jobs:
221
235
  - name: Run unit tests
222
236
  run: pnpm josh test:unit
223
237
 
224
- - name: Check release classification
225
- if: github.event_name == 'pull_request'
226
- run: pnpm josh pr:classification
227
-
228
238
  - name: Check bundle size
229
239
  run: pnpm size-limit
230
240
 
@@ -405,7 +415,11 @@ jobs:
405
415
  ${{ runner.os }}-build-cache-e2e-
406
416
 
407
417
  - name: Setup safe-chain
408
- run: pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci
418
+ run: |
419
+ installer="$RUNNER_TEMP/install-safe-chain.sh"
420
+ curl --proto '=https' -fsSL "https://github.com/AikidoSec/safe-chain/releases/download/$SAFE_CHAIN_INSTALLER_VERSION/install-safe-chain.sh" -o "$installer"
421
+ echo "$SAFE_CHAIN_INSTALLER_SHA256 $installer" | sha256sum -c -
422
+ sh "$installer" --ci
409
423
 
410
424
  - name: Install dependencies
411
425
  env:
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "strict": true,
4
- // tsx uses bundler-like resolution; "bundler" is the correct pairing for module:ESNext
4
+ // tsx uses bundler-like resolution; "bundler" is the correct pairing for module:ESNext.
5
+ // `module` is stated rather than inherited: TypeScript 5.x defaults it to a value "bundler"
6
+ // rejects (TS5095), so a consumer pinned below 6 failed its first check (#2725)
7
+ "module": "ESNext",
5
8
  "moduleResolution": "bundler",
6
9
  "esModuleInterop": true,
7
10
  "forceConsistentCasingInFileNames": true,