From 202950bac3f38407552d81052db7519cc9a781f5 Mon Sep 17 00:00:00 2001 From: Shane Neubauer Date: Fri, 23 Oct 2026 16:17:20 +1100 Subject: [PATCH] Mount Composer's `prisma dev logs` Composer's family gains `dev logs [address]` (composer ADR-0056): what the services of the app `prisma dev` runs from this directory write to stdout and stderr, with `--tail` (default 100) and `--follow` like `service logs`. Mount it beside `dev url` and `dev stop`, mention it in the `dev` group brief, and document the two new codes (DEV.NO_LOGS, DEV.LOGS_UNSUPPORTED) in the error reference. Like `dev url`, it touches only local emulators, so it goes in the e2e EXCLUSIONS with LOCAL_EMULATOR_REASON; Composer's local-dev-detach integration test runs it against real emulators. Needs the @prisma/composer-cli release that ships `dev logs`: until the product-published bump lands, `composerCommandFamily.commands["dev logs"]` is undefined and the mount tests fail. Verified locally against the composer workspace build: cli tests, typecheck and lint pass. --- docs/product/command-principles.md | 2 ++ docs/product/output-conventions.md | 5 +++++ docs/reference/error-reference.md | 22 +++++++++++++------ packages/cli/src/cli.ts | 3 ++- packages/cli/tests/composer-mount.test.ts | 26 +++++++++++++++++++++-- packages/cli/tests/e2e-coverage.test.ts | 1 + packages/cli/tests/mount-coverage.test.ts | 1 + 7 files changed, 50 insertions(+), 10 deletions(-) diff --git a/docs/product/command-principles.md b/docs/product/command-principles.md index f59a8821..912c8b18 100644 --- a/docs/product/command-principles.md +++ b/docs/product/command-principles.md @@ -107,6 +107,8 @@ The scaffolded config imports `definePrismaConfig` from `prisma/config` and spel Resolve a service version and show or stream its logs. +`dev logs` is the local counterpart: it reads the services of the app `prisma dev` runs from the current directory, so it sits under `dev` beside `dev url` and `dev stop` and `service logs` keeps meaning a deployed service. It takes the same `--tail` (default 100) and `--follow` (off by default) as `service logs`, so the two read alike. + ### `delete` and `remove` `delete` destroys a resource; `remove` detaches one thing from another without destroying it. A command that permanently destroys what it targets is spelled `delete` (`project delete`, `service delete`, `postgres delete`); `remove` is reserved for detachment. diff --git a/docs/product/output-conventions.md b/docs/product/output-conventions.md index 5ead36eb..563fd9a4 100644 --- a/docs/product/output-conventions.md +++ b/docs/product/output-conventions.md @@ -690,6 +690,11 @@ In human mode, `app logs` is a special case: raw app log lines are the primary streamed payload and go to stdout so they can be piped or redirected. CLI context, status, decoration, and errors stay on stderr. +`dev logs` follows the same rule for a local app. Without an address it +prefixes each line with `[address] ` so lines from several services stay +attributable in a pipe; with one, lines are bare. In `--json` mode each line is +an `output` frame whose `data` is `{ service, line }`. + ## Data Conventions - ids are opaque and stable diff --git a/docs/reference/error-reference.md b/docs/reference/error-reference.md index ed9fa939..1df274ba 100644 --- a/docs/reference/error-reference.md +++ b/docs/reference/error-reference.md @@ -18,7 +18,7 @@ Namespaces: | `BRANCH` | Branch listing (`prisma branch`) | | `BUCKET` | Bucket and bucket-key management (`prisma bucket`) | | `CLI` | Engine-level invocation: arguments, config loading, prompts, consent, credentials, child processes | -| `DEV` | Running an app on this machine with Composer (`prisma dev`, `prisma dev url`, `prisma dev stop`, `prisma emulator list`) | +| `DEV` | Running an app on this machine with Composer (`prisma dev`, `prisma dev url`, `prisma dev logs`, `prisma dev stop`, `prisma emulator list`) | | `FEEDBACK` | Sending product feedback (`prisma feedback`) | | `GIT` | Git repository connections (`prisma git`) | | `INIT` | Project initialization diagnostics (`prisma init`) | @@ -223,23 +223,31 @@ The typed path routed to no command (stricli's route failure, mapped at the adap ## DEV -These codes come from Composer, the command family behind `prisma dev`, `prisma dev url`, `prisma dev stop` and `prisma emulator list`. A node is one service, database or bucket in your app; its address is its name in the app, with dots for a nested node (`catalog.database`). Each node runs on a local emulator: a program on this machine that stands in for Prisma Cloud. +These codes come from Composer, the command family behind `prisma dev`, `prisma dev url`, `prisma dev logs`, `prisma dev stop` and `prisma emulator list`. A node is one service, database or bucket in your app; its address is its name in the app, with dots for a nested node (`catalog.database`). Each node runs on a local emulator: a program on this machine that stands in for Prisma Cloud. ### DEV.ADDRESS_UNKNOWN -`prisma dev url
` named an address that the app `prisma dev` last ran from this directory does not have. The `why` lists the addresses that are recorded for it. The fix is to pass one of those; a nested node has a dotted address, such as `catalog.database`. Meta: `address`, `known` (the recorded addresses). +`prisma dev url
` or `prisma dev logs
` named an address that the app `prisma dev` last ran from this directory does not have. The `why` lists the addresses that are recorded for it. The fix is to pass one of those; a nested node has a dotted address, such as `catalog.database`. Meta: `address`, `known` (the recorded addresses). ### DEV.EMULATOR_FAILED -A local emulator answered a request with an error. `prisma dev url`, `prisma dev stop` and `prisma dev` raise it when the emulator that runs a node fails a request. In `prisma emulator list` it is not a failure of the command: the emulator is listed with this code and the emulator's message instead of its instances, and the command still exits `0`. Meta: none. +A local emulator answered a request with an error. `prisma dev url`, `prisma dev logs`, `prisma dev stop` and `prisma dev` raise it when the emulator that runs a node fails a request. In `prisma emulator list` it is not a failure of the command: the emulator is listed with this code and the emulator's message instead of its instances, and the command still exits `0`. Meta: none. ### DEV.EMULATOR_UNREACHABLE -A local emulator could not be reached. `prisma dev url` raises it when the emulator that runs the node is not running; the fix is to run `prisma dev ` in this directory, which starts the emulators the app needs. It is also raised when an emulator is registered and its process is alive but it did not accept the connection; the fix then is to run the command again, and if it keeps failing, to end the emulator's process so the next `prisma dev` starts a new one. Meta: `address` and `kind` (the node's address and kind) when the emulator is not running; `url` (the emulator's address) when it did not accept the connection. +A local emulator could not be reached. `prisma dev url` raises it when the emulator that runs the node is not running, and `prisma dev logs` when the emulator that runs the services is not running; the fix is to run `prisma dev ` in this directory, which starts the emulators the app needs. It is also raised when an emulator is registered and its process is alive but it did not accept the connection; the fix then is to run the command again, and if it keeps failing, to end the emulator's process so the next `prisma dev` starts a new one. Meta: `address` and `kind` (the node's address and kind) when the emulator is not running; `url` (the emulator's address) when it did not accept the connection. ### DEV.EMULATORS_DIR_INVALID -Any command that reaches the local emulators (`prisma dev`, `prisma dev url`, `prisma dev stop`, `prisma emulator list`) found the `PRISMA_COMPOSER_EMULATORS_DIR` environment variable set to a relative path. Every process that starts or reaches the local emulators resolves this directory, and a relative path would name a different directory in each working directory. The fix is to set it to an absolute path (`~` is not expanded) or to unset it, which uses `~/.prisma-composer/emulators`. Meta: `variable`, `value`. +Any command that reaches the local emulators (`prisma dev`, `prisma dev url`, `prisma dev logs`, `prisma dev stop`, `prisma emulator list`) found the `PRISMA_COMPOSER_EMULATORS_DIR` environment variable set to a relative path. Every process that starts or reaches the local emulators resolves this directory, and a relative path would name a different directory in each working directory. The fix is to set it to an absolute path (`~` is not expanded) or to unset it, which uses `~/.prisma-composer/emulators`. Meta: `variable`, `value`. + +### DEV.LOGS_UNSUPPORTED + +`prisma dev logs` without `--follow` ran against an app whose Composer packages are older than the Composer in this `prisma` CLI, and they cannot read a service's log without following it. The fix is to upgrade the app's Composer packages (`@prisma/composer`, `@prisma/composer-prisma-cloud`) to the version this CLI uses, or to pass `--follow`. Meta: none. + +### DEV.NO_LOGS + +`prisma dev logs
` named a database or a bucket. Only services have logs: what a service's process writes to stdout and stderr. The fix is to pass a service's address, or no address for every service. Meta: `address`, `kind`. ### DEV.NOT_RUNNING @@ -247,7 +255,7 @@ Any command that reaches the local emulators (`prisma dev`, `prisma dev url`, `p ### DEV.NOT_STARTED_HERE -`prisma dev url` or `prisma dev stop` ran in a directory where `prisma dev` has not run. An app's local services, databases and buckets are recorded in the directory `prisma dev` runs from, and there is no such record here. The fix is to run `prisma dev ` in this directory, or to run the command from the directory where `prisma dev` ran. Meta: `cwd`. +`prisma dev url`, `prisma dev logs` or `prisma dev stop` ran in a directory where `prisma dev` has not run. An app's local services, databases and buckets are recorded in the directory `prisma dev` runs from, and there is no such record here. The fix is to run `prisma dev ` in this directory, or to run the command from the directory where `prisma dev` ran. Meta: `cwd`. ## FEEDBACK diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index b66688a3..9c5caa51 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -348,7 +348,7 @@ export const cliGroups: Readonly< orm: { brief: "Initialize a Prisma ORM project" }, dev: { brief: - "Run your app on this machine, with no Prisma account. Start it, print a node's local URL, stop it", + "Run your app on this machine, with no Prisma account. Start it, print a node's local URL, read its logs, stop it", }, emulator: { brief: @@ -426,6 +426,7 @@ export const mountedCommands: Readonly> = { deploy: composerCommandFamily.commands.deploy, dev: composerCommandFamily.commands.dev, "dev url": composerCommandFamily.commands["dev url"], + "dev logs": composerCommandFamily.commands["dev logs"], "dev stop": composerCommandFamily.commands["dev stop"], "emulator list": composerCommandFamily.commands["emulator list"], // The ORM family. Written out per path: the shell owns the tree diff --git a/packages/cli/tests/composer-mount.test.ts b/packages/cli/tests/composer-mount.test.ts index d95a6cbc..1b5c4950 100644 --- a/packages/cli/tests/composer-mount.test.ts +++ b/packages/cli/tests/composer-mount.test.ts @@ -70,11 +70,22 @@ const ONE_INSTANCE = listing([ { kind: "database", running: false, url: null, instances: [], error: null }, ]); -const COMPOSER_PATHS = ["dev", "dev url", "dev stop", "emulator list"]; +const COMPOSER_PATHS = [ + "dev", + "dev url", + "dev logs", + "dev stop", + "emulator list", +]; function shell(emulators: Listing = listing([])) { const family = createComposerFamily({ - operations: createOperationsDouble().operations, + operations: createOperationsDouble({ + devLogLines: [ + { service: "catalog", line: "catalog ready" }, + { service: "gateway", line: "GET / 200" }, + ], + }).operations, listEmulators: async () => emulators, }); const commands = Object.fromEntries( @@ -105,6 +116,16 @@ describe("Composer's commands in the shell", () => { expect(result.stdout).toBe("http://localhost:3000\n"); }); + it("pipes the log lines from dev logs, labelled by service", async () => { + const all = await shell().run(["dev", "logs"]); + const one = await shell().run(["dev", "logs", "gateway", "--tail", "20"]); + + expect(all.exitCode).toBe(0); + expect(all.stdout).toBe("[catalog] catalog ready\n[gateway] GET / 200\n"); + expect(one.exitCode).toBe(0); + expect(one.stdout).toBe("GET / 200\n"); + }); + it("pipes nothing from dev stop", async () => { const result = await shell().run(["dev", "stop"]); @@ -133,6 +154,7 @@ describe("Composer's commands in the shell", () => { expect(result.exitCode).toBe(0); expect(result.stdout).toContain("prisma-test dev"); expect(result.stdout).toContain("url
"); + expect(result.stdout).toContain("logs"); expect(result.stdout).toContain("stop"); }); }); diff --git a/packages/cli/tests/e2e-coverage.test.ts b/packages/cli/tests/e2e-coverage.test.ts index 46593825..a0b7aaed 100644 --- a/packages/cli/tests/e2e-coverage.test.ts +++ b/packages/cli/tests/e2e-coverage.test.ts @@ -107,6 +107,7 @@ const EXCLUSIONS: Readonly> = { "Provisions real cloud infrastructure for an app entry point, through a child `alchemy deploy`. Standing that up per CI run is neither cheap nor unattended-safe.", dev: "A session command: it runs until SIGINT or SIGTERM, redeploying on file change, so it has no happy path that terminates on its own.", "dev url": LOCAL_EMULATOR_REASON, + "dev logs": LOCAL_EMULATOR_REASON, "dev stop": LOCAL_EMULATOR_REASON, "emulator list": LOCAL_EMULATOR_REASON, }; diff --git a/packages/cli/tests/mount-coverage.test.ts b/packages/cli/tests/mount-coverage.test.ts index 5d86dc13..1a7eddeb 100644 --- a/packages/cli/tests/mount-coverage.test.ts +++ b/packages/cli/tests/mount-coverage.test.ts @@ -100,6 +100,7 @@ const EXPECTED_MOUNT_PATHS: readonly string[] = [ "db verify", "deploy", "dev", + "dev logs", "dev stop", "dev url", "emulator list",