@prisma/cli 8.0.0-rc.2 → 8.0.0-rc.2-dev.50

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 (2) hide show
  1. package/dist/cli.js +436 -69
  2. package/package.json +5 -5
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import process$1 from "node:process";
4
- import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, claimedIdentity, createCli, credentialWorkspaceId, credentialWorkspaceMismatchError, credentialsRequiredError, defineCommand, defineCommandFamily, defineSessionCommand, emptyServiceTokenError, flag, loadConfig, noSessionForWorkspaceError, positional, telemetryCommandGroup } from "@prisma/cli-engine";
4
+ import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, claimedIdentity, createCli, credentialWorkspaceId, credentialWorkspaceMismatchError, credentialsRequiredError, defineCommand, defineCommandFamily, defineSessionCommand, emptyServiceTokenError, flag, loadConfig, noSessionForWorkspaceError, positional, readActiveAccessToken, telemetryCommandGroup } from "@prisma/cli-engine";
5
5
  import { createComposerFamily } from "@prisma/composer/family";
6
6
  import { ormCommandFamily } from "@prisma/orm-toolchain/cli";
7
7
  import { CliStructuredError, notOk, ok } from "@prisma/cli-engine/protocol";
@@ -848,9 +848,9 @@ function sameWorkspaceId(left, right) {
848
848
  }
849
849
  //#endregion
850
850
  //#region src/auth/token-storage.ts
