@joshuafolkken/kit 1.879.0 → 1.881.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.
@@ -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",
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.879.0",
2
+ "version": "1.881.0",
3
3
  "name": "@joshuafolkken/kit",
4
4
  "type": "module",
5
5
  "license": "MIT",
@@ -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 }