@microwiseai/snapshot 0.7.0 → 0.9.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 (116) hide show
  1. package/dist/build-info.json +1 -0
  2. package/dist/commands/access.d.ts +25 -9
  3. package/dist/commands/access.d.ts.map +1 -1
  4. package/dist/commands/access.js +101 -47
  5. package/dist/commands/access.js.map +1 -1
  6. package/dist/commands/config.d.ts +3 -0
  7. package/dist/commands/config.d.ts.map +1 -0
  8. package/dist/commands/config.js +37 -0
  9. package/dist/commands/config.js.map +1 -0
  10. package/dist/commands/install.d.ts +54 -0
  11. package/dist/commands/install.d.ts.map +1 -1
  12. package/dist/commands/install.js +480 -65
  13. package/dist/commands/install.js.map +1 -1
  14. package/dist/commands/publish.d.ts +10 -0
  15. package/dist/commands/publish.d.ts.map +1 -1
  16. package/dist/commands/publish.js +94 -12
  17. package/dist/commands/publish.js.map +1 -1
  18. package/dist/index.js +45 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/access-index.d.ts +81 -0
  21. package/dist/lib/access-index.d.ts.map +1 -0
  22. package/dist/lib/access-index.js +175 -0
  23. package/dist/lib/access-index.js.map +1 -0
  24. package/dist/lib/access-sync.d.ts +66 -17
  25. package/dist/lib/access-sync.d.ts.map +1 -1
  26. package/dist/lib/access-sync.js +391 -213
  27. package/dist/lib/access-sync.js.map +1 -1
  28. package/dist/lib/config.d.ts +12 -0
  29. package/dist/lib/config.d.ts.map +1 -1
  30. package/dist/lib/config.js +38 -5
  31. package/dist/lib/config.js.map +1 -1
  32. package/dist/lib/extends-access.d.ts +34 -0
  33. package/dist/lib/extends-access.d.ts.map +1 -0
  34. package/dist/lib/extends-access.js +82 -0
  35. package/dist/lib/extends-access.js.map +1 -0
  36. package/dist/lib/gitlab-publisher.d.ts +70 -0
  37. package/dist/lib/gitlab-publisher.d.ts.map +1 -0
  38. package/dist/lib/gitlab-publisher.js +165 -0
  39. package/dist/lib/gitlab-publisher.js.map +1 -0
  40. package/dist/lib/gitlab.d.ts +4 -0
  41. package/dist/lib/gitlab.d.ts.map +1 -1
  42. package/dist/lib/gitlab.js +1 -1
  43. package/dist/lib/gitlab.js.map +1 -1
  44. package/dist/lib/grant.d.ts +157 -0
  45. package/dist/lib/grant.d.ts.map +1 -0
  46. package/dist/lib/grant.js +352 -0
  47. package/dist/lib/grant.js.map +1 -0
  48. package/dist/lib/install-verify.d.ts +16 -2
  49. package/dist/lib/install-verify.d.ts.map +1 -1
  50. package/dist/lib/install-verify.js +24 -3
  51. package/dist/lib/install-verify.js.map +1 -1
  52. package/dist/lib/ledger.d.ts +13 -0
  53. package/dist/lib/ledger.d.ts.map +1 -1
  54. package/dist/lib/ledger.js.map +1 -1
  55. package/dist/lib/prerequisites.d.ts.map +1 -1
  56. package/dist/lib/prerequisites.js +4 -1
  57. package/dist/lib/prerequisites.js.map +1 -1
  58. package/dist/lib/proxy-auth-globalconfig.d.ts +55 -0
  59. package/dist/lib/proxy-auth-globalconfig.d.ts.map +1 -0
  60. package/dist/lib/proxy-auth-globalconfig.js +110 -0
  61. package/dist/lib/proxy-auth-globalconfig.js.map +1 -0
  62. package/dist/lib/semver-range.d.ts +32 -0
  63. package/dist/lib/semver-range.d.ts.map +1 -0
  64. package/dist/lib/semver-range.js +243 -0
  65. package/dist/lib/semver-range.js.map +1 -0
  66. package/dist/lib/session.d.ts +5 -0
  67. package/dist/lib/session.d.ts.map +1 -1
  68. package/dist/lib/session.js +9 -0
  69. package/dist/lib/session.js.map +1 -1
  70. package/dist/lib/skit-adapter.d.ts.map +1 -1
  71. package/dist/lib/skit-adapter.js +80 -23
  72. package/dist/lib/skit-adapter.js.map +1 -1
  73. package/dist/lib/timing.d.ts +7 -0
  74. package/dist/lib/timing.d.ts.map +1 -0
  75. package/dist/lib/timing.js +14 -0
  76. package/dist/lib/timing.js.map +1 -0
  77. package/dist/lib/types.d.ts +3 -0
  78. package/dist/lib/types.d.ts.map +1 -1
  79. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.d.ts +25 -0
  80. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.d.ts.map +1 -0
  81. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.js +55 -0
  82. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/command-specs.js.map +1 -0
  83. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.d.ts +52 -0
  84. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.d.ts.map +1 -0
  85. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.js +62 -0
  86. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/config.js.map +1 -0
  87. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.d.ts +30 -0
  88. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.d.ts.map +1 -0
  89. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.js +39 -0
  90. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/detect.js.map +1 -0
  91. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.d.ts +35 -0
  92. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.d.ts.map +1 -0
  93. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.js +31 -0
  94. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/index.js.map +1 -0
  95. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.d.ts +42 -0
  96. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.d.ts.map +1 -0
  97. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.js +36 -0
  98. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/policy.js.map +1 -0
  99. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.d.ts +11 -0
  100. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.d.ts.map +1 -0
  101. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.js +46 -0
  102. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/report.js.map +1 -0
  103. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.d.ts +11 -0
  104. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.d.ts.map +1 -0
  105. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.js +81 -0
  106. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/semver.js.map +1 -0
  107. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.d.ts +29 -0
  108. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.d.ts.map +1 -0
  109. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.js +6 -0
  110. package/node_modules/@microwiseai/snapshot-downgrade-guard/dist/types.js.map +1 -0
  111. package/node_modules/@microwiseai/snapshot-downgrade-guard/package.json +55 -0
  112. package/package.json +9 -2
  113. package/dist/lib/transitive-resolver.d.ts +0 -89
  114. package/dist/lib/transitive-resolver.d.ts.map +0 -1
  115. package/dist/lib/transitive-resolver.js +0 -436
  116. package/dist/lib/transitive-resolver.js.map +0 -1
