@joshuafolkken/kit 1.881.0 → 1.883.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.
@@ -307,7 +307,7 @@ jobs:
307
307
  ${{ runner.os }}-cspell-cache-
308
308
 
309
309
  - name: Setup safe-chain
310
- run: pnpm dlx @aikidosec/safe-chain@1.5.1 setup-ci
310
+ run: pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci
311
311
 
312
312
  - name: Install dependencies
313
313
  env:
@@ -406,7 +406,7 @@ jobs:
406
406
  ${{ runner.os }}-pnpm-store-
407
407
 
408
408
  - name: Setup safe-chain
409
- run: pnpm dlx @aikidosec/safe-chain@1.5.1 setup-ci
409
+ run: pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci
410
410
 
411
411
  - name: Install dependencies
412
412
  env:
@@ -615,7 +615,7 @@ jobs:
615
615
  ${{ runner.os }}-build-cache-e2e-
616
616
 
617
617
  - name: Setup safe-chain
618
- run: pnpm dlx @aikidosec/safe-chain@1.5.1 setup-ci
618
+ run: pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci
619
619
 
620
620
  - name: Install dependencies
621
621
  env:
@@ -962,9 +962,17 @@ function run_upgrade_command(command) {
962
962
  }
963
963
  function run_all_upgrade_commands(commands) {
964
964
  let exit_code = 0;
965
+ const failed_commands = [];
965
966
  for (const command of commands) {
966
967
  const code = run_upgrade_command(command);
967
- if (code !== 0) exit_code = code;
968
+ if (code === 0) continue;
969
+ exit_code = code;
970
+ failed_commands.push(command);
971
+ }
972
+ if (failed_commands.length > 0) {
973
+ console.error(
974
+ `Upgrade incomplete; successful steps remain applied. Failed: ${failed_commands.join(", ")}`
975
+ );
968
976
  }
969
977
  return exit_code;
970
978
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.881.0",
2
+ "version": "1.883.0",
3
3
  "name": "@joshuafolkken/kit",
4
4
  "type": "module",
5
5
  "license": "MIT",
@@ -166,7 +166,7 @@
166
166
  }
167
167
  },
168
168
  "scripts": {
169
- "preinstall": "pnpm dlx @aikidosec/safe-chain@1.5.1 setup-ci",
169
+ "preinstall": "pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci",
170
170
  "build": "tsx scripts/build/build-bin.ts && tsx scripts/build/build-hooks.ts && tsx scripts/build/build-version.ts && tsx scripts/build/build-config-merge.ts && tsx scripts/build/build-managed-marker.ts && tsx scripts/build/build-self-sync-guard.ts && tsx scripts/build/build-claude-md.ts",
171
171
  "josh": "tsx scripts/josh/josh.ts"
172
172
  }
@@ -18,12 +18,9 @@ trustLockfile: true
18
18
  # `@types/node` and `tsx` are also excluded so routine Dependabot-style bumps are not
19
19
  # delayed by the minimum-release-age window.
20
20
  #
21
- # This list governs **pnpm's** `minimum-release-age` only. The `preinstall` hook installs
22
- # @aikidosec/safe-chain, which applies its own age policy and has no knowledge of these entries — a
23
- # package listed here is still hidden by safe-chain until it ages in. Pinning a dependency floor to
24
- # a release that safe-chain still withholds publishes a range no consumer can resolve (kit #742), so
25
- # `josh ranges` checks every published range against that filtered view — from `josh latest` right
26
- # after the ranges are rewritten, and again from `prepack` as the last gate before a publish.
21
+ # `josh init` / `josh sync` mirror this list to Safe Chain's .aikido project config (kit #2618).
22
+ # Older Safe Chain shims ignore .aikido and can still hide an excluded release. `josh ranges`
23
+ # checks published dependency ranges through that filtered view before a release (kit #742).
27
24
  minimumReleaseAgeExclude:
28
25
  - vite
29
26
  - '@types/node'
@@ -6,6 +6,7 @@ import { resolve_local_bin } from '#scripts/build/local-bin'
6
6
  import { doctor_io } from '#scripts/doctor/doctor-io'
7
7
  import { package_version_schema, with_package_manager_schema } from '#scripts/lib/schemas'
8
8
  import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
9
+ import { project_config } from '#scripts/safe-chain/project-config'
9
10
  import { security_updates } from '#scripts/security/security-updates'
10
11
  import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
11
12
  import { sync } from '#scripts/sync/sync'
