percher 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/percher.js +844 -197
- package/dist/package.json +3 -3
- package/dist/src/commands/apps.d.ts.map +1 -1
- package/dist/src/commands/apps.js +17 -1
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/status.d.ts +4 -0
- package/dist/src/commands/status.d.ts.map +1 -0
- package/dist/src/commands/status.js +53 -0
- package/dist/src/commands/status.js.map +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +3 -3
package/dist/bin/percher.js
CHANGED
|
@@ -10237,6 +10237,7 @@ function refineBunRuntime(detected, cwd) {
|
|
|
10237
10237
|
}
|
|
10238
10238
|
// ../core/dist/tarball.js
|
|
10239
10239
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
10240
|
+
import { createHash } from "node:crypto";
|
|
10240
10241
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
10241
10242
|
import { join as join2, relative, sep } from "node:path";
|
|
10242
10243
|
import { Readable } from "node:stream";
|
|
@@ -10287,18 +10288,29 @@ async function createTarball(options) {
|
|
|
10287
10288
|
}
|
|
10288
10289
|
if (options.extraIgnore)
|
|
10289
10290
|
ig.add(options.extraIgnore);
|
|
10290
|
-
const
|
|
10291
|
+
const fileBuffers = [];
|
|
10291
10292
|
for (const rel of walk(options.cwd, options.cwd, ig)) {
|
|
10292
|
-
const
|
|
10293
|
-
|
|
10293
|
+
const data = readFileSync2(join2(options.cwd, rel));
|
|
10294
|
+
fileBuffers.push({ path: rel, size: data.length, data });
|
|
10295
|
+
}
|
|
10296
|
+
fileBuffers.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
10297
|
+
const totalBytes = fileBuffers.reduce((sum, f) => sum + f.size, 0);
|
|
10298
|
+
const contentHasher = createHash("sha256");
|
|
10299
|
+
for (const f of fileBuffers) {
|
|
10300
|
+
contentHasher.update(`PATH:${f.path.length}:${f.path}
|
|
10301
|
+
`);
|
|
10302
|
+
contentHasher.update(`SIZE:${f.size}
|
|
10303
|
+
`);
|
|
10304
|
+
contentHasher.update(f.data);
|
|
10305
|
+
contentHasher.update(`
|
|
10306
|
+
`);
|
|
10294
10307
|
}
|
|
10295
|
-
const
|
|
10308
|
+
const contentHash = contentHasher.digest("hex");
|
|
10296
10309
|
const pack = $pack();
|
|
10297
10310
|
(async () => {
|
|
10298
|
-
for (const file of
|
|
10299
|
-
const data = readFileSync2(join2(options.cwd, file.path));
|
|
10311
|
+
for (const file of fileBuffers) {
|
|
10300
10312
|
await new Promise((resolve, reject) => {
|
|
10301
|
-
pack.entry({ name: file.path, size: data.length }, data, (err) => err ? reject(err) : resolve());
|
|
10313
|
+
pack.entry({ name: file.path, size: file.data.length }, file.data, (err) => err ? reject(err) : resolve());
|
|
10302
10314
|
});
|
|
10303
10315
|
}
|
|
10304
10316
|
pack.finalize();
|
|
@@ -10306,12 +10318,13 @@ async function createTarball(options) {
|
|
|
10306
10318
|
const gzip = createGzip();
|
|
10307
10319
|
const nodeStream = pack.pipe(gzip);
|
|
10308
10320
|
const webStream = Readable.toWeb(nodeStream);
|
|
10309
|
-
const topFiles = [...
|
|
10321
|
+
const topFiles = [...fileBuffers].sort((a, b) => b.size - a.size).slice(0, 5).map((f) => ({ path: f.path, size: f.size }));
|
|
10310
10322
|
return {
|
|
10311
10323
|
stream: webStream,
|
|
10312
10324
|
bytes: totalBytes,
|
|
10313
|
-
fileCount:
|
|
10314
|
-
topFiles
|
|
10325
|
+
fileCount: fileBuffers.length,
|
|
10326
|
+
topFiles,
|
|
10327
|
+
contentHash
|
|
10315
10328
|
};
|
|
10316
10329
|
}
|
|
10317
10330
|
// ../core/dist/env-scan-source.js
|
|
@@ -14652,10 +14665,160 @@ function readPercherTomlAppName(cwd) {
|
|
|
14652
14665
|
return null;
|
|
14653
14666
|
}
|
|
14654
14667
|
}
|
|
14668
|
+
// ../core/dist/event-renderer.js
|
|
14669
|
+
function renderDeployEvent(event) {
|
|
14670
|
+
if (event.stage === "queue") {
|
|
14671
|
+
if (event.status === "started") {
|
|
14672
|
+
return `Queued behind ${event.message ?? "other builds"} — your turn shortly`;
|
|
14673
|
+
}
|
|
14674
|
+
return null;
|
|
14675
|
+
}
|
|
14676
|
+
if (event.stage === "build")
|
|
14677
|
+
return renderBuildEvent(event);
|
|
14678
|
+
if (event.stage === "start") {
|
|
14679
|
+
if (event.status === "started")
|
|
14680
|
+
return "Starting container";
|
|
14681
|
+
if (event.status === "ok")
|
|
14682
|
+
return "Container started";
|
|
14683
|
+
if (event.status === "failed")
|
|
14684
|
+
return "Container failed to start";
|
|
14685
|
+
return null;
|
|
14686
|
+
}
|
|
14687
|
+
if (event.stage === "health") {
|
|
14688
|
+
if (event.status === "started")
|
|
14689
|
+
return "Checking health";
|
|
14690
|
+
if (event.status === "ok")
|
|
14691
|
+
return "Health check passed";
|
|
14692
|
+
if (event.status === "failed")
|
|
14693
|
+
return "Health check failed";
|
|
14694
|
+
return null;
|
|
14695
|
+
}
|
|
14696
|
+
if (event.stage === "swap") {
|
|
14697
|
+
if (event.status === "ok")
|
|
14698
|
+
return "Traffic swapped to new version";
|
|
14699
|
+
if (event.status === "failed")
|
|
14700
|
+
return "Traffic swap failed";
|
|
14701
|
+
return null;
|
|
14702
|
+
}
|
|
14703
|
+
if (event.stage === "canary") {
|
|
14704
|
+
if (event.status === "started")
|
|
14705
|
+
return "Canary monitor running";
|
|
14706
|
+
if (event.status === "ok")
|
|
14707
|
+
return "Canary monitor passed";
|
|
14708
|
+
if (event.status === "failed")
|
|
14709
|
+
return "Canary failed — rolling back";
|
|
14710
|
+
return null;
|
|
14711
|
+
}
|
|
14712
|
+
if (event.stage === "rollback") {
|
|
14713
|
+
if (event.status === "ok")
|
|
14714
|
+
return "Rolled back to previous version";
|
|
14715
|
+
if (event.status === "failed")
|
|
14716
|
+
return "Rollback failed";
|
|
14717
|
+
return null;
|
|
14718
|
+
}
|
|
14719
|
+
return null;
|
|
14720
|
+
}
|
|
14721
|
+
function renderBuildEvent(event) {
|
|
14722
|
+
if (event.subStage === "cache_hit" && event.status === "ok") {
|
|
14723
|
+
return "Build cache hit (reused image)";
|
|
14724
|
+
}
|
|
14725
|
+
if (event.subStage === "deps_install" && event.status === "ok") {
|
|
14726
|
+
return withDuration("Dependencies installed", event.durationMs);
|
|
14727
|
+
}
|
|
14728
|
+
if (event.subStage === "build_command" && event.status === "ok") {
|
|
14729
|
+
return withDuration("Build command finished", event.durationMs);
|
|
14730
|
+
}
|
|
14731
|
+
if (event.subStage === "image_finalize" && event.status === "ok") {
|
|
14732
|
+
return withDuration("Image built", event.durationMs);
|
|
14733
|
+
}
|
|
14734
|
+
if (event.subStage === "docker_layers" && event.status === "ok") {
|
|
14735
|
+
return withDuration("Docker layers built", event.durationMs);
|
|
14736
|
+
}
|
|
14737
|
+
if (event.subStage !== null) {
|
|
14738
|
+
if (event.status === "started")
|
|
14739
|
+
return `${labelForSubStage(event.subStage)}…`;
|
|
14740
|
+
if (event.status === "failed")
|
|
14741
|
+
return `${labelForSubStage(event.subStage)} failed`;
|
|
14742
|
+
return null;
|
|
14743
|
+
}
|
|
14744
|
+
if (event.status === "started")
|
|
14745
|
+
return "Build started";
|
|
14746
|
+
if (event.status === "failed")
|
|
14747
|
+
return "Build failed";
|
|
14748
|
+
return null;
|
|
14749
|
+
}
|
|
14750
|
+
function labelForSubStage(subStage) {
|
|
14751
|
+
switch (subStage) {
|
|
14752
|
+
case "deps_install":
|
|
14753
|
+
return "Installing dependencies";
|
|
14754
|
+
case "build_command":
|
|
14755
|
+
return "Running build command";
|
|
14756
|
+
case "image_finalize":
|
|
14757
|
+
return "Finalizing image";
|
|
14758
|
+
case "docker_layers":
|
|
14759
|
+
return "Building Docker layers";
|
|
14760
|
+
case "cache_hit":
|
|
14761
|
+
return "Reusing cached image";
|
|
14762
|
+
}
|
|
14763
|
+
}
|
|
14764
|
+
function withDuration(label, durationMs) {
|
|
14765
|
+
if (durationMs === null || durationMs <= 0)
|
|
14766
|
+
return label;
|
|
14767
|
+
if (durationMs < 1000)
|
|
14768
|
+
return `${label} (${durationMs}ms)`;
|
|
14769
|
+
const seconds = (durationMs / 1000).toFixed(1);
|
|
14770
|
+
return `${label} (${seconds}s)`;
|
|
14771
|
+
}
|
|
14655
14772
|
// ../core/dist/commands/whoami.js
|
|
14656
14773
|
async function whoami(ctx) {
|
|
14657
14774
|
return ctx.client.auth.whoami();
|
|
14658
14775
|
}
|
|
14776
|
+
// ../core/dist/commands/status.js
|
|
14777
|
+
var statusInputSchema = exports_external.object({
|
|
14778
|
+
json: exports_external.boolean().optional()
|
|
14779
|
+
});
|
|
14780
|
+
async function status(ctx, _input = {}) {
|
|
14781
|
+
let health;
|
|
14782
|
+
try {
|
|
14783
|
+
health = await ctx.client.platform.health();
|
|
14784
|
+
} catch (err) {
|
|
14785
|
+
return {
|
|
14786
|
+
verdict: "unreachable",
|
|
14787
|
+
summary: `API unreachable (${err.message})`,
|
|
14788
|
+
health: null,
|
|
14789
|
+
reachError: err.message
|
|
14790
|
+
};
|
|
14791
|
+
}
|
|
14792
|
+
const verdict = deriveVerdict(health);
|
|
14793
|
+
return {
|
|
14794
|
+
verdict,
|
|
14795
|
+
summary: summarize(verdict, health),
|
|
14796
|
+
health
|
|
14797
|
+
};
|
|
14798
|
+
}
|
|
14799
|
+
function deriveVerdict(health) {
|
|
14800
|
+
if (health.status !== "ok")
|
|
14801
|
+
return "worker-degraded";
|
|
14802
|
+
if (health.worker?.circuitBreaker?.open)
|
|
14803
|
+
return "worker-degraded";
|
|
14804
|
+
if (health.worker?.health && health.worker.health.ok === false)
|
|
14805
|
+
return "worker-degraded";
|
|
14806
|
+
return "operational";
|
|
14807
|
+
}
|
|
14808
|
+
function summarize(verdict, health) {
|
|
14809
|
+
if (verdict === "operational")
|
|
14810
|
+
return "All systems operational";
|
|
14811
|
+
const breaker = health.worker?.circuitBreaker;
|
|
14812
|
+
const workerHealth = health.worker?.health;
|
|
14813
|
+
if (breaker?.open) {
|
|
14814
|
+
return `Worker circuit breaker open (${breaker.failuresInWindow}/${breaker.threshold} failures in last ${Math.round(breaker.windowMs / 1000)}s) — deploys will fail fast until it recovers`;
|
|
14815
|
+
}
|
|
14816
|
+
if (workerHealth && workerHealth.ok === false) {
|
|
14817
|
+
const lastErr = workerHealth.lastErr ? ` — last error: ${workerHealth.lastErr}` : "";
|
|
14818
|
+
return `Worker unreachable (${workerHealth.consecutiveErrs} consecutive failed health checks)${lastErr}`;
|
|
14819
|
+
}
|
|
14820
|
+
return "API responded but reported a non-ok status";
|
|
14821
|
+
}
|
|
14659
14822
|
// ../core/dist/commands/account.js
|
|
14660
14823
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
14661
14824
|
import { join as join4, resolve } from "node:path";
|
|
@@ -14882,9 +15045,13 @@ class Http {
|
|
|
14882
15045
|
const ab = await res.arrayBuffer();
|
|
14883
15046
|
return new Uint8Array(ab);
|
|
14884
15047
|
}
|
|
14885
|
-
async postBinary(path, body) {
|
|
15048
|
+
async postBinary(path, body, extraHeaders) {
|
|
14886
15049
|
const headers = this.buildHeaders();
|
|
14887
15050
|
headers["Content-Type"] = "application/octet-stream";
|
|
15051
|
+
if (extraHeaders) {
|
|
15052
|
+
for (const [k, v] of Object.entries(extraHeaders))
|
|
15053
|
+
headers[k] = v;
|
|
15054
|
+
}
|
|
14888
15055
|
const init = {
|
|
14889
15056
|
method: "POST",
|
|
14890
15057
|
headers,
|
|
@@ -14893,7 +15060,12 @@ class Http {
|
|
|
14893
15060
|
if (body instanceof ReadableStream) {
|
|
14894
15061
|
init.duplex = "half";
|
|
14895
15062
|
}
|
|
14896
|
-
|
|
15063
|
+
let res;
|
|
15064
|
+
try {
|
|
15065
|
+
res = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
15066
|
+
} catch (err) {
|
|
15067
|
+
throw new Error(`network error: ${err.message}`, { cause: err });
|
|
15068
|
+
}
|
|
14897
15069
|
if (!res.ok)
|
|
14898
15070
|
throw await this.parseError(res);
|
|
14899
15071
|
return await res.json();
|
|
@@ -15011,7 +15183,17 @@ class AppsResource {
|
|
|
15011
15183
|
if (input.noCache)
|
|
15012
15184
|
params.set("noCache", "1");
|
|
15013
15185
|
const qs = params.toString();
|
|
15014
|
-
|
|
15186
|
+
const headers = {};
|
|
15187
|
+
if (input.idempotencyKey)
|
|
15188
|
+
headers["Idempotency-Key"] = input.idempotencyKey;
|
|
15189
|
+
if (input.traceId)
|
|
15190
|
+
headers["X-Trace-Id"] = input.traceId;
|
|
15191
|
+
if (input.tarballHash)
|
|
15192
|
+
headers["X-Tarball-Hash"] = input.tarballHash;
|
|
15193
|
+
if (typeof input.retriedAttempts === "number" && input.retriedAttempts > 0) {
|
|
15194
|
+
headers["X-Publish-Attempts"] = String(input.retriedAttempts);
|
|
15195
|
+
}
|
|
15196
|
+
return this.http.postBinary(`/apps/${encodeURIComponent(id)}/deploys${qs ? `?${qs}` : ""}`, input.tarball, headers);
|
|
15015
15197
|
}
|
|
15016
15198
|
promotePreview(appRef, deployId) {
|
|
15017
15199
|
return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/promote`, {});
|
|
@@ -15355,6 +15537,23 @@ class LogsResource {
|
|
|
15355
15537
|
}
|
|
15356
15538
|
}
|
|
15357
15539
|
|
|
15540
|
+
// ../client/dist/resources/platform.js
|
|
15541
|
+
class PlatformResource {
|
|
15542
|
+
http;
|
|
15543
|
+
constructor(http) {
|
|
15544
|
+
this.http = http;
|
|
15545
|
+
}
|
|
15546
|
+
health() {
|
|
15547
|
+
return this.http.get("/health");
|
|
15548
|
+
}
|
|
15549
|
+
preflight() {
|
|
15550
|
+
return this.http.get("/publish/preflight");
|
|
15551
|
+
}
|
|
15552
|
+
cacheProbe(input) {
|
|
15553
|
+
return this.http.post("/publish/cache-probe", input);
|
|
15554
|
+
}
|
|
15555
|
+
}
|
|
15556
|
+
|
|
15358
15557
|
// ../client/dist/client.js
|
|
15359
15558
|
class PercherClient {
|
|
15360
15559
|
apiUrl;
|
|
@@ -15366,6 +15565,7 @@ class PercherClient {
|
|
|
15366
15565
|
auth;
|
|
15367
15566
|
billing;
|
|
15368
15567
|
account;
|
|
15568
|
+
platform;
|
|
15369
15569
|
constructor(options = {}) {
|
|
15370
15570
|
const defaults = loadConfig();
|
|
15371
15571
|
this.apiUrl = options.apiUrl ?? defaults.apiUrl;
|
|
@@ -15383,6 +15583,7 @@ class PercherClient {
|
|
|
15383
15583
|
this.auth = new AuthResource(http);
|
|
15384
15584
|
this.billing = new BillingResource(http);
|
|
15385
15585
|
this.account = new AccountResource(http);
|
|
15586
|
+
this.platform = new PlatformResource(http);
|
|
15386
15587
|
}
|
|
15387
15588
|
}
|
|
15388
15589
|
// ../client/dist/build-env-prefixes.js
|
|
@@ -15636,6 +15837,17 @@ var unsuspendInputSchema = exports_external.object({
|
|
|
15636
15837
|
import { existsSync as existsSync6 } from "node:fs";
|
|
15637
15838
|
import { join as join7 } from "node:path";
|
|
15638
15839
|
|
|
15840
|
+
// ../shared/dist/timeouts.js
|
|
15841
|
+
var TIMEOUTS = {
|
|
15842
|
+
workerBuildSubprocess: 10 * 60000,
|
|
15843
|
+
apiToWorkerBuildFetch: 11 * 60000,
|
|
15844
|
+
clientPublishPoll: 12 * 60000,
|
|
15845
|
+
workerHealthcheck: 5000,
|
|
15846
|
+
watchdogList: 15000,
|
|
15847
|
+
mcpWaitDeployDefault: 30000,
|
|
15848
|
+
mcpWaitDeployMax: 12 * 60000
|
|
15849
|
+
};
|
|
15850
|
+
|
|
15639
15851
|
// ../core/dist/error-classifier.js
|
|
15640
15852
|
function extractEnvVarsFromLog(combined) {
|
|
15641
15853
|
const envVarPattern = /process\.env\.(\w+)\s*(?:is\s+)?(?:undefined|not set)|(?:missing|undefined).*?(\b[A-Z][A-Z0-9_]{2,}\b)/gi;
|
|
@@ -15666,7 +15878,7 @@ var patterns = [
|
|
|
15666
15878
|
}
|
|
15667
15879
|
},
|
|
15668
15880
|
{
|
|
15669
|
-
pattern: /Unable to connect.*?(access the url|cache\.nixos)|Could not resolve host|getaddrinfo ENOTFOUND|Worker \/[\w-]+(\/[\w-]+)? failed \(50\d\)|Failed to fetch.*?worker|deployer\/(run|build) failed|The socket connection was closed unexpectedly|WORKER_UNAVAILABLE
|
|
15881
|
+
pattern: /Unable to connect.*?(access the url|cache\.nixos)|Could not resolve host|getaddrinfo ENOTFOUND|Worker \/[\w-]+(\/[\w-]+)? failed \(50\d\)|Failed to fetch.*?worker|deployer\/(run|build) failed|The socket connection was closed unexpectedly|WORKER_UNAVAILABLE(_PERSISTENT)?:/i,
|
|
15670
15882
|
result: {
|
|
15671
15883
|
title: "Build infrastructure unavailable",
|
|
15672
15884
|
explanation: "The Percher build server couldn't reach an upstream service it needs (Nix cache, worker daemon, or registry). This is a platform-side problem, not your app code. These failures are usually transient and clear within 1–2 minutes.",
|
|
@@ -15907,10 +16119,14 @@ ${buildLog ?? ""}`;
|
|
|
15907
16119
|
async function pollDeployment(opts) {
|
|
15908
16120
|
const terminal = opts.terminal ?? ["live", "failed"];
|
|
15909
16121
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
15910
|
-
const timeoutMs = opts.timeoutMs ??
|
|
16122
|
+
const timeoutMs = opts.timeoutMs ?? TIMEOUTS.clientPublishPoll;
|
|
15911
16123
|
const maxRetries = opts.maxRetries ?? 5;
|
|
15912
16124
|
let deployment = opts.initial;
|
|
15913
16125
|
const startedAt = Date.now();
|
|
16126
|
+
const seenEventIds = new Set;
|
|
16127
|
+
if (opts.onEvent) {
|
|
16128
|
+
await drainNewEvents(opts, deployment.id, seenEventIds);
|
|
16129
|
+
}
|
|
15914
16130
|
while (!terminal.includes(deployment.status)) {
|
|
15915
16131
|
if (Date.now() - startedAt > timeoutMs) {
|
|
15916
16132
|
opts.onTimeout(deployment);
|
|
@@ -15939,9 +16155,204 @@ The deploy may already be live — your network connection dropped mid-poll. Che
|
|
|
15939
16155
|
await new Promise((r) => setTimeout(r, backoff));
|
|
15940
16156
|
}
|
|
15941
16157
|
}
|
|
16158
|
+
if (opts.onEvent) {
|
|
16159
|
+
await drainNewEvents(opts, deployment.id, seenEventIds);
|
|
16160
|
+
}
|
|
15942
16161
|
}
|
|
15943
16162
|
return deployment;
|
|
15944
16163
|
}
|
|
16164
|
+
async function drainNewEvents(opts, deployId, seenEventIds) {
|
|
16165
|
+
if (!opts.onEvent)
|
|
16166
|
+
return;
|
|
16167
|
+
try {
|
|
16168
|
+
const { events } = await opts.ctx.client.apps.getDeployEvents(opts.appId, deployId);
|
|
16169
|
+
for (const event of events) {
|
|
16170
|
+
if (seenEventIds.has(event.id))
|
|
16171
|
+
continue;
|
|
16172
|
+
seenEventIds.add(event.id);
|
|
16173
|
+
opts.onEvent(event);
|
|
16174
|
+
}
|
|
16175
|
+
} catch {}
|
|
16176
|
+
}
|
|
16177
|
+
|
|
16178
|
+
// ../core/dist/publish-retry.js
|
|
16179
|
+
var NON_RETRYABLE_CODES = new Set([
|
|
16180
|
+
"WORKER_UNAVAILABLE_PERSISTENT",
|
|
16181
|
+
"BUILD_FAILED",
|
|
16182
|
+
"BUILD_TIMEOUT",
|
|
16183
|
+
"DEPLOY_STALLED",
|
|
16184
|
+
"REQUIRED_ENV_MISSING",
|
|
16185
|
+
"ENV_KEY_UNDECLARED",
|
|
16186
|
+
"DAILY_QUOTA_EXCEEDED",
|
|
16187
|
+
"RETRY_LIMIT_REACHED",
|
|
16188
|
+
"ACCOUNT_SUSPENDED",
|
|
16189
|
+
"APP_SUSPENDED",
|
|
16190
|
+
"EMAIL_NOT_VERIFIED",
|
|
16191
|
+
"PREVIEW_LIMIT_REACHED",
|
|
16192
|
+
"VALIDATION_ERROR",
|
|
16193
|
+
"PLATFORM_DOWN"
|
|
16194
|
+
]);
|
|
16195
|
+
var WORKER_UNAVAILABLE_BACKOFF_MS = [5000, 15000, 45000];
|
|
16196
|
+
var NETWORK_BACKOFF_MS = [2000, 8000, 24000];
|
|
16197
|
+
var HTTP_5XX_BACKOFF_MS = [5000, 20000];
|
|
16198
|
+
function classifyDeployRetry(err, attempt) {
|
|
16199
|
+
const idx = attempt - 1;
|
|
16200
|
+
if (err instanceof PercherApiError) {
|
|
16201
|
+
if (err.code === "WORKER_UNAVAILABLE_PERSISTENT") {
|
|
16202
|
+
return {
|
|
16203
|
+
retryable: false,
|
|
16204
|
+
delayMs: 0,
|
|
16205
|
+
reason: "platform circuit breaker open — surfacing as incident",
|
|
16206
|
+
category: "persistent"
|
|
16207
|
+
};
|
|
16208
|
+
}
|
|
16209
|
+
if (err.code === "WORKER_UNAVAILABLE") {
|
|
16210
|
+
const delay = WORKER_UNAVAILABLE_BACKOFF_MS[idx];
|
|
16211
|
+
if (delay === undefined) {
|
|
16212
|
+
return {
|
|
16213
|
+
retryable: false,
|
|
16214
|
+
delayMs: 0,
|
|
16215
|
+
reason: "worker unavailable after 3 attempts",
|
|
16216
|
+
category: "worker_unavailable"
|
|
16217
|
+
};
|
|
16218
|
+
}
|
|
16219
|
+
return {
|
|
16220
|
+
retryable: true,
|
|
16221
|
+
delayMs: delay,
|
|
16222
|
+
reason: "worker was restarting",
|
|
16223
|
+
category: "worker_unavailable"
|
|
16224
|
+
};
|
|
16225
|
+
}
|
|
16226
|
+
if (err.code === "DEPLOY_RATE_LIMITED") {
|
|
16227
|
+
if (idx >= 2) {
|
|
16228
|
+
return {
|
|
16229
|
+
retryable: false,
|
|
16230
|
+
delayMs: 0,
|
|
16231
|
+
reason: "rate-limited after 2 attempts",
|
|
16232
|
+
category: "rate_limited"
|
|
16233
|
+
};
|
|
16234
|
+
}
|
|
16235
|
+
const retryAfterSec = typeof err.extra?.retryAfterSec === "number" ? err.extra.retryAfterSec : 5;
|
|
16236
|
+
return {
|
|
16237
|
+
retryable: true,
|
|
16238
|
+
delayMs: Math.min(60000, Math.max(1000, retryAfterSec * 1000)),
|
|
16239
|
+
reason: "per-app rate limit",
|
|
16240
|
+
category: "rate_limited"
|
|
16241
|
+
};
|
|
16242
|
+
}
|
|
16243
|
+
if (err.code === "USER_CONCURRENT_LIMIT") {
|
|
16244
|
+
if (idx >= 1) {
|
|
16245
|
+
return {
|
|
16246
|
+
retryable: false,
|
|
16247
|
+
delayMs: 0,
|
|
16248
|
+
reason: "concurrent-deploy cap still saturated after one wait",
|
|
16249
|
+
category: "rate_limited"
|
|
16250
|
+
};
|
|
16251
|
+
}
|
|
16252
|
+
const retryAfterSec = typeof err.extra?.retryAfterSec === "number" ? err.extra.retryAfterSec : 60;
|
|
16253
|
+
return {
|
|
16254
|
+
retryable: true,
|
|
16255
|
+
delayMs: Math.min(2 * 60000, Math.max(5000, retryAfterSec * 1000)),
|
|
16256
|
+
reason: "per-user concurrent-deploy cap",
|
|
16257
|
+
category: "rate_limited"
|
|
16258
|
+
};
|
|
16259
|
+
}
|
|
16260
|
+
if (err.code === "PLATFORM_OVERLOADED") {
|
|
16261
|
+
if (idx >= 3) {
|
|
16262
|
+
return {
|
|
16263
|
+
retryable: false,
|
|
16264
|
+
delayMs: 0,
|
|
16265
|
+
reason: "platform overloaded after 3 attempts",
|
|
16266
|
+
category: "rate_limited"
|
|
16267
|
+
};
|
|
16268
|
+
}
|
|
16269
|
+
const retryAfterSec = typeof err.extra?.retryAfterSec === "number" ? err.extra.retryAfterSec : 30;
|
|
16270
|
+
return {
|
|
16271
|
+
retryable: true,
|
|
16272
|
+
delayMs: Math.min(5 * 60000, Math.max(5000, retryAfterSec * 1000)),
|
|
16273
|
+
reason: "platform overloaded — queue-depth backpressure",
|
|
16274
|
+
category: "rate_limited"
|
|
16275
|
+
};
|
|
16276
|
+
}
|
|
16277
|
+
if (NON_RETRYABLE_CODES.has(err.code)) {
|
|
16278
|
+
return {
|
|
16279
|
+
retryable: false,
|
|
16280
|
+
delayMs: 0,
|
|
16281
|
+
reason: `non-retryable: ${err.code}`,
|
|
16282
|
+
category: "non_retryable"
|
|
16283
|
+
};
|
|
16284
|
+
}
|
|
16285
|
+
if (err.status >= 500 && err.status < 600) {
|
|
16286
|
+
const delay = HTTP_5XX_BACKOFF_MS[idx];
|
|
16287
|
+
if (delay === undefined) {
|
|
16288
|
+
return {
|
|
16289
|
+
retryable: false,
|
|
16290
|
+
delayMs: 0,
|
|
16291
|
+
reason: `HTTP ${err.status} after 2 attempts`,
|
|
16292
|
+
category: "http_5xx"
|
|
16293
|
+
};
|
|
16294
|
+
}
|
|
16295
|
+
return {
|
|
16296
|
+
retryable: true,
|
|
16297
|
+
delayMs: delay,
|
|
16298
|
+
reason: `HTTP ${err.status}`,
|
|
16299
|
+
category: "http_5xx"
|
|
16300
|
+
};
|
|
16301
|
+
}
|
|
16302
|
+
return {
|
|
16303
|
+
retryable: false,
|
|
16304
|
+
delayMs: 0,
|
|
16305
|
+
reason: `HTTP ${err.status} ${err.code}`,
|
|
16306
|
+
category: "non_retryable"
|
|
16307
|
+
};
|
|
16308
|
+
}
|
|
16309
|
+
if (err instanceof Error && /network error|fetch failed|ECONNRESET|ENOTFOUND|ETIMEDOUT/i.test(err.message)) {
|
|
16310
|
+
const delay = NETWORK_BACKOFF_MS[idx];
|
|
16311
|
+
if (delay === undefined) {
|
|
16312
|
+
return {
|
|
16313
|
+
retryable: false,
|
|
16314
|
+
delayMs: 0,
|
|
16315
|
+
reason: "network error after 3 attempts",
|
|
16316
|
+
category: "network"
|
|
16317
|
+
};
|
|
16318
|
+
}
|
|
16319
|
+
return {
|
|
16320
|
+
retryable: true,
|
|
16321
|
+
delayMs: delay,
|
|
16322
|
+
reason: "network blip",
|
|
16323
|
+
category: "network"
|
|
16324
|
+
};
|
|
16325
|
+
}
|
|
16326
|
+
return {
|
|
16327
|
+
retryable: false,
|
|
16328
|
+
delayMs: 0,
|
|
16329
|
+
reason: "unknown error",
|
|
16330
|
+
category: "non_retryable"
|
|
16331
|
+
};
|
|
16332
|
+
}
|
|
16333
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16334
|
+
async function runDeployWithRetry(opts) {
|
|
16335
|
+
const max = opts.maxAttempts ?? 4;
|
|
16336
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
16337
|
+
let attempt = 0;
|
|
16338
|
+
let lastErr;
|
|
16339
|
+
while (attempt < max) {
|
|
16340
|
+
attempt += 1;
|
|
16341
|
+
try {
|
|
16342
|
+
const result = await opts.call(attempt);
|
|
16343
|
+
return { result, attempts: attempt };
|
|
16344
|
+
} catch (err) {
|
|
16345
|
+
lastErr = err;
|
|
16346
|
+
const decision = classifyDeployRetry(err, attempt);
|
|
16347
|
+
if (!decision.retryable || attempt >= max) {
|
|
16348
|
+
throw err;
|
|
16349
|
+
}
|
|
16350
|
+
opts.onRetry?.({ attempt, decision, err });
|
|
16351
|
+
await sleep(decision.delayMs);
|
|
16352
|
+
}
|
|
16353
|
+
}
|
|
16354
|
+
throw lastErr;
|
|
16355
|
+
}
|
|
15945
16356
|
|
|
15946
16357
|
// ../core/dist/recovery.js
|
|
15947
16358
|
function recoveryNone(opts = {}) {
|
|
@@ -15969,7 +16380,7 @@ function recoveryWait(opts) {
|
|
|
15969
16380
|
args: {
|
|
15970
16381
|
app: opts.app,
|
|
15971
16382
|
deployId: opts.deployId,
|
|
15972
|
-
timeoutSeconds: opts.timeoutSeconds ??
|
|
16383
|
+
timeoutSeconds: opts.timeoutSeconds ?? TIMEOUTS.mcpWaitDeployDefault / 1000
|
|
15973
16384
|
},
|
|
15974
16385
|
reasonCode: opts.reasonCode
|
|
15975
16386
|
};
|
|
@@ -16299,12 +16710,21 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
16299
16710
|
}
|
|
16300
16711
|
}
|
|
16301
16712
|
ctx.status("Uploading...");
|
|
16713
|
+
const tarballBytes = new Uint8Array(await new Response(tarball.stream).arrayBuffer());
|
|
16714
|
+
const idempotencyKey = crypto.randomUUID();
|
|
16302
16715
|
let initialDeployment;
|
|
16303
16716
|
try {
|
|
16304
|
-
const deployResponse = await
|
|
16305
|
-
|
|
16306
|
-
|
|
16307
|
-
|
|
16717
|
+
const { result: deployResponse } = await runDeployWithRetry({
|
|
16718
|
+
call: (attempt) => ctx.client.apps.deploy(app.id, {
|
|
16719
|
+
tarball: tarballBytes,
|
|
16720
|
+
type: input.preview ? "preview" : undefined,
|
|
16721
|
+
noCache: input.noCache,
|
|
16722
|
+
idempotencyKey,
|
|
16723
|
+
retriedAttempts: attempt - 1
|
|
16724
|
+
}),
|
|
16725
|
+
onRetry: ({ attempt, decision }) => {
|
|
16726
|
+
ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
|
|
16727
|
+
}
|
|
16308
16728
|
});
|
|
16309
16729
|
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
16310
16730
|
throw new PercherCoreError(`${app.name} already has a deploy in progress (${deployResponse.deployId}, ${deployResponse.deployStatus}).`, {
|
|
@@ -16386,7 +16806,7 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
16386
16806
|
throw err;
|
|
16387
16807
|
}
|
|
16388
16808
|
const pollInterval = internals.pollIntervalMs ?? 2000;
|
|
16389
|
-
const timeoutMs = internals.timeoutMs ??
|
|
16809
|
+
const timeoutMs = internals.timeoutMs ?? TIMEOUTS.clientPublishPoll;
|
|
16390
16810
|
const startedAt = Date.now();
|
|
16391
16811
|
const deployment = await pollDeployment({
|
|
16392
16812
|
ctx,
|
|
@@ -16541,6 +16961,82 @@ function* walkDir(dir, base) {
|
|
|
16541
16961
|
|
|
16542
16962
|
// ../core/dist/commands/publish-failure.js
|
|
16543
16963
|
import { join as join9 } from "node:path";
|
|
16964
|
+
|
|
16965
|
+
// ../core/dist/structured-error-codes.js
|
|
16966
|
+
var MAPPINGS = {
|
|
16967
|
+
WORKER_UNAVAILABLE: {
|
|
16968
|
+
errorClass: "infra_unavailable",
|
|
16969
|
+
phase: "infra",
|
|
16970
|
+
cause: "worker_fetch_failed",
|
|
16971
|
+
title: "Build worker is unreachable",
|
|
16972
|
+
explanation: (raw) => raw ? `The Percher build worker did not respond in time: ${trimMessage(raw)}. This is a platform-side issue, not your code.` : "The Percher build worker did not respond in time. This is a platform-side issue, not your code.",
|
|
16973
|
+
suggestion: "Wait 30 seconds and run `percher publish` again. If retry #2 also fails, pause 5 minutes before the next attempt — back-to-back retries during a partial outage just waste your time. After 3 spaced-out retries, check status.percher.app or contact support. Your build never started."
|
|
16974
|
+
},
|
|
16975
|
+
WORKER_UNAVAILABLE_PERSISTENT: {
|
|
16976
|
+
errorClass: "infra_unavailable",
|
|
16977
|
+
phase: "infra",
|
|
16978
|
+
cause: "worker_circuit_breaker_open",
|
|
16979
|
+
title: "Build worker is in a crash loop",
|
|
16980
|
+
explanation: () => "The Percher build worker has failed repeatedly in the last minute and the platform's circuit breaker has flipped open. Retrying immediately would just queue more failures.",
|
|
16981
|
+
suggestion: "Wait 2-3 minutes for the platform to recover, then retry. If it persists past 5 minutes check status.percher.app or contact support — this is a platform incident, not your code."
|
|
16982
|
+
},
|
|
16983
|
+
BUILD_TIMEOUT: {
|
|
16984
|
+
errorClass: "build_failed",
|
|
16985
|
+
phase: "build",
|
|
16986
|
+
cause: "build_timeout",
|
|
16987
|
+
title: "Build timed out",
|
|
16988
|
+
explanation: () => "The build ran longer than the 10-minute platform limit and was aborted.",
|
|
16989
|
+
suggestion: "Common causes: a slow `postinstall` hook, very large dependency tree, or a Dockerfile that pulls a huge base image. Try `bun install` locally and check what's slow. If it's legitimately a heavy build, contact support to discuss raising the limit for your account."
|
|
16990
|
+
},
|
|
16991
|
+
BUILD_FAILED: {
|
|
16992
|
+
errorClass: "build_failed",
|
|
16993
|
+
phase: "build",
|
|
16994
|
+
cause: "build_nonzero_exit",
|
|
16995
|
+
title: "Build failed",
|
|
16996
|
+
explanation: (raw) => raw ? `The build process returned a non-zero exit code: ${trimMessage(raw)}. Read the build log below for the exact error.` : "The build process returned a non-zero exit code. Read the build log below for the exact error.",
|
|
16997
|
+
suggestion: "Run your build locally to reproduce: `bun install && bun run build`. The error in the build log usually points to a missing dependency, a TypeScript/syntax error, or a misconfigured framework setting."
|
|
16998
|
+
},
|
|
16999
|
+
WORKER_ERROR: {
|
|
17000
|
+
errorClass: "infra_unavailable",
|
|
17001
|
+
phase: "infra",
|
|
17002
|
+
cause: "worker_internal_error",
|
|
17003
|
+
title: "Platform error inside the build worker",
|
|
17004
|
+
explanation: (raw) => raw ? `The build worker hit an internal error: ${trimMessage(raw)}. This is a Percher bug, not your code.` : "The build worker hit an internal error. This is a Percher bug, not your code.",
|
|
17005
|
+
suggestion: "Retry once — transient platform errors usually clear within a minute. If it repeats, please report it: include the deploy ID and we'll investigate. Your account is not at fault."
|
|
17006
|
+
},
|
|
17007
|
+
DEPLOY_STALLED: {
|
|
17008
|
+
errorClass: "infra_unavailable",
|
|
17009
|
+
phase: "infra",
|
|
17010
|
+
cause: "deploy_stalled",
|
|
17011
|
+
title: "Deploy stalled",
|
|
17012
|
+
explanation: () => "The platform abandoned this deploy — likely because the API or build worker restarted mid-build. Your code is fine; the build never finished.",
|
|
17013
|
+
suggestion: "Run `percher publish` again. The new deploy will start fresh. If you see this repeatedly across multiple publishes, check status.percher.app — it implies a recurring platform issue."
|
|
17014
|
+
}
|
|
17015
|
+
};
|
|
17016
|
+
function classifyByErrorCode(code, rawMessage) {
|
|
17017
|
+
if (!code)
|
|
17018
|
+
return null;
|
|
17019
|
+
const mapping = MAPPINGS[code];
|
|
17020
|
+
if (!mapping)
|
|
17021
|
+
return null;
|
|
17022
|
+
return {
|
|
17023
|
+
title: mapping.title,
|
|
17024
|
+
explanation: mapping.explanation(rawMessage),
|
|
17025
|
+
suggestion: mapping.suggestion,
|
|
17026
|
+
errorClass: mapping.errorClass,
|
|
17027
|
+
phase: mapping.phase,
|
|
17028
|
+
cause: mapping.cause,
|
|
17029
|
+
relevantFiles: [],
|
|
17030
|
+
missingEnvVars: []
|
|
17031
|
+
};
|
|
17032
|
+
}
|
|
17033
|
+
function trimMessage(raw) {
|
|
17034
|
+
const firstLine = raw.split(`
|
|
17035
|
+
`)[0] ?? "";
|
|
17036
|
+
return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine;
|
|
17037
|
+
}
|
|
17038
|
+
|
|
17039
|
+
// ../core/dist/commands/publish-failure.js
|
|
16544
17040
|
function tomlPathFor(cwd) {
|
|
16545
17041
|
return join9(cwd, "percher.toml");
|
|
16546
17042
|
}
|
|
@@ -16564,7 +17060,7 @@ async function buildFailureError(ctx, app, deployment) {
|
|
|
16564
17060
|
buildLogFetchFailed = true;
|
|
16565
17061
|
}
|
|
16566
17062
|
const errorMessage = deployment.errorMessage ?? "";
|
|
16567
|
-
const classified = classifyError("Deployment failed", fullLog) ?? classifyError(errorMessage, "");
|
|
17063
|
+
const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError("Deployment failed", fullLog) ?? classifyError(errorMessage, "");
|
|
16568
17064
|
const rawErrorMessage = errorMessage && !buildLogTail && !classified?.explanation.includes(errorMessage) ? errorMessage : undefined;
|
|
16569
17065
|
const problems = deployment.problems && deployment.problems.length > 0 ? deployment.problems : undefined;
|
|
16570
17066
|
if (classified) {
|
|
@@ -16640,6 +17136,12 @@ async function buildFailureResult(opts) {
|
|
|
16640
17136
|
// ../core/dist/commands/publish-node.js
|
|
16641
17137
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
16642
17138
|
import { join as join10 } from "node:path";
|
|
17139
|
+
|
|
17140
|
+
// ../shared/dist/runtime.js
|
|
17141
|
+
var DEFAULT_NODE_VERSION_MAJOR = "24";
|
|
17142
|
+
var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
|
|
17143
|
+
|
|
17144
|
+
// ../core/dist/commands/publish-node.js
|
|
16643
17145
|
function resolveNodeVersion(cwd) {
|
|
16644
17146
|
const nvmrc = join10(cwd, ".nvmrc");
|
|
16645
17147
|
if (existsSync7(nvmrc)) {
|
|
@@ -16659,14 +17161,14 @@ function resolveNodeVersion(cwd) {
|
|
|
16659
17161
|
return { value: engines, source: "engines.node" };
|
|
16660
17162
|
}
|
|
16661
17163
|
} catch {}
|
|
16662
|
-
return { value:
|
|
17164
|
+
return { value: DEFAULT_NODE_VERSION_LABEL, source: "default" };
|
|
16663
17165
|
}
|
|
16664
17166
|
|
|
16665
17167
|
// ../core/dist/commands/wait-deploy.js
|
|
16666
17168
|
var waitForDeployInputSchema = exports_external.object({
|
|
16667
17169
|
app: exports_external.string().describe("App name (the value of [app].name in percher.toml)"),
|
|
16668
17170
|
deployId: exports_external.string().describe("Deploy id returned by percher_publish (e.g. dep_abc123)"),
|
|
16669
|
-
timeoutSeconds: exports_external.number().int().min(1).max(
|
|
17171
|
+
timeoutSeconds: exports_external.number().int().min(1).max(TIMEOUTS.mcpWaitDeployMax / 1000).optional().describe(`Maximum wall-clock seconds to wait before returning still_running (default ${TIMEOUTS.mcpWaitDeployDefault / 1000}, max ${TIMEOUTS.mcpWaitDeployMax / 1000}). Keeping this short is the point — the agent can resume in another short call without holding a long-lived connection.`),
|
|
16670
17172
|
pollIntervalSeconds: exports_external.number().int().min(1).max(30).optional().describe("Seconds between polls (default 2).")
|
|
16671
17173
|
});
|
|
16672
17174
|
async function resolveReplaced(opts) {
|
|
@@ -16932,121 +17434,185 @@ ${keyList}
|
|
|
16932
17434
|
Fix: bunx percher env set KEY=value
|
|
16933
17435
|
(Re-run with --force to suppress this check.)`);
|
|
16934
17436
|
}
|
|
16935
|
-
const uploadStart = Date.now();
|
|
16936
|
-
ctx.status("[3/4] Uploading...");
|
|
16937
|
-
let deployment;
|
|
16938
17437
|
try {
|
|
16939
|
-
const
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
note: input.message,
|
|
16943
|
-
noCache: input.noCache
|
|
16944
|
-
});
|
|
16945
|
-
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
16946
|
-
return buildAlreadyInProgressResult({
|
|
16947
|
-
active: deployResponse,
|
|
16948
|
-
app,
|
|
16949
|
-
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
16950
|
-
cwd: ctx.cwd
|
|
16951
|
-
});
|
|
16952
|
-
}
|
|
16953
|
-
deployment = deployResponse;
|
|
16954
|
-
} catch (err) {
|
|
16955
|
-
if (err instanceof PercherApiError && err.code === "RETRY_LIMIT_REACHED") {
|
|
16956
|
-
return buildRetryLimitReachedResult({
|
|
16957
|
-
err,
|
|
16958
|
-
app,
|
|
16959
|
-
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
16960
|
-
cwd: ctx.cwd
|
|
16961
|
-
});
|
|
16962
|
-
}
|
|
16963
|
-
if (err instanceof PercherApiError && err.code === "REQUIRED_ENV_MISSING") {
|
|
16964
|
-
const extra = err.extra ?? {};
|
|
16965
|
-
const keys = extra.keys ?? [];
|
|
16966
|
-
const source = extra.source ?? "contract";
|
|
16967
|
-
const sourceText = source === "contract" ? "declared in your [env].required" : source === "discovered" ? "learned from a previous build failure" : "both declared and learned from a previous build failure";
|
|
17438
|
+
const preflight = await ctx.client.platform.preflight();
|
|
17439
|
+
if (!preflight.accept) {
|
|
17440
|
+
const reasonText = preflight.reason === "worker_circuit_breaker_open" ? "platform circuit breaker is open — workers are restarting" : preflight.reason === "worker_unhealthy" ? "build worker is currently unreachable" : "platform is currently unavailable";
|
|
16968
17441
|
return {
|
|
16969
17442
|
status: "failed",
|
|
16970
17443
|
app,
|
|
16971
17444
|
fileCount: tarball.fileCount,
|
|
16972
17445
|
bytes: tarball.bytes,
|
|
16973
17446
|
error: {
|
|
16974
|
-
title: "
|
|
16975
|
-
explanation:
|
|
16976
|
-
suggestion:
|
|
16977
|
-
errorClass: "
|
|
16978
|
-
phase: "config"
|
|
16979
|
-
missingEnvVars: keys
|
|
17447
|
+
title: "Platform unavailable",
|
|
17448
|
+
explanation: `Preflight refused the upload: ${reasonText}.`,
|
|
17449
|
+
suggestion: "Wait ~30s and retry — the platform self-heals automatically and the auto-retry loop will pick it up on the next attempt.",
|
|
17450
|
+
errorClass: "infra_unavailable",
|
|
17451
|
+
phase: "config"
|
|
16980
17452
|
},
|
|
16981
|
-
recovery:
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
16986
|
-
|
|
16987
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
16988
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
16989
|
-
};
|
|
16990
|
-
}
|
|
16991
|
-
if (err instanceof PercherApiError && err.code === "ENV_KEY_UNDECLARED") {
|
|
16992
|
-
const extra = err.extra ?? {};
|
|
16993
|
-
const apiProblems = extra.problems ?? [];
|
|
16994
|
-
const problems = apiProblems.map((p) => ({
|
|
16995
|
-
file: p.file,
|
|
16996
|
-
line: p.line,
|
|
16997
|
-
message: `Source references env key '${p.key}' which is not declared in [env]. Add it to [env].required, [env].optional, or [env].ignore in percher.toml. Context: ${p.context ?? "(unavailable)"}`
|
|
16998
|
-
}));
|
|
16999
|
-
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
17000
|
-
return {
|
|
17001
|
-
status: "failed",
|
|
17002
|
-
app,
|
|
17003
|
-
fileCount: tarball.fileCount,
|
|
17004
|
-
bytes: tarball.bytes,
|
|
17005
|
-
error: {
|
|
17006
|
-
title: "Undeclared env key",
|
|
17007
|
-
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
17008
|
-
suggestion: "Add each key to [env].required (must exist before deploy), [env].optional (may be referenced), or [env].ignore (intentionally unset). See https://percher.app/docs/env for the contract.",
|
|
17009
|
-
errorClass: "config_invalid",
|
|
17010
|
-
phase: "config",
|
|
17011
|
-
relevantFiles: ["percher.toml"]
|
|
17453
|
+
recovery: {
|
|
17454
|
+
retryable: true,
|
|
17455
|
+
nextAction: "retry",
|
|
17456
|
+
suggestedTool: "percher_publish",
|
|
17457
|
+
args: {},
|
|
17458
|
+
reasonCode: "infra_unavailable"
|
|
17012
17459
|
},
|
|
17013
|
-
|
|
17014
|
-
problems,
|
|
17015
|
-
reasonCode: "env_key_undeclared"
|
|
17016
|
-
}),
|
|
17017
|
-
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`,
|
|
17460
|
+
summary: `Preflight refused — ${reasonText}.`,
|
|
17018
17461
|
configPath: tomlPathFor2(ctx.cwd),
|
|
17019
17462
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
17020
17463
|
};
|
|
17021
17464
|
}
|
|
17022
|
-
if (
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
}
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17465
|
+
if (preflight.queueWaitEstimateSec > 60) {
|
|
17466
|
+
ctx.status(`Build queue wait: ~${preflight.queueWaitEstimateSec}s — your build is queued behind ${preflight.queueDepth} other(s).`);
|
|
17467
|
+
}
|
|
17468
|
+
} catch {}
|
|
17469
|
+
const uploadStart = Date.now();
|
|
17470
|
+
ctx.status("[3/4] Uploading...");
|
|
17471
|
+
const tarballBytes = new Uint8Array(await new Response(tarball.stream).arrayBuffer());
|
|
17472
|
+
let cacheReusedDeployment = null;
|
|
17473
|
+
const contentHash = tarball.contentHash;
|
|
17474
|
+
if (!input.noCache) {
|
|
17475
|
+
try {
|
|
17476
|
+
const probe = await ctx.client.platform.cacheProbe({
|
|
17477
|
+
appId: app.id,
|
|
17478
|
+
tarballHash: contentHash
|
|
17479
|
+
});
|
|
17480
|
+
if (probe.hit) {
|
|
17481
|
+
ctx.status(`Build cache: identical bundle deployed previously (${probe.lastDeployId}) — redeploying without re-upload.`);
|
|
17482
|
+
cacheReusedDeployment = await ctx.client.apps.rerunDeploy(app.id, probe.lastDeployId);
|
|
17483
|
+
ctx.status("[4/4] Building & deploying (cache reuse)...");
|
|
17484
|
+
}
|
|
17485
|
+
} catch {}
|
|
17486
|
+
}
|
|
17487
|
+
const idempotencyKey = crypto.randomUUID();
|
|
17488
|
+
let deployment;
|
|
17489
|
+
if (cacheReusedDeployment) {
|
|
17490
|
+
deployment = cacheReusedDeployment;
|
|
17491
|
+
} else {
|
|
17492
|
+
try {
|
|
17493
|
+
const { result: deployResponse, attempts: deployAttempts } = await runDeployWithRetry({
|
|
17494
|
+
call: (attempt) => ctx.client.apps.deploy(app.id, {
|
|
17495
|
+
tarball: tarballBytes,
|
|
17496
|
+
type: input.preview ? "preview" : undefined,
|
|
17497
|
+
note: input.message,
|
|
17498
|
+
noCache: input.noCache,
|
|
17499
|
+
idempotencyKey,
|
|
17500
|
+
tarballHash: contentHash,
|
|
17501
|
+
retriedAttempts: attempt - 1
|
|
17043
17502
|
}),
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
};
|
|
17503
|
+
onRetry: ({ attempt, decision }) => {
|
|
17504
|
+
ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
|
|
17505
|
+
}
|
|
17506
|
+
});
|
|
17507
|
+
if (deployAttempts > 1) {
|
|
17508
|
+
ctx.status(`Recovered after ${deployAttempts - 1} retry${deployAttempts > 2 ? "ies" : ""}.`);
|
|
17509
|
+
}
|
|
17510
|
+
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
17511
|
+
return buildAlreadyInProgressResult({
|
|
17512
|
+
active: deployResponse,
|
|
17513
|
+
app,
|
|
17514
|
+
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
17515
|
+
cwd: ctx.cwd
|
|
17516
|
+
});
|
|
17517
|
+
}
|
|
17518
|
+
deployment = deployResponse;
|
|
17519
|
+
} catch (err) {
|
|
17520
|
+
if (err instanceof PercherApiError && err.code === "RETRY_LIMIT_REACHED") {
|
|
17521
|
+
return buildRetryLimitReachedResult({
|
|
17522
|
+
err,
|
|
17523
|
+
app,
|
|
17524
|
+
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
17525
|
+
cwd: ctx.cwd
|
|
17526
|
+
});
|
|
17527
|
+
}
|
|
17528
|
+
if (err instanceof PercherApiError && err.code === "REQUIRED_ENV_MISSING") {
|
|
17529
|
+
const extra = err.extra ?? {};
|
|
17530
|
+
const keys = extra.keys ?? [];
|
|
17531
|
+
const source = extra.source ?? "contract";
|
|
17532
|
+
const sourceText = source === "contract" ? "declared in your [env].required" : source === "discovered" ? "learned from a previous build failure" : "both declared and learned from a previous build failure";
|
|
17533
|
+
return {
|
|
17534
|
+
status: "failed",
|
|
17535
|
+
app,
|
|
17536
|
+
fileCount: tarball.fileCount,
|
|
17537
|
+
bytes: tarball.bytes,
|
|
17538
|
+
error: {
|
|
17539
|
+
title: "Required env keys not set",
|
|
17540
|
+
explanation: `${keys.length} env ${keys.length === 1 ? "key is" : "keys are"} ${sourceText} but missing from the env store: ${keys.join(", ")}.`,
|
|
17541
|
+
suggestion: `Run \`bunx percher env set ${keys[0] ?? "KEY"}=value\` (and similarly for the rest) and re-publish.`,
|
|
17542
|
+
errorClass: "missing_env",
|
|
17543
|
+
phase: "config",
|
|
17544
|
+
missingEnvVars: keys
|
|
17545
|
+
},
|
|
17546
|
+
recovery: recoveryEnv({
|
|
17547
|
+
app: app.name,
|
|
17548
|
+
keys,
|
|
17549
|
+
reasonCode: "missing_env"
|
|
17550
|
+
}),
|
|
17551
|
+
summary: `Missing required env ${keys.length === 1 ? "key" : "keys"}: ${keys.join(", ")}.`,
|
|
17552
|
+
configPath: tomlPathFor2(ctx.cwd),
|
|
17553
|
+
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
17554
|
+
};
|
|
17555
|
+
}
|
|
17556
|
+
if (err instanceof PercherApiError && err.code === "ENV_KEY_UNDECLARED") {
|
|
17557
|
+
const extra = err.extra ?? {};
|
|
17558
|
+
const apiProblems = extra.problems ?? [];
|
|
17559
|
+
const problems = apiProblems.map((p) => ({
|
|
17560
|
+
file: p.file,
|
|
17561
|
+
line: p.line,
|
|
17562
|
+
message: `Source references env key '${p.key}' which is not declared in [env]. Add it to [env].required, [env].optional, or [env].ignore in percher.toml. Context: ${p.context ?? "(unavailable)"}`
|
|
17563
|
+
}));
|
|
17564
|
+
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
17565
|
+
return {
|
|
17566
|
+
status: "failed",
|
|
17567
|
+
app,
|
|
17568
|
+
fileCount: tarball.fileCount,
|
|
17569
|
+
bytes: tarball.bytes,
|
|
17570
|
+
error: {
|
|
17571
|
+
title: "Undeclared env key",
|
|
17572
|
+
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
17573
|
+
suggestion: "Add each key to [env].required (must exist before deploy), [env].optional (may be referenced), or [env].ignore (intentionally unset). See https://percher.app/docs/env for the contract.",
|
|
17574
|
+
errorClass: "config_invalid",
|
|
17575
|
+
phase: "config",
|
|
17576
|
+
relevantFiles: ["percher.toml"]
|
|
17577
|
+
},
|
|
17578
|
+
recovery: recoveryFixConfig({
|
|
17579
|
+
problems,
|
|
17580
|
+
reasonCode: "env_key_undeclared"
|
|
17581
|
+
}),
|
|
17582
|
+
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`,
|
|
17583
|
+
configPath: tomlPathFor2(ctx.cwd),
|
|
17584
|
+
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
17585
|
+
};
|
|
17586
|
+
}
|
|
17587
|
+
if (err instanceof PercherApiError && err.code === "DAILY_QUOTA_EXCEEDED") {
|
|
17588
|
+
const extra = err.extra ?? {};
|
|
17589
|
+
const kind = extra.kind ?? "live";
|
|
17590
|
+
const used = extra.used ?? 0;
|
|
17591
|
+
const limit = extra.limit ?? 0;
|
|
17592
|
+
const resetAt = extra.resetAt ?? "";
|
|
17593
|
+
const otherKind = kind === "live" ? "preview" : "live";
|
|
17594
|
+
return {
|
|
17595
|
+
status: "failed",
|
|
17596
|
+
app,
|
|
17597
|
+
fileCount: tarball.fileCount,
|
|
17598
|
+
bytes: tarball.bytes,
|
|
17599
|
+
error: {
|
|
17600
|
+
title: "Daily deploy quota reached",
|
|
17601
|
+
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
17602
|
+
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/settings to raise this cap.`
|
|
17603
|
+
},
|
|
17604
|
+
recovery: recoveryAsk({
|
|
17605
|
+
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then, deploy a ${otherKind} instead, or upgrade at https://percher.app/settings.`,
|
|
17606
|
+
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
17607
|
+
reasonCode: "quota_exceeded"
|
|
17608
|
+
}),
|
|
17609
|
+
summary: `Daily ${kind}-deploy quota reached (${used}/${limit}). Resets ${resetAt}.`,
|
|
17610
|
+
configPath: tomlPathFor2(ctx.cwd),
|
|
17611
|
+
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
17612
|
+
};
|
|
17613
|
+
}
|
|
17614
|
+
throw err;
|
|
17048
17615
|
}
|
|
17049
|
-
throw err;
|
|
17050
17616
|
}
|
|
17051
17617
|
const replacedPreview = deployment.replacedPreview === true;
|
|
17052
17618
|
if (replacedPreview) {
|
|
@@ -17076,7 +17642,7 @@ ${keyList}
|
|
|
17076
17642
|
}
|
|
17077
17643
|
ctx.status("[4/4] Building & deploying...");
|
|
17078
17644
|
const buildStart = Date.now();
|
|
17079
|
-
const timeoutMs =
|
|
17645
|
+
const timeoutMs = TIMEOUTS.clientPublishPoll;
|
|
17080
17646
|
let lastStatus;
|
|
17081
17647
|
let nextHeartbeatMs = 15000;
|
|
17082
17648
|
let timedOut = null;
|
|
@@ -17100,6 +17666,11 @@ ${keyList}
|
|
|
17100
17666
|
nextHeartbeatMs = elapsedMs + 15000;
|
|
17101
17667
|
}
|
|
17102
17668
|
},
|
|
17669
|
+
onEvent: (event) => {
|
|
17670
|
+
const line = renderDeployEvent(event);
|
|
17671
|
+
if (line)
|
|
17672
|
+
ctx.status(line);
|
|
17673
|
+
},
|
|
17103
17674
|
onTimeout: (d) => {
|
|
17104
17675
|
timedOut = { deployment: d };
|
|
17105
17676
|
throw new class PollTimeoutSentinel extends Error {
|
|
@@ -17185,7 +17756,9 @@ ${keyList}
|
|
|
17185
17756
|
replacedPreview,
|
|
17186
17757
|
firstDeploy: firstDeploy || undefined,
|
|
17187
17758
|
cacheHit,
|
|
17759
|
+
cacheReused: cacheReusedDeployment !== null || undefined,
|
|
17188
17760
|
missingBuildEnvKeys: missingBuildEnvKeys.length > 0 ? missingBuildEnvKeys : undefined,
|
|
17761
|
+
traceId: deployment.id,
|
|
17189
17762
|
recovery: RECOVERY_NONE,
|
|
17190
17763
|
summary: buildLiveSummary({
|
|
17191
17764
|
appName: app.name,
|
|
@@ -17247,13 +17820,22 @@ async function handleUnauthorized(ctx, config, tarball, input) {
|
|
|
17247
17820
|
let app;
|
|
17248
17821
|
({ app, firstDeploy: firstDeployRetry } = await ensureApp2(ctx, config));
|
|
17249
17822
|
ctx.status("Uploading...");
|
|
17823
|
+
const freshTarballBytes = new Uint8Array(await new Response(freshTarball.stream).arrayBuffer());
|
|
17824
|
+
const freshIdempotencyKey = crypto.randomUUID();
|
|
17250
17825
|
let deployment;
|
|
17251
17826
|
try {
|
|
17252
|
-
const deployResponse = await
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17827
|
+
const { result: deployResponse } = await runDeployWithRetry({
|
|
17828
|
+
call: (attempt) => ctx.client.apps.deploy(app.id, {
|
|
17829
|
+
tarball: freshTarballBytes,
|
|
17830
|
+
type: input.preview ? "preview" : undefined,
|
|
17831
|
+
note: input.message,
|
|
17832
|
+
noCache: input.noCache,
|
|
17833
|
+
idempotencyKey: freshIdempotencyKey,
|
|
17834
|
+
retriedAttempts: attempt - 1
|
|
17835
|
+
}),
|
|
17836
|
+
onRetry: ({ attempt, decision }) => {
|
|
17837
|
+
ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
|
|
17838
|
+
}
|
|
17257
17839
|
});
|
|
17258
17840
|
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
17259
17841
|
return buildAlreadyInProgressResult({
|
|
@@ -17296,7 +17878,7 @@ async function handleUnauthorized(ctx, config, tarball, input) {
|
|
|
17296
17878
|
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
17297
17879
|
};
|
|
17298
17880
|
}
|
|
17299
|
-
const timeoutMs =
|
|
17881
|
+
const timeoutMs = TIMEOUTS.clientPublishPoll;
|
|
17300
17882
|
const start = Date.now();
|
|
17301
17883
|
while (deployment.status !== "live" && deployment.status !== "failed" && deployment.status !== "replaced") {
|
|
17302
17884
|
if (Date.now() - start > timeoutMs) {
|
|
@@ -17364,6 +17946,7 @@ async function handleUnauthorized(ctx, config, tarball, input) {
|
|
|
17364
17946
|
firstDeploy: firstDeployRetry || undefined,
|
|
17365
17947
|
recovery: RECOVERY_NONE,
|
|
17366
17948
|
cacheHit: deployment.cacheHit,
|
|
17949
|
+
traceId: deployment.id,
|
|
17367
17950
|
summary: buildLiveSummary({
|
|
17368
17951
|
appName: app.name,
|
|
17369
17952
|
url: url ?? app.url,
|
|
@@ -17646,7 +18229,7 @@ Context: ${p.context}` : ""}`
|
|
|
17646
18229
|
throw err;
|
|
17647
18230
|
}
|
|
17648
18231
|
const buildStart = Date.now();
|
|
17649
|
-
const timeoutMs =
|
|
18232
|
+
const timeoutMs = TIMEOUTS.clientPublishPoll;
|
|
17650
18233
|
let lastStatus;
|
|
17651
18234
|
let nextHeartbeatMs = 15000;
|
|
17652
18235
|
let deployment = initial;
|
|
@@ -17696,10 +18279,10 @@ Context: ${p.context}` : ""}`
|
|
|
17696
18279
|
const totalSeconds = (Date.now() - t0) / 1000;
|
|
17697
18280
|
const url = deployment.previewUrl ?? deployment.url ?? app.url;
|
|
17698
18281
|
if (deployment.status === "failed") {
|
|
17699
|
-
const recovery =
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
18282
|
+
const { error, recovery, summary } = await classifyDeploymentFailure({
|
|
18283
|
+
ctx,
|
|
18284
|
+
app,
|
|
18285
|
+
deployment
|
|
17703
18286
|
});
|
|
17704
18287
|
return {
|
|
17705
18288
|
status: "failed",
|
|
@@ -17707,14 +18290,9 @@ Context: ${p.context}` : ""}`
|
|
|
17707
18290
|
deployment,
|
|
17708
18291
|
fileCount: 0,
|
|
17709
18292
|
bytes: 0,
|
|
17710
|
-
error
|
|
17711
|
-
title: "Redeploy failed",
|
|
17712
|
-
explanation: deployment.errorMessage ?? "The redeploy did not succeed.",
|
|
17713
|
-
suggestion: `Inspect with \`bunx percher deploys inspect ${deployment.id}\`.`,
|
|
17714
|
-
problems: deployment.problems
|
|
17715
|
-
},
|
|
18293
|
+
error,
|
|
17716
18294
|
recovery,
|
|
17717
|
-
summary
|
|
18295
|
+
summary,
|
|
17718
18296
|
configPath: "",
|
|
17719
18297
|
bundle: { fileCount: 0, bytes: 0 }
|
|
17720
18298
|
};
|
|
@@ -17956,7 +18534,7 @@ async function rollback(ctx, input, internals = {}) {
|
|
|
17956
18534
|
}
|
|
17957
18535
|
let deployment = await ctx.client.apps.rollback(name, input.commitSha);
|
|
17958
18536
|
const pollInterval = internals.pollIntervalMs ?? 2000;
|
|
17959
|
-
const timeoutMs = internals.timeoutMs ??
|
|
18537
|
+
const timeoutMs = internals.timeoutMs ?? TIMEOUTS.clientPublishPoll;
|
|
17960
18538
|
const startedAt = Date.now();
|
|
17961
18539
|
while (deployment.status !== "live" && deployment.status !== "failed") {
|
|
17962
18540
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -18076,8 +18654,8 @@ function resolveSource(backups, source) {
|
|
|
18076
18654
|
}
|
|
18077
18655
|
return sorted.find((b) => b.backup_id === source) ?? null;
|
|
18078
18656
|
}
|
|
18079
|
-
function humanizeStatus(
|
|
18080
|
-
switch (
|
|
18657
|
+
function humanizeStatus(status2) {
|
|
18658
|
+
switch (status2) {
|
|
18081
18659
|
case "preparing":
|
|
18082
18660
|
return "Preparing";
|
|
18083
18661
|
case "quiescing":
|
|
@@ -18091,7 +18669,7 @@ function humanizeStatus(status) {
|
|
|
18091
18669
|
case "rolling_back":
|
|
18092
18670
|
return "Rolling back";
|
|
18093
18671
|
default:
|
|
18094
|
-
return
|
|
18672
|
+
return status2;
|
|
18095
18673
|
}
|
|
18096
18674
|
}
|
|
18097
18675
|
// ../core/dist/commands/deploys.js
|
|
@@ -18195,6 +18773,15 @@ var doctorInputSchema = exports_external.object({
|
|
|
18195
18773
|
mode: exports_external.enum(["auto", "deploy", "runtime", "config", "env", "account"]).optional(),
|
|
18196
18774
|
deployId: exports_external.string().optional()
|
|
18197
18775
|
});
|
|
18776
|
+
var STUCK_DEPLOY_THRESHOLD_MS = 15 * 60 * 1000;
|
|
18777
|
+
function isStuckDeploy(status2, createdAt, now = Date.now()) {
|
|
18778
|
+
if (status2 !== "building" && status2 !== "queued")
|
|
18779
|
+
return false;
|
|
18780
|
+
const created = Date.parse(createdAt);
|
|
18781
|
+
if (Number.isNaN(created))
|
|
18782
|
+
return false;
|
|
18783
|
+
return now - created > STUCK_DEPLOY_THRESHOLD_MS;
|
|
18784
|
+
}
|
|
18198
18785
|
async function doctor(ctx, input = {}) {
|
|
18199
18786
|
const checks = [];
|
|
18200
18787
|
const hasToken = !!ctx.client.token;
|
|
@@ -18204,7 +18791,7 @@ async function doctor(ctx, input = {}) {
|
|
|
18204
18791
|
message: hasToken ? "Token configured" : "No token found. Run: percher login"
|
|
18205
18792
|
});
|
|
18206
18793
|
if (!hasToken) {
|
|
18207
|
-
return
|
|
18794
|
+
return summarize2(checks, { appProvided: !!input.app });
|
|
18208
18795
|
}
|
|
18209
18796
|
try {
|
|
18210
18797
|
const start = Date.now();
|
|
@@ -18226,7 +18813,7 @@ async function doctor(ctx, input = {}) {
|
|
|
18226
18813
|
status: "fail",
|
|
18227
18814
|
message: `Cannot reach ${ctx.client.apiUrl}: ${err.message}`
|
|
18228
18815
|
});
|
|
18229
|
-
return
|
|
18816
|
+
return summarize2(checks, { appProvided: !!input.app });
|
|
18230
18817
|
}
|
|
18231
18818
|
const tomlPath = join13(ctx.cwd, "percher.toml");
|
|
18232
18819
|
const tomlExists = existsSync9(tomlPath);
|
|
@@ -18267,7 +18854,7 @@ async function doctor(ctx, input = {}) {
|
|
|
18267
18854
|
if (!appName) {
|
|
18268
18855
|
const reason = tomlExists && !tomlValid ? "percher.toml is invalid — fix the issues above and re-run" : "No app specified and no percher.toml found";
|
|
18269
18856
|
checks.push({ name: "App", status: "skip", message: reason });
|
|
18270
|
-
return
|
|
18857
|
+
return summarize2(checks, { appProvided: !!input.app });
|
|
18271
18858
|
}
|
|
18272
18859
|
let appStatus;
|
|
18273
18860
|
let lastDeployStatus;
|
|
@@ -18344,6 +18931,13 @@ async function doctor(ctx, input = {}) {
|
|
|
18344
18931
|
status: deployOk ? "pass" : "warn",
|
|
18345
18932
|
message: `${diag.lastDeploy.status} (${diag.lastDeploy.createdAt})`
|
|
18346
18933
|
});
|
|
18934
|
+
if (isStuckDeploy(diag.lastDeploy.status, diag.lastDeploy.createdAt)) {
|
|
18935
|
+
checks.push({
|
|
18936
|
+
name: "Deploy stuck",
|
|
18937
|
+
status: "warn",
|
|
18938
|
+
message: "Last deploy has been in-progress > 15 min — likely platform restart mid-build. Run `percher publish` again; the platform will clear the stuck row within ~2 min."
|
|
18939
|
+
});
|
|
18940
|
+
}
|
|
18347
18941
|
}
|
|
18348
18942
|
if (diag.lastCrash) {
|
|
18349
18943
|
checks.push({
|
|
@@ -18455,7 +19049,7 @@ async function doctor(ctx, input = {}) {
|
|
|
18455
19049
|
message: err.message
|
|
18456
19050
|
});
|
|
18457
19051
|
}
|
|
18458
|
-
return
|
|
19052
|
+
return summarize2(checks, {
|
|
18459
19053
|
appName,
|
|
18460
19054
|
appProvided: !!input.app,
|
|
18461
19055
|
appStatus,
|
|
@@ -18476,12 +19070,12 @@ async function doctor(ctx, input = {}) {
|
|
|
18476
19070
|
}
|
|
18477
19071
|
var TRANSITIONAL_DEPLOY_STATUSES = new Set(["queued", "building", "deploying"]);
|
|
18478
19072
|
var TRANSITIONAL_APP_STATUSES = new Set(["provisioning"]);
|
|
18479
|
-
function
|
|
19073
|
+
function summarize2(checks, vctx = {}) {
|
|
18480
19074
|
const passed = checks.filter((c) => c.status === "pass").length;
|
|
18481
19075
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
18482
19076
|
const warned = checks.filter((c) => c.status === "warn").length;
|
|
18483
19077
|
const total = checks.filter((c) => c.status !== "skip").length;
|
|
18484
|
-
const verdict =
|
|
19078
|
+
const verdict = deriveVerdict2(checks, vctx);
|
|
18485
19079
|
return {
|
|
18486
19080
|
checks,
|
|
18487
19081
|
passed,
|
|
@@ -18494,7 +19088,7 @@ function summarize(checks, vctx = {}) {
|
|
|
18494
19088
|
summary: verdict.summary
|
|
18495
19089
|
};
|
|
18496
19090
|
}
|
|
18497
|
-
function
|
|
19091
|
+
function deriveVerdict2(checks, vctx) {
|
|
18498
19092
|
const findCheck = (name) => checks.find((c) => c.name === name);
|
|
18499
19093
|
const auth = findCheck("Auth token");
|
|
18500
19094
|
if (auth?.status === "fail") {
|
|
@@ -18603,9 +19197,9 @@ function deriveVerdict(checks, vctx) {
|
|
|
18603
19197
|
summary: r.summary
|
|
18604
19198
|
};
|
|
18605
19199
|
}
|
|
18606
|
-
const
|
|
19200
|
+
const status2 = r.recovery.nextAction === "wait_deploy" ? "in_progress" : "needs_action";
|
|
18607
19201
|
return {
|
|
18608
|
-
status,
|
|
19202
|
+
status: status2,
|
|
18609
19203
|
diagnosis: {
|
|
18610
19204
|
title: "Last deploy was replaced",
|
|
18611
19205
|
explanation: r.summary,
|
|
@@ -18920,7 +19514,7 @@ async function doPush(ctx, config, callbacks) {
|
|
|
18920
19514
|
}
|
|
18921
19515
|
let deployment = deployResponse;
|
|
18922
19516
|
const startedAt = Date.now();
|
|
18923
|
-
const timeoutMs =
|
|
19517
|
+
const timeoutMs = TIMEOUTS.clientPublishPoll;
|
|
18924
19518
|
const pollInterval = 2000;
|
|
18925
19519
|
let lastStatus = "";
|
|
18926
19520
|
while (deployment.status !== "live" && deployment.status !== "failed") {
|
|
@@ -19062,7 +19656,7 @@ async function data(ctx, input = {}) {
|
|
|
19062
19656
|
hint: "Pass an app name or run from a directory with percher.toml."
|
|
19063
19657
|
});
|
|
19064
19658
|
}
|
|
19065
|
-
const
|
|
19659
|
+
const status2 = await ctx.client.data.getStatus(name);
|
|
19066
19660
|
const adminLink = await ctx.client.data.getAdminLink(name);
|
|
19067
19661
|
let collections = [];
|
|
19068
19662
|
let stats = { collections: 0, totalRecords: 0 };
|
|
@@ -19080,9 +19674,9 @@ async function data(ctx, input = {}) {
|
|
|
19080
19674
|
}
|
|
19081
19675
|
}
|
|
19082
19676
|
return {
|
|
19083
|
-
mode:
|
|
19084
|
-
status:
|
|
19085
|
-
version:
|
|
19677
|
+
mode: status2.mode,
|
|
19678
|
+
status: status2.status,
|
|
19679
|
+
version: status2.version ?? null,
|
|
19086
19680
|
collections,
|
|
19087
19681
|
stats,
|
|
19088
19682
|
adminUrl: adminLink.adminUrl ?? null,
|
|
@@ -19801,7 +20395,8 @@ var PLAN_CATALOG = {
|
|
|
19801
20395
|
kvStorageMb: 10,
|
|
19802
20396
|
uptimeIntervalSec: 0,
|
|
19803
20397
|
dailyLiveDeploys: 50,
|
|
19804
|
-
dailyPreviewDeploys: 25
|
|
20398
|
+
dailyPreviewDeploys: 25,
|
|
20399
|
+
userConcurrentDeploys: 2
|
|
19805
20400
|
}
|
|
19806
20401
|
},
|
|
19807
20402
|
starter: {
|
|
@@ -19831,7 +20426,8 @@ var PLAN_CATALOG = {
|
|
|
19831
20426
|
kvStorageMb: 50,
|
|
19832
20427
|
uptimeIntervalSec: 300,
|
|
19833
20428
|
dailyLiveDeploys: 100,
|
|
19834
|
-
dailyPreviewDeploys: 50
|
|
20429
|
+
dailyPreviewDeploys: 50,
|
|
20430
|
+
userConcurrentDeploys: 3
|
|
19835
20431
|
}
|
|
19836
20432
|
},
|
|
19837
20433
|
maker: {
|
|
@@ -19861,7 +20457,8 @@ var PLAN_CATALOG = {
|
|
|
19861
20457
|
kvStorageMb: 200,
|
|
19862
20458
|
uptimeIntervalSec: 60,
|
|
19863
20459
|
dailyLiveDeploys: 200,
|
|
19864
|
-
dailyPreviewDeploys: 100
|
|
20460
|
+
dailyPreviewDeploys: 100,
|
|
20461
|
+
userConcurrentDeploys: 5
|
|
19865
20462
|
}
|
|
19866
20463
|
},
|
|
19867
20464
|
pro: {
|
|
@@ -19891,7 +20488,8 @@ var PLAN_CATALOG = {
|
|
|
19891
20488
|
kvStorageMb: 1000,
|
|
19892
20489
|
uptimeIntervalSec: 30,
|
|
19893
20490
|
dailyLiveDeploys: 1000,
|
|
19894
|
-
dailyPreviewDeploys: 500
|
|
20491
|
+
dailyPreviewDeploys: 500,
|
|
20492
|
+
userConcurrentDeploys: 20
|
|
19895
20493
|
}
|
|
19896
20494
|
}
|
|
19897
20495
|
};
|
|
@@ -19901,7 +20499,7 @@ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as re
|
|
|
19901
20499
|
import { dirname as dirname3, join as join21 } from "node:path";
|
|
19902
20500
|
|
|
19903
20501
|
// ../core/dist/ai-files-manifest.js
|
|
19904
|
-
import { createHash } from "node:crypto";
|
|
20502
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
19905
20503
|
import { existsSync as existsSync15, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
19906
20504
|
import { dirname as dirname2, join as join20, resolve as resolve5 } from "node:path";
|
|
19907
20505
|
var MANIFEST_REL = ".percher/ai-files-state.json";
|
|
@@ -19909,7 +20507,7 @@ var EMPTY_MANIFEST = { version: 1, files: {} };
|
|
|
19909
20507
|
function hashContent(content) {
|
|
19910
20508
|
const normalised = content.replace(/\r\n/g, `
|
|
19911
20509
|
`);
|
|
19912
|
-
return
|
|
20510
|
+
return createHash2("sha256").update(normalised, "utf8").digest("hex");
|
|
19913
20511
|
}
|
|
19914
20512
|
function manifestPath(projectRoot) {
|
|
19915
20513
|
return join20(projectRoot, MANIFEST_REL);
|
|
@@ -20371,12 +20969,12 @@ function formatDeployTimestamp(iso) {
|
|
|
20371
20969
|
const pad = (n) => n.toString().padStart(2, "0");
|
|
20372
20970
|
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
20373
20971
|
}
|
|
20374
|
-
function formatStatus(
|
|
20375
|
-
if (
|
|
20376
|
-
return import_picocolors.default.green(
|
|
20377
|
-
if (
|
|
20378
|
-
return import_picocolors.default.red(
|
|
20379
|
-
return import_picocolors.default.yellow(
|
|
20972
|
+
function formatStatus(status2) {
|
|
20973
|
+
if (status2 === "live")
|
|
20974
|
+
return import_picocolors.default.green(status2);
|
|
20975
|
+
if (status2 === "failed")
|
|
20976
|
+
return import_picocolors.default.red(status2);
|
|
20977
|
+
return import_picocolors.default.yellow(status2);
|
|
20380
20978
|
}
|
|
20381
20979
|
function firstNonFlagAfter(argv, from, valueFlags = new Set) {
|
|
20382
20980
|
let i = from + 1;
|
|
@@ -20657,8 +21255,8 @@ function registerAccountCommands(cli, ctx) {
|
|
|
20657
21255
|
|
|
20658
21256
|
// src/commands/apps.ts
|
|
20659
21257
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
20660
|
-
function assertNever(
|
|
20661
|
-
throw new Error(`Unhandled publish status: ${JSON.stringify(
|
|
21258
|
+
function assertNever(status2, summary) {
|
|
21259
|
+
throw new Error(`Unhandled publish status: ${JSON.stringify(status2)}${summary ? ` (${summary})` : ""}`);
|
|
20662
21260
|
}
|
|
20663
21261
|
async function handlePublishResult(ctx, result, options = {}) {
|
|
20664
21262
|
switch (result.status) {
|
|
@@ -20760,7 +21358,12 @@ async function handlePublishResult(ctx, result, options = {}) {
|
|
|
20760
21358
|
case "live": {
|
|
20761
21359
|
const timing = result.timing ? ` (${result.timing.totalSeconds}s)` : "";
|
|
20762
21360
|
await writeLine(ctx, `${import_picocolors3.default.green("✓")} Published to ${result.url}${timing}`);
|
|
20763
|
-
if (result.
|
|
21361
|
+
if (result.traceId) {
|
|
21362
|
+
await writeLine(ctx, import_picocolors3.default.dim(` Trace: ${result.traceId}`));
|
|
21363
|
+
}
|
|
21364
|
+
if (result.cacheReused) {
|
|
21365
|
+
await writeLine(ctx, import_picocolors3.default.dim(" Build cache: reused — identical bundle, redeployed without re-upload (--no-cache to force a fresh build)"));
|
|
21366
|
+
} else if (result.cacheHit === true) {
|
|
20764
21367
|
await writeLine(ctx, import_picocolors3.default.dim(" Build cache: hit (reused image — re-run with --no-cache if your changes aren't reflected)"));
|
|
20765
21368
|
} else if (result.cacheHit === false) {
|
|
20766
21369
|
await writeLine(ctx, import_picocolors3.default.dim(" Build cache: miss (fresh build)"));
|
|
@@ -21116,11 +21719,11 @@ function registerDeploysCommands(cli, ctx, argv) {
|
|
|
21116
21719
|
await writeLine(ctx, `${import_picocolors6.default.bold("ID".padEnd(22))} ${import_picocolors6.default.bold("STATUS".padEnd(10))} ${import_picocolors6.default.bold("CREATED".padEnd(20))} ${import_picocolors6.default.bold("ERROR")}`);
|
|
21117
21720
|
for (const d of list) {
|
|
21118
21721
|
const id = d.id.padEnd(22);
|
|
21119
|
-
const
|
|
21722
|
+
const status2 = d.status.padEnd(10);
|
|
21120
21723
|
const created = formatDeployTimestamp(d.createdAt).padEnd(20);
|
|
21121
21724
|
const err = d.errorMessage ? d.errorMessage.split(`
|
|
21122
21725
|
`)[0]?.slice(0, 60) ?? "—" : import_picocolors6.default.dim("—");
|
|
21123
|
-
const colored = d.status === "live" ? import_picocolors6.default.green(
|
|
21726
|
+
const colored = d.status === "live" ? import_picocolors6.default.green(status2) : d.status === "failed" ? import_picocolors6.default.red(status2) : import_picocolors6.default.yellow(status2);
|
|
21124
21727
|
await writeLine(ctx, `${import_picocolors6.default.dim(id)} ${colored} ${import_picocolors6.default.dim(created)} ${err}`);
|
|
21125
21728
|
}
|
|
21126
21729
|
});
|
|
@@ -21338,6 +21941,49 @@ function registerGithubCommands(cli, ctx) {
|
|
|
21338
21941
|
cli.command("github setup-webhook", "Regenerate the GitHub webhook secret for an app (use after a failed `github connect`)").option("--app <name>", "App name (overrides percher.toml)").action(async () => {});
|
|
21339
21942
|
}
|
|
21340
21943
|
|
|
21944
|
+
// src/commands/status.ts
|
|
21945
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
21946
|
+
function registerStatusCommands(cli, ctx) {
|
|
21947
|
+
cli.command("status", "Show Percher platform status (API + worker health)").option("--json", "Output JSON instead of formatted text").action(async (options) => {
|
|
21948
|
+
const result = await status(ctx, { json: Boolean(options.json) });
|
|
21949
|
+
if (options.json) {
|
|
21950
|
+
await writeLine(ctx, JSON.stringify(result, null, 2));
|
|
21951
|
+
if (result.verdict === "unreachable")
|
|
21952
|
+
throw new SilentExitError(1);
|
|
21953
|
+
return;
|
|
21954
|
+
}
|
|
21955
|
+
await renderHumanStatus(ctx, result);
|
|
21956
|
+
if (result.verdict === "unreachable")
|
|
21957
|
+
throw new SilentExitError(1);
|
|
21958
|
+
});
|
|
21959
|
+
}
|
|
21960
|
+
async function renderHumanStatus(ctx, result) {
|
|
21961
|
+
const symbol = result.verdict === "operational" ? import_picocolors9.default.green("✓") : result.verdict === "worker-degraded" ? import_picocolors9.default.yellow("⚠") : import_picocolors9.default.red("✗");
|
|
21962
|
+
await writeLine(ctx, `${symbol} ${result.summary}`);
|
|
21963
|
+
if (!result.health)
|
|
21964
|
+
return;
|
|
21965
|
+
const worker = result.health.worker;
|
|
21966
|
+
if (!worker) {
|
|
21967
|
+
await writeLine(ctx, import_picocolors9.default.dim(" worker: not configured (local Docker socket)"));
|
|
21968
|
+
return;
|
|
21969
|
+
}
|
|
21970
|
+
if (worker.health) {
|
|
21971
|
+
const ok = worker.health.ok;
|
|
21972
|
+
const symbol2 = ok ? import_picocolors9.default.green("✓") : import_picocolors9.default.red("✗");
|
|
21973
|
+
const lastOk = worker.health.lastOkAt ?? "never";
|
|
21974
|
+
await writeLine(ctx, ` ${symbol2} worker health: ${ok ? "ok" : `failing (${worker.health.consecutiveErrs} consecutive)`} last-ok=${import_picocolors9.default.dim(lastOk)}`);
|
|
21975
|
+
}
|
|
21976
|
+
if (worker.circuitBreaker) {
|
|
21977
|
+
const breaker = worker.circuitBreaker;
|
|
21978
|
+
const symbol2 = breaker.open ? import_picocolors9.default.red("✗") : import_picocolors9.default.green("✓");
|
|
21979
|
+
const state = breaker.open ? "open (deploys fail fast)" : "closed";
|
|
21980
|
+
await writeLine(ctx, ` ${symbol2} circuit breaker: ${state} ${breaker.failuresInWindow}/${breaker.threshold} failures in last ${Math.round(breaker.windowMs / 1000)}s`);
|
|
21981
|
+
}
|
|
21982
|
+
if (result.health.version) {
|
|
21983
|
+
await writeLine(ctx, import_picocolors9.default.dim(` api version: ${result.health.version}`));
|
|
21984
|
+
}
|
|
21985
|
+
}
|
|
21986
|
+
|
|
21341
21987
|
// src/open.ts
|
|
21342
21988
|
import { spawn } from "node:child_process";
|
|
21343
21989
|
async function openUrl(url) {
|
|
@@ -21397,40 +22043,40 @@ async function createCliContext() {
|
|
|
21397
22043
|
}
|
|
21398
22044
|
|
|
21399
22045
|
// src/output.ts
|
|
21400
|
-
var
|
|
22046
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
21401
22047
|
async function renderError(stderr, error) {
|
|
21402
22048
|
const writer = stderr.getWriter();
|
|
21403
22049
|
const encoder = new TextEncoder;
|
|
21404
22050
|
const lines = [];
|
|
21405
22051
|
if (error instanceof PercherTomlError) {
|
|
21406
|
-
lines.push(`${
|
|
22052
|
+
lines.push(`${import_picocolors10.default.red(`error (${error.code}):`)} ${error.message}`);
|
|
21407
22053
|
if (error.tomlLine !== undefined) {
|
|
21408
|
-
lines.push(
|
|
22054
|
+
lines.push(import_picocolors10.default.dim(` at percher.toml line ${error.tomlLine}`));
|
|
21409
22055
|
}
|
|
21410
22056
|
for (const issue of error.issues ?? []) {
|
|
21411
|
-
lines.push(`${
|
|
22057
|
+
lines.push(`${import_picocolors10.default.dim(" · ") + import_picocolors10.default.yellow(issue.path.join("."))}: ${issue.message}`);
|
|
21412
22058
|
}
|
|
21413
22059
|
} else if (error instanceof PercherApiError) {
|
|
21414
22060
|
if (error.status === 502 || error.status === 503) {
|
|
21415
|
-
lines.push(`${
|
|
22061
|
+
lines.push(`${import_picocolors10.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error.status}). Try again in a few seconds.`);
|
|
21416
22062
|
} else {
|
|
21417
|
-
lines.push(`${
|
|
21418
|
-
lines.push(
|
|
22063
|
+
lines.push(`${import_picocolors10.default.red("error:")} ${error.message}`);
|
|
22064
|
+
lines.push(import_picocolors10.default.dim(` ${error.code} (HTTP ${error.status})`));
|
|
21419
22065
|
}
|
|
21420
22066
|
if (error.requestId)
|
|
21421
|
-
lines.push(
|
|
22067
|
+
lines.push(import_picocolors10.default.dim(` request: ${error.requestId}`));
|
|
21422
22068
|
} else if (error instanceof PercherCoreError) {
|
|
21423
|
-
lines.push(`${
|
|
22069
|
+
lines.push(`${import_picocolors10.default.red(`error (${error.code}):`)} ${error.message}`);
|
|
21424
22070
|
if (error.hint)
|
|
21425
|
-
lines.push(
|
|
22071
|
+
lines.push(import_picocolors10.default.dim(` ${error.hint}`));
|
|
21426
22072
|
} else if (error instanceof Error) {
|
|
21427
|
-
lines.push(`${
|
|
22073
|
+
lines.push(`${import_picocolors10.default.red("unexpected error:")} ${error.message}`);
|
|
21428
22074
|
if (error.stack)
|
|
21429
|
-
lines.push(
|
|
22075
|
+
lines.push(import_picocolors10.default.dim(error.stack.split(`
|
|
21430
22076
|
`).slice(1, 4).join(`
|
|
21431
22077
|
`)));
|
|
21432
22078
|
} else {
|
|
21433
|
-
lines.push(`${
|
|
22079
|
+
lines.push(`${import_picocolors10.default.red("unexpected error:")} ${String(error)}`);
|
|
21434
22080
|
}
|
|
21435
22081
|
try {
|
|
21436
22082
|
await writer.write(encoder.encode(`${lines.join(`
|
|
@@ -21443,7 +22089,7 @@ async function renderError(stderr, error) {
|
|
|
21443
22089
|
// package.json
|
|
21444
22090
|
var package_default = {
|
|
21445
22091
|
name: "percher",
|
|
21446
|
-
version: "0.
|
|
22092
|
+
version: "0.4.0",
|
|
21447
22093
|
description: "CLI for the Percher hosting platform — publish apps with one command",
|
|
21448
22094
|
type: "module",
|
|
21449
22095
|
bin: {
|
|
@@ -21466,7 +22112,7 @@ var package_default = {
|
|
|
21466
22112
|
},
|
|
21467
22113
|
dependencies: {
|
|
21468
22114
|
"@percher/client": "^0.2.7",
|
|
21469
|
-
"@percher/core": "^0.
|
|
22115
|
+
"@percher/core": "^0.4.0",
|
|
21470
22116
|
"@percher/toml": "^0.1.3",
|
|
21471
22117
|
cac: "^7.0.0",
|
|
21472
22118
|
picocolors: "^1.1.1"
|
|
@@ -21476,7 +22122,7 @@ var package_default = {
|
|
|
21476
22122
|
},
|
|
21477
22123
|
engines: {
|
|
21478
22124
|
bun: ">=1.0.0",
|
|
21479
|
-
node: ">=
|
|
22125
|
+
node: ">=22.0.0"
|
|
21480
22126
|
},
|
|
21481
22127
|
license: "MIT",
|
|
21482
22128
|
author: "Mattias Malmborg",
|
|
@@ -21531,7 +22177,7 @@ async function showTelemetryNotice() {
|
|
|
21531
22177
|
}
|
|
21532
22178
|
|
|
21533
22179
|
// src/version-check.ts
|
|
21534
|
-
var
|
|
22180
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
21535
22181
|
async function checkForUpdates() {
|
|
21536
22182
|
try {
|
|
21537
22183
|
const res = await fetch("https://registry.npmjs.org/percher/latest", {
|
|
@@ -21542,9 +22188,9 @@ async function checkForUpdates() {
|
|
|
21542
22188
|
const data2 = await res.json();
|
|
21543
22189
|
const latest = data2.version;
|
|
21544
22190
|
if (latest && latest !== VERSION) {
|
|
21545
|
-
console.error(
|
|
22191
|
+
console.error(import_picocolors11.default.yellow(`
|
|
21546
22192
|
Update available: ${VERSION} → ${latest}`));
|
|
21547
|
-
console.error(
|
|
22193
|
+
console.error(import_picocolors11.default.dim(`Run: bunx percher@latest
|
|
21548
22194
|
`));
|
|
21549
22195
|
}
|
|
21550
22196
|
} catch {}
|
|
@@ -21561,6 +22207,7 @@ async function run(argv, ctxOverride) {
|
|
|
21561
22207
|
registerAuthCommands(cli, ctx);
|
|
21562
22208
|
registerAccountCommands(cli, ctx);
|
|
21563
22209
|
registerGithubCommands(cli, ctx);
|
|
22210
|
+
registerStatusCommands(cli, ctx);
|
|
21564
22211
|
cli.help();
|
|
21565
22212
|
cli.version(VERSION);
|
|
21566
22213
|
const command = argv[0] ?? "help";
|