@kindgi/cli 0.1.9 → 0.1.10-rc.1

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 (111) hide show
  1. package/README.md +100 -6
  2. package/dist/build/bundle.d.ts +3 -2
  3. package/dist/build/bundle.d.ts.map +1 -1
  4. package/dist/build/bundle.js +3 -2
  5. package/dist/build/bundle.js.map +1 -1
  6. package/dist/build/computed-imports.d.ts +27 -0
  7. package/dist/build/computed-imports.d.ts.map +1 -0
  8. package/dist/build/computed-imports.js +45 -0
  9. package/dist/build/computed-imports.js.map +1 -0
  10. package/dist/build/containerfile.d.ts +2 -2
  11. package/dist/build/containerfile.d.ts.map +1 -1
  12. package/dist/build/containerfile.js +81 -12
  13. package/dist/build/containerfile.js.map +1 -1
  14. package/dist/build/defaults.d.ts +3 -2
  15. package/dist/build/defaults.d.ts.map +1 -1
  16. package/dist/build/defaults.js +28 -6
  17. package/dist/build/defaults.js.map +1 -1
  18. package/dist/build/host-install.d.ts +39 -7
  19. package/dist/build/host-install.d.ts.map +1 -1
  20. package/dist/build/host-install.js +57 -21
  21. package/dist/build/host-install.js.map +1 -1
  22. package/dist/build/image-config.d.ts +6 -0
  23. package/dist/build/image-config.d.ts.map +1 -1
  24. package/dist/build/image-config.js +43 -4
  25. package/dist/build/image-config.js.map +1 -1
  26. package/dist/build/loaded-install-check.d.ts +22 -0
  27. package/dist/build/loaded-install-check.d.ts.map +1 -0
  28. package/dist/build/loaded-install-check.js +240 -0
  29. package/dist/build/loaded-install-check.js.map +1 -0
  30. package/dist/build/loaded-install.d.ts +78 -0
  31. package/dist/build/loaded-install.d.ts.map +1 -0
  32. package/dist/build/loaded-install.js +791 -0
  33. package/dist/build/loaded-install.js.map +1 -0
  34. package/dist/build/runners.d.ts +17 -0
  35. package/dist/build/runners.d.ts.map +1 -1
  36. package/dist/commands/build.d.ts +33 -1
  37. package/dist/commands/build.d.ts.map +1 -1
  38. package/dist/commands/build.js +132 -6
  39. package/dist/commands/build.js.map +1 -1
  40. package/dist/commands/helpers.d.ts +6 -0
  41. package/dist/commands/helpers.d.ts.map +1 -1
  42. package/dist/commands/helpers.js +14 -0
  43. package/dist/commands/helpers.js.map +1 -1
  44. package/dist/commands/index.d.ts.map +1 -1
  45. package/dist/commands/index.js +4 -0
  46. package/dist/commands/index.js.map +1 -1
  47. package/dist/commands/init.js +2 -2
  48. package/dist/commands/init.js.map +1 -1
  49. package/dist/commands/license.d.ts.map +1 -1
  50. package/dist/commands/license.js +12 -7
  51. package/dist/commands/license.js.map +1 -1
  52. package/dist/commands/projects.d.ts.map +1 -1
  53. package/dist/commands/projects.js +1 -15
  54. package/dist/commands/projects.js.map +1 -1
  55. package/dist/commands/sites.d.ts.map +1 -1
  56. package/dist/commands/sites.js +16 -1
  57. package/dist/commands/sites.js.map +1 -1
  58. package/dist/commands/tenant.d.ts +3 -0
  59. package/dist/commands/tenant.d.ts.map +1 -0
  60. package/dist/commands/tenant.js +93 -0
  61. package/dist/commands/tenant.js.map +1 -0
  62. package/dist/commands/usage.d.ts +3 -0
  63. package/dist/commands/usage.d.ts.map +1 -0
  64. package/dist/commands/usage.js +153 -0
  65. package/dist/commands/usage.js.map +1 -0
  66. package/dist/deploy/response.d.ts.map +1 -1
  67. package/dist/deploy/response.js +15 -1
  68. package/dist/deploy/response.js.map +1 -1
  69. package/dist/dev/runtime-image.d.ts +1 -1
  70. package/dist/dev/runtime-image.d.ts.map +1 -1
  71. package/dist/dev/runtime-image.js +1 -1
  72. package/dist/dev/runtime-image.js.map +1 -1
  73. package/dist/exports/plan.d.ts.map +1 -1
  74. package/dist/exports/plan.js +2 -0
  75. package/dist/exports/plan.js.map +1 -1
  76. package/dist/init/dependency-specs.d.ts +3 -1
  77. package/dist/init/dependency-specs.d.ts.map +1 -1
  78. package/dist/init/dependency-specs.js +6 -2
  79. package/dist/init/dependency-specs.js.map +1 -1
  80. package/dist/license/renew.d.ts.map +1 -1
  81. package/dist/license/renew.js +2 -0
  82. package/dist/license/renew.js.map +1 -1
  83. package/dist/sdk-skills/kindgi-authoring-agents/SKILL.md +1 -1
  84. package/dist/sdk-skills/kindgi-authoring-flows/SKILL.md +1 -1
  85. package/dist/sdk-skills/kindgi-authoring-guardrails/SKILL.md +1 -1
  86. package/dist/sdk-skills/kindgi-authoring-mcp-servers/SKILL.md +1 -1
  87. package/dist/sdk-skills/kindgi-authoring-providers/SKILL.md +92 -39
  88. package/dist/sdk-skills/kindgi-authoring-tools/SKILL.md +51 -3
  89. package/dist/sdk-skills/kindgi-framework-feedback/SKILL.md +1 -1
  90. package/dist/sdk-skills/kindgi-getting-started/SKILL.md +23 -4
  91. package/dist/sdk-skills/kindgi-java-authoring-agents/SKILL.md +4 -2
  92. package/dist/sdk-skills/kindgi-java-authoring-flows/SKILL.md +1 -1
  93. package/dist/sdk-skills/kindgi-java-authoring-guardrails/SKILL.md +1 -1
  94. package/dist/sdk-skills/kindgi-java-authoring-tools/SKILL.md +1 -1
  95. package/dist/sdk-skills/kindgi-java-getting-started/SKILL.md +6 -3
  96. package/dist/sdk-skills/kindgi-python-authoring-agents/SKILL.md +1 -1
  97. package/dist/sdk-skills/kindgi-python-authoring-flows/SKILL.md +1 -1
  98. package/dist/sdk-skills/kindgi-python-authoring-guardrails/SKILL.md +1 -1
  99. package/dist/sdk-skills/kindgi-python-authoring-tools/SKILL.md +1 -1
  100. package/dist/sdk-skills/kindgi-python-getting-started/SKILL.md +6 -2
  101. package/dist/sdk-skills/kindgi-scala-authoring-agents/SKILL.md +4 -2
  102. package/dist/sdk-skills/kindgi-scala-authoring-flows/SKILL.md +1 -1
  103. package/dist/sdk-skills/kindgi-scala-authoring-guardrails/SKILL.md +1 -1
  104. package/dist/sdk-skills/kindgi-scala-authoring-tools/SKILL.md +1 -1
  105. package/dist/sdk-skills/kindgi-scala-getting-started/SKILL.md +6 -3
  106. package/dist/sdk-skills/kindgi-using/SKILL.md +1 -1
  107. package/dist/slug.d.ts +8 -1
  108. package/dist/slug.d.ts.map +1 -1
  109. package/dist/slug.js +8 -1
  110. package/dist/slug.js.map +1 -1
  111. package/package.json +13 -13