@@ -1,256 +1,434 @@
1
1
  /**
2
- * Access Sync - Synchronize snapshot package access with license-api
2
+ * Install-set closure resolver (formerly "access sync").
3
3
  *
4
- * When a snapshot is published, this module registers all packages
5
- * in the snapshot's install list with the appropriate access level.
6
- */
7
- import chalk from 'chalk';
8
- import { LICENSE_API_URL, GITLAB_GROUP_ID, REGISTRY_PROXY_URL } from './config.js';
9
- /**
10
- * Resolve the admin API key from the environment.
4
+ * Walks a snapshot's install-set dependency closure through the registry-proxy.
5
+ * The result feeds access-index.json (see ./access-index.ts), the serving SSOT
6
+ * that registry-proxy reads to grant snapshot members.
11
7
  *
12
- * LICENSE_API_ADMIN_KEY is required. There is intentionally NO fallback —
13
- * a missing key must fail loudly rather than silently using a weak default.
8
+ * The former license-api write path (POST /admin/package-access/with-deps,
9
+ * LICENSE_API_ADMIN_KEY) was retired by the 2026-09-28 owner decision
10
+ * (global state `snapshot-tier-access`, `owner-decision-2026-09-28-proxy-snapshot-driven`):
11
+ * "license-api package_access 에 snapshot 의존 패키지를 등록하지 않음(access-sync 의
12
+ * license-api 쓰기 경로 폐기)". Nothing in this module writes anywhere.
14
13
  */
