@prisma/composer-cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/bin.mjs +183 -175
  2. package/dist/bin.mjs.map +1 -1
  3. package/dist/{execute-deploy-destroy-DJ3BlpgU-DpcJjxvk.mjs → execute-deploy-destroy-Bsdh8hf4-BVXNSBmy.mjs} +4 -3
  4. package/dist/{execute-deploy-destroy-DJ3BlpgU-BCTSVylF.mjs.map → execute-deploy-destroy-Bsdh8hf4-BVXNSBmy.mjs.map} +1 -1
  5. package/dist/{execute-deploy-destroy-DJ3BlpgU-BCTSVylF.mjs → execute-deploy-destroy-Bsdh8hf4-CZVPrQtx.mjs} +4 -3
  6. package/dist/{execute-deploy-destroy-DJ3BlpgU-DpcJjxvk.mjs.map → execute-deploy-destroy-Bsdh8hf4-CZVPrQtx.mjs.map} +1 -1
  7. package/dist/{execute-dev-CQ0Lo2eN-DBThEMBv.mjs → execute-dev-CQ0Lo2eN-DxMcOjxh.mjs} +2 -2
  8. package/dist/{execute-dev-CQ0Lo2eN-DBThEMBv.mjs.map → execute-dev-CQ0Lo2eN-DxMcOjxh.mjs.map} +1 -1
  9. package/dist/{execute-dev-CQ0Lo2eN-BBNilsxZ.mjs → execute-dev-CQ0Lo2eN-WebHkuDn.mjs} +2 -2
  10. package/dist/{execute-dev-CQ0Lo2eN-BBNilsxZ.mjs.map → execute-dev-CQ0Lo2eN-WebHkuDn.mjs.map} +1 -1
  11. package/dist/{execute-log-CUzTOzSj-31WISHGD.mjs → execute-log-CUzTOzSj-BndClj6B.mjs} +2 -2
  12. package/dist/{execute-log-CUzTOzSj-31WISHGD.mjs.map → execute-log-CUzTOzSj-BndClj6B.mjs.map} +1 -1
  13. package/dist/{execute-log-CUzTOzSj-ERhhGgwJ.mjs → execute-log-CUzTOzSj-DPxnEKA2.mjs} +2 -2
  14. package/dist/{execute-log-CUzTOzSj-ERhhGgwJ.mjs.map → execute-log-CUzTOzSj-DPxnEKA2.mjs.map} +1 -1
  15. package/dist/{family-BqgXAnt0-BilZvb88.d.mts → family-BqgXAnt0-CU2L1I6j.d.mts} +50 -3
  16. package/dist/{family-BqgXAnt0-BilZvb88.d.mts.map → family-BqgXAnt0-CU2L1I6j.d.mts.map} +1 -1
  17. package/dist/family.d.mts +1 -1
  18. package/dist/family.mjs +182 -174
  19. package/dist/family.mjs.map +1 -1
  20. package/dist/{pipeline-Bt8QocRh-BWGl-fzK.mjs → pipeline-Bt8QocRh-BGjwJqNH.mjs} +138 -10
  21. package/dist/pipeline-Bt8QocRh-BGjwJqNH.mjs.map +1 -0
  22. package/dist/{pipeline-Bt8QocRh-DWh8gbfz.mjs → pipeline-Bt8QocRh-B_yOh-0h.mjs} +138 -10
  23. package/dist/pipeline-Bt8QocRh-B_yOh-0h.mjs.map +1 -0
  24. package/dist/testing.d.mts +1 -1
  25. package/package.json +6 -6
  26. package/dist/pipeline-Bt8QocRh-BWGl-fzK.mjs.map +0 -1
  27. package/dist/pipeline-Bt8QocRh-DWh8gbfz.mjs.map +0 -1
package/dist/family.mjs CHANGED
@@ -4,7 +4,7 @@ import { t as alchemyCommandLine } from "./run-alchemy--ZIvgqyU-Cub4-pDf.mjs";
4
4
  import { spawn } from "node:child_process";
5
5
  import { EnvironmentCredentialManager, createCli, defineCommand, defineCommandFamily, defineConfigSection, defineSessionCommand, exitWithChildStatus, flag, loadConfig, positional } from "@prisma/cli-engine";
6
6
  import { CliStructuredError, notOk, ok } from "@prisma/cli-engine/protocol";