@@ -203,6 +204,7 @@ function main(): void {
203
204
  merge_project_package_json()
204
205
 
205
206
  run_ai_file_actions()
207
+ project_config.sync_project_config(PROJECT_ROOT)
206
208
  console.info('\n✅ Done.\n')
207
209
  }
208
210
 
@@ -0,0 +1,104 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { yaml_document } from '#scripts/yaml/yaml-document'
4
+ import { dump } from 'js-yaml'
5
+ import { z } from 'zod'
6
+
7
+ const WORKSPACE_FILE = 'pnpm-workspace.yaml'
8
+ const PROJECT_CONFIG_FILE = '.aikido'
9
+ const EXCLUSIONS_FIELD = 'minimumPackageAgeExclusions'
10
+ const workspace_schema = z.object({ minimumReleaseAgeExclude: z.array(z.string()).optional() })
11
+
12
+ function read_mapping(value: unknown, label: string): Record<string, unknown> {
13
+ if (value === undefined) return {}
14
+ if (yaml_document.is_mapping_document(value)) return value
15
+ throw new Error(`${label} must be a YAML mapping`)
16
+ }
17
+
18
+ function is_block_line(line: string): boolean {
19
+ return line.trim() === '' || line.startsWith(' ') || line.startsWith('\t')
20
+ }
21
+
22
+ function trim_block_end(lines: ReadonlyArray<string>, start: number, end: number): number {
23
+ let trimmed = end
24
+ while (trimmed > start + 1 && lines[trimmed - 1]?.trim() === '') trimmed -= 1
25
+
26
+ return trimmed
27
+ }
28
+
29
+ function find_block_end(lines: ReadonlyArray<string>, start: number): number {
30
+ const remaining = lines.slice(start + 1)
31
+ const relative = remaining.findIndex((line, index) => {
32
+ if (is_block_line(line)) return false
33
+ if (!line.startsWith('#')) return true
34
+ const next = remaining
35
+ .slice(index + 1)
36
+ .find((candidate) => candidate.trim() !== '' && !candidate.startsWith('#'))
37
+
38
+ return next === undefined || !is_block_line(next)
39
+ })
40
+ const end = relative === -1 ? lines.length : start + 1 + relative
41
+
42
+ return trim_block_end(lines, start, end)
43
+ }
44
+
45
+ function append_block(existing: string, block: string): string {
46
+ if (existing.length === 0) return block
47
+ const prefix = existing.endsWith('\n') ? existing : `${existing}\n`
48
+
49
+ return `${prefix}\n${block}`
50
+ }
51
+
52
+ function preserve_anchor(block: string, key_line: string): string {
53
+ const anchor = /(?:^|\s)(&[^\s#]+)/u.exec(key_line)?.[1]
54
+ if (!anchor) return block
55
+ const header = 'safe-chain:'
56
+
57
+ return `${header} ${anchor}${block.slice(header.length)}`
58
+ }
59
+
60
+ function replace_safe_chain_block(existing: string, safe_chain: Record<string, unknown>): string {
61
+ const block = dump({ 'safe-chain': safe_chain }, { lineWidth: -1 })
62
+ const lines = existing.split('\n')
63
+ const start = lines.findIndex((line) =>
64
+ /^(?:safe-chain|'safe-chain'|"safe-chain")\s*:/u.test(line),
65
+ )
66
+ if (start === -1) return append_block(existing, block)
67
+ const replacement = preserve_anchor(block, lines[start] ?? '')
68
+ const end = find_block_end(lines, start)
69
+ const prefix = lines.slice(0, start).join('\n')
70
+ const suffix = lines.slice(end).join('\n')
71
+
72
+ return `${prefix}${prefix ? '\n' : ''}${replacement}${suffix}`
73
+ }
74
+
75
+ function merge_project_config(existing: string, workspace: string): string {
76
+ const source = workspace_schema.parse(yaml_document.parse_yaml(workspace))
77
+ const exclusions = source.minimumReleaseAgeExclude ?? []
78
+ const aikido = yaml_document.parse_yaml(existing)
79
+ const safe_chain = read_mapping(aikido['safe-chain'], 'safe-chain')
80
+ const npm = read_mapping(safe_chain['npm'], 'safe-chain.npm')
81
+ if (JSON.stringify(npm[EXCLUSIONS_FIELD]) === JSON.stringify(exclusions)) return existing
82
+ if (exclusions.length === 0 && existing.length === 0) return existing
83
+
84
+ const merged = { ...safe_chain, npm: { ...npm, [EXCLUSIONS_FIELD]: exclusions } }
85
+
86
+ return replace_safe_chain_block(existing, merged)
87
+ }
88
+
89
+ function sync_project_config(root: string): boolean {
90
+ const workspace_path = path.join(root, WORKSPACE_FILE)
91
+ if (!existsSync(workspace_path)) return false
92
+ const config_path = path.join(root, PROJECT_CONFIG_FILE)
93
+ const existing = existsSync(config_path) ? readFileSync(config_path, 'utf8') : ''
94
+ const workspace = readFileSync(workspace_path, 'utf8')
95
+ const merged = merge_project_config(existing, workspace)
96
+ if (merged === existing) return false
97
+ writeFileSync(config_path, merged)
98
+
99
+ return true
100
+ }
101
+
102
+ const project_config = { merge_project_config, sync_project_config }
103
+
104
+ export { project_config }
@@ -8,6 +8,7 @@ import { init_logic } from '#scripts/init/init-logic'
8
8
  import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
9
9
  import { plugin_install_hint_module } from '#scripts/init/plugin-install-hint'
10
10
  import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
11
+ import { project_config } from '#scripts/safe-chain/project-config'
11
12
  import { security_updates } from '#scripts/security/security-updates'
12
13
  import { sonar_file } from '#scripts/security/sonar-file'
13
14
  import { did_refuse_self_run } from '#scripts/self-sync-guard/self-sync-refusal'
@@ -367,6 +368,7 @@ function report_repository_settings(name_with_owner: string | undefined): void {
367
368
 
368
369
  function sync_project_artifacts(is_force: boolean): void {
369
370
  sync_ai_copy_all(is_force)
371
+ project_config.sync_project_config(PROJECT_ROOT)
370
372
  sync_prettier_config(path.join(PROJECT_ROOT, 'prettier.config.js'))
371
373
  sync_playwright_config(path.join(PROJECT_ROOT, 'playwright.config.ts'))
372
374
  sync_deploy_vps(path.join(PROJECT_ROOT, '.github/workflows/deploy-vps.yml'))
@@ -9,9 +9,9 @@ import semver from 'semver'
9
9
  // The check asks the registry through that same filtered view instead of modelling the policy:
10
10
  // `pnpm view <name>@<range> version` prints no version and exits non-zero
11
11
  // (ERR_PNPM_PACKAGE_NOT_FOUND) exactly when nothing visible satisfies the range. Modelling would
12
- // mean guessing safe-chain's threshold, and `minimumReleaseAgeExclude` cannot stand in for it —
13
- // that list governs pnpm's own `minimum-release-age`, and safe-chain has no knowledge of it, which
14
- // is precisely what made the original failure surprising.
12
+ // mean guessing safe-chain's threshold. `josh init` / `josh sync` now mirror
13
+ // `minimumReleaseAgeExclude` into Safe Chain's .aikido project config (kit #2618), but older
14
+ // installed shims ignore it and published ranges must still be checked against their filtered view.
15
15
 
16
16
  interface PublishedRange {
17
17
  name: string
@@ -204,10 +204,19 @@ function run_upgrade_command(command: string): number {
204
204
  // succeed) so a failure on either target is surfaced without aborting the remaining upgrades.
205
205
  function run_all_upgrade_commands(commands: ReadonlyArray<string>): number {
206
206
  let exit_code = 0
207
+ const failed_commands: Array<string> = []
207
208
 
208
209
  for (const command of commands) {
209
210
  const code = run_upgrade_command(command)
210
- if (code !== 0) exit_code = code
211
+ if (code === 0) continue
212
+ exit_code = code
213
+ failed_commands.push(command)
214
+ }
215
+
216
+ if (failed_commands.length > 0) {
217
+ console.error(
218
+ `Upgrade incomplete; successful steps remain applied. Failed: ${failed_commands.join(', ')}`,
219
+ )
211
220
  }
212
221
 
213
222
  return exit_code
@@ -178,7 +178,7 @@ jobs:
178
178
  ${{ runner.os }}-cspell-cache-
179
179
 
180
180
  - name: Setup safe-chain
181
- run: pnpm dlx @aikidosec/safe-chain@1.5.1 setup-ci
181
+ run: pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci
182
182
 
183
183
  - name: Install dependencies
184
184
  env:
@@ -400,7 +400,7 @@ jobs:
400
400
  ${{ runner.os }}-build-cache-e2e-
401
401
 
402
402
  - name: Setup safe-chain
403
- run: pnpm dlx @aikidosec/safe-chain@1.5.1 setup-ci
403
+ run: pnpm dlx @aikidosec/safe-chain@1.5.20 setup-ci
404
404
 
405
405
  - name: Install dependencies
406
406
  env: