percher 0.4.2 → 0.5.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 +991 -625
- package/dist/package.json +1 -1
- package/dist/src/commands/apps.d.ts.map +1 -1
- package/dist/src/commands/apps.js +108 -0
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/deploys.d.ts.map +1 -1
- package/dist/src/commands/deploys.js +46 -8
- package/dist/src/commands/deploys.js.map +1 -1
- package/package.json +4 -4
package/dist/bin/percher.js
CHANGED
|
@@ -103988,8 +103988,46 @@ function date4(params) {
|
|
|
103988
103988
|
|
|
103989
103989
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
103990
103990
|
config(en_default());
|
|
103991
|
-
// ../
|
|
103991
|
+
// ../shared/dist/app-name.js
|
|
103992
103992
|
var APP_NAME_RE = /^[a-z][a-z0-9-]{2,39}$/;
|
|
103993
|
+
// ../shared/dist/limits.js
|
|
103994
|
+
var MAX_TARBALL_BYTES = 500 * 1024 * 1024;
|
|
103995
|
+
// ../shared/dist/logs.js
|
|
103996
|
+
var MAX_LOG_SEARCH_LENGTH = 256;
|
|
103997
|
+
// ../shared/dist/platform-env.js
|
|
103998
|
+
var PERCHER_INJECTED_ENV_KEYS = new Set([
|
|
103999
|
+
"NODE_ENV",
|
|
104000
|
+
"PORT",
|
|
104001
|
+
"DATABASE_URL",
|
|
104002
|
+
"POCKETBASE_URL",
|
|
104003
|
+
"POCKETBASE_ADMIN_EMAIL",
|
|
104004
|
+
"POCKETBASE_ADMIN_PASSWORD",
|
|
104005
|
+
"PERCHER_APP_NAME",
|
|
104006
|
+
"PERCHER_APP_URL",
|
|
104007
|
+
"HTTP_PROXY",
|
|
104008
|
+
"HTTPS_PROXY",
|
|
104009
|
+
"NO_PROXY",
|
|
104010
|
+
"NODE_USE_ENV_PROXY"
|
|
104011
|
+
]);
|
|
104012
|
+
var PROXY_LOWERCASE_ALIASES = new Set([
|
|
104013
|
+
"http_proxy",
|
|
104014
|
+
"https_proxy",
|
|
104015
|
+
"no_proxy"
|
|
104016
|
+
]);
|
|
104017
|
+
// ../shared/dist/runtime.js
|
|
104018
|
+
var DEFAULT_NODE_VERSION_MAJOR = "24";
|
|
104019
|
+
var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
|
|
104020
|
+
// ../shared/dist/timeouts.js
|
|
104021
|
+
var TIMEOUTS = {
|
|
104022
|
+
workerBuildSubprocess: 10 * 60000,
|
|
104023
|
+
apiToWorkerBuildFetch: 11 * 60000,
|
|
104024
|
+
clientPublishPoll: 12 * 60000,
|
|
104025
|
+
workerHealthcheck: 5000,
|
|
104026
|
+
watchdogList: 15000,
|
|
104027
|
+
mcpWaitDeployDefault: 30000,
|
|
104028
|
+
mcpWaitDeployMax: 12 * 60000
|
|
104029
|
+
};
|
|
104030
|
+
// ../toml/dist/schema.js
|
|
103993
104031
|
var appSchema = exports_external.object({
|
|
103994
104032
|
name: exports_external.string().regex(APP_NAME_RE, "must be 3–40 chars, lowercase, start with a letter, [a-z0-9-] only"),
|
|
103995
104033
|
runtime: exports_external.enum(["node", "bun", "python", "static", "docker"]),
|
|
@@ -104030,6 +104068,7 @@ var buildSchema = exports_external.object({
|
|
|
104030
104068
|
var webSchema = exports_external.object({
|
|
104031
104069
|
port: exports_external.number().int().min(1024).max(65535),
|
|
104032
104070
|
health: exports_external.string().optional(),
|
|
104071
|
+
command: exports_external.string().min(1).max(1000).optional(),
|
|
104033
104072
|
password: exports_external.boolean().optional(),
|
|
104034
104073
|
spa: exports_external.boolean().optional()
|
|
104035
104074
|
}).strict();
|
|
@@ -104205,6 +104244,15 @@ var cronSchema = exports_external.object({
|
|
|
104205
104244
|
command: exports_external.string().min(1),
|
|
104206
104245
|
description: exports_external.string().max(200).optional()
|
|
104207
104246
|
}).strict();
|
|
104247
|
+
var cronsSchema = exports_external.record(exports_external.string(), cronSchema).superRefine((jobs, ctx) => {
|
|
104248
|
+
if (Object.hasOwn(jobs, "")) {
|
|
104249
|
+
ctx.addIssue({
|
|
104250
|
+
code: exports_external.ZodIssueCode.custom,
|
|
104251
|
+
path: [""],
|
|
104252
|
+
message: "cron job name must not be empty"
|
|
104253
|
+
});
|
|
104254
|
+
}
|
|
104255
|
+
});
|
|
104208
104256
|
var addonsSchema = exports_external.object({
|
|
104209
104257
|
pocketbase: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
|
|
104210
104258
|
cron: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
|
|
@@ -104229,7 +104277,7 @@ var percherConfigSchema = exports_external.object({
|
|
|
104229
104277
|
resources: resourcesSchema.optional(),
|
|
104230
104278
|
dev: devSchema.optional(),
|
|
104231
104279
|
required_env: requiredEnvSchema.optional(),
|
|
104232
|
-
crons:
|
|
104280
|
+
crons: cronsSchema.optional(),
|
|
104233
104281
|
addons: addonsSchema.optional(),
|
|
104234
104282
|
checks: exports_external.record(exports_external.string(), checkSchema).optional()
|
|
104235
104283
|
}).strict();
|
|
@@ -104900,6 +104948,20 @@ var billingUpgradeInputSchema = exports_external.object({
|
|
|
104900
104948
|
plan: exports_external.enum(["starter", "maker", "pro"])
|
|
104901
104949
|
});
|
|
104902
104950
|
var billingPortalInputSchema = exports_external.object({});
|
|
104951
|
+
// ../core/dist/commands/cache.js
|
|
104952
|
+
var cacheResetInputSchema = exports_external.object({
|
|
104953
|
+
app: exports_external.string().optional()
|
|
104954
|
+
});
|
|
104955
|
+
async function cacheReset(ctx, input = {}) {
|
|
104956
|
+
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
104957
|
+
if (!name) {
|
|
104958
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
104959
|
+
code: "env.no-app",
|
|
104960
|
+
hint: "Pass an app name or run from a directory with percher.toml."
|
|
104961
|
+
});
|
|
104962
|
+
}
|
|
104963
|
+
return ctx.client.apps.resetCache(name);
|
|
104964
|
+
}
|
|
104903
104965
|
// ../core/dist/commands/create.js
|
|
104904
104966
|
import { join as join6, resolve as resolve4 } from "node:path";
|
|
104905
104967
|
|
|
@@ -104959,9 +105021,8 @@ function scaffoldTemplate(input) {
|
|
|
104959
105021
|
}
|
|
104960
105022
|
|
|
104961
105023
|
// ../core/dist/commands/create.js
|
|
104962
|
-
var APP_NAME_RE2 = /^[a-z][a-z0-9-]{2,39}$/;
|
|
104963
105024
|
var createInputSchema = exports_external.object({
|
|
104964
|
-
name: exports_external.string().regex(
|
|
105025
|
+
name: exports_external.string().regex(APP_NAME_RE, "must be 3-40 chars, lowercase, start with a letter, [a-z0-9-] only"),
|
|
104965
105026
|
template: exports_external.string().min(1, "Template is required"),
|
|
104966
105027
|
directory: exports_external.string().optional(),
|
|
104967
105028
|
force: exports_external.boolean().optional()
|
|
@@ -105016,6 +105077,7 @@ var dashboardInputSchema = exports_external.object({
|
|
|
105016
105077
|
});
|
|
105017
105078
|
var DEFAULT_BASE = "https://percher.app";
|
|
105018
105079
|
var TAB_SHAPE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
105080
|
+
var SETTINGS_ANCHOR_TABS = new Set(["github", "env", "domains", "crash-diagnosis"]);
|
|
105019
105081
|
async function dashboard(ctx, input = {}) {
|
|
105020
105082
|
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
105021
105083
|
if (!name) {
|
|
@@ -105032,7 +105094,8 @@ async function dashboard(ctx, input = {}) {
|
|
|
105032
105094
|
});
|
|
105033
105095
|
}
|
|
105034
105096
|
const base = ctx.env.PERCHER_DASHBOARD_URL ?? DEFAULT_BASE;
|
|
105035
|
-
const
|
|
105097
|
+
const appBase = `${base}/apps/${encodeURIComponent(name)}`;
|
|
105098
|
+
const url2 = SETTINGS_ANCHOR_TABS.has(tab) ? `${appBase}/settings#${tab}` : `${appBase}?tab=${tab}`;
|
|
105036
105099
|
await ctx.openUrl(url2);
|
|
105037
105100
|
return url2;
|
|
105038
105101
|
}
|
|
@@ -105124,6 +105187,28 @@ async function deleteApp(ctx, input) {
|
|
|
105124
105187
|
}
|
|
105125
105188
|
await ctx.client.apps.delete(name);
|
|
105126
105189
|
}
|
|
105190
|
+
// ../client/dist/abortable-sleep.js
|
|
105191
|
+
function abortableSleep(ms, signal) {
|
|
105192
|
+
return new Promise((resolve6) => {
|
|
105193
|
+
if (!signal) {
|
|
105194
|
+
setTimeout(resolve6, ms);
|
|
105195
|
+
return;
|
|
105196
|
+
}
|
|
105197
|
+
if (signal.aborted) {
|
|
105198
|
+
resolve6();
|
|
105199
|
+
return;
|
|
105200
|
+
}
|
|
105201
|
+
const onAbort = () => {
|
|
105202
|
+
clearTimeout(t);
|
|
105203
|
+
resolve6();
|
|
105204
|
+
};
|
|
105205
|
+
const t = setTimeout(() => {
|
|
105206
|
+
signal.removeEventListener("abort", onAbort);
|
|
105207
|
+
resolve6();
|
|
105208
|
+
}, ms);
|
|
105209
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
105210
|
+
});
|
|
105211
|
+
}
|
|
105127
105212
|
// ../client/dist/build-env-prefixes.js
|
|
105128
105213
|
var AUTO_FORWARD_PREFIXES = [
|
|
105129
105214
|
"NEXT_PUBLIC_",
|
|
@@ -105196,6 +105281,21 @@ class PercherApiError extends Error {
|
|
|
105196
105281
|
this.extra = options.extra;
|
|
105197
105282
|
}
|
|
105198
105283
|
}
|
|
105284
|
+
|
|
105285
|
+
class NetworkError extends Error {
|
|
105286
|
+
constructor(message, options) {
|
|
105287
|
+
super(`network error: ${message}`, options);
|
|
105288
|
+
this.name = "NetworkError";
|
|
105289
|
+
}
|
|
105290
|
+
}
|
|
105291
|
+
var TRANSIENT_NETWORK_MESSAGE = /network error|fetch failed|ECONNRESET|ENOTFOUND|ETIMEDOUT/i;
|
|
105292
|
+
function isTransientNetworkError(err) {
|
|
105293
|
+
if (err instanceof NetworkError)
|
|
105294
|
+
return true;
|
|
105295
|
+
if (err instanceof PercherApiError)
|
|
105296
|
+
return false;
|
|
105297
|
+
return err instanceof Error && TRANSIENT_NETWORK_MESSAGE.test(err.message);
|
|
105298
|
+
}
|
|
105199
105299
|
var RESTORE_JOB_STATUSES_FOR_ERROR = [
|
|
105200
105300
|
"preparing",
|
|
105201
105301
|
"quiescing",
|
|
@@ -105244,6 +105344,11 @@ function isSunsetGateBody(body) {
|
|
|
105244
105344
|
}
|
|
105245
105345
|
|
|
105246
105346
|
// ../client/dist/http.js
|
|
105347
|
+
var BODY_SNIPPET_MAX = 120;
|
|
105348
|
+
function bodySnippet(body) {
|
|
105349
|
+
return body.length <= BODY_SNIPPET_MAX ? body : `${body.slice(0, BODY_SNIPPET_MAX)}…`;
|
|
105350
|
+
}
|
|
105351
|
+
|
|
105247
105352
|
class Http {
|
|
105248
105353
|
apiUrl;
|
|
105249
105354
|
token;
|
|
@@ -105257,8 +105362,8 @@ class Http {
|
|
|
105257
105362
|
this.clientVersion = options.clientVersion;
|
|
105258
105363
|
this.sessionId = options.sessionId;
|
|
105259
105364
|
}
|
|
105260
|
-
get(path) {
|
|
105261
|
-
return this.request("GET", path);
|
|
105365
|
+
get(path, opts) {
|
|
105366
|
+
return this.request("GET", path, undefined, opts?.signal);
|
|
105262
105367
|
}
|
|
105263
105368
|
post(path, body) {
|
|
105264
105369
|
return this.request("POST", path, body);
|
|
@@ -105296,7 +105401,7 @@ class Http {
|
|
|
105296
105401
|
try {
|
|
105297
105402
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, { method: "GET", headers });
|
|
105298
105403
|
} catch (err) {
|
|
105299
|
-
throw new
|
|
105404
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105300
105405
|
}
|
|
105301
105406
|
if (!res.ok)
|
|
105302
105407
|
throw await this.parseError(res);
|
|
@@ -105322,15 +105427,15 @@ class Http {
|
|
|
105322
105427
|
try {
|
|
105323
105428
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
105324
105429
|
} catch (err) {
|
|
105325
|
-
throw new
|
|
105430
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105326
105431
|
}
|
|
105327
105432
|
if (!res.ok)
|
|
105328
105433
|
throw await this.parseError(res);
|
|
105329
105434
|
return await res.json();
|
|
105330
105435
|
}
|
|
105331
|
-
async request(method, path, body) {
|
|
105436
|
+
async request(method, path, body, signal) {
|
|
105332
105437
|
const headers = this.buildHeaders();
|
|
105333
|
-
const init = { method, headers };
|
|
105438
|
+
const init = { method, headers, signal };
|
|
105334
105439
|
if (body !== undefined) {
|
|
105335
105440
|
headers["Content-Type"] = "application/json";
|
|
105336
105441
|
init.body = JSON.stringify(body);
|
|
@@ -105339,7 +105444,7 @@ class Http {
|
|
|
105339
105444
|
try {
|
|
105340
105445
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
105341
105446
|
} catch (err) {
|
|
105342
|
-
throw new
|
|
105447
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105343
105448
|
}
|
|
105344
105449
|
if (!res.ok)
|
|
105345
105450
|
throw await this.parseError(res);
|
|
@@ -105350,7 +105455,10 @@ class Http {
|
|
|
105350
105455
|
return await res.json();
|
|
105351
105456
|
if (contentType.startsWith("text/"))
|
|
105352
105457
|
return await res.text();
|
|
105353
|
-
|
|
105458
|
+
const raw = await res.text();
|
|
105459
|
+
if (raw.length === 0)
|
|
105460
|
+
return;
|
|
105461
|
+
throw new PercherApiError(`unexpected content-type "${contentType || "none"}" on ${method} ${path}: ${bodySnippet(raw)}`, { status: res.status, code: "UNEXPECTED_CONTENT_TYPE" });
|
|
105354
105462
|
}
|
|
105355
105463
|
buildHeaders() {
|
|
105356
105464
|
const headers = {
|
|
@@ -105451,11 +105559,12 @@ class AlertsResource {
|
|
|
105451
105559
|
}
|
|
105452
105560
|
}
|
|
105453
105561
|
|
|
105454
|
-
// ../client/dist/resources/apps.js
|
|
105562
|
+
// ../client/dist/resources/apps-types.js
|
|
105455
105563
|
function isDeployAlreadyInProgress(res) {
|
|
105456
105564
|
return res.status === "already_in_progress";
|
|
105457
105565
|
}
|
|
105458
105566
|
|
|
105567
|
+
// ../client/dist/resources/apps.js
|
|
105459
105568
|
class AppsResource {
|
|
105460
105569
|
http;
|
|
105461
105570
|
constructor(http) {
|
|
@@ -105470,8 +105579,8 @@ class AppsResource {
|
|
|
105470
105579
|
listLight() {
|
|
105471
105580
|
return this.http.get("/apps?light=1");
|
|
105472
105581
|
}
|
|
105473
|
-
get(id) {
|
|
105474
|
-
return this.http.get(`/apps/${encodeURIComponent(id)}
|
|
105582
|
+
get(id, opts) {
|
|
105583
|
+
return this.http.get(`/apps/${encodeURIComponent(id)}`, opts);
|
|
105475
105584
|
}
|
|
105476
105585
|
async deploy(id, input) {
|
|
105477
105586
|
const params = new URLSearchParams;
|
|
@@ -105510,6 +105619,12 @@ class AppsResource {
|
|
|
105510
105619
|
enabled
|
|
105511
105620
|
});
|
|
105512
105621
|
}
|
|
105622
|
+
setPreviewBranch(appRef, branch) {
|
|
105623
|
+
return this.http.put(`/apps/${encodeURIComponent(appRef)}/preview-branch`, { branch });
|
|
105624
|
+
}
|
|
105625
|
+
unsetPreviewBranch(appRef) {
|
|
105626
|
+
return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/preview-branch`);
|
|
105627
|
+
}
|
|
105513
105628
|
setDescription(appRef, description) {
|
|
105514
105629
|
return this.http.patch(`/apps/${encodeURIComponent(appRef)}/description`, {
|
|
105515
105630
|
description
|
|
@@ -105548,8 +105663,8 @@ class AppsResource {
|
|
|
105548
105663
|
discardPreview(appRef, deployId) {
|
|
105549
105664
|
return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/discard`, {});
|
|
105550
105665
|
}
|
|
105551
|
-
getDeployment(appRef, deployId) {
|
|
105552
|
-
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}
|
|
105666
|
+
getDeployment(appRef, deployId, opts) {
|
|
105667
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}`, opts);
|
|
105553
105668
|
}
|
|
105554
105669
|
listDeploys(appRef, opts) {
|
|
105555
105670
|
const params = new URLSearchParams;
|
|
@@ -105558,32 +105673,36 @@ class AppsResource {
|
|
|
105558
105673
|
if (opts?.limit)
|
|
105559
105674
|
params.set("limit", String(opts.limit));
|
|
105560
105675
|
const qs = params.toString();
|
|
105561
|
-
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys${qs ? `?${qs}` : ""}
|
|
105676
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys${qs ? `?${qs}` : ""}`, opts?.signal ? { signal: opts.signal } : undefined);
|
|
105562
105677
|
}
|
|
105563
|
-
async getBuildLog(appRef, deployId) {
|
|
105678
|
+
async getBuildLog(appRef, deployId, opts) {
|
|
105564
105679
|
const path = `/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/build-log`;
|
|
105565
105680
|
let attempt = 0;
|
|
105566
105681
|
const maxRetries = 5;
|
|
105567
105682
|
while (true) {
|
|
105568
105683
|
try {
|
|
105569
|
-
const res = await this.http.get(path);
|
|
105684
|
+
const res = await this.http.get(path, opts);
|
|
105570
105685
|
if (typeof res === "string")
|
|
105571
105686
|
return res;
|
|
105572
105687
|
return res?.buildLog ?? "";
|
|
105573
105688
|
} catch (err) {
|
|
105574
105689
|
attempt++;
|
|
105575
|
-
|
|
105576
|
-
const isNetwork = msg.startsWith("network error:");
|
|
105577
|
-
if (!isNetwork || attempt >= maxRetries)
|
|
105690
|
+
if (opts?.signal?.aborted || !isTransientNetworkError(err) || attempt >= maxRetries) {
|
|
105578
105691
|
throw err;
|
|
105692
|
+
}
|
|
105579
105693
|
const backoffMs = 1000 * 2 ** (attempt - 1);
|
|
105580
|
-
await
|
|
105694
|
+
await abortableSleep(backoffMs, opts?.signal);
|
|
105695
|
+
if (opts?.signal?.aborted)
|
|
105696
|
+
throw err;
|
|
105581
105697
|
}
|
|
105582
105698
|
}
|
|
105583
105699
|
}
|
|
105584
105700
|
unsuspend(appRef) {
|
|
105585
105701
|
return this.http.post(`/apps/${encodeURIComponent(appRef)}/unsuspend`, {});
|
|
105586
105702
|
}
|
|
105703
|
+
resetCache(appRef) {
|
|
105704
|
+
return this.http.post(`/apps/${encodeURIComponent(appRef)}/cache/reset`, {});
|
|
105705
|
+
}
|
|
105587
105706
|
delete(id) {
|
|
105588
105707
|
return this.http.delete(`/apps/${encodeURIComponent(id)}`);
|
|
105589
105708
|
}
|
|
@@ -106003,6 +106122,9 @@ class SecurityResource {
|
|
|
106003
106122
|
overview() {
|
|
106004
106123
|
return this.http.get("/admin/security/overview");
|
|
106005
106124
|
}
|
|
106125
|
+
platformBaseOverview() {
|
|
106126
|
+
return this.http.get("/admin/security/platform-base");
|
|
106127
|
+
}
|
|
106006
106128
|
rescanAllAsAdmin() {
|
|
106007
106129
|
return this.http.post("/admin/security/rescan", {});
|
|
106008
106130
|
}
|
|
@@ -106192,17 +106314,6 @@ async function deploysInspect(ctx, input) {
|
|
|
106192
106314
|
import { existsSync as existsSync7 } from "node:fs";
|
|
106193
106315
|
import { join as join10 } from "node:path";
|
|
106194
106316
|
|
|
106195
|
-
// ../shared/dist/timeouts.js
|
|
106196
|
-
var TIMEOUTS = {
|
|
106197
|
-
workerBuildSubprocess: 10 * 60000,
|
|
106198
|
-
apiToWorkerBuildFetch: 11 * 60000,
|
|
106199
|
-
clientPublishPoll: 12 * 60000,
|
|
106200
|
-
workerHealthcheck: 5000,
|
|
106201
|
-
watchdogList: 15000,
|
|
106202
|
-
mcpWaitDeployDefault: 30000,
|
|
106203
|
-
mcpWaitDeployMax: 12 * 60000
|
|
106204
|
-
};
|
|
106205
|
-
|
|
106206
106317
|
// ../core/dist/tarball.js
|
|
106207
106318
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
106208
106319
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -107159,15 +107270,17 @@ function trimMessage(raw) {
|
|
|
107159
107270
|
function tomlPathFor(cwd) {
|
|
107160
107271
|
return join11(cwd, "percher.toml");
|
|
107161
107272
|
}
|
|
107162
|
-
async function buildFailureError(ctx, app, deployment) {
|
|
107273
|
+
async function buildFailureError(ctx, app, deployment, signal) {
|
|
107163
107274
|
let buildLogTail;
|
|
107164
107275
|
let buildLogFetchFailed = false;
|
|
107165
107276
|
let fullLog = "";
|
|
107166
107277
|
try {
|
|
107167
|
-
fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id);
|
|
107168
|
-
if (!fullLog) {
|
|
107169
|
-
await
|
|
107170
|
-
|
|
107278
|
+
fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
|
|
107279
|
+
if (!fullLog && !signal?.aborted) {
|
|
107280
|
+
await abortableSleep(1500, signal);
|
|
107281
|
+
if (!signal?.aborted) {
|
|
107282
|
+
fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
|
|
107283
|
+
}
|
|
107171
107284
|
}
|
|
107172
107285
|
if (fullLog) {
|
|
107173
107286
|
const lines = fullLog.split(`
|
|
@@ -107213,7 +107326,7 @@ async function buildFailureError(ctx, app, deployment) {
|
|
|
107213
107326
|
}
|
|
107214
107327
|
async function classifyDeploymentFailure(opts) {
|
|
107215
107328
|
const { ctx, app, deployment, publishInput } = opts;
|
|
107216
|
-
const error48 = await buildFailureError(ctx, app, deployment);
|
|
107329
|
+
const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
|
|
107217
107330
|
const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
|
|
107218
107331
|
missingEnvVars: error48.missingEnvVars,
|
|
107219
107332
|
deployId: deployment.id,
|
|
@@ -107259,6 +107372,100 @@ var waitForDeployInputSchema = exports_external.object({
|
|
|
107259
107372
|
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.`),
|
|
107260
107373
|
pollIntervalSeconds: exports_external.number().int().min(1).max(30).optional().describe("Seconds between polls (default 2).")
|
|
107261
107374
|
});
|
|
107375
|
+
async function waitForDeploy(ctx, input, opts) {
|
|
107376
|
+
const timeoutSeconds = input.timeoutSeconds ?? TIMEOUTS.mcpWaitDeployDefault / 1000;
|
|
107377
|
+
const pollIntervalSeconds = input.pollIntervalSeconds ?? 2;
|
|
107378
|
+
const startedAt = Date.now();
|
|
107379
|
+
const deadlineMs = startedAt + timeoutSeconds * 1000;
|
|
107380
|
+
const controller = new AbortController;
|
|
107381
|
+
const abortTimer = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
107382
|
+
const onExternalAbort = () => controller.abort();
|
|
107383
|
+
opts?.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
107384
|
+
if (opts?.signal?.aborted)
|
|
107385
|
+
controller.abort();
|
|
107386
|
+
try {
|
|
107387
|
+
let app;
|
|
107388
|
+
let initial;
|
|
107389
|
+
try {
|
|
107390
|
+
[app, initial] = await Promise.all([
|
|
107391
|
+
ctx.client.apps.get(input.app, { signal: controller.signal }),
|
|
107392
|
+
ctx.client.apps.getDeployment(input.app, input.deployId, { signal: controller.signal })
|
|
107393
|
+
]);
|
|
107394
|
+
} catch (err) {
|
|
107395
|
+
if (controller.signal.aborted) {
|
|
107396
|
+
throw new PercherCoreError(`wait-for-deploy timed out after ${timeoutSeconds}s before the first status fetch completed`, {
|
|
107397
|
+
code: "wait.timeout",
|
|
107398
|
+
hint: "The API is slow or unreachable. Retry with a larger timeout, or run percher doctor."
|
|
107399
|
+
});
|
|
107400
|
+
}
|
|
107401
|
+
throw err;
|
|
107402
|
+
}
|
|
107403
|
+
let deployment = initial;
|
|
107404
|
+
while (!isTerminal(deployment.status)) {
|
|
107405
|
+
if (controller.signal.aborted || Date.now() >= deadlineMs) {
|
|
107406
|
+
return buildStillRunningResult(app, deployment, startedAt);
|
|
107407
|
+
}
|
|
107408
|
+
const sleepMs = Math.min(pollIntervalSeconds * 1000, deadlineMs - Date.now());
|
|
107409
|
+
await abortableSleep(sleepMs, controller.signal);
|
|
107410
|
+
if (controller.signal.aborted || Date.now() >= deadlineMs) {
|
|
107411
|
+
return buildStillRunningResult(app, deployment, startedAt);
|
|
107412
|
+
}
|
|
107413
|
+
try {
|
|
107414
|
+
deployment = await ctx.client.apps.getDeployment(input.app, input.deployId, {
|
|
107415
|
+
signal: controller.signal
|
|
107416
|
+
});
|
|
107417
|
+
} catch (err) {
|
|
107418
|
+
if (controller.signal.aborted) {
|
|
107419
|
+
return buildStillRunningResult(app, deployment, startedAt, "status fetch exceeded the wait budget");
|
|
107420
|
+
}
|
|
107421
|
+
if (isTransientNetworkError(err)) {
|
|
107422
|
+
const msg = err.message ?? String(err);
|
|
107423
|
+
return buildStillRunningResult(app, deployment, startedAt, msg);
|
|
107424
|
+
}
|
|
107425
|
+
throw err;
|
|
107426
|
+
}
|
|
107427
|
+
}
|
|
107428
|
+
if (deployment.status === "failed") {
|
|
107429
|
+
const { error: error48, recovery, summary } = await classifyDeploymentFailure({
|
|
107430
|
+
ctx,
|
|
107431
|
+
app,
|
|
107432
|
+
deployment,
|
|
107433
|
+
signal: opts?.signal
|
|
107434
|
+
});
|
|
107435
|
+
return {
|
|
107436
|
+
status: "failed",
|
|
107437
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107438
|
+
deployment,
|
|
107439
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107440
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107441
|
+
error: error48,
|
|
107442
|
+
recovery,
|
|
107443
|
+
summary
|
|
107444
|
+
};
|
|
107445
|
+
}
|
|
107446
|
+
if (deployment.status === "replaced") {
|
|
107447
|
+
return await buildReplacedResult({ ctx, app, deployment, startedAt, signal: opts?.signal });
|
|
107448
|
+
}
|
|
107449
|
+
const url2 = deployment.previewUrl ?? deployment.url ?? app.url;
|
|
107450
|
+
return {
|
|
107451
|
+
status: "live",
|
|
107452
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107453
|
+
deployment,
|
|
107454
|
+
url: url2,
|
|
107455
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107456
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107457
|
+
recovery: RECOVERY_NONE,
|
|
107458
|
+
summary: `${app.name} deploy ${deployment.id} is live → ${url2}`
|
|
107459
|
+
};
|
|
107460
|
+
} finally {
|
|
107461
|
+
clearTimeout(abortTimer);
|
|
107462
|
+
controller.abort();
|
|
107463
|
+
opts?.signal?.removeEventListener("abort", onExternalAbort);
|
|
107464
|
+
}
|
|
107465
|
+
}
|
|
107466
|
+
function isTerminal(status) {
|
|
107467
|
+
return status === "live" || status === "failed" || status === "replaced";
|
|
107468
|
+
}
|
|
107262
107469
|
async function resolveReplaced(opts) {
|
|
107263
107470
|
const { ctx, app, replacedDeployment } = opts;
|
|
107264
107471
|
const deployType = replacedDeployment.type ?? "live";
|
|
@@ -107266,7 +107473,8 @@ async function resolveReplaced(opts) {
|
|
|
107266
107473
|
try {
|
|
107267
107474
|
const deploys = await ctx.client.apps.listDeploys(app.name, {
|
|
107268
107475
|
limit: 5,
|
|
107269
|
-
type: deployType
|
|
107476
|
+
type: deployType,
|
|
107477
|
+
signal: opts.signal
|
|
107270
107478
|
});
|
|
107271
107479
|
latest = deploys.find((d) => d.id !== replacedDeployment.id);
|
|
107272
107480
|
} catch {
|
|
@@ -107313,6 +107521,46 @@ async function resolveReplaced(opts) {
|
|
|
107313
107521
|
summary: `${app.name} deploy ${replacedDeployment.id} was replaced; the latest deploy ${latest.id} is ${latest.status}. Surface to user.`
|
|
107314
107522
|
};
|
|
107315
107523
|
}
|
|
107524
|
+
async function buildReplacedResult(opts) {
|
|
107525
|
+
const { ctx, app, deployment, startedAt } = opts;
|
|
107526
|
+
const resolved = await resolveReplaced({
|
|
107527
|
+
ctx,
|
|
107528
|
+
app,
|
|
107529
|
+
replacedDeployment: deployment,
|
|
107530
|
+
signal: opts.signal
|
|
107531
|
+
});
|
|
107532
|
+
return {
|
|
107533
|
+
status: "replaced",
|
|
107534
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107535
|
+
deployment,
|
|
107536
|
+
url: resolved.url,
|
|
107537
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107538
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107539
|
+
recovery: resolved.recovery,
|
|
107540
|
+
summary: resolved.summary
|
|
107541
|
+
};
|
|
107542
|
+
}
|
|
107543
|
+
function buildStillRunningResult(app, deployment, startedAt, networkHint) {
|
|
107544
|
+
return {
|
|
107545
|
+
status: "still_running",
|
|
107546
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107547
|
+
deployment,
|
|
107548
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107549
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107550
|
+
recovery: recoveryWait({
|
|
107551
|
+
app: app.name,
|
|
107552
|
+
deployId: deployment.id,
|
|
107553
|
+
reasonCode: deployment.status === "queued" ? "deploy_queued" : deployment.status === "building" ? "deploy_building" : deployment.status === "deploying" ? "deploy_deploying" : "deploy_timeout"
|
|
107554
|
+
}),
|
|
107555
|
+
summary: networkHint ? `${app.name} deploy ${deployment.id} still ${deployment.status} (transient: ${networkHint}) — call percher_wait_for_deploy again.` : `${app.name} deploy ${deployment.id} still ${deployment.status} — call percher_wait_for_deploy again.`
|
|
107556
|
+
};
|
|
107557
|
+
}
|
|
107558
|
+
function secondsSinceCreated(deployment) {
|
|
107559
|
+
const t = Date.parse(deployment.createdAt);
|
|
107560
|
+
if (Number.isNaN(t))
|
|
107561
|
+
return 0;
|
|
107562
|
+
return Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
107563
|
+
}
|
|
107316
107564
|
|
|
107317
107565
|
// ../core/dist/commands/doctor.js
|
|
107318
107566
|
var doctorInputSchema = exports_external.object({
|
|
@@ -107440,17 +107688,19 @@ async function doctor(ctx, input = {}) {
|
|
|
107440
107688
|
message: diag.container.running ? "Running" : diag.container.state === "not-found" ? "No container found" : `State: ${diag.container.state}`
|
|
107441
107689
|
});
|
|
107442
107690
|
if (diag.containerHealth) {
|
|
107691
|
+
const latency = diag.containerHealth.latencyMs != null ? ` (${diag.containerHealth.latencyMs}ms)` : "";
|
|
107443
107692
|
checks3.push({
|
|
107444
107693
|
name: "Container health",
|
|
107445
107694
|
status: diag.containerHealth.healthy ? "pass" : "fail",
|
|
107446
|
-
message: diag.containerHealth.healthy ? `${diag.containerHealth.path} OK
|
|
107695
|
+
message: diag.containerHealth.healthy ? `${diag.containerHealth.path} OK${latency}` : `${diag.containerHealth.path} — not responding`
|
|
107447
107696
|
});
|
|
107448
107697
|
}
|
|
107449
107698
|
if (diag.publicRoute) {
|
|
107699
|
+
const latency = diag.publicRoute.latencyMs != null ? ` (${diag.publicRoute.latencyMs}ms)` : "";
|
|
107450
107700
|
checks3.push({
|
|
107451
107701
|
name: "Public route",
|
|
107452
107702
|
status: diag.publicRoute.healthy ? "pass" : "fail",
|
|
107453
|
-
message: diag.publicRoute.healthy ? `${diag.publicRoute.url}
|
|
107703
|
+
message: diag.publicRoute.healthy ? `${diag.publicRoute.url}${latency}` : `${diag.publicRoute.url} — not responding`
|
|
107454
107704
|
});
|
|
107455
107705
|
}
|
|
107456
107706
|
if (diag.pocketbase) {
|
|
@@ -107636,11 +107886,53 @@ function summarize(checks3, vctx = {}) {
|
|
|
107636
107886
|
summary: verdict.summary
|
|
107637
107887
|
};
|
|
107638
107888
|
}
|
|
107639
|
-
function
|
|
107889
|
+
function buildVerdictRuleContext(checks3, vctx) {
|
|
107640
107890
|
const findCheck = (name) => checks3.find((c) => c.name === name);
|
|
107641
|
-
const
|
|
107642
|
-
|
|
107891
|
+
const toml = findCheck("percher.toml");
|
|
107892
|
+
const verdictChecks = vctx.appProvided && toml?.status === "fail" ? checks3.filter((c) => c.name !== "percher.toml") : checks3;
|
|
107893
|
+
const containerDown = vctx.containerRunning === false && vctx.containerState !== undefined && vctx.containerState !== "not-found" || vctx.containerHealthy === false;
|
|
107894
|
+
return { checks: checks3, vctx, findCheck, verdictChecks, containerDown };
|
|
107895
|
+
}
|
|
107896
|
+
function runtimeCrashExplanation({ vctx, findCheck }) {
|
|
107897
|
+
const containerCheck = findCheck("Container");
|
|
107898
|
+
const healthCheck = findCheck("Container health");
|
|
107899
|
+
const lastCrashCheck = findCheck("Last crash");
|
|
107900
|
+
const explanationBits = [];
|
|
107901
|
+
if (vctx.appStatus === "crashed")
|
|
107902
|
+
explanationBits.push("App is in crashed state.");
|
|
107903
|
+
if (containerCheck?.status === "fail")
|
|
107904
|
+
explanationBits.push(containerCheck.message);
|
|
107905
|
+
if (healthCheck?.status === "fail")
|
|
107906
|
+
explanationBits.push(healthCheck.message);
|
|
107907
|
+
if (lastCrashCheck)
|
|
107908
|
+
explanationBits.push(lastCrashCheck.message);
|
|
107909
|
+
return explanationBits.length > 0 ? explanationBits.join(" ") : "Runtime is not responding — container or health check is failing.";
|
|
107910
|
+
}
|
|
107911
|
+
var FALLBACK_RULE = {
|
|
107912
|
+
id: "unknown-fallback",
|
|
107913
|
+
when: () => true,
|
|
107914
|
+
verdict: ({ verdictChecks }) => {
|
|
107915
|
+
const firstFailed = verdictChecks.find((c) => c.status === "fail");
|
|
107643
107916
|
return {
|
|
107917
|
+
status: "needs_action",
|
|
107918
|
+
diagnosis: firstFailed ? {
|
|
107919
|
+
title: firstFailed.name,
|
|
107920
|
+
explanation: firstFailed.message,
|
|
107921
|
+
reasonCode: "unknown"
|
|
107922
|
+
} : undefined,
|
|
107923
|
+
recovery: recoveryAsk({
|
|
107924
|
+
prompt: firstFailed ? `Doctor flagged a problem: ${firstFailed.name} — ${firstFailed.message}. Review the checks list and surface the failing item to the user.` : "Doctor reported one or more issues. Review the checks list and surface the failing items to the user.",
|
|
107925
|
+
reasonCode: "unknown"
|
|
107926
|
+
}),
|
|
107927
|
+
summary: firstFailed ? `${firstFailed.name} failed: ${firstFailed.message}` : "Doctor reported issues — review the checks list."
|
|
107928
|
+
};
|
|
107929
|
+
}
|
|
107930
|
+
};
|
|
107931
|
+
var VERDICT_RULES = [
|
|
107932
|
+
{
|
|
107933
|
+
id: "auth-missing",
|
|
107934
|
+
when: ({ findCheck }) => findCheck("Auth token")?.status === "fail",
|
|
107935
|
+
verdict: () => ({
|
|
107644
107936
|
status: "blocked",
|
|
107645
107937
|
diagnosis: {
|
|
107646
107938
|
title: "Authentication required",
|
|
@@ -107650,173 +107942,215 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107650
107942
|
},
|
|
107651
107943
|
recovery: recoveryLogin({ reasonCode: "auth_required" }),
|
|
107652
107944
|
summary: "Login required: run `percher login` (or set PERCHER_TOKEN)."
|
|
107653
|
-
}
|
|
107654
|
-
}
|
|
107655
|
-
|
|
107656
|
-
|
|
107657
|
-
|
|
107658
|
-
|
|
107659
|
-
|
|
107660
|
-
title: "Percher API is unreachable",
|
|
107661
|
-
explanation: api2.message,
|
|
107662
|
-
reasonCode: "infra_unavailable",
|
|
107663
|
-
phase: "infra"
|
|
107664
|
-
},
|
|
107665
|
-
recovery: recoveryAsk({
|
|
107666
|
-
prompt: `Percher API is unreachable (${api2.message}). This is usually a transient network issue — wait a moment and try again, or check status.percher.app.`,
|
|
107667
|
-
reasonCode: "infra_unavailable",
|
|
107668
|
-
retryable: true
|
|
107669
|
-
}),
|
|
107670
|
-
summary: "Cannot reach the Percher API — try again in a moment."
|
|
107671
|
-
};
|
|
107672
|
-
}
|
|
107673
|
-
const toml = findCheck("percher.toml");
|
|
107674
|
-
if (toml?.status === "fail" && !vctx.appProvided) {
|
|
107675
|
-
return {
|
|
107676
|
-
status: "blocked",
|
|
107677
|
-
diagnosis: {
|
|
107678
|
-
title: "Invalid percher.toml",
|
|
107679
|
-
explanation: toml.message,
|
|
107680
|
-
reasonCode: "config_invalid",
|
|
107681
|
-
phase: "config"
|
|
107682
|
-
},
|
|
107683
|
-
recovery: recoveryFixConfig({
|
|
107684
|
-
problems: [
|
|
107685
|
-
{
|
|
107686
|
-
file: "percher.toml",
|
|
107687
|
-
message: toml.message
|
|
107688
|
-
}
|
|
107689
|
-
],
|
|
107690
|
-
reasonCode: "config_invalid"
|
|
107691
|
-
}),
|
|
107692
|
-
summary: "percher.toml is invalid — fix the issues and re-run."
|
|
107693
|
-
};
|
|
107694
|
-
}
|
|
107695
|
-
const app = findCheck("App");
|
|
107696
|
-
if (app?.status === "skip") {
|
|
107697
|
-
return {
|
|
107698
|
-
status: "blocked",
|
|
107699
|
-
diagnosis: {
|
|
107700
|
-
title: "No app specified",
|
|
107701
|
-
explanation: app.message,
|
|
107702
|
-
reasonCode: "config_missing",
|
|
107703
|
-
phase: "config"
|
|
107704
|
-
},
|
|
107705
|
-
recovery: recoveryAsk({
|
|
107706
|
-
prompt: `${app.message} — pass \`--app <name>\` or run \`percher init\` in a project directory to generate a percher.toml.`,
|
|
107707
|
-
reasonCode: "config_missing"
|
|
107708
|
-
}),
|
|
107709
|
-
summary: "No percher.toml found and no --app supplied — pass --app or run percher init."
|
|
107710
|
-
};
|
|
107711
|
-
}
|
|
107712
|
-
const inTransitionalDeploy = !!vctx.lastDeployStatus && TRANSITIONAL_DEPLOY_STATUSES.has(vctx.lastDeployStatus);
|
|
107713
|
-
const inTransitionalApp = !!vctx.appStatus && TRANSITIONAL_APP_STATUSES.has(vctx.appStatus);
|
|
107714
|
-
if (inTransitionalDeploy || inTransitionalApp) {
|
|
107715
|
-
const reasonCode = vctx.lastDeployStatus === "queued" ? "deploy_queued" : vctx.lastDeployStatus === "building" ? "deploy_building" : vctx.lastDeployStatus === "deploying" ? "deploy_deploying" : "deploy_queued";
|
|
107716
|
-
const what = inTransitionalApp ? `App ${vctx.appName ?? ""} is provisioning` : `Last deploy is ${vctx.lastDeployStatus}`;
|
|
107717
|
-
const recovery = vctx.lastDeployId && vctx.appName ? recoveryWait({
|
|
107718
|
-
app: vctx.appName,
|
|
107719
|
-
deployId: vctx.lastDeployId,
|
|
107720
|
-
reasonCode
|
|
107721
|
-
}) : recoveryAsk({
|
|
107722
|
-
prompt: `${what.trim()} for ${vctx.appName ?? "this app"}. Wait for it to finish before retrying — there's no deployId to track yet.`,
|
|
107723
|
-
reasonCode
|
|
107724
|
-
});
|
|
107725
|
-
return {
|
|
107726
|
-
status: "in_progress",
|
|
107727
|
-
diagnosis: {
|
|
107728
|
-
title: what.trim(),
|
|
107729
|
-
explanation: vctx.lastDeployId ? `${what.trim()} (deploy ${vctx.lastDeployId}). Wait for it to finish before retrying.` : `${what.trim()}. Wait for it to finish before retrying.`,
|
|
107730
|
-
reasonCode,
|
|
107731
|
-
phase: "deploy"
|
|
107732
|
-
},
|
|
107733
|
-
recovery,
|
|
107734
|
-
summary: vctx.lastDeployId ? `${what.trim()} — wait for deploy ${vctx.lastDeployId}.` : `${what.trim()} — wait a moment and re-run.`
|
|
107735
|
-
};
|
|
107736
|
-
}
|
|
107737
|
-
const verdictChecks = vctx.appProvided && toml?.status === "fail" ? checks3.filter((c) => c.name !== "percher.toml") : checks3;
|
|
107738
|
-
if (vctx.replacedResolution) {
|
|
107739
|
-
const r = vctx.replacedResolution;
|
|
107740
|
-
const isResolvedLive = r.recovery.nextAction === "none" && !!r.url;
|
|
107741
|
-
if (isResolvedLive) {
|
|
107945
|
+
})
|
|
107946
|
+
},
|
|
107947
|
+
{
|
|
107948
|
+
id: "api-unreachable",
|
|
107949
|
+
when: ({ findCheck }) => findCheck("API reachability")?.status === "fail",
|
|
107950
|
+
verdict: ({ findCheck }) => {
|
|
107951
|
+
const message = findCheck("API reachability")?.message ?? "";
|
|
107742
107952
|
return {
|
|
107743
|
-
status: "
|
|
107744
|
-
|
|
107745
|
-
|
|
107953
|
+
status: "blocked",
|
|
107954
|
+
diagnosis: {
|
|
107955
|
+
title: "Percher API is unreachable",
|
|
107956
|
+
explanation: message,
|
|
107957
|
+
reasonCode: "infra_unavailable",
|
|
107958
|
+
phase: "infra"
|
|
107959
|
+
},
|
|
107960
|
+
recovery: recoveryAsk({
|
|
107961
|
+
prompt: `Percher API is unreachable (${message}). This is usually a transient network issue — wait a moment and try again, or check status.percher.app.`,
|
|
107962
|
+
reasonCode: "infra_unavailable",
|
|
107963
|
+
retryable: true
|
|
107964
|
+
}),
|
|
107965
|
+
summary: "Cannot reach the Percher API — try again in a moment."
|
|
107746
107966
|
};
|
|
107747
107967
|
}
|
|
107748
|
-
|
|
107749
|
-
|
|
107750
|
-
|
|
107751
|
-
|
|
107752
|
-
|
|
107753
|
-
|
|
107754
|
-
reasonCode: "replaced_by_newer",
|
|
107755
|
-
phase: "deploy"
|
|
107756
|
-
},
|
|
107757
|
-
recovery: { ...r.recovery, reasonCode: "replaced_by_newer" },
|
|
107758
|
-
summary: r.summary
|
|
107759
|
-
};
|
|
107760
|
-
}
|
|
107761
|
-
if (vctx.lastDeployStatus === "failed") {
|
|
107762
|
-
if (vctx.inputMode === "deploy") {
|
|
107763
|
-
const cls = vctx.buildLogClassification;
|
|
107764
|
-
if (cls && cls.errorClass === "missing_env" && cls.missingEnvVars.length > 0) {
|
|
107765
|
-
return {
|
|
107766
|
-
status: "needs_action",
|
|
107767
|
-
diagnosis: {
|
|
107768
|
-
title: cls.title,
|
|
107769
|
-
explanation: cls.explanation,
|
|
107770
|
-
reasonCode: "missing_env",
|
|
107771
|
-
phase: "build"
|
|
107772
|
-
},
|
|
107773
|
-
recovery: recoveryEnv({
|
|
107774
|
-
app: vctx.appName,
|
|
107775
|
-
keys: cls.missingEnvVars
|
|
107776
|
-
}),
|
|
107777
|
-
summary: vctx.appName ? `Build failed — missing env vars on ${vctx.appName}: ${cls.missingEnvVars.join(", ")}.` : `Build failed — missing env vars: ${cls.missingEnvVars.join(", ")}.`
|
|
107778
|
-
};
|
|
107779
|
-
}
|
|
107780
|
-
const fileProblems = (vctx.buildProblems ?? []).filter((p) => p.file).map(buildProblemToRecoveryProblem);
|
|
107781
|
-
if (fileProblems.length > 0) {
|
|
107782
|
-
return {
|
|
107783
|
-
status: "needs_action",
|
|
107784
|
-
diagnosis: {
|
|
107785
|
-
title: cls?.title ?? "Build failed with file-located problems",
|
|
107786
|
-
explanation: cls?.explanation ?? `Build extracted ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} with file locations. Patch the files directly.`,
|
|
107787
|
-
reasonCode: "build_failed",
|
|
107788
|
-
phase: "build"
|
|
107789
|
-
},
|
|
107790
|
-
recovery: recoveryFixProblems({
|
|
107791
|
-
problems: fileProblems,
|
|
107792
|
-
reasonCode: "build_failed"
|
|
107793
|
-
}),
|
|
107794
|
-
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} to patch.` : `Build failed — ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} to patch.`
|
|
107795
|
-
};
|
|
107796
|
-
}
|
|
107968
|
+
},
|
|
107969
|
+
{
|
|
107970
|
+
id: "toml-invalid",
|
|
107971
|
+
when: ({ vctx, findCheck }) => findCheck("percher.toml")?.status === "fail" && !vctx.appProvided,
|
|
107972
|
+
verdict: ({ findCheck }) => {
|
|
107973
|
+
const message = findCheck("percher.toml")?.message ?? "";
|
|
107797
107974
|
return {
|
|
107798
|
-
status: "
|
|
107975
|
+
status: "blocked",
|
|
107799
107976
|
diagnosis: {
|
|
107800
|
-
title:
|
|
107801
|
-
explanation:
|
|
107802
|
-
reasonCode: "
|
|
107803
|
-
phase: "
|
|
107977
|
+
title: "Invalid percher.toml",
|
|
107978
|
+
explanation: message,
|
|
107979
|
+
reasonCode: "config_invalid",
|
|
107980
|
+
phase: "config"
|
|
107804
107981
|
},
|
|
107805
|
-
recovery:
|
|
107806
|
-
|
|
107807
|
-
|
|
107808
|
-
|
|
107982
|
+
recovery: recoveryFixConfig({
|
|
107983
|
+
problems: [
|
|
107984
|
+
{
|
|
107985
|
+
file: "percher.toml",
|
|
107986
|
+
message
|
|
107987
|
+
}
|
|
107988
|
+
],
|
|
107989
|
+
reasonCode: "config_invalid"
|
|
107809
107990
|
}),
|
|
107810
|
-
summary:
|
|
107991
|
+
summary: "percher.toml is invalid — fix the issues and re-run."
|
|
107811
107992
|
};
|
|
107812
107993
|
}
|
|
107813
|
-
|
|
107814
|
-
|
|
107815
|
-
|
|
107816
|
-
|
|
107817
|
-
|
|
107818
|
-
|
|
107819
|
-
|
|
107994
|
+
},
|
|
107995
|
+
{
|
|
107996
|
+
id: "app-unresolved",
|
|
107997
|
+
when: ({ findCheck }) => findCheck("App")?.status === "skip",
|
|
107998
|
+
verdict: ({ findCheck }) => {
|
|
107999
|
+
const message = findCheck("App")?.message ?? "";
|
|
108000
|
+
return {
|
|
108001
|
+
status: "blocked",
|
|
108002
|
+
diagnosis: {
|
|
108003
|
+
title: "No app specified",
|
|
108004
|
+
explanation: message,
|
|
108005
|
+
reasonCode: "config_missing",
|
|
108006
|
+
phase: "config"
|
|
108007
|
+
},
|
|
108008
|
+
recovery: recoveryAsk({
|
|
108009
|
+
prompt: `${message} — pass \`--app <name>\` or run \`percher init\` in a project directory to generate a percher.toml.`,
|
|
108010
|
+
reasonCode: "config_missing"
|
|
108011
|
+
}),
|
|
108012
|
+
summary: "No percher.toml found and no --app supplied — pass --app or run percher init."
|
|
108013
|
+
};
|
|
108014
|
+
}
|
|
108015
|
+
},
|
|
108016
|
+
{
|
|
108017
|
+
id: "deploy-transitional",
|
|
108018
|
+
when: ({ vctx }) => !!vctx.lastDeployStatus && TRANSITIONAL_DEPLOY_STATUSES.has(vctx.lastDeployStatus) || !!vctx.appStatus && TRANSITIONAL_APP_STATUSES.has(vctx.appStatus),
|
|
108019
|
+
verdict: ({ vctx }) => {
|
|
108020
|
+
const inTransitionalApp = !!vctx.appStatus && TRANSITIONAL_APP_STATUSES.has(vctx.appStatus);
|
|
108021
|
+
const reasonCode = vctx.lastDeployStatus === "queued" ? "deploy_queued" : vctx.lastDeployStatus === "building" ? "deploy_building" : vctx.lastDeployStatus === "deploying" ? "deploy_deploying" : "deploy_queued";
|
|
108022
|
+
const what = inTransitionalApp ? `App ${vctx.appName ?? ""} is provisioning` : `Last deploy is ${vctx.lastDeployStatus}`;
|
|
108023
|
+
const recovery = vctx.lastDeployId && vctx.appName ? recoveryWait({
|
|
108024
|
+
app: vctx.appName,
|
|
108025
|
+
deployId: vctx.lastDeployId,
|
|
108026
|
+
reasonCode
|
|
108027
|
+
}) : recoveryAsk({
|
|
108028
|
+
prompt: `${what.trim()} for ${vctx.appName ?? "this app"}. Wait for it to finish before retrying — there's no deployId to track yet.`,
|
|
108029
|
+
reasonCode
|
|
108030
|
+
});
|
|
108031
|
+
return {
|
|
108032
|
+
status: "in_progress",
|
|
108033
|
+
diagnosis: {
|
|
108034
|
+
title: what.trim(),
|
|
108035
|
+
explanation: vctx.lastDeployId ? `${what.trim()} (deploy ${vctx.lastDeployId}). Wait for it to finish before retrying.` : `${what.trim()}. Wait for it to finish before retrying.`,
|
|
108036
|
+
reasonCode,
|
|
108037
|
+
phase: "deploy"
|
|
108038
|
+
},
|
|
108039
|
+
recovery,
|
|
108040
|
+
summary: vctx.lastDeployId ? `${what.trim()} — wait for deploy ${vctx.lastDeployId}.` : `${what.trim()} — wait a moment and re-run.`
|
|
108041
|
+
};
|
|
108042
|
+
}
|
|
108043
|
+
},
|
|
108044
|
+
{
|
|
108045
|
+
id: "deploy-replaced",
|
|
108046
|
+
when: ({ vctx }) => !!vctx.replacedResolution,
|
|
108047
|
+
verdict: (ctx) => {
|
|
108048
|
+
const r = ctx.vctx.replacedResolution;
|
|
108049
|
+
if (!r)
|
|
108050
|
+
return FALLBACK_RULE.verdict(ctx);
|
|
108051
|
+
const isResolvedLive = r.recovery.nextAction === "none" && !!r.url;
|
|
108052
|
+
if (isResolvedLive) {
|
|
108053
|
+
return {
|
|
108054
|
+
status: "ok",
|
|
108055
|
+
recovery: { ...r.recovery, reasonCode: "replaced_by_newer" },
|
|
108056
|
+
summary: r.summary
|
|
108057
|
+
};
|
|
108058
|
+
}
|
|
108059
|
+
const status = r.recovery.nextAction === "wait_deploy" ? "in_progress" : "needs_action";
|
|
108060
|
+
return {
|
|
108061
|
+
status,
|
|
108062
|
+
diagnosis: {
|
|
108063
|
+
title: "Last deploy was replaced",
|
|
108064
|
+
explanation: r.summary,
|
|
108065
|
+
reasonCode: "replaced_by_newer",
|
|
108066
|
+
phase: "deploy"
|
|
108067
|
+
},
|
|
108068
|
+
recovery: { ...r.recovery, reasonCode: "replaced_by_newer" },
|
|
108069
|
+
summary: r.summary
|
|
108070
|
+
};
|
|
108071
|
+
}
|
|
108072
|
+
},
|
|
108073
|
+
{
|
|
108074
|
+
id: "deploy-failed-missing-env",
|
|
108075
|
+
when: ({ vctx }) => {
|
|
108076
|
+
const cls = vctx.buildLogClassification;
|
|
108077
|
+
return vctx.lastDeployStatus === "failed" && vctx.inputMode === "deploy" && !!cls && cls.errorClass === "missing_env" && cls.missingEnvVars.length > 0;
|
|
108078
|
+
},
|
|
108079
|
+
verdict: (ctx) => {
|
|
108080
|
+
const { vctx } = ctx;
|
|
108081
|
+
const cls = vctx.buildLogClassification;
|
|
108082
|
+
if (!cls)
|
|
108083
|
+
return FALLBACK_RULE.verdict(ctx);
|
|
108084
|
+
return {
|
|
108085
|
+
status: "needs_action",
|
|
108086
|
+
diagnosis: {
|
|
108087
|
+
title: cls.title,
|
|
108088
|
+
explanation: cls.explanation,
|
|
108089
|
+
reasonCode: "missing_env",
|
|
108090
|
+
phase: "build"
|
|
108091
|
+
},
|
|
108092
|
+
recovery: recoveryEnv({
|
|
108093
|
+
app: vctx.appName,
|
|
108094
|
+
keys: cls.missingEnvVars
|
|
108095
|
+
}),
|
|
108096
|
+
summary: vctx.appName ? `Build failed — missing env vars on ${vctx.appName}: ${cls.missingEnvVars.join(", ")}.` : `Build failed — missing env vars: ${cls.missingEnvVars.join(", ")}.`
|
|
108097
|
+
};
|
|
108098
|
+
}
|
|
108099
|
+
},
|
|
108100
|
+
{
|
|
108101
|
+
id: "deploy-failed-file-problems",
|
|
108102
|
+
when: ({ vctx }) => vctx.lastDeployStatus === "failed" && vctx.inputMode === "deploy" && (vctx.buildProblems ?? []).filter((p) => p.file).length > 0,
|
|
108103
|
+
verdict: ({ vctx }) => {
|
|
108104
|
+
const cls = vctx.buildLogClassification;
|
|
108105
|
+
const fileProblems = (vctx.buildProblems ?? []).filter((p) => p.file).map(buildProblemToRecoveryProblem);
|
|
108106
|
+
return {
|
|
108107
|
+
status: "needs_action",
|
|
108108
|
+
diagnosis: {
|
|
108109
|
+
title: cls?.title ?? "Build failed with file-located problems",
|
|
108110
|
+
explanation: cls?.explanation ?? `Build extracted ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} with file locations. Patch the files directly.`,
|
|
108111
|
+
reasonCode: "build_failed",
|
|
108112
|
+
phase: "build"
|
|
108113
|
+
},
|
|
108114
|
+
recovery: recoveryFixProblems({
|
|
108115
|
+
problems: fileProblems,
|
|
108116
|
+
reasonCode: "build_failed"
|
|
108117
|
+
}),
|
|
108118
|
+
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} to patch.` : `Build failed — ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} to patch.`
|
|
108119
|
+
};
|
|
108120
|
+
}
|
|
108121
|
+
},
|
|
108122
|
+
{
|
|
108123
|
+
id: "deploy-failed-inspect-log",
|
|
108124
|
+
when: ({ vctx }) => vctx.lastDeployStatus === "failed" && vctx.inputMode === "deploy",
|
|
108125
|
+
verdict: ({ vctx }) => {
|
|
108126
|
+
const cls = vctx.buildLogClassification;
|
|
108127
|
+
return {
|
|
108128
|
+
status: "needs_action",
|
|
108129
|
+
diagnosis: {
|
|
108130
|
+
title: cls?.title ?? "Last deploy failed",
|
|
108131
|
+
explanation: cls ? `${cls.explanation} Doctor couldn't extract a structured fix; fall back to the build log.` : vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} terminated in failed state. Doctor couldn't classify the cause from the build log; inspect it directly.` : "The most recent deploy ended in failed state. Doctor couldn't classify the cause; inspect the build log.",
|
|
108132
|
+
reasonCode: "build_failed",
|
|
108133
|
+
phase: "build"
|
|
108134
|
+
},
|
|
108135
|
+
recovery: recoveryInspectBuildLog({
|
|
108136
|
+
deployId: vctx.lastDeployId,
|
|
108137
|
+
app: vctx.appName,
|
|
108138
|
+
reasonCode: "build_failed"
|
|
108139
|
+
}),
|
|
108140
|
+
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — inspect the build log.` : "Last deploy failed — inspect the latest failed deploy's build log."
|
|
108141
|
+
};
|
|
108142
|
+
}
|
|
108143
|
+
},
|
|
108144
|
+
{
|
|
108145
|
+
id: "deploy-failed-dispatch",
|
|
108146
|
+
when: ({ vctx }) => vctx.lastDeployStatus === "failed",
|
|
108147
|
+
verdict: ({ vctx }) => ({
|
|
108148
|
+
status: "needs_action",
|
|
108149
|
+
diagnosis: {
|
|
108150
|
+
title: "Last deploy failed",
|
|
108151
|
+
explanation: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} terminated in failed state. Inspect the build log to find the cause.` : "The most recent deploy ended in failed state. Inspect the build log to find the cause.",
|
|
108152
|
+
reasonCode: "build_failed",
|
|
108153
|
+
phase: "build"
|
|
107820
108154
|
},
|
|
107821
108155
|
recovery: {
|
|
107822
108156
|
retryable: false,
|
|
@@ -107830,10 +108164,12 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107830
108164
|
reasonCode: "build_failed"
|
|
107831
108165
|
},
|
|
107832
108166
|
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — call percher_doctor with mode='deploy'.` : "Last deploy failed — call percher_doctor with mode='deploy'."
|
|
107833
|
-
}
|
|
107834
|
-
}
|
|
107835
|
-
|
|
107836
|
-
|
|
108167
|
+
})
|
|
108168
|
+
},
|
|
108169
|
+
{
|
|
108170
|
+
id: "app-suspended",
|
|
108171
|
+
when: ({ vctx }) => vctx.appStatus === "suspended",
|
|
108172
|
+
verdict: ({ vctx }) => ({
|
|
107837
108173
|
status: "needs_action",
|
|
107838
108174
|
diagnosis: {
|
|
107839
108175
|
title: "App is suspended",
|
|
@@ -107846,33 +108182,27 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107846
108182
|
prompt: vctx.appName ? `${vctx.appName} is suspended. Resume the app from the dashboard (or call the unsuspend API) before retrying.` : "This app is suspended. Resume it from the dashboard before retrying."
|
|
107847
108183
|
}),
|
|
107848
108184
|
summary: vctx.appName ? `${vctx.appName} is suspended — resume the app before retrying.` : "App is suspended — resume it before retrying."
|
|
107849
|
-
}
|
|
107850
|
-
}
|
|
107851
|
-
|
|
107852
|
-
|
|
107853
|
-
|
|
107854
|
-
|
|
107855
|
-
status
|
|
107856
|
-
|
|
107857
|
-
|
|
107858
|
-
|
|
107859
|
-
|
|
107860
|
-
|
|
107861
|
-
|
|
107862
|
-
|
|
107863
|
-
|
|
107864
|
-
|
|
107865
|
-
|
|
107866
|
-
|
|
107867
|
-
|
|
107868
|
-
|
|
107869
|
-
|
|
107870
|
-
if (healthCheck?.status === "fail")
|
|
107871
|
-
explanationBits.push(healthCheck.message);
|
|
107872
|
-
if (lastCrashCheck)
|
|
107873
|
-
explanationBits.push(lastCrashCheck.message);
|
|
107874
|
-
const explanation = explanationBits.length > 0 ? explanationBits.join(" ") : "Runtime is not responding — container or health check is failing.";
|
|
107875
|
-
if (vctx.inputMode === "runtime") {
|
|
108185
|
+
})
|
|
108186
|
+
},
|
|
108187
|
+
{
|
|
108188
|
+
id: "all-healthy",
|
|
108189
|
+
when: ({ verdictChecks }) => verdictChecks.every((c) => c.status === "pass" || c.status === "skip" || c.status === "warn"),
|
|
108190
|
+
verdict: ({ vctx, verdictChecks }) => {
|
|
108191
|
+
const passing = verdictChecks.filter((c) => c.status === "pass").length;
|
|
108192
|
+
const noun = passing === 1 ? "check" : "checks";
|
|
108193
|
+
return {
|
|
108194
|
+
status: "ok",
|
|
108195
|
+
recovery: recoveryNone({ reasonCode: "none" }),
|
|
108196
|
+
summary: vctx.appName ? `All ${passing} ${noun} passed for ${vctx.appName}.` : `All ${passing} ${noun} passed.`
|
|
108197
|
+
};
|
|
108198
|
+
}
|
|
108199
|
+
},
|
|
108200
|
+
{
|
|
108201
|
+
id: "runtime-crashed-report",
|
|
108202
|
+
when: ({ vctx, containerDown }) => (vctx.appStatus === "crashed" || containerDown) && vctx.inputMode === "runtime",
|
|
108203
|
+
verdict: (ctx) => {
|
|
108204
|
+
const { vctx } = ctx;
|
|
108205
|
+
const explanation = runtimeCrashExplanation(ctx);
|
|
107876
108206
|
const cr = vctx.crashReport;
|
|
107877
108207
|
const hasAnalysis = cr && cr.analysisStatus === "completed";
|
|
107878
108208
|
const promptParts = [];
|
|
@@ -107909,58 +108239,62 @@ ${cr.logs.join(`
|
|
|
107909
108239
|
summary: vctx.appName ? hasAnalysis ? `${vctx.appName} crashed — ${cr.suggestion ?? cr.explanation ?? "surface the crash details to the user."}` : `${vctx.appName} runtime is unhealthy — surface the crash details to the user.` : "Runtime is unhealthy — surface the crash details to the user."
|
|
107910
108240
|
};
|
|
107911
108241
|
}
|
|
107912
|
-
|
|
107913
|
-
|
|
107914
|
-
|
|
107915
|
-
|
|
107916
|
-
|
|
107917
|
-
|
|
107918
|
-
|
|
107919
|
-
|
|
107920
|
-
|
|
107921
|
-
|
|
107922
|
-
|
|
107923
|
-
|
|
107924
|
-
|
|
107925
|
-
|
|
107926
|
-
|
|
107927
|
-
|
|
107928
|
-
|
|
107929
|
-
|
|
107930
|
-
|
|
107931
|
-
|
|
107932
|
-
|
|
107933
|
-
|
|
107934
|
-
|
|
107935
|
-
|
|
107936
|
-
|
|
107937
|
-
|
|
107938
|
-
|
|
107939
|
-
|
|
107940
|
-
|
|
107941
|
-
|
|
107942
|
-
|
|
107943
|
-
|
|
107944
|
-
|
|
107945
|
-
|
|
107946
|
-
|
|
107947
|
-
|
|
107948
|
-
|
|
107949
|
-
|
|
107950
|
-
|
|
107951
|
-
|
|
107952
|
-
|
|
107953
|
-
|
|
107954
|
-
|
|
107955
|
-
|
|
107956
|
-
|
|
107957
|
-
|
|
107958
|
-
|
|
107959
|
-
|
|
107960
|
-
|
|
107961
|
-
|
|
107962
|
-
|
|
107963
|
-
|
|
108242
|
+
},
|
|
108243
|
+
{
|
|
108244
|
+
id: "runtime-crashed-dispatch",
|
|
108245
|
+
when: ({ vctx, containerDown }) => vctx.appStatus === "crashed" || containerDown,
|
|
108246
|
+
verdict: (ctx) => {
|
|
108247
|
+
const { vctx } = ctx;
|
|
108248
|
+
const explanation = runtimeCrashExplanation(ctx);
|
|
108249
|
+
return {
|
|
108250
|
+
status: "needs_action",
|
|
108251
|
+
diagnosis: {
|
|
108252
|
+
title: vctx.appStatus === "crashed" ? "App crashed" : "Runtime not responding",
|
|
108253
|
+
explanation,
|
|
108254
|
+
reasonCode: "runtime_crashed",
|
|
108255
|
+
phase: "runtime"
|
|
108256
|
+
},
|
|
108257
|
+
recovery: {
|
|
108258
|
+
retryable: false,
|
|
108259
|
+
nextAction: "run_doctor",
|
|
108260
|
+
suggestedTool: "percher_doctor",
|
|
108261
|
+
args: { app: vctx.appName, mode: "runtime" },
|
|
108262
|
+
reasonCode: "runtime_crashed"
|
|
108263
|
+
},
|
|
108264
|
+
summary: vctx.appName ? `${vctx.appName} runtime needs investigation — call percher_doctor with mode='runtime'.` : "Runtime needs investigation — call percher_doctor with mode='runtime'."
|
|
108265
|
+
};
|
|
108266
|
+
}
|
|
108267
|
+
},
|
|
108268
|
+
{
|
|
108269
|
+
id: "public-route-unhealthy",
|
|
108270
|
+
when: ({ vctx }) => vctx.publicRouteHealthy === false,
|
|
108271
|
+
verdict: ({ vctx, findCheck }) => {
|
|
108272
|
+
const routeCheck = findCheck("Public route");
|
|
108273
|
+
return {
|
|
108274
|
+
status: "needs_action",
|
|
108275
|
+
diagnosis: {
|
|
108276
|
+
title: "Public route is not responding",
|
|
108277
|
+
explanation: routeCheck?.message ?? "Container looks healthy but the public URL isn't responding — route reconcile usually self-heals.",
|
|
108278
|
+
reasonCode: "infra_transient",
|
|
108279
|
+
phase: "infra"
|
|
108280
|
+
},
|
|
108281
|
+
recovery: {
|
|
108282
|
+
retryable: true,
|
|
108283
|
+
nextAction: "retry",
|
|
108284
|
+
suggestedTool: "percher_doctor",
|
|
108285
|
+
args: { app: vctx.appName },
|
|
108286
|
+
reasonCode: "infra_transient"
|
|
108287
|
+
},
|
|
108288
|
+
summary: vctx.appName ? `${vctx.appName} public route is failing — likely transient, re-run doctor in a moment.` : "Public route is failing — likely transient, re-run doctor in a moment."
|
|
108289
|
+
};
|
|
108290
|
+
}
|
|
108291
|
+
},
|
|
108292
|
+
FALLBACK_RULE
|
|
108293
|
+
];
|
|
108294
|
+
function deriveVerdict(checks3, vctx) {
|
|
108295
|
+
const ctx = buildVerdictRuleContext(checks3, vctx);
|
|
108296
|
+
const rule = VERDICT_RULES.find((r) => r.when(ctx)) ?? FALLBACK_RULE;
|
|
108297
|
+
return rule.verdict(ctx);
|
|
107964
108298
|
}
|
|
107965
108299
|
// ../core/dist/commands/domains.js
|
|
107966
108300
|
var domainAddInputSchema = exports_external.object({
|
|
@@ -108994,31 +109328,6 @@ async function login(ctx, input = {}) {
|
|
|
108994
109328
|
code: "login.expired"
|
|
108995
109329
|
});
|
|
108996
109330
|
}
|
|
108997
|
-
// ../shared/dist/logs.js
|
|
108998
|
-
var MAX_LOG_SEARCH_LENGTH = 256;
|
|
108999
|
-
// ../shared/dist/platform-env.js
|
|
109000
|
-
var PERCHER_INJECTED_ENV_KEYS = new Set([
|
|
109001
|
-
"NODE_ENV",
|
|
109002
|
-
"PORT",
|
|
109003
|
-
"DATABASE_URL",
|
|
109004
|
-
"POCKETBASE_URL",
|
|
109005
|
-
"POCKETBASE_ADMIN_EMAIL",
|
|
109006
|
-
"POCKETBASE_ADMIN_PASSWORD",
|
|
109007
|
-
"PERCHER_APP_NAME",
|
|
109008
|
-
"PERCHER_APP_URL",
|
|
109009
|
-
"HTTP_PROXY",
|
|
109010
|
-
"HTTPS_PROXY",
|
|
109011
|
-
"NO_PROXY",
|
|
109012
|
-
"NODE_USE_ENV_PROXY"
|
|
109013
|
-
]);
|
|
109014
|
-
var PROXY_LOWERCASE_ALIASES = new Set([
|
|
109015
|
-
"http_proxy",
|
|
109016
|
-
"https_proxy",
|
|
109017
|
-
"no_proxy"
|
|
109018
|
-
]);
|
|
109019
|
-
// ../shared/dist/runtime.js
|
|
109020
|
-
var DEFAULT_NODE_VERSION_MAJOR = "24";
|
|
109021
|
-
var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
|
|
109022
109331
|
// ../core/dist/commands/logs.js
|
|
109023
109332
|
var logsInputSchema = exports_external.object({
|
|
109024
109333
|
app: exports_external.string().optional(),
|
|
@@ -110661,6 +110970,45 @@ async function open(ctx, input = {}) {
|
|
|
110661
110970
|
await ctx.openUrl(app.url);
|
|
110662
110971
|
return app.url;
|
|
110663
110972
|
}
|
|
110973
|
+
// ../core/dist/commands/preview-branch.js
|
|
110974
|
+
var previewBranchSetInputSchema = exports_external.object({
|
|
110975
|
+
app: exports_external.string().optional(),
|
|
110976
|
+
branch: exports_external.string().min(1, "Branch is required")
|
|
110977
|
+
});
|
|
110978
|
+
var previewBranchInputSchema = exports_external.object({
|
|
110979
|
+
app: exports_external.string().optional()
|
|
110980
|
+
});
|
|
110981
|
+
function resolveApp3(ctx, app) {
|
|
110982
|
+
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
110983
|
+
if (!name) {
|
|
110984
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
110985
|
+
code: "env.no-app",
|
|
110986
|
+
hint: "Pass an app name or run from a directory with percher.toml."
|
|
110987
|
+
});
|
|
110988
|
+
}
|
|
110989
|
+
return name;
|
|
110990
|
+
}
|
|
110991
|
+
async function previewBranchSet(ctx, input) {
|
|
110992
|
+
const appRef = resolveApp3(ctx, input.app);
|
|
110993
|
+
return ctx.client.apps.setPreviewBranch(appRef, input.branch);
|
|
110994
|
+
}
|
|
110995
|
+
async function previewBranchUnset(ctx, input = {}) {
|
|
110996
|
+
const appRef = resolveApp3(ctx, input.app);
|
|
110997
|
+
return ctx.client.apps.unsetPreviewBranch(appRef);
|
|
110998
|
+
}
|
|
110999
|
+
var setPreviewBranchToolInputSchema = exports_external.object({
|
|
111000
|
+
app: exports_external.string().optional(),
|
|
111001
|
+
branch: exports_external.string().nullable().describe("Branch to pin as the preview address; pass null to clear it.")
|
|
111002
|
+
});
|
|
111003
|
+
async function previewBranchShow(ctx, input = {}) {
|
|
111004
|
+
const appRef = resolveApp3(ctx, input.app);
|
|
111005
|
+
const app = await ctx.client.apps.get(appRef);
|
|
111006
|
+
return {
|
|
111007
|
+
previewBranch: app.previewBranch ?? null,
|
|
111008
|
+
previewUrl: app.previewUrl ?? null,
|
|
111009
|
+
status: app.previewStatus ?? "disabled"
|
|
111010
|
+
};
|
|
111011
|
+
}
|
|
110664
111012
|
// ../core/dist/commands/publish.js
|
|
110665
111013
|
import { existsSync as existsSync14 } from "node:fs";
|
|
110666
111014
|
import { join as join20 } from "node:path";
|
|
@@ -110796,10 +111144,10 @@ async function pollDeployment(opts) {
|
|
|
110796
111144
|
polled = true;
|
|
110797
111145
|
} catch (err) {
|
|
110798
111146
|
attempt++;
|
|
110799
|
-
const
|
|
110800
|
-
const isNetwork = msg.startsWith("network error:");
|
|
111147
|
+
const isNetwork = isTransientNetworkError(err);
|
|
110801
111148
|
if (!isNetwork || attempt >= maxRetries) {
|
|
110802
111149
|
if (isNetwork) {
|
|
111150
|
+
const msg = err.message ?? String(err);
|
|
110803
111151
|
throw new Error(`${msg}
|
|
110804
111152
|
|
|
110805
111153
|
The deploy may already be live — your network connection dropped mid-poll. Check with: percher deploys list ${opts.appName}`, { cause: err });
|
|
@@ -110961,7 +111309,7 @@ function classifyDeployRetry(err, attempt) {
|
|
|
110961
111309
|
category: "non_retryable"
|
|
110962
111310
|
};
|
|
110963
111311
|
}
|
|
110964
|
-
if (
|
|
111312
|
+
if (isTransientNetworkError(err)) {
|
|
110965
111313
|
const delay = NETWORK_BACKOFF_MS[idx];
|
|
110966
111314
|
if (delay === undefined) {
|
|
110967
111315
|
return {
|
|
@@ -111096,6 +111444,12 @@ function* walkDir(dir, base) {
|
|
|
111096
111444
|
}
|
|
111097
111445
|
|
|
111098
111446
|
// ../core/dist/commands/publish-api-error.js
|
|
111447
|
+
var DEPLOY_GATE_CODES = new Set([
|
|
111448
|
+
"RETRY_LIMIT_REACHED",
|
|
111449
|
+
"REQUIRED_ENV_MISSING",
|
|
111450
|
+
"ENV_KEY_UNDECLARED",
|
|
111451
|
+
"DAILY_QUOTA_EXCEEDED"
|
|
111452
|
+
]);
|
|
111099
111453
|
function classifyPublishApiError(err, input) {
|
|
111100
111454
|
if (!(err instanceof PercherApiError))
|
|
111101
111455
|
return null;
|
|
@@ -111120,6 +111474,18 @@ function classifyPublishApiError(err, input) {
|
|
|
111120
111474
|
const auth = matchAuthError(err);
|
|
111121
111475
|
if (auth)
|
|
111122
111476
|
return buildResult(auth, input);
|
|
111477
|
+
const retryLimit = matchRetryLimitReached(err, input);
|
|
111478
|
+
if (retryLimit)
|
|
111479
|
+
return buildResult(retryLimit, input);
|
|
111480
|
+
const requiredEnv = matchRequiredEnvMissing(err, input);
|
|
111481
|
+
if (requiredEnv)
|
|
111482
|
+
return buildResult(requiredEnv, input);
|
|
111483
|
+
const undeclaredEnv = matchEnvKeyUndeclared(err, input);
|
|
111484
|
+
if (undeclaredEnv)
|
|
111485
|
+
return buildResult(undeclaredEnv, input);
|
|
111486
|
+
const dailyQuota = matchDailyQuotaExceeded(err, input);
|
|
111487
|
+
if (dailyQuota)
|
|
111488
|
+
return buildResult(dailyQuota, input);
|
|
111123
111489
|
const clientError = matchGenericClientError(err);
|
|
111124
111490
|
if (clientError)
|
|
111125
111491
|
return buildResult(clientError, input);
|
|
@@ -111128,12 +111494,17 @@ function classifyPublishApiError(err, input) {
|
|
|
111128
111494
|
function buildResult(c, input) {
|
|
111129
111495
|
return {
|
|
111130
111496
|
status: "failed",
|
|
111497
|
+
...input.app !== undefined ? { app: input.app } : {},
|
|
111131
111498
|
fileCount: input.bundle.fileCount,
|
|
111132
111499
|
bytes: input.bundle.bytes,
|
|
111133
111500
|
error: {
|
|
111134
111501
|
title: c.title,
|
|
111135
111502
|
explanation: c.explanation,
|
|
111136
|
-
suggestion: c.suggestion
|
|
111503
|
+
suggestion: c.suggestion,
|
|
111504
|
+
...c.errorClass !== undefined ? { errorClass: c.errorClass } : {},
|
|
111505
|
+
...c.phase !== undefined ? { phase: c.phase } : {},
|
|
111506
|
+
...c.missingEnvVars !== undefined ? { missingEnvVars: c.missingEnvVars } : {},
|
|
111507
|
+
...c.relevantFiles !== undefined ? { relevantFiles: c.relevantFiles } : {}
|
|
111137
111508
|
},
|
|
111138
111509
|
recovery: c.recovery,
|
|
111139
111510
|
summary: c.summary,
|
|
@@ -111152,9 +111523,9 @@ function matchPlanLimit(err) {
|
|
|
111152
111523
|
return {
|
|
111153
111524
|
title: "Plan limit reached",
|
|
111154
111525
|
explanation: err.message,
|
|
111155
|
-
suggestion: "Upgrade your plan at https://percher.app/
|
|
111526
|
+
suggestion: "Upgrade your plan at https://percher.app/account/billing, or delete unused apps with `percher delete <name>`.",
|
|
111156
111527
|
recovery: recoveryAsk({
|
|
111157
|
-
prompt: `${err.message}. Upgrade at https://percher.app/
|
|
111528
|
+
prompt: `${err.message}. Upgrade at https://percher.app/account/billing, or delete an unused app with \`percher delete <name>\`.`,
|
|
111158
111529
|
options: ["upgrade", "delete unused app", "cancel"],
|
|
111159
111530
|
reasonCode: "quota_exceeded"
|
|
111160
111531
|
}),
|
|
@@ -111172,7 +111543,7 @@ function matchRateLimit(err) {
|
|
|
111172
111543
|
return {
|
|
111173
111544
|
title: "Rate limit hit",
|
|
111174
111545
|
explanation: err.message,
|
|
111175
|
-
suggestion: "Wait a few minutes and try again. If you need a higher cap, upgrade at https://percher.app/
|
|
111546
|
+
suggestion: "Wait a few minutes and try again. If you need a higher cap, upgrade at https://percher.app/account/billing.",
|
|
111176
111547
|
recovery: recoveryAsk({
|
|
111177
111548
|
prompt: `Rate limit: ${err.message}. Wait a few minutes then retry, or upgrade your plan.`,
|
|
111178
111549
|
options: ["wait and retry", "upgrade", "cancel"],
|
|
@@ -111190,7 +111561,7 @@ function matchConcurrentLimit(err) {
|
|
|
111190
111561
|
explanation: err.message,
|
|
111191
111562
|
suggestion: "Wait for one of your in-flight deploys to finish, or upgrade your plan to raise the concurrency cap.",
|
|
111192
111563
|
recovery: recoveryAsk({
|
|
111193
|
-
prompt: `${err.message} Wait for one to finish, or upgrade at https://percher.app/
|
|
111564
|
+
prompt: `${err.message} Wait for one to finish, or upgrade at https://percher.app/account/billing.`,
|
|
111194
111565
|
options: ["wait", "upgrade", "cancel"],
|
|
111195
111566
|
reasonCode: "deploy_already_in_progress"
|
|
111196
111567
|
}),
|
|
@@ -111265,7 +111636,7 @@ function matchEmailNotVerified(err) {
|
|
|
111265
111636
|
return {
|
|
111266
111637
|
title: "Email not verified",
|
|
111267
111638
|
explanation: err.message,
|
|
111268
|
-
suggestion: "Open the verification link in your inbox, or visit https://percher.app/
|
|
111639
|
+
suggestion: "Open the verification link in your inbox, or visit https://percher.app/account to resend the email. Re-publish after verifying.",
|
|
111269
111640
|
recovery: recoveryAsk({
|
|
111270
111641
|
prompt: `${err.message} Verify the email, then retry publish.`,
|
|
111271
111642
|
options: ["check inbox", "open settings"],
|
|
@@ -111299,6 +111670,94 @@ function matchAuthError(err) {
|
|
|
111299
111670
|
summary: `Permission denied (${err.status} ${err.code || "—"}): ${err.message}`
|
|
111300
111671
|
};
|
|
111301
111672
|
}
|
|
111673
|
+
function matchRetryLimitReached(err, input) {
|
|
111674
|
+
if (err.code !== "RETRY_LIMIT_REACHED" || input.app === undefined)
|
|
111675
|
+
return null;
|
|
111676
|
+
const appName = input.app.name;
|
|
111677
|
+
const extra = err.extra ?? {};
|
|
111678
|
+
const resetAtStr = extra.resetAt ?? "";
|
|
111679
|
+
const resetAtForPrompt = resetAtStr || "the cooldown window expires";
|
|
111680
|
+
return {
|
|
111681
|
+
title: "Retry limit reached",
|
|
111682
|
+
explanation: "Percher already auto-retried once after an `infra_unavailable` failure for this app within the last 10 minutes. We won't auto-retry again — the next attempt should be a deliberate user decision.",
|
|
111683
|
+
suggestion: `Wait until ${resetAtForPrompt}, then re-run \`percher publish\`. If the failure persists past that, the underlying issue is probably not transient — run \`percher doctor\` to diagnose.`,
|
|
111684
|
+
recovery: recoveryAsk({
|
|
111685
|
+
prompt: `Percher already retried once after an infrastructure failure for ${appName}. Wait until ${resetAtForPrompt} or surface to the user.`,
|
|
111686
|
+
options: ["wait before retrying", "inspect status"],
|
|
111687
|
+
reasonCode: "retry_limit_reached",
|
|
111688
|
+
retryable: false
|
|
111689
|
+
}),
|
|
111690
|
+
summary: `Retry limit reached for ${appName}${resetAtStr ? ` — wait until ${resetAtStr}` : ""}.`
|
|
111691
|
+
};
|
|
111692
|
+
}
|
|
111693
|
+
function matchRequiredEnvMissing(err, input) {
|
|
111694
|
+
if (err.code !== "REQUIRED_ENV_MISSING" || input.app === undefined)
|
|
111695
|
+
return null;
|
|
111696
|
+
const extra = err.extra ?? {};
|
|
111697
|
+
const keys = extra.keys ?? [];
|
|
111698
|
+
const source = extra.source ?? "contract";
|
|
111699
|
+
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";
|
|
111700
|
+
return {
|
|
111701
|
+
title: "Required env keys not set",
|
|
111702
|
+
explanation: `${keys.length} env ${keys.length === 1 ? "key is" : "keys are"} ${sourceText} but missing from the env store: ${keys.join(", ")}.`,
|
|
111703
|
+
suggestion: `Run \`bunx percher env set ${keys[0] ?? "KEY"}=value\` (and similarly for the rest) and re-publish.`,
|
|
111704
|
+
errorClass: "missing_env",
|
|
111705
|
+
phase: "config",
|
|
111706
|
+
missingEnvVars: keys,
|
|
111707
|
+
recovery: recoveryEnv({
|
|
111708
|
+
app: input.app.name,
|
|
111709
|
+
keys,
|
|
111710
|
+
reasonCode: "missing_env"
|
|
111711
|
+
}),
|
|
111712
|
+
summary: `Missing required env ${keys.length === 1 ? "key" : "keys"}: ${keys.join(", ")}.`
|
|
111713
|
+
};
|
|
111714
|
+
}
|
|
111715
|
+
function matchEnvKeyUndeclared(err, input) {
|
|
111716
|
+
if (err.code !== "ENV_KEY_UNDECLARED" || input.app === undefined)
|
|
111717
|
+
return null;
|
|
111718
|
+
const extra = err.extra ?? {};
|
|
111719
|
+
const apiProblems = extra.problems ?? [];
|
|
111720
|
+
const problems = apiProblems.map((p) => ({
|
|
111721
|
+
file: p.file,
|
|
111722
|
+
line: p.line,
|
|
111723
|
+
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)"}`
|
|
111724
|
+
}));
|
|
111725
|
+
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
111726
|
+
return {
|
|
111727
|
+
title: "Undeclared env key",
|
|
111728
|
+
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
111729
|
+
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.",
|
|
111730
|
+
errorClass: "config_invalid",
|
|
111731
|
+
phase: "config",
|
|
111732
|
+
relevantFiles: ["percher.toml"],
|
|
111733
|
+
recovery: recoveryFixConfig({
|
|
111734
|
+
problems,
|
|
111735
|
+
reasonCode: "env_key_undeclared"
|
|
111736
|
+
}),
|
|
111737
|
+
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`
|
|
111738
|
+
};
|
|
111739
|
+
}
|
|
111740
|
+
function matchDailyQuotaExceeded(err, input) {
|
|
111741
|
+
if (err.code !== "DAILY_QUOTA_EXCEEDED" || input.app === undefined)
|
|
111742
|
+
return null;
|
|
111743
|
+
const extra = err.extra ?? {};
|
|
111744
|
+
const kind = extra.kind ?? "live";
|
|
111745
|
+
const used = extra.used ?? 0;
|
|
111746
|
+
const limit = extra.limit ?? 0;
|
|
111747
|
+
const resetAt = extra.resetAt ?? "";
|
|
111748
|
+
const otherKind = kind === "live" ? "preview" : "live";
|
|
111749
|
+
return {
|
|
111750
|
+
title: "Daily deploy quota reached",
|
|
111751
|
+
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
111752
|
+
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/account/billing to raise this cap.`,
|
|
111753
|
+
recovery: recoveryAsk({
|
|
111754
|
+
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/account/billing.`,
|
|
111755
|
+
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
111756
|
+
reasonCode: "quota_exceeded"
|
|
111757
|
+
}),
|
|
111758
|
+
summary: `Daily ${kind}-deploy quota reached (${used}/${limit}). Resets ${resetAt}.`
|
|
111759
|
+
};
|
|
111760
|
+
}
|
|
111302
111761
|
function matchGenericClientError(err) {
|
|
111303
111762
|
if (err.status < 400 || err.status >= 500)
|
|
111304
111763
|
return null;
|
|
@@ -111362,13 +111821,12 @@ var publishInputSchema = exports_external.object({
|
|
|
111362
111821
|
noCache: exports_external.boolean().optional().describe("Skip the image-cache lookup and force a fresh build. Use this when you suspect the cached image doesn't reflect your latest source — the new build's resulting image still gets cached, so future deploys benefit."),
|
|
111363
111822
|
waitForLive: exports_external.boolean().optional().describe("If true (default), publish blocks until the deploy is live or failed. If false, publish returns as soon as the deploy is queued and the agent can resume with percher_wait_for_deploy. Recommended for AI agents — gives back control quickly with a deployId so they can decide between waiting, asking the user, or working on something else.")
|
|
111364
111823
|
});
|
|
111365
|
-
var MAX_BYTES = 500 * 1024 * 1024;
|
|
111366
111824
|
var SUSPICIOUS_BYTES = 50 * 1024 * 1024;
|
|
111367
111825
|
var SUSPICIOUS_FILES = 1e4;
|
|
111368
111826
|
function tomlPathFor2(cwd) {
|
|
111369
111827
|
return join20(cwd, "percher.toml");
|
|
111370
111828
|
}
|
|
111371
|
-
async function
|
|
111829
|
+
async function buildReplacedResult2(opts) {
|
|
111372
111830
|
const { ctx, app, deployment, tarball, cwd } = opts;
|
|
111373
111831
|
const resolved = await resolveReplaced({
|
|
111374
111832
|
ctx,
|
|
@@ -111406,33 +111864,6 @@ function buildAlreadyInProgressResult(opts) {
|
|
|
111406
111864
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111407
111865
|
};
|
|
111408
111866
|
}
|
|
111409
|
-
function buildRetryLimitReachedResult(opts) {
|
|
111410
|
-
const { err, app, tarball, cwd } = opts;
|
|
111411
|
-
const extra = err.extra ?? {};
|
|
111412
|
-
const resetAtStr = extra.resetAt ?? "";
|
|
111413
|
-
const resetAtForPrompt = resetAtStr || "the cooldown window expires";
|
|
111414
|
-
const prompt = `Percher already retried once after an infrastructure failure for ${app.name}. Wait until ${resetAtForPrompt} or surface to the user.`;
|
|
111415
|
-
return {
|
|
111416
|
-
status: "failed",
|
|
111417
|
-
app,
|
|
111418
|
-
fileCount: tarball.fileCount,
|
|
111419
|
-
bytes: tarball.bytes,
|
|
111420
|
-
error: {
|
|
111421
|
-
title: "Retry limit reached",
|
|
111422
|
-
explanation: "Percher already auto-retried once after an `infra_unavailable` failure for this app within the last 10 minutes. We won't auto-retry again — the next attempt should be a deliberate user decision.",
|
|
111423
|
-
suggestion: `Wait until ${resetAtForPrompt}, then re-run \`percher publish\`. If the failure persists past that, the underlying issue is probably not transient — run \`percher doctor\` to diagnose.`
|
|
111424
|
-
},
|
|
111425
|
-
recovery: recoveryAsk({
|
|
111426
|
-
prompt,
|
|
111427
|
-
options: ["wait before retrying", "inspect status"],
|
|
111428
|
-
reasonCode: "retry_limit_reached",
|
|
111429
|
-
retryable: false
|
|
111430
|
-
}),
|
|
111431
|
-
summary: `Retry limit reached for ${app.name}${resetAtStr ? ` — wait until ${resetAtStr}` : ""}.`,
|
|
111432
|
-
configPath: tomlPathFor2(cwd),
|
|
111433
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111434
|
-
};
|
|
111435
|
-
}
|
|
111436
111867
|
function buildLiveSummary(opts) {
|
|
111437
111868
|
const cacheTag = opts.cacheHit ? "cached, " : "";
|
|
111438
111869
|
const fwSuffix = opts.framework ? ` (${opts.framework}, ${cacheTag}${opts.totalSeconds}s)` : ` (${cacheTag}${opts.totalSeconds}s)`;
|
|
@@ -111555,7 +111986,7 @@ ${bullets}`,
|
|
|
111555
111986
|
ctx.status("[2/4] Packaging files...");
|
|
111556
111987
|
const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
111557
111988
|
const packageMs = Date.now() - t0;
|
|
111558
|
-
if (tarball.bytes >
|
|
111989
|
+
if (tarball.bytes > MAX_TARBALL_BYTES) {
|
|
111559
111990
|
return {
|
|
111560
111991
|
status: "failed",
|
|
111561
111992
|
fileCount: tarball.fileCount,
|
|
@@ -111650,12 +112081,26 @@ ${keyList}
|
|
|
111650
112081
|
ctx.status(`Build queue wait: ~${preflight.queueWaitEstimateSec}s — your build is queued behind ${preflight.queueDepth} other(s).`);
|
|
111651
112082
|
}
|
|
111652
112083
|
} catch {}
|
|
112084
|
+
return executeDeploy({
|
|
112085
|
+
ctx,
|
|
112086
|
+
config: config2,
|
|
112087
|
+
input,
|
|
112088
|
+
app,
|
|
112089
|
+
firstDeploy,
|
|
112090
|
+
tarball,
|
|
112091
|
+
t0,
|
|
112092
|
+
packageMs,
|
|
112093
|
+
missingBuildEnvKeys
|
|
112094
|
+
});
|
|
112095
|
+
}
|
|
112096
|
+
async function executeDeploy(opts) {
|
|
112097
|
+
const { ctx, config: config2, input, app, firstDeploy, tarball, t0, packageMs, missingBuildEnvKeys } = opts;
|
|
111653
112098
|
const uploadStart = Date.now();
|
|
111654
112099
|
ctx.status("[3/4] Uploading...");
|
|
111655
112100
|
const tarballBytes = new Uint8Array(await new Response(tarball.stream).arrayBuffer());
|
|
111656
112101
|
let cacheReusedDeployment = null;
|
|
111657
112102
|
const contentHash = tarball.contentHash;
|
|
111658
|
-
if (!input.noCache) {
|
|
112103
|
+
if (!input.noCache && !input.preview) {
|
|
111659
112104
|
try {
|
|
111660
112105
|
const probe = await ctx.client.platform.cacheProbe({
|
|
111661
112106
|
appId: app.id,
|
|
@@ -111701,99 +112146,14 @@ ${keyList}
|
|
|
111701
112146
|
}
|
|
111702
112147
|
deployment = deployResponse;
|
|
111703
112148
|
} catch (err) {
|
|
111704
|
-
if (err instanceof PercherApiError && err.code
|
|
111705
|
-
|
|
111706
|
-
err,
|
|
111707
|
-
app,
|
|
111708
|
-
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
111709
|
-
cwd: ctx.cwd
|
|
111710
|
-
});
|
|
111711
|
-
}
|
|
111712
|
-
if (err instanceof PercherApiError && err.code === "REQUIRED_ENV_MISSING") {
|
|
111713
|
-
const extra = err.extra ?? {};
|
|
111714
|
-
const keys = extra.keys ?? [];
|
|
111715
|
-
const source = extra.source ?? "contract";
|
|
111716
|
-
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";
|
|
111717
|
-
return {
|
|
111718
|
-
status: "failed",
|
|
111719
|
-
app,
|
|
111720
|
-
fileCount: tarball.fileCount,
|
|
111721
|
-
bytes: tarball.bytes,
|
|
111722
|
-
error: {
|
|
111723
|
-
title: "Required env keys not set",
|
|
111724
|
-
explanation: `${keys.length} env ${keys.length === 1 ? "key is" : "keys are"} ${sourceText} but missing from the env store: ${keys.join(", ")}.`,
|
|
111725
|
-
suggestion: `Run \`bunx percher env set ${keys[0] ?? "KEY"}=value\` (and similarly for the rest) and re-publish.`,
|
|
111726
|
-
errorClass: "missing_env",
|
|
111727
|
-
phase: "config",
|
|
111728
|
-
missingEnvVars: keys
|
|
111729
|
-
},
|
|
111730
|
-
recovery: recoveryEnv({
|
|
111731
|
-
app: app.name,
|
|
111732
|
-
keys,
|
|
111733
|
-
reasonCode: "missing_env"
|
|
111734
|
-
}),
|
|
111735
|
-
summary: `Missing required env ${keys.length === 1 ? "key" : "keys"}: ${keys.join(", ")}.`,
|
|
111736
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111737
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111738
|
-
};
|
|
111739
|
-
}
|
|
111740
|
-
if (err instanceof PercherApiError && err.code === "ENV_KEY_UNDECLARED") {
|
|
111741
|
-
const extra = err.extra ?? {};
|
|
111742
|
-
const apiProblems = extra.problems ?? [];
|
|
111743
|
-
const problems = apiProblems.map((p) => ({
|
|
111744
|
-
file: p.file,
|
|
111745
|
-
line: p.line,
|
|
111746
|
-
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)"}`
|
|
111747
|
-
}));
|
|
111748
|
-
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
111749
|
-
return {
|
|
111750
|
-
status: "failed",
|
|
111751
|
-
app,
|
|
111752
|
-
fileCount: tarball.fileCount,
|
|
111753
|
-
bytes: tarball.bytes,
|
|
111754
|
-
error: {
|
|
111755
|
-
title: "Undeclared env key",
|
|
111756
|
-
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
111757
|
-
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.",
|
|
111758
|
-
errorClass: "config_invalid",
|
|
111759
|
-
phase: "config",
|
|
111760
|
-
relevantFiles: ["percher.toml"]
|
|
111761
|
-
},
|
|
111762
|
-
recovery: recoveryFixConfig({
|
|
111763
|
-
problems,
|
|
111764
|
-
reasonCode: "env_key_undeclared"
|
|
111765
|
-
}),
|
|
111766
|
-
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`,
|
|
111767
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111768
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111769
|
-
};
|
|
111770
|
-
}
|
|
111771
|
-
if (err instanceof PercherApiError && err.code === "DAILY_QUOTA_EXCEEDED") {
|
|
111772
|
-
const extra = err.extra ?? {};
|
|
111773
|
-
const kind = extra.kind ?? "live";
|
|
111774
|
-
const used = extra.used ?? 0;
|
|
111775
|
-
const limit = extra.limit ?? 0;
|
|
111776
|
-
const resetAt = extra.resetAt ?? "";
|
|
111777
|
-
const otherKind = kind === "live" ? "preview" : "live";
|
|
111778
|
-
return {
|
|
111779
|
-
status: "failed",
|
|
111780
|
-
app,
|
|
111781
|
-
fileCount: tarball.fileCount,
|
|
111782
|
-
bytes: tarball.bytes,
|
|
111783
|
-
error: {
|
|
111784
|
-
title: "Daily deploy quota reached",
|
|
111785
|
-
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
111786
|
-
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/settings to raise this cap.`
|
|
111787
|
-
},
|
|
111788
|
-
recovery: recoveryAsk({
|
|
111789
|
-
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.`,
|
|
111790
|
-
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
111791
|
-
reasonCode: "quota_exceeded"
|
|
111792
|
-
}),
|
|
111793
|
-
summary: `Daily ${kind}-deploy quota reached (${used}/${limit}). Resets ${resetAt}.`,
|
|
112149
|
+
if (err instanceof PercherApiError && DEPLOY_GATE_CODES.has(err.code)) {
|
|
112150
|
+
const classified = classifyPublishApiError(err, {
|
|
111794
112151
|
configPath: tomlPathFor2(ctx.cwd),
|
|
111795
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111796
|
-
|
|
112152
|
+
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
112153
|
+
app
|
|
112154
|
+
});
|
|
112155
|
+
if (classified)
|
|
112156
|
+
return classified;
|
|
111797
112157
|
}
|
|
111798
112158
|
throw err;
|
|
111799
112159
|
}
|
|
@@ -111891,7 +112251,7 @@ ${keyList}
|
|
|
111891
112251
|
}
|
|
111892
112252
|
const buildMs = Date.now() - buildStart;
|
|
111893
112253
|
if (deployment.status === "replaced") {
|
|
111894
|
-
return await
|
|
112254
|
+
return await buildReplacedResult2({
|
|
111895
112255
|
ctx,
|
|
111896
112256
|
app,
|
|
111897
112257
|
deployment,
|
|
@@ -112003,150 +112363,21 @@ async function handleUnauthorized(ctx, config2, tarball, input) {
|
|
|
112003
112363
|
token: newToken,
|
|
112004
112364
|
sessionId: ctx.client.sessionId
|
|
112005
112365
|
});
|
|
112366
|
+
const t0 = Date.now();
|
|
112006
112367
|
const freshTarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
112007
|
-
|
|
112008
|
-
|
|
112009
|
-
({
|
|
112010
|
-
|
|
112011
|
-
|
|
112012
|
-
|
|
112013
|
-
let deployment;
|
|
112014
|
-
try {
|
|
112015
|
-
const { result: deployResponse } = await runDeployWithRetry({
|
|
112016
|
-
call: (attempt) => ctx.client.apps.deploy(app.id, {
|
|
112017
|
-
tarball: freshTarballBytes,
|
|
112018
|
-
type: input.preview ? "preview" : undefined,
|
|
112019
|
-
note: input.message,
|
|
112020
|
-
noCache: input.noCache,
|
|
112021
|
-
idempotencyKey: freshIdempotencyKey,
|
|
112022
|
-
retriedAttempts: attempt - 1
|
|
112023
|
-
}),
|
|
112024
|
-
onRetry: ({ attempt, decision }) => {
|
|
112025
|
-
ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
|
|
112026
|
-
}
|
|
112027
|
-
});
|
|
112028
|
-
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
112029
|
-
return buildAlreadyInProgressResult({
|
|
112030
|
-
active: deployResponse,
|
|
112031
|
-
app,
|
|
112032
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
112033
|
-
cwd: ctx.cwd
|
|
112034
|
-
});
|
|
112035
|
-
}
|
|
112036
|
-
deployment = deployResponse;
|
|
112037
|
-
} catch (err) {
|
|
112038
|
-
if (err instanceof PercherApiError && err.code === "RETRY_LIMIT_REACHED") {
|
|
112039
|
-
return buildRetryLimitReachedResult({
|
|
112040
|
-
err,
|
|
112041
|
-
app,
|
|
112042
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
112043
|
-
cwd: ctx.cwd
|
|
112044
|
-
});
|
|
112045
|
-
}
|
|
112046
|
-
throw err;
|
|
112047
|
-
}
|
|
112048
|
-
const replacedPreview = deployment.replacedPreview === true;
|
|
112049
|
-
if (input.waitForLive === false) {
|
|
112050
|
-
ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
|
|
112051
|
-
return {
|
|
112052
|
-
status: "queued",
|
|
112053
|
-
app,
|
|
112054
|
-
deployment,
|
|
112055
|
-
fileCount: freshTarball.fileCount,
|
|
112056
|
-
bytes: freshTarball.bytes,
|
|
112057
|
-
replacedPreview: replacedPreview || undefined,
|
|
112058
|
-
firstDeploy: firstDeployRetry || undefined,
|
|
112059
|
-
recovery: recoveryWait({
|
|
112060
|
-
app: app.name,
|
|
112061
|
-
deployId: deployment.id,
|
|
112062
|
-
reasonCode: "deploy_queued"
|
|
112063
|
-
}),
|
|
112064
|
-
summary: `${app.name} deploy ${deployment.id} queued — resume with percher_wait_for_deploy.`,
|
|
112065
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
112066
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
112067
|
-
};
|
|
112068
|
-
}
|
|
112069
|
-
const timeoutMs = TIMEOUTS.clientPublishPoll;
|
|
112070
|
-
const start = Date.now();
|
|
112071
|
-
while (deployment.status !== "live" && deployment.status !== "failed" && deployment.status !== "replaced") {
|
|
112072
|
-
if (Date.now() - start > timeoutMs) {
|
|
112073
|
-
return {
|
|
112074
|
-
status: "failed",
|
|
112075
|
-
app,
|
|
112076
|
-
deployment,
|
|
112077
|
-
fileCount: freshTarball.fileCount,
|
|
112078
|
-
bytes: freshTarball.bytes,
|
|
112079
|
-
error: {
|
|
112080
|
-
title: "Deploy timed out",
|
|
112081
|
-
explanation: "The deployment did not complete within 5 minutes.",
|
|
112082
|
-
suggestion: `Run \`percher doctor --app ${app.name}\` (CLI) or the percher_doctor tool (MCP) to diagnose the stall. Doctor will read deploy state + build log and return a concrete next step.`
|
|
112083
|
-
},
|
|
112084
|
-
recovery: recoveryDoctor({
|
|
112085
|
-
app: app.name,
|
|
112086
|
-
deployId: deployment.id,
|
|
112087
|
-
mode: "deploy",
|
|
112088
|
-
reasonCode: "deploy_stalled"
|
|
112089
|
-
}),
|
|
112090
|
-
summary: `Deploy timed out after 5 minutes (${deployment.id}).`,
|
|
112091
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
112092
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
112093
|
-
};
|
|
112094
|
-
}
|
|
112095
|
-
ctx.status(`${deployment.status}...`);
|
|
112096
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
112097
|
-
deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
|
|
112098
|
-
}
|
|
112099
|
-
if (deployment.status === "replaced") {
|
|
112100
|
-
return await buildReplacedResult({
|
|
112101
|
-
ctx,
|
|
112102
|
-
app,
|
|
112103
|
-
deployment,
|
|
112104
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
112105
|
-
cwd: ctx.cwd
|
|
112106
|
-
});
|
|
112107
|
-
}
|
|
112108
|
-
if (deployment.status === "failed") {
|
|
112109
|
-
return buildFailureResult({
|
|
112110
|
-
ctx,
|
|
112111
|
-
app,
|
|
112112
|
-
deployment,
|
|
112113
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
112114
|
-
input
|
|
112115
|
-
});
|
|
112116
|
-
}
|
|
112117
|
-
const totalSeconds = (Date.now() - start) / 1000;
|
|
112118
|
-
const url2 = deployment.previewUrl ?? deployment.url ?? app.url;
|
|
112119
|
-
ctx.status(`Live at ${url2} (${totalSeconds.toFixed(0)}s)`);
|
|
112120
|
-
return {
|
|
112121
|
-
status: "live",
|
|
112122
|
-
url: url2,
|
|
112368
|
+
const packageMs = Date.now() - t0;
|
|
112369
|
+
const { app, firstDeploy } = await ensureApp2(ctx, config2);
|
|
112370
|
+
return executeDeploy({
|
|
112371
|
+
ctx,
|
|
112372
|
+
config: config2,
|
|
112373
|
+
input,
|
|
112123
112374
|
app,
|
|
112124
|
-
|
|
112125
|
-
|
|
112126
|
-
|
|
112127
|
-
|
|
112128
|
-
|
|
112129
|
-
|
|
112130
|
-
},
|
|
112131
|
-
fileCount: freshTarball.fileCount,
|
|
112132
|
-
bytes: freshTarball.bytes,
|
|
112133
|
-
replacedPreview,
|
|
112134
|
-
firstDeploy: firstDeployRetry || undefined,
|
|
112135
|
-
recovery: RECOVERY_NONE,
|
|
112136
|
-
cacheHit: deployment.cacheHit,
|
|
112137
|
-
traceId: deployment.id,
|
|
112138
|
-
summary: buildLiveSummary({
|
|
112139
|
-
appName: app.name,
|
|
112140
|
-
url: url2 ?? app.url,
|
|
112141
|
-
totalSeconds: Math.round(totalSeconds),
|
|
112142
|
-
framework: config2.app.framework,
|
|
112143
|
-
preview: input.preview === true,
|
|
112144
|
-
replacedPreview,
|
|
112145
|
-
cacheHit: deployment.cacheHit ?? false
|
|
112146
|
-
}),
|
|
112147
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
112148
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
112149
|
-
};
|
|
112375
|
+
firstDeploy,
|
|
112376
|
+
tarball: freshTarball,
|
|
112377
|
+
t0,
|
|
112378
|
+
packageMs,
|
|
112379
|
+
missingBuildEnvKeys: []
|
|
112380
|
+
});
|
|
112150
112381
|
}
|
|
112151
112382
|
async function loadOrGenerate(ctx) {
|
|
112152
112383
|
const tomlPath = join20(ctx.cwd, "percher.toml");
|
|
@@ -112207,7 +112438,6 @@ var pushInputSchema = exports_external.object({
|
|
|
112207
112438
|
preview: exports_external.boolean().optional(),
|
|
112208
112439
|
noCache: exports_external.boolean().optional().describe("Skip the image-cache lookup and force a fresh build.")
|
|
112209
112440
|
});
|
|
112210
|
-
var MAX_BYTES2 = 500 * 1024 * 1024;
|
|
112211
112441
|
var SUSPICIOUS_BYTES2 = 50 * 1024 * 1024;
|
|
112212
112442
|
var SUSPICIOUS_FILES2 = 1e4;
|
|
112213
112443
|
var PLACEHOLDER_PREFIXES = ["add-your-", "your-key-here"];
|
|
@@ -112281,7 +112511,7 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
112281
112511
|
await checkPlaceholders(ctx, config2);
|
|
112282
112512
|
await checkByoSecrets(ctx, config2);
|
|
112283
112513
|
const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
112284
|
-
if (tarball.bytes >
|
|
112514
|
+
if (tarball.bytes > MAX_TARBALL_BYTES) {
|
|
112285
112515
|
throw new PercherCoreError(`Repository is larger than 500 MB (current: ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB)`, {
|
|
112286
112516
|
code: "push.too-large",
|
|
112287
112517
|
hint: `Biggest files: ${tarball.topFiles.map((f) => f.path).join(", ")}`,
|
|
@@ -112436,10 +112666,10 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
112436
112666
|
const resetAt = extra.resetAt ?? "";
|
|
112437
112667
|
throw new PercherCoreError(`Daily ${kind}-deploy quota reached (${used}/${limit}). Resets at ${resetAt}.`, {
|
|
112438
112668
|
code: "push.quota-exceeded",
|
|
112439
|
-
hint: `Wait until ${resetAt} or upgrade your plan at https://percher.app/
|
|
112669
|
+
hint: `Wait until ${resetAt} or upgrade your plan at https://percher.app/account/billing.`,
|
|
112440
112670
|
recovery: {
|
|
112441
112671
|
...recoveryAsk({
|
|
112442
|
-
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then or upgrade at https://percher.app/
|
|
112672
|
+
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then or upgrade at https://percher.app/account/billing.`,
|
|
112443
112673
|
reasonCode: "quota_exceeded"
|
|
112444
112674
|
})
|
|
112445
112675
|
}
|
|
@@ -112593,10 +112823,10 @@ async function redeploy(ctx, input = {}) {
|
|
|
112593
112823
|
error: {
|
|
112594
112824
|
title: "Daily deploy quota reached",
|
|
112595
112825
|
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
112596
|
-
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/
|
|
112826
|
+
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/account/billing to raise this cap.`
|
|
112597
112827
|
},
|
|
112598
112828
|
recovery: recoveryAsk({
|
|
112599
|
-
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/
|
|
112829
|
+
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/account/billing.`,
|
|
112600
112830
|
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
112601
112831
|
reasonCode: "quota_exceeded"
|
|
112602
112832
|
}),
|
|
@@ -112609,7 +112839,7 @@ async function redeploy(ctx, input = {}) {
|
|
|
112609
112839
|
return failureResult({
|
|
112610
112840
|
title: "Deploy rate limit reached",
|
|
112611
112841
|
explanation: err.message,
|
|
112612
|
-
suggestion: "Wait a minute or two and try again. Hourly limits scale with your plan; upgrade at https://percher.app/
|
|
112842
|
+
suggestion: "Wait a minute or two and try again. Hourly limits scale with your plan; upgrade at https://percher.app/account/billing to raise it."
|
|
112613
112843
|
});
|
|
112614
112844
|
case "DEPLOY_RATE_LIMITED": {
|
|
112615
112845
|
const extra = err.extra ?? {};
|
|
@@ -112836,6 +113066,8 @@ function failureResult(error48) {
|
|
|
112836
113066
|
};
|
|
112837
113067
|
}
|
|
112838
113068
|
// ../core/dist/commands/rename.js
|
|
113069
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
|
|
113070
|
+
import { join as join22 } from "node:path";
|
|
112839
113071
|
var renameInputSchema = exports_external.object({
|
|
112840
113072
|
app: exports_external.string().optional(),
|
|
112841
113073
|
name: exports_external.string().min(3).max(40)
|
|
@@ -112848,12 +113080,33 @@ async function rename(ctx, input) {
|
|
|
112848
113080
|
hint: "Pass an app name or run from a directory with percher.toml."
|
|
112849
113081
|
});
|
|
112850
113082
|
}
|
|
112851
|
-
|
|
113083
|
+
const result = await ctx.client.apps.rename(appRef, input.name);
|
|
113084
|
+
return {
|
|
113085
|
+
...result,
|
|
113086
|
+
tomlUpdated: updateLocalTomlName(ctx.cwd, result.previousName, result.name)
|
|
113087
|
+
};
|
|
113088
|
+
}
|
|
113089
|
+
function updateLocalTomlName(cwd, oldName, newName) {
|
|
113090
|
+
const p = join22(cwd, "percher.toml");
|
|
113091
|
+
if (!existsSync16(p))
|
|
113092
|
+
return false;
|
|
113093
|
+
try {
|
|
113094
|
+
const src = readFileSync12(p, "utf8");
|
|
113095
|
+
if (parse5(src).app.name !== oldName)
|
|
113096
|
+
return false;
|
|
113097
|
+
const next = src.replace(new RegExp(`(name\\s*=\\s*")${oldName}(")`), `$1${newName}$2`);
|
|
113098
|
+
if (next === src || parse5(next).app.name !== newName)
|
|
113099
|
+
return false;
|
|
113100
|
+
writeFileSync8(p, next);
|
|
113101
|
+
return true;
|
|
113102
|
+
} catch {
|
|
113103
|
+
return false;
|
|
113104
|
+
}
|
|
112852
113105
|
}
|
|
112853
113106
|
// ../core/dist/commands/reproduce.js
|
|
112854
|
-
import { mkdtempSync, writeFileSync as
|
|
113107
|
+
import { mkdtempSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
112855
113108
|
import { tmpdir } from "node:os";
|
|
112856
|
-
import { join as
|
|
113109
|
+
import { join as join23 } from "node:path";
|
|
112857
113110
|
var reproduceInputSchema = exports_external.object({
|
|
112858
113111
|
app: exports_external.string().optional().describe("App name. Defaults to the [app].name in the current directory's percher.toml."),
|
|
112859
113112
|
deployId: exports_external.string().describe("Deploy id (e.g. dep_abc123) to reproduce. Required — usually the failed deploy you want to debug.")
|
|
@@ -112937,9 +113190,9 @@ async function reproduce(ctx, input) {
|
|
|
112937
113190
|
const envMap = await ctx.client.env.list(app.id);
|
|
112938
113191
|
currentAppEnvKeys = Object.keys(envMap).sort();
|
|
112939
113192
|
} catch {}
|
|
112940
|
-
const tmpDir = mkdtempSync(
|
|
112941
|
-
const tarballPath =
|
|
112942
|
-
|
|
113193
|
+
const tmpDir = mkdtempSync(join23(tmpdir(), `percher-reproduce-${input.deployId}-`));
|
|
113194
|
+
const tarballPath = join23(tmpDir, "source.tar.gz");
|
|
113195
|
+
writeFileSync9(tarballPath, bytes);
|
|
112943
113196
|
let extracted = false;
|
|
112944
113197
|
try {
|
|
112945
113198
|
const proc = Bun.spawn(["tar", "-xzf", tarballPath, "-C", tmpDir], {
|
|
@@ -113177,7 +113430,7 @@ async function rollback(ctx, input, internals = {}) {
|
|
|
113177
113430
|
}
|
|
113178
113431
|
// ../core/dist/commands/sharing.js
|
|
113179
113432
|
var ShareRoleSchema = exports_external.enum(["viewer", "admin"]);
|
|
113180
|
-
function
|
|
113433
|
+
function resolveApp4(ctx, app) {
|
|
113181
113434
|
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
113182
113435
|
if (!name) {
|
|
113183
113436
|
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
@@ -113205,19 +113458,19 @@ var shareRevokeInputSchema = exports_external.object({
|
|
|
113205
113458
|
shareId: exports_external.string().min(1, "shareId is required")
|
|
113206
113459
|
});
|
|
113207
113460
|
async function shareList(ctx, input = {}) {
|
|
113208
|
-
const appRef =
|
|
113461
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113209
113462
|
return ctx.client.sharing.list(appRef);
|
|
113210
113463
|
}
|
|
113211
113464
|
async function shareInvite(ctx, input) {
|
|
113212
|
-
const appRef =
|
|
113465
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113213
113466
|
return ctx.client.sharing.create(appRef, { email: input.email, role: input.role });
|
|
113214
113467
|
}
|
|
113215
113468
|
async function shareSetRole(ctx, input) {
|
|
113216
|
-
const appRef =
|
|
113469
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113217
113470
|
return ctx.client.sharing.setRole(appRef, input.shareId, input.role);
|
|
113218
113471
|
}
|
|
113219
113472
|
async function shareRevoke(ctx, input) {
|
|
113220
|
-
const appRef =
|
|
113473
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113221
113474
|
await ctx.client.sharing.revoke(appRef, input.shareId);
|
|
113222
113475
|
}
|
|
113223
113476
|
var invitesListInputSchema = exports_external.object({});
|
|
@@ -113283,7 +113536,7 @@ function summarize2(verdict, health) {
|
|
|
113283
113536
|
return "API responded but reported a non-ok status";
|
|
113284
113537
|
}
|
|
113285
113538
|
// ../core/dist/commands/transfers.js
|
|
113286
|
-
function
|
|
113539
|
+
function resolveApp5(ctx, app) {
|
|
113287
113540
|
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
113288
113541
|
if (!name) {
|
|
113289
113542
|
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
@@ -113305,15 +113558,15 @@ var transferListForAppInputSchema = exports_external.object({
|
|
|
113305
113558
|
app: exports_external.string().optional()
|
|
113306
113559
|
});
|
|
113307
113560
|
async function transferInitiate(ctx, input) {
|
|
113308
|
-
const appRef =
|
|
113561
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
113309
113562
|
return ctx.client.transfers.initiate(appRef, { email: input.email });
|
|
113310
113563
|
}
|
|
113311
113564
|
async function transferCancel(ctx, input) {
|
|
113312
|
-
const appRef =
|
|
113565
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
113313
113566
|
await ctx.client.transfers.cancel(appRef, input.transferId);
|
|
113314
113567
|
}
|
|
113315
113568
|
async function transferListForApp(ctx, input = {}) {
|
|
113316
|
-
const appRef =
|
|
113569
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
113317
113570
|
return ctx.client.transfers.listForApp(appRef);
|
|
113318
113571
|
}
|
|
113319
113572
|
var transfersIncomingInputSchema = exports_external.object({});
|
|
@@ -114215,6 +114468,67 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114215
114468
|
const timing = result.timing ? ` (${result.timing.totalSeconds.toFixed(0)}s)` : "";
|
|
114216
114469
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} Redeployed to ${result.url}${timing}`);
|
|
114217
114470
|
});
|
|
114471
|
+
cli.command("wait-for-deploy <deployId>", "Wait for an in-flight deploy to finish").option("--app <name>", "App name (defaults to percher.toml app.name)").option("--timeout <seconds>", "Max seconds to wait (default: 600)").action(async (deployId, options) => {
|
|
114472
|
+
const appName = options.app ?? readPercherTomlAppName(ctx.cwd);
|
|
114473
|
+
if (!appName) {
|
|
114474
|
+
await writeLine(ctx, `${import_picocolors4.default.red("✗")} No app specified — pass --app <name> or run from a directory with percher.toml.`);
|
|
114475
|
+
throw new SilentExitError(1);
|
|
114476
|
+
}
|
|
114477
|
+
const timeoutSec = Number.parseInt(options.timeout ?? "600", 10);
|
|
114478
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) {
|
|
114479
|
+
await writeLine(ctx, `${import_picocolors4.default.red("✗")} --timeout must be a positive number of seconds.`);
|
|
114480
|
+
throw new SilentExitError(1);
|
|
114481
|
+
}
|
|
114482
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
114483
|
+
let lastStatus = "";
|
|
114484
|
+
for (;; ) {
|
|
114485
|
+
const remainingMs = deadline - Date.now();
|
|
114486
|
+
if (remainingMs <= 0) {
|
|
114487
|
+
await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
|
|
114488
|
+
throw new SilentExitError(1);
|
|
114489
|
+
}
|
|
114490
|
+
let raceTimer;
|
|
114491
|
+
let result = null;
|
|
114492
|
+
const callController = new AbortController;
|
|
114493
|
+
try {
|
|
114494
|
+
result = await Promise.race([
|
|
114495
|
+
waitForDeploy(ctx, {
|
|
114496
|
+
app: appName,
|
|
114497
|
+
deployId,
|
|
114498
|
+
timeoutSeconds: Math.min(Math.ceil(remainingMs / 1000), 30)
|
|
114499
|
+
}, { signal: callController.signal }),
|
|
114500
|
+
new Promise((resolve7) => {
|
|
114501
|
+
raceTimer = setTimeout(() => resolve7(null), remainingMs);
|
|
114502
|
+
})
|
|
114503
|
+
]);
|
|
114504
|
+
} finally {
|
|
114505
|
+
clearTimeout(raceTimer);
|
|
114506
|
+
}
|
|
114507
|
+
if (result === null) {
|
|
114508
|
+
callController.abort();
|
|
114509
|
+
await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
|
|
114510
|
+
throw new SilentExitError(1);
|
|
114511
|
+
}
|
|
114512
|
+
if (result.deployment.status !== lastStatus) {
|
|
114513
|
+
lastStatus = result.deployment.status;
|
|
114514
|
+
await writeLine(ctx, import_picocolors4.default.dim(`${lastStatus}...`));
|
|
114515
|
+
}
|
|
114516
|
+
if (result.status === "live") {
|
|
114517
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
|
|
114518
|
+
return;
|
|
114519
|
+
}
|
|
114520
|
+
if (result.status === "failed" || result.status === "replaced") {
|
|
114521
|
+
const mark = result.status === "failed" ? import_picocolors4.default.red("✗") : import_picocolors4.default.yellow("!");
|
|
114522
|
+
await writeLine(ctx, `${mark} ${result.summary}`);
|
|
114523
|
+
if (result.error?.suggestion) {
|
|
114524
|
+
await writeLine(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
|
|
114525
|
+
}
|
|
114526
|
+
if (result.status === "failed")
|
|
114527
|
+
throw new SilentExitError(1);
|
|
114528
|
+
return;
|
|
114529
|
+
}
|
|
114530
|
+
}
|
|
114531
|
+
});
|
|
114218
114532
|
cli.command("push", "Package and deploy this project").option("--dry-run", "Show what would be deployed, don't upload").option("--force", "Skip size warnings").option("--preview", "Deploy as preview (does not replace the live version)").option("--no-cache", "Skip image-cache lookup and force a fresh build (use when you suspect cached image is stale)").action(async (options) => {
|
|
114219
114533
|
const result = await push(ctx, {
|
|
114220
114534
|
dryRun: options.dryRun,
|
|
@@ -114245,6 +114559,11 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114245
114559
|
const result = await rename(ctx, { app, name });
|
|
114246
114560
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} Renamed to ${result.name} — now live at ${result.url}`);
|
|
114247
114561
|
await writeLine(ctx, ` Previous name: ${result.previousName} (reserved for 60 days)`);
|
|
114562
|
+
if (result.tomlUpdated) {
|
|
114563
|
+
await writeLine(ctx, ` percher.toml updated: name = "${result.name}"`);
|
|
114564
|
+
} else {
|
|
114565
|
+
await writeLine(ctx, ` ${import_picocolors4.default.yellow("!")} Update percher.toml before the next publish: name = "${result.name}"`);
|
|
114566
|
+
}
|
|
114248
114567
|
await writeLine(ctx, ` ${import_picocolors4.default.dim(`Next rename available in ${result.cooldownDays} days`)}`);
|
|
114249
114568
|
});
|
|
114250
114569
|
cli.command("open [app]", "Open the app URL in your browser").action(async (app) => {
|
|
@@ -114306,6 +114625,17 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114306
114625
|
const result = await resume(ctx, { app });
|
|
114307
114626
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
|
|
114308
114627
|
});
|
|
114628
|
+
cli.command("cache <action> [app]", "Manage the app's build cache (reset)").action(async (action, app) => {
|
|
114629
|
+
if (action !== "reset") {
|
|
114630
|
+
throw new Error(`Unknown cache action "${action}". Use: percher cache reset [app]`);
|
|
114631
|
+
}
|
|
114632
|
+
const result = await cacheReset(ctx, { app });
|
|
114633
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
|
|
114634
|
+
});
|
|
114635
|
+
cli.command("cache reset [app]", "Reset the app's build cache (next deploy runs cold)").action(async (app) => {
|
|
114636
|
+
const result = await cacheReset(ctx, { app });
|
|
114637
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
|
|
114638
|
+
});
|
|
114309
114639
|
cli.command("doctor [app]", "Check app health and connectivity").option("--json", "Output as JSON").action(async (app, options) => {
|
|
114310
114640
|
const result = await doctor(ctx, { app, json: options.json });
|
|
114311
114641
|
if (options.json) {
|
|
@@ -114586,24 +114916,60 @@ function registerDeploysCommands(cli, ctx, argv) {
|
|
|
114586
114916
|
await writeLine(ctx, `${v.sha} ${v.timestamp} ${message}`);
|
|
114587
114917
|
}
|
|
114588
114918
|
});
|
|
114589
|
-
cli.command("preview <action> [
|
|
114919
|
+
cli.command("preview <action> [arg] [arg2]", "Preview deploys: promote <id> | discard <id> | branch set <branch> | branch unset | branch show").option("--app <name>", "App name (overrides percher.toml)").action(async (action, arg, arg2, options) => {
|
|
114920
|
+
if (action === "branch") {
|
|
114921
|
+
await runPreviewBranch(arg, arg2, options.app);
|
|
114922
|
+
return;
|
|
114923
|
+
}
|
|
114590
114924
|
const appRef = options.app ?? readPercherTomlAppName(ctx.cwd);
|
|
114591
|
-
if (!appRef)
|
|
114925
|
+
if (!appRef) {
|
|
114592
114926
|
throw new Error("App name required. Use --app or run from a project directory.");
|
|
114927
|
+
}
|
|
114593
114928
|
if (action === "promote") {
|
|
114594
|
-
if (!
|
|
114929
|
+
if (!arg)
|
|
114595
114930
|
throw new Error("Deploy ID required. Usage: percher preview promote <id>");
|
|
114596
|
-
const result = await ctx.client.apps.promotePreview(appRef,
|
|
114931
|
+
const result = await ctx.client.apps.promotePreview(appRef, arg);
|
|
114597
114932
|
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview promoted to live. Status: ${result.status}`);
|
|
114598
114933
|
} else if (action === "discard") {
|
|
114599
|
-
if (!
|
|
114934
|
+
if (!arg)
|
|
114600
114935
|
throw new Error("Deploy ID required. Usage: percher preview discard <id>");
|
|
114601
|
-
await ctx.client.apps.discardPreview(appRef,
|
|
114936
|
+
await ctx.client.apps.discardPreview(appRef, arg);
|
|
114602
114937
|
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview discarded.`);
|
|
114603
114938
|
} else {
|
|
114604
|
-
throw new Error(`Unknown preview action "${action}". Use promote | discard.`);
|
|
114939
|
+
throw new Error(`Unknown preview action "${action}". Use promote | discard | branch.`);
|
|
114605
114940
|
}
|
|
114606
114941
|
});
|
|
114942
|
+
async function runPreviewBranch(sub, branch, app) {
|
|
114943
|
+
if (sub === "set") {
|
|
114944
|
+
if (!branch)
|
|
114945
|
+
throw new Error("Branch required. Usage: percher preview branch set <branch>");
|
|
114946
|
+
const r = await previewBranchSet(ctx, { app, branch });
|
|
114947
|
+
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview branch set to "${branch}".`);
|
|
114948
|
+
if (r.previewUrl)
|
|
114949
|
+
await writeLine(ctx, ` Your preview address: ${import_picocolors7.default.bold(r.previewUrl)}`);
|
|
114950
|
+
await writeLine(ctx, r.status === "error" ? import_picocolors7.default.yellow(" ⚠ The initial deploy failed — push to the branch to retry.") : import_picocolors7.default.dim(" Building now — live within ~90s."));
|
|
114951
|
+
return;
|
|
114952
|
+
}
|
|
114953
|
+
if (sub === "unset") {
|
|
114954
|
+
await previewBranchUnset(ctx, { app });
|
|
114955
|
+
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview branch cleared.`);
|
|
114956
|
+
return;
|
|
114957
|
+
}
|
|
114958
|
+
if (sub === "show" || sub === undefined) {
|
|
114959
|
+
const s = await previewBranchShow(ctx, { app });
|
|
114960
|
+
if (!s.previewBranch) {
|
|
114961
|
+
await writeLine(ctx, "No preview branch set.");
|
|
114962
|
+
await writeLine(ctx, import_picocolors7.default.dim(" Set one: percher preview branch set <branch>"));
|
|
114963
|
+
return;
|
|
114964
|
+
}
|
|
114965
|
+
await writeLine(ctx, `Preview branch: ${import_picocolors7.default.bold(s.previewBranch)}`);
|
|
114966
|
+
if (s.previewUrl)
|
|
114967
|
+
await writeLine(ctx, ` Address: ${s.previewUrl}`);
|
|
114968
|
+
await writeLine(ctx, ` Status: ${s.status}`);
|
|
114969
|
+
return;
|
|
114970
|
+
}
|
|
114971
|
+
throw new Error(`Unknown preview branch action "${sub}". Use set <branch> | unset | show.`);
|
|
114972
|
+
}
|
|
114607
114973
|
}
|
|
114608
114974
|
|
|
114609
114975
|
// src/commands/env.ts
|
|
@@ -115198,7 +115564,7 @@ async function renderError(stderr, error48) {
|
|
|
115198
115564
|
// package.json
|
|
115199
115565
|
var package_default = {
|
|
115200
115566
|
name: "percher",
|
|
115201
|
-
version: "0.
|
|
115567
|
+
version: "0.5.0",
|
|
115202
115568
|
description: "CLI for the Percher hosting platform — publish apps with one command",
|
|
115203
115569
|
type: "module",
|
|
115204
115570
|
bin: {
|
|
@@ -115279,18 +115645,18 @@ async function showTelemetryNotice() {
|
|
|
115279
115645
|
if (isDisabled())
|
|
115280
115646
|
return;
|
|
115281
115647
|
const { homedir: homedir2 } = await import("node:os");
|
|
115282
|
-
const { existsSync:
|
|
115283
|
-
const { join:
|
|
115284
|
-
const configDir =
|
|
115285
|
-
const markerFile =
|
|
115286
|
-
if (
|
|
115648
|
+
const { existsSync: existsSync17, mkdirSync: mkdirSync6, writeFileSync: writeFileSync10 } = await import("node:fs");
|
|
115649
|
+
const { join: join24 } = await import("node:path");
|
|
115650
|
+
const configDir = join24(homedir2(), ".percher");
|
|
115651
|
+
const markerFile = join24(configDir, ".telemetry-notice-shown");
|
|
115652
|
+
if (existsSync17(markerFile))
|
|
115287
115653
|
return;
|
|
115288
115654
|
console.log("Percher sends non-account-linked usage stats (command, result, error type, OS, Bun version).");
|
|
115289
115655
|
console.log(`Disable with PERCHER_NO_TELEMETRY=1
|
|
115290
115656
|
`);
|
|
115291
115657
|
try {
|
|
115292
115658
|
mkdirSync6(configDir, { recursive: true });
|
|
115293
|
-
|
|
115659
|
+
writeFileSync10(markerFile, new Date().toISOString());
|
|
115294
115660
|
} catch {}
|
|
115295
115661
|
}
|
|
115296
115662
|
|