@jskit-ai/jskit-cli 0.2.128 → 0.2.130

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,549 @@
1
+ import {
2
+ mkdir,
3
+ rm,
4
+ writeFile
5
+ } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { createCliError } from "../../shared/cliError.js";
8
+ import { ensureObject } from "../../shared/collectionUtils.js";
9
+ import {
10
+ directoryLooksLikeJskitAppRoot,
11
+ loadLockFile
12
+ } from "../appState.js";
13
+ import {
14
+ fileExists,
15
+ hashBuffer,
16
+ readFileBufferIfExists,
17
+ writeJsonFile
18
+ } from "../ioAndMigrations.js";
19
+ import {
20
+ hydratePackageRegistryFromInstalledNodeModules,
21
+ loadAppLocalPackageRegistry,
22
+ loadPackageRegistry,
23
+ mergePackageRegistries
24
+ } from "../packageRegistries.js";
25
+ import {
26
+ CiCompositionError,
27
+ composeCiContributions
28
+ } from "./composer.js";
29
+ import {
30
+ JSKIT_CI_WORKFLOW_RELATIVE_PATH,
31
+ LEGACY_CI_WORKFLOW_RELATIVE_PATH,
32
+ LEGACY_VERIFY_WORKFLOW_HASH,
33
+ parseGithubWorkflow,
34
+ renderGithubServiceOptions,
35
+ renderGithubWorkflow
36
+ } from "./githubWorkflow.js";
37
+
38
+ function contentHash(content = "") {
39
+ return hashBuffer(Buffer.from(String(content || ""), "utf8"));
40
+ }
41
+
42
+ function collectInstalledPackageEntries({ lock, packageRegistry, installedPackageIds = null }) {
43
+ const installed = ensureObject(lock?.installedPackages);
44
+ const packageIds = Array.isArray(installedPackageIds)
45
+ ? installedPackageIds
46
+ : Object.keys(installed);
47
+ return [...new Set(packageIds.map((value) => String(value || "").trim()).filter(Boolean))]
48
+ .sort((left, right) => left.localeCompare(right))
49
+ .map((packageId) => {
50
+ const packageEntry = packageRegistry?.get?.(packageId);
51
+ if (!packageEntry) {
52
+ throw new CiCompositionError(
53
+ "descriptor-missing",
54
+ `Installed package descriptor not found for ${packageId}. Restore the package in node_modules or the JSKIT catalog before synchronizing CI.`,
55
+ { packageId }
56
+ );
57
+ }
58
+ return packageEntry;
59
+ });
60
+ }
61
+
62
+ function composeInstalledPackageCi({ lock, packageRegistry, installedPackageIds = null }) {
63
+ return composeCiContributions(
64
+ collectInstalledPackageEntries({ lock, packageRegistry, installedPackageIds })
65
+ );
66
+ }
67
+
68
+ function managedCiRecord(lock = {}) {
69
+ return ensureObject(ensureObject(lock.managed).ciWorkflow);
70
+ }
71
+
72
+ function setManagedCiRecord(lock, record) {
73
+ lock.managed = {
74
+ ...ensureObject(lock.managed),
75
+ ciWorkflow: {
76
+ path: String(record.path || JSKIT_CI_WORKFLOW_RELATIVE_PATH),
77
+ hash: String(record.hash || "")
78
+ }
79
+ };
80
+ }
81
+
82
+ function recoveryError(message, { force = false } = {}) {
83
+ const command = force ? "npx jskit app sync-ci --force" : "npx jskit app sync-ci";
84
+ return createCliError(`${message} Run: ${command}`);
85
+ }
86
+
87
+ async function inspectWorkflowOwnership({ appRoot, lock }) {
88
+ const record = managedCiRecord(lock);
89
+ const targetPath = path.join(appRoot, JSKIT_CI_WORKFLOW_RELATIVE_PATH);
90
+ const legacyPath = path.join(appRoot, LEGACY_CI_WORKFLOW_RELATIVE_PATH);
91
+ const target = await readFileBufferIfExists(targetPath);
92
+ const legacy = await readFileBufferIfExists(legacyPath);
93
+ return {
94
+ record,
95
+ targetPath,
96
+ target,
97
+ legacyPath,
98
+ legacy,
99
+ targetHash: target.exists ? hashBuffer(target.buffer) : "",
100
+ legacyIsGenerated:
101
+ legacy.exists && hashBuffer(legacy.buffer) === LEGACY_VERIFY_WORKFLOW_HASH
102
+ };
103
+ }
104
+
105
+ async function assertManagedCiWorkflowUnmodified({
106
+ appRoot,
107
+ lock,
108
+ allowManagedOverwrite = false
109
+ }) {
110
+ const ownership = await inspectWorkflowOwnership({ appRoot, lock });
111
+ const recordPath = String(ownership.record.path || "").trim();
112
+ const recordHash = String(ownership.record.hash || "").trim();
113
+
114
+ if (recordPath && recordPath !== JSKIT_CI_WORKFLOW_RELATIVE_PATH) {
115
+ throw createCliError(
116
+ `[ci:workflow-ownership-invalid] .jskit/lock.json records the managed CI workflow at ${recordPath}, but JSKIT owns ${JSKIT_CI_WORKFLOW_RELATIVE_PATH}. Restore the recorded path before synchronizing CI.`
117
+ );
118
+ }
119
+
120
+ if (recordHash && !allowManagedOverwrite) {
121
+ if (!ownership.target.exists) {
122
+ throw recoveryError(
123
+ `[ci:workflow-modified] The JSKIT-managed CI workflow is missing: ${JSKIT_CI_WORKFLOW_RELATIVE_PATH}.`,
124
+ { force: true }
125
+ );
126
+ }
127
+ if (ownership.targetHash !== recordHash) {
128
+ throw recoveryError(
129
+ `[ci:workflow-modified] ${JSKIT_CI_WORKFLOW_RELATIVE_PATH} differs from the generated version recorded in .jskit/lock.json. Move application-specific CI to a separate workflow before regenerating.`,
130
+ { force: true }
131
+ );
132
+ }
133
+ }
134
+
135
+ if (!recordHash && ownership.target.exists) {
136
+ throw createCliError(
137
+ `[ci:workflow-unowned] ${JSKIT_CI_WORKFLOW_RELATIVE_PATH} already exists without JSKIT ownership in .jskit/lock.json. Rename or remove it, then run: npx jskit app sync-ci`
138
+ );
139
+ }
140
+
141
+ if (ownership.legacy.exists && !ownership.legacyIsGenerated) {
142
+ throw createCliError(
143
+ `[ci:legacy-workflow-conflict] ${LEGACY_CI_WORKFLOW_RELATIVE_PATH} is not the original JSKIT scaffold and will not be removed. Move application-specific CI to a separate workflow, remove the duplicate verification workflow, then run: npx jskit app sync-ci`
144
+ );
145
+ }
146
+
147
+ return ownership;
148
+ }
149
+
150
+ async function synchronizeManagedCiWorkflow({
151
+ appRoot,
152
+ lock,
153
+ packageRegistry,
154
+ installedPackageIds = null,
155
+ touchedFiles = null,
156
+ dryRun = false,
157
+ allowManagedOverwrite = false
158
+ }) {
159
+ const model = composeInstalledPackageCi({
160
+ lock,
161
+ packageRegistry,
162
+ installedPackageIds
163
+ });
164
+ const content = renderGithubWorkflow(model);
165
+ const hash = contentHash(content);
166
+ const ownership = await assertManagedCiWorkflowUnmodified({
167
+ appRoot,
168
+ lock,
169
+ allowManagedOverwrite
170
+ });
171
+ const currentContent = ownership.target.exists ? ownership.target.buffer.toString("utf8") : "";
172
+ const workflowChanged = currentContent !== content;
173
+ const removedLegacyWorkflow = ownership.legacyIsGenerated;
174
+ const replacedModifiedWorkflow =
175
+ allowManagedOverwrite &&
176
+ Boolean(ownership.record.hash) &&
177
+ ownership.targetHash !== String(ownership.record.hash || "");
178
+
179
+ if (!dryRun) {
180
+ if (workflowChanged) {
181
+ await mkdir(path.dirname(ownership.targetPath), { recursive: true });
182
+ await writeFile(ownership.targetPath, content, "utf8");
183
+ }
184
+ if (removedLegacyWorkflow) {
185
+ await rm(ownership.legacyPath, { force: true });
186
+ }
187
+ setManagedCiRecord(lock, {
188
+ path: JSKIT_CI_WORKFLOW_RELATIVE_PATH,
189
+ hash
190
+ });
191
+ }
192
+
193
+ if (workflowChanged) {
194
+ touchedFiles?.add?.(JSKIT_CI_WORKFLOW_RELATIVE_PATH);
195
+ }
196
+ if (removedLegacyWorkflow) {
197
+ touchedFiles?.add?.(LEGACY_CI_WORKFLOW_RELATIVE_PATH);
198
+ }
199
+
200
+ return {
201
+ applicable: true,
202
+ path: JSKIT_CI_WORKFLOW_RELATIVE_PATH,
203
+ hash,
204
+ content,
205
+ model,
206
+ changed: workflowChanged || removedLegacyWorkflow,
207
+ workflowChanged,
208
+ removedLegacyWorkflow,
209
+ replacedModifiedWorkflow
210
+ };
211
+ }
212
+
213
+ function createValidationIssue(code, message, details = {}) {
214
+ return {
215
+ code: `ci:${code}`,
216
+ message: `[ci:${code}] ${message}`,
217
+ ...details
218
+ };
219
+ }
220
+
221
+ function formatRequirementPackages(packageIds = []) {
222
+ return packageIds.join(", ");
223
+ }
224
+
225
+ function describeCiRequirements(model = {}) {
226
+ const descriptions = [];
227
+ for (const service of model.services || []) {
228
+ descriptions.push(
229
+ `${formatRequirementPackages(model.sources?.services?.[service.id] || [])} requires the ${service.id} service`
230
+ );
231
+ }
232
+ if (Object.prototype.hasOwnProperty.call(model.environment || {}, "DB_CLIENT")) {
233
+ descriptions.push(
234
+ `${formatRequirementPackages(model.sources?.environment?.DB_CLIENT || [])} requires DB_CLIENT=${model.environment.DB_CLIENT}`
235
+ );
236
+ }
237
+ for (const step of model.steps || []) {
238
+ descriptions.push(
239
+ `${formatRequirementPackages(model.sources?.steps?.[step.id] || [])} requires ${step.label} before verification`
240
+ );
241
+ }
242
+ return descriptions.filter((value) => !value.startsWith(" requires")).join("; ");
243
+ }
244
+
245
+ function normalizeActualEnvironment(value) {
246
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
247
+ return Object.fromEntries(
248
+ Object.entries(source)
249
+ .sort(([left], [right]) => left.localeCompare(right))
250
+ .map(([key, entry]) => [key, String(entry)])
251
+ );
252
+ }
253
+
254
+ function collectEnvironmentIssues({ expected, actual, sources, issues }) {
255
+ for (const [key, value] of Object.entries(expected)) {
256
+ const packageIds = sources[key] || [];
257
+ if (!Object.prototype.hasOwnProperty.call(actual, key)) {
258
+ issues.push(createValidationIssue(
259
+ "environment-missing",
260
+ `${formatRequirementPackages(packageIds)} requires ${key}=${value} in the verify job environment.`
261
+ ));
262
+ continue;
263
+ }
264
+ if (actual[key] !== value) {
265
+ issues.push(createValidationIssue(
266
+ "environment-incorrect",
267
+ `${formatRequirementPackages(packageIds)} requires ${key}=${value}, but the managed workflow contains ${key}=${actual[key]}.`
268
+ ));
269
+ }
270
+ }
271
+ }
272
+
273
+ function collectServiceIssues({ model, verifyJob, issues }) {
274
+ const actualServices = ensureObject(verifyJob.services);
275
+ for (const service of model.services || []) {
276
+ const packageIds = model.sources?.services?.[service.id] || [];
277
+ const actual = actualServices[service.id];
278
+ if (!actual || typeof actual !== "object") {
279
+ issues.push(createValidationIssue(
280
+ "service-missing",
281
+ `${formatRequirementPackages(packageIds)} requires the ${service.id} service.`
282
+ ));
283
+ continue;
284
+ }
285
+ const expectedOptions = renderGithubServiceOptions(service);
286
+ const actualPorts = Array.isArray(actual.ports) ? actual.ports.map(String) : [];
287
+ const serviceMatches =
288
+ String(actual.image || "") === service.image &&
289
+ JSON.stringify(normalizeActualEnvironment(actual.env)) === JSON.stringify(service.environment) &&
290
+ JSON.stringify(actualPorts) === JSON.stringify(service.ports) &&
291
+ String(actual.options || "") === expectedOptions;
292
+ if (!serviceMatches) {
293
+ issues.push(createValidationIssue(
294
+ "service-incorrect",
295
+ `${formatRequirementPackages(packageIds)} requires the generated ${service.id} service image, environment, ports, and health check to remain unchanged.`
296
+ ));
297
+ }
298
+ }
299
+ }
300
+
301
+ function collectStepIssues({ model, verifyJob, issues }) {
302
+ const actualSteps = Array.isArray(verifyJob.steps) ? verifyJob.steps : [];
303
+ const stepIndexById = new Map();
304
+ for (const [index, step] of actualSteps.entries()) {
305
+ const id = String(step?.id || "").trim();
306
+ if (id && !stepIndexById.has(id)) {
307
+ stepIndexById.set(id, index);
308
+ }
309
+ }
310
+
311
+ for (const step of model.steps || []) {
312
+ const packageIds = model.sources?.steps?.[step.id] || [];
313
+ const index = stepIndexById.get(step.id);
314
+ if (typeof index === "undefined") {
315
+ issues.push(createValidationIssue(
316
+ "step-missing",
317
+ `${formatRequirementPackages(packageIds)} requires the "${step.label}" step (${step.id}).`
318
+ ));
319
+ continue;
320
+ }
321
+ const actual = actualSteps[index];
322
+ if (String(actual.name || "") !== step.label || String(actual.run || "") !== step.command) {
323
+ issues.push(createValidationIssue(
324
+ "step-incorrect",
325
+ `${formatRequirementPackages(packageIds)} requires ${step.id} to run ${step.command}.`
326
+ ));
327
+ }
328
+ }
329
+
330
+ const expectedOrder = [
331
+ "checkout",
332
+ "setup-node",
333
+ "install-dependencies",
334
+ ...(model.steps || []).map((step) => step.id),
335
+ "verify"
336
+ ];
337
+ const actualIndexes = expectedOrder.map((id) => stepIndexById.get(id));
338
+ if (
339
+ actualIndexes.every((index) => typeof index !== "undefined") &&
340
+ actualIndexes.some((index, position) => position > 0 && index <= actualIndexes[position - 1])
341
+ ) {
342
+ issues.push(createValidationIssue(
343
+ "step-order-incorrect",
344
+ `Managed preparation steps must run after npm ci and before npm run verify in this order: ${expectedOrder.join(" -> ")}.`
345
+ ));
346
+ }
347
+ }
348
+
349
+ async function validateManagedCiWorkflow({ appRoot, lock, packageRegistry }) {
350
+ let model = null;
351
+ try {
352
+ model = composeInstalledPackageCi({ lock, packageRegistry });
353
+ } catch (error) {
354
+ const message = error instanceof Error ? error.message : String(error);
355
+ return {
356
+ applicable: true,
357
+ valid: false,
358
+ model: null,
359
+ issues: [createValidationIssue(
360
+ "contribution-conflict",
361
+ `Installed package CI contributions could not be composed: ${message}`
362
+ )]
363
+ };
364
+ }
365
+
366
+ const record = managedCiRecord(lock);
367
+ const workflowPath = path.join(appRoot, JSKIT_CI_WORKFLOW_RELATIVE_PATH);
368
+ const legacyPath = path.join(appRoot, LEGACY_CI_WORKFLOW_RELATIVE_PATH);
369
+ const workflow = await readFileBufferIfExists(workflowPath);
370
+ const hasLegacyWorkflow = await fileExists(legacyPath);
371
+ const applicable =
372
+ Boolean(record.hash || record.path) ||
373
+ workflow.exists ||
374
+ hasLegacyWorkflow ||
375
+ model.packages.length > 0;
376
+ if (!applicable) {
377
+ return {
378
+ applicable: false,
379
+ valid: true,
380
+ model,
381
+ issues: []
382
+ };
383
+ }
384
+
385
+ const expectedContent = renderGithubWorkflow(model);
386
+ const expectedHash = contentHash(expectedContent);
387
+ const requirementSummary = describeCiRequirements(model);
388
+ const requirementSuffix = requirementSummary ? ` ${requirementSummary}.` : "";
389
+ const recovery = " Run: npx jskit app sync-ci";
390
+ const forcedRecovery = " Run: npx jskit app sync-ci --force";
391
+ const issues = [];
392
+ const recordPath = String(record.path || "").trim();
393
+ const recordHash = String(record.hash || "").trim();
394
+
395
+ if (recordPath !== JSKIT_CI_WORKFLOW_RELATIVE_PATH || !recordHash) {
396
+ issues.push(createValidationIssue(
397
+ "workflow-ownership-missing",
398
+ `The generated workflow is not recorded as a managed projection in .jskit/lock.json.${recovery}`
399
+ ));
400
+ }
401
+ if (!workflow.exists) {
402
+ const missingWorkflowRecovery = recordHash ? forcedRecovery : recovery;
403
+ issues.push(createValidationIssue(
404
+ "workflow-missing",
405
+ `The managed CI workflow is missing at ${JSKIT_CI_WORKFLOW_RELATIVE_PATH}.${missingWorkflowRecovery}`
406
+ ));
407
+ issues.push(createValidationIssue(
408
+ "workflow-out-of-date",
409
+ `The managed CI workflow does not match the installed packages.${requirementSuffix}${missingWorkflowRecovery}`
410
+ ));
411
+ return {
412
+ applicable,
413
+ valid: false,
414
+ model,
415
+ expectedContent,
416
+ expectedHash,
417
+ issues
418
+ };
419
+ }
420
+
421
+ const actualContent = workflow.buffer.toString("utf8");
422
+ const actualHash = hashBuffer(workflow.buffer);
423
+ const workflowWasModified = Boolean(recordHash && actualHash !== recordHash);
424
+ const workflowRecovery = workflowWasModified ? forcedRecovery : recovery;
425
+ if (workflowWasModified) {
426
+ issues.push(createValidationIssue(
427
+ "workflow-modified",
428
+ `${JSKIT_CI_WORKFLOW_RELATIVE_PATH} differs from the generated version recorded in .jskit/lock.json. Move application-specific CI to a separate workflow.${forcedRecovery}`
429
+ ));
430
+ }
431
+ if (actualHash !== expectedHash || recordHash !== expectedHash) {
432
+ issues.push(createValidationIssue(
433
+ "workflow-out-of-date",
434
+ `The managed CI workflow does not match the installed packages.${requirementSuffix}${workflowRecovery}`
435
+ ));
436
+ }
437
+
438
+ let document = null;
439
+ try {
440
+ document = parseGithubWorkflow(actualContent);
441
+ } catch (error) {
442
+ issues.push(createValidationIssue(
443
+ "workflow-invalid-yaml",
444
+ `${JSKIT_CI_WORKFLOW_RELATIVE_PATH} is not valid YAML: ${error instanceof Error ? error.message : String(error)}.${workflowRecovery}`
445
+ ));
446
+ return {
447
+ applicable,
448
+ valid: false,
449
+ model,
450
+ expectedContent,
451
+ expectedHash,
452
+ actualHash,
453
+ issues
454
+ };
455
+ }
456
+
457
+ const verifyJob = ensureObject(ensureObject(ensureObject(document).jobs).verify);
458
+ collectEnvironmentIssues({
459
+ expected: model.environment,
460
+ actual: normalizeActualEnvironment(verifyJob.env),
461
+ sources: model.sources.environment,
462
+ issues
463
+ });
464
+ collectServiceIssues({ model, verifyJob, issues });
465
+ collectStepIssues({ model, verifyJob, issues });
466
+
467
+ return {
468
+ applicable,
469
+ valid: issues.length === 0,
470
+ model,
471
+ expectedContent,
472
+ expectedHash,
473
+ actualHash,
474
+ issues
475
+ };
476
+ }
477
+
478
+ async function loadAppCiContext(appRoot) {
479
+ const { lockPath, lock } = await loadLockFile(appRoot);
480
+ const catalogRegistry = await loadPackageRegistry();
481
+ const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
482
+ const packageRegistry = mergePackageRegistries(catalogRegistry, appLocalRegistry);
483
+ await hydratePackageRegistryFromInstalledNodeModules({
484
+ appRoot,
485
+ packageRegistry,
486
+ seedPackageIds: Object.keys(ensureObject(lock.installedPackages))
487
+ });
488
+ return {
489
+ lockPath,
490
+ lock,
491
+ packageRegistry
492
+ };
493
+ }
494
+
495
+ async function assertAppManagedCiWorkflowUnmodified({ appRoot }) {
496
+ if (!(await directoryLooksLikeJskitAppRoot(appRoot))) {
497
+ return { applicable: false };
498
+ }
499
+ const { lock } = await loadLockFile(appRoot);
500
+ await assertManagedCiWorkflowUnmodified({ appRoot, lock });
501
+ return { applicable: true };
502
+ }
503
+
504
+ async function synchronizeAppCiWorkflow({
505
+ appRoot,
506
+ allowManagedOverwrite = false,
507
+ dryRun = false
508
+ }) {
509
+ if (!(await directoryLooksLikeJskitAppRoot(appRoot))) {
510
+ return { applicable: false, changed: false };
511
+ }
512
+ const { lockPath, lock, packageRegistry } = await loadAppCiContext(appRoot);
513
+ const result = await synchronizeManagedCiWorkflow({
514
+ appRoot,
515
+ lock,
516
+ packageRegistry,
517
+ dryRun,
518
+ allowManagedOverwrite
519
+ });
520
+ if (!dryRun) {
521
+ await writeJsonFile(lockPath, lock);
522
+ }
523
+ return {
524
+ ...result,
525
+ lockPath
526
+ };
527
+ }
528
+
529
+ async function validateAppCiWorkflow({ appRoot }) {
530
+ if (!(await directoryLooksLikeJskitAppRoot(appRoot))) {
531
+ return { applicable: false, valid: true, issues: [] };
532
+ }
533
+ const { lock, packageRegistry } = await loadAppCiContext(appRoot);
534
+ return validateManagedCiWorkflow({
535
+ appRoot,
536
+ lock,
537
+ packageRegistry
538
+ });
539
+ }
540
+
541
+ export {
542
+ assertAppManagedCiWorkflowUnmodified,
543
+ assertManagedCiWorkflowUnmodified,
544
+ composeInstalledPackageCi,
545
+ synchronizeAppCiWorkflow,
546
+ synchronizeManagedCiWorkflow,
547
+ validateAppCiWorkflow,
548
+ validateManagedCiWorkflow
549
+ };
@@ -4,6 +4,7 @@ import {
4
4
  ensureObject
5
5
  } from "../shared/collectionUtils.js";
