@jskit-ai/jskit-cli 0.2.27 → 0.2.28

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 (59) hide show
  1. package/package.json +3 -2
  2. package/src/server/cliRuntime/appState.js +226 -0
  3. package/src/server/cliRuntime/capabilitySupport.js +194 -0
  4. package/src/server/cliRuntime/descriptorValidation.js +150 -0
  5. package/src/server/cliRuntime/ioAndMigrations.js +381 -0
  6. package/src/server/cliRuntime/localPackageSupport.js +390 -0
  7. package/src/server/cliRuntime/mutationApplication.js +9 -0
  8. package/src/server/cliRuntime/mutationWhen.js +285 -0
  9. package/src/server/cliRuntime/mutations/fileMutations.js +247 -0
  10. package/src/server/cliRuntime/mutations/installMigrationMutation.js +213 -0
  11. package/src/server/cliRuntime/mutations/mutationPathUtils.js +12 -0
  12. package/src/server/cliRuntime/mutations/surfaceTargets.js +155 -0
  13. package/src/server/cliRuntime/mutations/templateContext.js +171 -0
  14. package/src/server/cliRuntime/mutations/textMutations.js +250 -0
  15. package/src/server/cliRuntime/packageInstallFlow.js +489 -0
  16. package/src/server/cliRuntime/packageIntrospection/exportEntries.js +259 -0
  17. package/src/server/cliRuntime/packageIntrospection/exportedSymbols.js +216 -0
  18. package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +98 -0
  19. package/src/server/cliRuntime/packageIntrospection/providerBindingIntrospection.js +377 -0
  20. package/src/server/cliRuntime/packageIntrospection.js +137 -0
  21. package/src/server/cliRuntime/packageOptions.js +299 -0
  22. package/src/server/cliRuntime/packageRegistries.js +343 -0
  23. package/src/server/cliRuntime/packageTemplateResolution.js +131 -0
  24. package/src/server/cliRuntime/viteProxy.js +356 -0
  25. package/src/server/commandHandlers/health.js +292 -0
  26. package/src/server/commandHandlers/list.js +292 -0
  27. package/src/server/commandHandlers/package.js +23 -0
  28. package/src/server/commandHandlers/packageCommands/add.js +282 -0
  29. package/src/server/commandHandlers/packageCommands/create.js +155 -0
  30. package/src/server/commandHandlers/packageCommands/generate.js +116 -0
  31. package/src/server/commandHandlers/packageCommands/migrations.js +155 -0
  32. package/src/server/commandHandlers/packageCommands/position.js +103 -0
  33. package/src/server/commandHandlers/packageCommands/remove.js +181 -0
  34. package/src/server/commandHandlers/packageCommands/update.js +40 -0
  35. package/src/server/commandHandlers/shared.js +314 -0
  36. package/src/server/commandHandlers/show/payloads.js +92 -0
  37. package/src/server/commandHandlers/show/renderBundleText.js +16 -0
  38. package/src/server/commandHandlers/show/renderHelpers.js +82 -0
  39. package/src/server/commandHandlers/show/renderPackageCapabilities.js +124 -0
  40. package/src/server/commandHandlers/show/renderPackageExports.js +203 -0
  41. package/src/server/commandHandlers/show/renderPackageText.js +332 -0
  42. package/src/server/commandHandlers/show.js +114 -0
  43. package/src/server/core/argParser.js +144 -0
  44. package/src/server/{runtimeDeps.js → core/buildCommandDeps.js} +2 -1
  45. package/src/server/core/commandCatalog.js +47 -0
  46. package/src/server/core/createCliRunner.js +150 -0
  47. package/src/server/core/createCommandHandlers.js +43 -0
  48. package/src/server/{runCli.js → core/dispatchCli.js} +14 -1
  49. package/src/server/core/usageHelp.js +344 -0
  50. package/src/server/index.js +1 -1
  51. package/src/server/{optionInterpolation.js → shared/optionInterpolation.js} +12 -1
  52. package/src/server/{pathResolution.js → shared/pathResolution.js} +1 -1
  53. package/src/server/argParser.js +0 -206
  54. package/src/server/cliRuntime.js +0 -4956
  55. package/src/server/commandHandlers.js +0 -2109
  56. /package/src/server/{cliError.js → shared/cliError.js} +0 -0
  57. /package/src/server/{collectionUtils.js → shared/collectionUtils.js} +0 -0
  58. /package/src/server/{outputFormatting.js → shared/outputFormatting.js} +0 -0
  59. /package/src/server/{packageIdHelpers.js → shared/packageIdHelpers.js} +0 -0
