@joshuafolkken/kit 1.879.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.
Files changed (34) hide show
  1. package/.github/workflows/ci.yml +3 -3
  2. package/dist/hooks/batch-guard.js +4 -4
  3. package/dist/hooks/{chunk-KABG6AEW.js → chunk-264QY6KS.js} +7 -0
  4. package/dist/hooks/{chunk-B4YNCUGB.js → chunk-36UE53LS.js} +2 -2
  5. package/dist/hooks/{chunk-WCYPBPJC.js → chunk-4OXFU3MK.js} +8 -8
  6. package/dist/hooks/{chunk-BIWLABFK.js → chunk-6MQCXEEA.js} +1 -1
  7. package/dist/hooks/{chunk-GXKKK23T.js → chunk-FNFOLFFV.js} +1 -1
  8. package/dist/hooks/{chunk-B2LIXYFL.js → chunk-FRJKVSK3.js} +2 -2
  9. package/dist/hooks/{chunk-2RUPMGGV.js → chunk-PRDCNGA6.js} +4 -4
  10. package/dist/hooks/{chunk-2XIGZQMQ.js → chunk-VGYSA2NT.js} +2 -2
  11. package/dist/hooks/{chunk-QNNWYQ2R.js → chunk-Y5OYCU2F.js} +3 -3
  12. package/dist/hooks/{chunk-E55NKX5F.js → chunk-ZBK4XRAS.js} +3 -3
  13. package/dist/hooks/codex-hook-adapter.js +10 -10
  14. package/dist/hooks/duplicate-read-guard.js +5 -5
  15. package/dist/hooks/format-edited.js +2 -2
  16. package/dist/hooks/investigation-guard.js +5 -5
  17. package/dist/hooks/pretool-guard.js +9 -9
  18. package/dist/hooks/stop-guard.js +3 -3
  19. package/dist/josh.js +7 -0
  20. package/dist/version/index.js +9 -1
  21. package/package.json +2 -2
  22. package/pnpm-workspace.yaml +3 -6
  23. package/scripts/init/init.ts +2 -0
  24. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +10 -0
  25. package/scripts/josh/josh-command-map.ts +1 -0
  26. package/scripts/josh/josh-commands-project.ts +6 -0
  27. package/scripts/registry-migration/migrate-logic.ts +229 -0
  28. package/scripts/registry-migration/migrate.ts +280 -0
  29. package/scripts/registry-migration/user-npmrc.ts +15 -0
  30. package/scripts/safe-chain/project-config.ts +104 -0
  31. package/scripts/sync/sync.ts +2 -0
  32. package/scripts/version/publishable-range.ts +3 -3
  33. package/scripts/version/version-commands.ts +10 -1
  34. package/templates/workflows/ci.yml +2 -2
@@ -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:
@@ -9,10 +9,10 @@ import {
9
9
  load_environment_file,
10
10
  notice_refusal_path,
11
11
  refusal_path
12
- } from "./chunk-B2LIXYFL.js";
13
- import "./chunk-B4YNCUGB.js";
14
- import "./chunk-GXKKK23T.js";
15
- import "./chunk-KABG6AEW.js";
12
+ } from "./chunk-FRJKVSK3.js";
13
+ import "./chunk-36UE53LS.js";
14
+ import "./chunk-FNFOLFFV.js";
15
+ import "./chunk-264QY6KS.js";
16
16
  import "./chunk-FXAKJKTB.js";
17
17
  import "./chunk-SSX23SJ2.js";
18
18
  export {
@@ -1016,6 +1016,12 @@ var PROJECT_COMMANDS = {
1016
1016
  category: "Project",
1017
1017
  reference: ["", "developer", ["files"]]
1018
1018
  },
1019
+ "registry:migrate": {
1020
+ script: "scripts/registry-migration/migrate.ts",
1021
+ description: "Migrate a kit-only project from GitHub Packages to public npm",
1022
+ category: "Project",
1023
+ reference: ["", "developer", ["files", "network", "processes"]]
1024
+ },
1019
1025
  "sync:scope": {
1020
1026
  script: "scripts/sync/managed-config-scope-cli.ts",
1021
1027
  description: "Say whether this change touches a file josh sync distributes",
@@ -1237,6 +1243,7 @@ var ALIASES = {
1237
1243
  pt: "port",
1238
1244
  i: "init",
1239
1245
  sy: "sync",
1246
+ rmi: "registry:migrate",
1240
1247
  sys: "sync:scope",
1241
1248
  g: "git",
1242
1249
  gp: "pr",
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  lane_child_marker
3
- } from "./chunk-GXKKK23T.js";
3
+ } from "./chunk-FNFOLFFV.js";
4
4
  import {
5
5
  time_bundle_call,
6
6
  time_format,
7
7
  time_round_trips,
8
8
  time_spans,
9
9
  time_transcript_line
10
- } from "./chunk-KABG6AEW.js";
10
+ } from "./chunk-264QY6KS.js";
11
11
 
12
12
  // scripts/lane/lane-guard-policy.ts
13
13
  var DEFAULT_MODE = "refuse";