6
6
  import { normalizeFileMutationRecord } from "./mutationWhen.js";
7
+ import { normalizeCiContribution } from "./ci/contract.js";
7
8
 
8
9
  const PACKAGE_KIND_RUNTIME = "runtime";
9
10
  const PACKAGE_KIND_GENERATOR = "generator";
@@ -191,9 +192,14 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
191
192
  validateFileMutationShape(normalized, descriptorPath);
192
193
  validateSourceMutationShape(normalized, descriptorPath);
193
194
  const lifecycle = validateLifecycleShape(normalized, descriptorPath);
195
+ const ci = normalizeCiContribution(normalized.ci, {
196
+ descriptorPath,
197
+ packageId
198
+ });
194
199
 
195
200
  return {
196
201
  ...normalized,
202
+ ci,
197
203
  lifecycle,
198
204
  kind: normalizePackageKind(normalized.kind, descriptorPath)
199
205
  };
@@ -224,11 +230,16 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
224
230
  validateFileMutationShape(normalized, descriptorPath);
225
231
  validateSourceMutationShape(normalized, descriptorPath);
226
232
  const lifecycle = validateLifecycleShape(normalized, descriptorPath);
233
+ const ci = normalizeCiContribution(normalized.ci, {
234
+ descriptorPath,
235
+ packageId
236
+ });
227
237
 