851
- const REFRESH_LOCK_RETRY_MS = 100;
852
- const REFRESH_LOCK_STALE_MS = 3e4;
853
- const REFRESH_LOCK_WAIT_TIMEOUT_MS = 25e3;
851
+ const REFRESH_LOCK_RETRY_MS$1 = 100;
852
+ const REFRESH_LOCK_STALE_MS$1 = 3e4;
853
+ const REFRESH_LOCK_WAIT_TIMEOUT_MS$1 = 25e3;
854
854
  const EMPTY_AUTH_CONTEXT = {
855
855
  activeWorkspaceId: null,
856
856
  workspaces: {}
@@ -889,7 +889,7 @@ function findTokensForWorkspace(allCredentials, workspaceId) {
889
889
  function tokensEqual(a, b) {
890
890
  return a?.workspaceId === b?.workspaceId && a?.accessToken === b?.accessToken && a?.refreshToken === b?.refreshToken;
891
891
  }
892
- function sleep$1(ms, signal) {
892
+ function sleep$2(ms, signal) {
893
893
  signal?.throwIfAborted();
894
894
  return new Promise((resolve, reject) => {
895
895
  const onAbort = () => {
@@ -1097,8 +1097,8 @@ var FileTokenStorage = class {
1097
1097
  async acquireRefreshLock() {
1098
1098
  const lockId = randomUUID();
1099
1099
  const startedAt = Date.now();
1100
- const retryMs = this.options.lockRetryMs ?? REFRESH_LOCK_RETRY_MS;
1101
- const waitTimeoutMs = this.options.lockWaitTimeoutMs ?? REFRESH_LOCK_WAIT_TIMEOUT_MS;
1100
+ const retryMs = this.options.lockRetryMs ?? REFRESH_LOCK_RETRY_MS$1;
1101
+ const waitTimeoutMs = this.options.lockWaitTimeoutMs ?? REFRESH_LOCK_WAIT_TIMEOUT_MS$1;
1102
1102
  this.signal?.throwIfAborted();
1103
1103
  await fs.mkdir(path.dirname(this.lockFilePath), { recursive: true });
1104
1104
  while (true) {
@@ -1106,7 +1106,7 @@ var FileTokenStorage = class {
1106
1106
  if (await this.tryCreateRefreshLock(lockId)) return lockId;
1107
1107
  if (await this.releaseStaleRefreshLock()) continue;
1108
1108
  this.throwIfRefreshLockWaitTimedOut(startedAt, waitTimeoutMs);
1109
- await sleep$1(retryMs, this.signal);
1109
+ await sleep$2(retryMs, this.signal);
1110
1110
  }
1111
1111
  }
1112
1112
  async tryCreateRefreshLock(lockId) {
@@ -1152,7 +1152,7 @@ var FileTokenStorage = class {
1152
1152
  const stats = await fs.stat(this.lockFilePath).catch(() => null);
1153
1153
  this.signal?.throwIfAborted();
1154
1154
  if (!stats) return null;
1155
- const staleMs = this.options.lockStaleMs ?? REFRESH_LOCK_STALE_MS;
1155
+ const staleMs = this.options.lockStaleMs ?? REFRESH_LOCK_STALE_MS$1;
1156
1156
  return Date.now() - stats.mtimeMs > staleMs ? lockId : null;
1157
1157
  }
1158
1158
  async releaseRefreshLock(lockId) {
@@ -4657,8 +4657,45 @@ const bucketListCommand = defineCommand({
4657
4657
  }
4658
4658
  });
4659
4659
  //#endregion
4660
+ //#region src/lib/ndjson.ts
4661
+ /**
4662
+ * Reads a newline-delimited JSON body line by line.
4663
+ *
4664
+ * Shared by every command that reads an NDJSON log page, so the stream
4665
+ * handling below is written and tested once. The two subtleties are the
4666
+ * reason: a chunk boundary can fall inside a line, and a body can end
4667
+ * without a trailing newline, so the last record arrives only if the
4668
+ * leftover buffer is flushed at `done`.
4669
+ */
4670
+ async function forEachNdjsonRecord(body, onRecord) {
4671
+ const reader = body.getReader();
4672
+ const decoder = new TextDecoder();
4673
+ let buffer = "";
4674
+ try {
4675
+ for (;;) {
4676
+ const { done, value } = await reader.read();
4677
+ if (value) buffer += decoder.decode(value, { stream: true });
4678
+ let newlineIndex = buffer.indexOf("\n");
4679
+ while (newlineIndex !== -1) {
4680
+ const line = buffer.slice(0, newlineIndex).trim();
4681
+ buffer = buffer.slice(newlineIndex + 1);
4682
+ if (line) onRecord(JSON.parse(line));
4683
+ newlineIndex = buffer.indexOf("\n");
4684
+ }
4685
+ if (done) {
4686
+ const tail = buffer.trim();
4687
+ if (tail) onRecord(JSON.parse(tail));
4688
+ return;
4689
+ }
4690
+ }
4691
+ } finally {
4692
+ await reader.cancel().catch(() => void 0);
4693
+ reader.releaseLock();
4694
+ }
4695
+ }
4696
+ //#endregion
4660
4697
  //#region src/commands/build/logs.ts
4661
- const TRAILING_NEWLINE = /\n$/;
4698
+ const TRAILING_NEWLINE$1 = /\n$/;
4662
4699
  function buildNotFoundError(buildId) {
4663
4700
  return new CliStructuredError("BUILD.NOT_FOUND", `Build ${buildId} was not found`, {
4664
4701
  why: "The build does not exist, or your workspace does not have access to it.",
@@ -4706,40 +4743,13 @@ function buildFailedError(buildId, record) {
4706
4743
  }] : []]
4707
4744
  });
4708
4745
  }
4709
- /** Reads a newline-delimited JSON body line by line. */
4710
- async function forEachNdjsonRecord(body, onRecord) {
4711
- const reader = body.getReader();
4712
- const decoder = new TextDecoder();
4713
- let buffer = "";
4714
- try {
4715
- for (;;) {
4716
- const { done, value } = await reader.read();
4717
- if (value) buffer += decoder.decode(value, { stream: true });
4718
- let newlineIndex = buffer.indexOf("\n");
4719
- while (newlineIndex !== -1) {
4720
- const line = buffer.slice(0, newlineIndex).trim();
4721
- buffer = buffer.slice(newlineIndex + 1);
4722
- if (line) onRecord(JSON.parse(line));
4723
- newlineIndex = buffer.indexOf("\n");
4724
- }
4725
- if (done) {
4726
- const tail = buffer.trim();
4727
- if (tail) onRecord(JSON.parse(tail));
4728
- return;
4729
- }
4730
- }
4731
- } finally {
4732
- await reader.cancel().catch(() => void 0);
4733
- reader.releaseLock();
4734
- }
4735
- }
4736
4746
  function reportRecord(ctx, record) {
4737
4747
  if (record.type === "log") {
4738
4748
  ctx.report({
4739
4749
  kind: "output",
4740
4750
  source: "build",
4741
4751
  channel: record.source === "stderr" || record.level === "error" ? "diagnostic" : "data",
4742
- line: record.text.replace(TRAILING_NEWLINE, ""),
4752
+ line: record.text.replace(TRAILING_NEWLINE$1, ""),
4743
4753
  data: {
4744
4754
  cursor: record.cursor,
4745
4755
  level: record.level,
@@ -11257,6 +11267,30 @@ function deploymentNotFoundError(deploymentId) {
11257
11267
  nextActions: [runCommandAction("Choose an available deployment id", "service deployment list")]
11258
11268
  });
11259
11269
  }
11270
+ /** `--tail` and `--from-start` ask for opposite ends of the log, so a
11271
+ * run naming both has no answer to give. Refused before any work. */
11272
+ function logsRangeConflictError() {
11273
+ return new CliStructuredError("SERVICE.LOGS_RANGE_CONFLICT", "Choose one end of the log to read from", {
11274
+ why: "--tail and --from-start are mutually exclusive: one reads the last lines, the other reads from the beginning.",
11275
+ nextActions: [adviceAction("Pass --tail <n> for the last n lines, or --from-start for the whole log.")]
11276
+ });
11277
+ }
11278
+ /** A deployment id resolves globally, so one that exists but belongs to
11279
+ * another project is its own failure — not "not found". */
11280
+ function deploymentOutsideProjectError(deploymentId) {
11281
+ return new CliStructuredError("SERVICE.DEPLOYMENT_OUTSIDE_PROJECT", `Deployment "${deploymentId}" belongs to another project`, {
11282
+ why: "The deployment exists, but the service that owns it is not in the resolved project.",
11283
+ nextActions: [adviceAction("Pass --project for the project that owns it."), runCommandAction("List services", "service list")]
11284
+ });
11285
+ }
11286
+ /** The deployment exists but names no owning service, so there is no
11287
+ * project to check it against and nothing to scope logs by. */
11288
+ function deploymentDetachedError(deploymentId) {
11289
+ return new CliStructuredError("SERVICE.DEPLOYMENT_DETACHED", `Deployment "${deploymentId}" has no owning service`, {
11290
+ why: "The Management API returned the deployment without a service, so it cannot be scoped to a project.",
11291
+ nextActions: [runCommandAction("Show the deployment", `service deployment show ${deploymentId}`)]
11292
+ });
11293
+ }
11260
11294
  function deploymentNotFoundForServiceError(deploymentId, serviceName) {
11261
11295
  return new CliStructuredError("SERVICE.DEPLOYMENT_NOT_FOUND", `Deployment "${deploymentId}" not found for service "${serviceName}"`, {
11262
11296
  why: "The requested deployment does not belong to the resolved service or is no longer available.",
@@ -12414,12 +12448,13 @@ async function resolveServiceReadState(ctx, options) {
12414
12448
  });
12415
12449
  const projectId = target.project.id;
12416
12450
  const stateStore = await openServiceStateStore(ctx);
12451
+ const services = await listServices(ctx, provider, projectId, target.branch.name);
12417
12452
  return {
12418
12453
  provider,
12419
12454
  stateStore,
12420
12455
  target,
12421
12456
  projectId,
12422
- selected: await resolveExistingServiceSelection(ctx, stateStore, projectId, await listServices(ctx, provider, projectId, target.branch.name), options.serviceName ?? compute.configServiceName)
12457
+ selected: options.skipSelection ? null : await resolveExistingServiceSelection(ctx, stateStore, projectId, services, options.serviceName ?? compute.configServiceName)
12423
12458
  };
12424
12459
  }
12425
12460
  async function resolveServiceDomainTarget(ctx, options) {
@@ -13325,7 +13360,7 @@ const serviceDomainShowCommand = defineCommand({
13325
13360
  //#endregion
13326
13361
  //#region src/commands/service/domain-wait.ts
13327
13362
  const DEFAULT_TIMEOUT_MS = 900 * 1e3;
13328
- const DEFAULT_POLL_INTERVAL_MS = 5e3;
13363
+ const DEFAULT_POLL_INTERVAL_MS$1 = 5e3;
13329
13364
  const UNIT_MULTIPLIER_MS = {
13330
13365
  ms: 1,
13331
13366
  s: 1e3,
@@ -13340,13 +13375,13 @@ function parseWaitTimeout(value) {
13340
13375
  if (!match) throw timeoutInvalidError(value);
13341
13376
  return Number.parseInt(match[1], 10) * (UNIT_MULTIPLIER_MS[match[2]] ?? 1);
13342
13377
  }
13343
- function pollIntervalMs(ctx) {
13378
+ function pollIntervalMs$1(ctx) {
13344
13379
  const raw = ctx.env.PRISMA_CLI_DOMAIN_WAIT_POLL_MS;
13345
- if (!raw) return DEFAULT_POLL_INTERVAL_MS;
13380
+ if (!raw) return DEFAULT_POLL_INTERVAL_MS$1;
13346
13381
  const parsed = Number.parseInt(raw, 10);
13347
- return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_POLL_INTERVAL_MS;
13382
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_POLL_INTERVAL_MS$1;
13348
13383
  }
13349
- async function sleep(milliseconds, signal) {
13384
+ async function sleep$1(milliseconds, signal) {
13350
13385
  if (milliseconds <= 0) return;
13351
13386
  signal.throwIfAborted();
13352
13387
  await new Promise((resolve, reject) => {
@@ -13392,7 +13427,7 @@ const serviceDomainWaitCommand = defineCommand({
13392
13427
  const domain = await resolveDomainByHostname(target.provider, target.service.id, hostname, "wait", ctx.signal);
13393
13428
  const start = Date.now();
13394
13429
  const deadline = start + timeoutMs;
13395
- const interval = pollIntervalMs(ctx);
13430
+ const interval = pollIntervalMs$1(ctx);
13396
13431
  let previousStatus = null;
13397
13432
  let current = domain;
13398
13433
  for (;;) {
@@ -13418,7 +13453,7 @@ const serviceDomainWaitCommand = defineCommand({
13418
13453
  }
13419
13454
  if (current.status === "failed") throw domainVerificationFailedError(hostname, current);
13420
13455
  if (timeoutMs === 0 || Date.now() >= deadline) throw domainVerificationTimeoutError(hostname, current.status);
13421
- await sleep(Math.min(interval, Math.max(deadline - Date.now(), 0)), ctx.signal);
13456
+ await sleep$1(Math.min(interval, Math.max(deadline - Date.now(), 0)), ctx.signal);
13422
13457
  current = await target.provider.showDomain(current.id, { signal: ctx.signal }).catch((error) => {
13423
13458
  throw domainCommandError("wait", error, hostname);
13424
13459
  });
@@ -13461,6 +13496,264 @@ const serviceListCommand = defineCommand({
13461
13496
  }
13462
13497
  });
13463
13498
  //#endregion
13499
+ //#region src/commands/service/logs.ts
13500
+ const TRAILING_NEWLINE = /\n$/;
13501
+ /** The endpoint's own default page size, restated so `--tail` and the
13502
+ * unflagged run send the same shape of request. */
13503
+ const DEFAULT_TAIL = 100;
13504
+ /** Contract: poll every 2s in --follow. Overridable so a test drives the
13505
+ * loop without waiting, the way `service domain wait` does. */
13506
+ const DEFAULT_POLL_INTERVAL_MS = 2e3;
13507
+ function pollIntervalMs(ctx) {
13508
+ const raw = ctx.env.PRISMA_CLI_SERVICE_LOGS_POLL_MS;
13509
+ if (!raw) return DEFAULT_POLL_INTERVAL_MS;
13510
+ const parsed = Number.parseInt(raw, 10);
13511
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : DEFAULT_POLL_INTERVAL_MS;
13512
+ }
13513
+ async function sleep(milliseconds, signal) {
13514
+ if (milliseconds <= 0) {
13515
+ signal.throwIfAborted();
13516
+ return;
13517
+ }
13518
+ signal.throwIfAborted();
13519
+ await new Promise((resolve, reject) => {
13520
+ const onAbort = () => {
13521
+ clearTimeout(timeout);
13522
+ reject(signal.reason);
13523
+ };
13524
+ const timeout = setTimeout(() => {
13525
+ signal.removeEventListener("abort", onAbort);
13526
+ resolve();
13527
+ }, milliseconds);
13528
+ signal.addEventListener("abort", onAbort, { once: true });
13529
+ });
13530
+ }
13531
+ function logsFailedError(deploymentId, status) {
13532
+ return new CliStructuredError("SERVICE.LOGS_FAILED", `Failed to read logs for deployment ${deploymentId}`, {
13533
+ why: `The Management API returned HTTP ${status}.`,
13534
+ meta: { status },
13535
+ nextActions: [adviceAction("Retry the command, or rerun with --log-level verbose for more detail."), runCommandAction("Show the deployment", `service deployment show ${deploymentId}`)]
13536
+ });
13537
+ }
13538
+ /**
13539
+ * The body ended mid-page, without the terminal record that closes one.
13540
+ * Distinct from SERVICE.LOGS_NO_CURSOR, which is a page that closed
13541
+ * properly and said there is nothing to resume from: this one is an
13542
+ * incomplete read, and the lines already printed are not the whole page.
13543
+ */
13544
+ function logsIncompleteError(deploymentId) {
13545
+ return new CliStructuredError("SERVICE.LOGS_INCOMPLETE", `Incomplete log page for deployment ${deploymentId}`, {
13546
+ why: "The response ended without the record that closes a page, so the lines shown may be only part of it.",
13547
+ nextActions: [adviceAction("Rerun the command to read the page again.")]
13548
+ });
13549
+ }
13550
+ /** An error terminal record is the platform reporting that the log read
13551
+ * itself failed, so it settles the run rather than printing. */
13552
+ function logStreamFailedError(deploymentId, record) {
13553
+ return new CliStructuredError("SERVICE.LOGS_FAILED", `Log stream failed for deployment ${deploymentId}`, {
13554
+ why: record.message,
13555
+ meta: {
13556
+ code: record.code,
13557
+ retryable: record.retryable,
13558
+ ...record.cursor === null ? {} : { cursor: record.cursor }
13559
+ },
13560
+ nextActions: [runCommandAction("Show the deployment", `service deployment show ${deploymentId}`)]
13561
+ });
13562
+ }
13563
+ function listDeployments(ctx, provider, serviceId) {
13564
+ return provider.listDeployments(serviceId, { signal: ctx.signal }).catch((error) => {
13565
+ throw deployFailedError("Failed to list service deployments", error, [runCommandAction("List deployments", "service deployment list")]);
13566
+ });
13567
+ }
13568
+ /** `--deployment <id>`: the id is global, so it is resolved directly and
13569
+ * then checked against the resolved project — a deployment that exists
13570
+ * but belongs elsewhere is reported as its own failure. */
13571
+ async function resolveExplicitDeployment(ctx, state, serviceName, deploymentId) {
13572
+ if (serviceName) {
13573
+ if (!state.selected) throw noDeploymentsError("No deployments available to read logs from", "The resolved project does not have any deployed service yet.");
13574
+ const deploymentsResult = await listDeployments(ctx, state.provider, state.selected.id);
13575
+ const deployment = requireDeploymentForService(deploymentsResult.deployments, deploymentId, state.selected.name);
13576
+ await rememberSelectedService(state.stateStore, state.projectId, deploymentsResult.app);
13577
+ return {
13578
+ service: deploymentsResult.app,
13579
+ deployment
13580
+ };
13581
+ }
13582
+ const shown = await state.provider.showDeployment(deploymentId, { signal: ctx.signal }).catch((error) => {
13583
+ throw deployFailedError("Failed to show deployment", error, [runCommandAction("List deployments", "service deployment list")]);
13584
+ });
13585
+ if (!shown) throw deploymentNotFoundError(deploymentId);
13586
+ if (!shown.app) throw deploymentDetachedError(deploymentId);
13587
+ const owning = (await listServices(ctx, state.provider, state.projectId, state.target.branch.name)).find((service) => service.id === shown.app?.id);
13588
+ if (!owning) throw deploymentOutsideProjectError(deploymentId);
13589
+ await rememberSelectedService(state.stateStore, state.projectId, owning);
13590
+ return {
13591
+ service: owning,
13592
+ deployment: shown.deployment
13593
+ };
13594
+ }
13595
+ /** No `--deployment`: read whatever is live for the selected service. */
13596
+ async function resolveLiveDeployment(ctx, state) {
13597
+ if (!state.selected) throw noDeploymentsError("No deployments available to read logs from", "The resolved project does not have any deployed service yet.");
13598
+ const deploymentsResult = await listDeployments(ctx, state.provider, state.selected.id);
13599
+ const currentLiveDeploymentId = resolveCurrentLiveDeploymentId(deploymentsResult.app, deploymentsResult.deployments);
13600
+ const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId);
13601
+ const deployment = currentLiveDeploymentId ? deployments.find((candidate) => candidate.id === currentLiveDeploymentId) ?? null : null;
13602
+ await rememberSelectedService(state.stateStore, state.projectId, deploymentsResult.app);
13603
+ if (!deployment) throw noDeploymentsError("No deployments available to read logs from", `The selected service "${deploymentsResult.app.name}" does not have a live deployment.`);
13604
+ return {
13605
+ service: deploymentsResult.app,
13606
+ deployment
13607
+ };
13608
+ }
13609
+ /**
13610
+ * Reads one page and reports its log records. Returns the terminal
13611
+ * record that closed it — the caller decides whether that ends the run
13612
+ * or starts the next page.
13613
+ *
13614
+ * Every page ends with a terminal record, so a body that stops without
13615
+ * one was truncated. The lines that did arrive have already been
13616
+ * reported, but the run must not settle as though it had read the whole
13617
+ * page: the user would have a partial log and no way to tell.
13618
+ */
13619
+ async function readPage(ctx, deploymentId, query) {
13620
+ const { data, response } = await ctx.api.GET("/v1/deployments/{deploymentId}/logs", {
13621
+ params: {
13622
+ path: { deploymentId },
13623
+ query
13624
+ },
13625
+ parseAs: "stream",
13626
+ signal: ctx.signal
13627
+ });
13628
+ const body = data;
13629
+ if (!response.ok || !body) {
13630
+ await body?.cancel().catch(() => void 0);
13631
+ throw response.status === 404 ? deploymentNotFoundError(deploymentId) : logsFailedError(deploymentId, response.status);
13632
+ }
13633
+ let terminal = null;
13634
+ await forEachNdjsonRecord(body, (record) => {
13635
+ if (record.type === "terminal") {
13636
+ terminal = record;
13637
+ return;
13638
+ }
13639
+ ctx.report({
13640
+ kind: "output",
13641
+ source: "logs",
13642
+ channel: "data",
13643
+ line: record.text.replace(TRAILING_NEWLINE, ""),
13644
+ data: {
13645
+ byteStart: record.byteStart,
13646
+ byteEnd: record.byteEnd
13647
+ }
13648
+ });
13649
+ });
13650
+ if (terminal === null) throw logsIncompleteError(deploymentId);
13651
+ return terminal;
13652
+ }
13653
+ /**
13654
+ * Following needs somewhere to resume from. Without a cursor the next
13655
+ * request would carry no range at all, the endpoint would apply its
13656
+ * default tail, and the same lines would print again every interval —
13657
+ * silent duplication the user cannot act on. So the run stops and says
13658
+ * why. It settles as an error rather than a clean end because `--follow`
13659
+ * has no successful ending: it runs until interrupted (130) or fails,
13660
+ * and an exit 0 here would be a novel outcome meaning "gave up".
13661
+ */
13662
+ function requireResumeCursor(deploymentId, cursor) {
13663
+ if (cursor === null) throw new CliStructuredError("SERVICE.LOGS_NO_CURSOR", `Cannot follow logs for deployment ${deploymentId}`, {
13664
+ why: "The log page ended without a resume cursor, so there is no point to continue reading from.",
13665
+ nextActions: [adviceAction("Rerun without --follow to read the page, or retry if the deployment is still starting.")]
13666
+ });
13667
+ return cursor;
13668
+ }
13669
+ /**
13670
+ * `--follow`: wait the poll interval, read the next page from the cursor
13671
+ * the last one ended on, repeat until the user interrupts. Never
13672
+ * returns — the run ends by abort (the engine settles 130) or by throw.
13673
+ */
13674
+ async function followPages(ctx, deploymentId, startCursor) {
13675
+ const interval = pollIntervalMs(ctx);
13676
+ let cursor = requireResumeCursor(deploymentId, startCursor);
13677
+ let retriedAfterError = false;
13678
+ for (;;) {
13679
+ await sleep(interval, ctx.signal);
13680
+ const next = await readPage(ctx, deploymentId, { cursor });
13681
+ if (next.kind === "error") {
13682
+ if (!next.retryable || retriedAfterError) throw logStreamFailedError(deploymentId, next);
13683
+ retriedAfterError = true;
13684
+ continue;
13685
+ }
13686
+ retriedAfterError = false;
13687
+ cursor = requireResumeCursor(deploymentId, next.cursor);
13688
+ }
13689
+ }
13690
+ const serviceLogsCommand = defineSessionCommand({
13691
+ help: {
13692
+ summary: "Read logs for a deployment of the service",
13693
+ examples: [
13694
+ "service logs",
13695
+ "service logs --tail 500",
13696
+ "service logs --follow",
13697
+ "service logs --deployment dep_123 --from-start"
13698
+ ]
13699
+ },
13700
+ args: {
13701
+ flags: {
13702
+ service: flag.string({
13703
+ brief: "Service name",
13704
+ placeholder: "name"
13705
+ }),
13706
+ project: flag.string({
13707
+ brief: "Project id or name",
13708
+ placeholder: "id-or-name"
13709
+ }),
13710
+ deployment: flag.string({
13711
+ brief: "Deployment id to read (default: the live deployment)",
13712
+ placeholder: "id"
13713
+ }),
13714
+ tail: flag.number({
13715
+ brief: `Read the last N lines (default ${DEFAULT_TAIL})`,
13716
+ placeholder: "n"
13717
+ }),
13718
+ fromStart: flag.boolean({ brief: "Read from the beginning instead of the last lines" }),
13719
+ follow: flag.boolean({ brief: "Keep polling for new lines until interrupted" })
13720
+ },
13721
+ positionals: { service: positional.optionalString({
13722
+ brief: "Service target from prisma.compute.ts when the config defines multiple services",
13723
+ placeholder: "service"
13724
+ }) }
13725
+ },
13726
+ needs: { credentials: true },
13727
+ handler: async (args, ctx) => {
13728
+ if (args.flags.fromStart && args.flags.tail !== void 0) throw logsRangeConflictError();
13729
+ const serviceNamed = args.flags.service ?? args.positionals.service;
13730
+ const resolveGlobally = Boolean(args.flags.deployment) && !serviceNamed;
13731
+ const state = await resolveServiceReadState(ctx, {
13732
+ ...args.flags.service !== void 0 ? { serviceName: args.flags.service } : {},
13733
+ ...args.flags.project !== void 0 ? { projectRef: args.flags.project } : {},
13734
+ ...args.positionals.service !== void 0 ? { configTarget: args.positionals.service } : {},
13735
+ commandName: "service logs",
13736
+ skipSelection: resolveGlobally
13737
+ });
13738
+ const target = args.flags.deployment ? await resolveExplicitDeployment(ctx, state, serviceNamed, args.flags.deployment) : await resolveLiveDeployment(ctx, state);
13739
+ const deploymentId = target.deployment.id;
13740
+ for (const line of [
13741
+ `project: ${state.projectId}`,
13742
+ `service: ${target.service.name}`,
13743
+ `deployment: ${deploymentId}`
13744
+ ]) ctx.report({
13745
+ kind: "output",
13746
+ source: "logs",
13747
+ channel: "diagnostic",
13748
+ line
13749
+ });
13750
+ const terminal = await readPage(ctx, deploymentId, args.flags.fromStart ? { from_start: "true" } : { tail: args.flags.tail ?? DEFAULT_TAIL });
13751
+ if (terminal.kind === "error") throw logStreamFailedError(deploymentId, terminal);
13752
+ if (!args.flags.follow) return ok(void 0);
13753
+ return followPages(ctx, deploymentId, terminal.cursor);
13754
+ }
13755
+ });
13756
+ //#endregion
13464
13757
  //#region src/commands/service/open.ts
13465
13758
  const serviceOpenCommand = defineCommand({
13466
13759
  help: {
@@ -13714,6 +14007,7 @@ const platformCommandFamily = defineCommandFamily({ commands: {
13714
14007
  gitConnect: gitConnectCommand,
13715
14008
  gitDisconnect: gitDisconnectCommand,
13716
14009
  serviceList: serviceListCommand,
14010
+ serviceLogs: serviceLogsCommand,
13717
14011
  serviceCreate: serviceCreateCommand,
13718
14012
  serviceShow: serviceShowCommand,
13719
14013
  serviceOpen: serviceOpenCommand,
@@ -13816,6 +14110,7 @@ const mountedCommands = {
13816
14110
  "git connect": gitConnectCommand,
13817
14111
  "git disconnect": gitDisconnectCommand,
13818
14112
  "service list": serviceListCommand,
14113
+ "service logs": serviceLogsCommand,
13819
14114
  "service create": serviceCreateCommand,
13820
14115
  "service show": serviceShowCommand,
13821
14116
  "service open": serviceOpenCommand,
@@ -14020,6 +14315,19 @@ const FILE_MODE = 384;
14020
14315
  const LOCK_STALE_MS = 5e3;
14021
14316
  const LOCK_RETRY_MS = 10;
14022
14317
  const LOCK_WAIT_TIMEOUT_MS = 1e4;
14318
+ const REFRESH_LOCK_STALE_MS = 3e4;
14319
+ const REFRESH_LOCK_RETRY_MS = 100;
14320
+ const REFRESH_LOCK_WAIT_TIMEOUT_MS = 3e4;
14321
+ const STATE_LOCK_TIMINGS = {
14322
+ staleMs: LOCK_STALE_MS,
14323
+ retryMs: LOCK_RETRY_MS,
14324
+ waitTimeoutMs: LOCK_WAIT_TIMEOUT_MS
14325
+ };
14326
+ const REFRESH_LOCK_TIMINGS = {
14327
+ staleMs: REFRESH_LOCK_STALE_MS,
14328
+ retryMs: REFRESH_LOCK_RETRY_MS,
14329
+ waitTimeoutMs: REFRESH_LOCK_WAIT_TIMEOUT_MS
14330
+ };
14023
14331
  const EMPTY_STATE = {
14024
14332
  version: 1,
14025
14333
  sessions: [],
@@ -14122,9 +14430,9 @@ async function writeCredentialState(filePath, state) {
14122
14430
  await fs.chmod(filePath, FILE_MODE).catch(() => {});
14123
14431
  }
14124
14432
  var StateLockTimeoutError = class extends CliStructuredError {
14125
- constructor(lockPath) {
14433
+ constructor(lockPath, waitTimeoutMs) {
14126
14434
  super("CLI.CREDENTIALS_LOCKED", "Another prisma process is still updating your credentials.", {
14127
- why: `The credentials lock at ${lockPath} was held for longer than ${LOCK_WAIT_TIMEOUT_MS}ms.`,
14435
+ why: `The credentials lock at ${lockPath} was held for longer than ${waitTimeoutMs}ms.`,
14128
14436
  nextActions: [{
14129
14437
  kind: "user-choice",
14130
14438
  label: "Wait for the other command to finish, then try again."
@@ -14140,8 +14448,20 @@ var StateLockTimeoutError = class extends CliStructuredError {
14140
14448
  * small fixed staleness threshold.
14141
14449
  */
14142
14450
  async function withStateLock(filePath, debug, run) {
14143
- const lockPath = `${filePath}.lock`;
14144
- const lockId = await acquireStateLock(lockPath, debug);
14451
+ return withFileLock(`${filePath}.lock`, debug, STATE_LOCK_TIMINGS, run);
14452
+ }
14453
+ /**
14454
+ * The cross-process lock the delegated refresh holds for its whole
14455
+ * read → exchange → write sequence, so two processes never spend the
14456
+ * same refresh token. Distinct from the state lock: it IS held across
14457
+ * network I/O, so its staleness and wait budgets are larger, and it
14458
+ * uses its own lock path so short mutations are not queued behind it.
14459
+ */
14460
+ async function withRefreshFileLock(filePath, debug, run) {
14461
+ return withFileLock(`${filePath}.refresh-lock`, debug, REFRESH_LOCK_TIMINGS, run);
14462
+ }
14463
+ async function withFileLock(lockPath, debug, timings, run) {
14464
+ const lockId = await acquireStateLock(lockPath, debug, timings);
14145
14465
  debug(`lock acquired ${lockPath}`);
14146
14466
  try {
14147
14467
  return await run();
@@ -14150,15 +14470,15 @@ async function withStateLock(filePath, debug, run) {
14150
14470
  debug(`lock released ${lockPath}`);
14151
14471
  }
14152
14472
  }
14153
- async function acquireStateLock(lockPath, debug) {
14473
+ async function acquireStateLock(lockPath, debug, timings) {
14154
14474
  const lockId = randomUUID();
14155
14475
  const startedAt = Date.now();
14156
14476
  await fs.mkdir(path.dirname(lockPath), { recursive: true });
14157
14477
  while (true) {
14158
14478
  if (await tryCreateStateLock(lockPath, lockId)) return lockId;
14159
- const tookOver = await takeOverStaleStateLock(lockPath, debug);
14160
- if (Date.now() - startedAt >= LOCK_WAIT_TIMEOUT_MS) throw new StateLockTimeoutError(lockPath);
14161
- if (!tookOver) await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
14479
+ const tookOver = await takeOverStaleStateLock(lockPath, debug, timings);
14480
+ if (Date.now() - startedAt >= timings.waitTimeoutMs) throw new StateLockTimeoutError(lockPath, timings.waitTimeoutMs);
14481
+ if (!tookOver) await new Promise((resolve) => setTimeout(resolve, timings.retryMs));
14162
14482
  }
14163
14483
  }
14164
14484
  async function tryCreateStateLock(lockPath, lockId) {
@@ -14184,10 +14504,10 @@ async function tryCreateStateLock(lockPath, lockId) {
14184
14504
  * then run their read-modify-write at once and one update is lost,
14185
14505
  * which is the very thing the lock exists to prevent.
14186
14506
  */
14187
- async function takeOverStaleStateLock(lockPath, debug) {
14507
+ async function takeOverStaleStateLock(lockPath, debug, timings) {
14188
14508
  const stale = await fs.stat(lockPath).catch(() => null);
14189
14509
  if (stale === null) return true;
14190
- if (Date.now() - stale.mtimeMs <= LOCK_STALE_MS) return false;
14510
+ if (Date.now() - stale.mtimeMs <= timings.staleMs) return false;
14191
14511
  const takenPath = `${lockPath}.${randomUUID()}.stale`;
14192
14512
  try {
14193
14513
  await fs.rename(lockPath, takenPath);
@@ -14225,12 +14545,16 @@ function memoryBackedStorage(credential, withRefreshLock) {
14225
14545
  let tokens = {
14226
14546
  workspaceId: credentialWorkspaceId(credential.token) ?? NO_WORKSPACE_CLAIMED,
14227
14547
  accessToken: credential.token,
14228
- refreshToken: credential.refreshToken
14548
+ refreshToken: credential.refreshToken,
14549
+ expiresAt: claimedExpiresAt(credential.token) ?? credential.expiresAt
14229
14550
  };
14230
14551
  return {
14231
14552
  getTokens: async () => tokens,
14232
- setTokens: async (rotated) => {
14233
- tokens = rotated;
14553
+ setTokens: async (rotated, expiresAt) => {
14554
+ tokens = {
14555
+ ...rotated,
14556
+ expiresAt: claimedExpiresAt(rotated.accessToken) ?? expiresAt ?? tokens?.expiresAt
14557
+ };
14234
14558
  },
14235
14559
  clearTokens: async () => {
14236
14560
  tokens = null;
@@ -14249,6 +14573,7 @@ var FileCredentialManager = class {
14249
14573
  #filePath;
14250
14574
  #debug;
14251
14575
  #fetchWorkspaceName;
14576
+ #refreshCredential;
14252
14577
  #pin = { kind: "unresolved" };
14253
14578
  /** Built for one pinned credential. Every mutation that moves the
14254
14579
  * pin discards it, so a command that mutates and then reaches for
@@ -14261,6 +14586,7 @@ var FileCredentialManager = class {
14261
14586
  this.#filePath = resolveStateFilePath(options.env).filePath;
14262
14587
  this.#debug = makeDebugLog(options.env, options.debugWrite);
14263
14588
  this.#fetchWorkspaceName = options.fetchWorkspaceName;
14589
+ this.#refreshCredential = options.refreshCredential;
14264
14590
  this.#debug(`state file ${this.#filePath}`);
14265
14591
  }
14266
14592
  get stateFilePath() {
@@ -14371,14 +14697,13 @@ var FileCredentialManager = class {
14371
14697
  this.#activeStorage ??= this.#buildActiveStorage();
14372
14698
  return this.#activeStorage;
14373
14699
  }
14374
- /** The spawn path's read: the active credential's access token,
14700
+ /** The delegated path's read: the active credential's access token,
14375
14701
  * fresh on every call, never the refresh token. Null when there is
14376
14702
  * no active credential to read — storage exists only once
14377
14703
  * activeCredential() has returned non-null. */
14378
- async activeAccessToken() {
14704
+ async activeAccessToken(options) {
14379
14705
  if (await this.activeCredential() === null) return null;
14380
- const tokens = await (await this.activeCredentialStorage()).getTokens();
14381
- return tokens === null ? null : tokens.accessToken;
14706
+ return readActiveAccessToken(await this.activeCredentialStorage(), this.#refreshCredential, options);
14382
14707
  }
14383
14708
  /** §11.2: which storage is chosen once, when the pin resolves. Each
14384
14709
  * has exactly one source of truth — the file, or process memory. */
@@ -14406,10 +14731,11 @@ var FileCredentialManager = class {
14406
14731
  return {
14407
14732
  workspaceId,
14408
14733
  accessToken: record.token,
14409
- ...record.refreshToken === void 0 ? {} : { refreshToken: record.refreshToken }
14734
+ ...record.refreshToken === void 0 ? {} : { refreshToken: record.refreshToken },
14735
+ ...record.expiresAt === void 0 ? {} : { expiresAt: new Date(record.expiresAt) }
14410
14736
  };
14411
14737
  },
14412
- setTokens: async (tokens) => {
14738
+ setTokens: async (tokens, expiresAt) => {
14413
14739
  this.#debug(`rotation write for session ${workspaceId}`);
14414
14740
  const claimed = credentialWorkspaceId(tokens.accessToken);
14415
14741
  if (claimed !== void 0 && claimed !== workspaceId) throw credentialWorkspaceMismatchError(workspaceId);
@@ -14421,7 +14747,7 @@ var FileCredentialManager = class {
14421
14747
  ...record.name === void 0 ? {} : { name: record.name },
14422
14748
  token: tokens.accessToken,
14423
14749
  ...tokens.refreshToken === void 0 ? {} : { refreshToken: tokens.refreshToken },
14424
- ...expiresAtSlice(tokens.accessToken, void 0)
14750
+ ...expiresAtSlice(tokens.accessToken, expiresAt ?? (record.expiresAt === void 0 ? void 0 : new Date(record.expiresAt)))
14425
14751
  };
14426
14752
  return {
14427
14753
  state: {
@@ -14450,7 +14776,7 @@ var FileCredentialManager = class {
14450
14776
  };
14451
14777
  });
14452
14778
  },
14453
- withRefreshLock: (fn) => this.#withRefreshLock(fn)
14779
+ withRefreshLock: (fn) => this.#withRefreshLock(() => withRefreshFileLock(this.#filePath, this.#debug, fn))
14454
14780
  };
14455
14781
  }
14456
14782
  #withRefreshLock(fn) {
@@ -14586,6 +14912,45 @@ function environmentCredential(token) {
14586
14912
  };
14587
14913
  }
14588
14914
  //#endregion
14915
+ //#region src/auth/refresh.ts
14916
+ const TRAILING_SLASH = /\/$/;
14917
+ const CREDENTIAL_REFRESH_TIMEOUT_MS = 1e4;
14918
+ /** The dumb HTTP adapter behind the engine's delegated-credential policy. */
14919
+ function makeCredentialRefresher(authBaseUrl) {
14920
+ const endpoint = `${authBaseUrl.replace(TRAILING_SLASH, "")}/token`;
14921
+ return async ({ refreshToken, signal }) => {
14922
+ signal.throwIfAborted();
14923
+ const refreshSignal = AbortSignal.any([signal, AbortSignal.timeout(CREDENTIAL_REFRESH_TIMEOUT_MS)]);
14924
+ const response = await fetch(endpoint, {
14925
+ method: "POST",
14926
+ headers: { "content-type": "application/x-www-form-urlencoded" },
14927
+ body: new URLSearchParams({
14928
+ grant_type: "refresh_token",
14929
+ refresh_token: refreshToken,
14930
+ client_id: CLIENT_ID
14931
+ }),
14932
+ signal: refreshSignal
14933
+ });
14934
+ const body = await readBody(response);
14935
+ if (response.status >= 400 && response.status < 500 && body?.error === "invalid_grant") return { kind: "invalid" };
14936
+ if (!response.ok || typeof body?.access_token !== "string" || typeof body.refresh_token !== "string" || typeof body.expires_in !== "number" || !Number.isFinite(body.expires_in) || body.expires_in < 0) throw new Error(`OAuth token refresh failed (status ${String(response.status)})`);
14937
+ return {
14938
+ kind: "success",
14939
+ accessToken: body.access_token,
14940
+ refreshToken: body.refresh_token,
14941
+ expiresAt: new Date(Date.now() + body.expires_in * 1e3)
14942
+ };
14943
+ };
14944
+ }
14945
+ async function readBody(response) {
14946
+ try {
14947
+ const body = await response.json();
14948
+ return typeof body === "object" && body !== null ? body : null;
14949
+ } catch {
14950
+ return null;
14951
+ }
14952
+ }
14953
+ //#endregion
14589
14954
  //#region src/auth/workspace-name.ts
14590
14955
  /** The manager's injected name lookup: a static-token client over the
14591
14956
  * credential just minted. The manager constructs no API client and
@@ -14797,6 +15162,7 @@ async function assembleRuntime(proc) {
14797
15162
  };
14798
15163
  warnOnDeprecatedStateFileEnvVar(proc);
14799
15164
  const apiBaseUrl = getApiBaseUrl(proc.env);
15165
+ const authBaseUrl = getAuthBaseUrl(proc.env);
14800
15166
  return {
14801
15167
  stdout: { write: (text) => {
14802
15168
  proc.stdout.write(text);
@@ -14823,13 +15189,14 @@ async function assembleRuntime(proc) {
14823
15189
  loadConfig: (configPath) => loadConfig(proc.cwd(), configPath),
14824
15190
  credentialManager: new FileCredentialManager({
14825
15191
  env: proc.env,
14826
- fetchWorkspaceName: fetchWorkspaceName(apiBaseUrl)
15192
+ fetchWorkspaceName: fetchWorkspaceName(apiBaseUrl),
15193
+ refreshCredential: makeCredentialRefresher(authBaseUrl)
14827
15194
  }),
14828
15195
  managementApiClientConfig: {
14829
15196
  clientId: CLIENT_ID,
14830
15197
  redirectUri: DEFAULT_REDIRECT_URI,
14831
15198
  apiBaseUrl,
14832
- authBaseUrl: getAuthBaseUrl(proc.env)
15199
+ authBaseUrl
14833
15200
  },
14834
15201
  spawn: spawnChild,
14835
15202
  /** The engine has already decided and composed; the bin only forks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "8.0.0-rc.2",
3
+ "version": "8.0.0-rc.2-dev.50",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "license": "Apache-2.0",
40
40
  "dependencies": {
41
- "@prisma/cli-engine": "8.0.0-rc.2",
41
+ "@prisma/cli-engine": "0.1.1",
42
42
  "@prisma/composer": "0.6.0-dev.16",
43
43
  "@prisma/compute-sdk": "0.39.0",
44
44
  "@prisma/credentials-store": "^7.8.0",
@@ -51,9 +51,9 @@
51
51
  "open": "^11.0.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@repo/cli-conformance": "8.0.0-rc.2",
55
- "@repo/cli-telemetry": "8.0.0-rc.2",
56
- "@repo/tsconfig": "8.0.0-rc.2",
54
+ "@repo/cli-conformance": "8.0.0-rc.2-dev.50",
55
+ "@repo/cli-telemetry": "8.0.0-rc.2-dev.50",
56
+ "@repo/tsconfig": "8.0.0-rc.2-dev.50",
57
57
  "@types/node": "^22.19.19",
58
58
  "tsdown": "^0.21.10",
59
59
  "tsx": "^4.22.4",