@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.
- package/dist/cli.js +436 -69
- 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$
|
|
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$
|
|
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:
|
|
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 ${
|
|
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
|
-
|
|
14144
|
-
|
|
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 >=
|
|
14161
|
-
if (!tookOver) await new Promise((resolve) => setTimeout(resolve,
|
|
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 <=
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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",
|