@@ -0,0 +1,791 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+ import { readFile, readdir, realpath } from 'node:fs/promises';
4
+ import { dirname, join, relative, resolve, sep } from 'node:path';
5
+ import { satisfies, validRange } from 'semver';
6
+ import { isMap, isScalar, parseDocument } from 'yaml';
7
+ import { checkReducedLockfile } from './loaded-install-check.js';
8
+ const PROD_FIELDS = ['dependencies', 'optionalDependencies'];
9
+ /** The command that brings node_modules back in line with the lockfile. */
10
+ export function reinstallCommand(manager) {
11
+ return manager === 'pnpm' ? 'pnpm install' : manager === 'npm' ? 'npm ci' : 'yarn install';
12
+ }
13
+ /** Cut the app's install down to the roots and what they depend on. */
14
+ export function reduceInstall(inputs) {
15
+ const devOnly = inputs.roots.find((root) => directDependency(inputs, root.name, false) === undefined &&
16
+ directDependency(inputs, root.name, true) !== undefined);
17
+ if (devOnly !== undefined) {
18
+ const manifest = inputs.packRel === '' ? 'package.json' : `${inputs.packRel}/package.json`;
19
+ return {
20
+ kind: 'err',
21
+ message: `The pack loads ${devOnly.name}, which ${manifest} lists only in devDependencies. The image keeps production dependencies only, so it wouldn't load there: move it to dependencies.`,
22
+ };
23
+ }
24
+ const outside = [...inputs.roots, ...inputs.tools].find((p) => p.folder.startsWith('..'));
25
+ if (outside !== undefined) {
26
+ return fallback(`${outside.name} is installed outside the project (${outside.folder})`);
27
+ }
28
+ switch (inputs.manager) {
29
+ case 'pnpm':
30
+ return reducePnpm(inputs);
31
+ case 'npm':
32
+ return reduceNpm(inputs);
33
+ case 'yarn':
34
+ return reduceYarnClassic(inputs);
35
+ }
36
+ }
37
+ function reducePnpm(inputs) {
38
+ const lockName = inputs.lockfile.name;
39
+ const parts = splitPnpmLockfile(inputs.lockfile.text);
40
+ if (parts === undefined)
41
+ return fallback(`${lockName} has more YAML documents than kindgi build knows`);
42
+ const doc = parseDocument(parts.project);
43
+ if (doc.errors.length > 0)
44
+ return fallback(`${lockName} doesn't parse as YAML`);
45
+ const lock = (doc.toJS() ?? {});
46
+ if (lock.lockfileVersion !== '9.0') {
47
+ return fallback(`${lockName} is lockfileVersion ${JSON.stringify(lock.lockfileVersion)}, and kindgi build cuts down only '9.0'`);
48
+ }
49
+ const importers = lock.importers ?? {};
50
+ const snapshots = lock.snapshots ?? {};
51
+ const packages = lock.packages ?? {};
52
+ const importerKeys = inputs.packRel === '' ? ['.'] : [inputs.packRel, '.'];
53
+ const roots = [];
54
+ for (const host of [...inputs.roots, ...inputs.tools]) {
55
+ const tool = !inputs.roots.includes(host);
56
+ const entry = pnpmRoot(host, importerKeys, importers, snapshots, tool);
57
+ if (entry.kind === 'stale')
58
+ return stale(inputs, host, undefined);
59
+ if (entry.kind === 'stop')
60
+ return entry.outcome;
61
+ if (pnpmVersion(entry.root.id) !== host.version)
62
+ return stale(inputs, host, pnpmVersion(entry.root.id));
63
+ if (!roots.some((r) => r.name === entry.root.name))
64
+ roots.push(entry.root);
65
+ }
66
+ // The closure: each snapshot's dependencies and optional dependencies, peers included.
67
+ const kept = new Set();
68
+ const todo = roots.map((r) => r.id);
69
+ for (let key = todo.pop(); key !== undefined; key = todo.pop()) {
70
+ if (kept.has(key))
71
+ continue;
72
+ const snapshot = snapshots[key];
73
+ if (snapshot === undefined || !Object.hasOwn(packages, withoutSuffix(key))) {
74
+ return fallback(`${lockName} has no entry for ${key}`);
75
+ }
76
+ kept.add(key);
77
+ for (const [name, ref] of [
78
+ ...Object.entries(snapshot?.dependencies ?? {}),
79
+ ...Object.entries(snapshot?.optionalDependencies ?? {}),
80
+ ]) {
81
+ if (isLocalRef(ref))
82
+ return localFallback(name, ref);
83
+ todo.push(pnpmKey(name, ref));
84
+ }
85
+ }
86
+ const keptPackages = new Set([...kept].map(withoutSuffix));
87
+ const patchKept = (selector) => [...keptPackages].some((key) => patchApplies(selector, key));
88
+ const importer = {};
89
+ for (const root of [...roots].sort((a, b) => compare(a.name, b.name))) {
90
+ (importer[root.field] ??= {})[root.name] = {
91
+ specifier: root.specifier,
92
+ version: root.ref ?? '',
93
+ };
94
+ }
95
+ doc.set('importers', doc.createNode({ '.': importer }));
96
+ keepItems(doc, 'packages', (key) => keptPackages.has(key));
97
+ keepItems(doc, 'snapshots', (key) => kept.has(key));
98
+ keepItems(doc, 'patchedDependencies', patchKept);
99
+ const lockText = `${parts.env ?? ''}${yamlText(doc)}`;
100
+ const written = {
101
+ 'package.json': manifestText(inputs, roots, (pnpmField) => trimPatches(pnpmField, patchKept)),
102
+ [lockName]: lockText,
103
+ };
104
+ const copied = new Set();
105
+ const workspace = inputs.workspaceText === undefined ? undefined : parseDocument(inputs.workspaceText);
106
+ if (workspace !== undefined) {
107
+ workspace.delete('packages');
108
+ for (const path of patchPaths(workspace.toJS()?.patchedDependencies, patchKept))
109
+ copied.add(path);
110
+ keepItems(workspace, 'patchedDependencies', patchKept);
111
+ if (isMap(workspace.contents) && workspace.contents.items.length > 0) {
112
+ // The image installs one project; pnpm 9 refuses a workspace file without packages.
113
+ workspace.set('packages', workspace.createNode(['.']));
114
+ const items = workspace.contents.items;
115
+ const added = items.pop();
116
+ if (added !== undefined)
117
+ items.unshift(added);
118
+ written['pnpm-workspace.yaml'] = yamlText(workspace);
119
+ }
120
+ }
121
+ const pnpmField = objectOf(inputs.rootManifest.pnpm);
122
+ for (const path of patchPaths(pnpmField.patchedDependencies, patchKept))
123
+ copied.add(path);
124
+ if (inputs.files.includes('.pnpmfile.cjs'))
125
+ copied.add('.pnpmfile.cjs');
126
+ const names = new Set([...keptPackages].map(pnpmName));
127
+ return reduced(inputs, written, copied, names, kept.size, Object.keys(snapshots).length);
128
+ }
129
+ /**
130
+ * A root's snapshot: the pack's importer lists it (or the root's, for a
131
+ * pack in a workspace member: Node reaches /app/node_modules from there),
132
+ * or, when neither does (a hoisted package), the installed folder names it.
133
+ */
134
+ function pnpmRoot(host, importerKeys, importers, snapshots, tool) {
135
+ for (const key of importerKeys) {
136
+ const importer = importers[key] ?? {};
137
+ const fields = tool ? [...PROD_FIELDS, 'devDependencies'] : PROD_FIELDS;
138
+ for (const field of fields) {
139
+ const dep = importer[field]?.[host.name];
140
+ if (dep === undefined)
141
+ continue;
142
+ if (isLocalRef(dep.version))
143
+ return { kind: 'stop', outcome: localFallback(host.name, dep.version) };
144
+ return {
145
+ kind: 'ok',
146
+ root: {
147
+ name: host.name,
148
+ field,
149
+ specifier: dep.specifier,
150
+ id: pnpmKey(host.name, dep.version),
151
+ ref: dep.version,
152
+ },
153
+ };
154
+ }
155
+ }
156
+ const folder = /(?:^|\/)node_modules\/\.pnpm\/([^/]+)\/node_modules\//.exec(host.folder)?.[1];
157
+ const candidates = Object.keys(snapshots).filter((key) => pnpmName(key) === host.name && pnpmVersion(key) === host.version);
158
+ const named = folder === undefined ? [] : candidates.filter((key) => pnpmFolderName(key) === folder);
159
+ const key = named.length === 1 ? named[0] : candidates.length === 1 ? candidates[0] : undefined;
160
+ if (key === undefined) {
161
+ return candidates.length === 0
162
+ ? { kind: 'stale' }
163
+ : {
164
+ kind: 'stop',
165
+ outcome: fallback(`${host.name} ${host.version} is in the lockfile more than once, and its folder doesn't say which`),
166
+ };
167
+ }
168
+ const ref = key.slice(host.name.length + 1);
169
+ return {
170
+ kind: 'ok',
171
+ root: { name: host.name, field: 'dependencies', specifier: withoutSuffix(ref), id: key, ref },
172
+ };
173
+ }
174
+ /** pnpm 11 and later can write an env document before the project's: kept as is. */
175
+ function splitPnpmLockfile(text) {
176
+ const starts = [...text.matchAll(/^---$/gm)].map((m) => m.index);
177
+ if (starts.length === 0 || (starts.length === 1 && starts[0] === 0))
178
+ return { project: text };
179
+ if (starts.length === 2 && starts[0] === 0) {
180
+ const second = starts[1];
181
+ return { env: text.slice(0, second), project: text.slice(second) };
182
+ }
183
+ return undefined;
184
+ }
185
+ /** A dependency's snapshot key: `name@version`, or the version itself when it's an alias (`string-width@4.2.3`). */
186
+ function pnpmKey(name, ref) {
187
+ return !/^[a-z+]+:/.test(ref) && withoutSuffix(ref).indexOf('@', 1) > 0 ? ref : `${name}@${ref}`;
188
+ }
189
+ function pnpmName(key) {
190
+ return key.slice(0, key.indexOf('@', 1));
191
+ }
192
+ /** The version part of a snapshot key, without its peer and patch suffixes. */
193
+ function pnpmVersion(key) {
194
+ return withoutSuffix(key).slice(key.indexOf('@', 1) + 1);
195
+ }
196
+ /** `react-dom@18.3.1(react@18.3.1)` → `react-dom@18.3.1`. */
197
+ function withoutSuffix(key) {
198
+ const paren = key.indexOf('(');
199
+ return paren === -1 ? key : key.slice(0, paren);
200
+ }
201
+ /** pnpm's virtual-store folder for a snapshot key, when it isn't shortened with a hash. */
202
+ function pnpmFolderName(key) {
203
+ let name = key.replace(/[\\/:*?"<>|]/g, '+');
204
+ if (name.includes('('))
205
+ name = name.replace(/\)$/, '').replace(/\)\(|\(|\)/g, '_');
206
+ return name;
207
+ }
208
+ /** Whether a patch selector (`name`, `name@1.2.3`, `name@^1`) applies to a package key (`name@1.2.3`). */
209
+ function patchApplies(selector, packageKey) {
210
+ const at = selector.indexOf('@', 1);
211
+ const name = at === -1 ? selector : selector.slice(0, at);
212
+ if (pnpmName(packageKey) !== name)
213
+ return false;
214
+ if (at === -1)
215
+ return true;
216
+ const wanted = selector.slice(at + 1);
217
+ const version = pnpmVersion(packageKey);
218
+ return wanted === version || (validRange(wanted) !== null && satisfies(version, wanted));
219
+ }
220
+ function patchPaths(patches, keep) {
221
+ return Object.entries(objectOf(patches))
222
+ .filter(([selector]) => keep(selector))
223
+ .map(([, value]) => (typeof value === 'string' ? value : objectOf(value).path))
224
+ .filter((path) => typeof path === 'string');
225
+ }
226
+ function trimPatches(pnpmField, keep) {
227
+ const patches = pnpmField.patchedDependencies;
228
+ if (patches === undefined)
229
+ return pnpmField;
230
+ const kept = Object.fromEntries(Object.entries(objectOf(patches)).filter(([selector]) => keep(selector)));
231
+ const { patchedDependencies: _, ...rest } = pnpmField;
232
+ return Object.keys(kept).length > 0 ? { ...rest, patchedDependencies: kept } : rest;
233
+ }
234
+ function reduceNpm(inputs) {
235
+ const lockName = inputs.lockfile.name;
236
+ let lock;
237
+ try {
238
+ lock = JSON.parse(inputs.lockfile.text);
239
+ }
240
+ catch {
241
+ return fallback(`${lockName} isn't JSON`);
242
+ }
243
+ if (lock.lockfileVersion !== 2 && lock.lockfileVersion !== 3) {
244
+ return fallback(`${lockName} is lockfileVersion ${String(lock.lockfileVersion)}, and kindgi build cuts down only 2 and 3`);
245
+ }
246
+ const entries = lock.packages ?? {};
247
+ const roots = [];
248
+ for (const host of [...inputs.roots, ...inputs.tools]) {
249
+ const entry = entries[host.folder];
250
+ if (entry?.link === true || isLocalRef(entry?.resolved ?? '')) {
251
+ return localFallback(host.name, entry?.resolved ?? 'a link');
252
+ }
253
+ if (entry === undefined || entry.version !== host.version)
254
+ return stale(inputs, host, entry?.version);
255
+ const tool = !inputs.roots.includes(host);
256
+ const direct = directDependency(inputs, host.name, tool);
257
+ if (!roots.some((r) => r.name === host.name)) {
258
+ roots.push({
259
+ name: host.name,
260
+ field: direct?.field ?? 'dependencies',
261
+ specifier: direct?.specifier ?? entry.version,
262
+ id: host.folder,
263
+ });
264
+ }
265
+ }
266
+ // The closure, resolved as Node does: each package's own node_modules, then its parents'.
267
+ const reach = (from) => {
268
+ const seen = new Set();
269
+ const todo = from.map((r) => r.id);
270
+ for (let path = todo.pop(); path !== undefined; path = todo.pop()) {
271
+ if (seen.has(path))
272
+ continue;
273
+ const entry = entries[path];
274
+ if (entry === undefined)
275
+ return fallback(`${lockName} has no entry for ${path}`);
276
+ if (entry.link === true || isLocalRef(entry.resolved ?? '')) {
277
+ return localFallback(packageNameOf(path), entry.resolved ?? 'a link');
278
+ }
279
+ seen.add(path);
280
+ const wanted = [
281
+ ...Object.keys(entry.dependencies ?? {}).map((name) => [name, true]),
282
+ ...Object.keys(entry.optionalDependencies ?? {}).map((name) => [name, false]),
283
+ ...Object.keys(entry.peerDependencies ?? {}).map((name) => [name, false]),
284
+ ];
285
+ for (const [name, required] of wanted) {
286
+ const found = npmResolve(entries, path, name);
287
+ if (found !== undefined)
288
+ todo.push(found);
289
+ else if (required)
290
+ return fallback(`${lockName} has no ${name} for ${path}`);
291
+ }
292
+ }
293
+ return seen;
294
+ };
295
+ const prod = reach(roots.filter((r) => r.field !== 'devDependencies'));
296
+ if (!(prod instanceof Set))
297
+ return prod;
298
+ const all = reach(roots);
299
+ if (!(all instanceof Set))
300
+ return all;
301
+ // A pack in a workspace member: its own nested packages move to the root's node_modules.
302
+ const memberPrefix = inputs.packRel === '' ? undefined : `${inputs.packRel}/`;
303
+ const moved = new Map();
304
+ for (const path of all) {
305
+ const to = memberPrefix !== undefined && path.startsWith(memberPrefix)
306
+ ? path.slice(memberPrefix.length)
307
+ : path;
308
+ const already = [...moved.entries()].find(([, target]) => target === to);
309
+ if (already !== undefined) {
310
+ return fallback(`${packageNameOf(to)} is installed twice for the pack (${already[0]} and ${path})`);
311
+ }
312
+ moved.set(path, to);
313
+ }
314
+ const root = entries[''] ?? {};
315
+ const packagesOut = {
316
+ '': {
317
+ ...(typeof root.name === 'string' && { name: root.name }),
318
+ ...(typeof root.version === 'string' && { version: root.version }),
319
+ ...dependencyFields(roots),
320
+ },
321
+ };
322
+ for (const [from, to] of [...moved.entries()].sort(([, a], [, b]) => compare(a, b))) {
323
+ const { dev: _dev, devOptional: _devOptional, ...entry } = entries[from];
324
+ packagesOut[to] = prod.has(from) ? entry : { ...entry, dev: true };
325
+ }
326
+ // lockfileVersion 2 also has the npm 6 tree (`dependencies`): the cut-down one has `packages` only.
327
+ const { packages: _, dependencies: _legacy, ...header } = lock;
328
+ const written = {
329
+ 'package.json': manifestText(inputs, roots),
330
+ [lockName]: `${JSON.stringify({ ...header, packages: packagesOut }, null, 2)}\n`,
331
+ };
332
+ const total = Object.entries(entries).filter(([path, e]) => path.includes('node_modules/') && e.link !== true).length;
333
+ const names = new Set([...moved.values()].map(packageNameOf));
334
+ return reduced(inputs, written, new Set(), names, all.size, total);
335
+ }
336
+ /** Where Node finds `name` from the package at `from`, by the lockfile's paths. */
337
+ function npmResolve(entries, from, name) {
338
+ for (let dir = from;; dir = dir.includes('/') ? dir.slice(0, dir.lastIndexOf('/')) : '') {
339
+ const path = dir === '' ? `node_modules/${name}` : `${dir}/node_modules/${name}`;
340
+ if (entries[path] !== undefined)
341
+ return path;
342
+ if (dir === '')
343
+ return undefined;
344
+ }
345
+ }
346
+ /** `node_modules/a/node_modules/@scope/b` → `@scope/b`. */
347
+ function packageNameOf(path) {
348
+ return path.slice(path.lastIndexOf('node_modules/') + 'node_modules/'.length);
349
+ }
350
+ function reduceYarnClassic(inputs) {
351
+ const lockName = inputs.lockfile.name;
352
+ const text = inputs.lockfile.text;
353
+ if (!/^# yarn lockfile v1$/m.test(text))
354
+ return fallback(`${lockName} isn't a yarn 1 lockfile kindgi build knows`);
355
+ const blocks = parseYarnLockfile(text);
356
+ const byKey = new Map();
357
+ for (const block of blocks)
358
+ for (const key of block.keys)
359
+ byKey.set(key, block);
360
+ const roots = [];
361
+ for (const host of [...inputs.roots, ...inputs.tools]) {
362
+ const block = blocks.find((b) => b.version === host.version && b.keys.some((k) => yarnName(k) === host.name));
363
+ if (block === undefined)
364
+ return stale(inputs, host, undefined);
365
+ if (isLocalRef(block.resolved) ||
366
+ block.keys.some((k) => isLocalRef(k.slice(yarnName(k).length + 1)))) {
367
+ return localFallback(host.name, block.resolved);
368
+ }
369
+ const tool = !inputs.roots.includes(host);
370
+ const direct = directDependency(inputs, host.name, tool);
371
+ const specifier = direct?.specifier ??
372
+ (block.keys.find((k) => yarnName(k) === host.name) ?? '').slice(host.name.length + 1);
373
+ if (!block.keys.includes(`${host.name}@${specifier}`)) {
374
+ return {
375
+ kind: 'err',
376
+ message: `${direct?.manifest ?? 'package.json'} lists ${host.name}@${specifier}, and ${lockName} has no entry for it: the lockfile is out of date with it. Run ${reinstallCommand(inputs.manager)}, then kindgi build again.`,
377
+ };
378
+ }
379
+ if (!roots.some((r) => r.name === host.name)) {
380
+ roots.push({
381
+ name: host.name,
382
+ field: direct?.field ?? 'dependencies',
383
+ specifier,
384
+ id: `${host.name}@${specifier}`,
385
+ });
386
+ }
387
+ }
388
+ // What the app's production install has: the projects' production dependencies and theirs.
389
+ const production = new Set();
390
+ const manifests = inputs.packRel === '' ? [inputs.rootManifest] : [inputs.packManifest, inputs.rootManifest];
391
+ const reach = manifests.flatMap((m) => PROD_FIELDS.flatMap((field) => Object.entries(objectOf(m[field])).map(([name, range]) => `${name}@${String(range)}`)));
392
+ for (let key = reach.pop(); key !== undefined; key = reach.pop()) {
393
+ const block = byKey.get(key);
394
+ if (block === undefined || production.has(block))
395
+ continue;
396
+ production.add(block);
397
+ reach.push(...block.dependencies.map(([name, range]) => `${name}@${range}`));
398
+ }
399
+ const kept = new Set();
400
+ const todo = roots.map((r) => r.id);
401
+ for (let key = todo.pop(); key !== undefined; key = todo.pop()) {
402
+ const block = byKey.get(key);
403
+ if (block === undefined)
404
+ return fallback(`${lockName} has no entry for ${key}`);
405
+ if (kept.has(block))
406
+ continue;
407
+ if (isLocalRef(block.resolved))
408
+ return localFallback(yarnName(key), block.resolved);
409
+ kept.add(block);
410
+ for (const [name, range, required] of block.dependencies) {
411
+ const dep = `${name}@${range}`;
412
+ if (byKey.has(dep) || required)
413
+ todo.push(dep);
414
+ }
415
+ // yarn 1 installs only what the manifest reaches, peers not included: a
416
+ // peer the app's production install has becomes a dependency of its own.
417
+ for (const peer of inputs.peers?.[`${yarnName(key)}@${block.version}`] ?? []) {
418
+ const target = blocks.find((b) => b.version === peer.version && b.keys.some((k) => yarnName(k) === peer.name));
419
+ if (target === undefined)
420
+ return stale(inputs, peer, undefined);
421
+ if (!production.has(target) || roots.some((r) => r.name === peer.name))
422
+ continue;
423
+ const peerKey = target.keys.find((k) => yarnName(k) === peer.name) ?? '';
424
+ roots.push({
425
+ name: peer.name,
426
+ field: 'dependencies',
427
+ specifier: peerKey.slice(peer.name.length + 1),
428
+ id: peerKey,
429
+ });
430
+ todo.push(peerKey);
431
+ }
432
+ }
433
+ const header = text.split('\n').filter((line) => line.startsWith('#'));
434
+ const body = blocks.filter((b) => kept.has(b)).map((b) => b.text);
435
+ const written = {
436
+ 'package.json': manifestText(inputs, roots),
437
+ [lockName]: `${header.join('\n')}\n\n\n${body.join('\n\n')}\n`,
438
+ };
439
+ const names = new Set([...kept].flatMap((block) => block.keys.map(yarnName)));
440
+ return reduced(inputs, written, new Set(), names, kept.size, blocks.length);
441
+ }
442
+ /** The blocks of a yarn 1 lockfile: `"a@^1", a@~1:`, its fields, its dependencies. */
443
+ function parseYarnLockfile(text) {
444
+ const blocks = [];
445
+ for (const chunk of text.split(/\n\s*\n/)) {
446
+ const lines = chunk.split('\n').filter((line) => line !== '' && !line.startsWith('#'));
447
+ const first = lines[0];
448
+ if (first === undefined || first.startsWith(' '))
449
+ continue;
450
+ const keys = first
451
+ .replace(/:$/, '')
452
+ .split(/,\s*/)
453
+ .map((key) => unquote(key.trim()));
454
+ let version = '';
455
+ let resolved = '';
456
+ let section;
457
+ const dependencies = [];
458
+ for (const line of lines.slice(1)) {
459
+ if (line.startsWith(' ') && section !== undefined) {
460
+ const match = /^\s+("[^"]+"|\S+)\s+(.+)$/.exec(line);
461
+ if (match !== null) {
462
+ dependencies.push([
463
+ unquote(match[1] ?? ''),
464
+ unquote(match[2] ?? ''),
465
+ section === 'dependencies',
466
+ ]);
467
+ }
468
+ continue;
469
+ }
470
+ section = undefined;
471
+ const field = /^ {2}(\S+)\s*(.*)$/.exec(line);
472
+ if (field === null)
473
+ continue;
474
+ if (field[1] === 'version')
475
+ version = unquote(field[2] ?? '');
476
+ else if (field[1] === 'resolved')
477
+ resolved = unquote(field[2] ?? '');
478
+ else if (field[1] === 'dependencies:' || field[1] === 'optionalDependencies:') {
479
+ section = field[1].slice(0, -1);
480
+ }
481
+ }
482
+ blocks.push({ text: lines.join('\n'), keys, version, resolved, dependencies });
483
+ }
484
+ return blocks;
485
+ }
486
+ function yarnName(key) {
487
+ return key.slice(0, key.indexOf('@', 1));
488
+ }
489
+ function unquote(value) {
490
+ return value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
491
+ }
492
+ // ---------------------------------------------------------------------
493
+ // Shared
494
+ // ---------------------------------------------------------------------
495
+ /** How the pack's project (or, for a pack in a member, the root's) lists `name`: a production field, or a dev one for a tool. */
496
+ function directDependency(inputs, name, tool) {
497
+ const manifests = inputs.packRel === ''
498
+ ? [['package.json', inputs.rootManifest]]
499
+ : [
500
+ [`${inputs.packRel}/package.json`, inputs.packManifest],
501
+ ['package.json', inputs.rootManifest],
502
+ ];
503
+ const fields = tool ? [...PROD_FIELDS, 'devDependencies'] : PROD_FIELDS;
504
+ for (const [path, manifest] of manifests) {
505
+ for (const field of fields) {
506
+ const specifier = objectOf(manifest[field])[name];
507
+ if (typeof specifier === 'string')
508
+ return { field, specifier, manifest: path };
509
+ }
510
+ }
511
+ return undefined;
512
+ }
513
+ /** The cut-down `package.json`: the roots, and the root manifest's settings the install reads. */
514
+ function manifestText(inputs, roots, pnpmField = (field) => field) {
515
+ const root = inputs.rootManifest;
516
+ const settings = inputs.manager === 'pnpm' ? 'pnpm' : inputs.manager === 'npm' ? 'overrides' : 'resolutions';
517
+ const setting = root[settings];
518
+ const manifest = {
519
+ ...(typeof root.name === 'string' && { name: root.name }),
520
+ ...(typeof root.version === 'string' && { version: root.version }),
521
+ private: true,
522
+ ...(typeof root.packageManager === 'string' && { packageManager: root.packageManager }),
523
+ ...(setting !== undefined && {
524
+ [settings]: settings === 'pnpm' ? pnpmField(objectOf(setting)) : setting,
525
+ }),
526
+ ...dependencyFields(roots),
527
+ };
528
+ return `${JSON.stringify(manifest, null, 2)}\n`;
529
+ }
530
+ function dependencyFields(roots) {
531
+ const fields = {};
532
+ for (const root of [...roots].sort((a, b) => compare(a.name, b.name))) {
533
+ (fields[root.field] ??= {})[root.name] = root.specifier;
534
+ }
535
+ return fields;
536
+ }
537
+ function reduced(inputs, written, copied, names, kept, total) {
538
+ if (inputs.packRel !== '')
539
+ copied.add(`${inputs.packRel}/package.json`);
540
+ const own = objectOf((inputs.packRel === '' ? inputs.rootManifest : inputs.packManifest).dependencies);
541
+ const leftOut = Object.keys(own)
542
+ .filter((name) => !names.has(name))
543
+ .sort(compare);
544
+ return {
545
+ kind: 'reduced',
546
+ reduced: { written, copied: [...copied].sort(), kept, total, leftOut },
547
+ };
548
+ }
549
+ /** A package the host has at a version the lockfile doesn't pin for it. */
550
+ function stale(inputs, host, locked) {
551
+ const pinned = locked === undefined
552
+ ? `${inputs.lockfile.name} has no ${host.name} ${host.version}`
553
+ : `${inputs.lockfile.name} pins ${locked}`;
554
+ return {
555
+ kind: 'err',
556
+ message: `${host.name} is installed at ${host.folder} (${host.version}), but ${pinned}: node_modules is out of date with the lockfile, so the build would use code the lockfile doesn't pin. Run ${reinstallCommand(inputs.manager)}, then kindgi build again.`,
557
+ };
558
+ }
559
+ function localFallback(name, ref) {
560
+ return fallback(`${name} is a local package (${ref})`);
561
+ }
562
+ function fallback(reason) {
563
+ return { kind: 'fallback', reason };
564
+ }
565
+ function isLocalRef(ref) {
566
+ return /^(link|file|workspace|portal):/.test(ref);
567
+ }
568
+ /** Keep a top-level map's entries whose key passes; drop the map when none does. */
569
+ function keepItems(doc, key, keep) {
570
+ const node = doc.get(key, true);
571
+ if (!isMap(node))
572
+ return;
573
+ node.items = node.items.filter((pair) => keep(String(isScalar(pair.key) ? pair.key.value : pair.key)));
574
+ if (node.items.length === 0)
575
+ doc.delete(key);
576
+ }
577
+ /** A YAML document as text, its first line never blank (the blank before a dropped `packages:` stays otherwise). */
578
+ function yamlText(doc) {
579
+ return doc.toString({ lineWidth: 0, flowCollectionPadding: false }).replace(/^\n+/, '');
580
+ }
581
+ function objectOf(value) {
582
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
583
+ ? value
584
+ : {};
585
+ }
586
+ function compare(a, b) {
587
+ return a < b ? -1 : a > b ? 1 : 0;
588
+ }
589
+ // ---------------------------------------------------------------------
590
+ // The host
591
+ // ---------------------------------------------------------------------
592
+ /** Where Node finds `name` from `fromDir`, up to the install root: its real folder and version. */
593
+ export async function findHostPackage(root, fromDir, name) {
594
+ const top = resolve(root);
595
+ for (let dir = resolve(fromDir);; dir = dirname(dir)) {
596
+ const folder = join(dir, 'node_modules', name);
597
+ const manifest = await readJson(join(folder, 'package.json'));
598
+ if (manifest !== undefined) {
599
+ const real = relative(await realpath(top), await realpath(folder))
600
+ .split(sep)
601
+ .join('/');
602
+ return {
603
+ name,
604
+ folder: real,
605
+ version: typeof manifest.version === 'string' ? manifest.version : '',
606
+ };
607
+ }
608
+ if (dir === top || dirname(dir) === dir)
609
+ return undefined;
610
+ }
611
+ }
612
+ /**
613
+ * With `image.dependencies: 'loaded'`: the app's install, cut down to what
614
+ * the pack loads, checked against the app's lockfile; or why the image
615
+ * takes the full install and trims it.
616
+ */
617
+ export async function loadedInstall(opts) {
618
+ const { install, packDir } = opts;
619
+ if (install.manager === 'yarn' && install.yarnBerry) {
620
+ return {
621
+ kind: 'fallback',
622
+ reason: "yarn berry's lockfile can't be cut down by hand and still install immutably",
623
+ };
624
+ }
625
+ const lockName = install.files.find((file) => LOCKFILE_NAMES.includes(file));
626
+ if (lockName === undefined)
627
+ return { kind: 'fallback', reason: 'the install has no lockfile kindgi build can cut down' };
628
+ const rootManifest = await readJson(join(install.root, 'package.json'));
629
+ if (rootManifest === undefined)
630
+ return { kind: 'err', message: `${join(install.root, 'package.json')} isn't JSON.` };
631
+ const packManifest = (await readJson(join(packDir, 'package.json'))) ?? {};
632
+ const roots = [];
633
+ const required = [
634
+ ...new Set([...opts.externals, ...opts.image.keep.filter(isPackageName)]),
635
+ ].sort();
636
+ for (const name of required) {
637
+ const host = await findHostPackage(install.root, packDir, name);
638
+ if (host === undefined) {
639
+ return {
640
+ kind: 'err',
641
+ message: `The pack loads ${name}, but Node doesn't find it from ${packDir}, where the image loads it from: add it to the dependencies of ${join(packDir, 'package.json')}, run ${reinstallCommand(install.manager)}, then kindgi build again.`,
642
+ };
643
+ }
644
+ roots.push(host);
645
+ }
646
+ for (const name of opts.image.keepIfInstalled.filter(isPackageName)) {
647
+ const host = await findHostPackage(install.root, packDir, name);
648
+ if (host !== undefined && !roots.some((r) => r.name === name))
649
+ roots.push(host);
650
+ }
651
+ const tools = [];
652
+ for (const bin of [...new Set(opts.image.steps.map((step) => step.bin))]) {
653
+ const tool = await stepTool(install, packDir, rootManifest, packManifest, bin);
654
+ if (tool === undefined) {
655
+ return { kind: 'fallback', reason: `no dependency of the pack has the build step's ${bin}` };
656
+ }
657
+ if (![...roots, ...tools].some((p) => p.name === tool.name))
658
+ tools.push(tool);
659
+ }
660
+ const peers = install.manager === 'yarn' ? await installedPeers(install) : undefined;
661
+ const lockText = await readFile(join(install.root, lockName), 'utf8');
662
+ const workspaceText = install.files.includes('pnpm-workspace.yaml')
663
+ ? await readFile(join(install.root, 'pnpm-workspace.yaml'), 'utf8')
664
+ : undefined;
665
+ const outcome = reduceInstall({
666
+ manager: install.manager,
667
+ packRel: install.packRel,
668
+ files: install.files,
669
+ lockfile: { name: lockName, text: lockText },
670
+ rootManifest,
671
+ packManifest,
672
+ ...(workspaceText !== undefined && { workspaceText }),
673
+ roots,
674
+ tools,
675
+ ...(peers !== undefined && { peers }),
676
+ });
677
+ if (outcome.kind !== 'reduced')
678
+ return outcome;
679
+ const reducedLock = outcome.reduced.written[lockName] ?? '';
680
+ const check = checkReducedLockfile({
681
+ manager: install.manager,
682
+ packRel: install.packRel,
683
+ lockfile: lockText,
684
+ reduced: reducedLock,
685
+ manifest: outcome.reduced.written['package.json'] ?? '',
686
+ rootManifest,
687
+ packManifest,
688
+ });
689
+ if (check.kind === 'refused') {
690
+ return {
691
+ kind: 'err',
692
+ message: `kindgi build cut ${lockName} down to what the pack loads, and the result doesn't match the app's lockfile: ${check.message}. Build with image.dependencies: 'all' meanwhile, and report this.`,
693
+ };
694
+ }
695
+ const files = [
696
+ ...new Set([...Object.keys(outcome.reduced.written), ...outcome.reduced.copied]),
697
+ ].sort();
698
+ return {
699
+ kind: 'reduced',
700
+ install: {
701
+ ...install,
702
+ workspace: false,
703
+ files,
704
+ projectManifests: install.projectManifests.filter((file) => outcome.reduced.copied.includes(file)),
705
+ reduced: outcome.reduced,
706
+ },
707
+ };
708
+ }
709
+ const LOCKFILE_NAMES = [
710
+ 'pnpm-lock.yaml',
711
+ 'package-lock.json',
712
+ 'npm-shrinkwrap.json',
713
+ 'yarn.lock',
714
+ ];
715
+ /** A keep that's a package, not a folder a step writes (`.prisma/client`). */
716
+ function isPackageName(name) {
717
+ return !name.startsWith('.');
718
+ }
719
+ /** The pack's dependency whose `bin` has `bin`: the tool a build step runs. */
720
+ async function stepTool(install, packDir, rootManifest, packManifest, bin) {
721
+ const manifests = install.packRel === '' ? [rootManifest] : [packManifest, rootManifest];
722
+ const names = [
723
+ ...new Set(manifests.flatMap((m) => ['dependencies', 'optionalDependencies', 'devDependencies'].flatMap((f) => Object.keys(objectOf(m[f]))))),
724
+ ].sort();
725
+ for (const name of names) {
726
+ const host = await findHostPackage(install.root, packDir, name);
727
+ if (host === undefined)
728
+ continue;
729
+ const manifest = await readJson(join(install.root, host.folder, 'package.json'));
730
+ const bins = manifest?.bin;
731
+ const has = typeof bins === 'string'
732
+ ? (name.split('/').pop() ?? name) === bin
733
+ : Object.hasOwn(objectOf(bins), bin);
734
+ if (has)
735
+ return host;
736
+ }
737
+ return undefined;
738
+ }
739
+ /**
740
+ * Every installed package's peer dependencies, resolved where Node finds
741
+ * them from the package's folder, by `name@version`: what yarn 1's
742
+ * lockfile leaves out. Links (workspace members) aren't followed.
743
+ */
744
+ async function installedPeers(install) {
745
+ const peers = {};
746
+ const projects = install.projectManifests.map((manifest) => dirname(join(install.root, manifest)));
747
+ const todo = projects.map((dir) => join(dir, 'node_modules'));
748
+ for (let dir = todo.pop(); dir !== undefined; dir = todo.pop()) {
749
+ let entries;
750
+ try {
751
+ entries = await readdir(dir, { withFileTypes: true });
752
+ }
753
+ catch {
754
+ continue;
755
+ }
756
+ for (const entry of entries) {
757
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
758
+ continue;
759
+ const folder = join(dir, entry.name);
760
+ if (entry.name.startsWith('@')) {
761
+ todo.push(folder);
762
+ continue;
763
+ }
764
+ todo.push(join(folder, 'node_modules'));
765
+ const manifest = await readJson(join(folder, 'package.json'));
766
+ const names = Object.keys(objectOf(manifest?.peerDependencies));
767
+ if (manifest === undefined || names.length === 0 || typeof manifest.version !== 'string')
768
+ continue;
769
+ const found = [];
770
+ for (const name of names) {
771
+ const host = await findHostPackage(install.root, folder, name);
772
+ if (host !== undefined)
773
+ found.push(host);
774
+ }
775
+ peers[`${String(manifest.name)}@${manifest.version}`] = found;
776
+ }
777
+ }
778
+ return peers;
779
+ }
780
+ async function readJson(path) {
781
+ try {
782
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
783
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
784
+ ? parsed
785
+ : undefined;
786
+ }
787
+ catch {
788
+ return undefined;
789
+ }
790
+ }
791
+ //# sourceMappingURL=loaded-install.js.map