@jskit-ai/jskit-cli 0.2.4

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.
@@ -0,0 +1,1657 @@
1
+ import { spawn } from "node:child_process";
2
+ import {
3
+ ensureArray,
4
+ ensureObject,
5
+ sortStrings
6
+ } from "./collectionUtils.js";
7
+
8
+ function createCommandHandlers(deps) {
9
+ const {
10
+ createCliError,
11
+ createColorFormatter,
12
+ resolveWrapWidth,
13
+ writeWrappedItems,
14
+ normalizeRelativePath,
15
+ normalizeRelativePosixPath,
16
+ resolveAppRootFromCwd,
17
+ loadLockFile,
18
+ loadPackageRegistry,
19
+ loadBundleRegistry,
20
+ loadAppLocalPackageRegistry,
21
+ mergePackageRegistries,
22
+ resolvePackageIdInput,
23
+ resolveInstalledPackageIdInput,
24
+ resolveInstalledNodeModulePackageEntry,
25
+ hydratePackageRegistryFromInstalledNodeModules,
26
+ validateInlineOptionsForPackage,
27
+ resolveLocalDependencyOrder,
28
+ validatePlannedCapabilityClosure,
29
+ resolvePackageOptions,
30
+ applyPackageInstall,
31
+ applyPackagePositioning,
32
+ adoptAppLocalPackageDependencies,
33
+ loadAppPackageJson,
34
+ resolveLocalPackageId,
35
+ createLocalPackageScaffoldFiles,
36
+ fileExists,
37
+ applyPackageJsonField,
38
+ toFileDependencySpecifier,
39
+ writeJsonFile,
40
+ writeFile,
41
+ mkdir,
42
+ path,
43
+ inspectPackageOfferings,
44
+ buildFileWriteGroups,
45
+ listDeclaredCapabilities,
46
+ buildCapabilityDetailsForPackage,
47
+ formatPackageSubpathImport,
48
+ normalizePlacementOutlets,
49
+ normalizePlacementContributions,
50
+ shouldShowPackageExportTarget,
51
+ classifyExportedSymbols,
52
+ deriveProviderDisplayName,
53
+ restorePackageJsonField,
54
+ readFileBufferIfExists,
55
+ removeEnvValue,
56
+ removeManagedViteProxyEntries,
57
+ hashBuffer,
58
+ rm
59
+ } = deps;
60
+
61
+ function renderResolvedSummary(commandType, targetId, resolvedPackageIds, touchedFiles, appRoot, lockPath, externalDependencies) {
62
+ const lines = [];
63
+ lines.push(`${commandType} ${targetId}.`);
64
+ lines.push(`Resolved packages (${resolvedPackageIds.length}):`);
65
+ for (const packageId of resolvedPackageIds) {
66
+ lines.push(`- ${packageId}`);
67
+ }
68
+
69
+ if (externalDependencies.length > 0) {
70
+ lines.push(`External dependencies (${externalDependencies.length}):`);
71
+ for (const dependencyId of externalDependencies) {
72
+ lines.push(`- ${dependencyId}`);
73
+ }
74
+ }
75
+
76
+ lines.push(`Touched files (${touchedFiles.length}):`);
77
+ for (const touchedFile of touchedFiles) {
78
+ lines.push(`- ${touchedFile}`);
79
+ }
80
+ lines.push(`Lock file: ${normalizeRelativePath(appRoot, lockPath)}`);
81
+ return lines.join("\n");
82
+ }
83
+
84
+ async function runNpmInstall(appRoot, stderr) {
85
+ await new Promise((resolve, reject) => {
86
+ const child = spawn("npm", ["install"], {
87
+ cwd: appRoot,
88
+ stdio: "inherit"
89
+ });
90
+
91
+ child.on("error", reject);
92
+ child.on("exit", (code) => {
93
+ if (code === 0) {
94
+ resolve();
95
+ } else {
96
+ reject(createCliError(`npm install failed with exit code ${code}.`));
97
+ }
98
+ });
99
+ }).catch((error) => {
100
+ stderr.write(`npm install failed: ${error.message}\n`);
101
+ throw error;
102
+ });
103
+ }
104
+
105
+ function getInstalledDependents(lock, packageId, packageRegistry) {
106
+ const dependents = [];
107
+ const installedPackageIds = Object.keys(ensureObject(lock.installedPackages));
108
+
109
+ for (const installedId of installedPackageIds) {
110
+ if (installedId === packageId) {
111
+ continue;
112
+ }
113
+ const packageEntry = packageRegistry.get(installedId);
114
+ if (!packageEntry) {
115
+ continue;
116
+ }
117
+ const dependencies = ensureArray(packageEntry.descriptor.dependsOn).map((value) => String(value));
118
+ if (dependencies.includes(packageId)) {
119
+ dependents.push(installedId);
120
+ }
121
+ }
122
+
123
+ return sortStrings(dependents);
124
+ }
125
+
126
+ function resolvePackageOptionNames(packageEntry) {
127
+ const optionSchemas = ensureObject(packageEntry?.descriptor?.options);
128
+ return Object.keys(optionSchemas);
129
+ }
130
+
131
+ function resolveBundleInlineOptionsForPackage(packageEntry, inlineOptions) {
132
+ const allowedOptionNames = new Set(resolvePackageOptionNames(packageEntry));
133
+ const resolved = {};
134
+
135
+ for (const [optionName, optionValue] of Object.entries(ensureObject(inlineOptions))) {
136
+ if (!allowedOptionNames.has(optionName)) {
137
+ continue;
138
+ }
139
+ resolved[optionName] = String(optionValue || "").trim();
140
+ }
141
+
142
+ return resolved;
143
+ }
144
+
145
+ function validateInlineOptionsForBundle({
146
+ bundleId,
147
+ inlineOptions,
148
+ packageIds,
149
+ packageRegistry
150
+ }) {
151
+ const providedOptionNames = Object.keys(ensureObject(inlineOptions));
152
+ if (providedOptionNames.length < 1) {
153
+ return;
154
+ }
155
+
156
+ const allowedOptionNames = new Set();
157
+ for (const packageId of ensureArray(packageIds).map((value) => String(value || "").trim()).filter(Boolean)) {
158
+ const packageEntry = packageRegistry.get(packageId);
159
+ if (!packageEntry) {
160
+ continue;
161
+ }
162
+ for (const optionName of resolvePackageOptionNames(packageEntry)) {
163
+ allowedOptionNames.add(optionName);
164
+ }
165
+ }
166
+
167
+ const unknownOptionNames = providedOptionNames.filter((optionName) => !allowedOptionNames.has(optionName));
168
+ if (unknownOptionNames.length < 1) {
169
+ return;
170
+ }
171
+
172
+ const sortedUnknown = sortStrings(unknownOptionNames);
173
+ const sortedAllowed = sortStrings([...allowedOptionNames]);
174
+ const suffix = sortedAllowed.length > 0
175
+ ? ` Allowed options: ${sortedAllowed.join(", ")}.`
176
+ : " This bundle does not accept inline options.";
177
+ throw createCliError(`Unknown option(s) for bundle ${bundleId}: ${sortedUnknown.join(", ")}.${suffix}`);
178
+ }
179
+
180
+ async function commandList({ positional, options, cwd, stdout }) {
181
+ const packageRegistry = await loadPackageRegistry();
182
+ const bundleRegistry = await loadBundleRegistry();
183
+
184
+ const appRoot = await resolveAppRootFromCwd(cwd);
185
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
186
+ const { lock } = await loadLockFile(appRoot);
187
+ const installedPackageEntries = ensureObject(lock.installedPackages);
188
+ const installedPackages = new Set(Object.keys(installedPackageEntries));
189
+ const installedUnknownPackageIds = sortStrings(
190
+ [...installedPackages].filter((packageId) => !packageRegistry.has(packageId))
191
+ );
192
+ const installedLocalPackageIds = sortStrings(
193
+ installedUnknownPackageIds.filter((packageId) => {
194
+ const lockEntry = ensureObject(installedPackageEntries[packageId]);
195
+ const sourceType = String(ensureObject(lockEntry.source).type || "").trim();
196
+ return sourceType === "local-package" || sourceType === "app-local-package" || appLocalRegistry.has(packageId);
197
+ })
198
+ );
199
+ const installedExternalPackageIds = sortStrings(
200
+ installedUnknownPackageIds.filter((packageId) => !installedLocalPackageIds.includes(packageId))
201
+ );
202
+ const availableLocalPackageIds = sortStrings(
203
+ [...appLocalRegistry.keys()].filter((packageId) => !installedPackages.has(packageId))
204
+ );
205
+
206
+ const mode = String(positional[0] || "").trim();
207
+ const shouldListBundles = !mode || mode === "bundles";
208
+ const shouldListPackages = !mode || mode === "packages";
209
+
210
+ if (!shouldListBundles && !shouldListPackages) {
211
+ throw createCliError(`Unknown list mode: ${mode}`, { showUsage: true });
212
+ }
213
+
214
+ const color = createColorFormatter(stdout);
215
+ const lines = [];
216
+ if (shouldListBundles) {
217
+ lines.push(color.heading("Available bundles:"));
218
+ const bundleIds = sortStrings([...bundleRegistry.keys()]);
219
+ for (const bundleId of bundleIds) {
220
+ const bundle = bundleRegistry.get(bundleId);
221
+ const packageIds = ensureArray(bundle.packages).map((value) => String(value));
222
+ const isInstalled = packageIds.length > 0 && packageIds.every((packageId) => installedPackages.has(packageId));
223
+ const providerLabel = Number(bundle.provider) === 1 ? " [provider]" : "";
224
+ const installedLabel = isInstalled ? " (installed)" : "";
225
+ lines.push(
226
+ `- ${color.item(bundle.bundleId)} ${color.version(`(${bundle.version})`)}${isInstalled ? color.installed(installedLabel) : installedLabel}${providerLabel ? color.provider(providerLabel) : providerLabel}: ${String(bundle.description || "")}`
227
+ );
228
+ if (options.full || options.expanded) {
229
+ for (const packageId of packageIds) {
230
+ lines.push(` - ${color.dim(packageId)}`);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ if (shouldListPackages) {
237
+ if (lines.length > 0) {
238
+ lines.push("");
239
+ }
240
+ lines.push(color.heading("Available packages:"));
241
+ const packageIds = sortStrings([...packageRegistry.keys()]);
242
+ for (const packageId of packageIds) {
243
+ const packageEntry = packageRegistry.get(packageId);
244
+ const installedLabel = installedPackages.has(packageId) ? " (installed)" : "";
245
+ lines.push(
246
+ `- ${color.item(packageId)} ${color.version(`(${packageEntry.version})`)}${installedLabel ? color.installed(installedLabel) : ""}`
247
+ );
248
+ }
249
+
250
+ if (installedLocalPackageIds.length > 0) {
251
+ lines.push("");
252
+ lines.push(color.heading("Installed local packages:"));
253
+ for (const packageId of installedLocalPackageIds) {
254
+ const lockEntry = ensureObject(installedPackageEntries[packageId]);
255
+ const version = String(lockEntry.version || "").trim();
256
+ const versionLabel = version ? ` ${color.version(`(${version})`)}` : "";
257
+ lines.push(`- ${color.item(packageId)}${versionLabel}${color.installed(" (installed)")}`);
258
+ }
259
+ }
260
+
261
+ if (installedExternalPackageIds.length > 0) {
262
+ lines.push("");
263
+ lines.push(color.heading("Installed external packages:"));
264
+ for (const packageId of installedExternalPackageIds) {
265
+ const lockEntry = ensureObject(installedPackageEntries[packageId]);
266
+ const version = String(lockEntry.version || "").trim();
267
+ const versionLabel = version ? ` ${color.version(`(${version})`)}` : "";
268
+ lines.push(`- ${color.item(packageId)}${versionLabel}${color.installed(" (installed)")}`);
269
+ }
270
+ }
271
+
272
+ if (availableLocalPackageIds.length > 0) {
273
+ lines.push("");
274
+ lines.push(color.heading("Available local packages (not installed):"));
275
+ for (const packageId of availableLocalPackageIds) {
276
+ const packageEntry = appLocalRegistry.get(packageId);
277
+ const version = String(packageEntry?.version || "").trim();
278
+ const versionLabel = version ? ` ${color.version(`(${version})`)}` : "";
279
+ lines.push(`- ${color.item(packageId)}${versionLabel}`);
280
+ }
281
+ }
282
+ }
283
+
284
+ if (options.json) {
285
+ const payload = {
286
+ bundles: shouldListBundles
287
+ ? sortStrings([...bundleRegistry.keys()]).map((bundleId) => {
288
+ const bundle = bundleRegistry.get(bundleId);
289
+ const packageIds = ensureArray(bundle.packages).map((value) => String(value));
290
+ return {
291
+ bundleId: bundle.bundleId,
292
+ version: bundle.version,
293
+ description: bundle.description || "",
294
+ provider: Number(bundle.provider) === 1,
295
+ installed: packageIds.length > 0 && packageIds.every((packageId) => installedPackages.has(packageId)),
296
+ packages: packageIds
297
+ };
298
+ })
299
+ : [],
300
+ packages: shouldListPackages
301
+ ? sortStrings([...packageRegistry.keys()]).map((packageId) => {
302
+ const packageEntry = packageRegistry.get(packageId);
303
+ return {
304
+ packageId,
305
+ version: packageEntry.version,
306
+ installed: installedPackages.has(packageId)
307
+ };
308
+ })
309
+ : [],
310
+ installedLocalPackages: shouldListPackages
311
+ ? installedLocalPackageIds.map((packageId) => {
312
+ const lockEntry = ensureObject(installedPackageEntries[packageId]);
313
+ return {
314
+ packageId,
315
+ version: String(lockEntry.version || "").trim()
316
+ };
317
+ })
318
+ : [],
319
+ installedExternalPackages: shouldListPackages
320
+ ? installedExternalPackageIds.map((packageId) => {
321
+ const lockEntry = ensureObject(installedPackageEntries[packageId]);
322
+ return {
323
+ packageId,
324
+ version: String(lockEntry.version || "").trim(),
325
+ source: ensureObject(lockEntry.source)
326
+ };
327
+ })
328
+ : [],
329
+ availableLocalPackages: shouldListPackages
330
+ ? availableLocalPackageIds.map((packageId) => {
331
+ const packageEntry = appLocalRegistry.get(packageId);
332
+ return {
333
+ packageId,
334
+ version: String(packageEntry?.version || "").trim(),
335
+ packagePath: normalizeRelativePosixPath(String(packageEntry?.relativeDir || ""))
336
+ };
337
+ })
338
+ : []
339
+ };
340
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
341
+ } else {
342
+ stdout.write(`${lines.join("\n")}\n`);
343
+ }
344
+
345
+ return 0;
346
+ }
347
+
348
+ async function commandShow({ positional, options, stdout }) {
349
+ const id = String(positional[0] || "").trim();
350
+ if (!id) {
351
+ throw createCliError("show requires an id.", { showUsage: true });
352
+ }
353
+
354
+ const packageRegistry = await loadPackageRegistry();
355
+ const bundleRegistry = await loadBundleRegistry();
356
+ const color = createColorFormatter(stdout);
357
+ const writeField = (label, value, formatValue = (raw) => raw) => {
358
+ stdout.write(`${color.dim(`${label}:`)} ${formatValue(String(value || ""))}\n`);
359
+ };
360
+ const writeBindingsSection = (side, bindings) => {
361
+ const sectionSide = String(side || "").trim().toLowerCase();
362
+ const bindingEntries = ensureArray(bindings);
363
+ stdout.write(`${color.heading(`Container bindings ${sectionSide} (${bindingEntries.length}):`)}\n`);
364
+ if (bindingEntries.length < 1) {
365
+ stdout.write(`- ${color.dim("none detected")}\n`);
366
+ return;
367
+ }
368
+
369
+ for (const bindingRecord of bindingEntries) {
370
+ const binding = ensureObject(bindingRecord);
371
+ const token = String(binding.token || "").trim();
372
+ const tokenExpression = String(binding.tokenExpression || "").trim();
373
+ const tokenLabel = binding.tokenResolved === true
374
+ ? token
375
+ : token || tokenExpression;
376
+ const bindingMethod = String(binding.binding || "").trim();
377
+ const providerName = deriveProviderDisplayName(binding);
378
+ const lifecycle = String(binding.lifecycle || "").trim();
379
+ const lifecycleSuffix = lifecycle && lifecycle !== "unknown" ? ` ${color.dim(`(${lifecycle})`)}` : "";
380
+ const unresolvedSuffix = binding.tokenResolved === true ? "" : color.dim(" [unresolved token]");
381
+ stdout.write(
382
+ `- ${color.item(tokenLabel)} ${color.installed(`[${bindingMethod}]`)} ${color.dim("by")} ${color.item(providerName)}${lifecycleSuffix}${unresolvedSuffix}\n`
383
+ );
384
+ if (options.details) {
385
+ const location = String(binding.location || "").trim();
386
+ if (location) {
387
+ stdout.write(` ${color.dim(`source: ${location}`)}\n`);
388
+ }
389
+ const providerLabel = String(binding.provider || "").trim();
390
+ if (providerLabel) {
391
+ stdout.write(` ${color.dim(`provider: ${providerLabel}`)}\n`);
392
+ }
393
+ if (binding.tokenResolved !== true && tokenExpression) {
394
+ stdout.write(` ${color.dim(`token expression: ${tokenExpression}`)}\n`);
395
+ }
396
+ }
397
+ }
398
+ };
399
+ const writeRuntimeProviders = (side, providers) => {
400
+ const sectionSide = String(side || "").trim().toLowerCase();
401
+ const providerEntries = ensureArray(providers);
402
+ if (providerEntries.length < 1) {
403
+ return;
404
+ }
405
+
406
+ stdout.write(`${color.heading(`Runtime ${sectionSide} providers (${providerEntries.length}):`)}\n`);
407
+ for (const provider of providerEntries) {
408
+ const record = ensureObject(provider);
409
+ const entrypoint = String(record.entrypoint || "").trim();
410
+ const exportName = String(record.export || "").trim();
411
+ const label = exportName ? `${entrypoint}#${exportName}` : entrypoint;
412
+ stdout.write(`- ${color.item(label)}\n`);
413
+ }
414
+ };
415
+ const resolvedPackageId = resolvePackageIdInput(id, packageRegistry);
416
+
417
+ if (resolvedPackageId) {
418
+ const packageEntry = packageRegistry.get(resolvedPackageId);
419
+ const descriptor = packageEntry.descriptor;
420
+ const fileWriteGroups = buildFileWriteGroups(ensureArray(ensureObject(descriptor.mutations).files));
421
+ const fileWriteCount = fileWriteGroups.reduce((total, group) => total + ensureArray(group.files).length, 0);
422
+ const capabilities = ensureObject(descriptor.capabilities);
423
+ const runtime = ensureObject(descriptor.runtime);
424
+ const metadata = ensureObject(descriptor.metadata);
425
+ const mutations = ensureObject(descriptor.mutations);
426
+ const runtimeMutations = ensureObject(ensureObject(mutations.dependencies).runtime);
427
+ const devMutations = ensureObject(ensureObject(mutations.dependencies).dev);
428
+ const scriptMutations = ensureObject(ensureObject(mutations.packageJson).scripts);
429
+ const textMutations = ensureArray(mutations.text);
430
+ const packageInsights = await inspectPackageOfferings({ packageEntry });
431
+ const payload = {
432
+ kind: "package",
433
+ packageId: descriptor.packageId,
434
+ version: descriptor.version,
435
+ description: String(descriptor.description || ""),
436
+ dependsOn: ensureArray(descriptor.dependsOn).map((value) => String(value)),
437
+ capabilities,
438
+ options: ensureObject(descriptor.options),
439
+ runtime,
440
+ metadata,
441
+ mutations,
442
+ fileWritePlan: {
443
+ groupCount: fileWriteGroups.length,
444
+ fileCount: fileWriteCount,
445
+ groups: fileWriteGroups
446
+ },
447
+ descriptorPath: packageEntry.descriptorRelativePath,
448
+ introspection: {
449
+ available: Boolean(packageInsights.available),
450
+ notes: ensureArray(packageInsights.notes)
451
+ },
452
+ packageExports: ensureArray(packageInsights.packageExports),
453
+ containerBindings: ensureObject(packageInsights.containerBindings),
454
+ exportedSymbols: ensureArray(packageInsights.exportedSymbols)
455
+ };
456
+ const provides = listDeclaredCapabilities(payload.capabilities, "provides");
457
+ const requires = listDeclaredCapabilities(payload.capabilities, "requires");
458
+ const capabilityDetails = options.details
459
+ ? buildCapabilityDetailsForPackage({
460
+ packageRegistry,
461
+ packageId: payload.packageId,
462
+ dependsOn: payload.dependsOn,
463
+ provides,
464
+ requires
465
+ })
466
+ : null;
467
+ if (capabilityDetails) {
468
+ payload.capabilityDetails = capabilityDetails;
469
+ }
470
+ if (options.json) {
471
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
472
+ } else {
473
+ const runtimeMutationEntries = Object.entries(runtimeMutations);
474
+ const devMutationEntries = Object.entries(devMutations);
475
+ const scriptMutationEntries = Object.entries(scriptMutations);
476
+ const wrapWidth = resolveWrapWidth(stdout, 80);
477
+ const introspection = ensureObject(payload.introspection);
478
+ const introspectionAvailable = introspection.available === true;
479
+ const introspectionNotes = ensureArray(introspection.notes)
480
+ .map((value) => String(value || "").trim())
481
+ .filter(Boolean);
482
+ const metadataApiSummary = ensureObject(ensureObject(payload.metadata).apiSummary);
483
+ const metadataUi = ensureObject(ensureObject(payload.metadata).ui);
484
+ const summarySurfaces = ensureArray(metadataApiSummary.surfaces)
485
+ .map((entry) => {
486
+ const record = ensureObject(entry);
487
+ return {
488
+ subpath: String(record.subpath || "").trim(),
489
+ summary: String(record.summary || "").trim()
490
+ };
491
+ })
492
+ .filter((entry) => entry.subpath && entry.summary);
493
+ const containerTokenSummary = ensureObject(metadataApiSummary.containerTokens);
494
+ const quickServerTokens = ensureArray(containerTokenSummary.server).map((value) => String(value || "").trim()).filter(Boolean);
495
+ const quickClientTokens = ensureArray(containerTokenSummary.client).map((value) => String(value || "").trim()).filter(Boolean);
496
+ const metadataUiPlacements = ensureObject(metadataUi.placements);
497
+ const placementOutlets = normalizePlacementOutlets(metadataUiPlacements.outlets);
498
+ const placementContributions = normalizePlacementContributions(metadataUiPlacements.contributions);
499
+ const packageExports = ensureArray(payload.packageExports);
500
+ const exportedSymbols = ensureArray(payload.exportedSymbols);
501
+ const exportedSymbolsByFile = new Map(
502
+ exportedSymbols
503
+ .map((entry) => ensureObject(entry))
504
+ .map((entry) => {
505
+ const file = normalizeRelativePosixPath(String(entry.file || "").trim());
506
+ return file ? [file, entry] : null;
507
+ })
508
+ .filter(Boolean)
509
+ );
510
+ const bindingSections = ensureObject(payload.containerBindings);
511
+ const serverBindings = ensureArray(bindingSections.server);
512
+ const clientBindings = ensureArray(bindingSections.client);
513
+ stdout.write(`${color.heading("Information")}\n`);
514
+ writeField("Package", payload.packageId, color.item);
515
+ writeField("Version", payload.version, color.installed);
516
+ if (payload.description) {
517
+ writeField("Description", payload.description);
518
+ }
519
+ writeField("Descriptor", payload.descriptorPath, color.dim);
520
+ if (summarySurfaces.length > 0) {
521
+ stdout.write(`${color.heading("Summary:")}\n`);
522
+ for (const summaryEntry of summarySurfaces) {
523
+ const importPath = formatPackageSubpathImport(payload.packageId, summaryEntry.subpath);
524
+ stdout.write(`- ${color.item(`${importPath}:`)}\n`);
525
+ stdout.write(` ${summaryEntry.summary}\n`);
526
+ }
527
+ }
528
+ if (quickServerTokens.length > 0 || quickClientTokens.length > 0) {
529
+ stdout.write(`${color.heading("Container tokens")} ${color.dim("-- app.make('...'):")}\n`);
530
+ if (quickServerTokens.length > 0) {
531
+ stdout.write(`- ${color.installed("server")}: ${quickServerTokens.map((token) => color.item(token)).join(", ")}\n`);
532
+ }
533
+ if (quickClientTokens.length > 0) {
534
+ stdout.write(`- ${color.installed("client")}: ${quickClientTokens.map((token) => color.item(token)).join(", ")}\n`);
535
+ }
536
+ }
537
+ if (placementOutlets.length > 0) {
538
+ stdout.write(`${color.heading(`Placement outlets (accepted host/position pairs) (${placementOutlets.length}):`)}\n`);
539
+ for (const outlet of placementOutlets) {
540
+ const surfaces = ensureArray(outlet.surfaces).map((value) => String(value || "").trim()).filter(Boolean);
541
+ const surfacesLabel = surfaces.length > 0 ? ` ${color.installed(`[surfaces:${surfaces.join(", ")}]`)}` : "";
542
+ const description = String(outlet.description || "").trim();
543
+ const descriptionSuffix = description ? `: ${description}` : "";
544
+ stdout.write(`- ${color.item(`${outlet.host}.${outlet.position}`)}${surfacesLabel}${descriptionSuffix}\n`);
545
+ if (options.details) {
546
+ const sourceLabel = String(outlet.source || "").trim();
547
+ if (sourceLabel) {
548
+ stdout.write(` ${color.dim(`source: ${sourceLabel}`)}\n`);
549
+ }
550
+ }
551
+ }
552
+ }
553
+ if (placementContributions.length > 0) {
554
+ stdout.write(`${color.heading(`Placement contributions (default entries) (${placementContributions.length}):`)}\n`);
555
+ for (const contribution of placementContributions) {
556
+ const surfaces = ensureArray(contribution.surfaces).map((value) => String(value || "").trim()).filter(Boolean);
557
+ const surfacesLabel = surfaces.length > 0 ? surfaces.join(", ") : "*";
558
+ const orderSuffix = Number.isFinite(contribution.order) ? ` ${color.installed(`[order:${contribution.order}]`)}` : "";
559
+ const componentToken = String(contribution.componentToken || "").trim();
560
+ const componentSuffix = componentToken ? ` ${color.dim(`component:${componentToken}`)}` : "";
561
+ const description = String(contribution.description || "").trim();
562
+ const descriptionSuffix = description ? `: ${description}` : "";
563
+ stdout.write(
564
+ `- ${color.item(contribution.id)} ${color.dim("->")} ${color.item(`${contribution.host}.${contribution.position}`)} ${color.installed(`[surfaces:${surfacesLabel}]`)}${orderSuffix}${componentSuffix}${descriptionSuffix}\n`
565
+ );
566
+ if (options.details) {
567
+ const when = String(contribution.when || "").trim();
568
+ if (when) {
569
+ stdout.write(` ${color.dim(`when: ${when}`)}\n`);
570
+ }
571
+ const sourceLabel = String(contribution.source || "").trim();
572
+ if (sourceLabel) {
573
+ stdout.write(` ${color.dim(`source: ${sourceLabel}`)}\n`);
574
+ }
575
+ }
576
+ }
577
+ }
578
+ if (introspectionAvailable) {
579
+ writeBindingsSection("server", serverBindings);
580
+ writeBindingsSection("client", clientBindings);
581
+ }
582
+ if (introspectionAvailable) {
583
+ stdout.write(`${color.heading(`Package exports (${packageExports.length}):`)}\n`);
584
+ if (packageExports.length < 1) {
585
+ stdout.write(`- ${color.dim("none declared")}\n`);
586
+ } else {
587
+ const symbolDetailsShown = new Set();
588
+ for (const packageExport of packageExports) {
589
+ const record = ensureObject(packageExport);
590
+ const subpath = String(record.subpath || ".").trim() || ".";
591
+ const condition = String(record.condition || "default").trim() || "default";
592
+ const target = String(record.target || "").trim();
593
+ const targetType = String(record.targetType || "").trim();
594
+ const conditionSuffix = condition !== "default" ? ` ${color.installed(`[${condition}]`)}` : "";
595
+ const status = targetType === "file"
596
+ ? record.targetExists === true
597
+ ? color.installed("[ok]")
598
+ : color.provider("[missing]")
599
+ : targetType === "pattern"
600
+ ? color.dim("[pattern]")
601
+ : color.dim("[external]");
602
+ const showTarget = shouldShowPackageExportTarget({ subpath, target, targetType });
603
+ const targetSuffix = showTarget ? ` -> ${color.item(target)}` : "";
604
+ const subpathLabel = options.details ? color.white(subpath) : color.item(subpath);
605
+ stdout.write(`- ${subpathLabel}${conditionSuffix}${targetSuffix} ${status}\n`);
606
+
607
+ if (!options.details) {
608
+ continue;
609
+ }
610
+ if (targetType !== "file" || !target.startsWith("./")) {
611
+ continue;
612
+ }
613
+
614
+ const normalizedTarget = normalizeRelativePosixPath(target.slice(2));
615
+ const summary = ensureObject(exportedSymbolsByFile.get(normalizedTarget));
616
+ if (!summary || Object.keys(summary).length < 1) {
617
+ continue;
618
+ }
619
+
620
+ const detailKey = `${subpath}::${normalizedTarget}`;
621
+ if (symbolDetailsShown.has(detailKey)) {
622
+ continue;
623
+ }
624
+ symbolDetailsShown.add(detailKey);
625
+
626
+ const symbols = ensureArray(summary.symbols).map((value) => String(value)).filter(Boolean);
627
+ const classifiedSymbols = classifyExportedSymbols(symbols);
628
+ const writeClassifiedSymbols = (label, entries) => {
629
+ const items = ensureArray(entries).map((entry) => String(entry || "").trim()).filter(Boolean);
630
+ if (items.length < 1) {
631
+ return;
632
+ }
633
+ writeWrappedItems({
634
+ stdout,
635
+ heading: ` ${color.installed(`${label} (${items.length}):`)}`,
636
+ lineIndent: " ",
637
+ wrapWidth,
638
+ items: items.map((symbol) => ({
639
+ text: symbol,
640
+ rendered: color.item(symbol)
641
+ }))
642
+ });
643
+ };
644
+ writeClassifiedSymbols("providers", classifiedSymbols.providers);
645
+ writeClassifiedSymbols("functions/helpers", classifiedSymbols.functions);
646
+ writeClassifiedSymbols("constants", classifiedSymbols.constants);
647
+ writeClassifiedSymbols("classes/types", classifiedSymbols.classesOrTypes);
648
+ writeClassifiedSymbols("internal/test hooks", classifiedSymbols.internals);
649
+ writeClassifiedSymbols("other symbols", classifiedSymbols.others);
650
+
651
+ if (summary.hasDefaultExport === true) {
652
+ stdout.write(` ${color.installed("default export: yes")}\n`);
653
+ }
654
+ const starReExports = ensureArray(summary.starReExports).map((value) => String(value)).filter(Boolean);
655
+ const namedReExports = ensureArray(summary.namedReExports).map((value) => String(value)).filter(Boolean);
656
+ const reExportSummary = [];
657
+ if (namedReExports.length > 0) {
658
+ reExportSummary.push(`named from ${namedReExports.length} files`);
659
+ }
660
+ if (starReExports.length > 0) {
661
+ reExportSummary.push(`star from ${starReExports.length} files`);
662
+ }
663
+ if (options.debugExports && reExportSummary.length > 0) {
664
+ stdout.write(` ${color.dim(`re-export sources: ${reExportSummary.join(", ")}`)}\n`);
665
+ }
666
+
667
+ if (options.debugExports && starReExports.length > 0) {
668
+ writeWrappedItems({
669
+ stdout,
670
+ heading: ` ${color.installed(`star re-exports (${starReExports.length}):`)}`,
671
+ lineIndent: " ",
672
+ wrapWidth,
673
+ items: starReExports.map((specifier) => ({
674
+ text: specifier,
675
+ rendered: color.item(specifier)
676
+ }))
677
+ });
678
+ }
679
+ if (options.debugExports && namedReExports.length > 0) {
680
+ writeWrappedItems({
681
+ stdout,
682
+ heading: ` ${color.installed(`named re-exports (${namedReExports.length}):`)}`,
683
+ lineIndent: " ",
684
+ wrapWidth,
685
+ items: namedReExports.map((specifier) => ({
686
+ text: specifier,
687
+ rendered: color.item(specifier)
688
+ }))
689
+ });
690
+ }
691
+ }
692
+ }
693
+ } else {
694
+ stdout.write(`${color.heading("Code introspection:")}\n`);
695
+ stdout.write(`- ${color.dim("Source files unavailable (descriptor metadata only).")}\n`);
696
+ }
697
+ if (payload.dependsOn.length > 0) {
698
+ writeWrappedItems({
699
+ stdout,
700
+ heading: `${color.heading("Depends on")} ${color.installed(`(${payload.dependsOn.length})`)}:`,
701
+ wrapWidth,
702
+ items: payload.dependsOn.map((dependencyId) => {
703
+ const text = String(dependencyId);
704
+ return {
705
+ text,
706
+ rendered: color.item(text)
707
+ };
708
+ })
709
+ });
710
+ }
711
+ if (runtimeMutationEntries.length > 0) {
712
+ writeWrappedItems({
713
+ stdout,
714
+ heading: color.heading(`Dependency mutations runtime (${runtimeMutationEntries.length}):`),
715
+ wrapWidth,
716
+ items: runtimeMutationEntries.map(([dependencyId, versionSpec]) => {
717
+ const dependencyText = String(dependencyId);
718
+ const versionText = String(versionSpec);
719
+ return {
720
+ text: `${dependencyText} ${versionText}`,
721
+ rendered: `${color.item(dependencyText)} ${color.installed(versionText)}`
722
+ };
723
+ })
724
+ });
725
+ }
726
+
727
+ if (provides.length > 0 || requires.length > 0) {
728
+ stdout.write(`${color.heading("Capabilities:")}\n`);
729
+ if (provides.length > 0) {
730
+ const providesText = provides.map((capabilityId) => color.item(capabilityId)).join(" ");
731
+ stdout.write(`${color.installed("Provides:")} ${providesText}\n`);
732
+ }
733
+ if (requires.length > 0) {
734
+ const requiresText = requires.map((capabilityId) => color.item(capabilityId)).join(" ");
735
+ stdout.write(`${color.installed("Requires:")} ${requiresText}\n`);
736
+ }
737
+ }
738
+ if (capabilityDetails && (capabilityDetails.provides.length > 0 || capabilityDetails.requires.length > 0)) {
739
+ const formatPackageSummary = (detail) => {
740
+ const packageId = String(detail?.packageId || "").trim();
741
+ const version = String(detail?.version || "").trim();
742
+ const descriptorPath = String(detail?.descriptorPath || "").trim();
743
+ const versionSuffix = version ? `@${version}` : "";
744
+ const pathSuffix = descriptorPath ? ` [${descriptorPath}]` : "";
745
+ return `${packageId}${versionSuffix}${pathSuffix}`;
746
+ };
747
+
748
+ const writeCapabilityRecord = ({ heading, records, includeDependsOnProviders = false }) => {
749
+ if (records.length < 1) {
750
+ return;
751
+ }
752
+ stdout.write(`${color.heading(heading)}\n`);
753
+ for (const record of records) {
754
+ const capabilityId = String(record.capabilityId || "").trim();
755
+ stdout.write(`- ${color.item(capabilityId)}\n`);
756
+
757
+ const providerItems = ensureArray(record.providerDetails).map((detail) => ({
758
+ text: formatPackageSummary(detail),
759
+ rendered: color.item(formatPackageSummary(detail))
760
+ }));
761
+ if (providerItems.length > 0) {
762
+ writeWrappedItems({
763
+ stdout,
764
+ heading: ` ${color.installed(`providers (${providerItems.length}):`)}`,
765
+ lineIndent: " ",
766
+ wrapWidth,
767
+ items: providerItems
768
+ });
769
+ }
770
+
771
+ if (includeDependsOnProviders) {
772
+ const providersInDependsOn = ensureArray(record.providersInDependsOn).map((packageId) => ({
773
+ text: String(packageId),
774
+ rendered: color.item(String(packageId))
775
+ }));
776
+ if (providersInDependsOn.length > 0) {
777
+ writeWrappedItems({
778
+ stdout,
779
+ heading: ` ${color.installed(`providers in dependsOn (${providersInDependsOn.length}):`)}`,
780
+ lineIndent: " ",
781
+ wrapWidth,
782
+ items: providersInDependsOn
783
+ });
784
+ }
785
+ }
786
+
787
+ const requirerItems = ensureArray(record.requirerDetails).map((detail) => ({
788
+ text: formatPackageSummary(detail),
789
+ rendered: color.item(formatPackageSummary(detail))
790
+ }));
791
+ if (requirerItems.length > 0) {
792
+ writeWrappedItems({
793
+ stdout,
794
+ heading: ` ${color.installed(`required by (${requirerItems.length}):`)}`,
795
+ lineIndent: " ",
796
+ wrapWidth,
797
+ items: requirerItems
798
+ });
799
+ }
800
+ }
801
+ };
802
+
803
+ stdout.write(`${color.heading("Capability details:")}\n`);
804
+ writeCapabilityRecord({
805
+ heading: `Provides detail (${capabilityDetails.provides.length}):`,
806
+ records: capabilityDetails.provides,
807
+ includeDependsOnProviders: false
808
+ });
809
+ writeCapabilityRecord({
810
+ heading: `Requires detail (${capabilityDetails.requires.length}):`,
811
+ records: capabilityDetails.requires,
812
+ includeDependsOnProviders: true
813
+ });
814
+ }
815
+
816
+ const uiRoutes = ensureArray(ensureObject(payload.metadata.ui).routes);
817
+ if (uiRoutes.length > 0) {
818
+ stdout.write(`${color.heading(`UI routes (${uiRoutes.length}):`)}\n`);
819
+ for (const route of uiRoutes) {
820
+ const record = ensureObject(route);
821
+ const routePath = String(record.path || "").trim();
822
+ const scope = String(record.scope || "").trim();
823
+ const routeId = String(record.id || record.name || "").trim();
824
+ const purpose = String(record.purpose || "").trim();
825
+ const modeLabel = record.autoRegister === false ? "advisory" : "auto";
826
+ const scopeLabel = scope ? ` (${scope})` : "";
827
+ const modePart = ` ${color.installed(`[${modeLabel}]`)}`;
828
+ const purposePart = purpose ? ` ${purpose}` : "";
829
+ const idPart = routeId ? ` ${color.installed(`(id:${routeId})`)}` : "";
830
+ stdout.write(`- ${color.item(routePath)}${color.installed(scopeLabel)}${modePart}${purposePart}${idPart}\n`);
831
+ }
832
+ }
833
+
834
+ const serverRoutes = ensureArray(ensureObject(payload.metadata.server).routes);
835
+ if (serverRoutes.length > 0) {
836
+ stdout.write(`${color.heading(`Server routes (${serverRoutes.length}):`)}\n`);
837
+ for (const route of serverRoutes) {
838
+ const record = ensureObject(route);
839
+ const method = String(record.method || "").trim().toUpperCase();
840
+ const routePath = String(record.path || "").trim();
841
+ const summary = String(record.summary || "").trim();
842
+ const routeLabel = `${method} ${routePath}`.trim();
843
+ const summarySuffix = summary ? `: ${summary}` : "";
844
+ stdout.write(`- ${color.item(routeLabel)}${summarySuffix}\n`);
845
+ }
846
+ }
847
+
848
+ const optionNames = Object.keys(payload.options);
849
+ if (optionNames.length > 0) {
850
+ stdout.write(`${color.heading(`Options (${optionNames.length}):`)}\n`);
851
+ for (const optionName of optionNames) {
852
+ const schema = ensureObject(payload.options[optionName]);
853
+ const required = schema.required ? "required" : "optional";
854
+ const defaultSuffix = schema.defaultValue ? ` (default: ${schema.defaultValue})` : "";
855
+ stdout.write(`- ${color.item(optionName)} ${color.installed(`[${required}]`)}${color.dim(defaultSuffix)}\n`);
856
+ }
857
+ }
858
+
859
+ if (devMutationEntries.length > 0) {
860
+ writeWrappedItems({
861
+ stdout,
862
+ heading: color.heading(`Dependency mutations dev (${devMutationEntries.length}):`),
863
+ wrapWidth,
864
+ items: devMutationEntries.map(([dependencyId, versionSpec]) => {
865
+ const dependencyText = String(dependencyId);
866
+ const versionText = String(versionSpec);
867
+ return {
868
+ text: `${dependencyText} ${versionText}`,
869
+ rendered: `${color.item(dependencyText)} ${color.installed(versionText)}`
870
+ };
871
+ })
872
+ });
873
+ }
874
+ if (scriptMutationEntries.length > 0) {
875
+ stdout.write(`${color.heading(`Script mutations (${scriptMutationEntries.length}):`)}\n`);
876
+ for (const [scriptName, scriptValue] of scriptMutationEntries) {
877
+ stdout.write(`- ${color.item(scriptName)}: ${String(scriptValue)}\n`);
878
+ }
879
+ }
880
+ if (textMutations.length > 0) {
881
+ stdout.write(`${color.heading(`Text mutations (${textMutations.length}):`)}\n`);
882
+ for (const mutation of textMutations) {
883
+ const record = ensureObject(mutation);
884
+ const op = String(record.op || "").trim();
885
+ const file = String(record.file || "").trim();
886
+ const key = String(record.key || "").trim();
887
+ const position = String(record.position || "").trim();
888
+ const reason = String(record.reason || "").trim();
889
+ const reasonSuffix = reason ? `: ${reason}` : "";
890
+ let mutationLabel = `${op} ${file} ${key}`.trim();
891
+ if (op === "append-text") {
892
+ mutationLabel = `${op} ${file}`;
893
+ if (position) {
894
+ mutationLabel = `${mutationLabel} [${position}]`;
895
+ }
896
+ }
897
+ stdout.write(`- ${color.item(mutationLabel)}${reasonSuffix}\n`);
898
+ }
899
+ }
900
+
901
+ if (payload.fileWritePlan.fileCount > 0) {
902
+ stdout.write(`${color.heading(`File writes (${payload.fileWritePlan.fileCount}):`)}\n`);
903
+ for (const group of payload.fileWritePlan.groups) {
904
+ const groupId = String(group.id || "").trim();
905
+ const category = String(group.category || "").trim();
906
+ const reason = String(group.reason || "").trim();
907
+ const files = ensureArray(group.files);
908
+ let marker = "";
909
+ if (groupId) {
910
+ marker = `id:${groupId}`;
911
+ } else if (category) {
912
+ marker = `category:${category}`;
913
+ }
914
+ const markerSuffix = marker ? ` (${marker})` : "";
915
+ for (const file of files) {
916
+ const targetPath = String(ensureObject(file).to || "").trim();
917
+ if (!targetPath) {
918
+ continue;
919
+ }
920
+ stdout.write(`- ${color.item(targetPath)}${color.installed(markerSuffix)}:\n`);
921
+ if (reason) {
922
+ stdout.write(` ${reason}\n`);
923
+ }
924
+ }
925
+ }
926
+ }
927
+
928
+ const serverProviders = ensureArray(ensureObject(payload.runtime.server).providers);
929
+ const clientProviders = ensureArray(ensureObject(payload.runtime.client).providers);
930
+ writeRuntimeProviders("server", serverProviders);
931
+ writeRuntimeProviders("client", clientProviders);
932
+ if (introspectionNotes.length > 0) {
933
+ stdout.write(`${color.heading(`Introspection notes (${introspectionNotes.length}):`)}\n`);
934
+ for (const note of introspectionNotes) {
935
+ stdout.write(`- ${color.dim(note)}\n`);
936
+ }
937
+ }
938
+ }
939
+ return 0;
940
+ }
941
+
942
+ if (bundleRegistry.has(id)) {
943
+ const bundle = bundleRegistry.get(id);
944
+ const payload = {
945
+ kind: "bundle",
946
+ bundleId: bundle.bundleId,
947
+ version: bundle.version,
948
+ description: String(bundle.description || ""),
949
+ provider: Number(bundle.provider) === 1,
950
+ curated: Number(bundle.curated) === 1,
951
+ packages: ensureArray(bundle.packages).map((value) => String(value))
952
+ };
953
+ if (options.json) {
954
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
955
+ } else {
956
+ stdout.write(`${color.heading("Information")}\n`);
957
+ writeField("Bundle", payload.bundleId, color.item);
958
+ writeField("Version", payload.version, color.installed);
959
+ if (payload.description) {
960
+ writeField("Description", payload.description);
961
+ }
962
+ stdout.write(`${color.heading(`Packages (${payload.packages.length}):`)}\n`);
963
+ for (const packageId of payload.packages) {
964
+ stdout.write(`- ${color.item(packageId)}\n`);
965
+ }
966
+ }
967
+ return 0;
968
+ }
969
+
970
+ throw createCliError(`Unknown package or bundle: ${id}`);
971
+ }
972
+
973
+ async function commandCreate({ positional, options, cwd, io }) {
974
+ const targetType = String(positional[0] || "").trim();
975
+ const rawName = String(positional[1] || "").trim();
976
+ if (targetType !== "package" || !rawName) {
977
+ throw createCliError("create requires: create package <name>", { showUsage: true });
978
+ }
979
+
980
+ const appRoot = await resolveAppRootFromCwd(cwd);
981
+ const { packageJsonPath, packageJson } = await loadAppPackageJson(appRoot);
982
+ const { lockPath, lock } = await loadLockFile(appRoot);
983
+ const installedPackages = ensureObject(lock.installedPackages);
984
+ const dependencies = ensureObject(packageJson.dependencies);
985
+ const devDependencies = ensureObject(packageJson.devDependencies);
986
+
987
+ const { packageId, packageDirName } = resolveLocalPackageId({
988
+ rawName,
989
+ appPackageName: packageJson.name,
990
+ inlineOptions: options.inlineOptions
991
+ });
992
+ const localPackagesRoot = path.join(appRoot, "packages");
993
+ const packageRoot = path.join(localPackagesRoot, packageDirName);
994
+ const packageRelativePath = normalizeRelativePath(appRoot, packageRoot);
995
+ const descriptorRelativePath = `${normalizeRelativePosixPath(packageRelativePath)}/package.descriptor.mjs`;
996
+ const localDependencySpecifier = toFileDependencySpecifier(packageRelativePath);
997
+ const packageDescription = String(options.inlineOptions.description || "").trim() || `App-local package ${packageId}.`;
998
+
999
+ if (await fileExists(packageRoot)) {
1000
+ throw createCliError(`Package directory already exists: ${normalizeRelativePath(appRoot, packageRoot)}`);
1001
+ }
1002
+ if (Object.prototype.hasOwnProperty.call(installedPackages, packageId)) {
1003
+ throw createCliError(`Package is already present in lock file: ${packageId}`);
1004
+ }
1005
+ if (Object.prototype.hasOwnProperty.call(dependencies, packageId)) {
1006
+ throw createCliError(`package.json dependencies already contains ${packageId}.`);
1007
+ }
1008
+ if (Object.prototype.hasOwnProperty.call(devDependencies, packageId)) {
1009
+ throw createCliError(`package.json devDependencies already contains ${packageId}.`);
1010
+ }
1011
+
1012
+ const scaffoldFiles = createLocalPackageScaffoldFiles({
1013
+ packageId,
1014
+ packageDescription
1015
+ });
1016
+ const touchedFiles = new Set(["package.json", normalizeRelativePath(appRoot, lockPath)]);
1017
+ for (const scaffoldFile of scaffoldFiles) {
1018
+ touchedFiles.add(`${normalizeRelativePosixPath(packageRelativePath)}/${normalizeRelativePosixPath(scaffoldFile.relativePath)}`);
1019
+ }
1020
+
1021
+ if (!options.dryRun) {
1022
+ for (const scaffoldFile of scaffoldFiles) {
1023
+ const absoluteFilePath = path.join(packageRoot, scaffoldFile.relativePath);
1024
+ await mkdir(path.dirname(absoluteFilePath), { recursive: true });
1025
+ await writeFile(absoluteFilePath, String(scaffoldFile.content || ""), "utf8");
1026
+ }
1027
+ }
1028
+
1029
+ const dependencyApplied = applyPackageJsonField(packageJson, "dependencies", packageId, localDependencySpecifier);
1030
+ const managedRecord = {
1031
+ packageId,
1032
+ version: "0.1.0",
1033
+ source: {
1034
+ type: "local-package",
1035
+ packagePath: normalizeRelativePosixPath(packageRelativePath),
1036
+ descriptorPath: descriptorRelativePath
1037
+ },
1038
+ managed: {
1039
+ packageJson: {
1040
+ dependencies: {},
1041
+ devDependencies: {},
1042
+ scripts: {}
1043
+ },
1044
+ text: {},
1045
+ vite: {},
1046
+ files: [],
1047
+ migrations: []
1048
+ },
1049
+ options: {},
1050
+ installedAt: new Date().toISOString()
1051
+ };
1052
+ if (dependencyApplied.changed) {
1053
+ managedRecord.managed.packageJson.dependencies[packageId] = dependencyApplied.managed;
1054
+ }
1055
+ lock.installedPackages[packageId] = managedRecord;
1056
+
1057
+ const touchedFileList = sortStrings([...touchedFiles]);
1058
+ if (!options.dryRun) {
1059
+ await writeJsonFile(packageJsonPath, packageJson);
1060
+ await writeJsonFile(lockPath, lock);
1061
+ if (!options.noInstall) {
1062
+ await runNpmInstall(appRoot, io.stderr);
1063
+ }
1064
+ }
1065
+
1066
+ if (options.json) {
1067
+ io.stdout.write(
1068
+ `${JSON.stringify(
1069
+ {
1070
+ targetType: "package",
1071
+ packageId,
1072
+ packageDirectory: normalizeRelativePosixPath(packageRelativePath),
1073
+ descriptorPath: descriptorRelativePath,
1074
+ dependency: localDependencySpecifier,
1075
+ touchedFiles: touchedFileList,
1076
+ lockPath: normalizeRelativePath(appRoot, lockPath),
1077
+ dryRun: options.dryRun
1078
+ },
1079
+ null,
1080
+ 2
1081
+ )}\n`
1082
+ );
1083
+ } else {
1084
+ io.stdout.write(`Created local package ${packageId}.\n`);
1085
+ io.stdout.write(`Directory: ${normalizeRelativePosixPath(packageRelativePath)}\n`);
1086
+ io.stdout.write(`Dependency: ${packageId} -> ${localDependencySpecifier}\n`);
1087
+ io.stdout.write(`Descriptor: ${descriptorRelativePath}\n`);
1088
+ io.stdout.write(`Touched files (${touchedFileList.length}):\n`);
1089
+ for (const touchedFile of touchedFileList) {
1090
+ io.stdout.write(`- ${touchedFile}\n`);
1091
+ }
1092
+ io.stdout.write(`Lock file: ${normalizeRelativePath(appRoot, lockPath)}\n`);
1093
+ if (options.dryRun) {
1094
+ io.stdout.write("Dry run enabled: no files were written.\n");
1095
+ }
1096
+ }
1097
+
1098
+ return 0;
1099
+ }
1100
+
1101
+ async function commandAdd({ positional, options, cwd, io }) {
1102
+ const targetType = String(positional[0] || "").trim();
1103
+ const targetId = String(positional[1] || "").trim();
1104
+
1105
+ if (!targetType || !targetId) {
1106
+ throw createCliError("add requires target type and id (add bundle <id> | add package <id>).", {
1107
+ showUsage: true
1108
+ });
1109
+ }
1110
+ if (targetType !== "bundle" && targetType !== "package") {
1111
+ throw createCliError(`Unsupported add target type: ${targetType}`, { showUsage: true });
1112
+ }
1113
+
1114
+ const appRoot = await resolveAppRootFromCwd(cwd);
1115
+ const packageRegistry = await loadPackageRegistry();
1116
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
1117
+ const bundleRegistry = await loadBundleRegistry();
1118
+ const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
1119
+ const { packageJsonPath, packageJson } = await loadAppPackageJson(appRoot);
1120
+ const { lockPath, lock } = await loadLockFile(appRoot);
1121
+ let resolvedTargetPackageId = targetType === "package" ? resolvePackageIdInput(targetId, combinedPackageRegistry) : "";
1122
+ if (targetType === "package" && !resolvedTargetPackageId) {
1123
+ const installedNodeModuleEntry = await resolveInstalledNodeModulePackageEntry({
1124
+ appRoot,
1125
+ packageId: targetId
1126
+ });
1127
+ if (installedNodeModuleEntry) {
1128
+ combinedPackageRegistry.set(installedNodeModuleEntry.packageId, installedNodeModuleEntry);
1129
+ resolvedTargetPackageId = installedNodeModuleEntry.packageId;
1130
+ }
1131
+ }
1132
+
1133
+ const targetPackageIds = targetType === "bundle"
1134
+ ? ensureArray(bundleRegistry.get(targetId)?.packages).map((value) => String(value))
1135
+ : [resolvedTargetPackageId];
1136
+ if (targetType === "bundle" && targetPackageIds.length === 0) {
1137
+ throw createCliError(`Unknown bundle: ${targetId}`);
1138
+ }
1139
+ if (targetType === "package" && !resolvedTargetPackageId) {
1140
+ throw createCliError(
1141
+ `Unknown package: ${targetId}. Install an external module first (npm install ${targetId}) if you want to adopt it into lock.`
1142
+ );
1143
+ }
1144
+
1145
+ await hydratePackageRegistryFromInstalledNodeModules({
1146
+ appRoot,
1147
+ packageRegistry: combinedPackageRegistry,
1148
+ seedPackageIds: targetPackageIds
1149
+ });
1150
+
1151
+ if (targetType === "package") {
1152
+ const targetPackageEntry = combinedPackageRegistry.get(resolvedTargetPackageId);
1153
+ if (!targetPackageEntry) {
1154
+ throw createCliError(`Unknown package: ${targetId}`);
1155
+ }
1156
+ validateInlineOptionsForPackage(targetPackageEntry, options.inlineOptions);
1157
+ }
1158
+
1159
+ const { ordered: resolvedPackageIds, externalDependencies } = resolveLocalDependencyOrder(
1160
+ targetPackageIds,
1161
+ combinedPackageRegistry
1162
+ );
1163
+ const plannedInstalledPackageIds = sortStrings([
1164
+ ...new Set([
1165
+ ...Object.keys(ensureObject(lock.installedPackages)).map((value) => String(value || "").trim()).filter(Boolean),
1166
+ ...resolvedPackageIds
1167
+ ])
1168
+ ]);
1169
+ validatePlannedCapabilityClosure(
1170
+ plannedInstalledPackageIds,
1171
+ combinedPackageRegistry,
1172
+ `add ${targetType} ${targetId}`
1173
+ );
1174
+
1175
+ if (targetType === "bundle") {
1176
+ validateInlineOptionsForBundle({
1177
+ bundleId: targetId,
1178
+ inlineOptions: options.inlineOptions,
1179
+ packageIds: resolvedPackageIds,
1180
+ packageRegistry: combinedPackageRegistry
1181
+ });
1182
+ }
1183
+
1184
+ const packagesToInstall = [];
1185
+ const resolvedOptionsByPackage = {};
1186
+ const forceReapplyTarget = options?.forceReapplyTarget === true;
1187
+ const hasInlineOptions = Object.keys(ensureObject(options.inlineOptions)).length > 0;
1188
+ for (const packageId of resolvedPackageIds) {
1189
+ const packageEntry = combinedPackageRegistry.get(packageId);
1190
+ const existingInstall = ensureObject(lock.installedPackages[packageId]);
1191
+ const existingVersion = String(existingInstall.version || "").trim();
1192
+ const isDirectTargetPackage = targetType === "package" && packageId === resolvedTargetPackageId;
1193
+ const packageInlineOptions = targetType === "bundle"
1194
+ ? resolveBundleInlineOptionsForPackage(packageEntry, options.inlineOptions)
1195
+ : isDirectTargetPackage
1196
+ ? ensureObject(options.inlineOptions)
1197
+ : {};
1198
+ const hasPackageInlineOptions = Object.keys(packageInlineOptions).length > 0;
1199
+ const shouldReapplyInstalledPackage =
1200
+ (isDirectTargetPackage && (forceReapplyTarget || hasInlineOptions)) ||
1201
+ (targetType === "bundle" && hasPackageInlineOptions);
1202
+ if (existingVersion && existingVersion === packageEntry.version && !shouldReapplyInstalledPackage) {
1203
+ continue;
1204
+ }
1205
+ packagesToInstall.push(packageId);
1206
+ const lockEntryOptions = ensureObject(existingInstall.options);
1207
+ resolvedOptionsByPackage[packageId] = await resolvePackageOptions(
1208
+ packageEntry,
1209
+ {
1210
+ ...lockEntryOptions,
1211
+ ...packageInlineOptions
1212
+ },
1213
+ io,
1214
+ { appRoot }
1215
+ );
1216
+ }
1217
+
1218
+ const touchedFiles = new Set();
1219
+ const installedPackageRecords = [];
1220
+
1221
+ for (const packageId of packagesToInstall) {
1222
+ const packageEntry = combinedPackageRegistry.get(packageId);
1223
+ const managedRecord = await applyPackageInstall({
1224
+ packageEntry,
1225
+ packageOptions: resolvedOptionsByPackage[packageId],
1226
+ appRoot,
1227
+ appPackageJson: packageJson,
1228
+ lock,
1229
+ packageRegistry: combinedPackageRegistry,
1230
+ touchedFiles
1231
+ });
1232
+ installedPackageRecords.push(managedRecord);
1233
+ }
1234
+
1235
+ const {
1236
+ appLocalRegistry: refreshedAppLocalRegistry,
1237
+ adoptedPackageIds
1238
+ } = await adoptAppLocalPackageDependencies({
1239
+ appRoot,
1240
+ appPackageJson: packageJson,
1241
+ lock
1242
+ });
1243
+ for (const [packageId, packageEntry] of refreshedAppLocalRegistry.entries()) {
1244
+ combinedPackageRegistry.set(packageId, packageEntry);
1245
+ }
1246
+ if (adoptedPackageIds.length > 0) {
1247
+ const postInstallPackageIds = sortStrings(Object.keys(ensureObject(lock.installedPackages)));
1248
+ validatePlannedCapabilityClosure(
1249
+ postInstallPackageIds,
1250
+ combinedPackageRegistry,
1251
+ `add ${targetType} ${targetId}`
1252
+ );
1253
+ }
1254
+
1255
+ const finalResolvedPackageIds = sortStrings([...resolvedPackageIds, ...adoptedPackageIds]);
1256
+
1257
+ const touchedFileList = sortStrings([...touchedFiles]);
1258
+ const successLabel = targetType === "bundle" ? "Added bundle" : "Added package";
1259
+ const installWarnings = installedPackageRecords
1260
+ .flatMap((record) => ensureArray(ensureObject(record).warnings))
1261
+ .map((value) => String(value || "").trim())
1262
+ .filter(Boolean);
1263
+
1264
+ if (!options.dryRun) {
1265
+ await writeJsonFile(packageJsonPath, packageJson);
1266
+ await writeJsonFile(lockPath, lock);
1267
+ if (!options.noInstall) {
1268
+ await runNpmInstall(appRoot, io.stderr);
1269
+ }
1270
+ }
1271
+
1272
+ if (options.json) {
1273
+ io.stdout.write(`${JSON.stringify({
1274
+ targetType,
1275
+ targetId,
1276
+ resolvedPackages: finalResolvedPackageIds,
1277
+ touchedFiles: touchedFileList,
1278
+ lockPath: normalizeRelativePath(appRoot, lockPath),
1279
+ externalDependencies,
1280
+ dryRun: options.dryRun,
1281
+ installed: installedPackageRecords,
1282
+ warnings: installWarnings
1283
+ }, null, 2)}\n`);
1284
+ } else {
1285
+ io.stdout.write(
1286
+ `${renderResolvedSummary(
1287
+ `${successLabel}`,
1288
+ targetId,
1289
+ finalResolvedPackageIds,
1290
+ touchedFileList,
1291
+ appRoot,
1292
+ lockPath,
1293
+ externalDependencies
1294
+ )}\n`
1295
+ );
1296
+ if (installWarnings.length > 0) {
1297
+ io.stdout.write(`Warnings (${installWarnings.length}):\n`);
1298
+ for (const warning of installWarnings) {
1299
+ io.stdout.write(`- ${warning}\n`);
1300
+ }
1301
+ }
1302
+ if (options.dryRun) {
1303
+ io.stdout.write("Dry run enabled: no files were written.\n");
1304
+ }
1305
+ }
1306
+
1307
+ return 0;
1308
+ }
1309
+
1310
+ async function commandUpdate({ positional, options, cwd, io }) {
1311
+ const targetType = String(positional[0] || "").trim();
1312
+ const targetId = String(positional[1] || "").trim();
1313
+ if (targetType !== "package" || !targetId) {
1314
+ throw createCliError("update requires: update package <packageId>", { showUsage: true });
1315
+ }
1316
+
1317
+ const appRoot = await resolveAppRootFromCwd(cwd);
1318
+ const { lock } = await loadLockFile(appRoot);
1319
+ const installedPackages = ensureObject(lock.installedPackages);
1320
+ const resolvedTargetId = resolveInstalledPackageIdInput(targetId, installedPackages);
1321
+ if (!resolvedTargetId) {
1322
+ throw createCliError(`Package is not installed: ${targetId}`);
1323
+ }
1324
+
1325
+ return commandAdd({
1326
+ positional: ["package", resolvedTargetId],
1327
+ options: {
1328
+ ...options,
1329
+ forceReapplyTarget: true
1330
+ },
1331
+ cwd,
1332
+ io
1333
+ });
1334
+ }
1335
+
1336
+ async function commandPosition({ positional, options, cwd, io }) {
1337
+ const targetType = String(positional[0] || "").trim();
1338
+ const targetId = String(positional[1] || "").trim();
1339
+ if (targetType !== "element" || !targetId) {
1340
+ throw createCliError("position requires: position element <packageId>", { showUsage: true });
1341
+ }
1342
+
1343
+ const appRoot = await resolveAppRootFromCwd(cwd);
1344
+ const packageRegistry = await loadPackageRegistry();
1345
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
1346
+ const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
1347
+ const { lockPath, lock } = await loadLockFile(appRoot);
1348
+ const installedPackages = ensureObject(lock.installedPackages);
1349
+ const resolvedTargetId = resolveInstalledPackageIdInput(targetId, installedPackages);
1350
+ if (!resolvedTargetId) {
1351
+ throw createCliError(`Element is not installed: ${targetId}`);
1352
+ }
1353
+
1354
+ await hydratePackageRegistryFromInstalledNodeModules({
1355
+ appRoot,
1356
+ packageRegistry: combinedPackageRegistry,
1357
+ seedPackageIds: [resolvedTargetId]
1358
+ });
1359
+ const packageEntry = combinedPackageRegistry.get(resolvedTargetId);
1360
+ if (!packageEntry) {
1361
+ throw createCliError(
1362
+ `Installed element descriptor not found: ${resolvedTargetId}. Ensure it exists in catalog, app packages/, or node_modules.`
1363
+ );
1364
+ }
1365
+ validateInlineOptionsForPackage(packageEntry, options.inlineOptions);
1366
+
1367
+ const installedRecord = ensureObject(installedPackages[resolvedTargetId]);
1368
+ const resolvedOptions = await resolvePackageOptions(
1369
+ packageEntry,
1370
+ {
1371
+ ...ensureObject(installedRecord.options),
1372
+ ...ensureObject(options.inlineOptions)
1373
+ },
1374
+ io,
1375
+ { appRoot }
1376
+ );
1377
+
1378
+ const touchedFiles = new Set();
1379
+ const positionedRecord = await applyPackagePositioning({
1380
+ packageEntry,
1381
+ packageOptions: resolvedOptions,
1382
+ appRoot,
1383
+ lock,
1384
+ touchedFiles
1385
+ });
1386
+ const touchedFileList = sortStrings([...touchedFiles]);
1387
+
1388
+ if (!options.dryRun) {
1389
+ await writeJsonFile(lockPath, lock);
1390
+ }
1391
+
1392
+ if (options.json) {
1393
+ io.stdout.write(`${JSON.stringify({
1394
+ targetType: "element",
1395
+ elementId: resolvedTargetId,
1396
+ packageId: resolvedTargetId,
1397
+ touchedFiles: touchedFileList,
1398
+ lockPath: normalizeRelativePath(appRoot, lockPath),
1399
+ dryRun: options.dryRun,
1400
+ positioned: positionedRecord
1401
+ }, null, 2)}\n`);
1402
+ } else {
1403
+ io.stdout.write(`Positioned element ${resolvedTargetId}.\n`);
1404
+ io.stdout.write(`Touched files (${touchedFileList.length}):\n`);
1405
+ for (const touchedFile of touchedFileList) {
1406
+ io.stdout.write(`- ${touchedFile}\n`);
1407
+ }
1408
+ io.stdout.write(`Lock file: ${normalizeRelativePath(appRoot, lockPath)}\n`);
1409
+ if (options.dryRun) {
1410
+ io.stdout.write("Dry run enabled: no files were written.\n");
1411
+ }
1412
+ }
1413
+
1414
+ return 0;
1415
+ }
1416
+
1417
+ async function commandRemove({ positional, options, cwd, io }) {
1418
+ const targetType = String(positional[0] || "").trim();
1419
+ const targetId = String(positional[1] || "").trim();
1420
+ if (targetType !== "package" || !targetId) {
1421
+ throw createCliError("remove requires: remove package <packageId>", { showUsage: true });
1422
+ }
1423
+
1424
+ const appRoot = await resolveAppRootFromCwd(cwd);
1425
+ const packageRegistry = await loadPackageRegistry();
1426
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
1427
+ const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
1428
+ const { packageJsonPath, packageJson } = await loadAppPackageJson(appRoot);
1429
+ const { lockPath, lock } = await loadLockFile(appRoot);
1430
+ const installed = ensureObject(lock.installedPackages);
1431
+ await hydratePackageRegistryFromInstalledNodeModules({
1432
+ appRoot,
1433
+ packageRegistry: combinedPackageRegistry,
1434
+ seedPackageIds: Object.keys(installed)
1435
+ });
1436
+ const resolvedTargetId = resolveInstalledPackageIdInput(targetId, installed);
1437
+
1438
+ if (!resolvedTargetId) {
1439
+ throw createCliError(`Package is not installed: ${targetId}`);
1440
+ }
1441
+
1442
+ const dependents = getInstalledDependents(lock, resolvedTargetId, combinedPackageRegistry);
1443
+ if (dependents.length > 0) {
1444
+ throw createCliError(
1445
+ `Cannot remove ${resolvedTargetId}; installed packages depend on it: ${dependents.join(", ")}`
1446
+ );
1447
+ }
1448
+
1449
+ const lockEntry = ensureObject(installed[resolvedTargetId]);
1450
+ const managed = ensureObject(lockEntry.managed);
1451
+ const touchedFiles = new Set();
1452
+
1453
+ const managedPackageJson = ensureObject(managed.packageJson);
1454
+ for (const [dependencyId, managedChange] of Object.entries(ensureObject(managedPackageJson.dependencies))) {
1455
+ if (restorePackageJsonField(packageJson, "dependencies", dependencyId, managedChange)) {
1456
+ touchedFiles.add("package.json");
1457
+ }
1458
+ }
1459
+ for (const [dependencyId, managedChange] of Object.entries(ensureObject(managedPackageJson.devDependencies))) {
1460
+ if (restorePackageJsonField(packageJson, "devDependencies", dependencyId, managedChange)) {
1461
+ touchedFiles.add("package.json");
1462
+ }
1463
+ }
1464
+ for (const [scriptName, managedChange] of Object.entries(ensureObject(managedPackageJson.scripts))) {
1465
+ if (restorePackageJsonField(packageJson, "scripts", scriptName, managedChange)) {
1466
+ touchedFiles.add("package.json");
1467
+ }
1468
+ }
1469
+
1470
+ const managedText = ensureObject(managed.text);
1471
+ for (const change of Object.values(managedText)) {
1472
+ const changeRecord = ensureObject(change);
1473
+ if (String(changeRecord.op || "") !== "upsert-env") {
1474
+ continue;
1475
+ }
1476
+ const relativeFile = String(changeRecord.file || "").trim();
1477
+ if (!relativeFile) {
1478
+ continue;
1479
+ }
1480
+ const absoluteFile = path.join(appRoot, relativeFile);
1481
+ const existing = await readFileBufferIfExists(absoluteFile);
1482
+ if (!existing.exists) {
1483
+ continue;
1484
+ }
1485
+ const updated = removeEnvValue(
1486
+ existing.buffer.toString("utf8"),
1487
+ String(changeRecord.key || ""),
1488
+ String(changeRecord.value || ""),
1489
+ {
1490
+ hadPrevious: Boolean(changeRecord.hadPrevious),
1491
+ previousValue: String(changeRecord.previousValue || "")
1492
+ }
1493
+ );
1494
+ if (updated.changed) {
1495
+ await writeFile(absoluteFile, updated.content, "utf8");
1496
+ touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
1497
+ }
1498
+ }
1499
+
1500
+ await removeManagedViteProxyEntries({
1501
+ appRoot,
1502
+ packageId: resolvedTargetId,
1503
+ managedViteChanges: ensureObject(managed.vite),
1504
+ touchedFiles
1505
+ });
1506
+
1507
+ for (const fileChange of ensureArray(managed.files)) {
1508
+ const changeRecord = ensureObject(fileChange);
1509
+ if (changeRecord.preserveOnRemove === true) {
1510
+ continue;
1511
+ }
1512
+ const relativeFile = String(changeRecord.path || "").trim();
1513
+ if (!relativeFile) {
1514
+ continue;
1515
+ }
1516
+ const absoluteFile = path.join(appRoot, relativeFile);
1517
+ const existing = await readFileBufferIfExists(absoluteFile);
1518
+ if (!existing.exists) {
1519
+ continue;
1520
+ }
1521
+ if (hashBuffer(existing.buffer) !== String(changeRecord.hash || "")) {
1522
+ continue;
1523
+ }
1524
+
1525
+ if (changeRecord.hadPrevious) {
1526
+ const previousBuffer = Buffer.from(String(changeRecord.previousContentBase64 || ""), "base64");
1527
+ await writeFile(absoluteFile, previousBuffer);
1528
+ } else {
1529
+ await rm(absoluteFile);
1530
+ }
1531
+ touchedFiles.add(relativeFile);
1532
+ }
1533
+
1534
+ delete installed[resolvedTargetId];
1535
+ const touchedFileList = sortStrings([...touchedFiles]);
1536
+
1537
+ if (!options.dryRun) {
1538
+ await writeJsonFile(packageJsonPath, packageJson);
1539
+ await writeJsonFile(lockPath, lock);
1540
+ if (!options.noInstall) {
1541
+ await runNpmInstall(appRoot, io.stderr);
1542
+ }
1543
+ }
1544
+
1545
+ if (options.json) {
1546
+ io.stdout.write(`${JSON.stringify({
1547
+ removedPackage: resolvedTargetId,
1548
+ touchedFiles: touchedFileList,
1549
+ lockPath: normalizeRelativePath(appRoot, lockPath),
1550
+ dryRun: options.dryRun
1551
+ }, null, 2)}\n`);
1552
+ } else {
1553
+ io.stdout.write(`Removed package ${resolvedTargetId}.\n`);
1554
+ io.stdout.write(`Touched files (${touchedFileList.length}):\n`);
1555
+ for (const touchedFile of touchedFileList) {
1556
+ io.stdout.write(`- ${touchedFile}\n`);
1557
+ }
1558
+ io.stdout.write(`Lock file: ${normalizeRelativePath(appRoot, lockPath)}\n`);
1559
+ if (options.dryRun) {
1560
+ io.stdout.write("Dry run enabled: no files were written.\n");
1561
+ }
1562
+ }
1563
+
1564
+ return 0;
1565
+ }
1566
+
1567
+ async function commandDoctor({ cwd, options, stdout }) {
1568
+ const appRoot = await resolveAppRootFromCwd(cwd);
1569
+ const { lock } = await loadLockFile(appRoot);
1570
+ const packageRegistry = await loadPackageRegistry();
1571
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
1572
+ const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
1573
+ const issues = [];
1574
+ const installed = ensureObject(lock.installedPackages);
1575
+ await hydratePackageRegistryFromInstalledNodeModules({
1576
+ appRoot,
1577
+ packageRegistry: combinedPackageRegistry,
1578
+ seedPackageIds: Object.keys(installed)
1579
+ });
1580
+
1581
+ for (const [packageId, lockEntryValue] of Object.entries(installed)) {
1582
+ const lockEntry = ensureObject(lockEntryValue);
1583
+ if (!combinedPackageRegistry.has(packageId)) {
1584
+ issues.push(`Installed package not found in package registry: ${packageId}`);
1585
+ continue;
1586
+ }
1587
+
1588
+ const managed = ensureObject(lockEntry.managed);
1589
+ for (const fileChange of ensureArray(managed.files)) {
1590
+ const changeRecord = ensureObject(fileChange);
1591
+ const relativePath = String(changeRecord.path || "").trim();
1592
+ const absolutePath = path.join(appRoot, relativePath);
1593
+ if (!(await fileExists(absolutePath))) {
1594
+ issues.push(`${packageId}: managed file missing: ${relativePath}`);
1595
+ }
1596
+ }
1597
+ }
1598
+
1599
+ const payload = {
1600
+ appRoot,
1601
+ lockVersion: lock.lockVersion,
1602
+ installedPackages: sortStrings(Object.keys(installed)),
1603
+ issues
1604
+ };
1605
+
1606
+ if (options.json) {
1607
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
1608
+ } else {
1609
+ stdout.write(`App root: ${appRoot}\n`);
1610
+ stdout.write(`Installed packages: ${payload.installedPackages.length}\n`);
1611
+ if (issues.length === 0) {
1612
+ stdout.write("Doctor status: healthy\n");
1613
+ } else {
1614
+ stdout.write(`Doctor status: unhealthy (${issues.length} issue(s))\n`);
1615
+ for (const issue of issues) {
1616
+ stdout.write(`- ${issue}\n`);
1617
+ }
1618
+ }
1619
+ }
1620
+
1621
+ return issues.length === 0 ? 0 : 1;
1622
+ }
1623
+
1624
+ async function commandLintDescriptors({ options, stdout }) {
1625
+ const packageRegistry = await loadPackageRegistry();
1626
+ const bundleRegistry = await loadBundleRegistry();
1627
+ const payload = {
1628
+ packageCount: packageRegistry.size,
1629
+ bundleCount: bundleRegistry.size,
1630
+ packages: sortStrings([...packageRegistry.keys()]),
1631
+ bundles: sortStrings([...bundleRegistry.keys()])
1632
+ };
1633
+
1634
+ if (options.json) {
1635
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
1636
+ } else {
1637
+ stdout.write(`Descriptor lint passed.\n`);
1638
+ stdout.write(`Packages: ${payload.packageCount}\n`);
1639
+ stdout.write(`Bundles: ${payload.bundleCount}\n`);
1640
+ }
1641
+ return 0;
1642
+ }
1643
+
1644
+ return {
1645
+ commandList,
1646
+ commandShow,
1647
+ commandCreate,
1648
+ commandAdd,
1649
+ commandPosition,
1650
+ commandUpdate,
1651
+ commandRemove,
1652
+ commandDoctor,
1653
+ commandLintDescriptors
1654
+ };
1655
+ }
1656
+
1657
+ export { createCommandHandlers };