@prisma/cli 8.0.0-rc.2 → 8.0.0-rc.2-dev.49
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 +334 -39
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -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 = () => {
|
|
@@ -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) {
|
|
@@ -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,
|
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.49",
|
|
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.49",
|
|
55
|
+
"@repo/cli-telemetry": "8.0.0-rc.2-dev.49",
|
|
56
|
+
"@repo/tsconfig": "8.0.0-rc.2-dev.49",
|
|
57
57
|
"@types/node": "^22.19.19",
|
|
58
58
|
"tsdown": "^0.21.10",
|
|
59
59
|
"tsx": "^4.22.4",
|