@@ -1,4956 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import {
4
- access,
5
- constants as fsConstants,
6
- mkdtemp,
7
- mkdir,
8
- readFile,
9
- readdir,
10
- rm,
11
- writeFile
12
- } from "node:fs/promises";
13
- import { tmpdir } from "node:os";
14
- import path from "node:path";
15
- import process from "node:process";
16
- import { pathToFileURL } from "node:url";
17
- import { createCliError } from "./cliError.js";
18
- import {
19
- ensureArray,
20
- ensureObject,
21
- sortStrings
22
- } from "./collectionUtils.js";
23
- import {
24
- createColorFormatter,
25
- resolveWrapWidth,
26
- writeWrappedItems
27
- } from "./outputFormatting.js";
28
- import {
29
- BUNDLES_ROOT,
30
- CATALOG_PACKAGES_PATH,
31
- CLI_PACKAGE_ROOT
32
- } from "./pathResolution.js";
33
- import {
34
- appendTextSnippet,
35
- escapeRegExp,
36
- interpolateOptionValue,
37
- normalizeSkipChecks,
38
- promptForRequiredOption
39
- } from "./optionInterpolation.js";
40
- import { createCommandHandlers } from "./commandHandlers.js";
41
- import { parseArgs, printUsage } from "./argParser.js";
42
- import { createCommandHandlerDeps } from "./runtimeDeps.js";
43
- import { createRunCli } from "./runCli.js";
44
- import {
45
- toScopedPackageId,
46
- resolvePackageIdInput,
47
- resolveInstalledPackageIdInput
48
- } from "./packageIdHelpers.js";
49
-
50
- const LOCK_RELATIVE_PATH = ".jskit/lock.json";
51
- const LOCK_VERSION = 1;
52
- const VITE_DEV_PROXY_CONFIG_RELATIVE_PATH = ".jskit/vite.dev.proxy.json";
53
- const VITE_DEV_PROXY_CONFIG_VERSION = 1;
54
- const PUBLIC_APP_CONFIG_RELATIVE_PATH = "config/public.js";
55
- const SERVER_APP_CONFIG_RELATIVE_PATH = "config/server.js";
56
- const PACKAGE_KIND_RUNTIME = "runtime";
57
- const PACKAGE_KIND_GENERATOR = "generator";
58
- const PACKAGE_KINDS = Object.freeze([PACKAGE_KIND_RUNTIME, PACKAGE_KIND_GENERATOR]);
59
- const WORKSPACE_VISIBILITY_LEVELS = Object.freeze(["workspace", "workspace_user"]);
60
- const WORKSPACE_VISIBILITY_SET = new Set(WORKSPACE_VISIBILITY_LEVELS);
61
- const MATERIALIZED_PACKAGE_ROOTS = new Map();
62
- const MATERIALIZED_PACKAGE_TEMP_DIRECTORIES = new Set();
63
- const LOCAL_WORKSPACE_PACKAGE_ROOTS = new Map();
64
- let LOCAL_WORKSPACE_PACKAGE_ID_INDEX = null;
65
- const BUILTIN_CAPABILITY_PROVIDERS = Object.freeze({
66
- "runtime.actions": Object.freeze(["@jskit-ai/kernel"])
67
- });
68
- const SETTINGS_FIELDS_CONTRACT_TARGETS = Object.freeze({
69
- "packages/main/src/shared/resources/consoleSettingsFields.js": Object.freeze({
70
- contractId: "users.settings-fields.console.v1",
71
- marker: "@jskit-contract users.settings-fields.console.v1",
72
- requiredSnippets: Object.freeze([
73
- "defineField",
74
- "resetConsoleSettingsFields"
75
- ])
76
- }),
77
- "packages/main/src/shared/resources/workspaceSettingsFields.js": Object.freeze({
78
- contractId: "users.settings-fields.workspace.v1",
79
- marker: "@jskit-contract users.settings-fields.workspace.v1",
80
- requiredSnippets: Object.freeze([
81
- "defineField",
82
- "resetWorkspaceSettingsFields"
83
- ])
84
- })
85
- });
86
-
87
- function normalizeMutationExtension(value) {
88
- const extension = String(value || "").trim();
89
- if (!extension) {
90
- return "";
91
- }
92
- if (extension.startsWith(".")) {
93
- return extension;
94
- }
95
- return `.${extension}`;
96
- }
97
-
98
- function normalizeTemplateContextRecord(value) {
99
- const record = ensureObject(value);
100
- const entrypoint = String(record.entrypoint || "").trim();
101
- const exportName = String(record.export || "").trim();
102
- if (!entrypoint && !exportName) {
103
- return null;
104
- }
105
-
106
- return Object.freeze({
107
- entrypoint,
108
- export: exportName || "buildTemplateContext"
109
- });
110
- }
111
-
112
- function normalizeFileMutationRecord(value) {
113
- const record = ensureObject(value);
114
- const op = String(record.op || "copy-file").trim().toLowerCase() || "copy-file";
115
- return {
116
- op,
117
- from: String(record.from || "").trim(),
118
- to: String(record.to || "").trim(),
119
- toSurface: String(record.toSurface || "").trim(),
120
- toSurfacePath: String(record.toSurfacePath || "").trim(),
121
- toSurfaceRoot: record.toSurfaceRoot === true,
122
- toDir: String(record.toDir || "").trim(),
123
- extension: normalizeMutationExtension(record.extension),
124
- preserveOnRemove: record.preserveOnRemove === true,
125
- id: String(record.id || "").trim(),
126
- category: String(record.category || "").trim(),
127
- reason: String(record.reason || "").trim(),
128
- templateContext: normalizeTemplateContextRecord(record.templateContext),
129
- when: normalizeMutationWhen(record.when)
130
- };
131
- }
132
-
133
- function normalizeMutationWhen(value) {
134
- const source = ensureObject(value);
135
- const option = String(source.option || "").trim();
136
- const config = String(source.config || "").trim();
137
- const equals = String(source.equals || "").trim();
138
- const notEquals = String(source.notEquals || "").trim();
139
- const includes = ensureArray(source.in).map((entry) => String(entry || "").trim()).filter(Boolean);
140
- const excludes = ensureArray(source.notIn).map((entry) => String(entry || "").trim()).filter(Boolean);
141
-
142
- if (!option && !config) {
143
- return null;
144
- }
145
-
146
- return {
147
- option,
148
- config,
149
- equals,
150
- notEquals,
151
- includes,
152
- excludes
153
- };
154
- }
155
-
156
- function readObjectPath(source, rawPath) {
157
- const valueSource = ensureObject(source);
158
- const fullPath = String(rawPath || "").trim();
159
- if (!fullPath) {
160
- return undefined;
161
- }
162
-
163
- if (Object.prototype.hasOwnProperty.call(valueSource, fullPath)) {
164
- return valueSource[fullPath];
165
- }
166
-
167
- const segments = fullPath
168
- .split(".")
169
- .map((entry) => String(entry || "").trim())
170
- .filter(Boolean);
171
- if (segments.length < 1) {
172
- return undefined;
173
- }
174
-
175
- let cursor = valueSource;
176
- for (const segment of segments) {
177
- if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) {
178
- return undefined;
179
- }
180
- if (!Object.prototype.hasOwnProperty.call(cursor, segment)) {
181
- return undefined;
182
- }
183
- cursor = cursor[segment];
184
- }
185
-
186
- return cursor;
187
- }
188
-
189
- function normalizeWhenSourceValue(value) {
190
- if (value == null) {
191
- return "";
192
- }
193
- if (typeof value === "string") {
194
- return value.trim();
195
- }
196
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
197
- return String(value).trim();
198
- }
199
- return "";
200
- }
201
-
202
- function normalizeWhenComparisonValue(value) {
203
- const normalizedValue = normalizeWhenSourceValue(value);
204
- if (!normalizedValue.includes(",")) {
205
- return normalizedValue;
206
- }
207
-
208
- return normalizedValue
209
- .split(",")
210
- .map((entry) => String(entry || "").trim())
211
- .filter(Boolean)
212
- .join(",");
213
- }
214
-
215
- function splitWhenComparisonTokens(value) {
216
- return normalizeWhenComparisonValue(value)
217
- .split(",")
218
- .map((entry) => String(entry || "").trim())
219
- .filter(Boolean);
220
- }
221
-
222
- function matchesWhenComparisonValue(optionValue, optionTokens, expectedValue) {
223
- const expected = normalizeWhenComparisonValue(expectedValue);
224
- if (!expected) {
225
- return false;
226
- }
227
-
228
- const expectedTokens = splitWhenComparisonTokens(expected);
229
- if (expectedTokens.length > 1) {
230
- return optionValue === expected;
231
- }
232
-
233
- if (optionTokens.length > 1) {
234
- return optionTokens.includes(expectedTokens[0]);
235
- }
236
-
237
- return optionValue === expectedTokens[0];
238
- }
239
-
240
- function resolveWhenConfigValue(configContext = {}, configPath = "") {
241
- const normalizedPath = String(configPath || "").trim();
242
- if (!normalizedPath) {
243
- return "";
244
- }
245
-
246
- const publicConfig = ensureObject(configContext.public);
247
- const serverConfig = ensureObject(configContext.server);
248
- const mergedConfig = ensureObject(configContext.merged);
249
- if (normalizedPath === "public") {
250
- return publicConfig;
251
- }
252
- if (normalizedPath === "server") {
253
- return serverConfig;
254
- }
255
- if (normalizedPath === "merged") {
256
- return mergedConfig;
257
- }
258
-
259
- if (normalizedPath.startsWith("public.")) {
260
- return readObjectPath(publicConfig, normalizedPath.slice("public.".length));
261
- }
262
- if (normalizedPath.startsWith("server.")) {
263
- return readObjectPath(serverConfig, normalizedPath.slice("server.".length));
264
- }
265
- if (normalizedPath.startsWith("merged.")) {
266
- return readObjectPath(mergedConfig, normalizedPath.slice("merged.".length));
267
- }
268
-
269
- return readObjectPath(mergedConfig, normalizedPath);
270
- }
271
-
272
- function shouldApplyMutationWhen(
273
- when,
274
- {
275
- options = {},
276
- configContext = {},
277
- packageId = "",
278
- mutationContext = "mutation"
279
- } = {}
280
- ) {
281
- if (!when || typeof when !== "object") {
282
- return true;
283
- }
284
-
285
- const optionName = String(when.option || "").trim();
286
- const configPath = String(when.config || "").trim();
287
- if (!optionName && !configPath) {
288
- return true;
289
- }
290
- if (optionName && configPath) {
291
- const packagePrefix = packageId ? `${packageId} ` : "";
292
- throw createCliError(
293
- `Invalid ${packagePrefix}${mutationContext}: when cannot declare both "option" and "config".`
294
- );
295
- }
296
-
297
- const sourceValue = optionName
298
- ? readObjectPath(options, optionName)
299
- : resolveWhenConfigValue(configContext, configPath);
300
- const optionValue = normalizeWhenComparisonValue(sourceValue);
301
- const optionTokens = splitWhenComparisonTokens(optionValue);
302
- const equals = normalizeWhenComparisonValue(when.equals);
303
- const notEquals = normalizeWhenComparisonValue(when.notEquals);
304
- const includes = ensureArray(when.includes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
305
- const excludes = ensureArray(when.excludes).map((entry) => normalizeWhenComparisonValue(entry)).filter(Boolean);
306
-
307
- if (equals && optionValue !== equals) {
308
- return false;
309
- }
310
- if (notEquals && optionValue === notEquals) {
311
- return false;
312
- }
313
- if (includes.length > 0 && !includes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
314
- return false;
315
- }
316
- if (excludes.length > 0 && excludes.some((entry) => matchesWhenComparisonValue(optionValue, optionTokens, entry))) {
317
- return false;
318
- }
319
-
320
- return true;
321
- }
322
-
323
- function buildFileWriteGroups(fileMutations, { packageId = "" } = {}) {
324
- const groups = [];
325
- const groupsByKey = new Map();
326
-
327
- for (const mutation of ensureArray(fileMutations)) {
328
- const normalized = normalizeFileMutationRecord(mutation);
329
- if (normalized.op === "install-migration") {
330
- if (!normalized.from || !normalized.id) {
331
- continue;
332
- }
333
- } else if (!normalized.from || (!normalized.to && !normalized.toSurface)) {
334
- continue;
335
- }
336
-
337
- const destinationLabel = normalized.to
338
- ? normalized.to
339
- : normalized.toSurfaceRoot
340
- ? `surface:${normalized.toSurface}.root`
341
- : `surface:${normalized.toSurface}/${normalized.toSurfacePath}`;
342
-
343
- const key = normalized.id
344
- ? `id:${normalized.id}`
345
- : normalized.category || normalized.reason
346
- ? `meta:${normalized.category}::${normalized.reason}`
347
- : `path:${destinationLabel}`;
348
-
349
- let group = groupsByKey.get(key);
350
- if (!group) {
351
- group = {
352
- id: normalized.id,
353
- category: normalized.category,
354
- reason: normalized.reason,
355
- files: []
356
- };
357
- groupsByKey.set(key, group);
358
- groups.push(group);
359
- } else {
360
- if (!group.category && normalized.category) {
361
- group.category = normalized.category;
362
- }
363
- if (!group.reason && normalized.reason) {
364
- group.reason = normalized.reason;
365
- }
366
- }
367
-
368
- if (normalized.op === "install-migration") {
369
- const toDir = normalized.toDir || "migrations";
370
- const extension = normalized.extension || ".cjs";
371
- group.files.push({
372
- from: normalized.from,
373
- to: buildManagedMigrationRelativePathLabel({
374
- toDir,
375
- packageId,
376
- migrationId: normalized.id,
377
- extension
378
- })
379
- });
380
- continue;
381
- }
382
-
383
- group.files.push({
384
- from: normalized.from,
385
- to: destinationLabel
386
- });
387
- }
388
-
389
- return groups;
390
- }
391
-
392
- function normalizeRelativePath(fromRoot, absolutePath) {
393
- return path.relative(fromRoot, absolutePath).split(path.sep).join("/");
394
- }
395
-
396
- function hashBuffer(buffer) {
397
- return createHash("sha256").update(buffer).digest("hex");
398
- }
399
-
400
- function normalizeMigrationExtension(value = "", fallback = ".cjs") {
401
- const normalizedFallback = String(fallback || ".cjs").trim() || ".cjs";
402
- const raw = String(value || "").trim();
403
- const candidate = raw ? (raw.startsWith(".") ? raw : `.${raw}`) : normalizedFallback;
404
- if (!/^\.[a-z0-9]+$/i.test(candidate)) {
405
- throw createCliError(`Invalid install-migration extension: ${candidate}`);
406
- }
407
- return candidate.toLowerCase();
408
- }
409
-
410
- const MIGRATION_ID_PATTERN = /^[a-z0-9._-]+$/;
411
-
412
- function normalizeMigrationId(value, packageId) {
413
- const normalized = String(value || "").trim();
414
- if (!normalized) {
415
- throw createCliError(`Invalid install-migration mutation in ${packageId}: \"id\" is required.`);
416
- }
417
- if (!MIGRATION_ID_PATTERN.test(normalized)) {
418
- throw createCliError(
419
- `Invalid install-migration mutation in ${packageId}: "id" must match ${MIGRATION_ID_PATTERN.source}.`
420
- );
421
- }
422
- return normalized;
423
- }
424
-
425
- function resolveAppRelativePathWithinRoot(appRoot, relativePath, contextLabel = "path") {
426
- const normalized = normalizeRelativePosixPath(String(relativePath || "").trim());
427
- if (!normalized) {
428
- throw createCliError(`Invalid ${contextLabel}: path is required.`);
429
- }
430
- const segments = normalized.split("/");
431
- if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
432
- throw createCliError(`Invalid ${contextLabel}: path must be a safe relative path.`);
433
- }
434
-
435
- const appRootAbsolute = path.resolve(appRoot);
436
- const absolutePath = path.resolve(appRootAbsolute, normalized);
437
- const relativeFromRoot = path.relative(appRootAbsolute, absolutePath);
438
- if (
439
- relativeFromRoot === ".." ||
440
- relativeFromRoot.startsWith(`..${path.sep}`) ||
441
- path.isAbsolute(relativeFromRoot)
442
- ) {
443
- throw createCliError(`Invalid ${contextLabel}: path must stay within app root.`);
444
- }
445
-
446
- return {
447
- relativePath: normalized,
448
- absolutePath
449
- };
450
- }
451
-
452
- function normalizeMigrationDirectory(value, packageId) {
453
- const normalized = normalizeRelativePosixPath(String(value || "").trim() || "migrations");
454
- if (!normalized) {
455
- throw createCliError(`Invalid install-migration mutation in ${packageId}: "toDir" cannot be empty.`);
456
- }
457
-
458
- const segments = normalized.split("/");
459
- if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
460
- throw createCliError(`Invalid install-migration mutation in ${packageId}: "toDir" must be a safe relative path.`);
461
- }
462
-
463
- return segments.join("/");
464
- }
465
-
466
- function formatMigrationTimestamp(date = new Date()) {
467
- const source = date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date();
468
- const year = String(source.getUTCFullYear()).padStart(4, "0");
469
- const month = String(source.getUTCMonth() + 1).padStart(2, "0");
470
- const day = String(source.getUTCDate()).padStart(2, "0");
471
- const hour = String(source.getUTCHours()).padStart(2, "0");
472
- const minute = String(source.getUTCMinutes()).padStart(2, "0");
473
- const second = String(source.getUTCSeconds()).padStart(2, "0");
474
- return `${year}${month}${day}${hour}${minute}${second}`;
475
- }
476
-
477
- function buildManagedMigrationFileName({ packageId = "", migrationId = "", extension = ".cjs", timestamp = "" } = {}) {
478
- const normalizedMigrationId = normalizeMigrationId(migrationId, packageId);
479
- const normalizedExtension = normalizeMigrationExtension(extension, ".cjs");
480
- const normalizedTimestamp = String(timestamp || "").trim();
481
- if (!/^\d{14}$/.test(normalizedTimestamp)) {
482
- throw createCliError(
483
- `Invalid install-migration mutation in ${packageId}: timestamp must be a 14-digit UTC string (YYYYMMDDHHmmss).`
484
- );
485
- }
486
- return `${normalizedTimestamp}_${normalizedMigrationId}${normalizedExtension}`;
487
- }
488
-
489
- function buildManagedMigrationRelativePath({ toDir = "migrations", packageId = "", migrationId = "", extension = ".cjs", timestamp = "" } = {}) {
490
- const normalizedDirectory = normalizeMigrationDirectory(toDir, packageId);
491
- const fileName = buildManagedMigrationFileName({
492
- packageId,
493
- migrationId,
494
- extension,
495
- timestamp
496
- });
497
- return path.posix.join(normalizedDirectory, fileName);
498
- }
499
-
500
- function buildManagedMigrationRelativePathLabel({ toDir = "migrations", migrationId = "", extension = ".cjs" } = {}) {
501
- const directory = normalizeRelativePosixPath(String(toDir || "").trim() || "migrations") || "migrations";
502
- const id = String(migrationId || "<migration-id>").trim() || "<migration-id>";
503
- const rawExtension = String(extension || ".cjs").trim() || ".cjs";
504
- const ext = rawExtension.startsWith(".") ? rawExtension : `.${rawExtension}`;
505
- return `${directory}/<timestamp>_${id}${ext}`.replace(/\/{2,}/g, "/");
506
- }
507
-
508
- async function findExistingManagedMigrationPathById({
509
- appRoot,
510
- toDir = "migrations",
511
- packageId = "",
512
- migrationId = "",
513
- extension = ".cjs"
514
- } = {}) {
515
- const normalizedDirectory = normalizeMigrationDirectory(toDir, packageId);
516
- const resolvedDirectory = resolveAppRelativePathWithinRoot(
517
- appRoot,
518
- normalizedDirectory,
519
- `${packageId} migration directory for ${migrationId}`
520
- );
521
- if (!(await fileExists(resolvedDirectory.absolutePath))) {
522
- return null;
523
- }
524
-
525
- const normalizedMigrationId = normalizeMigrationId(migrationId, packageId);
526
- const normalizedExtension = normalizeMigrationExtension(extension, ".cjs");
527
- const suffix = `_${normalizedMigrationId}${normalizedExtension}`;
528
- const entries = await readdir(resolvedDirectory.absolutePath, { withFileTypes: true }).catch(() => []);
529
- const matches = [];
530
-
531
- for (const entry of entries) {
532
- if (!entry.isFile()) {
533
- continue;
534
- }
535
- const fileName = String(entry.name || "").trim();
536
- if (!fileName.endsWith(suffix)) {
537
- continue;
538
- }
539
- const timestamp = fileName.slice(0, fileName.length - suffix.length);
540
- if (!/^\d{14}$/.test(timestamp)) {
541
- continue;
542
- }
543
- matches.push({
544
- relativePath: path.posix.join(resolvedDirectory.relativePath, fileName),
545
- absolutePath: path.join(resolvedDirectory.absolutePath, fileName)
546
- });
547
- }
548
-
549
- matches.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
550
- if (matches.length > 1) {
551
- throw createCliError(
552
- `${packageId}: found multiple migration files for ${normalizedMigrationId} in ${resolvedDirectory.relativePath}. Keep one file for this migration id.`
553
- );
554
- }
555
- return matches[0] || null;
556
- }
557
-
558
- function upsertManagedMigrationRecord(managedMigrations, record) {
559
- const records = ensureArray(managedMigrations);
560
- const normalizedId = String(ensureObject(record).id || "").trim();
561
- if (!normalizedId) {
562
- return;
563
- }
564
-
565
- const nextRecord = {
566
- ...ensureObject(record),
567
- id: normalizedId
568
- };
569
- const existingIndex = records.findIndex(
570
- (entry) => String(ensureObject(entry).id || "").trim() === normalizedId
571
- );
572
- if (existingIndex >= 0) {
573
- records[existingIndex] = nextRecord;
574
- return;
575
- }
576
-
577
- records.push(nextRecord);
578
- }
579
-
580
- async function fileExists(absolutePath) {
581
- try {
582
- await access(absolutePath, fsConstants.F_OK);
583
- return true;
584
- } catch {
585
- return false;
586
- }
587
- }
588
-
589
- async function readJsonFile(absolutePath) {
590
- const source = await readFile(absolutePath, "utf8");
591
- return JSON.parse(source);
592
- }
593
-
594
- async function writeJsonFile(absolutePath, value) {
595
- await mkdir(path.dirname(absolutePath), { recursive: true });
596
- await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
597
- }
598
-
599
- async function readFileBufferIfExists(absolutePath) {
600
- if (!(await fileExists(absolutePath))) {
601
- return {
602
- exists: false,
603
- buffer: Buffer.alloc(0)
604
- };
605
- }
606
-
607
- return {
608
- exists: true,
609
- buffer: await readFile(absolutePath)
610
- };
611
- }
612
-
613
- async function loadAppConfigModuleConfig(appRoot, relativePath) {
614
- const absolutePath = path.join(appRoot, relativePath);
615
- if (!(await fileExists(absolutePath))) {
616
- return {};
617
- }
618
-
619
- let moduleNamespace = null;
620
- try {
621
- moduleNamespace = await import(`${pathToFileURL(absolutePath).href}?t=${Date.now()}_${Math.random()}`);
622
- } catch (error) {
623
- throw createCliError(
624
- `Unable to load ${relativePath}: ${String(error?.message || error || "unknown error")}`
625
- );
626
- }
627
-
628
- const defaultExport = ensureObject(moduleNamespace?.default);
629
- const fromNamedExport = ensureObject(moduleNamespace?.config);
630
- const fromDefaultConfig = ensureObject(defaultExport?.config);
631
-
632
- if (Object.keys(fromNamedExport).length > 0) {
633
- return fromNamedExport;
634
- }
635
- if (Object.keys(fromDefaultConfig).length > 0) {
636
- return fromDefaultConfig;
637
- }
638
- return defaultExport;
639
- }
640
-
641
- async function loadMutationWhenConfigContext(appRoot) {
642
- const publicConfig = await loadAppConfigModuleConfig(appRoot, PUBLIC_APP_CONFIG_RELATIVE_PATH);
643
- const serverConfig = await loadAppConfigModuleConfig(appRoot, SERVER_APP_CONFIG_RELATIVE_PATH);
644
- return {
645
- public: publicConfig,
646
- server: serverConfig,
647
- merged: {
648
- ...publicConfig,
649
- ...serverConfig
650
- }
651
- };
652
- }
653
-
654
- function createEmptyViteDevProxyConfig() {
655
- return Object.freeze({
656
- version: VITE_DEV_PROXY_CONFIG_VERSION,
657
- entries: Object.freeze([])
658
- });
659
- }
660
-
661
- function normalizeViteDevProxyPath(value = "", { context = "vite proxy entry" } = {}) {
662
- const normalizedPath = String(value || "").trim();
663
- if (!normalizedPath || !normalizedPath.startsWith("/")) {
664
- throw createCliError(`${context} requires "path" starting with "/".`);
665
- }
666
- return normalizedPath.replace(/\/{2,}/g, "/");
667
- }
668
-
669
- function normalizeViteDevProxyEntry(value = {}, { context = "vite proxy entry" } = {}) {
670
- const source = ensureObject(value);
671
- const packageId = String(source.packageId || "").trim();
672
- const entryId = String(source.id || "").trim();
673
- if (!packageId) {
674
- throw createCliError(`${context} requires "packageId".`);
675
- }
676
- if (!entryId) {
677
- throw createCliError(`${context} requires "id".`);
678
- }
679
-
680
- const normalized = {
681
- packageId,
682
- id: entryId,
683
- path: normalizeViteDevProxyPath(source.path, {
684
- context: `${context} (${packageId}:${entryId})`
685
- })
686
- };
687
-
688
- const target = String(source.target || "").trim();
689
- if (target) {
690
- normalized.target = target;
691
- }
692
- if (Object.prototype.hasOwnProperty.call(source, "changeOrigin")) {
693
- normalized.changeOrigin = source.changeOrigin === true;
694
- }
695
- if (Object.prototype.hasOwnProperty.call(source, "ws")) {
696
- normalized.ws = source.ws === true;
697
- }
698
-
699
- return Object.freeze(normalized);
700
- }
701
-
702
- function normalizeViteDevProxyConfig(value = {}, { context = "vite proxy config" } = {}) {
703
- const source = ensureObject(value);
704
- const normalizedEntries = [];
705
- const seenEntryKeys = new Set();
706
- const seenPaths = new Set();
707
-
708
- for (const [index, entry] of ensureArray(source.entries).entries()) {
709
- const normalizedEntry = normalizeViteDevProxyEntry(entry, {
710
- context: `${context}.entries[${index}]`
711
- });
712
- const entryKey = `${normalizedEntry.packageId}::${normalizedEntry.id}`;
713
- if (seenEntryKeys.has(entryKey)) {
714
- throw createCliError(`${context} has duplicate entry "${entryKey}".`);
715
- }
716
- if (seenPaths.has(normalizedEntry.path)) {
717
- throw createCliError(`${context} has duplicate path "${normalizedEntry.path}".`);
718
- }
719
-
720
- seenEntryKeys.add(entryKey);
721
- seenPaths.add(normalizedEntry.path);
722
- normalizedEntries.push(normalizedEntry);
723
- }
724
-
725
- normalizedEntries.sort((left, right) => {
726
- const pathDiff = left.path.localeCompare(right.path);
727
- if (pathDiff !== 0) {
728
- return pathDiff;
729
- }
730
- const packageDiff = left.packageId.localeCompare(right.packageId);
731
- if (packageDiff !== 0) {
732
- return packageDiff;
733
- }
734
- return left.id.localeCompare(right.id);
735
- });
736
-
737
- return Object.freeze({
738
- version: VITE_DEV_PROXY_CONFIG_VERSION,
739
- entries: Object.freeze(normalizedEntries)
740
- });
741
- }
742
-
743
- function resolveViteDevProxyConfigAbsolutePath(appRoot) {
744
- return path.join(appRoot, VITE_DEV_PROXY_CONFIG_RELATIVE_PATH);
745
- }
746
-
747
- async function loadViteDevProxyConfig(appRoot, { context = "vite proxy config" } = {}) {
748
- const absolutePath = resolveViteDevProxyConfigAbsolutePath(appRoot);
749
- const existing = await readFileBufferIfExists(absolutePath);
750
- if (!existing.exists) {
751
- return Object.freeze({
752
- absolutePath,
753
- exists: false,
754
- config: createEmptyViteDevProxyConfig()
755
- });
756
- }
757
-
758
- const relativePath = normalizeRelativePath(appRoot, absolutePath);
759
- let parsed = {};
760
- try {
761
- parsed = JSON.parse(existing.buffer.toString("utf8"));
762
- } catch {
763
- throw createCliError(`Invalid ${context} at ${relativePath}: expected valid JSON.`);
764
- }
765
-
766
- return Object.freeze({
767
- absolutePath,
768
- exists: true,
769
- config: normalizeViteDevProxyConfig(parsed, {
770
- context: `${context} (${relativePath})`
771
- })
772
- });
773
- }
774
-
775
- async function writeViteDevProxyConfig(appRoot, config = {}, touchedFiles = null) {
776
- const absolutePath = resolveViteDevProxyConfigAbsolutePath(appRoot);
777
- const relativePath = normalizeRelativePath(appRoot, absolutePath);
778
- const normalizedConfig = normalizeViteDevProxyConfig(config);
779
-
780
- if (normalizedConfig.entries.length < 1) {
781
- if (await fileExists(absolutePath)) {
782
- await rm(absolutePath);
783
- if (touchedFiles && typeof touchedFiles.add === "function") {
784
- touchedFiles.add(relativePath);
785
- }
786
- }
787
- return;
788
- }
789
-
790
- await writeJsonFile(absolutePath, normalizedConfig);
791
- if (touchedFiles && typeof touchedFiles.add === "function") {
792
- touchedFiles.add(relativePath);
793
- }
794
- }
795
-
796
- function normalizeViteProxyMutationRecord(value = {}) {
797
- const source = ensureObject(value);
798
- const changeOrigin = Object.prototype.hasOwnProperty.call(source, "changeOrigin")
799
- ? source.changeOrigin === true
800
- : undefined;
801
- const ws = Object.prototype.hasOwnProperty.call(source, "ws") ? source.ws === true : undefined;
802
- return Object.freeze({
803
- id: String(source.id || "").trim(),
804
- path: String(source.path || "").trim(),
805
- target: String(source.target || "").trim(),
806
- changeOrigin,
807
- ws,
808
- when: normalizeMutationWhen(source.when)
809
- });
810
- }
811
-
812
- async function applyViteMutations(packageEntry, appRoot, viteMutations, options, managedVite, touchedFiles) {
813
- const mutations = ensureArray(ensureObject(viteMutations).proxy).map((entry) => normalizeViteProxyMutationRecord(entry));
814
- if (mutations.length < 1) {
815
- return;
816
- }
817
-
818
- const { config: currentConfig } = await loadViteDevProxyConfig(appRoot, {
819
- context: `vite proxy config for ${packageEntry.packageId}`
820
- });
821
- const nextEntries = [...currentConfig.entries];
822
- let changed = false;
823
-
824
- for (const mutation of mutations) {
825
- const configContext = mutation.when?.config ? await loadMutationWhenConfigContext(appRoot) : {};
826
- if (
827
- !shouldApplyMutationWhen(mutation.when, {
828
- options,
829
- configContext,
830
- packageId: packageEntry.packageId,
831
- mutationContext: "vite proxy mutation"
832
- })
833
- ) {
834
- continue;
835
- }
836
-
837
- const normalizedId = interpolateOptionValue(
838
- mutation.id,
839
- options,
840
- packageEntry.packageId,
841
- "mutations.vite.proxy.id"
842
- );
843
- if (!normalizedId) {
844
- throw createCliError(`Invalid vite proxy mutation in ${packageEntry.packageId}: "id" is required.`);
845
- }
846
-
847
- const normalizedPath = normalizeViteDevProxyPath(
848
- interpolateOptionValue(
849
- mutation.path,
850
- options,
851
- packageEntry.packageId,
852
- `mutations.vite.proxy.${normalizedId}.path`
853
- ),
854
- {
855
- context: `Invalid vite proxy mutation in ${packageEntry.packageId} (${normalizedId})`
856
- }
857
- );
858
-
859
- const normalizedTarget = mutation.target
860
- ? String(
861
- interpolateOptionValue(
862
- mutation.target,
863
- options,
864
- packageEntry.packageId,
865
- `mutations.vite.proxy.${normalizedId}.target`
866
- ) || ""
867
- ).trim()
868
- : "";
869
-
870
- for (let index = nextEntries.length - 1; index >= 0; index -= 1) {
871
- const entry = nextEntries[index];
872
- if (entry.packageId === packageEntry.packageId && entry.id === normalizedId) {
873
- nextEntries.splice(index, 1);
874
- changed = true;
875
- }
876
- }
877
-
878
- const conflictingEntry = nextEntries.find((entry) => entry.path === normalizedPath);
879
- if (conflictingEntry) {
880
- throw createCliError(
881
- `Invalid vite proxy mutation in ${packageEntry.packageId}: path "${normalizedPath}" conflicts with ${conflictingEntry.packageId} (${conflictingEntry.id}).`
882
- );
883
- }
884
-
885
- nextEntries.push(
886
- Object.freeze({
887
- packageId: packageEntry.packageId,
888
- id: normalizedId,
889
- path: normalizedPath,
890
- ...(normalizedTarget ? { target: normalizedTarget } : {}),
891
- ...(typeof mutation.changeOrigin === "boolean" ? { changeOrigin: mutation.changeOrigin } : {}),
892
- ...(typeof mutation.ws === "boolean" ? { ws: mutation.ws } : {})
893
- })
894
- );
895
- changed = true;
896
-
897
- const mutationKey = `${normalizedPath}::${normalizedId}`;
898
- managedVite[mutationKey] = Object.freeze({
899
- op: "upsert-vite-proxy",
900
- id: normalizedId,
901
- path: normalizedPath
902
- });
903
- }
904
-
905
- if (!changed) {
906
- return;
907
- }
908
-
909
- const nextConfig = normalizeViteDevProxyConfig(
910
- {
911
- entries: nextEntries
912
- },
913
- {
914
- context: `vite proxy config for ${packageEntry.packageId}`
915
- }
916
- );
917
-
918
- if (JSON.stringify(currentConfig) === JSON.stringify(nextConfig)) {
919
- return;
920
- }
921
-
922
- await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles);
923
- }
924
-
925
- async function removeManagedViteProxyEntries({ appRoot, packageId, managedViteChanges = {}, touchedFiles = null } = {}) {
926
- const managedChanges = Object.values(ensureObject(managedViteChanges))
927
- .map((entry) => ensureObject(entry))
928
- .filter((entry) => String(entry.op || "").trim() === "upsert-vite-proxy");
929
- if (managedChanges.length < 1) {
930
- return;
931
- }
932
-
933
- const { exists, config: currentConfig } = await loadViteDevProxyConfig(appRoot, {
934
- context: `vite proxy config while removing ${packageId}`
935
- });
936
- if (!exists) {
937
- return;
938
- }
939
-
940
- let nextEntries = [...currentConfig.entries];
941
- for (const change of managedChanges) {
942
- const changeId = String(change.id || "").trim();
943
- const changePath = String(change.path || "").trim();
944
- if (!changeId) {
945
- continue;
946
- }
947
- nextEntries = nextEntries.filter((entry) => {
948
- if (entry.packageId !== packageId || entry.id !== changeId) {
949
- return true;
950
- }
951
- if (changePath && entry.path !== changePath) {
952
- return true;
953
- }
954
- return false;
955
- });
956
- }
957
-
958
- const nextConfig = normalizeViteDevProxyConfig(
959
- {
960
- entries: nextEntries
961
- },
962
- {
963
- context: `vite proxy config while removing ${packageId}`
964
- }
965
- );
966
- if (JSON.stringify(currentConfig) === JSON.stringify(nextConfig)) {
967
- return;
968
- }
969
-
970
- await writeViteDevProxyConfig(appRoot, nextConfig, touchedFiles);
971
- }
972
-
973
- async function resolveAppRootFromCwd(cwd) {
974
- const startDirectory = path.resolve(String(cwd || process.cwd()));
975
- let currentDirectory = startDirectory;
976
-
977
- while (true) {
978
- const packageJsonPath = path.join(currentDirectory, "package.json");
979
- if (await fileExists(packageJsonPath)) {
980
- return currentDirectory;
981
- }
982
-
983
- const parentDirectory = path.dirname(currentDirectory);
984
- if (parentDirectory === currentDirectory) {
985
- throw createCliError(
986
- `Could not locate package.json starting from ${startDirectory}. Run jskit from an app directory (or a child directory of one).`
987
- );
988
- }
989
- currentDirectory = parentDirectory;
990
- }
991
- }
992
-
993
- async function loadAppPackageJson(appRoot) {
994
- const packageJsonPath = path.join(appRoot, "package.json");
995
- const packageJson = await readJsonFile(packageJsonPath);
996
- return {
997
- packageJsonPath,
998
- packageJson
999
- };
1000
- }
1001
-
1002
- function createDefaultLock() {
1003
- return {
1004
- lockVersion: LOCK_VERSION,
1005
- installedPackages: {}
1006
- };
1007
- }
1008
-
1009
- async function loadLockFile(appRoot) {
1010
- const lockPath = path.join(appRoot, LOCK_RELATIVE_PATH);
1011
- if (!(await fileExists(lockPath))) {
1012
- return {
1013
- lockPath,
1014
- lock: createDefaultLock()
1015
- };
1016
- }
1017
-
1018
- const lock = await readJsonFile(lockPath);
1019
- const installedPackages = ensureObject(lock?.installedPackages);
1020
- const lockVersion = Number(lock?.lockVersion);
1021
- return {
1022
- lockPath,
1023
- lock: {
1024
- lockVersion: Number.isFinite(lockVersion) && lockVersion > 0 ? lockVersion : LOCK_VERSION,
1025
- installedPackages
1026
- }
1027
- };
1028
- }
1029
-
1030
- function createManagedPackageJsonChange(hadPrevious, previousValue, value) {
1031
- return {
1032
- hadPrevious: Boolean(hadPrevious),
1033
- previousValue: hadPrevious ? String(previousValue) : "",
1034
- value: String(value)
1035
- };
1036
- }
1037
-
1038
- function ensurePackageJsonSection(packageJson, sectionName) {
1039
- const sectionValue = ensureObject(packageJson[sectionName]);
1040
- packageJson[sectionName] = sectionValue;
1041
- return sectionValue;
1042
- }
1043
-
1044
- function applyPackageJsonField(packageJson, sectionName, key, value) {
1045
- const section = ensurePackageJsonSection(packageJson, sectionName);
1046
- const nextValue = String(value);
1047
- const hadPrevious = Object.prototype.hasOwnProperty.call(section, key);
1048
- const previousValue = hadPrevious ? String(section[key]) : "";
1049
- const changed = !hadPrevious || previousValue !== nextValue;
1050
- section[key] = nextValue;
1051
- return {
1052
- changed,
1053
- managed: createManagedPackageJsonChange(hadPrevious, previousValue, nextValue)
1054
- };
1055
- }
1056
-
1057
- function removePackageJsonField(packageJson, sectionName, key) {
1058
- const section = ensureObject(packageJson[sectionName]);
1059
- if (!Object.prototype.hasOwnProperty.call(section, key)) {
1060
- return false;
1061
- }
1062
- delete section[key];
1063
- if (Object.keys(section).length < 1) {
1064
- delete packageJson[sectionName];
1065
- }
1066
- return true;
1067
- }
1068
-
1069
- function restorePackageJsonField(packageJson, sectionName, key, managedChange) {
1070
- const section = ensurePackageJsonSection(packageJson, sectionName);
1071
- const currentValue = Object.prototype.hasOwnProperty.call(section, key) ? String(section[key]) : "";
1072
- if (currentValue !== String(managedChange?.value || "")) {
1073
- return false;
1074
- }
1075
-
1076
- if (managedChange?.hadPrevious) {
1077
- section[key] = String(managedChange.previousValue || "");
1078
- } else {
1079
- delete section[key];
1080
- }
1081
- return true;
1082
- }
1083
-
1084
- function parseEnvLineValue(line, key) {
1085
- const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
1086
- if (!pattern.test(line)) {
1087
- return null;
1088
- }
1089
- const index = line.indexOf("=");
1090
- if (index === -1) {
1091
- return "";
1092
- }
1093
- return line.slice(index + 1);
1094
- }
1095
-
1096
- function upsertEnvValue(content, key, value) {
1097
- const lines = String(content || "").split(/\r?\n/);
1098
- const lookupPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
1099
- let index = -1;
1100
-
1101
- for (let cursor = 0; cursor < lines.length; cursor += 1) {
1102
- if (lookupPattern.test(lines[cursor])) {
1103
- index = cursor;
1104
- break;
1105
- }
1106
- }
1107
-
1108
- const hadPrevious = index >= 0;
1109
- const previousValue = hadPrevious ? String(parseEnvLineValue(lines[index], key) || "") : "";
1110
- const nextLine = `${key}=${value}`;
1111
-
1112
- if (hadPrevious) {
1113
- lines[index] = nextLine;
1114
- } else {
1115
- if (lines.length === 1 && lines[0] === "") {
1116
- lines[0] = nextLine;
1117
- } else {
1118
- lines.push(nextLine);
1119
- }
1120
- }
1121
-
1122
- const normalized = `${lines.join("\n").replace(/\n+$/, "")}\n`;
1123
- return {
1124
- hadPrevious,
1125
- previousValue,
1126
- content: normalized
1127
- };
1128
- }
1129
-
1130
- function removeEnvValue(content, key, expectedValue, previous) {
1131
- const lines = String(content || "").split(/\r?\n/);
1132
- const lookupPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
1133
- let index = -1;
1134
-
1135
- for (let cursor = 0; cursor < lines.length; cursor += 1) {
1136
- if (lookupPattern.test(lines[cursor])) {
1137
- index = cursor;
1138
- break;
1139
- }
1140
- }
1141
-
1142
- if (index < 0) {
1143
- return {
1144
- changed: false,
1145
- content: content
1146
- };
1147
- }
1148
-
1149
- const currentValue = String(parseEnvLineValue(lines[index], key) || "");
1150
- if (currentValue !== String(expectedValue || "")) {
1151
- return {
1152
- changed: false,
1153
- content: content
1154
- };
1155
- }
1156
-
1157
- if (previous?.hadPrevious) {
1158
- lines[index] = `${key}=${String(previous.previousValue || "")}`;
1159
- } else {
1160
- lines.splice(index, 1);
1161
- }
1162
-
1163
- const normalized = `${lines.join("\n").replace(/\n+$/, "")}\n`;
1164
- return {
1165
- changed: true,
1166
- content: normalized
1167
- };
1168
- }
1169
-
1170
- function normalizePackageKind(rawValue, descriptorPath) {
1171
- const normalized = String(rawValue || "").trim().toLowerCase();
1172
- if (!normalized) {
1173
- throw createCliError(
1174
- `Invalid package descriptor at ${descriptorPath}: missing kind (expected ${PACKAGE_KINDS.join(" | ")}).`
1175
- );
1176
- }
1177
- if (!PACKAGE_KINDS.includes(normalized)) {
1178
- throw createCliError(
1179
- `Invalid package descriptor at ${descriptorPath}: kind must be one of: ${PACKAGE_KINDS.join(", ")}.`
1180
- );
1181
- }
1182
- return normalized;
1183
- }
1184
-
1185
- function validateInstallMigrationMutationShape(descriptor, descriptorPath) {
1186
- const packageId = String(ensureObject(descriptor).packageId || "").trim() || "unknown-package";
1187
- const mutations = ensureObject(ensureObject(descriptor).mutations);
1188
- const files = ensureArray(mutations.files);
1189
- for (const fileMutation of files) {
1190
- const normalized = normalizeFileMutationRecord(fileMutation);
1191
- if (normalized.op !== "install-migration") {
1192
- continue;
1193
- }
1194
- if (!normalized.from) {
1195
- throw createCliError(
1196
- `Invalid package descriptor at ${descriptorPath}: install-migration in ${packageId} requires "from".`
1197
- );
1198
- }
1199
- if (!normalized.id) {
1200
- throw createCliError(
1201
- `Invalid package descriptor at ${descriptorPath}: install-migration in ${packageId} requires "id".`
1202
- );
1203
- }
1204
- }
1205
- }
1206
-
1207
- function validatePackageDescriptorShape(descriptor, descriptorPath) {
1208
- const normalized = ensureObject(descriptor);
1209
- const packageId = String(normalized.packageId || "").trim();
1210
- const version = String(normalized.version || "").trim();
1211
-
1212
- if (!packageId.startsWith("@jskit-ai/")) {
1213
- throw createCliError(`Invalid package descriptor at ${descriptorPath}: packageId must start with @jskit-ai/.`);
1214
- }
1215
- if (!version) {
1216
- throw createCliError(`Invalid package descriptor at ${descriptorPath}: missing version.`);
1217
- }
1218
-
1219
- const runtime = ensureObject(normalized.runtime);
1220
- const server = ensureObject(runtime.server);
1221
- const client = ensureObject(runtime.client);
1222
- const hasServerProviders = Array.isArray(server.providers);
1223
- const hasClientProviders = Array.isArray(client.providers);
1224
- if (!hasServerProviders && !hasClientProviders) {
1225
- throw createCliError(
1226
- `Invalid package descriptor at ${descriptorPath}: runtime.server.providers or runtime.client.providers must be declared.`
1227
- );
1228
- }
1229
-
1230
- validateInstallMigrationMutationShape(normalized, descriptorPath);
1231
-
1232
- return {
1233
- ...normalized,
1234
- kind: normalizePackageKind(normalized.kind, descriptorPath)
1235
- };
1236
- }
1237
-
1238
- function isGeneratorPackageEntry(packageEntry) {
1239
- const descriptor = ensureObject(packageEntry?.descriptor);
1240
- return String(descriptor.kind || "").trim().toLowerCase() === PACKAGE_KIND_GENERATOR;
1241
- }
1242
-
1243
- function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { expectedPackageId = "", fallbackVersion = "" } = {}) {
1244
- const normalized = ensureObject(descriptor);
1245
- const packageId = String(normalized.packageId || "").trim();
1246
- const version = String(normalized.version || "").trim() || String(fallbackVersion || "").trim();
1247
-
1248
- if (!packageId) {
1249
- throw createCliError(`Invalid app-local package descriptor at ${descriptorPath}: missing packageId.`);
1250
- }
1251
- if (expectedPackageId && packageId !== expectedPackageId) {
1252
- throw createCliError(
1253
- `Descriptor/package mismatch at ${descriptorPath}: package.descriptor.mjs has ${packageId} but package.json has ${expectedPackageId}.`
1254
- );
1255
- }
1256
- if (!version) {
1257
- throw createCliError(`Invalid app-local package descriptor at ${descriptorPath}: missing version.`);
1258
- }
1259
-
1260
- validateInstallMigrationMutationShape(normalized, descriptorPath);
1261
-
1262
- return {
1263
- ...normalized,
1264
- packageId,
1265
- version,
1266
- kind: normalizePackageKind(normalized.kind, descriptorPath)
1267
- };
1268
- }
1269
-
1270
- function createPackageEntry({
1271
- packageId,
1272
- version,
1273
- descriptor,
1274
- rootDir = "",
1275
- relativeDir = "",
1276
- descriptorRelativePath = "",
1277
- packageJson = {},
1278
- sourceType = "",
1279
- source = {}
1280
- }) {
1281
- const normalizedSourceType = String(sourceType || "").trim() || "package";
1282
- const normalizedDescriptorPath = String(descriptorRelativePath || "").trim();
1283
- const normalizedSource = {
1284
- type: normalizedSourceType,
1285
- ...ensureObject(source)
1286
- };
1287
- if (!normalizedSource.descriptorPath && normalizedDescriptorPath) {
1288
- normalizedSource.descriptorPath = normalizedDescriptorPath;
1289
- }
1290
- return {
1291
- packageId: String(packageId || "").trim(),
1292
- version: String(version || "").trim(),
1293
- descriptor: ensureObject(descriptor),
1294
- rootDir: String(rootDir || "").trim(),
1295
- relativeDir: String(relativeDir || "").trim(),
1296
- descriptorRelativePath: normalizedDescriptorPath,
1297
- packageJson: ensureObject(packageJson),
1298
- sourceType: normalizedSourceType,
1299
- source: normalizedSource
1300
- };
1301
- }
1302
-
1303
- function mergePackageRegistries(...registries) {
1304
- const merged = new Map();
1305
- for (const registry of registries) {
1306
- if (!(registry instanceof Map)) {
1307
- continue;
1308
- }
1309
- for (const [packageId, packageEntry] of registry.entries()) {
1310
- merged.set(packageId, packageEntry);
1311
- }
1312
- }
1313
- return merged;
1314
- }
1315
-
1316
- function validateBundleDescriptorShape(descriptor, descriptorPath) {
1317
- const normalized = ensureObject(descriptor);
1318
- const bundleId = String(normalized.bundleId || "").trim();
1319
- const version = String(normalized.version || "").trim();
1320
- const packages = ensureArray(normalized.packages).map((value) => String(value));
1321
-
1322
- if (!bundleId) {
1323
- throw createCliError(`Invalid bundle descriptor at ${descriptorPath}: missing bundleId.`);
1324
- }
1325
- if (!version) {
1326
- throw createCliError(`Invalid bundle descriptor at ${descriptorPath}: missing version.`);
1327
- }
1328
- if (packages.length < 2) {
1329
- throw createCliError(`Invalid bundle descriptor at ${descriptorPath}: bundles must contain at least two packages.`);
1330
- }
1331
-
1332
- return normalized;
1333
- }
1334
-
1335
- async function loadAppLocalPackageRegistry(appRoot) {
1336
- const localPackagesRoot = path.join(appRoot, "packages");
1337
- if (!(await fileExists(localPackagesRoot))) {
1338
- return new Map();
1339
- }
1340
-
1341
- const registry = new Map();
1342
- const entries = await readdir(localPackagesRoot, { withFileTypes: true });
1343
- for (const entry of entries) {
1344
- if (!entry.isDirectory() || entry.name.startsWith(".")) {
1345
- continue;
1346
- }
1347
-
1348
- const packageRoot = path.join(localPackagesRoot, entry.name);
1349
- const packageJsonPath = path.join(packageRoot, "package.json");
1350
- const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
1351
- if (!(await fileExists(packageJsonPath)) || !(await fileExists(descriptorPath))) {
1352
- continue;
1353
- }
1354
-
1355
- const packageJson = await readJsonFile(packageJsonPath);
1356
- const packageId = String(packageJson?.name || "").trim();
1357
- if (!packageId) {
1358
- throw createCliError(`Invalid app-local package at ${normalizeRelativePath(appRoot, packageRoot)}: package.json missing name.`);
1359
- }
1360
-
1361
- const descriptorModule = await import(pathToFileURL(descriptorPath).href + `?t=${Date.now()}_${Math.random()}`);
1362
- const descriptor = validateAppLocalPackageDescriptorShape(descriptorModule?.default, descriptorPath, {
1363
- expectedPackageId: packageId,
1364
- fallbackVersion: String(packageJson?.version || "").trim()
1365
- });
1366
-
1367
- const relativeDir = normalizeRelativePath(appRoot, packageRoot);
1368
- const descriptorRelativePath = normalizeRelativePath(appRoot, descriptorPath);
1369
- registry.set(
1370
- packageId,
1371
- createPackageEntry({
1372
- packageId: descriptor.packageId,
1373
- version: descriptor.version,
1374
- descriptor,
1375
- rootDir: packageRoot,
1376
- relativeDir,
1377
- descriptorRelativePath,
1378
- packageJson,
1379
- sourceType: "app-local-package",
1380
- source: {
1381
- packagePath: normalizeRelativePosixPath(relativeDir),
1382
- descriptorPath: descriptorRelativePath
1383
- }
1384
- })
1385
- );
1386
- }
1387
-
1388
- return registry;
1389
- }
1390
-
1391
- async function loadCatalogPackageRegistry() {
1392
- if (!(await fileExists(CATALOG_PACKAGES_PATH))) {
1393
- return new Map();
1394
- }
1395
-
1396
- const catalog = await readJsonFile(CATALOG_PACKAGES_PATH);
1397
- const packageRecords = ensureArray(catalog?.packages);
1398
- const registry = new Map();
1399
-
1400
- for (const packageRecord of packageRecords) {
1401
- const record = ensureObject(packageRecord);
1402
- const packageId = String(record.packageId || "").trim();
1403
- const descriptorPath = `${normalizeRelativePath(CLI_PACKAGE_ROOT, CATALOG_PACKAGES_PATH)}#${packageId || "unknown"}`;
1404
- const descriptor = validatePackageDescriptorShape(record.descriptor, descriptorPath);
1405
- if (!packageId) {
1406
- throw createCliError(`Invalid catalog package entry at ${descriptorPath}: missing packageId.`);
1407
- }
1408
- if (descriptor.packageId !== packageId) {
1409
- throw createCliError(
1410
- `Invalid catalog package entry at ${descriptorPath}: descriptor packageId ${descriptor.packageId} does not match catalog packageId ${packageId}.`
1411
- );
1412
- }
1413
-
1414
- const version = String(record.version || descriptor.version || "").trim();
1415
- if (!version) {
1416
- throw createCliError(`Invalid catalog package entry at ${descriptorPath}: missing version.`);
1417
- }
1418
-
1419
- registry.set(
1420
- packageId,
1421
- createPackageEntry({
1422
- packageId,
1423
- version,
1424
- descriptor: {
1425
- ...descriptor,
1426
- version
1427
- },
1428
- rootDir: "",
1429
- relativeDir: "",
1430
- descriptorRelativePath: descriptorPath,
1431
- packageJson: {
1432
- name: packageId,
1433
- version
1434
- },
1435
- sourceType: "catalog",
1436
- source: {
1437
- descriptorPath
1438
- }
1439
- })
1440
- );
1441
- }
1442
-
1443
- return registry;
1444
- }
1445
-
1446
- async function loadPackageRegistry() {
1447
- const catalogRegistry = await loadCatalogPackageRegistry();
1448
- if (catalogRegistry.size === 0) {
1449
- throw createCliError(
1450
- "Unable to load package registry from @jskit-ai/jskit-catalog. Install it alongside @jskit-ai/jskit-cli or set JSKIT_CATALOG_PACKAGES_PATH."
1451
- );
1452
- }
1453
-
1454
- return catalogRegistry;
1455
- }
1456
-
1457
- async function loadInstalledNodeModulePackageEntry({ appRoot, packageId }) {
1458
- const normalizedPackageId = String(packageId || "").trim();
1459
- if (!normalizedPackageId) {
1460
- return null;
1461
- }
1462
-
1463
- const packageRoot = path.resolve(appRoot, "node_modules", ...normalizedPackageId.split("/"));
1464
- const packageJsonPath = path.join(packageRoot, "package.json");
1465
- if (!(await fileExists(packageJsonPath))) {
1466
- return null;
1467
- }
1468
-
1469
- const packageJson = await readJsonFile(packageJsonPath);
1470
- const resolvedPackageId = String(packageJson?.name || "").trim() || normalizedPackageId;
1471
- const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
1472
- if (!(await fileExists(descriptorPath))) {
1473
- return null;
1474
- }
1475
-
1476
- const descriptorModule = await import(pathToFileURL(descriptorPath).href + `?t=${Date.now()}_${Math.random()}`);
1477
- const descriptor = validateAppLocalPackageDescriptorShape(descriptorModule?.default, descriptorPath, {
1478
- expectedPackageId: resolvedPackageId,
1479
- fallbackVersion: String(packageJson?.version || "").trim()
1480
- });
1481
- const relativeDir = normalizeRelativePath(appRoot, packageRoot);
1482
- const descriptorRelativePath = normalizeRelativePath(appRoot, descriptorPath);
1483
-
1484
- return createPackageEntry({
1485
- packageId: descriptor.packageId,
1486
- version: descriptor.version,
1487
- descriptor,
1488
- rootDir: packageRoot,
1489
- relativeDir,
1490
- descriptorRelativePath,
1491
- packageJson,
1492
- sourceType: "npm-installed-package",
1493
- source: {
1494
- packagePath: normalizeRelativePosixPath(relativeDir),
1495
- descriptorPath: descriptorRelativePath
1496
- }
1497
- });
1498
- }
1499
-
1500
- async function resolveInstalledNodeModulePackageEntry({ appRoot, packageId }) {
1501
- const raw = String(packageId || "").trim();
1502
- if (!raw) {
1503
- return null;
1504
- }
1505
-
1506
- const candidates = [];
1507
- const seen = new Set();
1508
- const appendCandidate = (value) => {
1509
- const candidate = String(value || "").trim();
1510
- if (!candidate || seen.has(candidate)) {
1511
- return;
1512
- }
1513
- seen.add(candidate);
1514
- candidates.push(candidate);
1515
- };
1516
-
1517
- appendCandidate(raw);
1518
- appendCandidate(toScopedPackageId(raw));
1519
-
1520
- for (const candidateId of candidates) {
1521
- const entry = await loadInstalledNodeModulePackageEntry({
1522
- appRoot,
1523
- packageId: candidateId
1524
- });
1525
- if (entry) {
1526
- return entry;
1527
- }
1528
- }
1529
-
1530
- return null;
1531
- }
1532
-
1533
- async function hydratePackageRegistryFromInstalledNodeModules({
1534
- appRoot,
1535
- packageRegistry,
1536
- seedPackageIds = []
1537
- }) {
1538
- const queue = ensureArray(seedPackageIds)
1539
- .map((value) => String(value || "").trim())
1540
- .filter(Boolean);
1541
- const visited = new Set();
1542
-
1543
- while (queue.length > 0) {
1544
- const packageId = queue.shift();
1545
- if (!packageId || visited.has(packageId)) {
1546
- continue;
1547
- }
1548
- visited.add(packageId);
1549
-
1550
- let packageEntry = packageRegistry.get(packageId);
1551
- if (!packageEntry) {
1552
- const resolvedEntry = await resolveInstalledNodeModulePackageEntry({
1553
- appRoot,
1554
- packageId
1555
- });
1556
- if (!resolvedEntry) {
1557
- continue;
1558
- }
1559
-
1560
- packageRegistry.set(resolvedEntry.packageId, resolvedEntry);
1561
- packageEntry = resolvedEntry;
1562
- if (resolvedEntry.packageId !== packageId && !visited.has(resolvedEntry.packageId)) {
1563
- queue.push(resolvedEntry.packageId);
1564
- }
1565
- }
1566
-
1567
- const dependsOn = ensureArray(packageEntry?.descriptor?.dependsOn).map((value) => String(value || "").trim()).filter(Boolean);
1568
- for (const dependencyId of dependsOn) {
1569
- if (!visited.has(dependencyId)) {
1570
- queue.push(dependencyId);
1571
- }
1572
- }
1573
- }
1574
- }
1575
-
1576
- async function loadBundleRegistry() {
1577
- if (!(await fileExists(BUNDLES_ROOT))) {
1578
- return new Map();
1579
- }
1580
-
1581
- const bundles = new Map();
1582
- const entries = await readdir(BUNDLES_ROOT, { withFileTypes: true });
1583
- for (const entry of entries) {
1584
- if (!entry.isDirectory() || entry.name.startsWith(".")) {
1585
- continue;
1586
- }
1587
-
1588
- const descriptorPath = path.join(BUNDLES_ROOT, entry.name, "bundle.descriptor.mjs");
1589
- if (!(await fileExists(descriptorPath))) {
1590
- continue;
1591
- }
1592
-
1593
- const descriptorModule = await import(pathToFileURL(descriptorPath).href);
1594
- const descriptor = validateBundleDescriptorShape(descriptorModule?.default, descriptorPath);
1595
- bundles.set(descriptor.bundleId, descriptor);
1596
- }
1597
-
1598
- return bundles;
1599
- }
1600
-
1601
- function resolvePackageDependencySpecifier(packageEntry, { existingValue = "" } = {}) {
1602
- const source = ensureObject(packageEntry?.source);
1603
- const sourceType = String(source.type || packageEntry?.sourceType || "").trim();
1604
- if (sourceType === "app-local-package" || sourceType === "local-package") {
1605
- const packagePath = normalizeRelativePosixPath(String(source.packagePath || packageEntry?.relativeDir || "").trim());
1606
- if (!packagePath) {
1607
- throw createCliError(`Unable to resolve local package path for ${String(packageEntry?.packageId || "unknown package")}.`);
1608
- }
1609
- return toFileDependencySpecifier(packagePath);
1610
- }
1611
- if (sourceType === "npm-installed-package") {
1612
- const normalizedExisting = String(existingValue || "").trim();
1613
- if (normalizedExisting) {
1614
- return normalizedExisting;
1615
- }
1616
- }
1617
-
1618
- const descriptorVersion = String(packageEntry?.version || "").trim();
1619
- if (descriptorVersion) {
1620
- return normalizeJskitDependencySpecifier(packageEntry?.packageId, descriptorVersion);
1621
- }
1622
- const packageJsonVersion = String(packageEntry?.packageJson?.version || "").trim();
1623
- if (packageJsonVersion) {
1624
- return normalizeJskitDependencySpecifier(packageEntry?.packageId, packageJsonVersion);
1625
- }
1626
- throw createCliError(`Unable to resolve dependency specifier for ${String(packageEntry?.packageId || "unknown package")}.`);
1627
- }
1628
-
1629
- function normalizeJskitDependencySpecifier(packageId, dependencySpecifier) {
1630
- const normalizedPackageId = String(packageId || "").trim();
1631
- const normalizedSpecifier = String(dependencySpecifier || "").trim();
1632
- if (!normalizedSpecifier || !normalizedPackageId.startsWith("@jskit-ai/")) {
1633
- return normalizedSpecifier;
1634
- }
1635
-
1636
- const semverMatch = /^(\d+)\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?$/.exec(normalizedSpecifier);
1637
- if (!semverMatch) {
1638
- return normalizedSpecifier;
1639
- }
1640
-
1641
- return `${semverMatch[1]}.x`;
1642
- }
1643
-
1644
- function normalizePackageNameSegment(rawValue, { label = "package name" } = {}) {
1645
- const lowered = String(rawValue || "")
1646
- .trim()
1647
- .toLowerCase()
1648
- .replace(/[^a-z0-9._-]+/g, "-")
1649
- .replace(/-+/g, "-")
1650
- .replace(/^[._-]+|[._-]+$/g, "");
1651
- if (!lowered) {
1652
- throw createCliError(`Invalid ${label}. Use letters, numbers, dash, underscore, or dot.`);
1653
- }
1654
- return lowered;
1655
- }
1656
-
1657
- function normalizeScopeName(rawScope) {
1658
- const normalized = String(rawScope || "").trim().replace(/^@+/, "");
1659
- return normalizePackageNameSegment(normalized, { label: "scope" });
1660
- }
1661
-
1662
- function resolveDefaultLocalScopeFromAppName(appPackageName) {
1663
- const appName = String(appPackageName || "").trim();
1664
- if (!appName) {
1665
- return "app";
1666
- }
1667
-
1668
- const unscoped = appName.startsWith("@")
1669
- ? appName.slice(appName.indexOf("/") + 1)
1670
- : appName;
1671
- return normalizeScopeName(unscoped || "app");
1672
- }
1673
-
1674
- function normalizeRelativePosixPath(pathValue) {
1675
- return String(pathValue || "")
1676
- .trim()
1677
- .replace(/\\/g, "/")
1678
- .replace(/^\/+|\/+$/g, "")
1679
- .replace(/\/{2,}/g, "/");
1680
- }
1681
-
1682
- function toFileDependencySpecifier(relativePath) {
1683
- const normalized = normalizeRelativePosixPath(relativePath);
1684
- if (!normalized) {
1685
- throw createCliError("Cannot create file: dependency specifier from empty relative path.");
1686
- }
1687
- return `file:${normalized}`;
1688
- }
1689
-
1690
- function resolveLocalPackageId({ rawName, appPackageName, inlineOptions }) {
1691
- const explicitPackageId = String(inlineOptions["package-id"] || "").trim();
1692
- if (explicitPackageId) {
1693
- const scopedPattern = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/;
1694
- if (!scopedPattern.test(explicitPackageId)) {
1695
- throw createCliError(
1696
- `Invalid --package-id ${explicitPackageId}. Expected format: @scope/name (lowercase alphanumeric, ., _, -).`
1697
- );
1698
- }
1699
- const packageName = explicitPackageId.slice(explicitPackageId.indexOf("/") + 1);
1700
- return {
1701
- packageId: explicitPackageId,
1702
- packageDirName: normalizePackageNameSegment(packageName)
1703
- };
1704
- }
1705
-
1706
- const packageDirName = normalizePackageNameSegment(rawName);
1707
- const scopeName = String(inlineOptions.scope || "").trim()
1708
- ? normalizeScopeName(inlineOptions.scope)
1709
- : resolveDefaultLocalScopeFromAppName(appPackageName);
1710
- return {
1711
- packageId: `@${scopeName}/${packageDirName}`,
1712
- packageDirName
1713
- };
1714
- }
1715
-
1716
- function createLocalPackageDescriptorTemplate({ packageId, description }) {
1717
- return `export default Object.freeze({
1718
- packageVersion: 1,
1719
- packageId: "${packageId}",
1720
- version: "0.1.0",
1721
- kind: "runtime",
1722
- description: ${JSON.stringify(String(description || ""))},
1723
- dependsOn: [
1724
- // "@jskit-ai/kernel"
1725
- ],
1726
- capabilities: {
1727
- provides: [
1728
- // "example.feature"
1729
- ],
1730
- requires: [
1731
- // "example.dependency"
1732
- ]
1733
- },
1734
- options: {
1735
- // "example-option": {
1736
- // required: true,
1737
- // promptLabel: "Enter option value",
1738
- // promptHint: "Used by mutations.text interpolation",
1739
- // defaultValue: "example"
1740
- // }
1741
- },
1742
- runtime: {
1743
- server: {
1744
- providers: [
1745
- // {
1746
- // entrypoint: "src/server/providers/ExampleServerProvider.js",
1747
- // export: "ExampleServerProvider"
1748
- // }
1749
- ]
1750
- },
1751
- client: {
1752
- providers: [
1753
- // {
1754
- // entrypoint: "src/client/providers/ExampleClientProvider.js",
1755
- // export: "ExampleClientProvider"
1756
- // }
1757
- ]
1758
- }
1759
- },
1760
- metadata: {
1761
- server: {
1762
- routes: [
1763
- // {
1764
- // method: "GET",
1765
- // path: "/api/example",
1766
- // summary: "Describe server route validator"
1767
- // }
1768
- ]
1769
- },
1770
- ui: {
1771
- routes: [
1772
- // {
1773
- // id: "example.route",
1774
- // path: "/example",
1775
- // scope: "global",
1776
- // name: "example-route",
1777
- // componentKey: "example-route",
1778
- // autoRegister: true,
1779
- // guard: {
1780
- // policy: "public"
1781
- // },
1782
- // purpose: "Describe what this route is for."
1783
- // }
1784
- ],
1785
- elements: [
1786
- // {
1787
- // key: "example-route",
1788
- // export: "ExampleView",
1789
- // entrypoint: "src/client/views/ExampleView.vue",
1790
- // purpose: "UI element exposed by this package."
1791
- // }
1792
- ],
1793
- overrides: [
1794
- // {
1795
- // targetId: "some.existing.route",
1796
- // mode: "replace",
1797
- // reason: "Explain override intent."
1798
- // }
1799
- ]
1800
- }
1801
- },
1802
- mutations: {
1803
- dependencies: {
1804
- runtime: {
1805
- // "@example/runtime-dependency": "^1.0.0"
1806
- },
1807
- dev: {
1808
- // "@example/dev-dependency": "^1.0.0"
1809
- }
1810
- },
1811
- packageJson: {
1812
- scripts: {
1813
- // "lint:example": "eslint src/example"
1814
- }
1815
- },
1816
- procfile: {
1817
- // worker: "node ./bin/worker.js"
1818
- },
1819
- vite: {
1820
- proxy: [
1821
- // {
1822
- // id: "example-socket-proxy",
1823
- // path: "/socket.io",
1824
- // changeOrigin: true,
1825
- // ws: true,
1826
- // target: "http://localhost:3000",
1827
- // reason: "Explain why this proxy is needed."
1828
- // }
1829
- ]
1830
- },
1831
- text: [
1832
- // {
1833
- // op: "upsert-env",
1834
- // file: ".env",
1835
- // key: "EXAMPLE_ENV",
1836
- // value: "\${option:example-option}",
1837
- // reason: "Explain why this env var is needed.",
1838
- // category: "runtime-config",
1839
- // id: "example-env"
1840
- // }
1841
- ],
1842
- files: [
1843
- // {
1844
- // from: "templates/src/pages/example/index.vue",
1845
- // to: "src/pages/example/index.vue",
1846
- // reason: "Explain what is scaffolded.",
1847
- // category: "example",
1848
- // id: "example-file"
1849
- // }
1850
- ]
1851
- }
1852
- });
1853
- `;
1854
- }
1855
-
1856
- function createLocalPackageScaffoldFiles({ packageId, packageDescription }) {
1857
- return [
1858
- {
1859
- relativePath: "package.json",
1860
- content: `${JSON.stringify(
1861
- {
1862
- name: packageId,
1863
- version: "0.1.0",
1864
- private: true,
1865
- type: "module",
1866
- exports: {
1867
- ".": "./src/index.js",
1868
- "./client": "./src/client/index.js",
1869
- "./server": "./src/server/index.js",
1870
- "./shared": "./src/shared/index.js"
1871
- }
1872
- },
1873
- null,
1874
- 2
1875
- )}\n`
1876
- },
1877
- {
1878
- relativePath: "package.descriptor.mjs",
1879
- content: createLocalPackageDescriptorTemplate({
1880
- packageId,
1881
- description: packageDescription
1882
- })
1883
- },
1884
- {
1885
- relativePath: "src/index.js",
1886
- content: "export {};\n"
1887
- },
1888
- {
1889
- relativePath: "src/server/index.js",
1890
- content: "export {};\n"
1891
- },
1892
- {
1893
- relativePath: "src/client/index.js",
1894
- content: [
1895
- "const routeComponents = Object.freeze({});",
1896
- "",
1897
- "async function bootClient({ logger } = {}) {",
1898
- " if (logger && typeof logger.debug === \"function\") {",
1899
- ` logger.debug({ packageId: ${JSON.stringify(packageId)} }, "bootClient executed.");`,
1900
- " }",
1901
- "}",
1902
- "",
1903
- "export { routeComponents, bootClient };",
1904
- ""
1905
- ].join("\n")
1906
- },
1907
- {
1908
- relativePath: "src/shared/index.js",
1909
- content: "export {};\n"
1910
- },
1911
- {
1912
- relativePath: "README.md",
1913
- content: [
1914
- `# ${packageId}`,
1915
- "",
1916
- "App-local JSKIT module scaffold.",
1917
- "",
1918
- "## Next Steps",
1919
- "",
1920
- "- Define runtime providers in `package.descriptor.mjs`.",
1921
- "- Add client/server exports under `src/`.",
1922
- "- Keep package version in sync with descriptor version.",
1923
- ""
1924
- ].join("\n")
1925
- }
1926
- ];
1927
- }
1928
-
1929
- function resolveLocalDependencyOrder(initialPackageIds, packageRegistry) {
1930
- const ordered = [];
1931
- const visited = new Set();
1932
- const visiting = new Set();
1933
- const externalDependencies = new Set();
1934
-
1935
- function visit(packageId, lineage = []) {
1936
- if (visited.has(packageId)) {
1937
- return;
1938
- }
1939
- if (visiting.has(packageId)) {
1940
- const cyclePath = [...lineage, packageId].join(" -> ");
1941
- throw createCliError(`Dependency cycle detected: ${cyclePath}`);
1942
- }
1943
-
1944
- const packageEntry = packageRegistry.get(packageId);
1945
- if (!packageEntry) {
1946
- throw createCliError(`Unknown package: ${packageId}`);
1947
- }
1948
-
1949
- visiting.add(packageId);
1950
- for (const dependencyId of ensureArray(packageEntry.descriptor.dependsOn).map((value) => String(value))) {
1951
- if (packageRegistry.has(dependencyId)) {
1952
- visit(dependencyId, [...lineage, packageId]);
1953
- } else {
1954
- externalDependencies.add(dependencyId);
1955
- }
1956
- }
1957
- visiting.delete(packageId);
1958
- visited.add(packageId);
1959
- ordered.push(packageId);
1960
- }
1961
-
1962
- for (const packageId of initialPackageIds) {
1963
- visit(packageId);
1964
- }
1965
-
1966
- return {
1967
- ordered,
1968
- externalDependencies: sortStrings([...externalDependencies])
1969
- };
1970
- }
1971
-
1972
- function listDeclaredCapabilities(capabilitiesSection, fieldName) {
1973
- const section = ensureObject(capabilitiesSection);
1974
- const source = ensureArray(section[fieldName]);
1975
- const normalized = [];
1976
- const seen = new Set();
1977
- for (const value of source) {
1978
- const capabilityId = String(value || "").trim();
1979
- if (!capabilityId || seen.has(capabilityId)) {
1980
- continue;
1981
- }
1982
- seen.add(capabilityId);
1983
- normalized.push(capabilityId);
1984
- }
1985
- return normalized;
1986
- }
1987
-
1988
- function buildCapabilityGraph(packageRegistry) {
1989
- const graph = new Map();
1990
- const ensureNode = (capabilityId) => {
1991
- if (!graph.has(capabilityId)) {
1992
- graph.set(capabilityId, {
1993
- providers: new Set(),
1994
- requirers: new Set()
1995
- });
1996
- }
1997
- return graph.get(capabilityId);
1998
- };
1999
-
2000
- for (const [packageId, packageEntry] of packageRegistry.entries()) {
2001
- const capabilities = ensureObject(packageEntry?.descriptor?.capabilities);
2002
- for (const capabilityId of listDeclaredCapabilities(capabilities, "provides")) {
2003
- ensureNode(capabilityId).providers.add(packageId);
2004
- }
2005
- for (const capabilityId of listDeclaredCapabilities(capabilities, "requires")) {
2006
- ensureNode(capabilityId).requirers.add(packageId);
2007
- }
2008
- }
2009
-
2010
- for (const [capabilityId, providers] of Object.entries(BUILTIN_CAPABILITY_PROVIDERS)) {
2011
- const node = ensureNode(capabilityId);
2012
- for (const providerId of ensureArray(providers).map((value) => String(value || "").trim()).filter(Boolean)) {
2013
- node.providers.add(providerId);
2014
- }
2015
- }
2016
-
2017
- const normalizedGraph = new Map();
2018
- for (const [capabilityId, node] of graph.entries()) {
2019
- normalizedGraph.set(capabilityId, {
2020
- providers: sortStrings([...node.providers]),
2021
- requirers: sortStrings([...node.requirers])
2022
- });
2023
- }
2024
- return normalizedGraph;
2025
- }
2026
-
2027
- function createCapabilityPackageDetail(packageId, packageRegistry) {
2028
- const packageEntry = packageRegistry.get(packageId);
2029
- return {
2030
- packageId,
2031
- version: String(packageEntry?.version || packageEntry?.descriptor?.version || "").trim(),
2032
- descriptorPath: String(packageEntry?.descriptorRelativePath || "").trim()
2033
- };
2034
- }
2035
-
2036
- function buildCapabilityDetailsForPackage({ packageRegistry, packageId, dependsOn = [], provides = [], requires = [] }) {
2037
- const graph = buildCapabilityGraph(packageRegistry);
2038
- const dependsOnSet = new Set(ensureArray(dependsOn).map((value) => String(value || "").trim()).filter(Boolean));
2039
-
2040
- function buildCapabilityRecord(capabilityId) {
2041
- const node = graph.get(capabilityId) || {
2042
- providers: [],
2043
- requirers: []
2044
- };
2045
- const providers = sortStrings(ensureArray(node.providers));
2046
- const requirers = sortStrings(ensureArray(node.requirers));
2047
- const providersInDependsOn = providers.filter((providerId) => dependsOnSet.has(providerId));
2048
- return {
2049
- capabilityId,
2050
- providers,
2051
- requirers,
2052
- providersInDependsOn,
2053
- providerDetails: providers.map((providerId) => createCapabilityPackageDetail(providerId, packageRegistry)),
2054
- requirerDetails: requirers.map((requirerId) => createCapabilityPackageDetail(requirerId, packageRegistry)),
2055
- isProvidedByCurrentPackage: providers.includes(packageId),
2056
- isRequiredByCurrentPackage: requirers.includes(packageId)
2057
- };
2058
- }
2059
-
2060
- return {
2061
- provides: ensureArray(provides).map((capabilityId) => buildCapabilityRecord(capabilityId)),
2062
- requires: ensureArray(requires).map((capabilityId) => buildCapabilityRecord(capabilityId))
2063
- };
2064
- }
2065
-
2066
- function escapeRegexLiteral(value) {
2067
- return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2068
- }
2069
-
2070
- function parseQuotedStringLiteral(value) {
2071
- const source = String(value || "").trim();
2072
- if (source.length < 2) {
2073
- return null;
2074
- }
2075
-
2076
- const quote = source[0];
2077
- if ((quote !== "\"" && quote !== "'") || source[source.length - 1] !== quote) {
2078
- return null;
2079
- }
2080
-
2081
- if (quote === "\"") {
2082
- try {
2083
- return JSON.parse(source);
2084
- } catch {
2085
- return null;
2086
- }
2087
- }
2088
-
2089
- return source
2090
- .slice(1, -1)
2091
- .replace(/\\\\/g, "\\")
2092
- .replace(/\\'/g, "'")
2093
- .replace(/\\"/g, "\"")
2094
- .replace(/\\n/g, "\n")
2095
- .replace(/\\r/g, "\r")
2096
- .replace(/\\t/g, "\t");
2097
- }
2098
-
2099
- function resolveLineNumberAtIndex(source, index) {
2100
- const text = String(source || "");
2101
- const maxIndex = Math.max(0, Math.min(Number(index) || 0, text.length));
2102
- let line = 1;
2103
- for (let cursor = 0; cursor < maxIndex; cursor += 1) {
2104
- if (text[cursor] === "\n") {
2105
- line += 1;
2106
- }
2107
- }
2108
- return line;
2109
- }
2110
-
2111
- function findMatchingBraceIndex(source, openBraceIndex) {
2112
- const text = String(source || "");
2113
- const startIndex = Number(openBraceIndex);
2114
- if (!Number.isInteger(startIndex) || startIndex < 0 || startIndex >= text.length || text[startIndex] !== "{") {
2115
- return -1;
2116
- }
2117
-
2118
- let depth = 0;
2119
- let inSingleQuote = false;
2120
- let inDoubleQuote = false;
2121
- let inTemplateQuote = false;
2122
- let inLineComment = false;
2123
- let inBlockComment = false;
2124
-
2125
- for (let cursor = startIndex; cursor < text.length; cursor += 1) {
2126
- const current = text[cursor];
2127
- const next = text[cursor + 1] || "";
2128
-
2129
- if (inLineComment) {
2130
- if (current === "\n") {
2131
- inLineComment = false;
2132
- }
2133
- continue;
2134
- }
2135
-
2136
- if (inBlockComment) {
2137
- if (current === "*" && next === "/") {
2138
- inBlockComment = false;
2139
- cursor += 1;
2140
- }
2141
- continue;
2142
- }
2143
-
2144
- if (inSingleQuote) {
2145
- if (current === "\\") {
2146
- cursor += 1;
2147
- continue;
2148
- }
2149
- if (current === "'") {
2150
- inSingleQuote = false;
2151
- }
2152
- continue;
2153
- }
2154
-
2155
- if (inDoubleQuote) {
2156
- if (current === "\\") {
2157
- cursor += 1;
2158
- continue;
2159
- }
2160
- if (current === "\"") {
2161
- inDoubleQuote = false;
2162
- }
2163
- continue;
2164
- }
2165
-
2166
- if (inTemplateQuote) {
2167
- if (current === "\\") {
2168
- cursor += 1;
2169
- continue;
2170
- }
2171
- if (current === "`") {
2172
- inTemplateQuote = false;
2173
- }
2174
- continue;
2175
- }
2176
-
2177
- if (current === "/" && next === "/") {
2178
- inLineComment = true;
2179
- cursor += 1;
2180
- continue;
2181
- }
2182
- if (current === "/" && next === "*") {
2183
- inBlockComment = true;
2184
- cursor += 1;
2185
- continue;
2186
- }
2187
- if (current === "'") {
2188
- inSingleQuote = true;
2189
- continue;
2190
- }
2191
- if (current === "\"") {
2192
- inDoubleQuote = true;
2193
- continue;
2194
- }
2195
- if (current === "`") {
2196
- inTemplateQuote = true;
2197
- continue;
2198
- }
2199
-
2200
- if (current === "{") {
2201
- depth += 1;
2202
- continue;
2203
- }
2204
- if (current === "}") {
2205
- depth -= 1;
2206
- if (depth === 0) {
2207
- return cursor;
2208
- }
2209
- }
2210
- }
2211
-
2212
- return -1;
2213
- }
2214
-
2215
- function extractProviderLifecycleMethodRanges(source, providerExportName) {
2216
- const text = String(source || "");
2217
- const providerName = String(providerExportName || "").trim();
2218
- if (!text) {
2219
- return [];
2220
- }
2221
-
2222
- const fallback = [
2223
- {
2224
- lifecycle: "unknown",
2225
- start: 0,
2226
- end: text.length
2227
- }
2228
- ];
2229
- if (!providerName) {
2230
- return fallback;
2231
- }
2232
-
2233
- const classPattern = new RegExp(`\\bclass\\s+${escapeRegexLiteral(providerName)}\\b`);
2234
- const classMatch = classPattern.exec(text);
2235
- if (!classMatch) {
2236
- return fallback;
2237
- }
2238
-
2239
- const classOpenBraceIndex = text.indexOf("{", classMatch.index + classMatch[0].length);
2240
- if (classOpenBraceIndex < 0) {
2241
- return fallback;
2242
- }
2243
- const classCloseBraceIndex = findMatchingBraceIndex(text, classOpenBraceIndex);
2244
- if (classCloseBraceIndex < 0) {
2245
- return fallback;
2246
- }
2247
-
2248
- const classBody = text.slice(classOpenBraceIndex + 1, classCloseBraceIndex);
2249
- const methodPattern = /\b(?:async\s+)?(register|boot)\s*\([^)]*\)\s*\{/g;
2250
- const ranges = [];
2251
- let methodMatch = methodPattern.exec(classBody);
2252
- while (methodMatch) {
2253
- const lifecycle = String(methodMatch[1] || "").trim() || "unknown";
2254
- const methodOpenOffset = methodMatch[0].lastIndexOf("{");
2255
- if (methodOpenOffset < 0) {
2256
- methodMatch = methodPattern.exec(classBody);
2257
- continue;
2258
- }
2259
- const methodOpenIndex = classOpenBraceIndex + 1 + methodMatch.index + methodOpenOffset;
2260
- const methodCloseIndex = findMatchingBraceIndex(text, methodOpenIndex);
2261
- if (methodCloseIndex < 0) {
2262
- methodMatch = methodPattern.exec(classBody);
2263
- continue;
2264
- }
2265
- ranges.push({
2266
- lifecycle,
2267
- start: methodOpenIndex + 1,
2268
- end: methodCloseIndex
2269
- });
2270
- methodMatch = methodPattern.exec(classBody);
2271
- }
2272
-
2273
- if (ranges.length > 0) {
2274
- return ranges;
2275
- }
2276
- return [
2277
- {
2278
- lifecycle: "class",
2279
- start: classOpenBraceIndex + 1,
2280
- end: classCloseBraceIndex
2281
- }
2282
- ];
2283
- }
2284
-
2285
- function collectConstTokenAssignments(source) {
2286
- const text = String(source || "");
2287
- const assignments = new Map();
2288
- const pattern = /^\s*const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*([^;]+);\s*$/gm;
2289
- let match = pattern.exec(text);
2290
- while (match) {
2291
- const identifier = String(match[1] || "").trim();
2292
- const expression = String(match[2] || "").trim();
2293
- if (identifier && expression) {
2294
- assignments.set(identifier, expression);
2295
- }
2296
- match = pattern.exec(text);
2297
- }
2298
- return assignments;
2299
- }
2300
-
2301
- function resolveTokenFromExpression(expression, constAssignments, visited = new Set()) {
2302
- let normalized = String(expression || "").trim();
2303
- if (!normalized) {
2304
- return {
2305
- token: "",
2306
- resolved: false,
2307
- kind: "empty"
2308
- };
2309
- }
2310
-
2311
- while (normalized.startsWith("(") && normalized.endsWith(")")) {
2312
- normalized = normalized.slice(1, -1).trim();
2313
- }
2314
-
2315
- const quoted = parseQuotedStringLiteral(normalized);
2316
- if (quoted !== null) {
2317
- return {
2318
- token: quoted,
2319
- resolved: true,
2320
- kind: "string"
2321
- };
2322
- }
2323
-
2324
- const symbolMatch = /^Symbol\.for\(\s*(['"])(.*?)\1\s*\)$/.exec(normalized);
2325
- if (symbolMatch) {
2326
- return {
2327
- token: `Symbol.for(${symbolMatch[2]})`,
2328
- resolved: true,
2329
- kind: "symbol"
2330
- };
2331
- }
2332
-
2333
- if (/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)+$/.test(normalized)) {
2334
- return {
2335
- token: normalized,
2336
- resolved: true,
2337
- kind: "member"
2338
- };
2339
- }
2340
-
2341
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(normalized)) {
2342
- const identifier = normalized;
2343
- if (visited.has(identifier)) {
2344
- return {
2345
- token: identifier,
2346
- resolved: false,
2347
- kind: "cyclic-identifier"
2348
- };
2349
- }
2350
- const nextExpression = constAssignments.get(identifier);
2351
- if (nextExpression) {
2352
- return resolveTokenFromExpression(nextExpression, constAssignments, new Set([...visited, identifier]));
2353
- }
2354
- return {
2355
- token: identifier,
2356
- resolved: false,
2357
- kind: "identifier"
2358
- };
2359
- }
2360
-
2361
- return {
2362
- token: normalized,
2363
- resolved: false,
2364
- kind: "expression"
2365
- };
2366
- }
2367
-
2368
- function collectContainerBindingsFromProviderSource({ source, providerLabel, entrypoint, providerExportName }) {
2369
- const text = String(source || "");
2370
- if (!text) {
2371
- return [];
2372
- }
2373
-
2374
- const constAssignments = collectConstTokenAssignments(text);
2375
- const methodRanges = extractProviderLifecycleMethodRanges(text, providerExportName);
2376
- const records = [];
2377
-
2378
- for (const range of methodRanges) {
2379
- const lifecycle = String(range?.lifecycle || "unknown").trim() || "unknown";
2380
- const start = Number(range?.start) || 0;
2381
- const end = Number(range?.end) || text.length;
2382
- const slice = text.slice(start, end);
2383
- const bindingPattern = /\bapp\.(singleton|bind|scoped|instance)\s*\(\s*([\s\S]*?)\s*,/g;
2384
- let match = bindingPattern.exec(slice);
2385
- while (match) {
2386
- const binding = String(match[1] || "").trim();
2387
- const tokenExpression = String(match[2] || "")
2388
- .replace(/\s+/g, " ")
2389
- .trim();
2390
- if (!tokenExpression) {
2391
- match = bindingPattern.exec(slice);
2392
- continue;
2393
- }
2394
- const tokenResolution = resolveTokenFromExpression(tokenExpression, constAssignments);
2395
- const line = resolveLineNumberAtIndex(text, start + match.index);
2396
- records.push({
2397
- provider: providerLabel,
2398
- entrypoint: String(entrypoint || "").trim(),
2399
- exportName: String(providerExportName || "").trim(),
2400
- lifecycle,
2401
- binding,
2402
- token: String(tokenResolution.token || "").trim(),
2403
- tokenExpression,
2404
- tokenResolved: Boolean(tokenResolution.resolved),
2405
- tokenKind: String(tokenResolution.kind || "").trim(),
2406
- location: `${String(entrypoint || "").trim()}:${line}`,
2407
- line
2408
- });
2409
- match = bindingPattern.exec(slice);
2410
- }
2411
- }
2412
-
2413
- return records;
2414
- }
2415
-
2416
- function collectPackageExportEntries(exportsField) {
2417
- const entries = [];
2418
- const normalizeExportSubpath = (subpath) => {
2419
- const normalized = String(subpath || ".").trim() || ".";
2420
- if (normalized === "." || normalized === "./") {
2421
- return {
2422
- normalized: ".",
2423
- segments: []
2424
- };
2425
- }
2426
-
2427
- const withoutPrefix = normalized.startsWith("./") ? normalized.slice(2) : normalized;
2428
- const segments = withoutPrefix.split("/").map((value) => String(value || "").trim()).filter(Boolean);
2429
- return {
2430
- normalized: normalized.startsWith("./") ? normalized : `./${withoutPrefix}`,
2431
- segments
2432
- };
2433
- };
2434
-
2435
- const resolveSubpathSortPriority = (subpath) => {
2436
- const normalized = normalizeExportSubpath(subpath);
2437
- const firstSegment = String(normalized.segments[0] || "").trim();
2438
- if (firstSegment === "client") {
2439
- return 0;
2440
- }
2441
- if (firstSegment === "server") {
2442
- return 1;
2443
- }
2444
- if (firstSegment === "shared") {
2445
- return 2;
2446
- }
2447
- if (normalized.normalized === ".") {
2448
- return 3;
2449
- }
2450
- return 10;
2451
- };
2452
-
2453
- const appendEntry = (subpath, conditions, target) => {
2454
- const normalizedSubpath = String(subpath || ".").trim() || ".";
2455
- const normalizedTarget = String(target || "").trim();
2456
- if (!normalizedTarget) {
2457
- return;
2458
- }
2459
- const normalizedConditions = ensureArray(conditions).map((value) => String(value || "").trim()).filter(Boolean);
2460
- entries.push({
2461
- subpath: normalizedSubpath,
2462
- condition: normalizedConditions.length > 0 ? normalizedConditions.join(".") : "default",
2463
- target: normalizedTarget
2464
- });
2465
- };
2466
-
2467
- const visit = (subpath, value, conditionStack = []) => {
2468
- if (typeof value === "string") {
2469
- appendEntry(subpath, conditionStack, value);
2470
- return;
2471
- }
2472
- if (Array.isArray(value)) {
2473
- for (const item of value) {
2474
- visit(subpath, item, conditionStack);
2475
- }
2476
- return;
2477
- }
2478
- if (!value || typeof value !== "object") {
2479
- return;
2480
- }
2481
- for (const [conditionName, nested] of Object.entries(value)) {
2482
- visit(subpath, nested, [...conditionStack, conditionName]);
2483
- }
2484
- };
2485
-
2486
- if (typeof exportsField === "string" || Array.isArray(exportsField)) {
2487
- visit(".", exportsField, []);
2488
- } else if (exportsField && typeof exportsField === "object") {
2489
- const root = ensureObject(exportsField);
2490
- const rootKeys = Object.keys(root);
2491
- const hasSubpathKeys = rootKeys.some((key) => key.startsWith("."));
2492
- if (hasSubpathKeys) {
2493
- for (const [subpath, value] of Object.entries(root)) {
2494
- visit(subpath, value, []);
2495
- }
2496
- } else {
2497
- visit(".", root, []);
2498
- }
2499
- }
2500
-
2501
- const deduplicated = [];
2502
- const seen = new Set();
2503
- for (const entry of entries) {
2504
- const key = `${entry.subpath}::${entry.condition}::${entry.target}`;
2505
- if (seen.has(key)) {
2506
- continue;
2507
- }
2508
- seen.add(key);
2509
- deduplicated.push(entry);
2510
- }
2511
- return deduplicated.sort((left, right) => {
2512
- const leftPriority = resolveSubpathSortPriority(left.subpath);
2513
- const rightPriority = resolveSubpathSortPriority(right.subpath);
2514
- if (leftPriority !== rightPriority) {
2515
- return leftPriority - rightPriority;
2516
- }
2517
-
2518
- const leftParts = normalizeExportSubpath(left.subpath);
2519
- const rightParts = normalizeExportSubpath(right.subpath);
2520
- const leftRoot = String(leftParts.segments[0] || "");
2521
- const rightRoot = String(rightParts.segments[0] || "");
2522
- const rootComparison = leftRoot.localeCompare(rightRoot);
2523
- if (rootComparison !== 0) {
2524
- return rootComparison;
2525
- }
2526
-
2527
- const depthComparison = leftParts.segments.length - rightParts.segments.length;
2528
- if (depthComparison !== 0) {
2529
- return depthComparison;
2530
- }
2531
-
2532
- const subpathComparison = left.subpath.localeCompare(right.subpath);
2533
- if (subpathComparison !== 0) {
2534
- return subpathComparison;
2535
- }
2536
- const conditionComparison = left.condition.localeCompare(right.condition);
2537
- if (conditionComparison !== 0) {
2538
- return conditionComparison;
2539
- }
2540
- return left.target.localeCompare(right.target);
2541
- });
2542
- }
2543
-
2544
- async function describePackageExports({ packageRoot, packageJson }) {
2545
- const rootDir = String(packageRoot || "").trim();
2546
- if (!rootDir) {
2547
- return [];
2548
- }
2549
-
2550
- const exportsField = ensureObject(packageJson).exports;
2551
- const entries = collectPackageExportEntries(exportsField);
2552
- const records = [];
2553
-
2554
- for (const entry of entries) {
2555
- const subpath = String(entry.subpath || ".").trim() || ".";
2556
- const condition = String(entry.condition || "default").trim() || "default";
2557
- const target = String(entry.target || "").trim();
2558
- const isPattern = subpath.includes("*") || target.includes("*");
2559
- const isRelativeTarget = target.startsWith("./");
2560
- let targetExists = null;
2561
- if (isRelativeTarget && !isPattern) {
2562
- const absoluteTargetPath = path.resolve(rootDir, target);
2563
- targetExists = await fileExists(absoluteTargetPath);
2564
- }
2565
-
2566
- let targetType = "external";
2567
- if (isPattern) {
2568
- targetType = "pattern";
2569
- } else if (isRelativeTarget) {
2570
- targetType = "file";
2571
- }
2572
-
2573
- records.push({
2574
- subpath,
2575
- condition,
2576
- target,
2577
- targetType,
2578
- targetExists
2579
- });
2580
- }
2581
-
2582
- return records;
2583
- }
2584
-
2585
- function parseNamedExportSpecifiers(specifierSource) {
2586
- const source = String(specifierSource || "");
2587
- return source
2588
- .split(",")
2589
- .map((entry) => entry.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/g, "").trim())
2590
- .filter(Boolean)
2591
- .map((entry) => entry.replace(/\s+/g, " "))
2592
- .map((entry) => {
2593
- const aliasMatch = /^(.+?)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/.exec(entry);
2594
- if (aliasMatch) {
2595
- return aliasMatch[2];
2596
- }
2597
- return entry;
2598
- })
2599
- .filter((entry) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(entry));
2600
- }
2601
-
2602
- function parseExportedSymbolsFromSource(source) {
2603
- const text = String(source || "");
2604
- const symbols = new Set();
2605
- const starReExports = new Set();
2606
- const namedReExports = new Set();
2607
-
2608
- const namespaceStarPattern = /export\s+\*\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+["']([^"']+)["']\s*;?/g;
2609
- let match = namespaceStarPattern.exec(text);
2610
- while (match) {
2611
- symbols.add(String(match[1] || "").trim());
2612
- starReExports.add(String(match[2] || "").trim());
2613
- match = namespaceStarPattern.exec(text);
2614
- }
2615
-
2616
- const starPattern = /export\s+\*\s+from\s+["']([^"']+)["']\s*;?/g;
2617
- match = starPattern.exec(text);
2618
- while (match) {
2619
- starReExports.add(String(match[1] || "").trim());
2620
- match = starPattern.exec(text);
2621
- }
2622
-
2623
- const namedPattern = /export\s*\{([\s\S]*?)\}\s*(?:from\s*["']([^"']+)["'])?\s*;?/g;
2624
- match = namedPattern.exec(text);
2625
- while (match) {
2626
- const listSource = String(match[1] || "");
2627
- for (const symbol of parseNamedExportSpecifiers(listSource)) {
2628
- symbols.add(symbol);
2629
- }
2630
- if (match[2]) {
2631
- namedReExports.add(String(match[2] || "").trim());
2632
- }
2633
- match = namedPattern.exec(text);
2634
- }
2635
-
2636
- const functionPattern = /export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
2637
- match = functionPattern.exec(text);
2638
- while (match) {
2639
- symbols.add(String(match[1] || "").trim());
2640
- match = functionPattern.exec(text);
2641
- }
2642
-
2643
- const classPattern = /export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/g;
2644
- match = classPattern.exec(text);
2645
- while (match) {
2646
- symbols.add(String(match[1] || "").trim());
2647
- match = classPattern.exec(text);
2648
- }
2649
-
2650
- const variablePattern = /export\s+(?:const|let|var)\s+([\s\S]*?);/g;
2651
- match = variablePattern.exec(text);
2652
- while (match) {
2653
- const declaration = String(match[1] || "");
2654
- const names = declaration.split(",").map((entry) => String(entry || "").trim());
2655
- for (const name of names) {
2656
- const declarationMatch = /^([A-Za-z_$][A-Za-z0-9_$]*)\b/.exec(name);
2657
- if (declarationMatch) {
2658
- symbols.add(String(declarationMatch[1] || "").trim());
2659
- }
2660
- }
2661
- match = variablePattern.exec(text);
2662
- }
2663
-
2664
- const hasDefaultExport = /\bexport\s+default\b/.test(text);
2665
- return {
2666
- symbols: sortStrings([...symbols]),
2667
- starReExports: sortStrings([...starReExports]),
2668
- namedReExports: sortStrings([...namedReExports]),
2669
- hasDefaultExport
2670
- };
2671
- }
2672
-
2673
- function classifyExportedSymbols(symbols = []) {
2674
- const source = ensureArray(symbols).map((value) => String(value || "").trim()).filter(Boolean);
2675
- const providers = [];
2676
- const constants = [];
2677
- const functions = [];
2678
- const classesOrTypes = [];
2679
- const internals = [];
2680
- const others = [];
2681
-
2682
- for (const symbol of source) {
2683
- if (/Provider$/.test(symbol)) {
2684
- providers.push(symbol);
2685
- continue;
2686
- }
2687
- if (/^__/.test(symbol)) {
2688
- internals.push(symbol);
2689
- continue;
2690
- }
2691
- if (/^[A-Z0-9_]+$/.test(symbol)) {
2692
- constants.push(symbol);
2693
- continue;
2694
- }
2695
- if (/^[a-z]/.test(symbol)) {
2696
- functions.push(symbol);
2697
- continue;
2698
- }
2699
- if (/^[A-Z]/.test(symbol)) {
2700
- classesOrTypes.push(symbol);
2701
- continue;
2702
- }
2703
- others.push(symbol);
2704
- }
2705
-
2706
- return {
2707
- providers: sortStrings(providers),
2708
- constants: sortStrings(constants),
2709
- functions: sortStrings(functions),
2710
- classesOrTypes: sortStrings(classesOrTypes),
2711
- internals: sortStrings(internals),
2712
- others: sortStrings(others)
2713
- };
2714
- }
2715
-
2716
- function formatPackageSubpathImport(packageId, subpath) {
2717
- const normalizedPackageId = String(packageId || "").trim();
2718
- const normalizedSubpath = String(subpath || "").trim();
2719
- if (!normalizedPackageId) {
2720
- return normalizedSubpath;
2721
- }
2722
- if (!normalizedSubpath || normalizedSubpath === ".") {
2723
- return normalizedPackageId;
2724
- }
2725
- if (normalizedSubpath.startsWith("./")) {
2726
- return `${normalizedPackageId}/${normalizedSubpath.slice(2)}`;
2727
- }
2728
- if (normalizedSubpath.startsWith("/")) {
2729
- return `${normalizedPackageId}${normalizedSubpath}`;
2730
- }
2731
- return `${normalizedPackageId}/${normalizedSubpath}`;
2732
- }
2733
-
2734
- function normalizePlacementOutlets(value) {
2735
- const outlets = [];
2736
- const source = ensureArray(value);
2737
- for (const entry of source) {
2738
- const record = ensureObject(entry);
2739
- const host = String(record.host || "").trim();
2740
- const position = String(record.position || "").trim();
2741
- if (!host || !position) {
2742
- continue;
2743
- }
2744
-
2745
- const surfaces = [...new Set(ensureArray(record.surfaces).map((item) => String(item || "").trim()).filter(Boolean))];
2746
- const description = String(record.description || "").trim();
2747
- const sourceLabel = String(record.source || "").trim();
2748
- outlets.push(
2749
- Object.freeze({
2750
- host,
2751
- position,
2752
- surfaces: Object.freeze(surfaces),
2753
- description,
2754
- source: sourceLabel
2755
- })
2756
- );
2757
- }
2758
-
2759
- return Object.freeze(
2760
- [...outlets].sort((left, right) => {
2761
- const hostCompare = left.host.localeCompare(right.host);
2762
- if (hostCompare !== 0) {
2763
- return hostCompare;
2764
- }
2765
- return left.position.localeCompare(right.position);
2766
- })
2767
- );
2768
- }
2769
-
2770
- function normalizePlacementContributions(value) {
2771
- const contributions = [];
2772
- for (const entry of ensureArray(value)) {
2773
- const record = ensureObject(entry);
2774
- const id = String(record.id || "").trim();
2775
- const host = String(record.host || "").trim();
2776
- const position = String(record.position || "").trim();
2777
- if (!id || !host || !position) {
2778
- continue;
2779
- }
2780
-
2781
- const surfaces = [...new Set(ensureArray(record.surfaces).map((item) => String(item || "").trim()).filter(Boolean))];
2782
- const componentToken = String(record.componentToken || "").trim();
2783
- const when = String(record.when || "").trim();
2784
- const description = String(record.description || "").trim();
2785
- const source = String(record.source || "").trim();
2786
- const parsedOrder = Number(record.order);
2787
- const order = Number.isFinite(parsedOrder) ? Math.trunc(parsedOrder) : null;
2788
- contributions.push(
2789
- Object.freeze({
2790
- id,
2791
- host,
2792
- position,
2793
- surfaces: Object.freeze(surfaces),
2794
- order,
2795
- componentToken,
2796
- when,
2797
- description,
2798
- source
2799
- })
2800
- );
2801
- }
2802
-
2803
- return Object.freeze(
2804
- [...contributions].sort((left, right) => {
2805
- const hostCompare = left.host.localeCompare(right.host);
2806
- if (hostCompare !== 0) {
2807
- return hostCompare;
2808
- }
2809
- const positionCompare = left.position.localeCompare(right.position);
2810
- if (positionCompare !== 0) {
2811
- return positionCompare;
2812
- }
2813
- const leftOrder = Number.isFinite(left.order) ? left.order : Number.POSITIVE_INFINITY;
2814
- const rightOrder = Number.isFinite(right.order) ? right.order : Number.POSITIVE_INFINITY;
2815
- if (leftOrder !== rightOrder) {
2816
- return leftOrder - rightOrder;
2817
- }
2818
- return left.id.localeCompare(right.id);
2819
- })
2820
- );
2821
- }
2822
-
2823
- function deriveCanonicalExportTargetForSubpath(subpath) {
2824
- const normalizedSubpath = String(subpath || "").trim();
2825
- if (!normalizedSubpath) {
2826
- return "";
2827
- }
2828
- if (normalizedSubpath === ".") {
2829
- return "./src/index.js";
2830
- }
2831
- if (!normalizedSubpath.startsWith("./")) {
2832
- return "";
2833
- }
2834
-
2835
- const bareSubpath = normalizedSubpath.slice(2);
2836
- if (!bareSubpath) {
2837
- return "";
2838
- }
2839
- if (bareSubpath === "client" || bareSubpath === "server" || bareSubpath === "shared") {
2840
- return `./src/${bareSubpath}/index.js`;
2841
- }
2842
-
2843
- const roots = ["client", "server", "shared"];
2844
- for (const root of roots) {
2845
- if (!bareSubpath.startsWith(`${root}/`)) {
2846
- continue;
2847
- }
2848
- const suffix = bareSubpath.slice(root.length + 1);
2849
- if (!suffix) {
2850
- return "";
2851
- }
2852
- const hasJsExtension = /\.(?:c|m)?js$/.test(suffix);
2853
- const normalizedSuffix = hasJsExtension ? suffix : `${suffix}.js`;
2854
- return `./src/${root}/${normalizedSuffix}`;
2855
- }
2856
-
2857
- return "";
2858
- }
2859
-
2860
- function shouldShowPackageExportTarget({ subpath, target, targetType }) {
2861
- if (String(targetType || "").trim() !== "file") {
2862
- return true;
2863
- }
2864
-
2865
- const canonicalTarget = deriveCanonicalExportTargetForSubpath(subpath);
2866
- if (!canonicalTarget) {
2867
- return true;
2868
- }
2869
-
2870
- const normalizeTarget = (value) => {
2871
- const raw = String(value || "").trim();
2872
- if (!raw) {
2873
- return "";
2874
- }
2875
- const withoutPrefix = raw.startsWith("./") ? raw.slice(2) : raw;
2876
- return `./${normalizeRelativePosixPath(withoutPrefix)}`;
2877
- };
2878
-
2879
- return normalizeTarget(target) !== normalizeTarget(canonicalTarget);
2880
- }
2881
-
2882
- function deriveProviderDisplayName(bindingRecord) {
2883
- const binding = ensureObject(bindingRecord);
2884
- const providerLabel = String(binding.provider || "").trim();
2885
- if (!providerLabel) {
2886
- return "";
2887
- }
2888
-
2889
- const hashIndex = providerLabel.lastIndexOf("#");
2890
- if (hashIndex > -1 && hashIndex < providerLabel.length - 1) {
2891
- return providerLabel.slice(hashIndex + 1);
2892
- }
2893
-
2894
- const entrypoint = String(binding.entrypoint || "").trim();
2895
- if (!entrypoint) {
2896
- return providerLabel;
2897
- }
2898
- const basename = path.posix.basename(entrypoint);
2899
- return basename.replace(/\.(?:c|m)?js$/i, "") || providerLabel;
2900
- }
2901
-
2902
- async function collectExportFileSymbolSummaries({ packageRoot, packageExports, notes }) {
2903
- const rootDir = String(packageRoot || "").trim();
2904
- if (!rootDir) {
2905
- return [];
2906
- }
2907
-
2908
- const exportTargets = new Map();
2909
- for (const entry of ensureArray(packageExports)) {
2910
- const record = ensureObject(entry);
2911
- if (record.targetType !== "file" || record.targetExists !== true) {
2912
- continue;
2913
- }
2914
-
2915
- const target = String(record.target || "").trim();
2916
- if (!target.startsWith("./")) {
2917
- continue;
2918
- }
2919
- const normalizedTarget = normalizeRelativePosixPath(target.replace(/^\.\//, ""));
2920
- const basename = path.posix.basename(normalizedTarget);
2921
- if (!/\.(?:js|mjs|cjs)$/i.test(basename)) {
2922
- continue;
2923
- }
2924
-
2925
- if (!exportTargets.has(normalizedTarget)) {
2926
- exportTargets.set(normalizedTarget, {
2927
- file: normalizedTarget,
2928
- subpaths: new Set(),
2929
- conditions: new Set()
2930
- });
2931
- }
2932
- const bucket = exportTargets.get(normalizedTarget);
2933
- bucket.subpaths.add(String(record.subpath || ".").trim() || ".");
2934
- const condition = String(record.condition || "default").trim() || "default";
2935
- if (condition !== "default") {
2936
- bucket.conditions.add(condition);
2937
- }
2938
- }
2939
-
2940
- const summaries = [];
2941
- for (const [relativeTargetPath, bucket] of exportTargets.entries()) {
2942
- const absoluteTargetPath = path.resolve(rootDir, relativeTargetPath);
2943
- if (!(await fileExists(absoluteTargetPath))) {
2944
- ensureArray(notes).push(`Export file missing: ${relativeTargetPath}`);
2945
- continue;
2946
- }
2947
-
2948
- let source = "";
2949
- try {
2950
- source = await readFile(absoluteTargetPath, "utf8");
2951
- } catch (error) {
2952
- ensureArray(notes).push(
2953
- `Failed to read export file ${relativeTargetPath}: ${String(error?.message || error || "unknown error")}`
2954
- );
2955
- continue;
2956
- }
2957
-
2958
- const summary = parseExportedSymbolsFromSource(source);
2959
- summaries.push({
2960
- file: normalizeRelativePosixPath(relativeTargetPath),
2961
- subpaths: sortStrings([...bucket.subpaths]),
2962
- conditions: sortStrings([...bucket.conditions]),
2963
- symbols: ensureArray(summary.symbols),
2964
- hasDefaultExport: Boolean(summary.hasDefaultExport),
2965
- starReExports: ensureArray(summary.starReExports),
2966
- namedReExports: ensureArray(summary.namedReExports)
2967
- });
2968
- }
2969
-
2970
- return summaries.sort((left, right) => String(left.file || "").localeCompare(String(right.file || "")));
2971
- }
2972
-
2973
- async function inspectPackageOfferings({ packageEntry }) {
2974
- const rootDir = String(packageEntry?.rootDir || "").trim();
2975
- const notes = [];
2976
- const details = {
2977
- available: Boolean(rootDir),
2978
- notes,
2979
- packageExports: [],
2980
- containerBindings: {
2981
- server: [],
2982
- client: []
2983
- },
2984
- exportedSymbols: []
2985
- };
2986
-
2987
- if (!rootDir) {
2988
- notes.push("Source files are unavailable for static introspection (catalog metadata only).");
2989
- return details;
2990
- }
2991
-
2992
- const packageJson = ensureObject(packageEntry?.packageJson);
2993
- details.packageExports = await describePackageExports({
2994
- packageRoot: rootDir,
2995
- packageJson
2996
- });
2997
-
2998
- const runtime = ensureObject(packageEntry?.descriptor?.runtime);
2999
- const runtimeSides = [
3000
- {
3001
- side: "server",
3002
- providers: ensureArray(ensureObject(runtime.server).providers)
3003
- },
3004
- {
3005
- side: "client",
3006
- providers: ensureArray(ensureObject(runtime.client).providers)
3007
- }
3008
- ];
3009
-
3010
- for (const runtimeSide of runtimeSides) {
3011
- const side = String(runtimeSide.side || "").trim();
3012
- if (!side) {
3013
- continue;
3014
- }
3015
- const bindings = [];
3016
- for (const provider of runtimeSide.providers) {
3017
- const record = ensureObject(provider);
3018
- const entrypoint = String(record.entrypoint || "").trim();
3019
- const exportName = String(record.export || "").trim();
3020
- if (!entrypoint) {
3021
- continue;
3022
- }
3023
-
3024
- const providerLabel = exportName ? `${entrypoint}#${exportName}` : entrypoint;
3025
- if (entrypoint.includes("*")) {
3026
- notes.push(`Skipped wildcard provider entrypoint during introspection: ${providerLabel}`);
3027
- continue;
3028
- }
3029
-
3030
- const providerPath = path.resolve(rootDir, entrypoint);
3031
- if (!(await fileExists(providerPath))) {
3032
- notes.push(`Provider file missing during introspection: ${providerLabel}`);
3033
- continue;
3034
- }
3035
-
3036
- let source = "";
3037
- try {
3038
- source = await readFile(providerPath, "utf8");
3039
- } catch (error) {
3040
- notes.push(`Failed reading provider ${providerLabel}: ${String(error?.message || error || "unknown error")}`);
3041
- continue;
3042
- }
3043
-
3044
- bindings.push(
3045
- ...collectContainerBindingsFromProviderSource({
3046
- source,
3047
- providerLabel,
3048
- entrypoint,
3049
- providerExportName: exportName
3050
- })
3051
- );
3052
- }
3053
-
3054
- details.containerBindings[side] = bindings.sort((left, right) => {
3055
- const tokenComparison = String(left?.token || "").localeCompare(String(right?.token || ""));
3056
- if (tokenComparison !== 0) {
3057
- return tokenComparison;
3058
- }
3059
- const providerComparison = String(left?.provider || "").localeCompare(String(right?.provider || ""));
3060
- if (providerComparison !== 0) {
3061
- return providerComparison;
3062
- }
3063
- return Number(left?.line || 0) - Number(right?.line || 0);
3064
- });
3065
- }
3066
-
3067
- details.exportedSymbols = await collectExportFileSymbolSummaries({
3068
- packageRoot: rootDir,
3069
- packageExports: details.packageExports,
3070
- notes
3071
- });
3072
-
3073
- return details;
3074
- }
3075
-
3076
- function collectPlannedCapabilityIssues(plannedPackageIds, packageRegistry) {
3077
- const selectedPackageIds = sortStrings(
3078
- [...new Set(ensureArray(plannedPackageIds).map((value) => String(value || "").trim()).filter(Boolean))]
3079
- );
3080
- const selectedPackageSet = new Set(selectedPackageIds);
3081
- const providersByCapability = new Map();
3082
-
3083
- for (const [capabilityId, providers] of Object.entries(BUILTIN_CAPABILITY_PROVIDERS)) {
3084
- if (!providersByCapability.has(capabilityId)) {
3085
- providersByCapability.set(capabilityId, new Set());
3086
- }
3087
- for (const providerId of ensureArray(providers).map((value) => String(value || "").trim()).filter(Boolean)) {
3088
- providersByCapability.get(capabilityId).add(providerId);
3089
- }
3090
- }
3091
-
3092
- for (const packageId of selectedPackageIds) {
3093
- const packageEntry = packageRegistry.get(packageId);
3094
- if (!packageEntry) {
3095
- continue;
3096
- }
3097
- const provides = listDeclaredCapabilities(packageEntry.descriptor.capabilities, "provides");
3098
- for (const capabilityId of provides) {
3099
- if (!providersByCapability.has(capabilityId)) {
3100
- providersByCapability.set(capabilityId, new Set());
3101
- }
3102
- providersByCapability.get(capabilityId).add(packageId);
3103
- }
3104
- }
3105
-
3106
- const issues = [];
3107
- for (const packageId of selectedPackageIds) {
3108
- const packageEntry = packageRegistry.get(packageId);
3109
- if (!packageEntry) {
3110
- continue;
3111
- }
3112
- const requires = listDeclaredCapabilities(packageEntry.descriptor.capabilities, "requires");
3113
- for (const capabilityId of requires) {
3114
- const selectedProviders = providersByCapability.get(capabilityId);
3115
- if (selectedProviders && selectedProviders.size > 0) {
3116
- continue;
3117
- }
3118
-
3119
- const availableProviders = [];
3120
- for (const [candidatePackageId, candidatePackageEntry] of packageRegistry.entries()) {
3121
- if (selectedPackageSet.has(candidatePackageId)) {
3122
- continue;
3123
- }
3124
- const candidateProvides = listDeclaredCapabilities(candidatePackageEntry.descriptor.capabilities, "provides");
3125
- if (candidateProvides.includes(capabilityId)) {
3126
- availableProviders.push(candidatePackageId);
3127
- }
3128
- }
3129
-
3130
- issues.push({
3131
- packageId,
3132
- capabilityId,
3133
- availableProviders: sortStrings(availableProviders)
3134
- });
3135
- }
3136
- }
3137
-
3138
- return issues;
3139
- }
3140
-
3141
- function validatePlannedCapabilityClosure(plannedPackageIds, packageRegistry, actionLabel) {
3142
- const issues = collectPlannedCapabilityIssues(plannedPackageIds, packageRegistry);
3143
- if (issues.length === 0) {
3144
- return;
3145
- }
3146
-
3147
- const lines = [`Cannot ${actionLabel}: capability requirements are not satisfied.`];
3148
- for (const issue of issues) {
3149
- const providersHint = issue.availableProviders.length > 0
3150
- ? ` Available providers: ${issue.availableProviders.join(", ")}.`
3151
- : "";
3152
- lines.push(
3153
- `- ${issue.packageId} requires capability ${issue.capabilityId}, but no selected package provides it.${providersHint}`
3154
- );
3155
- }
3156
-
3157
- throw createCliError(lines.join("\n"));
3158
- }
3159
-
3160
- function resolveSurfaceVisibilityOptionPolicy(packageEntry = {}) {
3161
- const descriptor = ensureObject(packageEntry?.descriptor);
3162
- const optionPolicies = ensureObject(descriptor.optionPolicies);
3163
- const surfaceVisibilityPolicy = optionPolicies.surfaceVisibility;
3164
- if (!surfaceVisibilityPolicy || surfaceVisibilityPolicy === false) {
3165
- return null;
3166
- }
3167
-
3168
- let policy = {};
3169
- if (surfaceVisibilityPolicy === true) {
3170
- policy = {};
3171
- } else if (typeof surfaceVisibilityPolicy === "object" && !Array.isArray(surfaceVisibilityPolicy)) {
3172
- policy = ensureObject(surfaceVisibilityPolicy);
3173
- } else {
3174
- throw createCliError(
3175
- `Invalid option policy in package ${packageEntry.packageId}: surfaceVisibility must be true or an object.`
3176
- );
3177
- }
3178
-
3179
- const surfaceOption = String(policy.surfaceOption || "surface").trim();
3180
- const visibilityOption = String(policy.visibilityOption || "visibility").trim();
3181
- if (!surfaceOption || !visibilityOption) {
3182
- throw createCliError(
3183
- `Invalid option policy in package ${packageEntry.packageId}: surfaceVisibility requires non-empty surfaceOption and visibilityOption.`
3184
- );
3185
- }
3186
-
3187
- return Object.freeze({
3188
- surfaceOption,
3189
- visibilityOption,
3190
- allowAuto: policy.allowAuto !== false
3191
- });
3192
- }
3193
-
3194
- function resolveSurfaceDefinitionsForOptionPolicy(configContext = {}) {
3195
- const publicConfig = ensureObject(configContext.public);
3196
- const mergedConfig = ensureObject(configContext.merged);
3197
- const sourceDefinitions = ensureObject(publicConfig.surfaceDefinitions);
3198
- const fallbackDefinitions = ensureObject(mergedConfig.surfaceDefinitions);
3199
- const surfaceDefinitions =
3200
- Object.keys(sourceDefinitions).length > 0 ? sourceDefinitions : fallbackDefinitions;
3201
-
3202
- const normalizedDefinitions = {};
3203
- for (const [key, value] of Object.entries(surfaceDefinitions)) {
3204
- const definition = ensureObject(value);
3205
- const definitionId = normalizeSurfaceIdForMutation(definition.id || key);
3206
- if (!definitionId) {
3207
- continue;
3208
- }
3209
-
3210
- normalizedDefinitions[definitionId] = Object.freeze({
3211
- id: definitionId,
3212
- enabled: definition.enabled !== false,
3213
- requiresWorkspace: definition.requiresWorkspace === true
3214
- });
3215
- }
3216
-
3217
- return Object.freeze(normalizedDefinitions);
3218
- }
3219
-
3220
- function normalizeResolvedOptionValue(value = "") {
3221
- return String(value || "").trim().toLowerCase();
3222
- }
3223
-
3224
- function validateSurfaceVisibilityOptionPolicy({
3225
- packageEntry,
3226
- resolvedOptions = {},
3227
- policy,
3228
- configContext = {}
3229
- } = {}) {
3230
- const packageId = String(packageEntry?.packageId || "").trim() || "unknown-package";
3231
- const surfaceIds = parseSurfaceIdListForMutation(resolvedOptions?.[policy.surfaceOption]);
3232
- const visibility = normalizeResolvedOptionValue(resolvedOptions?.[policy.visibilityOption]);
3233
- if (surfaceIds.length < 1 || !visibility) {
3234
- return;
3235
- }
3236
- const skipWorkspaceRequirement = policy.allowAuto && visibility === "auto";
3237
-
3238
- const surfaceDefinitions = resolveSurfaceDefinitionsForOptionPolicy(configContext);
3239
- for (const surfaceId of surfaceIds) {
3240
- const surfaceDefinition = surfaceDefinitions[surfaceId];
3241
- if (!surfaceDefinition) {
3242
- throw createCliError(
3243
- `Invalid option combination for package ${packageId}: --${policy.surfaceOption} includes unknown surface "${surfaceId}" in config/public.js.`
3244
- );
3245
- }
3246
- if (surfaceDefinition.enabled !== true) {
3247
- throw createCliError(
3248
- `Invalid option combination for package ${packageId}: surface "${surfaceId}" is disabled in config/public.js.`
3249
- );
3250
- }
3251
-
3252
- if (!skipWorkspaceRequirement && WORKSPACE_VISIBILITY_SET.has(visibility) && surfaceDefinition.requiresWorkspace !== true) {
3253
- throw createCliError(
3254
- `Invalid option combination for package ${packageId}: --${policy.visibilityOption} "${visibility}" requires surfaces with requiresWorkspace=true, but "${surfaceId}" has requiresWorkspace=false.`
3255
- );
3256
- }
3257
- }
3258
- }
3259
-
3260
- async function validateResolvedOptionPolicies({
3261
- packageEntry,
3262
- resolvedOptions = {},
3263
- appRoot = "",
3264
- resolveConfigContext
3265
- } = {}) {
3266
- const policy = resolveSurfaceVisibilityOptionPolicy(packageEntry);
3267
- if (!policy) {
3268
- return;
3269
- }
3270
- if (!appRoot) {
3271
- return;
3272
- }
3273
-
3274
- const configContext = await resolveConfigContext();
3275
- validateSurfaceVisibilityOptionPolicy({
3276
- packageEntry,
3277
- resolvedOptions,
3278
- policy,
3279
- configContext
3280
- });
3281
- }
3282
-
3283
- async function resolvePackageOptions(packageEntry, inlineOptions, io, { appRoot = "" } = {}) {
3284
- const optionSchemas = ensureObject(packageEntry.descriptor.options);
3285
- const optionNames = Object.keys(optionSchemas);
3286
- const resolved = {};
3287
- const inlineOptionValues = ensureObject(inlineOptions);
3288
- const hasInlineOption = (name) => Object.prototype.hasOwnProperty.call(inlineOptionValues, name);
3289
- let configContext = null;
3290
-
3291
- async function loadConfigContext() {
3292
- if (!configContext) {
3293
- configContext = await loadMutationWhenConfigContext(appRoot);
3294
- }
3295
- return configContext;
3296
- }
3297
-
3298
- async function resolveOptionDefaultFromConfig(configPath = "") {
3299
- const normalizedConfigPath = String(configPath || "").trim();
3300
- if (!normalizedConfigPath || !appRoot) {
3301
- return "";
3302
- }
3303
-
3304
- await loadConfigContext();
3305
- return normalizeWhenSourceValue(resolveWhenConfigValue(configContext, normalizedConfigPath));
3306
- }
3307
-
3308
- for (const optionName of optionNames) {
3309
- const schema = ensureObject(optionSchemas[optionName]);
3310
- const allowEmpty = schema.allowEmpty === true;
3311
- if (hasInlineOption(optionName)) {
3312
- const inlineValue = String(inlineOptionValues[optionName] || "").trim();
3313
- if (inlineValue || allowEmpty) {
3314
- resolved[optionName] = inlineValue;
3315
- continue;
3316
- }
3317
- if (schema.required) {
3318
- throw createCliError(`Package ${packageEntry.packageId} option ${optionName} requires a non-empty value.`);
3319
- }
3320
- }
3321
-
3322
- const defaultFromConfigPath = String(schema.defaultFromConfig || "").trim();
3323
- if (defaultFromConfigPath) {
3324
- const defaultFromConfigValue = await resolveOptionDefaultFromConfig(defaultFromConfigPath);
3325
- if (defaultFromConfigValue || allowEmpty) {
3326
- resolved[optionName] = defaultFromConfigValue;
3327
- continue;
3328
- }
3329
- }
3330
-
3331
- if (typeof schema.defaultValue === "string" && schema.defaultValue.trim()) {
3332
- resolved[optionName] = schema.defaultValue.trim();
3333
- continue;
3334
- }
3335
-
3336
- if (schema.required) {
3337
- resolved[optionName] = await promptForRequiredOption({
3338
- ownerType: "package",
3339
- ownerId: packageEntry.packageId,
3340
- optionName,
3341
- optionSchema: schema,
3342
- stdin: io.stdin,
3343
- stdout: io.stdout
3344
- });
3345
- continue;
3346
- }
3347
-
3348
- resolved[optionName] = "";
3349
- }
3350
-
3351
- await validateResolvedOptionPolicies({
3352
- packageEntry,
3353
- resolvedOptions: resolved,
3354
- appRoot,
3355
- resolveConfigContext: loadConfigContext
3356
- });
3357
-
3358
- return resolved;
3359
- }
3360
-
3361
- function validateInlineOptionsForPackage(packageEntry, inlineOptions) {
3362
- const optionSchemas = ensureObject(packageEntry?.descriptor?.options);
3363
- const allowedOptionNames = Object.keys(optionSchemas);
3364
- const allowed = new Set(allowedOptionNames);
3365
- const providedOptionNames = Object.keys(ensureObject(inlineOptions));
3366
- const unknownOptionNames = providedOptionNames.filter((optionName) => !allowed.has(optionName));
3367
-
3368
- if (unknownOptionNames.length < 1) {
3369
- return;
3370
- }
3371
-
3372
- const sortedUnknown = sortStrings(unknownOptionNames);
3373
- const suffix = allowedOptionNames.length > 0
3374
- ? ` Allowed options: ${sortStrings(allowedOptionNames).join(", ")}.`
3375
- : " This package does not accept inline options.";
3376
-
3377
- throw createCliError(
3378
- `Unknown option(s) for package ${packageEntry.packageId}: ${sortedUnknown.join(", ")}.${suffix}`
3379
- );
3380
- }
3381
-
3382
- function createManagedRecordBase(packageEntry, options) {
3383
- const sourceRecord = {
3384
- type: String(packageEntry?.sourceType || "packages-directory"),
3385
- ...ensureObject(packageEntry?.source)
3386
- };
3387
- if (!sourceRecord.descriptorPath && String(packageEntry?.descriptorRelativePath || "").trim()) {
3388
- sourceRecord.descriptorPath = String(packageEntry.descriptorRelativePath).trim();
3389
- }
3390
-
3391
- return {
3392
- packageId: packageEntry.packageId,
3393
- version: packageEntry.version,
3394
- source: sourceRecord,
3395
- managed: {
3396
- packageJson: {
3397
- dependencies: {},
3398
- devDependencies: {},
3399
- scripts: {}
3400
- },
3401
- text: {},
3402
- vite: {},
3403
- files: [],
3404
- migrations: []
3405
- },
3406
- options,
3407
- installedAt: new Date().toISOString()
3408
- };
3409
- }
3410
-
3411
- async function runCommandCapture(command, args, { cwd } = {}) {
3412
- return await new Promise((resolve, reject) => {
3413
- const child = spawn(command, args, {
3414
- cwd,
3415
- stdio: ["ignore", "pipe", "pipe"]
3416
- });
3417
-
3418
- let stdout = "";
3419
- let stderr = "";
3420
-
3421
- child.stdout.on("data", (chunk) => {
3422
- stdout += chunk.toString("utf8");
3423
- });
3424
- child.stderr.on("data", (chunk) => {
3425
- stderr += chunk.toString("utf8");
3426
- });
3427
-
3428
- child.on("error", reject);
3429
- child.on("exit", (code) => {
3430
- if (code === 0) {
3431
- resolve({
3432
- stdout,
3433
- stderr
3434
- });
3435
- return;
3436
- }
3437
- const details = String(stderr || stdout || "").trim();
3438
- reject(createCliError(`${command} ${args.join(" ")} failed with exit code ${code}.${details ? ` ${details}` : ""}`));
3439
- });
3440
- });
3441
- }
3442
-
3443
- function extractPackTarballName(packStdout) {
3444
- const lines = String(packStdout || "")
3445
- .split(/\r?\n/)
3446
- .map((value) => value.trim())
3447
- .filter(Boolean);
3448
- return lines.length > 0 ? lines[lines.length - 1] : "";
3449
- }
3450
-
3451
- async function materializePackageRootFromRegistry({ packageEntry, appRoot }) {
3452
- const cacheKey = `${packageEntry.packageId}@${packageEntry.version}`;
3453
- if (MATERIALIZED_PACKAGE_ROOTS.has(cacheKey)) {
3454
- return MATERIALIZED_PACKAGE_ROOTS.get(cacheKey);
3455
- }
3456
-
3457
- const tempRoot = await mkdtemp(path.join(tmpdir(), "jskit-cli-pack-"));
3458
- MATERIALIZED_PACKAGE_TEMP_DIRECTORIES.add(tempRoot);
3459
- const packageSpec = `${packageEntry.packageId}@${packageEntry.version}`;
3460
- const packResult = await runCommandCapture(
3461
- "npm",
3462
- ["pack", packageSpec, "--silent", "--pack-destination", tempRoot],
3463
- { cwd: appRoot }
3464
- );
3465
- const tarballName = extractPackTarballName(packResult.stdout);
3466
- if (!tarballName) {
3467
- throw createCliError(`Unable to materialize ${packageSpec}: npm pack produced no tarball name.`);
3468
- }
3469
-
3470
- const tarballPath = path.join(tempRoot, tarballName);
3471
- if (!(await fileExists(tarballPath))) {
3472
- throw createCliError(`Unable to materialize ${packageSpec}: tarball missing at ${tarballPath}.`);
3473
- }
3474
-
3475
- const extractedRoot = path.join(tempRoot, "extracted");
3476
- await mkdir(extractedRoot, { recursive: true });
3477
- await runCommandCapture("tar", ["-xzf", tarballPath, "-C", extractedRoot], { cwd: appRoot });
3478
- const packageRoot = path.join(extractedRoot, "package");
3479
- const descriptorPath = path.join(packageRoot, "package.descriptor.mjs");
3480
- if (!(await fileExists(descriptorPath))) {
3481
- throw createCliError(`Materialized package ${packageSpec} does not contain package.descriptor.mjs.`);
3482
- }
3483
-
3484
- MATERIALIZED_PACKAGE_ROOTS.set(cacheKey, packageRoot);
3485
- return packageRoot;
3486
- }
3487
-
3488
- async function resolvePackageRootFromNodeModules({ appRoot, packageId }) {
3489
- const normalizedAppRoot = path.resolve(String(appRoot || "").trim());
3490
- const normalizedPackageId = String(packageId || "").trim();
3491
- if (!normalizedAppRoot || !normalizedPackageId) {
3492
- return "";
3493
- }
3494
-
3495
- const candidateRoot = path.resolve(normalizedAppRoot, "node_modules", ...normalizedPackageId.split("/"));
3496
- const candidateDescriptorPath = path.join(candidateRoot, "package.descriptor.mjs");
3497
- if (!(await fileExists(candidateDescriptorPath))) {
3498
- return "";
3499
- }
3500
-
3501
- return candidateRoot;
3502
- }
3503
-
3504
- async function loadLocalWorkspacePackageIdIndex() {
3505
- if (LOCAL_WORKSPACE_PACKAGE_ID_INDEX instanceof Map) {
3506
- return LOCAL_WORKSPACE_PACKAGE_ID_INDEX;
3507
- }
3508
-
3509
- const repoRoot = path.resolve(CLI_PACKAGE_ROOT, "..", "..");
3510
- const parentDirectories = [
3511
- path.join(repoRoot, "packages"),
3512
- path.join(repoRoot, "tooling")
3513
- ];
3514
- const packageIdIndex = new Map();
3515
-
3516
- for (const parentDirectory of parentDirectories) {
3517
- if (!(await fileExists(parentDirectory))) {
3518
- continue;
3519
- }
3520
-
3521
- const entries = await readdir(parentDirectory, { withFileTypes: true });
3522
- for (const entry of entries) {
3523
- if (!entry.isDirectory() || entry.name.startsWith(".")) {
3524
- continue;
3525
- }
3526
-
3527
- const candidateRoot = path.join(parentDirectory, entry.name);
3528
- const packageJsonPath = path.join(candidateRoot, "package.json");
3529
- const descriptorPath = path.join(candidateRoot, "package.descriptor.mjs");
3530
- if (!(await fileExists(packageJsonPath)) || !(await fileExists(descriptorPath))) {
3531
- continue;
3532
- }
3533
-
3534
- let packageJson = {};
3535
- try {
3536
- packageJson = await readJsonFile(packageJsonPath);
3537
- } catch {
3538
- continue;
3539
- }
3540
-
3541
- const packageId = String(packageJson?.name || "").trim();
3542
- if (!packageId.startsWith("@jskit-ai/")) {
3543
- continue;
3544
- }
3545
- if (packageIdIndex.has(packageId)) {
3546
- continue;
3547
- }
3548
- packageIdIndex.set(packageId, candidateRoot);
3549
- }
3550
- }
3551
-
3552
- LOCAL_WORKSPACE_PACKAGE_ID_INDEX = packageIdIndex;
3553
- return packageIdIndex;
3554
- }
3555
-
3556
- async function resolvePackageRootFromLocalWorkspace({ packageId }) {
3557
- const normalizedPackageId = String(packageId || "").trim();
3558
- if (!normalizedPackageId.startsWith("@jskit-ai/")) {
3559
- return "";
3560
- }
3561
- if (LOCAL_WORKSPACE_PACKAGE_ROOTS.has(normalizedPackageId)) {
3562
- return LOCAL_WORKSPACE_PACKAGE_ROOTS.get(normalizedPackageId);
3563
- }
3564
-
3565
- const packageIdIndex = await loadLocalWorkspacePackageIdIndex();
3566
- const packageRoot = String(packageIdIndex.get(normalizedPackageId) || "").trim();
3567
- LOCAL_WORKSPACE_PACKAGE_ROOTS.set(normalizedPackageId, packageRoot);
3568
- return packageRoot;
3569
- }
3570
-
3571
- async function resolvePackageTemplateRoot({ packageEntry, appRoot }) {
3572
- const packageRoot = String(packageEntry?.rootDir || "").trim();
3573
- if (packageRoot) {
3574
- return packageRoot;
3575
- }
3576
-
3577
- const installedPackageRoot = await resolvePackageRootFromNodeModules({
3578
- appRoot,
3579
- packageId: packageEntry?.packageId
3580
- });
3581
- if (installedPackageRoot) {
3582
- return installedPackageRoot;
3583
- }
3584
-
3585
- const localWorkspacePackageRoot = await resolvePackageRootFromLocalWorkspace({
3586
- packageId: packageEntry?.packageId
3587
- });
3588
- if (localWorkspacePackageRoot) {
3589
- return localWorkspacePackageRoot;
3590
- }
3591
-
3592
- return await materializePackageRootFromRegistry({ packageEntry, appRoot });
3593
- }
3594
-
3595
- async function cleanupMaterializedPackageRoots() {
3596
- for (const tempDirectory of MATERIALIZED_PACKAGE_TEMP_DIRECTORIES) {
3597
- await rm(tempDirectory, { recursive: true, force: true }).catch(() => {});
3598
- }
3599
- MATERIALIZED_PACKAGE_TEMP_DIRECTORIES.clear();
3600
- MATERIALIZED_PACKAGE_ROOTS.clear();
3601
- LOCAL_WORKSPACE_PACKAGE_ROOTS.clear();
3602
- LOCAL_WORKSPACE_PACKAGE_ID_INDEX = null;
3603
- }
3604
-
3605
- function interpolateFileMutationRecord(mutation, options, packageId) {
3606
- const mutationKey = String(
3607
- mutation?.id || mutation?.to || mutation?.toSurface || mutation?.from || "files"
3608
- ).trim();
3609
- const interpolate = (value, field) =>
3610
- interpolateOptionValue(String(value || ""), options, packageId, `${mutationKey}.${field}`);
3611
-
3612
- return {
3613
- ...mutation,
3614
- from: interpolate(mutation.from, "from"),
3615
- to: interpolate(mutation.to, "to"),
3616
- toSurface: interpolate(mutation.toSurface, "toSurface"),
3617
- toSurfacePath: interpolate(mutation.toSurfacePath, "toSurfacePath"),
3618
- toDir: interpolate(mutation.toDir, "toDir"),
3619
- extension: interpolate(mutation.extension, "extension"),
3620
- id: interpolate(mutation.id, "id"),
3621
- category: interpolate(mutation.category, "category"),
3622
- reason: interpolate(mutation.reason, "reason"),
3623
- templateContext: mutation.templateContext
3624
- ? {
3625
- entrypoint: interpolate(mutation.templateContext.entrypoint, "templateContext.entrypoint"),
3626
- export: interpolate(mutation.templateContext.export, "templateContext.export")
3627
- }
3628
- : null
3629
- };
3630
- }
3631
-
3632
- function applyTemplateContextReplacements(sourceContent, replacements) {
3633
- let output = String(sourceContent || "");
3634
- for (const [placeholder, value] of Object.entries(ensureObject(replacements))) {
3635
- const normalizedPlaceholder = String(placeholder || "");
3636
- if (!normalizedPlaceholder) {
3637
- continue;
3638
- }
3639
- output = output.split(normalizedPlaceholder).join(String(value == null ? "" : value));
3640
- }
3641
- return output;
3642
- }
3643
-
3644
- async function copyTemplateFile(
3645
- sourcePath,
3646
- targetPath,
3647
- options,
3648
- packageId,
3649
- interpolationKey,
3650
- templateContextReplacements = null
3651
- ) {
3652
- const sourceContent = await readFile(sourcePath, "utf8");
3653
- let renderedContent = sourceContent.includes("${")
3654
- ? interpolateOptionValue(sourceContent, options, packageId, interpolationKey)
3655
- : sourceContent;
3656
- if (templateContextReplacements) {
3657
- renderedContent = applyTemplateContextReplacements(renderedContent, templateContextReplacements);
3658
- }
3659
-
3660
- await mkdir(path.dirname(targetPath), { recursive: true });
3661
- await writeFile(targetPath, renderedContent, "utf8");
3662
- }
3663
-
3664
- async function resolveTemplateContextReplacementsForMutation({
3665
- packageEntry,
3666
- mutation,
3667
- options,
3668
- appRoot,
3669
- sourcePath,
3670
- targetPaths
3671
- } = {}) {
3672
- const templateContext = ensureObject(mutation?.templateContext);
3673
- const hasTemplateContext = Object.keys(templateContext).length > 0;
3674
- const entrypoint = String(templateContext.entrypoint || "").trim();
3675
- if (!hasTemplateContext) {
3676
- return null;
3677
- }
3678
- if (!entrypoint) {
3679
- throw createCliError(
3680
- `Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint is required when templateContext is set.`
3681
- );
3682
- }
3683
- const exportName = String(templateContext.export || "").trim() || "buildTemplateContext";
3684
- const resolvedEntrypointPath = resolveAppRelativePathWithinRoot(
3685
- packageEntry.rootDir,
3686
- entrypoint,
3687
- `${packageEntry.packageId} files mutation templateContext.entrypoint`
3688
- );
3689
- const absoluteEntrypointPath = resolvedEntrypointPath.absolutePath;
3690
- if (!(await fileExists(absoluteEntrypointPath))) {
3691
- throw createCliError(
3692
- `Invalid files mutation in ${packageEntry.packageId}: templateContext.entrypoint not found at ${entrypoint}.`
3693
- );
3694
- }
3695
-
3696
- let moduleNamespace = null;
3697
- try {
3698
- moduleNamespace = await import(`${pathToFileURL(absoluteEntrypointPath).href}?t=${Date.now()}_${Math.random()}`);
3699
- } catch (error) {
3700
- throw createCliError(
3701
- `Unable to load templateContext entrypoint ${entrypoint} for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
3702
- );
3703
- }
3704
-
3705
- const resolver = moduleNamespace?.[exportName];
3706
- if (typeof resolver !== "function") {
3707
- throw createCliError(
3708
- `Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" is not a function.`
3709
- );
3710
- }
3711
-
3712
- let replacements = null;
3713
- try {
3714
- replacements = await resolver({
3715
- packageId: packageEntry.packageId,
3716
- packageRoot: packageEntry.rootDir,
3717
- appRoot,
3718
- options: Object.freeze({ ...ensureObject(options) }),
3719
- mutation: Object.freeze({ ...ensureObject(mutation) }),
3720
- sourcePath,
3721
- targetPaths: Object.freeze([...ensureArray(targetPaths)])
3722
- });
3723
- } catch (error) {
3724
- throw createCliError(
3725
- `templateContext export "${exportName}" failed for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
3726
- );
3727
- }
3728
-
3729
- if (replacements == null) {
3730
- return null;
3731
- }
3732
- if (!replacements || typeof replacements !== "object" || Array.isArray(replacements)) {
3733
- throw createCliError(
3734
- `Invalid files mutation in ${packageEntry.packageId}: templateContext export "${exportName}" must return an object map of placeholder replacements.`
3735
- );
3736
- }
3737
-
3738
- const normalizedReplacements = {};
3739
- for (const [placeholder, value] of Object.entries(replacements)) {
3740
- const normalizedPlaceholder = String(placeholder || "").trim();
3741
- if (!normalizedPlaceholder) {
3742
- continue;
3743
- }
3744
- normalizedReplacements[normalizedPlaceholder] = String(value == null ? "" : value);
3745
- }
3746
-
3747
- return Object.freeze(normalizedReplacements);
3748
- }
3749
-
3750
- function normalizeSurfaceIdForMutation(value = "") {
3751
- return String(value || "")
3752
- .trim()
3753
- .toLowerCase();
3754
- }
3755
-
3756
- function parseSurfaceIdListForMutation(value = "") {
3757
- const sourceValues = Array.isArray(value) ? value : [value];
3758
- const surfaceIds = [];
3759
- const seen = new Set();
3760
-
3761
- for (const sourceValue of sourceValues) {
3762
- const parsed = String(sourceValue || "")
3763
- .split(",")
3764
- .map((entry) => normalizeSurfaceIdForMutation(entry))
3765
- .filter(Boolean);
3766
- for (const surfaceId of parsed) {
3767
- if (seen.has(surfaceId)) {
3768
- continue;
3769
- }
3770
- seen.add(surfaceId);
3771
- surfaceIds.push(surfaceId);
3772
- }
3773
- }
3774
-
3775
- return Object.freeze(surfaceIds);
3776
- }
3777
-
3778
- function normalizeSurfacePagesRootForMutation(value = "") {
3779
- const rawValue = String(value || "").trim();
3780
- if (!rawValue || rawValue === "/") {
3781
- return "";
3782
- }
3783
-
3784
- return rawValue
3785
- .replace(/\\/g, "/")
3786
- .replace(/\/{2,}/g, "/")
3787
- .replace(/^\/+|\/+$/g, "");
3788
- }
3789
-
3790
- function normalizeSurfacePathForMutation(value = "", { context = "toSurfacePath" } = {}) {
3791
- const rawValue = String(value || "").trim();
3792
- if (!rawValue) {
3793
- return "";
3794
- }
3795
-
3796
- const normalized = rawValue
3797
- .replace(/\\/g, "/")
3798
- .replace(/\/{2,}/g, "/")
3799
- .replace(/^\.\/+/, "")
3800
- .replace(/^\/+/, "");
3801
- const segments = normalized.split("/");
3802
- const materializedSegments = [];
3803
- for (const segmentValue of segments) {
3804
- const segment = String(segmentValue || "").trim();
3805
- if (!segment || segment === ".") {
3806
- continue;
3807
- }
3808
- if (segment === "..") {
3809
- throw createCliError(`Invalid ${context}: path traversal is not allowed.`);
3810
- }
3811
- materializedSegments.push(segment);
3812
- }
3813
-
3814
- return materializedSegments.join("/");
3815
- }
3816
-
3817
- function resolveSurfaceDefinitionFromConfigForMutation({
3818
- configContext = {},
3819
- surfaceId = "",
3820
- packageId = ""
3821
- } = {}) {
3822
- const normalizedSurfaceId = normalizeSurfaceIdForMutation(surfaceId);
3823
- if (!normalizedSurfaceId) {
3824
- throw createCliError(`Invalid files mutation in ${packageId}: "toSurface" is required when using surface targeting.`);
3825
- }
3826
-
3827
- const publicConfig = ensureObject(configContext.public);
3828
- const mergedConfig = ensureObject(configContext.merged);
3829
- const sourceDefinitions = ensureObject(publicConfig.surfaceDefinitions);
3830
- const fallbackDefinitions = ensureObject(mergedConfig.surfaceDefinitions);
3831
- const surfaceDefinitions =
3832
- Object.keys(sourceDefinitions).length > 0 ? sourceDefinitions : fallbackDefinitions;
3833
-
3834
- for (const [key, value] of Object.entries(surfaceDefinitions)) {
3835
- const definition = ensureObject(value);
3836
- const definitionId = normalizeSurfaceIdForMutation(definition.id || key);
3837
- if (definitionId !== normalizedSurfaceId) {
3838
- continue;
3839
- }
3840
- if (definition.enabled === false) {
3841
- throw createCliError(
3842
- `Invalid files mutation in ${packageId}: surface "${normalizedSurfaceId}" is disabled.`
3843
- );
3844
- }
3845
- if (!Object.prototype.hasOwnProperty.call(definition, "pagesRoot")) {
3846
- throw createCliError(
3847
- `Invalid files mutation in ${packageId}: surface "${normalizedSurfaceId}" is missing pagesRoot in config/public.js.`
3848
- );
3849
- }
3850
-
3851
- return Object.freeze({
3852
- ...definition,
3853
- id: definitionId,
3854
- pagesRoot: normalizeSurfacePagesRootForMutation(definition.pagesRoot)
3855
- });
3856
- }
3857
-
3858
- throw createCliError(
3859
- `Invalid files mutation in ${packageId}: unknown surface "${normalizedSurfaceId}" in config/public.js.`
3860
- );
3861
- }
3862
-
3863
- function resolveSurfaceTargetPathsForMutation({
3864
- appRoot,
3865
- packageId,
3866
- mutation,
3867
- configContext
3868
- } = {}) {
3869
- const normalizedSurfaceIds = parseSurfaceIdListForMutation(mutation.toSurface);
3870
- if (normalizedSurfaceIds.length < 1) {
3871
- throw createCliError(`Invalid files mutation in ${packageId}: "toSurface" is required when using surface targeting.`);
3872
- }
3873
-
3874
- if (mutation.toSurfaceRoot === true) {
3875
- if (String(mutation.toSurfacePath || "").trim()) {
3876
- throw createCliError(
3877
- `Invalid files mutation in ${packageId}: "toSurfacePath" cannot be combined with "toSurfaceRoot".`
3878
- );
3879
- }
3880
-
3881
- const targetPaths = [];
3882
- for (const surfaceId of normalizedSurfaceIds) {
3883
- const definition = resolveSurfaceDefinitionFromConfigForMutation({
3884
- configContext,
3885
- surfaceId,
3886
- packageId
3887
- });
3888
- if (!definition.pagesRoot) {
3889
- throw createCliError(
3890
- `Invalid files mutation in ${packageId}: root surface "${surfaceId}" cannot use "toSurfaceRoot".`
3891
- );
3892
- }
3893
- targetPaths.push(path.join(appRoot, "src/pages", `${definition.pagesRoot}.vue`));
3894
- }
3895
- return Object.freeze(targetPaths);
3896
- }
3897
-
3898
- const normalizedSurfacePath = normalizeSurfacePathForMutation(mutation.toSurfacePath, {
3899
- context: "toSurfacePath"
3900
- });
3901
- if (!normalizedSurfacePath) {
3902
- throw createCliError(
3903
- `Invalid files mutation in ${packageId}: "toSurfacePath" is required when using "toSurface".`
3904
- );
3905
- }
3906
-
3907
- const targetPaths = [];
3908
- for (const surfaceId of normalizedSurfaceIds) {
3909
- const definition = resolveSurfaceDefinitionFromConfigForMutation({
3910
- configContext,
3911
- surfaceId,
3912
- packageId
3913
- });
3914
- const basePagesDirectory = definition.pagesRoot
3915
- ? path.join(appRoot, "src/pages", definition.pagesRoot)
3916
- : path.join(appRoot, "src/pages");
3917
- targetPaths.push(path.join(basePagesDirectory, normalizedSurfacePath));
3918
- }
3919
- return Object.freeze(targetPaths);
3920
- }
3921
-
3922
- async function applyFileMutations(
3923
- packageEntry,
3924
- options,
3925
- appRoot,
3926
- fileMutations,
3927
- managedFiles,
3928
- managedMigrations,
3929
- touchedFiles,
3930
- warnings = [],
3931
- precomputedTemplateContextByMutationIndex = null
3932
- ) {
3933
- const mutationList = ensureArray(fileMutations);
3934
- const managedMigrationById = new Map();
3935
- for (const managedMigrationValue of ensureArray(managedMigrations)) {
3936
- const managedMigration = ensureObject(managedMigrationValue);
3937
- const migrationId = String(managedMigration.id || "").trim();
3938
- if (!migrationId) {
3939
- continue;
3940
- }
3941
- managedMigrationById.set(migrationId, managedMigration);
3942
- }
3943
-
3944
- for (const [mutationIndex, mutationValue] of mutationList.entries()) {
3945
- const normalizedMutation = normalizeFileMutationRecord(mutationValue);
3946
- const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
3947
- const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
3948
- if (
3949
- !shouldApplyMutationWhen(normalizedMutation.when, {
3950
- options,
3951
- configContext,
3952
- packageId: packageEntry.packageId,
3953
- mutationContext: "files mutation"
3954
- })
3955
- ) {
3956
- continue;
3957
- }
3958
-
3959
- const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
3960
- const operation = mutation.op || "copy-file";
3961
-
3962
- if (operation === "install-migration") {
3963
- const rawMutation = ensureObject(mutationValue);
3964
- if (Object.hasOwn(rawMutation, "preserveOnRemove")) {
3965
- warnings.push(
3966
- `${packageEntry.packageId}: install-migration ignores preserveOnRemove (migrations are always preserved on remove).`
3967
- );
3968
- }
3969
-
3970
- const from = mutation.from;
3971
- const toDir = mutation.toDir || "migrations";
3972
- if (!from) {
3973
- throw createCliError(`Invalid install-migration mutation in ${packageEntry.packageId}: \"from\" is required.`);
3974
- }
3975
- const migrationId = normalizeMigrationId(mutation.id, packageEntry.packageId);
3976
-
3977
- const sourcePath = path.join(packageEntry.rootDir, from);
3978
- if (!(await fileExists(sourcePath))) {
3979
- throw createCliError(`Missing migration template source ${sourcePath} for ${packageEntry.packageId}.`);
3980
- }
3981
-
3982
- const hasPrecomputedTemplateContext =
3983
- precomputedTemplateContextByMutationIndex instanceof Map &&
3984
- precomputedTemplateContextByMutationIndex.has(mutationIndex);
3985
- const templateContextReplacements = hasPrecomputedTemplateContext
3986
- ? precomputedTemplateContextByMutationIndex.get(mutationIndex)
3987
- : await resolveTemplateContextReplacementsForMutation({
3988
- packageEntry,
3989
- mutation,
3990
- options,
3991
- appRoot,
3992
- sourcePath,
3993
- targetPaths: [path.join(appRoot, toDir)]
3994
- });
3995
-
3996
- const sourceContent = await readFile(sourcePath, "utf8");
3997
- let renderedSourceContent = sourceContent.includes("${")
3998
- ? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id || from}.source`)
3999
- : sourceContent;
4000
- if (templateContextReplacements) {
4001
- renderedSourceContent = applyTemplateContextReplacements(renderedSourceContent, templateContextReplacements);
4002
- }
4003
- const sourceExtension = normalizeMigrationExtension(path.extname(from), ".cjs");
4004
- const extension = normalizeMigrationExtension(mutation.extension, sourceExtension);
4005
- const sourceHash = hashBuffer(Buffer.from(renderedSourceContent, "utf8"));
4006
-
4007
- const existingManagedRecord = managedMigrationById.get(migrationId);
4008
- if (existingManagedRecord) {
4009
- const existingManagedPath = normalizeRelativePosixPath(String(existingManagedRecord.path || "").trim());
4010
- if (!existingManagedPath) {
4011
- throw createCliError(
4012
- `${packageEntry.packageId}: managed migration ${migrationId} is missing path in lock.`
4013
- );
4014
- }
4015
- const resolvedManagedPath = resolveAppRelativePathWithinRoot(
4016
- appRoot,
4017
- existingManagedPath,
4018
- `${packageEntry.packageId} managed migration path for ${migrationId}`
4019
- );
4020
- const relativePath = resolvedManagedPath.relativePath;
4021
- const absolutePath = resolvedManagedPath.absolutePath;
4022
- let existingSourceHash = String(existingManagedRecord.hash || "").trim();
4023
- if (!existingSourceHash && existingManagedPath && (await fileExists(absolutePath))) {
4024
- const existingSource = await readFile(absolutePath);
4025
- existingSourceHash = hashBuffer(existingSource);
4026
- }
4027
-
4028
- if (existingSourceHash && existingSourceHash !== sourceHash) {
4029
- throw createCliError(
4030
- `${packageEntry.packageId}: migration ${migrationId} changed after install. Keep migrations immutable and create a new migration id.`
4031
- );
4032
- }
4033
-
4034
- if (!(await fileExists(absolutePath))) {
4035
- await mkdir(path.dirname(absolutePath), { recursive: true });
4036
- await writeFile(absolutePath, renderedSourceContent, "utf8");
4037
- touchedFiles.add(relativePath);
4038
- }
4039
-
4040
- const nextManagedRecord = {
4041
- ...existingManagedRecord,
4042
- id: migrationId,
4043
- path: relativePath,
4044
- hash: sourceHash,
4045
- skipped: true,
4046
- reason: mutation.reason || String(existingManagedRecord.reason || ""),
4047
- category: mutation.category || String(existingManagedRecord.category || "")
4048
- };
4049
- managedMigrationById.set(migrationId, nextManagedRecord);
4050
- upsertManagedMigrationRecord(managedMigrations, nextManagedRecord);
4051
- warnings.push(
4052
- `${packageEntry.packageId}: skipped migration ${migrationId} (already managed at ${nextManagedRecord.path}).`
4053
- );
4054
- continue;
4055
- }
4056
-
4057
- const existingPathById = await findExistingManagedMigrationPathById({
4058
- appRoot,
4059
- toDir,
4060
- packageId: packageEntry.packageId,
4061
- migrationId,
4062
- extension
4063
- });
4064
- if (existingPathById) {
4065
- const existingSource = await readFile(existingPathById.absolutePath);
4066
- const existingSourceHash = hashBuffer(existingSource);
4067
- if (existingSourceHash !== sourceHash) {
4068
- throw createCliError(
4069
- `${packageEntry.packageId}: migration ${migrationId} changed after install. Keep migrations immutable and create a new migration id.`
4070
- );
4071
- }
4072
- const nextManagedRecord = {
4073
- id: migrationId,
4074
- path: existingPathById.relativePath,
4075
- hash: sourceHash,
4076
- skipped: true,
4077
- reason: mutation.reason,
4078
- category: mutation.category
4079
- };
4080
- managedMigrationById.set(migrationId, nextManagedRecord);
4081
- upsertManagedMigrationRecord(managedMigrations, nextManagedRecord);
4082
- warnings.push(
4083
- `${packageEntry.packageId}: skipped migration ${migrationId} (already exists at ${nextManagedRecord.path}).`
4084
- );
4085
- continue;
4086
- }
4087
-
4088
- const baseNowMs = Date.now();
4089
- let targetPath = null;
4090
- for (let secondOffset = 0; secondOffset < 86400; secondOffset += 1) {
4091
- const timestamp = formatMigrationTimestamp(new Date(baseNowMs + secondOffset * 1000));
4092
- const candidateRelativePath = buildManagedMigrationRelativePath({
4093
- toDir,
4094
- packageId: packageEntry.packageId,
4095
- migrationId,
4096
- extension,
4097
- timestamp
4098
- });
4099
- const candidatePath = resolveAppRelativePathWithinRoot(
4100
- appRoot,
4101
- candidateRelativePath,
4102
- `${packageEntry.packageId} migration path for ${migrationId}`
4103
- );
4104
- if (await fileExists(candidatePath.absolutePath)) {
4105
- continue;
4106
- }
4107
- targetPath = candidatePath;
4108
- break;
4109
- }
4110
-
4111
- if (!targetPath) {
4112
- throw createCliError(
4113
- `${packageEntry.packageId}: unable to allocate migration filename for ${migrationId} in ${toDir}.`
4114
- );
4115
- }
4116
-
4117
- const relativePath = targetPath.relativePath;
4118
- const absolutePath = targetPath.absolutePath;
4119
- await mkdir(path.dirname(absolutePath), { recursive: true });
4120
- await writeFile(absolutePath, renderedSourceContent, "utf8");
4121
- touchedFiles.add(relativePath);
4122
-
4123
- const nextManagedRecord = {
4124
- id: migrationId,
4125
- path: relativePath,
4126
- hash: sourceHash,
4127
- skipped: false,
4128
- reason: mutation.reason,
4129
- category: mutation.category
4130
- };
4131
- managedMigrationById.set(migrationId, nextManagedRecord);
4132
- upsertManagedMigrationRecord(managedMigrations, nextManagedRecord);
4133
- continue;
4134
- }
4135
-
4136
- if (operation !== "copy-file") {
4137
- throw createCliError(`Unsupported files mutation op \"${operation}\" in ${packageEntry.packageId}.`);
4138
- }
4139
-
4140
- const from = mutation.from;
4141
- const to = mutation.to;
4142
- const toSurface = mutation.toSurface;
4143
- if (to && toSurface) {
4144
- throw createCliError(
4145
- `Invalid files mutation in ${packageEntry.packageId}: "to" and "toSurface" cannot both be set.`
4146
- );
4147
- }
4148
- if (!from || (!to && !toSurface)) {
4149
- throw createCliError(
4150
- `Invalid files mutation in ${packageEntry.packageId}: "from" plus one destination ("to" or "toSurface") are required.`
4151
- );
4152
- }
4153
-
4154
- const sourcePath = path.join(packageEntry.rootDir, from);
4155
- if (!(await fileExists(sourcePath))) {
4156
- throw createCliError(`Missing template source ${sourcePath} for ${packageEntry.packageId}.`);
4157
- }
4158
-
4159
- const targetPaths = toSurface
4160
- ? resolveSurfaceTargetPathsForMutation({
4161
- appRoot,
4162
- packageId: packageEntry.packageId,
4163
- mutation,
4164
- configContext
4165
- })
4166
- : [path.join(appRoot, to)];
4167
- const hasPrecomputedTemplateContext =
4168
- precomputedTemplateContextByMutationIndex instanceof Map &&
4169
- precomputedTemplateContextByMutationIndex.has(mutationIndex);
4170
- const templateContextReplacements = hasPrecomputedTemplateContext
4171
- ? precomputedTemplateContextByMutationIndex.get(mutationIndex)
4172
- : await resolveTemplateContextReplacementsForMutation({
4173
- packageEntry,
4174
- mutation,
4175
- options,
4176
- appRoot,
4177
- sourcePath,
4178
- targetPaths
4179
- });
4180
- for (const targetPath of targetPaths) {
4181
- const previous = await readFileBufferIfExists(targetPath);
4182
- await copyTemplateFile(
4183
- sourcePath,
4184
- targetPath,
4185
- options,
4186
- packageEntry.packageId,
4187
- `${mutation.id || to || from}.source`,
4188
- templateContextReplacements
4189
- );
4190
- const nextBuffer = await readFile(targetPath);
4191
-
4192
- managedFiles.push({
4193
- path: normalizeRelativePath(appRoot, targetPath),
4194
- hash: hashBuffer(nextBuffer),
4195
- hadPrevious: previous.exists,
4196
- previousContentBase64: previous.exists ? previous.buffer.toString("base64") : "",
4197
- preserveOnRemove: mutation.preserveOnRemove,
4198
- reason: mutation.reason,
4199
- category: mutation.category,
4200
- id: mutation.id
4201
- });
4202
- touchedFiles.add(normalizeRelativePath(appRoot, targetPath));
4203
- }
4204
- }
4205
- }
4206
-
4207
- async function preflightFileMutationTemplateContexts(
4208
- packageEntry,
4209
- options,
4210
- appRoot,
4211
- fileMutations
4212
- ) {
4213
- const mutationList = ensureArray(fileMutations);
4214
- const replacementsByMutationIndex = new Map();
4215
-
4216
- for (const [mutationIndex, mutationValue] of mutationList.entries()) {
4217
- const normalizedMutation = normalizeFileMutationRecord(mutationValue);
4218
- const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
4219
- const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
4220
- if (
4221
- !shouldApplyMutationWhen(normalizedMutation.when, {
4222
- options,
4223
- configContext,
4224
- packageId: packageEntry.packageId,
4225
- mutationContext: "files mutation"
4226
- })
4227
- ) {
4228
- continue;
4229
- }
4230
-
4231
- const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
4232
- const templateContext = ensureObject(mutation.templateContext);
4233
- if (Object.keys(templateContext).length < 1) {
4234
- continue;
4235
- }
4236
-
4237
- const operation = mutation.op || "copy-file";
4238
- if (operation !== "copy-file" && operation !== "install-migration") {
4239
- continue;
4240
- }
4241
-
4242
- const from = mutation.from;
4243
- const to = mutation.to;
4244
- const toSurface = mutation.toSurface;
4245
- if (!from) {
4246
- throw createCliError(
4247
- `Invalid files mutation in ${packageEntry.packageId}: "from" is required.`
4248
- );
4249
- }
4250
- if (operation === "copy-file") {
4251
- if (to && toSurface) {
4252
- throw createCliError(
4253
- `Invalid files mutation in ${packageEntry.packageId}: "to" and "toSurface" cannot both be set.`
4254
- );
4255
- }
4256
- if (!to && !toSurface) {
4257
- throw createCliError(
4258
- `Invalid files mutation in ${packageEntry.packageId}: "from" plus one destination ("to" or "toSurface") are required.`
4259
- );
4260
- }
4261
- }
4262
-
4263
- const sourcePath = path.join(packageEntry.rootDir, from);
4264
- if (!(await fileExists(sourcePath))) {
4265
- throw createCliError(`Missing template source ${sourcePath} for ${packageEntry.packageId}.`);
4266
- }
4267
-
4268
- const targetPaths = operation === "copy-file"
4269
- ? toSurface
4270
- ? resolveSurfaceTargetPathsForMutation({
4271
- appRoot,
4272
- packageId: packageEntry.packageId,
4273
- mutation,
4274
- configContext
4275
- })
4276
- : [path.join(appRoot, to)]
4277
- : [path.join(appRoot, mutation.toDir || "migrations")];
4278
- const replacements = await resolveTemplateContextReplacementsForMutation({
4279
- packageEntry,
4280
- mutation,
4281
- options,
4282
- appRoot,
4283
- sourcePath,
4284
- targetPaths
4285
- });
4286
- replacementsByMutationIndex.set(mutationIndex, replacements);
4287
- }
4288
-
4289
- return replacementsByMutationIndex;
4290
- }
4291
-
4292
- async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
4293
- for (const mutation of textMutations) {
4294
- const when = normalizeMutationWhen(mutation?.when);
4295
- const configContext = when?.config ? await loadMutationWhenConfigContext(appRoot) : {};
4296
- if (
4297
- !shouldApplyMutationWhen(when, {
4298
- options,
4299
- configContext,
4300
- packageId: packageEntry.packageId,
4301
- mutationContext: "text mutation"
4302
- })
4303
- ) {
4304
- continue;
4305
- }
4306
-
4307
- const operation = String(mutation?.op || "").trim();
4308
- if (operation === "upsert-env") {
4309
- const relativeFile = String(mutation?.file || "").trim();
4310
- const key = String(mutation?.key || "").trim();
4311
- if (!relativeFile || !key) {
4312
- throw createCliError(`Invalid upsert-env mutation in ${packageEntry.packageId}: "file" and "key" are required.`);
4313
- }
4314
-
4315
- const absoluteFile = path.join(appRoot, relativeFile);
4316
- const previous = await readFileBufferIfExists(absoluteFile);
4317
- const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
4318
- const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId, key);
4319
- const upserted = upsertEnvValue(previousContent, key, resolvedValue);
4320
-
4321
- await mkdir(path.dirname(absoluteFile), { recursive: true });
4322
- await writeFile(absoluteFile, upserted.content, "utf8");
4323
-
4324
- const recordKey = `${relativeFile}::${String(mutation?.id || key)}`;
4325
- managedText[recordKey] = {
4326
- file: relativeFile,
4327
- op: "upsert-env",
4328
- key,
4329
- value: resolvedValue,
4330
- hadPrevious: upserted.hadPrevious,
4331
- previousValue: upserted.previousValue,
4332
- reason: String(mutation?.reason || ""),
4333
- category: String(mutation?.category || ""),
4334
- id: String(mutation?.id || "")
4335
- };
4336
- touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
4337
- continue;
4338
- }
4339
-
4340
- if (operation === "append-text") {
4341
- const relativeFile = String(mutation?.file || "").trim();
4342
- const snippet = String(mutation?.value || "");
4343
- const position = String(mutation?.position || "bottom").trim().toLowerCase();
4344
- if (!relativeFile) {
4345
- throw createCliError(`Invalid append-text mutation in ${packageEntry.packageId}: "file" is required.`);
4346
- }
4347
- if (position !== "top" && position !== "bottom") {
4348
- throw createCliError(`Invalid append-text mutation in ${packageEntry.packageId}: "position" must be "top" or "bottom".`);
4349
- }
4350
- await validateSettingsFieldsContractMutationTarget({
4351
- appRoot,
4352
- relativeFile,
4353
- packageId: packageEntry.packageId
4354
- });
4355
-
4356
- const absoluteFile = path.join(appRoot, relativeFile);
4357
- const previous = await readFileBufferIfExists(absoluteFile);
4358
- const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
4359
- const mutationId = String(mutation?.id || "").trim() || "append-text";
4360
- const resolvedSnippet = interpolateOptionValue(snippet, options, packageEntry.packageId, mutationId);
4361
- const skipChecks = normalizeSkipChecks(mutation?.skipIfContains)
4362
- .map((entry) => interpolateOptionValue(entry, options, packageEntry.packageId, `${mutationId}.skipIfContains`))
4363
- .filter((entry) => String(entry || "").trim().length > 0);
4364
-
4365
- const shouldSkip = skipChecks.some((pattern) => previousContent.includes(String(pattern)));
4366
- if (shouldSkip) {
4367
- continue;
4368
- }
4369
-
4370
- const appended = appendTextSnippet(previousContent, resolvedSnippet, position);
4371
- if (!appended.changed) {
4372
- continue;
4373
- }
4374
-
4375
- await mkdir(path.dirname(absoluteFile), { recursive: true });
4376
- await writeFile(absoluteFile, appended.content, "utf8");
4377
-
4378
- const recordKey = `${relativeFile}::${mutationId}`;
4379
- managedText[recordKey] = {
4380
- file: relativeFile,
4381
- op: "append-text",
4382
- value: resolvedSnippet,
4383
- position,
4384
- reason: String(mutation?.reason || ""),
4385
- category: String(mutation?.category || ""),
4386
- id: String(mutation?.id || "")
4387
- };
4388
- touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
4389
- continue;
4390
- }
4391
-
4392
- throw createCliError(`Unsupported text mutation op "${operation}" in ${packageEntry.packageId}.`);
4393
- }
4394
- }
4395
-
4396
- function normalizeMutationRelativeFilePath(value = "") {
4397
- return String(value || "")
4398
- .trim()
4399
- .replace(/\\/g, "/")
4400
- .replace(/\/{2,}/g, "/")
4401
- .replace(/^\.\/+/, "")
4402
- .replace(/^\/+/, "");
4403
- }
4404
-
4405
- function resolveSettingsFieldsContractTarget(relativeFile = "") {
4406
- const normalizedRelativeFile = normalizeMutationRelativeFilePath(relativeFile);
4407
- if (!normalizedRelativeFile) {
4408
- return null;
4409
- }
4410
- const target = SETTINGS_FIELDS_CONTRACT_TARGETS[normalizedRelativeFile];
4411
- if (!target) {
4412
- return null;
4413
- }
4414
- return {
4415
- normalizedRelativeFile,
4416
- target
4417
- };
4418
- }
4419
-
4420
- async function validateSettingsFieldsContractMutationTarget({
4421
- appRoot,
4422
- relativeFile,
4423
- packageId
4424
- } = {}) {
4425
- const contractTarget = resolveSettingsFieldsContractTarget(relativeFile);
4426
- if (!contractTarget) {
4427
- return;
4428
- }
4429
-
4430
- const { normalizedRelativeFile, target } = contractTarget;
4431
- const absoluteFile = path.join(appRoot, normalizedRelativeFile);
4432
- const existing = await readFileBufferIfExists(absoluteFile);
4433
- if (!existing.exists) {
4434
- throw createCliError(
4435
- `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing. ` +
4436
- `Install @jskit-ai/users-core to scaffold ${target.contractId}.`
4437
- );
4438
- }
4439
-
4440
- const source = existing.buffer.toString("utf8");
4441
- if (!source.includes(target.marker)) {
4442
- throw createCliError(
4443
- `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing contract marker "${target.marker}".`
4444
- );
4445
- }
4446
- for (const snippet of target.requiredSnippets) {
4447
- if (source.includes(snippet)) {
4448
- continue;
4449
- }
4450
- throw createCliError(
4451
- `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} must include "${snippet}" for ${target.contractId}.`
4452
- );
4453
- }
4454
- }
4455
-
4456
- function isPositioningTextMutation(value = {}) {
4457
- const mutation = ensureObject(value);
4458
- const operation = String(mutation.op || "").trim();
4459
- if (operation !== "append-text") {
4460
- return false;
4461
- }
4462
- return normalizeMutationRelativeFilePath(mutation.file) === "src/placement.js";
4463
- }
4464
-
4465
- function resolvePositioningMutations(descriptorMutations = {}) {
4466
- const mutations = ensureObject(descriptorMutations);
4467
- const files = ensureArray(mutations.files).filter((mutationValue) => {
4468
- const normalized = normalizeFileMutationRecord(mutationValue);
4469
- return Boolean(normalized.toSurface);
4470
- });
4471
- const text = ensureArray(mutations.text).filter((mutationValue) => isPositioningTextMutation(mutationValue));
4472
- return {
4473
- files,
4474
- text
4475
- };
4476
- }
4477
-
4478
- function cloneManagedMap(value = {}) {
4479
- const cloned = {};
4480
- for (const [key, entry] of Object.entries(ensureObject(value))) {
4481
- cloned[key] = {
4482
- ...ensureObject(entry)
4483
- };
4484
- }
4485
- return cloned;
4486
- }
4487
-
4488
- function cloneManagedArray(value = []) {
4489
- return ensureArray(value).map((entry) => ({
4490
- ...ensureObject(entry)
4491
- }));
4492
- }
4493
-
4494
- function resolveManagedSourceRecord(packageEntry, existingInstall = {}) {
4495
- const existingSource = ensureObject(existingInstall.source);
4496
- if (Object.keys(existingSource).length > 0) {
4497
- return {
4498
- ...existingSource
4499
- };
4500
- }
4501
-
4502
- const sourceRecord = {
4503
- type: String(packageEntry?.sourceType || "packages-directory"),
4504
- ...ensureObject(packageEntry?.source)
4505
- };
4506
- if (!sourceRecord.descriptorPath && String(packageEntry?.descriptorRelativePath || "").trim()) {
4507
- sourceRecord.descriptorPath = String(packageEntry.descriptorRelativePath).trim();
4508
- }
4509
- return sourceRecord;
4510
- }
4511
-
4512
- async function applyPackagePositioning({
4513
- packageEntry,
4514
- packageOptions,
4515
- appRoot,
4516
- lock,
4517
- touchedFiles
4518
- }) {
4519
- const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
4520
- if (Object.keys(existingInstall).length < 1) {
4521
- throw createCliError(`Package is not installed: ${packageEntry.packageId}`);
4522
- }
4523
-
4524
- const existingManaged = ensureObject(existingInstall.managed);
4525
- const existingPackageJsonManaged = ensureObject(existingManaged.packageJson);
4526
- const nextManaged = {
4527
- packageJson: {
4528
- dependencies: cloneManagedMap(existingPackageJsonManaged.dependencies),
4529
- devDependencies: cloneManagedMap(existingPackageJsonManaged.devDependencies),
4530
- scripts: cloneManagedMap(existingPackageJsonManaged.scripts)
4531
- },
4532
- text: cloneManagedMap(existingManaged.text),
4533
- vite: cloneManagedMap(existingManaged.vite),
4534
- files: cloneManagedArray(existingManaged.files),
4535
- migrations: cloneManagedArray(existingManaged.migrations)
4536
- };
4537
-
4538
- const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
4539
- const packageEntryForMutations =
4540
- templateRoot === packageEntry.rootDir
4541
- ? packageEntry
4542
- : {
4543
- ...packageEntry,
4544
- rootDir: templateRoot
4545
- };
4546
-
4547
- const mutations = ensureObject(packageEntry.descriptor.mutations);
4548
- const positioningMutations = resolvePositioningMutations(mutations);
4549
- const appliedManagedFiles = [];
4550
- const appliedManagedText = {};
4551
- if (positioningMutations.files.length > 0) {
4552
- await applyFileMutations(
4553
- packageEntryForMutations,
4554
- packageOptions,
4555
- appRoot,
4556
- positioningMutations.files,
4557
- appliedManagedFiles,
4558
- [],
4559
- touchedFiles
4560
- );
4561
- }
4562
- if (positioningMutations.text.length > 0) {
4563
- await applyTextMutations(
4564
- packageEntryForMutations,
4565
- appRoot,
4566
- positioningMutations.text,
4567
- packageOptions,
4568
- appliedManagedText,
4569
- touchedFiles
4570
- );
4571
- }
4572
-
4573
- if (appliedManagedFiles.length > 0) {
4574
- const replacedPaths = new Set(
4575
- appliedManagedFiles
4576
- .map((entry) => String(ensureObject(entry).path || "").trim())
4577
- .filter(Boolean)
4578
- );
4579
- const retainedFiles = nextManaged.files.filter((entry) => {
4580
- const managedPath = String(ensureObject(entry).path || "").trim();
4581
- return !managedPath || !replacedPaths.has(managedPath);
4582
- });
4583
- nextManaged.files = [...retainedFiles, ...appliedManagedFiles];
4584
- }
4585
-
4586
- if (Object.keys(appliedManagedText).length > 0) {
4587
- nextManaged.text = {
4588
- ...nextManaged.text,
4589
- ...appliedManagedText
4590
- };
4591
- }
4592
-
4593
- const managedRecord = {
4594
- ...existingInstall,
4595
- packageId: packageEntry.packageId,
4596
- version: packageEntry.version,
4597
- source: resolveManagedSourceRecord(packageEntry, existingInstall),
4598
- managed: nextManaged,
4599
- options: {
4600
- ...ensureObject(packageOptions)
4601
- },
4602
- installedAt: String(existingInstall.installedAt || new Date().toISOString())
4603
- };
4604
- lock.installedPackages[packageEntry.packageId] = managedRecord;
4605
- return managedRecord;
4606
- }
4607
-
4608
- async function applyPackageMigrationsOnly({
4609
- packageEntry,
4610
- packageOptions,
4611
- appRoot,
4612
- lock,
4613
- touchedFiles
4614
- }) {
4615
- const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
4616
- if (Object.keys(existingInstall).length < 1) {
4617
- throw createCliError(`Package is not installed: ${packageEntry.packageId}`);
4618
- }
4619
-
4620
- const existingManaged = ensureObject(existingInstall.managed);
4621
- const existingPackageJsonManaged = ensureObject(existingManaged.packageJson);
4622
- const nextManaged = {
4623
- packageJson: {
4624
- dependencies: cloneManagedMap(existingPackageJsonManaged.dependencies),
4625
- devDependencies: cloneManagedMap(existingPackageJsonManaged.devDependencies),
4626
- scripts: cloneManagedMap(existingPackageJsonManaged.scripts)
4627
- },
4628
- text: cloneManagedMap(existingManaged.text),
4629
- vite: cloneManagedMap(existingManaged.vite),
4630
- files: cloneManagedArray(existingManaged.files),
4631
- migrations: cloneManagedArray(existingManaged.migrations)
4632
- };
4633
-
4634
- const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
4635
- const packageEntryForMutations =
4636
- templateRoot === packageEntry.rootDir
4637
- ? packageEntry
4638
- : {
4639
- ...packageEntry,
4640
- rootDir: templateRoot
4641
- };
4642
- const mutations = ensureObject(packageEntry.descriptor.mutations);
4643
- const migrationFileMutations = ensureArray(mutations.files).filter((mutationValue) => {
4644
- const normalized = normalizeFileMutationRecord(mutationValue);
4645
- const operation = String(normalized.op || "copy-file").trim();
4646
- return operation === "install-migration";
4647
- });
4648
- const mutationWarnings = [];
4649
-
4650
- if (migrationFileMutations.length > 0) {
4651
- await applyFileMutations(
4652
- packageEntryForMutations,
4653
- packageOptions,
4654
- appRoot,
4655
- migrationFileMutations,
4656
- [],
4657
- nextManaged.migrations,
4658
- touchedFiles,
4659
- mutationWarnings
4660
- );
4661
- }
4662
-
4663
- const managedRecord = {
4664
- ...existingInstall,
4665
- packageId: packageEntry.packageId,
4666
- source: resolveManagedSourceRecord(packageEntry, existingInstall),
4667
- managed: nextManaged,
4668
- options: {
4669
- ...ensureObject(packageOptions)
4670
- },
4671
- migrationSyncVersion: packageEntry.version,
4672
- installedAt: String(existingInstall.installedAt || new Date().toISOString())
4673
- };
4674
- lock.installedPackages[packageEntry.packageId] = managedRecord;
4675
- if (mutationWarnings.length > 0) {
4676
- managedRecord.warnings = mutationWarnings;
4677
- }
4678
- return managedRecord;
4679
- }
4680
-
4681
- async function applyPackageInstall({
4682
- packageEntry,
4683
- packageOptions,
4684
- appRoot,
4685
- appPackageJson,
4686
- lock,
4687
- packageRegistry,
4688
- touchedFiles
4689
- }) {
4690
- const existingInstall = ensureObject(lock.installedPackages[packageEntry.packageId]);
4691
- const existingManaged = ensureObject(existingInstall.managed);
4692
- await removeManagedViteProxyEntries({
4693
- appRoot,
4694
- packageId: packageEntry.packageId,
4695
- managedViteChanges: ensureObject(existingManaged.vite),
4696
- touchedFiles
4697
- });
4698
-
4699
- const managedRecord = createManagedRecordBase(packageEntry, packageOptions);
4700
- managedRecord.managed.migrations = cloneManagedArray(existingManaged.migrations);
4701
- const generatorPackage = isGeneratorPackageEntry(packageEntry);
4702
- const mutationWarnings = [];
4703
- const mutations = ensureObject(packageEntry.descriptor.mutations);
4704
- const fileMutations = ensureArray(mutations.files);
4705
- const templateRoot = await resolvePackageTemplateRoot({ packageEntry, appRoot });
4706
- const packageEntryForMutations =
4707
- templateRoot === packageEntry.rootDir
4708
- ? packageEntry
4709
- : {
4710
- ...packageEntry,
4711
- rootDir: templateRoot
4712
- };
4713
-
4714
- const precomputedTemplateContextByMutationIndex = await preflightFileMutationTemplateContexts(
4715
- packageEntryForMutations,
4716
- packageOptions,
4717
- appRoot,
4718
- fileMutations
4719
- );
4720
-
4721
- const mutationDependencies = ensureObject(mutations.dependencies);
4722
- const runtimeDependencies = ensureObject(mutationDependencies.runtime);
4723
- const devDependencies = ensureObject(mutationDependencies.dev);
4724
- const mutationScripts = ensureObject(ensureObject(mutations.packageJson).scripts);
4725
-
4726
- for (const [rawDependencyId, rawDependencyVersion] of Object.entries(runtimeDependencies)) {
4727
- const dependencyId = interpolateOptionValue(
4728
- rawDependencyId,
4729
- packageOptions,
4730
- packageEntry.packageId,
4731
- `dependencies.runtime.${rawDependencyId}.id`
4732
- );
4733
- const dependencyVersion = interpolateOptionValue(
4734
- String(rawDependencyVersion || ""),
4735
- packageOptions,
4736
- packageEntry.packageId,
4737
- `dependencies.runtime.${rawDependencyId}.value`
4738
- );
4739
- if (!dependencyId) {
4740
- throw createCliError(
4741
- `Invalid runtime dependency key after option interpolation in ${packageEntry.packageId}: ${rawDependencyId}`
4742
- );
4743
- }
4744
-
4745
- const localPackage = packageRegistry.get(dependencyId);
4746
- const existingRuntimeDependencyValue = String(ensureObject(appPackageJson.dependencies)[dependencyId] || "").trim();
4747
- const resolvedValue = localPackage
4748
- ? resolvePackageDependencySpecifier(localPackage, { existingValue: existingRuntimeDependencyValue })
4749
- : String(dependencyVersion);
4750
- const normalizedResolvedValue = normalizeJskitDependencySpecifier(dependencyId, resolvedValue);
4751
- const applied = applyPackageJsonField(appPackageJson, "dependencies", dependencyId, normalizedResolvedValue);
4752
- if (applied.changed) {
4753
- managedRecord.managed.packageJson.dependencies[dependencyId] = applied.managed;
4754
- touchedFiles.add("package.json");
4755
- }
4756
- }
4757
-
4758
- for (const [rawDependencyId, rawDependencyVersion] of Object.entries(devDependencies)) {
4759
- const dependencyId = interpolateOptionValue(
4760
- rawDependencyId,
4761
- packageOptions,
4762
- packageEntry.packageId,
4763
- `dependencies.dev.${rawDependencyId}.id`
4764
- );
4765
- const dependencyVersion = interpolateOptionValue(
4766
- String(rawDependencyVersion || ""),
4767
- packageOptions,
4768
- packageEntry.packageId,
4769
- `dependencies.dev.${rawDependencyId}.value`
4770
- );
4771
- if (!dependencyId) {
4772
- throw createCliError(
4773
- `Invalid dev dependency key after option interpolation in ${packageEntry.packageId}: ${rawDependencyId}`
4774
- );
4775
- }
4776
-
4777
- const localPackage = packageRegistry.get(dependencyId);
4778
- const existingDevDependencyValue = String(ensureObject(appPackageJson.devDependencies)[dependencyId] || "").trim();
4779
- const resolvedValue = localPackage
4780
- ? resolvePackageDependencySpecifier(localPackage, { existingValue: existingDevDependencyValue })
4781
- : String(dependencyVersion);
4782
- const normalizedResolvedValue = normalizeJskitDependencySpecifier(dependencyId, resolvedValue);
4783
- const applied = applyPackageJsonField(appPackageJson, "devDependencies", dependencyId, normalizedResolvedValue);
4784
- if (applied.changed) {
4785
- managedRecord.managed.packageJson.devDependencies[dependencyId] = applied.managed;
4786
- touchedFiles.add("package.json");
4787
- }
4788
- }
4789
-
4790
- if (generatorPackage) {
4791
- const removedRuntimeDependency = removePackageJsonField(appPackageJson, "dependencies", packageEntry.packageId);
4792
- const removedDevDependency = removePackageJsonField(appPackageJson, "devDependencies", packageEntry.packageId);
4793
- if (removedRuntimeDependency || removedDevDependency) {
4794
- touchedFiles.add("package.json");
4795
- }
4796
- } else {
4797
- const existingSelfDependencyValue = String(ensureObject(appPackageJson.dependencies)[packageEntry.packageId] || "").trim();
4798
- const selfDependencyValue = resolvePackageDependencySpecifier(packageEntry, {
4799
- existingValue: existingSelfDependencyValue
4800
- });
4801
- const normalizedSelfDependencyValue = normalizeJskitDependencySpecifier(packageEntry.packageId, selfDependencyValue);
4802
- const selfApplied = applyPackageJsonField(
4803
- appPackageJson,
4804
- "dependencies",
4805
- packageEntry.packageId,
4806
- normalizedSelfDependencyValue
4807
- );
4808
- if (selfApplied.changed) {
4809
- managedRecord.managed.packageJson.dependencies[packageEntry.packageId] = selfApplied.managed;
4810
- touchedFiles.add("package.json");
4811
- }
4812
- }
4813
-
4814
- for (const [scriptName, scriptValue] of Object.entries(mutationScripts)) {
4815
- const applied = applyPackageJsonField(appPackageJson, "scripts", scriptName, scriptValue);
4816
- if (applied.changed) {
4817
- managedRecord.managed.packageJson.scripts[scriptName] = applied.managed;
4818
- touchedFiles.add("package.json");
4819
- }
4820
- }
4821
-
4822
- await applyFileMutations(
4823
- packageEntryForMutations,
4824
- packageOptions,
4825
- appRoot,
4826
- fileMutations,
4827
- managedRecord.managed.files,
4828
- managedRecord.managed.migrations,
4829
- touchedFiles,
4830
- mutationWarnings,
4831
- precomputedTemplateContextByMutationIndex
4832
- );
4833
-
4834
- await applyTextMutations(
4835
- packageEntryForMutations,
4836
- appRoot,
4837
- ensureArray(mutations.text),
4838
- packageOptions,
4839
- managedRecord.managed.text,
4840
- touchedFiles
4841
- );
4842
-
4843
- await applyViteMutations(
4844
- packageEntryForMutations,
4845
- appRoot,
4846
- ensureObject(mutations.vite),
4847
- packageOptions,
4848
- managedRecord.managed.vite,
4849
- touchedFiles
4850
- );
4851
-
4852
- if (generatorPackage) {
4853
- delete lock.installedPackages[packageEntry.packageId];
4854
- } else {
4855
- managedRecord.migrationSyncVersion = packageEntry.version;
4856
- lock.installedPackages[packageEntry.packageId] = managedRecord;
4857
- }
4858
- if (mutationWarnings.length > 0) {
4859
- managedRecord.warnings = mutationWarnings;
4860
- }
4861
- return managedRecord;
4862
- }
4863
-
4864
- async function adoptAppLocalPackageDependencies({
4865
- appRoot,
4866
- appPackageJson,
4867
- lock
4868
- }) {
4869
- const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
4870
- const runtimeDependencies = ensureObject(appPackageJson.dependencies);
4871
- const adoptedPackageIds = [];
4872
-
4873
- for (const dependencyId of sortStrings(Object.keys(runtimeDependencies))) {
4874
- if (lock.installedPackages[dependencyId]) {
4875
- continue;
4876
- }
4877
-
4878
- const localPackageEntry = appLocalRegistry.get(dependencyId);
4879
- if (!localPackageEntry) {
4880
- continue;
4881
- }
4882
-
4883
- lock.installedPackages[dependencyId] = createManagedRecordBase(localPackageEntry, {});
4884
- adoptedPackageIds.push(dependencyId);
4885
- }
4886
-
4887
- return {
4888
- appLocalRegistry,
4889
- adoptedPackageIds: sortStrings(adoptedPackageIds)
4890
- };
4891
- }
4892
-
4893
- const commandHandlers = createCommandHandlers(
4894
- createCommandHandlerDeps({
4895
- createCliError,
4896
- createColorFormatter,
4897
- resolveWrapWidth,
4898
- writeWrappedItems,
4899
- normalizeRelativePath,
4900
- normalizeRelativePosixPath,
4901
- resolveAppRootFromCwd,
4902
- loadLockFile,
4903
- loadPackageRegistry,
4904
- loadBundleRegistry,
4905
- loadAppLocalPackageRegistry,
4906
- mergePackageRegistries,
4907
- resolvePackageIdInput,
4908
- resolveInstalledPackageIdInput,
4909
- resolveInstalledNodeModulePackageEntry,
4910
- hydratePackageRegistryFromInstalledNodeModules,
4911
- validateInlineOptionsForPackage,
4912
- resolveLocalDependencyOrder,
4913
- validatePlannedCapabilityClosure,
4914
- resolvePackageOptions,
4915
- applyPackageInstall,
4916
- applyPackageMigrationsOnly,
4917
- applyPackagePositioning,
4918
- adoptAppLocalPackageDependencies,
4919
- loadAppPackageJson,
4920
- resolveLocalPackageId,
4921
- createLocalPackageScaffoldFiles,
4922
- fileExists,
4923
- applyPackageJsonField,
4924
- toFileDependencySpecifier,
4925
- writeJsonFile,
4926
- writeFile,
4927
- mkdir,
4928
- path,
4929
- inspectPackageOfferings,
4930
- buildFileWriteGroups,
4931
- listDeclaredCapabilities,
4932
- buildCapabilityDetailsForPackage,
4933
- formatPackageSubpathImport,
4934
- normalizePlacementOutlets,
4935
- normalizePlacementContributions,
4936
- shouldShowPackageExportTarget,
4937
- classifyExportedSymbols,
4938
- deriveProviderDisplayName,
4939
- restorePackageJsonField,
4940
- readFileBufferIfExists,
4941
- removeEnvValue,
4942
- removeManagedViteProxyEntries,
4943
- hashBuffer,
4944
- rm
4945
- })
4946
- );
4947
-
4948
- const runCli = createRunCli({
4949
- parseArgs,
4950
- printUsage,
4951
- commandHandlers,
4952
- cleanupMaterializedPackageRoots,
4953
- createCliError
4954
- });
4955
-
4956
- export { runCli };