@@ -29,28 +29,28 @@ import {
29
29
  run_watcher_guard,
30
30
  shell_segments,
31
31
  tail_commands
32
- } from "./chunk-2XIGZQMQ.js";
32
+ } from "./chunk-VGYSA2NT.js";
33
33
  import {
34
34
  batch_outcome
35
- } from "./chunk-B2LIXYFL.js";
35
+ } from "./chunk-FRJKVSK3.js";
36
36
  import {
37
37
  investigation_refusal
38
- } from "./chunk-E55NKX5F.js";
38
+ } from "./chunk-ZBK4XRAS.js";
39
39
  import {
40
40
  duplicate_read_outcome
41
- } from "./chunk-2RUPMGGV.js";
41
+ } from "./chunk-PRDCNGA6.js";
42
42
  import {
43
43
  investigation_reads
44
- } from "./chunk-QNNWYQ2R.js";
44
+ } from "./chunk-Y5OYCU2F.js";
45
45
  import {
46
46
  lane_guard_policy,
47
47
  time_batch_guard
48
- } from "./chunk-B4YNCUGB.js";
48
+ } from "./chunk-36UE53LS.js";
49
49
  import {
50
50
  hook_decision,
51
51
  lane_child_marker,
52
52
  run_issue_number
53
- } from "./chunk-GXKKK23T.js";
53
+ } from "./chunk-FNFOLFFV.js";
54
54
  import {
55
55
  PLATFORM_TEMP_ROOT,
56
56
  cost_attribute,
@@ -61,7 +61,7 @@ import {
61
61
  lane_paths,
62
62
  stamp_file,
63
63
  time_shell
64
- } from "./chunk-KABG6AEW.js";
64
+ } from "./chunk-264QY6KS.js";
65
65
 
66
66
  // scripts/hooks/pretool-guard.ts
67
67
  import { text } from "node:stream/consumers";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ESLINT_EDIT_CACHE_FLAGS,
3
3
  time_density_hook
4
- } from "./chunk-KABG6AEW.js";
4
+ } from "./chunk-264QY6KS.js";
5
5
 
6
6
  // scripts/hooks/format-edited-file.ts
7
7
  import { existsSync as existsSync3 } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  stamp_file,
4
4
  time_density_hook,
5
5
  time_hook_transcript
6
- } from "./chunk-KABG6AEW.js";
6
+ } from "./chunk-264QY6KS.js";
7
7
  import {
8
8
  josh_environment_file
9
9
  } from "./chunk-FXAKJKTB.js";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  lane_guard_policy,
3
3
  time_batch_guard
4
- } from "./chunk-B4YNCUGB.js";
4
+ } from "./chunk-36UE53LS.js";
5
5
  import {
6
6
  hook_decision
7
- } from "./chunk-GXKKK23T.js";
7
+ } from "./chunk-FNFOLFFV.js";
8
8
 
9
9
  // scripts/hooks/batch-guard.ts
10
10
  import { text } from "node:stream/consumers";
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  investigation_reads
3
- } from "./chunk-QNNWYQ2R.js";
3
+ } from "./chunk-Y5OYCU2F.js";
4
4
  import {
5
5
  lane_guard_policy
6
- } from "./chunk-B4YNCUGB.js";
6
+ } from "./chunk-36UE53LS.js";
7
7
  import {
8
8
  hook_decision
9
- } from "./chunk-GXKKK23T.js";
9
+ } from "./chunk-FNFOLFFV.js";
10
10
  import {
11
11
  json_value,
12
12
  time_spans
13
- } from "./chunk-KABG6AEW.js";
13
+ } from "./chunk-264QY6KS.js";
14
14
 
15
15
  // scripts/delegation/duplicate-read-guard.ts
16
16
  import { statSync } from "node:fs";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  lane_child_marker,
3
3
  run_issue_number
4
- } from "./chunk-GXKKK23T.js";
4
+ } from "./chunk-FNFOLFFV.js";
5
5
  import {
6
6
  PLATFORM_TEMP_ROOT,
7
7
  cost_blocks,
@@ -11,7 +11,7 @@ import {
11
11
  time_density_hook,
12
12
  time_shell,
13
13
  time_transcript_line
14
- } from "./chunk-KABG6AEW.js";
14
+ } from "./chunk-264QY6KS.js";
15
15
  import {
16
16
  ENV_FILE_NAME,
17
17
  LANE_SEAT_KEY,
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  time_batch_guard
3
- } from "./chunk-B4YNCUGB.js";
3
+ } from "./chunk-36UE53LS.js";
4
4
  import {
5
5
  hook_decision
6
- } from "./chunk-GXKKK23T.js";
6
+ } from "./chunk-FNFOLFFV.js";
7
7
  import {
8
8
  cost_blocks,
9
9
  cost_transcript,
10
10
  time_bundle_call,
11
11
  time_shell,
12
12
  time_spans
13
- } from "./chunk-KABG6AEW.js";
13
+ } from "./chunk-264QY6KS.js";
14
14
 
15
15
  // scripts/delegation/investigation-reads.ts
16
16
  import path from "node:path";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  investigation_reads