228
238
  return {
229
239
  ...normalized,
230
240
  packageId,
231
241
  version,
242
+ ci,
232
243
  lifecycle,
233
244
  kind: normalizePackageKind(normalized.kind, descriptorPath)
234
245
  };
@@ -11,8 +11,8 @@ import {
11
11
  import { runAppAdoptManagedScriptsCommand } from "./appCommands/adoptManagedScripts.js";
12
12
  import { runAppLinkLocalPackagesCommand } from "./appCommands/linkLocalPackages.js";
13
13
  import { runAppMigrateSourceMutationsCommand } from "./appCommands/migrateSourceMutations.js";
14
- import { runAppPreparePreviewUserCommand } from "./appCommands/preparePreviewUser.js";
15
14
  import { runAppReleaseCommand } from "./appCommands/release.js";
15
+ import { runAppSyncCiCommand } from "./appCommands/syncCi.js";
16
16
  import { runAppUpdatePackagesCommand } from "./appCommands/updatePackages.js";
17
17
  import { runAppVerifyCommand } from "./appCommands/verify.js";
18
18
  import { runAppVerifyUiCommand } from "./appCommands/verifyUi.js";
@@ -144,6 +144,9 @@ function createAppCommands(ctx = {}) {
144
144
  if (definition.name === "update-packages") {
145
145
  return runAppUpdatePackagesCommand(ctx, { appRoot, options, stdout, stderr });
146
146
  }
147
+ if (definition.name === "sync-ci") {
148
+ return runAppSyncCiCommand(ctx, { appRoot, options, stdout, stderr });
149
+ }
147
150
  if (definition.name === "link-local-packages") {
148
151
  return runAppLinkLocalPackagesCommand(ctx, { appRoot, options, stdout, stderr });
149
152
  }
@@ -156,10 +159,6 @@ function createAppCommands(ctx = {}) {
156
159
  if (definition.name === "migrate-source-mutations") {
157
160
  return runAppMigrateSourceMutationsCommand(ctx, { appRoot, options, stdout, stderr });
158
161
  }
159
- if (definition.name === "prepare-preview-user") {
160
- return runAppPreparePreviewUserCommand(ctx, { appRoot, options, stdout, stderr });
161
- }
162
-
163
162
  throw createCliError(`Unhandled app subcommand: ${definition.name}.`, {
164
163
  renderUsage: () => renderAppHelp(stderr, definition)
165
164
  });