15
- function getAdminApiKey() {
16
- const key = process.env.LICENSE_API_ADMIN_KEY;
17
- if (!key) {
18
- throw new Error('LICENSE_API_ADMIN_KEY environment variable is required for package access sync. ' +
19
- 'Set it before running access sync.');
20
- }
21
- return key;
14
+ import chalk from 'chalk';
15
+ import { createGunzip } from 'zlib';
16
+ import { Readable } from 'stream';
17
+ import { extract } from 'tar-stream';
18
+ import { maxSatisfying, parseVersion } from './semver-range.js';
19
+ import { REGISTRY_PROXY_URL, getLicenseKey, getProxyAuthToken } from './config.js';
20
+ import { cachedGenericVersions, fetchGitlabGenericTarball, fetchGitlabPackument, } from './gitlab-publisher.js';
21
+ function proxyAuthHeaders() {
22
+ const token = getLicenseKey() || getProxyAuthToken();
23
+ return token ? { Authorization: `Bearer ${token}` } : {};
22
24
  }
23
25
  /**
24
- * Parse package name into scope and name
25
- * @example "@ist/loggable-cli" -> { scope: "@ist", name: "loggable-cli" }
26
+ * Normalize a dependency spec. Handles `npm:` aliases (install the real
27
+ * package) and `workspace:`; other protocols (file:, git, url, link:) are not
28
+ * registry packages → null.
26
29
  */
27
- function parsePackageName(fullName) {
28
- const match = fullName.match(/^(@[^/]+)\/(.+)$/);
29
- if (!match)
30
+ function normalizeSpec(name, spec) {
31
+ let s = (spec ?? '').trim();
32
+ if (s.startsWith('npm:')) {
33
+ const m = /^npm:(@?[^@]+)(?:@(.*))?$/.exec(s);
34
+ if (!m)
35
+ return null;
36
+ return { name: m[1], spec: m[2] ?? 'latest' };
37
+ }
38
+ if (s.startsWith('workspace:'))
39
+ s = s.slice('workspace:'.length);
40
+ if (/^(file:|link:|git\+|git:|https?:|github:)/.test(s) || /^[^@\s]+\/[^\s]+$/.test(s))
30
41
  return null;
31
- return { scope: match[1], name: match[2] };
32
- }
33
- /**
34
- * Convert SnapshotAccess to license-api access level
35
- */
36
- function getAccessLevel(access) {
37
- if (!access)
38
- return 'licensed'; // Default to licensed
39
- if (access === 'public')
40
- return 'public';
41
- if (access === 'authenticated')
42
- return 'authenticated';
43
- if (typeof access === 'object' && access.type === 'licensed')
44
- return 'licensed';
45
- return 'licensed';
42
+ return { name, spec: s === '' ? '*' : s };
46
43
  }
47
44
  /**
48
- * Get product ID from access config (for licensed packages)
45
+ * Resolve a spec against a version list: dist-tag → exact → highest version
46
+ * satisfying the range (prerelease allowed only for prerelease pins).
47
+ * @internal Exported for testing
49
48
  */
50
- function getProductId(access) {
51
- if (typeof access === 'object' && access.type === 'licensed') {
52
- return access.productId;
53
- }
54
- return undefined;
49
+ export function resolveSpecVersion(versions, distTags, spec) {
50
+ if (distTags[spec])
51
+ return distTags[spec];
52
+ if (versions.includes(spec))
53
+ return spec;
54
+ return maxSatisfying(versions, spec);
55
55
  }
56
- /**
57
- * Register a package with license-api via a single idempotent upsert.
58
- *
59
- * Uses POST /admin/package-access/with-deps. The (scope, packageName) pair is a
60
- * unique key on the license-api side, so this call is idempotent — repeated
61
- * calls update the existing rule instead of creating duplicates. This replaces
62
- * the previous non-atomic find-then-create (GET + PUT/POST) flow.
63
- *
64
- * `dependencies` is intentionally sent as an empty array: access-sync already
65
- * enumerates the full (transitive) package set on the client side, so the
66
- * server-side automatic dependency resolution must be disabled to avoid
67
- * duplicate/conflicting rule creation.
68
- */
69
- async function registerPackage(rule) {
70
- try {
71
- const body = {
72
- scope: rule.scope,
73
- packageName: rule.packageName,
74
- accessLevel: rule.accessLevel,
75
- priority: rule.priority ?? 100,
76
- description: rule.description,
77
- requiredProductIds: rule.requiredProductIds,
78
- parentPackage: rule.parentPackage,
79
- // Disable server-side auto dependency resolution — the client enumerates
80
- // all packages itself and registers each one individually.
81
- dependencies: [],
82
- };
83
- const url = `${LICENSE_API_URL}/admin/package-access/with-deps`;
84
- const response = await fetch(url, {
85
- method: 'POST',
86
- headers: {
87
- 'Content-Type': 'application/json',
88
- 'X-Admin-Api-Key': getAdminApiKey(),
89
- },
90
- body: JSON.stringify(body),
91
- });
92
- if (response.ok) {
93
- return true;
56
+ async function classifyNpm(name, cache, headers, source = 'proxy') {
57
+ const cached = cache.get(name);
58
+ if (cached)
59
+ return cached;
60
+ const p = (async () => {
61
+ if (!name.startsWith('@'))
62
+ return { kind: 'npmjs' };
63
+ if (source === 'gitlab') {
64
+ const r = await fetchGitlabPackument(name);
65
+ if (r.kind === 'npmjs')
66
+ return { kind: 'npmjs' };
67
+ if (r.kind === 'error')
68
+ return { kind: 'unknown', reason: r.reason };
69
+ const packument = r.packument;
70
+ return {
71
+ kind: 'ours',
72
+ versions: Object.keys(packument.versions ?? {}),
73
+ distTags: packument['dist-tags'] ?? {},
74
+ packument,
75
+ };
94
76
  }
95
- const error = await response.json().catch(() => ({}));
96
- console.error(chalk.yellow(` Warning: Failed to upsert ${rule.scope}/${rule.packageName}: ${JSON.stringify(error)}`));
97
- return false;
98
- }
99
- catch (error) {
100
- console.error(chalk.yellow(` Warning: Error with ${rule.scope}/${rule.packageName}: ${error}`));
101
- return false;
102
- }
77
+ try {
78
+ const res = await fetch(`${REGISTRY_PROXY_URL}/${name}`, { headers });
79
+ if (res.ok) {
80
+ const packument = (await res.json());
81
+ return {
82
+ kind: 'ours',
83
+ versions: Object.keys(packument.versions ?? {}),
84
+ distTags: packument['dist-tags'] ?? {},
85
+ packument,
86
+ };
87
+ }
88
+ if (res.status === 404)
89
+ return { kind: 'npmjs' };
90
+ const vres = await fetch(`${REGISTRY_PROXY_URL}/${name}/versions`, { headers });
91
+ if (vres.ok) {
92
+ const v = (await vres.json());
93
+ return {
94
+ kind: 'ours',
95
+ versions: v.versions ?? [],
96
+ distTags: v.latest ? { latest: v.latest } : {},
97
+ packument: null,
98
+ packumentStatus: res.status,
99
+ };
100
+ }
101
+ if (vres.status === 404)
102
+ return { kind: 'npmjs' };
103
+ return { kind: 'unknown', reason: `HTTP ${res.status}/${vres.status}` };
104
+ }
105
+ catch (error) {
106
+ return { kind: 'unknown', reason: String(error) };
107
+ }
108
+ })();
109
+ cache.set(name, p);
110
+ return p;
103
111
  }
104
112
  /**
105
- * Resolve npm dependencies for a package via registry-proxy
113
+ * Pick the npm deps (name → spec) a kit's skit.json declares under
114
+ * `dependencies.npm`. Entries marked `source: "npm"` come from public npmjs
115
+ * and are excluded. Object entries use their `version` field.
116
+ * @internal Exported for testing
106
117
  */
107
- async function resolveNpmDeps(packageName) {
108
- try {
109
- const response = await fetch(`${REGISTRY_PROXY_URL}/resolve-deps`, {
110
- method: 'POST',
111
- headers: { 'Content-Type': 'application/json' },
112
- body: JSON.stringify({
113
- packages: [packageName],
114
- groupId: GITLAB_GROUP_ID,
115
- scopes: ['@ist'],
116
- includePeer: false,
117
- }),
118
- });
119
- if (!response.ok) {
120
- return [];
118
+ export function kitNpmDeps(skitJson) {
119
+ const npm = skitJson
120
+ ?.dependencies?.npm;
121
+ if (!npm || typeof npm !== 'object')
122
+ return [];
123
+ const out = [];
124
+ for (const [name, spec] of Object.entries(npm)) {
125
+ if (spec && typeof spec === 'object') {
126
+ const o = spec;
127
+ if (o.source === 'npm')
128
+ continue;
129
+ out.push([name, o.version ?? 'latest']);
121
130
  }
122
- const data = await response.json();
123
- if (data.success && data.dependencies) {
124
- // Filter to only @ist scoped packages
125
- return data.dependencies
126
- .map(d => d.name)
127
- .filter(name => name.startsWith('@ist/'));
131
+ else {
132
+ out.push([name, typeof spec === 'string' ? spec : 'latest']);
128
133
  }
129
- return [];
130
134
  }
131
- catch {
135
+ return out;
136
+ }
137
+ /** @internal Exported for testing */
138
+ export function kitNpmDepNames(skitJson) {
139
+ return kitNpmDeps(skitJson).map(([name]) => name);
140
+ }
141
+ /** Kit → kit deps from skit.json `dependencies.skit`. */
142
+ function kitSkitDeps(skitJson) {
143
+ const skit = skitJson
144
+ ?.dependencies?.skit;
145
+ if (!skit || typeof skit !== 'object')
132
146
  return [];
133
- }
147
+ return Object.entries(skit).map(([name, spec]) => [
148
+ name,
149
+ typeof spec === 'string' ? spec : (spec?.version ?? 'latest'),
150
+ ]);
151
+ }
152
+ /** Read skit.json out of a gzipped kit tarball. */
153
+ function extractSkitJsonFromBuffer(buffer) {
154
+ return new Promise((resolve, reject) => {
155
+ const extractor = extract();
156
+ let skitJson = null;
157
+ extractor.on('entry', (header, stream, next) => {
158
+ const chunks = [];
159
+ stream.on('data', (chunk) => chunks.push(chunk));
160
+ stream.on('end', () => {
161
+ const name = header.name.replace(/^\.\//, '');
162
+ // Top-level skit.json only (either at root or under a single wrapper dir).
163
+ if (skitJson === null && (name === 'skit.json' || /^[^/]+\/skit\.json$/.test(name))) {
164
+ try {
165
+ skitJson = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
166
+ }
167
+ catch {
168
+ reject(new Error('Failed to parse skit.json'));
169
+ return;
170
+ }
171
+ }
172
+ next();
173
+ });
174
+ stream.on('error', reject);
175
+ });
176
+ extractor.on('finish', () => {
177
+ if (skitJson !== null)
178
+ resolve(skitJson);
179
+ else
180
+ reject(new Error('skit.json not found in kit tarball'));
181
+ });
182
+ extractor.on('error', reject);
183
+ Readable.from(buffer).pipe(createGunzip()).on('error', reject).pipe(extractor);
184
+ });
134
185
  }
186
+ const pendingKey = (p) => `${p.kind}:${p.name}@${p.version}`;
135
187
  /**
136
- * Extract all packages from snapshot install config
188
+ * Walker over the install-set dependency closure (see the block comment
189
+ * above). `drain()` processes the queue; items whose deps cannot be read
190
+ * (kit tarball / packument 401, …) are kept in `pending` and reported as gaps.
191
+ *
192
+ * With `transitive: false` only declared entries are collected (install.*
193
+ * members + kit skit.json npm/skit deps); package.json is not descended.
137
194
  */
138
- function extractPackagesFromSnapshot(snapshot) {
139
- const packages = [];
140
- // New structure: install.<installer>.<package>
141
- if (snapshot.install) {
142
- for (const installerPackages of Object.values(snapshot.install)) {
143
- packages.push(...Object.keys(installerPackages));
195
+ class ClosureWalker {
196
+ opts;
197
+ register = new Set();
198
+ npmjs = new Set();
199
+ unresolved = [];
200
+ /** Non-retryable traversal gaps (e.g. no version satisfies the pin). */
201
+ unmatched = [];
202
+ pending = new Map();
203
+ /** `${kind}:${name}` → exact versions resolved by the walk. */
204
+ resolved = new Map();
205
+ queue = [];
206
+ seenSpec = new Set();
207
+ visited = new Set();
208
+ cache = new Map();
209
+ headers;
210
+ hasToken;
211
+ constructor(snapshot, opts) {
212
+ this.opts = opts;
213
+ // GitLab mode never sends the user's proxy token anywhere.
214
+ this.headers = opts.source === 'gitlab' ? {} : proxyAuthHeaders();
215
+ this.hasToken = opts.source === 'gitlab' || 'Authorization' in this.headers;
216
+ for (const [installer, pkgs] of Object.entries(snapshot.install ?? {})) {
217
+ const kind = installer === 'skit' ? 'kit' : 'npm';
218
+ for (const [name, spec] of Object.entries(pkgs))
219
+ this.queue.push({ kind, name, spec, declared: true });
220
+ }
221
+ for (const [name, spec] of Object.entries(snapshot.packages ?? {})) {
222
+ this.queue.push({ kind: 'npm', name, spec: String(spec), declared: true });
223
+ }
224
+ for (const [name, spec] of Object.entries(snapshot.skits ?? {})) {
225
+ this.queue.push({ kind: 'kit', name, spec, declared: true });
144
226
  }
145
227
  }
146
- // Legacy structure: packages
147
- if (snapshot.packages) {
148
- packages.push(...Object.keys(snapshot.packages));
228
+ async drain() {
229
+ while (this.queue.length > 0) {
230
+ const item = this.queue.shift();
231
+ const specKey = `${item.kind}:${item.name}@${item.spec}:${item.declared}`;
232
+ if (this.seenSpec.has(specKey))
233
+ continue;
234
+ this.seenSpec.add(specKey);
235
+ if (item.kind === 'kit')
236
+ await this.processKit(item);
237
+ else
238
+ await this.processNpm(item);
239
+ }
149
240
  }
150
- // Legacy structure: skits
151
- if (snapshot.skits) {
152
- packages.push(...Object.keys(snapshot.skits));
241
+ result() {
242
+ const pending = [...this.pending.values()];
243
+ return {
244
+ packages: [...this.register],
245
+ npmjsExcluded: [...this.npmjs].filter((n) => !this.register.has(n)),
246
+ skippedKits: pending.filter((p) => p.kind === 'kit').map((p) => `${p.name}@${p.version}`),
247
+ untraversed: [
248
+ ...pending.filter((p) => p.kind === 'npm').map((p) => `${p.name}@${p.version} (${p.reason})`),
249
+ ...this.unmatched,
250
+ ],
251
+ unresolved: [...this.unresolved],
252
+ resolved: [...this.resolved.values()].map((r) => ({ ...r, versions: [...r.versions] })),
253
+ };
153
254
  }
154
- return [...new Set(packages)]; // Dedupe
155
- }
156
- /**
157
- * Sync snapshot packages with license-api
158
- *
159
- * @param snapshot - Parsed snapshot.json
160
- * @param options - Sync options
161
- */
162
- export async function syncPackageAccess(snapshot, options = {}) {
163
- const { verbose = false, dryRun = false, includeTransitive = true } = options;
164
- const result = {
165
- success: true,
166
- registered: [],
167
- failed: [],
168
- skipped: [],
169
- };
170
- const accessLevel = getAccessLevel(snapshot.access);
171
- const productId = getProductId(snapshot.access);
172
- console.log(chalk.cyan(`\nSyncing package access for ${snapshot.name}...`));
173
- console.log(chalk.dim(` Access level: ${accessLevel}`));
174
- // 1. Extract direct packages from snapshot
175
- const directPackages = extractPackagesFromSnapshot(snapshot);
176
- if (verbose) {
177
- console.log(chalk.dim(` Direct packages: ${directPackages.length}`));
255
+ recordVersion(kind, name, version) {
256
+ const key = `${kind}:${name}`;
257
+ const entry = this.resolved.get(key) ?? { name, kind, versions: [] };
258
+ if (!entry.versions.includes(version))
259
+ entry.versions.push(version);
260
+ this.resolved.set(key, entry);
178
261
  }
179
- // 2. Resolve transitive dependencies if requested
180
- let allPackages = [...directPackages];
181
- if (includeTransitive) {
182
- console.log(chalk.dim(' Resolving transitive dependencies...'));
183
- for (const pkg of directPackages) {
184
- const deps = await resolveNpmDeps(pkg);
185
- allPackages.push(...deps);
262
+ addPending(item) {
263
+ this.pending.set(pendingKey(item), item);
264
+ }
265
+ async processNpm(item) {
266
+ if (item.declared)
267
+ this.register.add(item.name);
268
+ if (!this.opts.transitive)
269
+ return;
270
+ const norm = normalizeSpec(item.name, item.spec);
271
+ if (!norm) {
272
+ if (this.opts.verbose)
273
+ console.log(chalk.dim(` Skipping non-registry spec ${item.name}@${item.spec}`));
274
+ return;
275
+ }
276
+ const cls = await classifyNpm(norm.name, this.cache, this.headers, this.opts.source);
277
+ if (cls.kind === 'npmjs') {
278
+ if (!item.declared)
279
+ this.npmjs.add(norm.name);
280
+ return;
186
281
  }
187
- allPackages = [...new Set(allPackages)]; // Dedupe
188
- if (verbose) {
189
- console.log(chalk.dim(` Total packages (with deps): ${allPackages.length}`));
282
+ if (cls.kind === 'unknown') {
283
+ console.error(chalk.yellow(` Warning: could not look up ${norm.name} (${cls.reason})`));
284
+ this.unresolved.push(`${norm.name} (${cls.reason})`);
285
+ return;
190
286
  }
287
+ this.register.add(norm.name);
288
+ let version = resolveSpecVersion(cls.versions, cls.distTags, norm.spec);
289
+ if (!version) {
290
+ const fallback = cls.distTags.latest ?? maxSatisfying(cls.versions, '*');
291
+ console.error(chalk.yellow(` Warning: no version of ${norm.name} satisfies "${norm.spec}"` +
292
+ (fallback ? `; traversing ${fallback} instead` : '')));
293
+ if (!fallback) {
294
+ this.unmatched.push(`${norm.name}@${norm.spec} (no matching version)`);
295
+ return;
296
+ }
297
+ // Not an exact resolution: the install would not get this version, so it
298
+ // is a gap (not recorded in `resolved`), but deps are still walked.
299
+ this.unmatched.push(`${norm.name}@${norm.spec} (no matching version; traversed ${fallback})`);
300
+ version = fallback;
301
+ }
302
+ else {
303
+ this.recordVersion('npm', norm.name, version);
304
+ }
305
+ const key = `npm:${norm.name}@${version}`;
306
+ if (this.visited.has(key))
307
+ return;
308
+ this.visited.add(key);
309
+ const manifest = cls.packument?.versions?.[version];
310
+ if (!manifest) {
311
+ const reason = cls.packument
312
+ ? 'version missing from packument'
313
+ : `packument HTTP ${cls.packumentStatus}${this.hasToken ? '' : ', 토큰 없음'}`;
314
+ console.error(chalk.yellow(` Warning: deps of ${norm.name}@${version} not traversed (${reason})`));
315
+ this.addPending({ kind: 'npm', name: norm.name, version, reason });
316
+ return;
317
+ }
318
+ this.expandManifest(norm.name, version, manifest);
191
319
  }
192
- // 2b. Always register the snapshot's own artifact package.
193
- //
194
- // Member packages are registered above, but the snapshot artifact itself
195
- // (e.g. "@ist/testing") was previously never registered. As a result the
196
- // registry proxy had no access rule for the snapshot artifact and rejected
197
- // downloads (e.g. testing -> 401). Register the snapshot's own package with
198
- // the access level declared in snapshot.json so the proxy knows it.
199
- if (snapshot.name && !allPackages.includes(snapshot.name)) {
200
- allPackages.unshift(snapshot.name);
320
+ expandManifest(name, version, manifest) {
321
+ const deps = {
322
+ ...manifest.dependencies,
323
+ ...manifest.optionalDependencies,
324
+ ...manifest.peerDependencies,
325
+ };
326
+ if (this.opts.verbose && Object.keys(deps).length > 0) {
327
+ console.log(chalk.dim(` ${name}@${version} deps: ${Object.keys(deps).join(', ')}`));
328
+ }
329
+ for (const [dep, spec] of Object.entries(deps)) {
330
+ this.queue.push({ kind: 'npm', name: dep, spec, declared: false });
331
+ }
201
332
  }
202
- // 3. Register each package
203
- console.log(chalk.dim(` Registering ${allPackages.length} packages...`));
204
- for (const pkg of allPackages) {
205
- const parsed = parsePackageName(pkg);
206
- if (!parsed) {
207
- result.skipped.push(pkg);
208
- continue;
333
+ async processKit(item) {
334
+ this.register.add(item.name);
335
+ const registryName = kitRegistryName(item.name);
336
+ let version = parseVersion(item.spec) ? item.spec : null;
337
+ if (!version) {
338
+ try {
339
+ if (this.opts.source === 'gitlab') {
340
+ const versions = (await cachedGenericVersions(registryName)).map((p) => p.version);
341
+ const latest = maxSatisfying(versions, '*');
342
+ version = resolveSpecVersion(versions, latest ? { latest } : {}, item.spec);
343
+ }
344
+ else {
345
+ const res = await fetch(`${REGISTRY_PROXY_URL}/generic/${registryName}/versions`, { headers: this.headers });
346
+ if (res.ok) {
347
+ const v = (await res.json());
348
+ version = resolveSpecVersion(v.versions ?? [], v.latest ? { latest: v.latest } : {}, item.spec);
349
+ }
350
+ }
351
+ }
352
+ catch {
353
+ // handled below
354
+ }
355
+ if (!version) {
356
+ this.unmatched.push(`${item.name}@${item.spec} (kit version not resolved)`);
357
+ console.error(chalk.yellow(` Warning: could not resolve kit ${item.name}@${item.spec}; skipping its deps`));
358
+ return;
359
+ }
209
360
  }
210
- const isSelf = pkg === snapshot.name;
211
- const rule = {
212
- scope: parsed.scope,
213
- packageName: parsed.name,
214
- accessLevel,
215
- priority: 100,
216
- description: isSelf
217
- ? `Snapshot artifact ${snapshot.name}`
218
- : `Auto-synced from ${snapshot.name}`,
219
- // The snapshot's own package has no parent; member packages point to it.
220
- parentPackage: isSelf ? undefined : snapshot.name,
221
- };
222
- if (accessLevel === 'licensed' && productId) {
223
- rule.requiredProductIds = productId;
361
+ this.recordVersion('generic', item.name, version);
362
+ const key = `kit:${item.name}@${version}`;
363
+ if (this.visited.has(key))
364
+ return;
365
+ this.visited.add(key);
366
+ const kit = item.name;
367
+ const out = await this.fetchKit(kit, version);
368
+ if (out.ok)
369
+ return;
370
+ this.addPending({ kind: 'kit', name: kit, version, reason: out.reason });
371
+ if (out.status === undefined) {
372
+ console.error(chalk.yellow(` Warning: could not read kit ${kit}@${version}: ${out.reason}; skipping its npm deps`));
224
373
  }
225
- if (dryRun) {
226
- console.log(chalk.yellow(` [dry-run] Would register: ${pkg} (${accessLevel})`));
227
- result.registered.push(pkg);
374
+ else if ((out.status === 401 || out.status === 403) && !this.hasToken) {
375
+ console.error(chalk.yellow(` Warning: kit ${kit}@${version} 의 npm 의존을 등록하지 못함 (HTTP ${out.status}, 토큰 없음): ` +
376
+ 'IST_SESSION_TOKEN(또는 SNAPSHOT_LICENSE_KEY) 설정 후 다시 실행'));
228
377
  }
229
378
  else {
230
- const success = await registerPackage(rule);
231
- if (success) {
232
- result.registered.push(pkg);
233
- if (verbose) {
234
- console.log(chalk.green(` ✓ ${pkg}`));
235
- }
379
+ console.error(chalk.yellow(` Warning: could not fetch kit ${kit}@${version} (HTTP ${out.status}); skipping its npm deps`));
380
+ }
381
+ }
382
+ /** Fetch a kit tarball, read skit.json and enqueue its npm/skit deps. */
383
+ async fetchKit(kit, version) {
384
+ const registryName = kitRegistryName(kit);
385
+ try {
386
+ let buffer;
387
+ if (this.opts.source === 'gitlab') {
388
+ const r = await fetchGitlabGenericTarball(registryName, version);
389
+ if (!r.ok)
390
+ return { ok: false, status: r.status, reason: r.reason };
391
+ buffer = r.body;
236
392
  }
237
393
  else {
238
- result.failed.push(pkg);
239
- result.success = false;
394
+ const url = `${REGISTRY_PROXY_URL}/generic/${registryName}/${version}/${registryName}-${version}.tar.gz`;
395
+ const response = await fetch(url, { headers: this.headers });
396
+ if (!response.ok)
397
+ return { ok: false, status: response.status, reason: `HTTP ${response.status}` };
398
+ buffer = Buffer.from(await response.arrayBuffer());
399
+ }
400
+ const skitJson = await extractSkitJsonFromBuffer(buffer);
401
+ const npmDeps = kitNpmDeps(skitJson);
402
+ const skitDeps = kitSkitDeps(skitJson);
403
+ if (this.opts.verbose && npmDeps.length + skitDeps.length > 0) {
404
+ console.log(chalk.dim(` Kit ${kit}@${version} deps: ${[...npmDeps, ...skitDeps].map(([n]) => n).join(', ')}`));
240
405
  }
406
+ for (const [name, spec] of npmDeps)
407
+ this.queue.push({ kind: 'npm', name, spec, declared: true });
408
+ for (const [name, spec] of skitDeps)
409
+ this.queue.push({ kind: 'kit', name, spec, declared: true });
410
+ return { ok: true };
411
+ }
412
+ catch (error) {
413
+ return { ok: false, reason: String(error) };
241
414
  }
242
415
  }
243
- // 4. Summary
244
- console.log();
245
- if (result.registered.length > 0) {
246
- console.log(chalk.green(` ✓ Registered: ${result.registered.length} packages`));
247
- }
248
- if (result.failed.length > 0) {
249
- console.log(chalk.red(` ✗ Failed: ${result.failed.length} packages`));
250
- }
251
- if (result.skipped.length > 0) {
252
- console.log(chalk.yellow(` ⊘ Skipped: ${result.skipped.length} packages (invalid format)`));
253
- }
254
- return result;
416
+ }
417
+ /** `@ist/qa-kit` → `ist__qa-kit` (registry-proxy generic path form). */
418
+ export function kitRegistryName(kit) {
419
+ return kit.replace(/^@/, '').replace(/\//g, '__');
420
+ }
421
+ /**
422
+ * Walk the install-set dependency closure once.
423
+ * @internal Exported for testing
424
+ */
425
+ export async function resolveInstallClosure(snapshot, options = {}) {
426
+ const walker = new ClosureWalker(snapshot, {
427
+ transitive: options.transitive ?? true,
428
+ verbose: options.verbose ?? false,
429
+ source: options.source ?? 'proxy',
430
+ });
431
+ await walker.drain();
432
+ return walker.result();
255
433
  }
256
434
  //# sourceMappingURL=access-sync.js.map