3
- } from "./chunk-QNNWYQ2R.js";
3
+ } from "./chunk-Y5OYCU2F.js";
4
4
  import {
5
5
  lane_guard_policy
6
- } from "./chunk-B4YNCUGB.js";
6
+ } from "./chunk-36UE53LS.js";
7
7
  import {
8
8
  hook_decision
9
- } from "./chunk-GXKKK23T.js";
9
+ } from "./chunk-FNFOLFFV.js";
10
10
 
11
11
  // scripts/delegation/investigation-guard.ts
12
12
  import { text } from "node:stream/consumers";
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env tsx
2
2
  import {
3
3
  pretool_guard
4
- } from "./chunk-WCYPBPJC.js";
5
- import "./chunk-2XIGZQMQ.js";
4
+ } from "./chunk-4OXFU3MK.js";
5
+ import "./chunk-VGYSA2NT.js";
6
6
  import {
7
7
  build_envelope,
8
8
  compose_context,
9
9
  format_edited_payload
10
- } from "./chunk-BIWLABFK.js";
11
- import "./chunk-B2LIXYFL.js";
12
- import "./chunk-E55NKX5F.js";
13
- import "./chunk-2RUPMGGV.js";
14
- import "./chunk-QNNWYQ2R.js";
15
- import "./chunk-B4YNCUGB.js";
10
+ } from "./chunk-6MQCXEEA.js";
11
+ import "./chunk-FRJKVSK3.js";
12
+ import "./chunk-ZBK4XRAS.js";
13
+ import "./chunk-PRDCNGA6.js";
14
+ import "./chunk-Y5OYCU2F.js";
15
+ import "./chunk-36UE53LS.js";
16
16
  import {
17
17
  hook_decision
18
- } from "./chunk-GXKKK23T.js";
18
+ } from "./chunk-FNFOLFFV.js";
19
19
  import {
20
20
  time_density_hook
21
- } from "./chunk-KABG6AEW.js";
21
+ } from "./chunk-264QY6KS.js";
22
22
  import "./chunk-FXAKJKTB.js";
23
23
  import "./chunk-SSX23SJ2.js";
24
24
 
@@ -9,11 +9,11 @@ import {
9
9
  is_enabled,
10
10
  notice_refusal_path,
11
11
  refusal_path
12
- } from "./chunk-2RUPMGGV.js";
13
- import "./chunk-QNNWYQ2R.js";
14
- import "./chunk-B4YNCUGB.js";
15
- import "./chunk-GXKKK23T.js";
16
- import "./chunk-KABG6AEW.js";
12
+ } from "./chunk-PRDCNGA6.js";
13
+ import "./chunk-Y5OYCU2F.js";
14
+ import "./chunk-36UE53LS.js";
15
+ import "./chunk-FNFOLFFV.js";
16
+ import "./chunk-264QY6KS.js";
17
17
  import "./chunk-FXAKJKTB.js";
18
18
  import "./chunk-SSX23SJ2.js";
19
19
  export {
@@ -15,8 +15,8 @@ import {
15
15
  relative_to_root,
16
16
  resolve_invocations,
17
17
  select_invocation
18
- } from "./chunk-BIWLABFK.js";
19
- import "./chunk-KABG6AEW.js";
18
+ } from "./chunk-6MQCXEEA.js";
19
+ import "./chunk-264QY6KS.js";
20
20
  import "./chunk-SSX23SJ2.js";
21
21
  export {
22
22
  ESLINT_DAEMON,
@@ -10,11 +10,11 @@ import {
10
10
  is_enabled,
11
11
  load_environment_file,
12
12
  refusal_path
13
- } from "./chunk-E55NKX5F.js";
14
- import "./chunk-QNNWYQ2R.js";
15
- import "./chunk-B4YNCUGB.js";
16
- import "./chunk-GXKKK23T.js";
17
- import "./chunk-KABG6AEW.js";
13
+ } from "./chunk-ZBK4XRAS.js";
14
+ import "./chunk-Y5OYCU2F.js";
15
+ import "./chunk-36UE53LS.js";
16
+ import "./chunk-FNFOLFFV.js";
17
+ import "./chunk-264QY6KS.js";
18
18
  import "./chunk-FXAKJKTB.js";
19
19
  import "./chunk-SSX23SJ2.js";
20
20
  export {
@@ -3,15 +3,15 @@ import {
3
3
  pretool_guard,
4
4
  pretool_outcome,
5
5
  pretool_outcome_async
6
- } from "./chunk-WCYPBPJC.js";
7
- import "./chunk-2XIGZQMQ.js";
8
- import "./chunk-B2LIXYFL.js";
9
- import "./chunk-E55NKX5F.js";
10
- import "./chunk-2RUPMGGV.js";
11
- import "./chunk-QNNWYQ2R.js";
12
- import "./chunk-B4YNCUGB.js";
13
- import "./chunk-GXKKK23T.js";
14
- import "./chunk-KABG6AEW.js";
6
+ } from "./chunk-4OXFU3MK.js";
7
+ import "./chunk-VGYSA2NT.js";
8
+ import "./chunk-FRJKVSK3.js";
9
+ import "./chunk-ZBK4XRAS.js";
10
+ import "./chunk-PRDCNGA6.js";
11
+ import "./chunk-Y5OYCU2F.js";
12
+ import "./chunk-36UE53LS.js";
13
+ import "./chunk-FNFOLFFV.js";
14
+ import "./chunk-264QY6KS.js";
15
15
  import "./chunk-FXAKJKTB.js";