7
- //#region ../../0-framework/3-tooling/cli/dist/log-WOf7R2HN.mjs
7
+ //#region ../../0-framework/3-tooling/cli/dist/log-E_5UMZl8.mjs
8
8
  /** In-package variant threading the injection seam (the CLI's RunDeps, unit
9
9
  * tests). Deliberately NOT re-exported through `./control` — the seam mirrors
10
10
  * internal types and is not part of the published surface. */
@@ -12,7 +12,7 @@ async function deployWithDeps(input, deps) {
12
12
  const cwd = input.cwd ?? process.cwd();
13
13
  let executor;
14
14
  try {
15
- executor = await import("./execute-deploy-destroy-DJ3BlpgU-BCTSVylF.mjs");
15
+ executor = await import("./execute-deploy-destroy-Bsdh8hf4-CZVPrQtx.mjs");
16
16
  } catch (error) {
17
17
  return notOk$1(executorLoadFailure("deploy", error, cwd));
18
18
  }
@@ -25,7 +25,7 @@ async function destroyWithDeps(input, deps) {
25
25
  const cwd = input.cwd ?? process.cwd();
26
26
  let executor;
27
27
  try {
28
- executor = await import("./execute-deploy-destroy-DJ3BlpgU-BCTSVylF.mjs");
28
+ executor = await import("./execute-deploy-destroy-Bsdh8hf4-CZVPrQtx.mjs");
29
29
  } catch (error) {
30
30
  return notOk$1(executorLoadFailure("destroy", error, cwd));
31
31
  }
@@ -38,7 +38,7 @@ async function devWithDeps(input, deps) {
38
38
  const cwd = input.cwd ?? process.cwd();
39
39
  let executor;
40
40
  try {
41
- executor = await import("./execute-dev-CQ0Lo2eN-DBThEMBv.mjs");
41
+ executor = await import("./execute-dev-CQ0Lo2eN-DxMcOjxh.mjs");
42
42
  } catch (error) {
43
43
  return notOk$1(executorLoadFailure("dev", error, cwd));
44
44
  }
@@ -51,14 +51,14 @@ async function logWithDeps(input, deps) {
51
51
  const cwd = input.cwd ?? process.cwd();
52
52
  let executor;
53
53
  try {
54
- executor = await import("./execute-log-CUzTOzSj-31WISHGD.mjs");
54
+ executor = await import("./execute-log-CUzTOzSj-BndClj6B.mjs");
55
55
  } catch (error) {
56
56
  return notOk$1(executorLoadFailure("log", error, cwd));
57
57
  }
58
58
  return executor.executeLog(input, deps, cwd);
59
59
  }
60
60
  //#endregion
61
- //#region ../../0-framework/3-tooling/cli/dist/engine-cli-CMw8Uug4.mjs
61
+ //#region ../../0-framework/3-tooling/cli/dist/engine-cli-BI4fPmYY.mjs
62
62
  /**
63
63
  * The family boundary's error translation.
64
64
  *
@@ -266,6 +266,23 @@ const composerSection = defineConfigSection({
266
266
  validate
267
267
  });
268
268
  /**
269
+ * `destroy`'s target, from its two mutually exclusive flags. Kept out of the
270
+ * grammar because the engine's flag layer has no "exactly one of these"
271
+ * construct — and because both wrong shapes need their own remedy, which a
272
+ * generic arity error could not give.
273
+ */
274
+ function targetOf(stage, production) {
275
+ if (stage !== void 0 && production) return notOk(new CliStructuredError("DEPLOY.TARGET_CONFLICT", "Pass either --stage <name> or --production to `destroy`, not both."));
276
+ if (stage === void 0 && !production) return notOk(new CliStructuredError("DEPLOY.TARGET_MISSING", "`destroy` requires an explicit target.", { nextActions: [{
277
+ kind: "user-choice",
278
+ label: "Pass --stage <name> to tear down a branch environment, or --production to tear down the production environment."
279
+ }] }));
280
+ return ok(stage !== void 0 ? {
281
+ kind: "stage",
282
+ stage
283
+ } : { kind: "production" });
284
+ }
285
+ /**
269
286
  * The workspace the in-process leg acts in, from the engine's credential
270
287
  * read. Handlers never decode a token and never read PRISMA_WORKSPACE_ID:
271
288
  * whether the credential came from a stored session or the environment, the
@@ -280,16 +297,18 @@ async function workspaceIdOf(ctx) {
280
297
  return (await ctx.activeCredential())?.workspaceId;
281
298
  }
282
299
  /**
283
- * `deploy <entry>` — a result command that hands the terminal to alchemy.
300
+ * `destroy <entry>` — deploy's mirror: same derivation, alchemy destroy.
284
301
  *
285
- * `--production` is gone. It was accepted and then always errored ("only
286
- * valid with destroy"), so no invocation using it could ever have succeeded;
287
- * deploy targets production by default when no `--stage` is given.
302
+ * No confirmation prompt, matching the CLI this replaces. The front door
303
+ * cannot know what the child will tear down, so a consent step here would be
304
+ * asking the user to approve a list nobody has computed yet; if destroy
305
+ * deserves one it belongs in composer's own operation, not in the grammar.
288
306
  */
289
- const createDeployCommand = (operations) => defineCommand({
307
+ const createDestroyCommand = (operations) => defineCommand({
290
308
  help: {
291
- summary: "Deploy the application whose root node is <entry>'s default export.",
292
- examples: ["{bin} deploy src/service.ts", "{bin} deploy src/service.ts --stage feat-auth"]
309
+ summary: "Tear down the application whose root node is <entry>'s default export.",
310
+ description: "Same derivation as deploy. Requires an explicit target: --stage <name> for a branch environment, or --production for the production environment.",
311
+ examples: ["{bin} destroy src/service.ts --stage feat-auth", "{bin} destroy src/service.ts --production"]
293
312
  },
294
313
  args: {
295
314
  positionals: { entry: positional.string({
@@ -298,21 +317,14 @@ const createDeployCommand = (operations) => defineCommand({
298
317
  }) },
299
318
  flags: {
300
319
  name: flag.string({
301
- brief: "Override the root node's name — the deploy's application name.",
320
+ brief: "Override the root node's name — the application name to tear down.",
302
321
  placeholder: "name"
303
322
  }),
304
323
  stage: flag.string({
305
- brief: "Deploy scope to target; omit for production.",
324
+ brief: "Tear down this branch environment.",
306
325
  placeholder: "stage"
307
326
  }),
308
- report: flag.string({
309
- brief: "Write the deploy's outcome as JSON to this path — resources, preview URLs, and the failure cause. Also settable as PRISMA_COMPOSER_REPORT_FILE.",
310
- placeholder: "path"
311
- }),
312
- buildId: flag.string({
313
- brief: "Join the deploy record your CI already created rather than letting the target create one. Each target also reads its own environment variable for this; the flag wins.",
314
- placeholder: "id"
315
- })
327
+ production: flag.boolean({ brief: "Tear down the project-level production environment." })
316
328
  }
317
329
  },
318
330
  needs: {
@@ -321,69 +333,129 @@ const createDeployCommand = (operations) => defineCommand({
321
333
  },
322
334
  maySpawn: true,
323
335
  handler: async (args, ctx) => {
336
+ const target = targetOf(args.flags.stage, args.flags.production);
337
+ if (!target.ok) return target;
324
338
  const alchemy = convergeSpawn(ctx);
325
- return settleConverge(await operations.deploy({
339
+ return settleConverge(await operations.destroy({
326
340
  entry: args.positionals.entry,
327
341
  name: args.flags.name,
328
- stage: args.flags.stage,
342
+ target: target.value,
329
343
  cwd: ctx.cwd,
330
- reportPath: args.flags.report,
331
- reportId: args.flags.buildId
344
+ onEvent: (event) => {
345
+ if (event.kind !== "no-local-deploy-state") return;
346
+ ctx.report({
347
+ kind: "message",
348
+ severity: "warn",
349
+ text: `No prior deploy state under ${event.cwd} — if you deployed from a different directory, run destroy from there; otherwise this is a no-op.`
350
+ });
351
+ }
332
352
  }, operationDeps({
333
353
  alchemy,
334
354
  configPath: ctx.config.configPath,
335
355
  workspaceId: await workspaceIdOf(ctx),
336
356
  client: ctx.api
337
- })), ctx, ({ summary }) => ctx.present({ data: { summary: summary ?? null } }, {
338
- human: (ui) => summary === void 0 ? [{
339
- kind: "summary",
340
- status: "ok",
341
- text: "Deployed."
342
- }] : [{
357
+ })), ctx, () => ctx.present({ data: void 0 }, {
358
+ human: () => [{
343
359
  kind: "summary",
344
360
  status: "ok",
345
- text: `Deployed ${ui.emphasize(summary.app)}.`
346
- }, {
347
- kind: "table",
348
- columns: ["Address", "Deployed"],
349
- rows: summary.nodes.map((node) => [node.address, node.entities.map((entity) => `${entity.kind} ${entity.id}`).join(", ")])
361
+ text: "Destroyed."
350
362
  }],
351
363
  stdout: () => [],
352
- json: () => ({ summary: summary ?? null }),
364
+ json: () => void 0,
353
365
  next: () => []
354
366
  }));
355
367
  }
356
368
  });
357
369
  /**
358
- * `destroy`'s target, from its two mutually exclusive flags. Kept out of the
359
- * grammar because the engine's flag layer has no "exactly one of these"
360
- * construct — and because both wrong shapes need their own remedy, which a
361
- * generic arity error could not give.
370
+ * `log <entry> [address]` a session command that tails the merged logs of
371
+ * the app running locally on this machine.
372
+ *
373
+ * It reads the LOCAL dev-emulator daemon, not the platform's logs surface, so
374
+ * it is credential-free. It hands the terminal to nothing, which is why it
375
+ * does not declare `maySpawn` and keeps json support: the log lines ARE its
376
+ * json surface.
377
+ *
378
+ * Windows is refused inside the operation itself (LOG.PLATFORM_UNSUPPORTED),
379
+ * which is why nothing here reads the platform.
362
380
  */
363
- function targetOf(stage, production) {
364
- if (stage !== void 0 && production) return notOk(new CliStructuredError("DEPLOY.TARGET_CONFLICT", "Pass either --stage <name> or --production to `destroy`, not both."));
365
- if (stage === void 0 && !production) return notOk(new CliStructuredError("DEPLOY.TARGET_MISSING", "`destroy` requires an explicit target.", { nextActions: [{
366
- kind: "user-choice",
367
- label: "Pass --stage <name> to tear down a branch environment, or --production to tear down the production environment."
368
- }] }));
369
- return ok(stage !== void 0 ? {
370
- kind: "stage",
371
- stage
372
- } : { kind: "production" });
373
- }
381
+ /** An empty screen reads as broken, so show a little recent history before going live. */
382
+ const DEFAULT_TAIL = 20;
383
+ const createLogCommand = (operations) => defineSessionCommand({
384
+ help: {
385
+ summary: "Tail the merged logs of the locally-running application whose root node is <entry>'s default export.",
386
+ examples: ["{bin} log src/service.ts", "{bin} log src/service.ts catalog.service"]
387
+ },
388
+ args: {
389
+ positionals: {
390
+ entry: positional.string({
391
+ brief: "The module whose default export is the application root.",
392
+ placeholder: "entry"
393
+ }),
394
+ address: positional.optionalString({
395
+ brief: "Only this service's lines, by its dotted address (catalog.service); every service when absent.",
396
+ placeholder: "address"
397
+ })
398
+ },
399
+ flags: {
400
+ name: flag.string({
401
+ brief: "Override the root node's name — the dev instance's application name.",
402
+ placeholder: "name"
403
+ }),
404
+ tail: flag.number({
405
+ brief: `How many trailing history lines to show before live output (default ${String(DEFAULT_TAIL)}).`,
406
+ placeholder: "lines",
407
+ default: DEFAULT_TAIL
408
+ })
409
+ }
410
+ },
411
+ needs: { config: composerSection },
412
+ handler: async (args, ctx) => {
413
+ const reportLogEvent = (event) => {
414
+ ctx.report({
415
+ kind: "message",
416
+ severity: "warn",
417
+ text: event.kind === "stream-failed" ? `Stream failed: ${event.message}` : `Falling behind — dropped the ${String(event.count)} oldest lines.`
418
+ });
419
+ };
420
+ const result = await operations.log({
421
+ entry: args.positionals.entry,
422
+ name: args.flags.name,
423
+ address: args.positionals.address,
424
+ tail: args.flags.tail ?? DEFAULT_TAIL,
425
+ cwd: ctx.cwd,
426
+ signal: ctx.signal,
427
+ onEvent: reportLogEvent
428
+ }, { configPath: ctx.config.configPath });
429
+ if (!result.ok) return notOk(toEngineError(result.failure));
430
+ const attached = result.value;
431
+ if (attached.services.length === 0) {
432
+ ctx.report({
433
+ kind: "message",
434
+ severity: "warn",
435
+ text: `No running services for "${attached.appName}" — start it first with \`dev ${args.positionals.entry}\`.`
436
+ });
437
+ return ok(void 0);
438
+ }
439
+ for await (const { service, line } of attached.lines) ctx.report({
440
+ kind: "output",
441
+ source: service,
442
+ channel: "data",
443
+ line: `[${service}] ${line}`
444
+ });
445
+ return ok(void 0);
446
+ }
447
+ });
374
448
  /**
375
- * `destroy <entry>` — deploy's mirror: same derivation, alchemy destroy.
449
+ * `deploy <entry>` — a result command that hands the terminal to alchemy.
376
450
  *
377
- * No confirmation prompt, matching the CLI this replaces. The front door
378
- * cannot know what the child will tear down, so a consent step here would be
379
- * asking the user to approve a list nobody has computed yet; if destroy
380
- * deserves one it belongs in composer's own operation, not in the grammar.
451
+ * `--production` is gone. It was accepted and then always errored ("only
452
+ * valid with destroy"), so no invocation using it could ever have succeeded;
453
+ * deploy targets production by default when no `--stage` is given.
381
454
  */
382
- const createDestroyCommand = (operations) => defineCommand({
455
+ const createDeployCommand = (operations) => defineCommand({
383
456
  help: {
384
- summary: "Tear down the application whose root node is <entry>'s default export.",
385
- description: "Same derivation as deploy. Requires an explicit target: --stage <name> for a branch environment, or --production for the production environment.",
386
- examples: ["{bin} destroy src/service.ts --stage feat-auth", "{bin} destroy src/service.ts --production"]
457
+ summary: "Deploy the application whose root node is <entry>'s default export.",
458
+ examples: ["{bin} deploy src/service.ts", "{bin} deploy src/service.ts --stage feat-auth"]
387
459
  },
388
460
  args: {
389
461
  positionals: { entry: positional.string({
@@ -392,14 +464,21 @@ const createDestroyCommand = (operations) => defineCommand({
392
464
  }) },
393
465
  flags: {
394
466
  name: flag.string({
395
- brief: "Override the root node's name — the application name to tear down.",
467
+ brief: "Override the root node's name — the deploy's application name.",
396
468
  placeholder: "name"
397
469
  }),
398
470
  stage: flag.string({
399
- brief: "Tear down this branch environment.",
471
+ brief: "Deploy scope to target; omit for production.",
400
472
  placeholder: "stage"
401
473
  }),
402
- production: flag.boolean({ brief: "Tear down the project-level production environment." })
474
+ report: flag.string({
475
+ brief: "Write the deploy's outcome as JSON to this path — resources, preview URLs, and the failure cause. Also settable as PRISMA_COMPOSER_REPORT_FILE.",
476
+ placeholder: "path"
477
+ }),
478
+ buildId: flag.string({
479
+ brief: "Join the deploy record your CI already created rather than letting the target create one. Each target also reads its own environment variable for this; the flag wins.",
480
+ placeholder: "id"
481
+ })
403
482
  }
404
483
  },
405
484
  needs: {
@@ -408,35 +487,35 @@ const createDestroyCommand = (operations) => defineCommand({
408
487
  },
409
488
  maySpawn: true,
410
489
  handler: async (args, ctx) => {
411
- const target = targetOf(args.flags.stage, args.flags.production);
412
- if (!target.ok) return target;
413
490
  const alchemy = convergeSpawn(ctx);
414
- return settleConverge(await operations.destroy({
491
+ return settleConverge(await operations.deploy({
415
492
  entry: args.positionals.entry,
416
493
  name: args.flags.name,
417
- target: target.value,
494
+ stage: args.flags.stage,
418
495
  cwd: ctx.cwd,
419
- onEvent: (event) => {
420
- if (event.kind !== "no-local-deploy-state") return;
421
- ctx.report({
422
- kind: "message",
423
- severity: "warn",
424
- text: `No prior deploy state under ${event.cwd} — if you deployed from a different directory, run destroy from there; otherwise this is a no-op.`
425
- });
426
- }
496
+ reportPath: args.flags.report,
497
+ reportId: args.flags.buildId
427
498
  }, operationDeps({
428
499
  alchemy,
429
500
  configPath: ctx.config.configPath,
430
501
  workspaceId: await workspaceIdOf(ctx),
431
502
  client: ctx.api
432
- })), ctx, () => ctx.present({ data: void 0 }, {
433
- human: () => [{
503
+ })), ctx, ({ summary }) => ctx.present({ data: { summary: summary ?? null } }, {
504
+ human: (ui) => summary === void 0 ? [{
434
505
  kind: "summary",
435
506
  status: "ok",
436
- text: "Destroyed."
507
+ text: "Deployed."
508
+ }] : [{
509
+ kind: "summary",
510
+ status: "ok",
511
+ text: `Deployed ${ui.emphasize(summary.app)}.`
512
+ }, {
513
+ kind: "table",
514
+ columns: ["Address", "Deployed"],
515
+ rows: summary.nodes.map((node) => [node.address, node.entities.map((entity) => `${entity.kind} ${entity.id}`).join(", ")])
437
516
  }],
438
517
  stdout: () => [],
439
- json: () => void 0,
518
+ json: () => ({ summary: summary ?? null }),
440
519
  next: () => []
441
520
  }));
442
521
  }
@@ -578,7 +657,7 @@ Generated stack file: ${event.stackFilePath}\nRun \`${event.reproduceCommand}\`
578
657
  const createDevCommand = (operations) => defineSessionCommand({
579
658
  help: {
580
659
  summary: "Bring up the application whose root node is <entry>'s default export, entirely on this machine.",
581
- description: "Runs credential-free and watches the app for changes, reconverging on every edit. Logs are a separate command; run `log <entry>` to tail them.",
660
+ description: "Runs credential-free and watches the app for changes, reconverging on every edit.",
582
661
  examples: ["{bin} dev src/service.ts", "{bin} dev src/service.ts --fresh"]
583
662
  },
584
663
  args: {
@@ -615,85 +694,6 @@ const createDevCommand = (operations) => defineSessionCommand({
615
694
  }
616
695
  });
617
696
  /**
618
- * `log <entry> [address]` — a session command that tails the merged logs of
619
- * the app running locally on this machine.
620
- *
621
- * It reads the LOCAL dev-emulator daemon, not the platform's logs surface, so
622
- * it is credential-free. It hands the terminal to nothing, which is why it
623
- * does not declare `maySpawn` and keeps json support: the log lines ARE its
624
- * json surface.
625
- *
626
- * Windows is refused inside the operation itself (LOG.PLATFORM_UNSUPPORTED),
627
- * which is why nothing here reads the platform.
628
- */
629
- /** An empty screen reads as broken, so show a little recent history before going live. */
630
- const DEFAULT_TAIL = 20;
631
- const createLogCommand = (operations) => defineSessionCommand({
632
- help: {
633
- summary: "Tail the merged logs of the locally-running application whose root node is <entry>'s default export.",
634
- examples: ["{bin} log src/service.ts", "{bin} log src/service.ts catalog.service"]
635
- },
636
- args: {
637
- positionals: {
638
- entry: positional.string({
639
- brief: "The module whose default export is the application root.",
640
- placeholder: "entry"
641
- }),
642
- address: positional.optionalString({
643
- brief: "Only this service's lines, by its dotted address (catalog.service); every service when absent.",
644
- placeholder: "address"
645
- })
646
- },
647
- flags: {
648
- name: flag.string({
649
- brief: "Override the root node's name — the dev instance's application name.",
650
- placeholder: "name"
651
- }),
652
- tail: flag.number({
653
- brief: `How many trailing history lines to show before live output (default ${String(DEFAULT_TAIL)}).`,
654
- placeholder: "lines",
655
- default: DEFAULT_TAIL
656
- })
657
- }
658
- },
659
- needs: { config: composerSection },
660
- handler: async (args, ctx) => {
661
- const reportLogEvent = (event) => {
662
- ctx.report({
663
- kind: "message",
664
- severity: "warn",
665
- text: event.kind === "stream-failed" ? `Stream failed: ${event.message}` : `Falling behind — dropped the ${String(event.count)} oldest lines.`
666
- });
667
- };
668
- const result = await operations.log({
669
- entry: args.positionals.entry,
670
- name: args.flags.name,
671
- address: args.positionals.address,
672
- tail: args.flags.tail ?? DEFAULT_TAIL,
673
- cwd: ctx.cwd,
674
- signal: ctx.signal,
675
- onEvent: reportLogEvent
676
- }, { configPath: ctx.config.configPath });
677
- if (!result.ok) return notOk(toEngineError(result.failure));
678
- const attached = result.value;
679
- if (attached.services.length === 0) {
680
- ctx.report({
681
- kind: "message",
682
- severity: "warn",
683
- text: `No running services for "${attached.appName}" — start it first with \`dev ${args.positionals.entry}\`.`
684
- });
685
- return ok(void 0);
686
- }
687
- for await (const { service, line } of attached.lines) ctx.report({
688
- kind: "output",
689
- source: service,
690
- channel: "data",
691
- line: `[${service}] ${line}`
692
- });
693
- return ok(void 0);
694
- }
695
- });
696
- /**
697
697
  * Composer's `CommandFamily` — the unit the engine mounts, whether the process
698
698
  * is composer's own CLI or the `prisma` bin.
699
699
  *
@@ -708,9 +708,14 @@ const createLogCommand = (operations) => defineSessionCommand({
708
708
  * scripts/check-family-static-graph.mjs enforces this against BUILT output,
709
709
  * where type-only imports have already been erased.
710
710
  *
711
- * The four commands close over the operations they are given, so a host can
711
+ * The commands close over the operations they are given, so a host can
712
712
  * mount this family against the test double and exercise real grammar, real
713
713
  * arg validation and real handlers with no alchemy behind them.
714
+ *
715
+ * The family ships `deploy` and `dev` only: `destroy` and `log` were retired
716
+ * from the family surface (2026-08-21 PM review), so hosts mounting the family
717
+ * — the `prisma` bin — never see them. They remain first-class commands of
718
+ * composer's own bin, mounted on top of the family in engine-cli.ts.
714
719
  */
715
720
  const realOperations = {
716
721
  deploy: deployWithDeps,
@@ -724,9 +729,7 @@ function createComposerFamily(options = {}) {
724
729
  configSection: composerSection,
725
730
  commands: {
726
731
  deploy: createDeployCommand(operations),
727
- destroy: createDestroyCommand(operations),
728
- dev: createDevCommand(operations),
729
- log: createLogCommand(operations)
732
+ dev: createDevCommand(operations)
730
733
  },
731
734
  docsBaseUrl: DOCS_BASE
732
735
  });
@@ -895,22 +898,27 @@ function createRuntime(host, loadConfig) {
895
898
  const BINARY_NAME = "prisma-composer";
896
899
  /**
897
900
  * Composer's commands mount at the top level of its own CLI (`prisma-composer
898
- * deploy`); under the `prisma` bin the same family mounts one level down
899
- * (`prisma composer deploy`). The family is identical either way only the
900
- * mount paths differ, which is exactly the split `createCli` draws between
901
- * `commandFamilies` and `commands`.
901
+ * deploy`). The bin mounts more than the family carries: `destroy` and `log`
902
+ * were retired from the family (the surface the `prisma` bin mounts, per the
903
+ * 2026-08-21 PM review) but stay first-class commands of `prisma-composer`
904
+ * itself, so their definitions are mounted here on top of the family.
902
905
  */
903
- function mountedTree(family) {
904
- return { ...family.commands };
906
+ function mountedTree(family, operations) {
907
+ return {
908
+ ...family.commands,
909
+ destroy: createDestroyCommand(operations),
910
+ log: createLogCommand(operations)
911
+ };
905
912
  }
906
913
  function createComposerCli(spec) {
907
- const family = createComposerFamily({ operations: spec.operations });
914
+ const operations = spec.operations ?? realOperations;
915
+ const family = createComposerFamily({ operations });
908
916
  return createCli({
909
917
  name: BINARY_NAME,
910
918
  version: spec.version,
911
919
  commandFamilies: [family],
912
920
  groups: {},
913
- commands: mountedTree(family)
921
+ commands: mountedTree(family, operations)
914
922
  });
915
923
  }
916
924
  /**