16
16
  import "./chunk-SSX23SJ2.js";
17
17
  export {
@@ -15,20 +15,20 @@ import {
15
15
  run_hold,
16
16
  run_invocation,
17
17
  run_watcher_guard
18
- } from "./chunk-2XIGZQMQ.js";
18
+ } from "./chunk-VGYSA2NT.js";
19
19
  import {
20
20
  session_language
21
21
  } from "./chunk-6V5OPIUU.js";
22
22
  import {
23
23
  hook_decision,
24
24
  lane_child_marker
25
- } from "./chunk-GXKKK23T.js";
25
+ } from "./chunk-FNFOLFFV.js";
26
26
  import {
27
27
  PROJECT_ROOT,
28
28
  stamp_file,
29
29
  time_density_hook,
30
30
  time_hook_transcript
31
- } from "./chunk-KABG6AEW.js";
31
+ } from "./chunk-264QY6KS.js";
32
32
  import "./chunk-FXAKJKTB.js";
33
33
  import "./chunk-SSX23SJ2.js";
34
34
 
package/dist/josh.js CHANGED
@@ -1173,6 +1173,12 @@ var PROJECT_COMMANDS = {
1173
1173
  category: "Project",
1174
1174
  reference: ["", "developer", ["files"]]
1175
1175
  },
1176
+ "registry:migrate": {
1177
+ script: "scripts/registry-migration/migrate.ts",
1178
+ description: "Migrate a kit-only project from GitHub Packages to public npm",
1179
+ category: "Project",
1180
+ reference: ["", "developer", ["files", "network", "processes"]]
1181
+ },
1176
1182
  "sync:scope": {
1177
1183
  script: "scripts/sync/managed-config-scope-cli.ts",
1178
1184
  description: "Say whether this change touches a file josh sync distributes",
@@ -1403,6 +1409,7 @@ var ALIASES = {
1403
1409
  pt: "port",
1404
1410
  i: "init",
1405
1411
  sy: "sync",
1412
+ rmi: "registry:migrate",
1406
1413
  sys: "sync:scope",
1407
1414
  g: "git",
1408
1415
  gp: "pr",
@@ -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.879.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
 
@@ -102,6 +102,7 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
102
102
  "rh": "run:hold",
103
103
  "rl": "report:lint",
104
104
  "rmg": "run:merge",
105
+ "rmi": "registry:migrate",
105
106
  "rn": "run:next",
106
107
  "rp": "run:prep",
107
108
  "rpy": "repo:party",
@@ -787,6 +788,15 @@ exports[`COMMAND_MAP — reference metadata > keeps every canonical command and
787
788
  "processes",
788
789
  ],
789
790
  ],
791
+ "registry:migrate": [
792
+ "",
793
+ "developer",
794
+ [
795
+ "files",
796
+ "network",
797
+ "processes",
798
+ ],
799
+ ],
790
800
  "release": [
791
801
  "[version]",
792
802
  "maintainer",
@@ -54,6 +54,7 @@ const ALIASES: Record<string, string> = {
54
54
  pt: 'port',
55
55
  i: 'init',
56
56
  sy: 'sync',
57
+ rmi: 'registry:migrate',
57
58
  sys: 'sync:scope',
58
59
  g: 'git',
59
60
  gp: 'pr',
@@ -17,6 +17,12 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
17
17
  category: 'Project',
18
18
  reference: ['', 'developer', ['files']],
19
19
  },
20
+ 'registry:migrate': {
21
+ script: 'scripts/registry-migration/migrate.ts',
22
+ description: 'Migrate a kit-only project from GitHub Packages to public npm',
23
+ category: 'Project',
24
+ reference: ['', 'developer', ['files', 'network', 'processes']],
25
+ },
20
26
  'sync:scope': {
21
27
  script: 'scripts/sync/managed-config-scope-cli.ts',
22
28
  description: 'Say whether this change touches a file josh sync distributes',
@@ -0,0 +1,229 @@
1
+ import { fix_gh_packages_logic } from '#scripts/gh/fix-gh-packages-logic'
2
+
3
+ const SCOPE = '@joshuafolkken'
4
+ const REGISTRY_KEY = `${SCOPE}:registry`
5
+ const PUBLIC_REGISTRY = 'https://registry.npmjs.org/'
6
+ const GITHUB_REGISTRY = 'https://npm.pkg.github.com/'
7
+ const MAPPING = `${REGISTRY_KEY}=${PUBLIC_REGISTRY}`
8
+ const REGISTRY_VALUE_INDEX = 1
9
+ const SPLIT_LIMIT = 2
10
+
11
+ interface MigrationPlan {
12
+ content: string
13
+ packages: ReadonlyArray<string>
14
+ blocked: ReadonlyArray<string>
15
+ change: string
16
+ }
17
+
18
+ type IntegrityPair = readonly [string, string]
19
+
20
+ function normalized_registry(value: string): string {
21
+ let normalized = value.trim()
22
+ while (normalized.endsWith('/')) normalized = normalized.slice(0, -1)
23
+
24
+ return `${normalized}/`
25
+ }
26
+
27
+ function mapping_lines(content: string): ReadonlyArray<string> {
28
+ return content.split(/\r?\n/u).filter((line) => line.trimStart().startsWith(`${REGISTRY_KEY}=`))
29
+ }
30
+
31
+ function registry_of(content: string): string | undefined {
32
+ const lines = mapping_lines(content)
33
+ if (lines.length !== 1) return undefined
34
+
35
+ return normalized_registry(lines[0]?.split('=', SPLIT_LIMIT)[REGISTRY_VALUE_INDEX] ?? '')
36
+ }
37
+
38
+ function scoped_packages(lockfile: string): ReadonlyArray<string> {
39
+ const packages = fix_gh_packages_logic.parse_lockfile_packages(lockfile)
40
+
41
+ return Object.keys(packages)
42
+ .filter((key) => key.startsWith(`${SCOPE}/`))
43
+ .map((key) => fix_gh_packages_logic.package_path_from_key(key))
44
+ .filter((name, index, names) => names.indexOf(name) === index)
45
+ }
46
+
47
+ function scoped_versions(lockfile: string): ReadonlyArray<readonly [string, string]> {
48
+ const packages = fix_gh_packages_logic.parse_lockfile_packages(lockfile)
49
+
50
+ return Object.keys(packages)
51
+ .filter((key) => key.startsWith(`${SCOPE}/`))
52
+ .map(
53
+ (key) =>
54
+ [
55
+ fix_gh_packages_logic.package_path_from_key(key),
56
+ fix_gh_packages_logic.package_version_from_key(key),
57
+ ] as const,
58
+ )
59
+ }
60
+
61
+ function is_github_tarball(value: string): boolean {
62
+ try {
63
+ const url = new URL(value)
64
+
65
+ return url.protocol === 'https:' && url.hostname === 'npm.pkg.github.com'
66
+ } catch {
67
+ return false
68
+ }
69
+ }
70
+
71
+ function github_tarballs(lockfile: string): ReadonlyArray<string> {
72
+ const packages = fix_gh_packages_logic.parse_lockfile_packages(lockfile)
73
+
74
+ return Object.entries(packages)
75
+ .filter(
76
+ ([key, entry]) =>
77
+ key.startsWith(`${SCOPE}/`) &&
78
+ entry.resolution?.tarball !== undefined &&
79
+ is_github_tarball(entry.resolution.tarball),
80
+ )
81
+ .map(([key]) => key)
82
+ }
83
+
84
+ function remove_flow_tarball(line: string): string {
85
+ const marker = ', tarball: https://npm.pkg.github.com/'
86
+ const start = line.indexOf(marker)
87
+ if (start === -1) return line
88
+ const end = line.indexOf('}', start)
89
+ if (end === -1) return line
90
+
91
+ return `${line.slice(0, start)}${line.slice(end)}`
92
+ }
93
+
94
+ function is_package_entry(line: string): boolean {
95
+ return /^ {2}[^ ]/u.test(line)
96
+ }
97
+
98
+ function is_github_tarball_line(line: string): boolean {
99
+ return line.trimStart().startsWith('tarball: https://npm.pkg.github.com/')
100
+ }
101
+
102
+ function clean_kit_line(
103
+ line: string,
104
+ version: string,
105
+ integrities: ReadonlyMap<string, IntegrityPair>,
106
+ ): string | undefined {
107
+ if (!version) return line
108
+ if (is_github_tarball_line(line)) return undefined
109
+ const cleaned = remove_flow_tarball(line)
110
+ const pair = integrities.get(version)
111
+ if (pair === undefined) return cleaned
112
+
113
+ return cleaned.split(`integrity: ${pair[0]}`).join(`integrity: ${pair[1]}`)
114
+ }
115
+
116
+ function next_kit_version(line: string, version: string): string {
117
+ if (!is_package_entry(line)) return version
118
+ const prefix = " '@joshuafolkken/kit@"
119
+
120
+ return line.startsWith(prefix) ? (line.slice(prefix.length).split("'", 1)[0] ?? '') : ''
121
+ }
122
+
123
+ function integrity_pair(
124
+ old_integrity: string | undefined,
125
+ npm_integrity: string | undefined,
126
+ ): IntegrityPair | undefined {
127
+ if (old_integrity === undefined || npm_integrity === undefined) return undefined
128
+
129
+ return [old_integrity, npm_integrity]
130
+ }
131
+
132
+ function lock_integrities(
133
+ lockfile: string,
134
+ npm_integrities: ReadonlyMap<string, string>,
135
+ ): Map<string, IntegrityPair> {
136
+ const packages = fix_gh_packages_logic.parse_lockfile_packages(lockfile)
137
+ const pairs = new Map<string, IntegrityPair>()
138
+ const kit_entries = Object.entries(packages).filter(([key]) => key.startsWith(`${SCOPE}/kit@`))
139
+
140
+ for (const [key, entry] of kit_entries) {
141
+ const version = fix_gh_packages_logic.package_version_from_key(key)
142
+ const pair = integrity_pair(entry.resolution?.integrity, npm_integrities.get(version))
143
+ if (pair !== undefined) pairs.set(version, pair)
144
+ }
145
+
146
+ return pairs
147
+ }
148
+
149
+ function push_if_defined(lines: Array<string>, cleaned: string | undefined): void {
150
+ if (cleaned !== undefined) lines.push(cleaned)
151
+ }
152
+
153
+ function rewrite_kit_lockfile(
154
+ lockfile: string,
155
+ npm_integrities: ReadonlyMap<string, string>,
156
+ ): string {
157
+ let kit_version = ''
158
+ const lines: Array<string> = []
159
+ const integrities = lock_integrities(lockfile, npm_integrities)
160
+
161
+ for (const line of lockfile.split('\n')) {
162
+ kit_version = next_kit_version(line, kit_version)
163
+ const cleaned = clean_kit_line(line, kit_version, integrities)
164
+
165
+ push_if_defined(lines, cleaned)
166
+ }
167
+
168
+ return lines.join('\n')
169
+ }
170
+
171
+ function append_mapping(content: string, eol: string): string {
172
+ const separator = content && !content.endsWith('\n') ? eol : ''
173
+
174
+ return `${content}${separator}${MAPPING}${eol}`
175
+ }
176
+
177
+ function replace_mapping(content: string): string {
178
+ const lines = mapping_lines(content)
179
+ const eol = content.includes('\r\n') ? '\r\n' : '\n'
180
+
181
+ if (lines.length === 0) return append_mapping(content, eol)
182
+
183
+ return content
184
+ .split(/\r?\n/u)
185
+ .map((line) => (line === lines[0] ? MAPPING : line))
186
+ .join(eol)
187
+ }
188
+
189
+ function unsupported_registry(project_npmrc: string): string | undefined {
190
+ const registry = registry_of(project_npmrc)
191
+
192
+ if (registry === undefined || [GITHUB_REGISTRY, PUBLIC_REGISTRY].includes(registry)) {
193
+ return undefined
194
+ }
195
+
196
+ return `unsupported project registry: ${registry}`
197
+ }
198
+
199
+ function has_duplicate_mapping(project_npmrc: string, user_npmrc: string): boolean {
200
+ return mapping_lines(project_npmrc).length > 1 || mapping_lines(user_npmrc).length > 1
201
+ }
202
+
203
+ function previous_registry(project_npmrc: string, user_npmrc: string): string {
204
+ return registry_of(project_npmrc) ?? registry_of(user_npmrc) ?? 'default npm'
205
+ }
206
+
207
+ function plan(project_npmrc: string, user_npmrc: string, lockfile: string): MigrationPlan {
208
+ const packages = scoped_packages(lockfile)
209
+ const blocked = packages.filter((name) => name !== `${SCOPE}/kit`)
210
+ if (has_duplicate_mapping(project_npmrc, user_npmrc)) blocked.push('duplicate registry mappings')
211
+ const unsupported = unsupported_registry(project_npmrc)
212
+ if (unsupported !== undefined) blocked.push(unsupported)
213
+ const before = previous_registry(project_npmrc, user_npmrc)
214
+ const change = `${before} → ${PUBLIC_REGISTRY}`
215
+
216
+ return { content: replace_mapping(project_npmrc), packages, blocked, change }
217
+ }
218
+
219
+ const migrate_logic = {
220
+ plan,
221
+ github_tarballs,
222
+ rewrite_kit_lockfile,
223
+ registry_of,
224
+ scoped_packages,
225
+ scoped_versions,
226
+ }
227
+
228
+ export { migrate_logic }
229
+ export type { MigrationPlan }
@@ -0,0 +1,280 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { execa } from 'execa'
5
+ import { z } from 'zod'
6
+ import { migrate_logic, type MigrationPlan } from './migrate-logic'
7
+ import { user_npmrc } from './user-npmrc'
8
+
9
+ const NPMRC = '.npmrc'
10
+ const LOCKFILE = 'pnpm-lock.yaml'
11
+ const WORKSPACE = 'pnpm-workspace.yaml'
12
+ const PUBLIC_REGISTRY = 'https://registry.npmjs.org/'
13
+ const FETCH_TIMEOUT_MS = 10_000
14
+ const SCOPE = '@joshuafolkken/'
15
+ const manifest_schema = z.looseObject({
16
+ dependencies: z.record(z.string(), z.string()).optional(),
17
+ devDependencies: z.record(z.string(), z.string()).optional(),
18
+ optionalDependencies: z.record(z.string(), z.string()).optional(),
19
+ peerDependencies: z.record(z.string(), z.string()).optional(),
20
+ })
21
+ const version_schema = z.looseObject({
22
+ dist: z.looseObject({ integrity: z.string(), tarball: z.string() }),
23
+ })
24
+
25
+ function is_public_npm_url(value: string): boolean {
26
+ try {
27
+ const url = new URL(value)
28
+
29
+ return url.origin === 'https://registry.npmjs.org' && url.username === '' && url.password === ''
30
+ } catch {
31
+ return false
32
+ }
33
+ }
34
+
35
+ interface VersionCheck {
36
+ missing: ReadonlyArray<string>
37
+ integrities: ReadonlyMap<string, string>
38
+ }
39
+
40
+ async function probe_integrity(
41
+ name: string,
42
+ version: string,
43
+ probe_version: MigrationDependencies['fetch_version'],
44
+ ): Promise<string | undefined> {
45
+ if (!version) return undefined
46
+
47
+ return await probe_version(name, version)
48
+ }
49
+
50
+ interface MigrationDependencies {
51
+ fetch_version: (name: string, version: string) => Promise<string | undefined>
52
+ install: (cwd: string) => Promise<void>
53
+ effective_registry: (cwd: string) => Promise<string>
54
+ user_npmrc: string
55
+ environment: NodeJS.ProcessEnv
56
+ }
57
+
58
+ interface MigrationContext {
59
+ cwd: string
60
+ npmrc_path: string
61
+ lockfile_path: string
62
+ workspace_path: string
63
+ original_npmrc: string
64
+ original_lockfile: string
65
+ original_workspace: string
66
+ has_npmrc: boolean
67
+ has_lockfile: boolean
68
+ has_workspace: boolean
69
+ plan: MigrationPlan
70
+ }
71
+
72
+ function read_file(file_path: string): string {
73
+ return existsSync(file_path) ? readFileSync(file_path, 'utf8') : ''
74
+ }
75
+
76
+ function has_registry_override(environment: NodeJS.ProcessEnv): boolean {
77
+ return Object.keys(environment).some((key) =>
78
+ /^(?:npm|pnpm)_config_.*(?:joshuafolkken.*registry|userconfig)/iu.test(key),
79
+ )
80
+ }
81
+
82
+ function manifest_packages(content: string): ReadonlyArray<string> {
83
+ const manifest = manifest_schema.parse(JSON.parse(content))
84
+ const sections = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
85
+
86
+ return sections.flatMap((section) => {
87
+ const value: unknown = Reflect.get(manifest, section)
88
+ if (typeof value !== 'object' || value === null) return []
89
+
90
+ return Object.keys(value).filter((name) => name.startsWith(SCOPE))
91
+ })
92
+ }
93
+
94
+ function restore(context: MigrationContext): void {
95
+ if (context.has_npmrc) writeFileSync(context.npmrc_path, context.original_npmrc)
96
+ else rmSync(context.npmrc_path, { force: true })
97
+ writeFileSync(context.lockfile_path, context.original_lockfile)
98
+ if (context.has_workspace) writeFileSync(context.workspace_path, context.original_workspace)
99
+ else rmSync(context.workspace_path, { force: true })
100
+ }
101
+
102
+ async function verify_public_versions(
103
+ lockfile: string,
104
+ probe_version: MigrationDependencies['fetch_version'],
105
+ ): Promise<VersionCheck> {
106
+ const missing: Array<string> = []
107
+ const integrities = new Map<string, string>()
108
+
109
+ for (const [name, version] of migrate_logic.scoped_versions(lockfile)) {
110
+ const integrity = await probe_integrity(name, version, probe_version)
111
+ if (integrity === undefined) missing.push(`${name}@${version}`)
112
+ else integrities.set(version, integrity)
113
+ }
114
+
115
+ return { missing, integrities }
116
+ }
117
+
118
+ function report_context(context: MigrationContext): void {
119
+ console.info(`Current registry and planned change: ${context.plan.change}`)
120
+ console.info(`Installed scoped packages: ${context.plan.packages.join(', ') || '(none)'}`)
121
+ }
122
+
123
+ function create_context(cwd: string, dependencies: MigrationDependencies): MigrationContext {
124
+ const npmrc_path = path.join(cwd, NPMRC)
125
+ const lockfile_path = path.join(cwd, LOCKFILE)
126
+ const workspace_path = path.join(cwd, WORKSPACE)
127
+ const original_npmrc = read_file(npmrc_path)
128
+ const original_lockfile = read_file(lockfile_path)
129
+ const original_workspace = read_file(workspace_path)
130
+ const plan = migrate_logic.plan(original_npmrc, dependencies.user_npmrc, original_lockfile)
131
+
132
+ return {
133
+ cwd,
134
+ npmrc_path,
135
+ lockfile_path,
136
+ workspace_path,
137
+ original_npmrc,
138
+ original_lockfile,
139
+ original_workspace,
140
+ plan,
141
+ has_npmrc: existsSync(npmrc_path),
142
+ has_lockfile: existsSync(lockfile_path),
143
+ has_workspace: existsSync(workspace_path),
144
+ }
145
+ }
146
+
147
+ function initial_block(
148
+ context: MigrationContext,
149
+ dependencies: MigrationDependencies,
150
+ ): string | undefined {
151
+ if (!context.has_lockfile) return 'No lockfile; run pnpm install before migrating.'
152
+ const manifest = manifest_packages(read_file(path.join(context.cwd, 'package.json')))
153
+ const missing_lock = manifest.filter((name) => !context.plan.packages.includes(name))
154
+ const blocked = [...context.plan.blocked, ...missing_lock]
155
+ if (!context.plan.packages.includes(`${SCOPE}kit`)) blocked.push('kit is absent from lockfile')
156
+ if (has_registry_override(dependencies.environment)) blocked.push('registry environment override')
157
+ if (blocked.length === 0) return undefined
158
+
159
+ return `Migration blocked: ${blocked.join(', ')}. Keep GitHub Packages until every package is available on npm.`
160
+ }
161
+
162
+ async function verify_resolution(
163
+ context: MigrationContext,
164
+ dependencies: MigrationDependencies,
165
+ integrities: ReadonlyMap<string, string>,
166
+ ): Promise<void> {
167
+ await dependencies.install(context.cwd)
168
+
169
+ if (read_file(context.workspace_path) !== context.original_workspace) {
170
+ throw new Error('pnpm changed pnpm-workspace.yaml outside the migration')
171
+ }
172
+
173
+ const actual = await dependencies.effective_registry(context.cwd)
174
+ if (!is_public_npm_url(actual)) throw new Error(`effective registry is ${actual}`)
175
+ const lockfile = read_file(context.lockfile_path)
176
+ const old_tarballs = migrate_logic.github_tarballs(lockfile)
177
+ if (old_tarballs.length > 0) throw new Error(`GitHub tarballs remain: ${old_tarballs.join(', ')}`)
178
+
179
+ if (migrate_logic.rewrite_kit_lockfile(lockfile, integrities) !== lockfile) {
180
+ throw new Error('lockfile integrity does not match public npm')
181
+ }
182
+ }
183
+
184
+ async function apply(
185
+ context: MigrationContext,
186
+ dependencies: MigrationDependencies,
187
+ integrities: ReadonlyMap<string, string>,
188
+ ): Promise<string> {
189
+ try {
190
+ writeFileSync(context.npmrc_path, context.plan.content)
191
+ writeFileSync(
192
+ context.lockfile_path,
193
+ migrate_logic.rewrite_kit_lockfile(context.original_lockfile, integrities),
194
+ )
195
+ await verify_resolution(context, dependencies, integrities)
196
+ } catch (error) {
197
+ restore(context)
198
+
199
+ return `Migration blocked; original settings restored: ${String(error)}`
200
+ }
201
+
202
+ return 'Migrated to public npm. The project registry and lockfile now resolve from npm.'
203
+ }
204
+
205
+ function is_already_migrated(
206
+ context: MigrationContext,
207
+ integrities: ReadonlyMap<string, string>,
208
+ ): boolean {
209
+ return (
210
+ context.plan.content === context.original_npmrc &&
211
+ migrate_logic.rewrite_kit_lockfile(context.original_lockfile, integrities) ===
212
+ context.original_lockfile
213
+ )
214
+ }
215
+
216
+ async function migrate(cwd: string, dependencies: MigrationDependencies): Promise<string> {
217
+ const context = create_context(cwd, dependencies)
218
+
219
+ report_context(context)
220
+ const block = initial_block(context, dependencies)
221
+ if (block !== undefined) return block
222
+ const version_check = await verify_public_versions(
223
+ context.original_lockfile,
224
+ dependencies.fetch_version,
225
+ )
226
+
227
+ if (version_check.missing.length > 0) {
228
+ return `Migration blocked: unpublished on npm: ${version_check.missing.join(', ')}.`
229
+ }
230
+
231
+ if (is_already_migrated(context, version_check.integrities)) {
232
+ return 'Already using public npm; no changes made.'
233
+ }
234
+
235
+ return await apply(context, dependencies, version_check.integrities)
236
+ }
237
+
238
+ async function fetch_version(name: string, version: string): Promise<string | undefined> {
239
+ const encoded = encodeURIComponent(name)
240
+ const response = await fetch(`${PUBLIC_REGISTRY}${encoded}/${encodeURIComponent(version)}`, {
241
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
242
+ })
243
+
244
+ if (!response.ok) return undefined
245
+ const parsed = version_schema.safeParse(await response.json())
246
+ if (!parsed.success || !is_public_npm_url(parsed.data.dist.tarball)) return undefined
247
+
248
+ return parsed.data.dist.integrity
249
+ }
250
+
251
+ async function install(cwd: string): Promise<void> {
252
+ await execa('pnpm', ['install', '--lockfile-only', '--ignore-scripts', '--force'], { cwd })
253
+ }
254
+
255
+ async function effective_registry(cwd: string): Promise<string> {
256
+ const result = await execa('pnpm', ['config', 'get', '@joshuafolkken:registry'], { cwd })
257
+
258
+ return result.stdout.trim()
259
+ }
260
+
261
+ async function main(): Promise<void> {
262
+ const result = await migrate(process.cwd(), {
263
+ fetch_version,
264
+ install,
265
+ effective_registry,
266
+ user_npmrc: user_npmrc.read(),
267
+ environment: process.env,
268
+ })
269
+
270
+ console.info(result)
271
+
272
+ if (result.startsWith('Migration blocked') || result.startsWith('No lockfile')) process.exit(1)
273
+ }
274
+
275
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await main()
276
+
277
+ const registry_migration = { migrate }
278
+
279
+ export { registry_migration }
280
+ export type { MigrationDependencies }
@@ -0,0 +1,15 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+
5
+ const NPMRC = '.npmrc'
6
+
7
+ function read(home: string = os.homedir()): string {
8
+ const file = path.join(home, NPMRC)
9
+
10
+ return existsSync(file) ? readFileSync(file, 'utf8') : ''
11
+ }
12
+
13
+ const user_npmrc = { read }
14
+
15
+ export { user_npmrc }
@@ -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: