percher 0.4.1 → 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 +1422 -687
- package/dist/package.json +1 -1
- package/dist/src/cli-utils.d.ts +2 -0
- package/dist/src/cli-utils.d.ts.map +1 -1
- package/dist/src/cli-utils.js +16 -0
- package/dist/src/cli-utils.js.map +1 -1
- package/dist/src/commands/apps.d.ts.map +1 -1
- package/dist/src/commands/apps.js +115 -1
- 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/dist/src/commands/export.d.ts +4 -0
- package/dist/src/commands/export.d.ts.map +1 -0
- package/dist/src/commands/export.js +18 -0
- package/dist/src/commands/export.js.map +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/output.d.ts.map +1 -1
- package/dist/src/output.js +18 -1
- package/dist/src/output.js.map +1 -1
- package/package.json +4 -4
package/dist/bin/percher.js
CHANGED
|
@@ -8824,7 +8824,7 @@ var require_util = __commonJS((exports) => {
|
|
|
8824
8824
|
}
|
|
8825
8825
|
path = url2.path;
|
|
8826
8826
|
}
|
|
8827
|
-
var
|
|
8827
|
+
var isAbsolute3 = exports.isAbsolute(path);
|
|
8828
8828
|
var parts = path.split(/\/+/);
|
|
8829
8829
|
for (var part, up = 0, i = parts.length - 1;i >= 0; i--) {
|
|
8830
8830
|
part = parts[i];
|
|
@@ -8844,7 +8844,7 @@ var require_util = __commonJS((exports) => {
|
|
|
8844
8844
|
}
|
|
8845
8845
|
path = parts.join("/");
|
|
8846
8846
|
if (path === "") {
|
|
8847
|
-
path =
|
|
8847
|
+
path = isAbsolute3 ? "/" : ".";
|
|
8848
8848
|
}
|
|
8849
8849
|
if (url2) {
|
|
8850
8850
|
url2.path = path;
|
|
@@ -101395,7 +101395,7 @@ function finalize(ctx, schema) {
|
|
|
101395
101395
|
result.$schema = "http://json-schema.org/draft-07/schema#";
|
|
101396
101396
|
} else if (ctx.target === "draft-04") {
|
|
101397
101397
|
result.$schema = "http://json-schema.org/draft-04/schema#";
|
|
101398
|
-
} else if (ctx.target === "openapi-3.0") {}
|
|
101398
|
+
} else if (ctx.target === "openapi-3.0") {}
|
|
101399
101399
|
if (ctx.external?.uri) {
|
|
101400
101400
|
const id = ctx.external.registry.get(schema)?.id;
|
|
101401
101401
|
if (!id)
|
|
@@ -101643,7 +101643,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
101643
101643
|
if (val === undefined) {
|
|
101644
101644
|
if (ctx.unrepresentable === "throw") {
|
|
101645
101645
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
101646
|
-
}
|
|
101646
|
+
}
|
|
101647
101647
|
} else if (typeof val === "bigint") {
|
|
101648
101648
|
if (ctx.unrepresentable === "throw") {
|
|
101649
101649
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -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();
|
|
@@ -104202,8 +104241,18 @@ var devSchema = exports_external.object({
|
|
|
104202
104241
|
var requiredEnvSchema = exports_external.record(exports_external.string(), exports_external.enum(["secret", "url", "string"]));
|
|
104203
104242
|
var cronSchema = exports_external.object({
|
|
104204
104243
|
schedule: exports_external.string().min(1),
|
|
104205
|
-
command: exports_external.string().min(1)
|
|
104244
|
+
command: exports_external.string().min(1),
|
|
104245
|
+
description: exports_external.string().max(200).optional()
|
|
104206
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
|
+
});
|
|
104207
104256
|
var addonsSchema = exports_external.object({
|
|
104208
104257
|
pocketbase: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
|
|
104209
104258
|
cron: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
|
|
@@ -104228,7 +104277,7 @@ var percherConfigSchema = exports_external.object({
|
|
|
104228
104277
|
resources: resourcesSchema.optional(),
|
|
104229
104278
|
dev: devSchema.optional(),
|
|
104230
104279
|
required_env: requiredEnvSchema.optional(),
|
|
104231
|
-
crons:
|
|
104280
|
+
crons: cronsSchema.optional(),
|
|
104232
104281
|
addons: addonsSchema.optional(),
|
|
104233
104282
|
checks: exports_external.record(exports_external.string(), checkSchema).optional()
|
|
104234
104283
|
}).strict();
|
|
@@ -104899,6 +104948,20 @@ var billingUpgradeInputSchema = exports_external.object({
|
|
|
104899
104948
|
plan: exports_external.enum(["starter", "maker", "pro"])
|
|
104900
104949
|
});
|
|
104901
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
|
+
}
|
|
104902
104965
|
// ../core/dist/commands/create.js
|
|
104903
104966
|
import { join as join6, resolve as resolve4 } from "node:path";
|
|
104904
104967
|
|
|
@@ -104958,9 +105021,8 @@ function scaffoldTemplate(input) {
|
|
|
104958
105021
|
}
|
|
104959
105022
|
|
|
104960
105023
|
// ../core/dist/commands/create.js
|
|
104961
|
-
var APP_NAME_RE2 = /^[a-z][a-z0-9-]{2,39}$/;
|
|
104962
105024
|
var createInputSchema = exports_external.object({
|
|
104963
|
-
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"),
|
|
104964
105026
|
template: exports_external.string().min(1, "Template is required"),
|
|
104965
105027
|
directory: exports_external.string().optional(),
|
|
104966
105028
|
force: exports_external.boolean().optional()
|
|
@@ -105015,6 +105077,7 @@ var dashboardInputSchema = exports_external.object({
|
|
|
105015
105077
|
});
|
|
105016
105078
|
var DEFAULT_BASE = "https://percher.app";
|
|
105017
105079
|
var TAB_SHAPE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
105080
|
+
var SETTINGS_ANCHOR_TABS = new Set(["github", "env", "domains", "crash-diagnosis"]);
|
|
105018
105081
|
async function dashboard(ctx, input = {}) {
|
|
105019
105082
|
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
105020
105083
|
if (!name) {
|
|
@@ -105031,7 +105094,8 @@ async function dashboard(ctx, input = {}) {
|
|
|
105031
105094
|
});
|
|
105032
105095
|
}
|
|
105033
105096
|
const base = ctx.env.PERCHER_DASHBOARD_URL ?? DEFAULT_BASE;
|
|
105034
|
-
const
|
|
105097
|
+
const appBase = `${base}/apps/${encodeURIComponent(name)}`;
|
|
105098
|
+
const url2 = SETTINGS_ANCHOR_TABS.has(tab) ? `${appBase}/settings#${tab}` : `${appBase}?tab=${tab}`;
|
|
105035
105099
|
await ctx.openUrl(url2);
|
|
105036
105100
|
return url2;
|
|
105037
105101
|
}
|
|
@@ -105123,6 +105187,28 @@ async function deleteApp(ctx, input) {
|
|
|
105123
105187
|
}
|
|
105124
105188
|
await ctx.client.apps.delete(name);
|
|
105125
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
|
+
}
|
|
105126
105212
|
// ../client/dist/build-env-prefixes.js
|
|
105127
105213
|
var AUTO_FORWARD_PREFIXES = [
|
|
105128
105214
|
"NEXT_PUBLIC_",
|
|
@@ -105195,6 +105281,21 @@ class PercherApiError extends Error {
|
|
|
105195
105281
|
this.extra = options.extra;
|
|
105196
105282
|
}
|
|
105197
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
|
+
}
|
|
105198
105299
|
var RESTORE_JOB_STATUSES_FOR_ERROR = [
|
|
105199
105300
|
"preparing",
|
|
105200
105301
|
"quiescing",
|
|
@@ -105221,7 +105322,33 @@ function isRestoreJobStatus(v) {
|
|
|
105221
105322
|
return typeof v === "string" && RESTORE_JOB_STATUSES_FOR_ERROR.includes(v);
|
|
105222
105323
|
}
|
|
105223
105324
|
|
|
105325
|
+
class SunsetError extends PercherApiError {
|
|
105326
|
+
gateCode;
|
|
105327
|
+
sunsetDate;
|
|
105328
|
+
finalDeletionDate;
|
|
105329
|
+
dashboardUrl;
|
|
105330
|
+
constructor(message, options) {
|
|
105331
|
+
super(message, options);
|
|
105332
|
+
this.name = "SunsetError";
|
|
105333
|
+
this.gateCode = options.gateCode;
|
|
105334
|
+
this.sunsetDate = options.sunsetDate;
|
|
105335
|
+
this.finalDeletionDate = options.finalDeletionDate;
|
|
105336
|
+
this.dashboardUrl = options.dashboardUrl;
|
|
105337
|
+
}
|
|
105338
|
+
}
|
|
105339
|
+
function isSunsetGateBody(body) {
|
|
105340
|
+
if (typeof body !== "object" || body === null)
|
|
105341
|
+
return false;
|
|
105342
|
+
const b = body;
|
|
105343
|
+
return (b.error === "platform_read_only" || b.error === "platform_delete_gated") && typeof b.sunsetDate === "string" && typeof b.finalDeletionDate === "string" && typeof b.dashboardUrl === "string";
|
|
105344
|
+
}
|
|
105345
|
+
|
|
105224
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
|
+
|
|
105225
105352
|
class Http {
|
|
105226
105353
|
apiUrl;
|
|
105227
105354
|
token;
|
|
@@ -105235,8 +105362,8 @@ class Http {
|
|
|
105235
105362
|
this.clientVersion = options.clientVersion;
|
|
105236
105363
|
this.sessionId = options.sessionId;
|
|
105237
105364
|
}
|
|
105238
|
-
get(path) {
|
|
105239
|
-
return this.request("GET", path);
|
|
105365
|
+
get(path, opts) {
|
|
105366
|
+
return this.request("GET", path, undefined, opts?.signal);
|
|
105240
105367
|
}
|
|
105241
105368
|
post(path, body) {
|
|
105242
105369
|
return this.request("POST", path, body);
|
|
@@ -105274,7 +105401,7 @@ class Http {
|
|
|
105274
105401
|
try {
|
|
105275
105402
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, { method: "GET", headers });
|
|
105276
105403
|
} catch (err) {
|
|
105277
|
-
throw new
|
|
105404
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105278
105405
|
}
|
|
105279
105406
|
if (!res.ok)
|
|
105280
105407
|
throw await this.parseError(res);
|
|
@@ -105300,15 +105427,15 @@ class Http {
|
|
|
105300
105427
|
try {
|
|
105301
105428
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
105302
105429
|
} catch (err) {
|
|
105303
|
-
throw new
|
|
105430
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105304
105431
|
}
|
|
105305
105432
|
if (!res.ok)
|
|
105306
105433
|
throw await this.parseError(res);
|
|
105307
105434
|
return await res.json();
|
|
105308
105435
|
}
|
|
105309
|
-
async request(method, path, body) {
|
|
105436
|
+
async request(method, path, body, signal) {
|
|
105310
105437
|
const headers = this.buildHeaders();
|
|
105311
|
-
const init = { method, headers };
|
|
105438
|
+
const init = { method, headers, signal };
|
|
105312
105439
|
if (body !== undefined) {
|
|
105313
105440
|
headers["Content-Type"] = "application/json";
|
|
105314
105441
|
init.body = JSON.stringify(body);
|
|
@@ -105317,7 +105444,7 @@ class Http {
|
|
|
105317
105444
|
try {
|
|
105318
105445
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
105319
105446
|
} catch (err) {
|
|
105320
|
-
throw new
|
|
105447
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105321
105448
|
}
|
|
105322
105449
|
if (!res.ok)
|
|
105323
105450
|
throw await this.parseError(res);
|
|
@@ -105328,7 +105455,10 @@ class Http {
|
|
|
105328
105455
|
return await res.json();
|
|
105329
105456
|
if (contentType.startsWith("text/"))
|
|
105330
105457
|
return await res.text();
|
|
105331
|
-
|
|
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" });
|
|
105332
105462
|
}
|
|
105333
105463
|
buildHeaders() {
|
|
105334
105464
|
const headers = {
|
|
@@ -105342,10 +105472,23 @@ class Http {
|
|
|
105342
105472
|
return headers;
|
|
105343
105473
|
}
|
|
105344
105474
|
async parseError(res) {
|
|
105345
|
-
let
|
|
105475
|
+
let raw;
|
|
105346
105476
|
try {
|
|
105347
|
-
|
|
105477
|
+
raw = await res.json();
|
|
105348
105478
|
} catch {}
|
|
105479
|
+
if (res.status === 503 && isSunsetGateBody(raw)) {
|
|
105480
|
+
const requestIdHdr = res.headers.get("x-request-id") ?? undefined;
|
|
105481
|
+
return new SunsetError(raw.message, {
|
|
105482
|
+
status: res.status,
|
|
105483
|
+
code: raw.error,
|
|
105484
|
+
requestId: requestIdHdr,
|
|
105485
|
+
gateCode: raw.error,
|
|
105486
|
+
sunsetDate: raw.sunsetDate,
|
|
105487
|
+
finalDeletionDate: raw.finalDeletionDate,
|
|
105488
|
+
dashboardUrl: raw.dashboardUrl
|
|
105489
|
+
});
|
|
105490
|
+
}
|
|
105491
|
+
const body = typeof raw === "object" && raw !== null ? raw : {};
|
|
105349
105492
|
const code = body.error?.code ?? (res.status >= 500 ? "INTERNAL_ERROR" : "VALIDATION_ERROR");
|
|
105350
105493
|
const message = body.error?.message ?? res.statusText ?? "request failed";
|
|
105351
105494
|
const extra = {};
|
|
@@ -105385,6 +105528,12 @@ class AccountResource {
|
|
|
105385
105528
|
getDiagnostics() {
|
|
105386
105529
|
return this.http.get("/account/diagnostics");
|
|
105387
105530
|
}
|
|
105531
|
+
listTokens() {
|
|
105532
|
+
return this.http.get("/account/tokens");
|
|
105533
|
+
}
|
|
105534
|
+
revokeToken(id) {
|
|
105535
|
+
return this.http.delete(`/account/tokens/${encodeURIComponent(id)}`);
|
|
105536
|
+
}
|
|
105388
105537
|
exportData() {
|
|
105389
105538
|
return this.http.get("/account/export");
|
|
105390
105539
|
}
|
|
@@ -105410,11 +105559,12 @@ class AlertsResource {
|
|
|
105410
105559
|
}
|
|
105411
105560
|
}
|
|
105412
105561
|
|
|
105413
|
-
// ../client/dist/resources/apps.js
|
|
105562
|
+
// ../client/dist/resources/apps-types.js
|
|
105414
105563
|
function isDeployAlreadyInProgress(res) {
|
|
105415
105564
|
return res.status === "already_in_progress";
|
|
105416
105565
|
}
|
|
105417
105566
|
|
|
105567
|
+
// ../client/dist/resources/apps.js
|
|
105418
105568
|
class AppsResource {
|
|
105419
105569
|
http;
|
|
105420
105570
|
constructor(http) {
|
|
@@ -105426,8 +105576,11 @@ class AppsResource {
|
|
|
105426
105576
|
list() {
|
|
105427
105577
|
return this.http.get("/apps");
|
|
105428
105578
|
}
|
|
105429
|
-
|
|
105430
|
-
return this.http.get(
|
|
105579
|
+
listLight() {
|
|
105580
|
+
return this.http.get("/apps?light=1");
|
|
105581
|
+
}
|
|
105582
|
+
get(id, opts) {
|
|
105583
|
+
return this.http.get(`/apps/${encodeURIComponent(id)}`, opts);
|
|
105431
105584
|
}
|
|
105432
105585
|
async deploy(id, input) {
|
|
105433
105586
|
const params = new URLSearchParams;
|
|
@@ -105461,6 +105614,43 @@ class AppsResource {
|
|
|
105461
105614
|
crashAnalysisEnabled: enabled
|
|
105462
105615
|
});
|
|
105463
105616
|
}
|
|
105617
|
+
setGalleryOptIn(appRef, enabled) {
|
|
105618
|
+
return this.http.put(`/apps/${encodeURIComponent(appRef)}/gallery-opt-in`, {
|
|
105619
|
+
enabled
|
|
105620
|
+
});
|
|
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
|
+
}
|
|
105628
|
+
setDescription(appRef, description) {
|
|
105629
|
+
return this.http.patch(`/apps/${encodeURIComponent(appRef)}/description`, {
|
|
105630
|
+
description
|
|
105631
|
+
});
|
|
105632
|
+
}
|
|
105633
|
+
setIconColour(appRef, colour) {
|
|
105634
|
+
return this.http.patch(`/apps/${encodeURIComponent(appRef)}/icon`, {
|
|
105635
|
+
colour
|
|
105636
|
+
});
|
|
105637
|
+
}
|
|
105638
|
+
uploadIconImage(appRef, bytes, mime) {
|
|
105639
|
+
const ab = new ArrayBuffer(bytes.byteLength);
|
|
105640
|
+
new Uint8Array(ab).set(bytes);
|
|
105641
|
+
return this.http.postBinary(`/apps/${encodeURIComponent(appRef)}/icon/image`, new Blob([ab]), { "Content-Type": mime });
|
|
105642
|
+
}
|
|
105643
|
+
deleteIconImage(appRef) {
|
|
105644
|
+
return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/icon/image`);
|
|
105645
|
+
}
|
|
105646
|
+
uploadPreviewImage(appRef, bytes, mime) {
|
|
105647
|
+
const ab = new ArrayBuffer(bytes.byteLength);
|
|
105648
|
+
new Uint8Array(ab).set(bytes);
|
|
105649
|
+
return this.http.postBinary(`/apps/${encodeURIComponent(appRef)}/preview/image`, new Blob([ab]), { "Content-Type": mime });
|
|
105650
|
+
}
|
|
105651
|
+
deletePreviewImage(appRef) {
|
|
105652
|
+
return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/preview/image`);
|
|
105653
|
+
}
|
|
105464
105654
|
async downloadSource(appRef, deployId) {
|
|
105465
105655
|
return this.http.getBinary(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/source`);
|
|
105466
105656
|
}
|
|
@@ -105473,8 +105663,8 @@ class AppsResource {
|
|
|
105473
105663
|
discardPreview(appRef, deployId) {
|
|
105474
105664
|
return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/discard`, {});
|
|
105475
105665
|
}
|
|
105476
|
-
getDeployment(appRef, deployId) {
|
|
105477
|
-
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);
|
|
105478
105668
|
}
|
|
105479
105669
|
listDeploys(appRef, opts) {
|
|
105480
105670
|
const params = new URLSearchParams;
|
|
@@ -105483,32 +105673,36 @@ class AppsResource {
|
|
|
105483
105673
|
if (opts?.limit)
|
|
105484
105674
|
params.set("limit", String(opts.limit));
|
|
105485
105675
|
const qs = params.toString();
|
|
105486
|
-
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);
|
|
105487
105677
|
}
|
|
105488
|
-
async getBuildLog(appRef, deployId) {
|
|
105678
|
+
async getBuildLog(appRef, deployId, opts) {
|
|
105489
105679
|
const path = `/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/build-log`;
|
|
105490
105680
|
let attempt = 0;
|
|
105491
105681
|
const maxRetries = 5;
|
|
105492
105682
|
while (true) {
|
|
105493
105683
|
try {
|
|
105494
|
-
const res = await this.http.get(path);
|
|
105684
|
+
const res = await this.http.get(path, opts);
|
|
105495
105685
|
if (typeof res === "string")
|
|
105496
105686
|
return res;
|
|
105497
105687
|
return res?.buildLog ?? "";
|
|
105498
105688
|
} catch (err) {
|
|
105499
105689
|
attempt++;
|
|
105500
|
-
|
|
105501
|
-
const isNetwork = msg.startsWith("network error:");
|
|
105502
|
-
if (!isNetwork || attempt >= maxRetries)
|
|
105690
|
+
if (opts?.signal?.aborted || !isTransientNetworkError(err) || attempt >= maxRetries) {
|
|
105503
105691
|
throw err;
|
|
105692
|
+
}
|
|
105504
105693
|
const backoffMs = 1000 * 2 ** (attempt - 1);
|
|
105505
|
-
await
|
|
105694
|
+
await abortableSleep(backoffMs, opts?.signal);
|
|
105695
|
+
if (opts?.signal?.aborted)
|
|
105696
|
+
throw err;
|
|
105506
105697
|
}
|
|
105507
105698
|
}
|
|
105508
105699
|
}
|
|
105509
105700
|
unsuspend(appRef) {
|
|
105510
105701
|
return this.http.post(`/apps/${encodeURIComponent(appRef)}/unsuspend`, {});
|
|
105511
105702
|
}
|
|
105703
|
+
resetCache(appRef) {
|
|
105704
|
+
return this.http.post(`/apps/${encodeURIComponent(appRef)}/cache/reset`, {});
|
|
105705
|
+
}
|
|
105512
105706
|
delete(id) {
|
|
105513
105707
|
return this.http.delete(`/apps/${encodeURIComponent(id)}`);
|
|
105514
105708
|
}
|
|
@@ -105545,12 +105739,18 @@ class AppsResource {
|
|
|
105545
105739
|
listJobs(appRef, limit = 20) {
|
|
105546
105740
|
return this.http.get(`/apps/${encodeURIComponent(appRef)}/jobs?limit=${limit}`);
|
|
105547
105741
|
}
|
|
105742
|
+
listCrons(appRef) {
|
|
105743
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/crons`);
|
|
105744
|
+
}
|
|
105548
105745
|
getDiagnostics(appRef) {
|
|
105549
105746
|
return this.http.get(`/apps/${encodeURIComponent(appRef)}/diagnostics`);
|
|
105550
105747
|
}
|
|
105551
105748
|
getTopology(appRef) {
|
|
105552
105749
|
return this.http.get(`/apps/${encodeURIComponent(appRef)}/topology`);
|
|
105553
105750
|
}
|
|
105751
|
+
getPocketBaseVolume(appRef) {
|
|
105752
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/pocketbase/volume`);
|
|
105753
|
+
}
|
|
105554
105754
|
getResources(appRef, opts = {}) {
|
|
105555
105755
|
const params = new URLSearchParams;
|
|
105556
105756
|
if (opts.window)
|
|
@@ -105712,6 +105912,40 @@ class EnvResource {
|
|
|
105712
105912
|
}
|
|
105713
105913
|
}
|
|
105714
105914
|
|
|
105915
|
+
// ../client/dist/resources/env-scan.js
|
|
105916
|
+
class EnvScanResource {
|
|
105917
|
+
http;
|
|
105918
|
+
constructor(http) {
|
|
105919
|
+
this.http = http;
|
|
105920
|
+
}
|
|
105921
|
+
scan(appId) {
|
|
105922
|
+
return this.http.post(`/apps/${encodeURIComponent(appId)}/env-scan`);
|
|
105923
|
+
}
|
|
105924
|
+
}
|
|
105925
|
+
|
|
105926
|
+
// ../client/dist/resources/export.js
|
|
105927
|
+
class ExportResource {
|
|
105928
|
+
http;
|
|
105929
|
+
constructor(http) {
|
|
105930
|
+
this.http = http;
|
|
105931
|
+
}
|
|
105932
|
+
mint(appRef, options) {
|
|
105933
|
+
return this.http.post(`/apps/${encodeURIComponent(appRef)}/export/mint`, {
|
|
105934
|
+
deliveryKind: options?.deliveryKind
|
|
105935
|
+
});
|
|
105936
|
+
}
|
|
105937
|
+
revoke(appRef, tokenId) {
|
|
105938
|
+
return this.http.post(`/apps/${encodeURIComponent(appRef)}/export/tokens/${encodeURIComponent(tokenId)}/revoke`);
|
|
105939
|
+
}
|
|
105940
|
+
history(appRef) {
|
|
105941
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/export/history`);
|
|
105942
|
+
}
|
|
105943
|
+
async download(downloadUrl) {
|
|
105944
|
+
const path = downloadUrl.startsWith("http") ? new URL(downloadUrl).pathname + new URL(downloadUrl).search : downloadUrl;
|
|
105945
|
+
return this.http.stream(path);
|
|
105946
|
+
}
|
|
105947
|
+
}
|
|
105948
|
+
|
|
105715
105949
|
// ../client/dist/sse.js
|
|
105716
105950
|
async function* parseSseStream(stream, signal) {
|
|
105717
105951
|
const reader = stream.getReader();
|
|
@@ -105859,6 +106093,46 @@ class PlatformResource {
|
|
|
105859
106093
|
}
|
|
105860
106094
|
}
|
|
105861
106095
|
|
|
106096
|
+
// ../client/dist/resources/search.js
|
|
106097
|
+
class SearchResource {
|
|
106098
|
+
http;
|
|
106099
|
+
constructor(http) {
|
|
106100
|
+
this.http = http;
|
|
106101
|
+
}
|
|
106102
|
+
query(q) {
|
|
106103
|
+
return this.http.get(`/search?q=${encodeURIComponent(q)}`);
|
|
106104
|
+
}
|
|
106105
|
+
}
|
|
106106
|
+
|
|
106107
|
+
// ../client/dist/resources/security.js
|
|
106108
|
+
class SecurityResource {
|
|
106109
|
+
http;
|
|
106110
|
+
constructor(http) {
|
|
106111
|
+
this.http = http;
|
|
106112
|
+
}
|
|
106113
|
+
forApp(appRef) {
|
|
106114
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/security`);
|
|
106115
|
+
}
|
|
106116
|
+
forAppAsAdmin(appRef) {
|
|
106117
|
+
return this.http.get(`/admin/security/apps/${encodeURIComponent(appRef)}`);
|
|
106118
|
+
}
|
|
106119
|
+
resendNotificationAsAdmin(appRef) {
|
|
106120
|
+
return this.http.post(`/admin/security/apps/${encodeURIComponent(appRef)}/notify`, {});
|
|
106121
|
+
}
|
|
106122
|
+
overview() {
|
|
106123
|
+
return this.http.get("/admin/security/overview");
|
|
106124
|
+
}
|
|
106125
|
+
platformBaseOverview() {
|
|
106126
|
+
return this.http.get("/admin/security/platform-base");
|
|
106127
|
+
}
|
|
106128
|
+
rescanAllAsAdmin() {
|
|
106129
|
+
return this.http.post("/admin/security/rescan", {});
|
|
106130
|
+
}
|
|
106131
|
+
rescanAppAsAdmin(appRef) {
|
|
106132
|
+
return this.http.post(`/admin/security/apps/${encodeURIComponent(appRef)}/rescan`, {});
|
|
106133
|
+
}
|
|
106134
|
+
}
|
|
106135
|
+
|
|
105862
106136
|
// ../client/dist/resources/sharing.js
|
|
105863
106137
|
class SharingResource {
|
|
105864
106138
|
http;
|
|
@@ -105929,12 +106203,16 @@ class PercherClient {
|
|
|
105929
106203
|
apps;
|
|
105930
106204
|
data;
|
|
105931
106205
|
env;
|
|
106206
|
+
envScan;
|
|
106207
|
+
export;
|
|
105932
106208
|
logs;
|
|
105933
106209
|
auth;
|
|
105934
106210
|
billing;
|
|
105935
106211
|
account;
|
|
105936
106212
|
alerts;
|
|
105937
106213
|
platform;
|
|
106214
|
+
security;
|
|
106215
|
+
search;
|
|
105938
106216
|
sharing;
|
|
105939
106217
|
invites;
|
|
105940
106218
|
transfers;
|
|
@@ -105953,12 +106231,16 @@ class PercherClient {
|
|
|
105953
106231
|
this.apps = new AppsResource(http);
|
|
105954
106232
|
this.data = new DataResource(http);
|
|
105955
106233
|
this.env = new EnvResource(http);
|
|
106234
|
+
this.envScan = new EnvScanResource(http);
|
|
106235
|
+
this.export = new ExportResource(http);
|
|
105956
106236
|
this.logs = new LogsResource(http);
|
|
105957
106237
|
this.auth = new AuthResource(http);
|
|
105958
106238
|
this.billing = new BillingResource(http);
|
|
105959
106239
|
this.account = new AccountResource(http);
|
|
105960
106240
|
this.alerts = new AlertsResource(http);
|
|
105961
106241
|
this.platform = new PlatformResource(http);
|
|
106242
|
+
this.security = new SecurityResource(http);
|
|
106243
|
+
this.search = new SearchResource(http);
|
|
105962
106244
|
this.sharing = new SharingResource(http);
|
|
105963
106245
|
this.invites = new InvitesResource(http);
|
|
105964
106246
|
this.transfers = new TransfersResource(http);
|
|
@@ -106032,17 +106314,6 @@ async function deploysInspect(ctx, input) {
|
|
|
106032
106314
|
import { existsSync as existsSync7 } from "node:fs";
|
|
106033
106315
|
import { join as join10 } from "node:path";
|
|
106034
106316
|
|
|
106035
|
-
// ../shared/dist/timeouts.js
|
|
106036
|
-
var TIMEOUTS = {
|
|
106037
|
-
workerBuildSubprocess: 10 * 60000,
|
|
106038
|
-
apiToWorkerBuildFetch: 11 * 60000,
|
|
106039
|
-
clientPublishPoll: 12 * 60000,
|
|
106040
|
-
workerHealthcheck: 5000,
|
|
106041
|
-
watchdogList: 15000,
|
|
106042
|
-
mcpWaitDeployDefault: 30000,
|
|
106043
|
-
mcpWaitDeployMax: 12 * 60000
|
|
106044
|
-
};
|
|
106045
|
-
|
|
106046
106317
|
// ../core/dist/tarball.js
|
|
106047
106318
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
106048
106319
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -106418,6 +106689,19 @@ var patterns = [
|
|
|
106418
106689
|
missingEnvVars: []
|
|
106419
106690
|
}
|
|
106420
106691
|
},
|
|
106692
|
+
{
|
|
106693
|
+
pattern: /Bad Gateway|HTTP 502|\b502\b/i,
|
|
106694
|
+
result: {
|
|
106695
|
+
title: "Gateway error (transient)",
|
|
106696
|
+
explanation: "A gateway in front of your app returned 502 Bad Gateway while the platform was routing the deploy. This is a transient platform-side hiccup, not your app code.",
|
|
106697
|
+
suggestion: "Wait 30 seconds and run percher publish again. A single retry almost always lands cleanly. If 502s persist across several spaced-out retries, check status.percher.app — that's a platform incident, not your app.",
|
|
106698
|
+
errorClass: "infra_unavailable",
|
|
106699
|
+
phase: "infra",
|
|
106700
|
+
cause: "bad_gateway",
|
|
106701
|
+
relevantFiles: [],
|
|
106702
|
+
missingEnvVars: []
|
|
106703
|
+
}
|
|
106704
|
+
},
|
|
106421
106705
|
{
|
|
106422
106706
|
pattern: /start command could not be found|no start command/i,
|
|
106423
106707
|
result: {
|
|
@@ -106536,7 +106820,20 @@ var patterns = [
|
|
|
106536
106820
|
}
|
|
106537
106821
|
},
|
|
106538
106822
|
{
|
|
106539
|
-
pattern: /
|
|
106823
|
+
pattern: /committed_budget|\bbox admission|platform (?:is )?at capacity/i,
|
|
106824
|
+
result: {
|
|
106825
|
+
title: "Platform at capacity",
|
|
106826
|
+
explanation: "The platform couldn't fit this deploy right now — the box is full, so the box-admission gate refused to start the container. This is a platform-side limit, not a problem with your app code or configuration.",
|
|
106827
|
+
suggestion: "Wait a few minutes and run percher publish again — capacity frees up as other deploys finish. This failure does not count against your app, and a retry usually lands once there's room. If it persists, contact the operator.",
|
|
106828
|
+
errorClass: "platform_at_capacity",
|
|
106829
|
+
phase: "infra",
|
|
106830
|
+
cause: "box_admission_rejected",
|
|
106831
|
+
relevantFiles: [],
|
|
106832
|
+
missingEnvVars: []
|
|
106833
|
+
}
|
|
106834
|
+
},
|
|
106835
|
+
{
|
|
106836
|
+
pattern: /pocketbase[^\n]{0,40}(?:error|fail|crash|panic|exit|unhealthy|could ?not|cannot|connection refused|timed out|not (?:start|ready|reachable))|(?:failed to (?:start|connect|reach)|error (?:starting|connecting to)|unable to (?:start|reach|connect to))[^\n]{0,40}pocketbase/i,
|
|
106540
106837
|
result: {
|
|
106541
106838
|
title: "PocketBase error",
|
|
106542
106839
|
explanation: "PocketBase could not start or connect properly.",
|
|
@@ -106819,6 +107116,12 @@ function recoveryFromErrorClass(errorClass, failure = {}) {
|
|
|
106819
107116
|
args: failure.publishInput ?? {},
|
|
106820
107117
|
reasonCode: "infra_unavailable"
|
|
106821
107118
|
});
|
|
107119
|
+
case "platform_at_capacity":
|
|
107120
|
+
return recoveryRetry({
|
|
107121
|
+
suggestedTool: "percher_publish",
|
|
107122
|
+
args: failure.publishInput ?? {},
|
|
107123
|
+
reasonCode: "infra_transient"
|
|
107124
|
+
});
|
|
106822
107125
|
case "config_invalid":
|
|
106823
107126
|
return recoveryFixConfig({
|
|
106824
107127
|
problems: [
|
|
@@ -106967,15 +107270,17 @@ function trimMessage(raw) {
|
|
|
106967
107270
|
function tomlPathFor(cwd) {
|
|
106968
107271
|
return join11(cwd, "percher.toml");
|
|
106969
107272
|
}
|
|
106970
|
-
async function buildFailureError(ctx, app, deployment) {
|
|
107273
|
+
async function buildFailureError(ctx, app, deployment, signal) {
|
|
106971
107274
|
let buildLogTail;
|
|
106972
107275
|
let buildLogFetchFailed = false;
|
|
106973
107276
|
let fullLog = "";
|
|
106974
107277
|
try {
|
|
106975
|
-
fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id);
|
|
106976
|
-
if (!fullLog) {
|
|
106977
|
-
await
|
|
106978
|
-
|
|
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
|
+
}
|
|
106979
107284
|
}
|
|
106980
107285
|
if (fullLog) {
|
|
106981
107286
|
const lines = fullLog.split(`
|
|
@@ -106987,7 +107292,7 @@ async function buildFailureError(ctx, app, deployment) {
|
|
|
106987
107292
|
buildLogFetchFailed = true;
|
|
106988
107293
|
}
|
|
106989
107294
|
const errorMessage = deployment.errorMessage ?? "";
|
|
106990
|
-
const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError("Deployment failed", fullLog)
|
|
107295
|
+
const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError(errorMessage || "Deployment failed", fullLog);
|
|
106991
107296
|
const rawErrorMessage = errorMessage && !buildLogTail && !classified?.explanation.includes(errorMessage) ? errorMessage : undefined;
|
|
106992
107297
|
const problems = deployment.problems && deployment.problems.length > 0 ? deployment.problems : undefined;
|
|
106993
107298
|
if (classified) {
|
|
@@ -107021,7 +107326,7 @@ async function buildFailureError(ctx, app, deployment) {
|
|
|
107021
107326
|
}
|
|
107022
107327
|
async function classifyDeploymentFailure(opts) {
|
|
107023
107328
|
const { ctx, app, deployment, publishInput } = opts;
|
|
107024
|
-
const error48 = await buildFailureError(ctx, app, deployment);
|
|
107329
|
+
const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
|
|
107025
107330
|
const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
|
|
107026
107331
|
missingEnvVars: error48.missingEnvVars,
|
|
107027
107332
|
deployId: deployment.id,
|
|
@@ -107067,6 +107372,100 @@ var waitForDeployInputSchema = exports_external.object({
|
|
|
107067
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.`),
|
|
107068
107373
|
pollIntervalSeconds: exports_external.number().int().min(1).max(30).optional().describe("Seconds between polls (default 2).")
|
|
107069
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
|
+
}
|
|
107070
107469
|
async function resolveReplaced(opts) {
|
|
107071
107470
|
const { ctx, app, replacedDeployment } = opts;
|
|
107072
107471
|
const deployType = replacedDeployment.type ?? "live";
|
|
@@ -107074,7 +107473,8 @@ async function resolveReplaced(opts) {
|
|
|
107074
107473
|
try {
|
|
107075
107474
|
const deploys = await ctx.client.apps.listDeploys(app.name, {
|
|
107076
107475
|
limit: 5,
|
|
107077
|
-
type: deployType
|
|
107476
|
+
type: deployType,
|
|
107477
|
+
signal: opts.signal
|
|
107078
107478
|
});
|
|
107079
107479
|
latest = deploys.find((d) => d.id !== replacedDeployment.id);
|
|
107080
107480
|
} catch {
|
|
@@ -107121,6 +107521,46 @@ async function resolveReplaced(opts) {
|
|
|
107121
107521
|
summary: `${app.name} deploy ${replacedDeployment.id} was replaced; the latest deploy ${latest.id} is ${latest.status}. Surface to user.`
|
|
107122
107522
|
};
|
|
107123
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
|
+
}
|
|
107124
107564
|
|
|
107125
107565
|
// ../core/dist/commands/doctor.js
|
|
107126
107566
|
var doctorInputSchema = exports_external.object({
|
|
@@ -107248,17 +107688,19 @@ async function doctor(ctx, input = {}) {
|
|
|
107248
107688
|
message: diag.container.running ? "Running" : diag.container.state === "not-found" ? "No container found" : `State: ${diag.container.state}`
|
|
107249
107689
|
});
|
|
107250
107690
|
if (diag.containerHealth) {
|
|
107691
|
+
const latency = diag.containerHealth.latencyMs != null ? ` (${diag.containerHealth.latencyMs}ms)` : "";
|
|
107251
107692
|
checks3.push({
|
|
107252
107693
|
name: "Container health",
|
|
107253
107694
|
status: diag.containerHealth.healthy ? "pass" : "fail",
|
|
107254
|
-
message: diag.containerHealth.healthy ? `${diag.containerHealth.path} OK
|
|
107695
|
+
message: diag.containerHealth.healthy ? `${diag.containerHealth.path} OK${latency}` : `${diag.containerHealth.path} — not responding`
|
|
107255
107696
|
});
|
|
107256
107697
|
}
|
|
107257
107698
|
if (diag.publicRoute) {
|
|
107699
|
+
const latency = diag.publicRoute.latencyMs != null ? ` (${diag.publicRoute.latencyMs}ms)` : "";
|
|
107258
107700
|
checks3.push({
|
|
107259
107701
|
name: "Public route",
|
|
107260
107702
|
status: diag.publicRoute.healthy ? "pass" : "fail",
|
|
107261
|
-
message: diag.publicRoute.healthy ? `${diag.publicRoute.url}
|
|
107703
|
+
message: diag.publicRoute.healthy ? `${diag.publicRoute.url}${latency}` : `${diag.publicRoute.url} — not responding`
|
|
107262
107704
|
});
|
|
107263
107705
|
}
|
|
107264
107706
|
if (diag.pocketbase) {
|
|
@@ -107444,11 +107886,53 @@ function summarize(checks3, vctx = {}) {
|
|
|
107444
107886
|
summary: verdict.summary
|
|
107445
107887
|
};
|
|
107446
107888
|
}
|
|
107447
|
-
function
|
|
107889
|
+
function buildVerdictRuleContext(checks3, vctx) {
|
|
107448
107890
|
const findCheck = (name) => checks3.find((c) => c.name === name);
|
|
107449
|
-
const
|
|
107450
|
-
|
|
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");
|
|
107451
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: () => ({
|
|
107452
107936
|
status: "blocked",
|
|
107453
107937
|
diagnosis: {
|
|
107454
107938
|
title: "Authentication required",
|
|
@@ -107458,150 +107942,188 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107458
107942
|
},
|
|
107459
107943
|
recovery: recoveryLogin({ reasonCode: "auth_required" }),
|
|
107460
107944
|
summary: "Login required: run `percher login` (or set PERCHER_TOKEN)."
|
|
107461
|
-
}
|
|
107462
|
-
}
|
|
107463
|
-
|
|
107464
|
-
|
|
107465
|
-
|
|
107466
|
-
|
|
107467
|
-
|
|
107468
|
-
title: "Percher API is unreachable",
|
|
107469
|
-
explanation: api2.message,
|
|
107470
|
-
reasonCode: "infra_unavailable",
|
|
107471
|
-
phase: "infra"
|
|
107472
|
-
},
|
|
107473
|
-
recovery: recoveryAsk({
|
|
107474
|
-
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.`,
|
|
107475
|
-
reasonCode: "infra_unavailable",
|
|
107476
|
-
retryable: true
|
|
107477
|
-
}),
|
|
107478
|
-
summary: "Cannot reach the Percher API — try again in a moment."
|
|
107479
|
-
};
|
|
107480
|
-
}
|
|
107481
|
-
const toml = findCheck("percher.toml");
|
|
107482
|
-
if (toml?.status === "fail" && !vctx.appProvided) {
|
|
107483
|
-
return {
|
|
107484
|
-
status: "blocked",
|
|
107485
|
-
diagnosis: {
|
|
107486
|
-
title: "Invalid percher.toml",
|
|
107487
|
-
explanation: toml.message,
|
|
107488
|
-
reasonCode: "config_invalid",
|
|
107489
|
-
phase: "config"
|
|
107490
|
-
},
|
|
107491
|
-
recovery: recoveryFixConfig({
|
|
107492
|
-
problems: [
|
|
107493
|
-
{
|
|
107494
|
-
file: "percher.toml",
|
|
107495
|
-
message: toml.message
|
|
107496
|
-
}
|
|
107497
|
-
],
|
|
107498
|
-
reasonCode: "config_invalid"
|
|
107499
|
-
}),
|
|
107500
|
-
summary: "percher.toml is invalid — fix the issues and re-run."
|
|
107501
|
-
};
|
|
107502
|
-
}
|
|
107503
|
-
const app = findCheck("App");
|
|
107504
|
-
if (app?.status === "skip") {
|
|
107505
|
-
return {
|
|
107506
|
-
status: "blocked",
|
|
107507
|
-
diagnosis: {
|
|
107508
|
-
title: "No app specified",
|
|
107509
|
-
explanation: app.message,
|
|
107510
|
-
reasonCode: "config_missing",
|
|
107511
|
-
phase: "config"
|
|
107512
|
-
},
|
|
107513
|
-
recovery: recoveryAsk({
|
|
107514
|
-
prompt: `${app.message} — pass \`--app <name>\` or run \`percher init\` in a project directory to generate a percher.toml.`,
|
|
107515
|
-
reasonCode: "config_missing"
|
|
107516
|
-
}),
|
|
107517
|
-
summary: "No percher.toml found and no --app supplied — pass --app or run percher init."
|
|
107518
|
-
};
|
|
107519
|
-
}
|
|
107520
|
-
const inTransitionalDeploy = !!vctx.lastDeployStatus && TRANSITIONAL_DEPLOY_STATUSES.has(vctx.lastDeployStatus);
|
|
107521
|
-
const inTransitionalApp = !!vctx.appStatus && TRANSITIONAL_APP_STATUSES.has(vctx.appStatus);
|
|
107522
|
-
if (inTransitionalDeploy || inTransitionalApp) {
|
|
107523
|
-
const reasonCode = vctx.lastDeployStatus === "queued" ? "deploy_queued" : vctx.lastDeployStatus === "building" ? "deploy_building" : vctx.lastDeployStatus === "deploying" ? "deploy_deploying" : "deploy_queued";
|
|
107524
|
-
const what = inTransitionalApp ? `App ${vctx.appName ?? ""} is provisioning` : `Last deploy is ${vctx.lastDeployStatus}`;
|
|
107525
|
-
const recovery = vctx.lastDeployId && vctx.appName ? recoveryWait({
|
|
107526
|
-
app: vctx.appName,
|
|
107527
|
-
deployId: vctx.lastDeployId,
|
|
107528
|
-
reasonCode
|
|
107529
|
-
}) : recoveryAsk({
|
|
107530
|
-
prompt: `${what.trim()} for ${vctx.appName ?? "this app"}. Wait for it to finish before retrying — there's no deployId to track yet.`,
|
|
107531
|
-
reasonCode
|
|
107532
|
-
});
|
|
107533
|
-
return {
|
|
107534
|
-
status: "in_progress",
|
|
107535
|
-
diagnosis: {
|
|
107536
|
-
title: what.trim(),
|
|
107537
|
-
explanation: vctx.lastDeployId ? `${what.trim()} (deploy ${vctx.lastDeployId}). Wait for it to finish before retrying.` : `${what.trim()}. Wait for it to finish before retrying.`,
|
|
107538
|
-
reasonCode,
|
|
107539
|
-
phase: "deploy"
|
|
107540
|
-
},
|
|
107541
|
-
recovery,
|
|
107542
|
-
summary: vctx.lastDeployId ? `${what.trim()} — wait for deploy ${vctx.lastDeployId}.` : `${what.trim()} — wait a moment and re-run.`
|
|
107543
|
-
};
|
|
107544
|
-
}
|
|
107545
|
-
const verdictChecks = vctx.appProvided && toml?.status === "fail" ? checks3.filter((c) => c.name !== "percher.toml") : checks3;
|
|
107546
|
-
if (vctx.replacedResolution) {
|
|
107547
|
-
const r = vctx.replacedResolution;
|
|
107548
|
-
const isResolvedLive = r.recovery.nextAction === "none" && !!r.url;
|
|
107549
|
-
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 ?? "";
|
|
107550
107952
|
return {
|
|
107551
|
-
status: "
|
|
107552
|
-
|
|
107553
|
-
|
|
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."
|
|
107554
107966
|
};
|
|
107555
107967
|
}
|
|
107556
|
-
|
|
107557
|
-
|
|
107558
|
-
|
|
107559
|
-
|
|
107560
|
-
|
|
107561
|
-
|
|
107562
|
-
|
|
107563
|
-
|
|
107564
|
-
|
|
107565
|
-
|
|
107566
|
-
|
|
107567
|
-
|
|
107568
|
-
|
|
107569
|
-
|
|
107570
|
-
|
|
107571
|
-
|
|
107572
|
-
|
|
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 ?? "";
|
|
107974
|
+
return {
|
|
107975
|
+
status: "blocked",
|
|
107976
|
+
diagnosis: {
|
|
107977
|
+
title: "Invalid percher.toml",
|
|
107978
|
+
explanation: message,
|
|
107979
|
+
reasonCode: "config_invalid",
|
|
107980
|
+
phase: "config"
|
|
107981
|
+
},
|
|
107982
|
+
recovery: recoveryFixConfig({
|
|
107983
|
+
problems: [
|
|
107984
|
+
{
|
|
107985
|
+
file: "percher.toml",
|
|
107986
|
+
message
|
|
107987
|
+
}
|
|
107988
|
+
],
|
|
107989
|
+
reasonCode: "config_invalid"
|
|
107990
|
+
}),
|
|
107991
|
+
summary: "percher.toml is invalid — fix the issues and re-run."
|
|
107992
|
+
};
|
|
107993
|
+
}
|
|
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) {
|
|
107573
108053
|
return {
|
|
107574
|
-
status: "
|
|
107575
|
-
|
|
107576
|
-
|
|
107577
|
-
explanation: cls.explanation,
|
|
107578
|
-
reasonCode: "missing_env",
|
|
107579
|
-
phase: "build"
|
|
107580
|
-
},
|
|
107581
|
-
recovery: recoveryEnv({
|
|
107582
|
-
app: vctx.appName,
|
|
107583
|
-
keys: cls.missingEnvVars
|
|
107584
|
-
}),
|
|
107585
|
-
summary: vctx.appName ? `Build failed — missing env vars on ${vctx.appName}: ${cls.missingEnvVars.join(", ")}.` : `Build failed — missing env vars: ${cls.missingEnvVars.join(", ")}.`
|
|
108054
|
+
status: "ok",
|
|
108055
|
+
recovery: { ...r.recovery, reasonCode: "replaced_by_newer" },
|
|
108056
|
+
summary: r.summary
|
|
107586
108057
|
};
|
|
107587
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;
|
|
107588
108105
|
const fileProblems = (vctx.buildProblems ?? []).filter((p) => p.file).map(buildProblemToRecoveryProblem);
|
|
107589
|
-
|
|
107590
|
-
|
|
107591
|
-
|
|
107592
|
-
|
|
107593
|
-
|
|
107594
|
-
|
|
107595
|
-
|
|
107596
|
-
|
|
107597
|
-
|
|
107598
|
-
|
|
107599
|
-
|
|
107600
|
-
|
|
107601
|
-
|
|
107602
|
-
|
|
107603
|
-
|
|
107604
|
-
|
|
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;
|
|
107605
108127
|
return {
|
|
107606
108128
|
status: "needs_action",
|
|
107607
108129
|
diagnosis: {
|
|
@@ -107618,7 +108140,11 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107618
108140
|
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — inspect the build log.` : "Last deploy failed — inspect the latest failed deploy's build log."
|
|
107619
108141
|
};
|
|
107620
108142
|
}
|
|
107621
|
-
|
|
108143
|
+
},
|
|
108144
|
+
{
|
|
108145
|
+
id: "deploy-failed-dispatch",
|
|
108146
|
+
when: ({ vctx }) => vctx.lastDeployStatus === "failed",
|
|
108147
|
+
verdict: ({ vctx }) => ({
|
|
107622
108148
|
status: "needs_action",
|
|
107623
108149
|
diagnosis: {
|
|
107624
108150
|
title: "Last deploy failed",
|
|
@@ -107638,10 +108164,12 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107638
108164
|
reasonCode: "build_failed"
|
|
107639
108165
|
},
|
|
107640
108166
|
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — call percher_doctor with mode='deploy'.` : "Last deploy failed — call percher_doctor with mode='deploy'."
|
|
107641
|
-
}
|
|
107642
|
-
}
|
|
107643
|
-
|
|
107644
|
-
|
|
108167
|
+
})
|
|
108168
|
+
},
|
|
108169
|
+
{
|
|
108170
|
+
id: "app-suspended",
|
|
108171
|
+
when: ({ vctx }) => vctx.appStatus === "suspended",
|
|
108172
|
+
verdict: ({ vctx }) => ({
|
|
107645
108173
|
status: "needs_action",
|
|
107646
108174
|
diagnosis: {
|
|
107647
108175
|
title: "App is suspended",
|
|
@@ -107654,33 +108182,27 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107654
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."
|
|
107655
108183
|
}),
|
|
107656
108184
|
summary: vctx.appName ? `${vctx.appName} is suspended — resume the app before retrying.` : "App is suspended — resume it before retrying."
|
|
107657
|
-
}
|
|
107658
|
-
}
|
|
107659
|
-
|
|
107660
|
-
|
|
107661
|
-
|
|
107662
|
-
|
|
107663
|
-
status
|
|
107664
|
-
|
|
107665
|
-
|
|
107666
|
-
|
|
107667
|
-
|
|
107668
|
-
|
|
107669
|
-
|
|
107670
|
-
|
|
107671
|
-
|
|
107672
|
-
|
|
107673
|
-
|
|
107674
|
-
|
|
107675
|
-
|
|
107676
|
-
|
|
107677
|
-
|
|
107678
|
-
if (healthCheck?.status === "fail")
|
|
107679
|
-
explanationBits.push(healthCheck.message);
|
|
107680
|
-
if (lastCrashCheck)
|
|
107681
|
-
explanationBits.push(lastCrashCheck.message);
|
|
107682
|
-
const explanation = explanationBits.length > 0 ? explanationBits.join(" ") : "Runtime is not responding — container or health check is failing.";
|
|
107683
|
-
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);
|
|
107684
108206
|
const cr = vctx.crashReport;
|
|
107685
108207
|
const hasAnalysis = cr && cr.analysisStatus === "completed";
|
|
107686
108208
|
const promptParts = [];
|
|
@@ -107717,58 +108239,62 @@ ${cr.logs.join(`
|
|
|
107717
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."
|
|
107718
108240
|
};
|
|
107719
108241
|
}
|
|
107720
|
-
|
|
107721
|
-
|
|
107722
|
-
|
|
107723
|
-
|
|
107724
|
-
|
|
107725
|
-
|
|
107726
|
-
|
|
107727
|
-
|
|
107728
|
-
|
|
107729
|
-
|
|
107730
|
-
|
|
107731
|
-
|
|
107732
|
-
|
|
107733
|
-
|
|
107734
|
-
|
|
107735
|
-
|
|
107736
|
-
|
|
107737
|
-
|
|
107738
|
-
|
|
107739
|
-
|
|
107740
|
-
|
|
107741
|
-
|
|
107742
|
-
|
|
107743
|
-
|
|
107744
|
-
|
|
107745
|
-
|
|
107746
|
-
|
|
107747
|
-
|
|
107748
|
-
|
|
107749
|
-
|
|
107750
|
-
|
|
107751
|
-
|
|
107752
|
-
|
|
107753
|
-
|
|
107754
|
-
|
|
107755
|
-
|
|
107756
|
-
|
|
107757
|
-
|
|
107758
|
-
|
|
107759
|
-
|
|
107760
|
-
|
|
107761
|
-
|
|
107762
|
-
|
|
107763
|
-
|
|
107764
|
-
|
|
107765
|
-
|
|
107766
|
-
|
|
107767
|
-
|
|
107768
|
-
|
|
107769
|
-
|
|
107770
|
-
|
|
107771
|
-
|
|
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);
|
|
107772
108298
|
}
|
|
107773
108299
|
// ../core/dist/commands/domains.js
|
|
107774
108300
|
var domainAddInputSchema = exports_external.object({
|
|
@@ -107858,6 +108384,82 @@ async function envList(ctx, input = {}) {
|
|
|
107858
108384
|
masked[k] = mask(v);
|
|
107859
108385
|
return masked;
|
|
107860
108386
|
}
|
|
108387
|
+
// ../core/dist/commands/export.js
|
|
108388
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
108389
|
+
var exportInputSchema = exports_external.object({
|
|
108390
|
+
app: exports_external.string().optional(),
|
|
108391
|
+
out: exports_external.string().optional(),
|
|
108392
|
+
deliveryKind: exports_external.enum(["cli", "mcp"]).default("cli")
|
|
108393
|
+
});
|
|
108394
|
+
function resolveAppName3(ctx, provided) {
|
|
108395
|
+
const name = provided ?? readPercherTomlAppName(ctx.cwd);
|
|
108396
|
+
if (!name) {
|
|
108397
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
108398
|
+
code: "export.no-app",
|
|
108399
|
+
hint: "Pass --app <name> or run from a directory with percher.toml."
|
|
108400
|
+
});
|
|
108401
|
+
}
|
|
108402
|
+
return name;
|
|
108403
|
+
}
|
|
108404
|
+
async function streamToFile(outPath, source) {
|
|
108405
|
+
const file2 = Bun.file(outPath);
|
|
108406
|
+
const writer = file2.writer();
|
|
108407
|
+
let bytes = 0;
|
|
108408
|
+
const reader = source.getReader();
|
|
108409
|
+
try {
|
|
108410
|
+
while (true) {
|
|
108411
|
+
const { done, value } = await reader.read();
|
|
108412
|
+
if (done)
|
|
108413
|
+
break;
|
|
108414
|
+
if (value && value.length > 0) {
|
|
108415
|
+
writer.write(value);
|
|
108416
|
+
bytes += value.length;
|
|
108417
|
+
}
|
|
108418
|
+
}
|
|
108419
|
+
await writer.end();
|
|
108420
|
+
} catch (err) {
|
|
108421
|
+
try {
|
|
108422
|
+
await writer.end();
|
|
108423
|
+
} catch {}
|
|
108424
|
+
try {
|
|
108425
|
+
await Bun.write(outPath, "");
|
|
108426
|
+
await Bun.file(outPath).delete?.();
|
|
108427
|
+
} catch {}
|
|
108428
|
+
throw err;
|
|
108429
|
+
}
|
|
108430
|
+
return { bytes };
|
|
108431
|
+
}
|
|
108432
|
+
function resolveOutPath(ctxCwd, filename) {
|
|
108433
|
+
if (isAbsolute(filename))
|
|
108434
|
+
return filename;
|
|
108435
|
+
return resolvePath(ctxCwd, filename);
|
|
108436
|
+
}
|
|
108437
|
+
async function exportApp(ctx, input) {
|
|
108438
|
+
const name = resolveAppName3(ctx, input.app);
|
|
108439
|
+
const mint = await ctx.client.export.mint(name, { deliveryKind: input.deliveryKind });
|
|
108440
|
+
const dl = await ctx.client.export.download(mint.downloadUrl);
|
|
108441
|
+
if (!dl.body) {
|
|
108442
|
+
throw new PercherCoreError("Export download returned an empty body", {
|
|
108443
|
+
code: "export.empty-body",
|
|
108444
|
+
hint: "Try again — the link is single-use, so a new mint is required."
|
|
108445
|
+
});
|
|
108446
|
+
}
|
|
108447
|
+
const filenameFromHeader = (() => {
|
|
108448
|
+
const cd = dl.headers.get("Content-Disposition") ?? "";
|
|
108449
|
+
const m = cd.match(/filename="?([^";]+)"?/);
|
|
108450
|
+
return m?.[1];
|
|
108451
|
+
})();
|
|
108452
|
+
const filename = input.out ?? filenameFromHeader ?? `${name}-export.tar.gz`;
|
|
108453
|
+
const outPath = resolveOutPath(ctx.cwd, filename);
|
|
108454
|
+
const { bytes } = await streamToFile(outPath, dl.body);
|
|
108455
|
+
return {
|
|
108456
|
+
appName: name,
|
|
108457
|
+
outPath,
|
|
108458
|
+
bytes,
|
|
108459
|
+
expiresAt: mint.expiresAt,
|
|
108460
|
+
includedSecrets: false
|
|
108461
|
+
};
|
|
108462
|
+
}
|
|
107861
108463
|
// ../core/dist/commands/forgejo.js
|
|
107862
108464
|
var FORGEJO_URL_RE = /^https:\/\/[\w.-]+\/[\w.-]+\/[\w.-]+(\.git)?$/;
|
|
107863
108465
|
var forgejoConnectInputSchema = exports_external.object({
|
|
@@ -108726,11 +109328,6 @@ async function login(ctx, input = {}) {
|
|
|
108726
109328
|
code: "login.expired"
|
|
108727
109329
|
});
|
|
108728
109330
|
}
|
|
108729
|
-
// ../shared/dist/logs.js
|
|
108730
|
-
var MAX_LOG_SEARCH_LENGTH = 256;
|
|
108731
|
-
// ../shared/dist/runtime.js
|
|
108732
|
-
var DEFAULT_NODE_VERSION_MAJOR = "24";
|
|
108733
|
-
var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
|
|
108734
109331
|
// ../core/dist/commands/logs.js
|
|
108735
109332
|
var logsInputSchema = exports_external.object({
|
|
108736
109333
|
app: exports_external.string().optional(),
|
|
@@ -109641,7 +110238,7 @@ function collectWarnings2(collections, rlsPolicies, unsupported) {
|
|
|
109641
110238
|
}
|
|
109642
110239
|
// ../core/dist/commands/migrate-supabase-schema.js
|
|
109643
110240
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
109644
|
-
import { isAbsolute, join as join17, relative as relative3, resolve as resolve6 } from "node:path";
|
|
110241
|
+
import { isAbsolute as isAbsolute2, join as join17, relative as relative3, resolve as resolve6 } from "node:path";
|
|
109645
110242
|
|
|
109646
110243
|
// ../core/dist/commands/migrate-supabase-scripts.js
|
|
109647
110244
|
function generateDataImportScripts(preview) {
|
|
@@ -110154,7 +110751,7 @@ async function migrateSupabaseSchema(ctx, input) {
|
|
|
110154
110751
|
}
|
|
110155
110752
|
function resolveOutputDir(cwd, requested) {
|
|
110156
110753
|
const raw = requested ?? "./migration-preview";
|
|
110157
|
-
return
|
|
110754
|
+
return isAbsolute2(raw) ? raw : resolve6(cwd, raw);
|
|
110158
110755
|
}
|
|
110159
110756
|
async function writePreviewFiles(ctx, outputDir, preview) {
|
|
110160
110757
|
await mkdir2(outputDir, { recursive: true });
|
|
@@ -110373,6 +110970,45 @@ async function open(ctx, input = {}) {
|
|
|
110373
110970
|
await ctx.openUrl(app.url);
|
|
110374
110971
|
return app.url;
|
|
110375
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
|
+
}
|
|
110376
111012
|
// ../core/dist/commands/publish.js
|
|
110377
111013
|
import { existsSync as existsSync14 } from "node:fs";
|
|
110378
111014
|
import { join as join20 } from "node:path";
|
|
@@ -110508,10 +111144,10 @@ async function pollDeployment(opts) {
|
|
|
110508
111144
|
polled = true;
|
|
110509
111145
|
} catch (err) {
|
|
110510
111146
|
attempt++;
|
|
110511
|
-
const
|
|
110512
|
-
const isNetwork = msg.startsWith("network error:");
|
|
111147
|
+
const isNetwork = isTransientNetworkError(err);
|
|
110513
111148
|
if (!isNetwork || attempt >= maxRetries) {
|
|
110514
111149
|
if (isNetwork) {
|
|
111150
|
+
const msg = err.message ?? String(err);
|
|
110515
111151
|
throw new Error(`${msg}
|
|
110516
111152
|
|
|
110517
111153
|
The deploy may already be live — your network connection dropped mid-poll. Check with: percher deploys list ${opts.appName}`, { cause: err });
|
|
@@ -110673,7 +111309,7 @@ function classifyDeployRetry(err, attempt) {
|
|
|
110673
111309
|
category: "non_retryable"
|
|
110674
111310
|
};
|
|
110675
111311
|
}
|
|
110676
|
-
if (
|
|
111312
|
+
if (isTransientNetworkError(err)) {
|
|
110677
111313
|
const delay = NETWORK_BACKOFF_MS[idx];
|
|
110678
111314
|
if (delay === undefined) {
|
|
110679
111315
|
return {
|
|
@@ -110808,6 +111444,12 @@ function* walkDir(dir, base) {
|
|
|
110808
111444
|
}
|
|
110809
111445
|
|
|
110810
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
|
+
]);
|
|
110811
111453
|
function classifyPublishApiError(err, input) {
|
|
110812
111454
|
if (!(err instanceof PercherApiError))
|
|
110813
111455
|
return null;
|
|
@@ -110832,6 +111474,18 @@ function classifyPublishApiError(err, input) {
|
|
|
110832
111474
|
const auth = matchAuthError(err);
|
|
110833
111475
|
if (auth)
|
|
110834
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);
|
|
110835
111489
|
const clientError = matchGenericClientError(err);
|
|
110836
111490
|
if (clientError)
|
|
110837
111491
|
return buildResult(clientError, input);
|
|
@@ -110840,12 +111494,17 @@ function classifyPublishApiError(err, input) {
|
|
|
110840
111494
|
function buildResult(c, input) {
|
|
110841
111495
|
return {
|
|
110842
111496
|
status: "failed",
|
|
111497
|
+
...input.app !== undefined ? { app: input.app } : {},
|
|
110843
111498
|
fileCount: input.bundle.fileCount,
|
|
110844
111499
|
bytes: input.bundle.bytes,
|
|
110845
111500
|
error: {
|
|
110846
111501
|
title: c.title,
|
|
110847
111502
|
explanation: c.explanation,
|
|
110848
|
-
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 } : {}
|
|
110849
111508
|
},
|
|
110850
111509
|
recovery: c.recovery,
|
|
110851
111510
|
summary: c.summary,
|
|
@@ -110864,9 +111523,9 @@ function matchPlanLimit(err) {
|
|
|
110864
111523
|
return {
|
|
110865
111524
|
title: "Plan limit reached",
|
|
110866
111525
|
explanation: err.message,
|
|
110867
|
-
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>`.",
|
|
110868
111527
|
recovery: recoveryAsk({
|
|
110869
|
-
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>\`.`,
|
|
110870
111529
|
options: ["upgrade", "delete unused app", "cancel"],
|
|
110871
111530
|
reasonCode: "quota_exceeded"
|
|
110872
111531
|
}),
|
|
@@ -110884,7 +111543,7 @@ function matchRateLimit(err) {
|
|
|
110884
111543
|
return {
|
|
110885
111544
|
title: "Rate limit hit",
|
|
110886
111545
|
explanation: err.message,
|
|
110887
|
-
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.",
|
|
110888
111547
|
recovery: recoveryAsk({
|
|
110889
111548
|
prompt: `Rate limit: ${err.message}. Wait a few minutes then retry, or upgrade your plan.`,
|
|
110890
111549
|
options: ["wait and retry", "upgrade", "cancel"],
|
|
@@ -110902,7 +111561,7 @@ function matchConcurrentLimit(err) {
|
|
|
110902
111561
|
explanation: err.message,
|
|
110903
111562
|
suggestion: "Wait for one of your in-flight deploys to finish, or upgrade your plan to raise the concurrency cap.",
|
|
110904
111563
|
recovery: recoveryAsk({
|
|
110905
|
-
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.`,
|
|
110906
111565
|
options: ["wait", "upgrade", "cancel"],
|
|
110907
111566
|
reasonCode: "deploy_already_in_progress"
|
|
110908
111567
|
}),
|
|
@@ -110927,14 +111586,46 @@ function matchSuspended(err) {
|
|
|
110927
111586
|
function matchAppSuspended(err) {
|
|
110928
111587
|
if (err.code !== "APP_SUSPENDED" || err.status !== 403)
|
|
110929
111588
|
return null;
|
|
111589
|
+
const liftAfterRaw = typeof err.extra?.liftAfter === "string" ? err.extra.liftAfter : null;
|
|
111590
|
+
const liftableNow = err.extra?.liftableNow === true;
|
|
111591
|
+
const canLiftViaPublish = err.extra?.canLiftViaPublish === true;
|
|
111592
|
+
const fixInstructions = typeof err.extra?.fixInstructions === "string" && err.extra.fixInstructions.length > 0 ? err.extra.fixInstructions : null;
|
|
111593
|
+
let suggestion;
|
|
111594
|
+
let prompt;
|
|
111595
|
+
let options;
|
|
111596
|
+
if (canLiftViaPublish && liftableNow) {
|
|
111597
|
+
suggestion = fixInstructions ? `Fix the underlying cause (${fixInstructions}) and run \`percher publish\` — a successful deploy will lift the suspension automatically.` : "Fix the underlying cause and run `percher publish` — a successful deploy will lift the suspension automatically.";
|
|
111598
|
+
prompt = fixInstructions ? `${err.message}
|
|
111599
|
+
|
|
111600
|
+
The suspension window has passed. Fix the underlying cause per the admin's instructions (${fixInstructions}), then run \`percher publish\` — a successful deploy will lift the suspension.` : `${err.message}
|
|
111601
|
+
|
|
111602
|
+
The suspension window has passed. Fix the underlying cause, then run \`percher publish\` — a successful deploy will lift the suspension.`;
|
|
111603
|
+
options = ["fix and republish", "open dashboard", "contact support"];
|
|
111604
|
+
} else if (canLiftViaPublish && liftAfterRaw) {
|
|
111605
|
+
suggestion = fixInstructions ? `Wait until ${liftAfterRaw}, then fix the underlying cause (${fixInstructions}) and run \`percher publish\`. A successful deploy after the window passes will lift the suspension.` : `Wait until ${liftAfterRaw}, then run \`percher publish\`. A successful deploy after the window passes will lift the suspension.`;
|
|
111606
|
+
prompt = `${err.message}
|
|
111607
|
+
|
|
111608
|
+
You can try again after ${liftAfterRaw}. Until then, retrying will fail.`;
|
|
111609
|
+
options = ["wait and republish", "open dashboard", "contact support"];
|
|
111610
|
+
} else if (liftAfterRaw) {
|
|
111611
|
+
suggestion = `A Percher admin will review the suspension after ${liftAfterRaw}. Contact support if you need it lifted sooner.`;
|
|
111612
|
+
prompt = `${err.message}
|
|
111613
|
+
|
|
111614
|
+
Admin will review after ${liftAfterRaw}. Contact support at https://percher.app if you need it lifted sooner.`;
|
|
111615
|
+
options = ["wait", "contact support"];
|
|
111616
|
+
} else {
|
|
111617
|
+
suggestion = "Contact support at https://percher.app or check the dashboard for unsuspend options. Re-publishing won't unsuspend the app.";
|
|
111618
|
+
prompt = `${err.message} Open https://percher.app or contact support — re-publish won't help until the app is unsuspended.`;
|
|
111619
|
+
options = ["contact support", "open dashboard"];
|
|
111620
|
+
}
|
|
110930
111621
|
return {
|
|
110931
111622
|
title: "App suspended",
|
|
110932
111623
|
explanation: err.message,
|
|
110933
|
-
suggestion
|
|
111624
|
+
suggestion,
|
|
110934
111625
|
recovery: recoveryAsk({
|
|
110935
|
-
prompt
|
|
110936
|
-
options
|
|
110937
|
-
reasonCode: "
|
|
111626
|
+
prompt,
|
|
111627
|
+
options,
|
|
111628
|
+
reasonCode: "app_suspended"
|
|
110938
111629
|
}),
|
|
110939
111630
|
summary: `App suspended: ${err.message}`
|
|
110940
111631
|
};
|
|
@@ -110945,7 +111636,7 @@ function matchEmailNotVerified(err) {
|
|
|
110945
111636
|
return {
|
|
110946
111637
|
title: "Email not verified",
|
|
110947
111638
|
explanation: err.message,
|
|
110948
|
-
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.",
|
|
110949
111640
|
recovery: recoveryAsk({
|
|
110950
111641
|
prompt: `${err.message} Verify the email, then retry publish.`,
|
|
110951
111642
|
options: ["check inbox", "open settings"],
|
|
@@ -110979,6 +111670,94 @@ function matchAuthError(err) {
|
|
|
110979
111670
|
summary: `Permission denied (${err.status} ${err.code || "—"}): ${err.message}`
|
|
110980
111671
|
};
|
|
110981
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
|
+
}
|
|
110982
111761
|
function matchGenericClientError(err) {
|
|
110983
111762
|
if (err.status < 400 || err.status >= 500)
|
|
110984
111763
|
return null;
|
|
@@ -111042,13 +111821,12 @@ var publishInputSchema = exports_external.object({
|
|
|
111042
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."),
|
|
111043
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.")
|
|
111044
111823
|
});
|
|
111045
|
-
var MAX_BYTES = 500 * 1024 * 1024;
|
|
111046
111824
|
var SUSPICIOUS_BYTES = 50 * 1024 * 1024;
|
|
111047
111825
|
var SUSPICIOUS_FILES = 1e4;
|
|
111048
111826
|
function tomlPathFor2(cwd) {
|
|
111049
111827
|
return join20(cwd, "percher.toml");
|
|
111050
111828
|
}
|
|
111051
|
-
async function
|
|
111829
|
+
async function buildReplacedResult2(opts) {
|
|
111052
111830
|
const { ctx, app, deployment, tarball, cwd } = opts;
|
|
111053
111831
|
const resolved = await resolveReplaced({
|
|
111054
111832
|
ctx,
|
|
@@ -111086,33 +111864,6 @@ function buildAlreadyInProgressResult(opts) {
|
|
|
111086
111864
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111087
111865
|
};
|
|
111088
111866
|
}
|
|
111089
|
-
function buildRetryLimitReachedResult(opts) {
|
|
111090
|
-
const { err, app, tarball, cwd } = opts;
|
|
111091
|
-
const extra = err.extra ?? {};
|
|
111092
|
-
const resetAtStr = extra.resetAt ?? "";
|
|
111093
|
-
const resetAtForPrompt = resetAtStr || "the cooldown window expires";
|
|
111094
|
-
const prompt = `Percher already retried once after an infrastructure failure for ${app.name}. Wait until ${resetAtForPrompt} or surface to the user.`;
|
|
111095
|
-
return {
|
|
111096
|
-
status: "failed",
|
|
111097
|
-
app,
|
|
111098
|
-
fileCount: tarball.fileCount,
|
|
111099
|
-
bytes: tarball.bytes,
|
|
111100
|
-
error: {
|
|
111101
|
-
title: "Retry limit reached",
|
|
111102
|
-
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.",
|
|
111103
|
-
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.`
|
|
111104
|
-
},
|
|
111105
|
-
recovery: recoveryAsk({
|
|
111106
|
-
prompt,
|
|
111107
|
-
options: ["wait before retrying", "inspect status"],
|
|
111108
|
-
reasonCode: "retry_limit_reached",
|
|
111109
|
-
retryable: false
|
|
111110
|
-
}),
|
|
111111
|
-
summary: `Retry limit reached for ${app.name}${resetAtStr ? ` — wait until ${resetAtStr}` : ""}.`,
|
|
111112
|
-
configPath: tomlPathFor2(cwd),
|
|
111113
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111114
|
-
};
|
|
111115
|
-
}
|
|
111116
111867
|
function buildLiveSummary(opts) {
|
|
111117
111868
|
const cacheTag = opts.cacheHit ? "cached, " : "";
|
|
111118
111869
|
const fwSuffix = opts.framework ? ` (${opts.framework}, ${cacheTag}${opts.totalSeconds}s)` : ` (${cacheTag}${opts.totalSeconds}s)`;
|
|
@@ -111235,7 +111986,7 @@ ${bullets}`,
|
|
|
111235
111986
|
ctx.status("[2/4] Packaging files...");
|
|
111236
111987
|
const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
111237
111988
|
const packageMs = Date.now() - t0;
|
|
111238
|
-
if (tarball.bytes >
|
|
111989
|
+
if (tarball.bytes > MAX_TARBALL_BYTES) {
|
|
111239
111990
|
return {
|
|
111240
111991
|
status: "failed",
|
|
111241
111992
|
fileCount: tarball.fileCount,
|
|
@@ -111330,12 +112081,26 @@ ${keyList}
|
|
|
111330
112081
|
ctx.status(`Build queue wait: ~${preflight.queueWaitEstimateSec}s — your build is queued behind ${preflight.queueDepth} other(s).`);
|
|
111331
112082
|
}
|
|
111332
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;
|
|
111333
112098
|
const uploadStart = Date.now();
|
|
111334
112099
|
ctx.status("[3/4] Uploading...");
|
|
111335
112100
|
const tarballBytes = new Uint8Array(await new Response(tarball.stream).arrayBuffer());
|
|
111336
112101
|
let cacheReusedDeployment = null;
|
|
111337
112102
|
const contentHash = tarball.contentHash;
|
|
111338
|
-
if (!input.noCache) {
|
|
112103
|
+
if (!input.noCache && !input.preview) {
|
|
111339
112104
|
try {
|
|
111340
112105
|
const probe = await ctx.client.platform.cacheProbe({
|
|
111341
112106
|
appId: app.id,
|
|
@@ -111381,99 +112146,14 @@ ${keyList}
|
|
|
111381
112146
|
}
|
|
111382
112147
|
deployment = deployResponse;
|
|
111383
112148
|
} catch (err) {
|
|
111384
|
-
if (err instanceof PercherApiError && err.code
|
|
111385
|
-
|
|
111386
|
-
err,
|
|
111387
|
-
app,
|
|
111388
|
-
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
111389
|
-
cwd: ctx.cwd
|
|
111390
|
-
});
|
|
111391
|
-
}
|
|
111392
|
-
if (err instanceof PercherApiError && err.code === "REQUIRED_ENV_MISSING") {
|
|
111393
|
-
const extra = err.extra ?? {};
|
|
111394
|
-
const keys = extra.keys ?? [];
|
|
111395
|
-
const source = extra.source ?? "contract";
|
|
111396
|
-
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";
|
|
111397
|
-
return {
|
|
111398
|
-
status: "failed",
|
|
111399
|
-
app,
|
|
111400
|
-
fileCount: tarball.fileCount,
|
|
111401
|
-
bytes: tarball.bytes,
|
|
111402
|
-
error: {
|
|
111403
|
-
title: "Required env keys not set",
|
|
111404
|
-
explanation: `${keys.length} env ${keys.length === 1 ? "key is" : "keys are"} ${sourceText} but missing from the env store: ${keys.join(", ")}.`,
|
|
111405
|
-
suggestion: `Run \`bunx percher env set ${keys[0] ?? "KEY"}=value\` (and similarly for the rest) and re-publish.`,
|
|
111406
|
-
errorClass: "missing_env",
|
|
111407
|
-
phase: "config",
|
|
111408
|
-
missingEnvVars: keys
|
|
111409
|
-
},
|
|
111410
|
-
recovery: recoveryEnv({
|
|
111411
|
-
app: app.name,
|
|
111412
|
-
keys,
|
|
111413
|
-
reasonCode: "missing_env"
|
|
111414
|
-
}),
|
|
111415
|
-
summary: `Missing required env ${keys.length === 1 ? "key" : "keys"}: ${keys.join(", ")}.`,
|
|
111416
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111417
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111418
|
-
};
|
|
111419
|
-
}
|
|
111420
|
-
if (err instanceof PercherApiError && err.code === "ENV_KEY_UNDECLARED") {
|
|
111421
|
-
const extra = err.extra ?? {};
|
|
111422
|
-
const apiProblems = extra.problems ?? [];
|
|
111423
|
-
const problems = apiProblems.map((p) => ({
|
|
111424
|
-
file: p.file,
|
|
111425
|
-
line: p.line,
|
|
111426
|
-
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)"}`
|
|
111427
|
-
}));
|
|
111428
|
-
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
111429
|
-
return {
|
|
111430
|
-
status: "failed",
|
|
111431
|
-
app,
|
|
111432
|
-
fileCount: tarball.fileCount,
|
|
111433
|
-
bytes: tarball.bytes,
|
|
111434
|
-
error: {
|
|
111435
|
-
title: "Undeclared env key",
|
|
111436
|
-
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
111437
|
-
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.",
|
|
111438
|
-
errorClass: "config_invalid",
|
|
111439
|
-
phase: "config",
|
|
111440
|
-
relevantFiles: ["percher.toml"]
|
|
111441
|
-
},
|
|
111442
|
-
recovery: recoveryFixConfig({
|
|
111443
|
-
problems,
|
|
111444
|
-
reasonCode: "env_key_undeclared"
|
|
111445
|
-
}),
|
|
111446
|
-
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`,
|
|
111447
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111448
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111449
|
-
};
|
|
111450
|
-
}
|
|
111451
|
-
if (err instanceof PercherApiError && err.code === "DAILY_QUOTA_EXCEEDED") {
|
|
111452
|
-
const extra = err.extra ?? {};
|
|
111453
|
-
const kind = extra.kind ?? "live";
|
|
111454
|
-
const used = extra.used ?? 0;
|
|
111455
|
-
const limit = extra.limit ?? 0;
|
|
111456
|
-
const resetAt = extra.resetAt ?? "";
|
|
111457
|
-
const otherKind = kind === "live" ? "preview" : "live";
|
|
111458
|
-
return {
|
|
111459
|
-
status: "failed",
|
|
111460
|
-
app,
|
|
111461
|
-
fileCount: tarball.fileCount,
|
|
111462
|
-
bytes: tarball.bytes,
|
|
111463
|
-
error: {
|
|
111464
|
-
title: "Daily deploy quota reached",
|
|
111465
|
-
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
111466
|
-
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/settings to raise this cap.`
|
|
111467
|
-
},
|
|
111468
|
-
recovery: recoveryAsk({
|
|
111469
|
-
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.`,
|
|
111470
|
-
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
111471
|
-
reasonCode: "quota_exceeded"
|
|
111472
|
-
}),
|
|
111473
|
-
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, {
|
|
111474
112151
|
configPath: tomlPathFor2(ctx.cwd),
|
|
111475
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111476
|
-
|
|
112152
|
+
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
112153
|
+
app
|
|
112154
|
+
});
|
|
112155
|
+
if (classified)
|
|
112156
|
+
return classified;
|
|
111477
112157
|
}
|
|
111478
112158
|
throw err;
|
|
111479
112159
|
}
|
|
@@ -111571,7 +112251,7 @@ ${keyList}
|
|
|
111571
112251
|
}
|
|
111572
112252
|
const buildMs = Date.now() - buildStart;
|
|
111573
112253
|
if (deployment.status === "replaced") {
|
|
111574
|
-
return await
|
|
112254
|
+
return await buildReplacedResult2({
|
|
111575
112255
|
ctx,
|
|
111576
112256
|
app,
|
|
111577
112257
|
deployment,
|
|
@@ -111683,150 +112363,21 @@ async function handleUnauthorized(ctx, config2, tarball, input) {
|
|
|
111683
112363
|
token: newToken,
|
|
111684
112364
|
sessionId: ctx.client.sessionId
|
|
111685
112365
|
});
|
|
112366
|
+
const t0 = Date.now();
|
|
111686
112367
|
const freshTarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
111687
|
-
|
|
111688
|
-
|
|
111689
|
-
({
|
|
111690
|
-
|
|
111691
|
-
|
|
111692
|
-
|
|
111693
|
-
let deployment;
|
|
111694
|
-
try {
|
|
111695
|
-
const { result: deployResponse } = await runDeployWithRetry({
|
|
111696
|
-
call: (attempt) => ctx.client.apps.deploy(app.id, {
|
|
111697
|
-
tarball: freshTarballBytes,
|
|
111698
|
-
type: input.preview ? "preview" : undefined,
|
|
111699
|
-
note: input.message,
|
|
111700
|
-
noCache: input.noCache,
|
|
111701
|
-
idempotencyKey: freshIdempotencyKey,
|
|
111702
|
-
retriedAttempts: attempt - 1
|
|
111703
|
-
}),
|
|
111704
|
-
onRetry: ({ attempt, decision }) => {
|
|
111705
|
-
ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
|
|
111706
|
-
}
|
|
111707
|
-
});
|
|
111708
|
-
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
111709
|
-
return buildAlreadyInProgressResult({
|
|
111710
|
-
active: deployResponse,
|
|
111711
|
-
app,
|
|
111712
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
111713
|
-
cwd: ctx.cwd
|
|
111714
|
-
});
|
|
111715
|
-
}
|
|
111716
|
-
deployment = deployResponse;
|
|
111717
|
-
} catch (err) {
|
|
111718
|
-
if (err instanceof PercherApiError && err.code === "RETRY_LIMIT_REACHED") {
|
|
111719
|
-
return buildRetryLimitReachedResult({
|
|
111720
|
-
err,
|
|
111721
|
-
app,
|
|
111722
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
111723
|
-
cwd: ctx.cwd
|
|
111724
|
-
});
|
|
111725
|
-
}
|
|
111726
|
-
throw err;
|
|
111727
|
-
}
|
|
111728
|
-
const replacedPreview = deployment.replacedPreview === true;
|
|
111729
|
-
if (input.waitForLive === false) {
|
|
111730
|
-
ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
|
|
111731
|
-
return {
|
|
111732
|
-
status: "queued",
|
|
111733
|
-
app,
|
|
111734
|
-
deployment,
|
|
111735
|
-
fileCount: freshTarball.fileCount,
|
|
111736
|
-
bytes: freshTarball.bytes,
|
|
111737
|
-
replacedPreview: replacedPreview || undefined,
|
|
111738
|
-
firstDeploy: firstDeployRetry || undefined,
|
|
111739
|
-
recovery: recoveryWait({
|
|
111740
|
-
app: app.name,
|
|
111741
|
-
deployId: deployment.id,
|
|
111742
|
-
reasonCode: "deploy_queued"
|
|
111743
|
-
}),
|
|
111744
|
-
summary: `${app.name} deploy ${deployment.id} queued — resume with percher_wait_for_deploy.`,
|
|
111745
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111746
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
111747
|
-
};
|
|
111748
|
-
}
|
|
111749
|
-
const timeoutMs = TIMEOUTS.clientPublishPoll;
|
|
111750
|
-
const start = Date.now();
|
|
111751
|
-
while (deployment.status !== "live" && deployment.status !== "failed" && deployment.status !== "replaced") {
|
|
111752
|
-
if (Date.now() - start > timeoutMs) {
|
|
111753
|
-
return {
|
|
111754
|
-
status: "failed",
|
|
111755
|
-
app,
|
|
111756
|
-
deployment,
|
|
111757
|
-
fileCount: freshTarball.fileCount,
|
|
111758
|
-
bytes: freshTarball.bytes,
|
|
111759
|
-
error: {
|
|
111760
|
-
title: "Deploy timed out",
|
|
111761
|
-
explanation: "The deployment did not complete within 5 minutes.",
|
|
111762
|
-
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.`
|
|
111763
|
-
},
|
|
111764
|
-
recovery: recoveryDoctor({
|
|
111765
|
-
app: app.name,
|
|
111766
|
-
deployId: deployment.id,
|
|
111767
|
-
mode: "deploy",
|
|
111768
|
-
reasonCode: "deploy_stalled"
|
|
111769
|
-
}),
|
|
111770
|
-
summary: `Deploy timed out after 5 minutes (${deployment.id}).`,
|
|
111771
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111772
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
111773
|
-
};
|
|
111774
|
-
}
|
|
111775
|
-
ctx.status(`${deployment.status}...`);
|
|
111776
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
111777
|
-
deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
|
|
111778
|
-
}
|
|
111779
|
-
if (deployment.status === "replaced") {
|
|
111780
|
-
return await buildReplacedResult({
|
|
111781
|
-
ctx,
|
|
111782
|
-
app,
|
|
111783
|
-
deployment,
|
|
111784
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
111785
|
-
cwd: ctx.cwd
|
|
111786
|
-
});
|
|
111787
|
-
}
|
|
111788
|
-
if (deployment.status === "failed") {
|
|
111789
|
-
return buildFailureResult({
|
|
111790
|
-
ctx,
|
|
111791
|
-
app,
|
|
111792
|
-
deployment,
|
|
111793
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
111794
|
-
input
|
|
111795
|
-
});
|
|
111796
|
-
}
|
|
111797
|
-
const totalSeconds = (Date.now() - start) / 1000;
|
|
111798
|
-
const url2 = deployment.previewUrl ?? deployment.url ?? app.url;
|
|
111799
|
-
ctx.status(`Live at ${url2} (${totalSeconds.toFixed(0)}s)`);
|
|
111800
|
-
return {
|
|
111801
|
-
status: "live",
|
|
111802
|
-
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,
|
|
111803
112374
|
app,
|
|
111804
|
-
|
|
111805
|
-
|
|
111806
|
-
|
|
111807
|
-
|
|
111808
|
-
|
|
111809
|
-
|
|
111810
|
-
},
|
|
111811
|
-
fileCount: freshTarball.fileCount,
|
|
111812
|
-
bytes: freshTarball.bytes,
|
|
111813
|
-
replacedPreview,
|
|
111814
|
-
firstDeploy: firstDeployRetry || undefined,
|
|
111815
|
-
recovery: RECOVERY_NONE,
|
|
111816
|
-
cacheHit: deployment.cacheHit,
|
|
111817
|
-
traceId: deployment.id,
|
|
111818
|
-
summary: buildLiveSummary({
|
|
111819
|
-
appName: app.name,
|
|
111820
|
-
url: url2 ?? app.url,
|
|
111821
|
-
totalSeconds: Math.round(totalSeconds),
|
|
111822
|
-
framework: config2.app.framework,
|
|
111823
|
-
preview: input.preview === true,
|
|
111824
|
-
replacedPreview,
|
|
111825
|
-
cacheHit: deployment.cacheHit ?? false
|
|
111826
|
-
}),
|
|
111827
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111828
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
111829
|
-
};
|
|
112375
|
+
firstDeploy,
|
|
112376
|
+
tarball: freshTarball,
|
|
112377
|
+
t0,
|
|
112378
|
+
packageMs,
|
|
112379
|
+
missingBuildEnvKeys: []
|
|
112380
|
+
});
|
|
111830
112381
|
}
|
|
111831
112382
|
async function loadOrGenerate(ctx) {
|
|
111832
112383
|
const tomlPath = join20(ctx.cwd, "percher.toml");
|
|
@@ -111887,7 +112438,6 @@ var pushInputSchema = exports_external.object({
|
|
|
111887
112438
|
preview: exports_external.boolean().optional(),
|
|
111888
112439
|
noCache: exports_external.boolean().optional().describe("Skip the image-cache lookup and force a fresh build.")
|
|
111889
112440
|
});
|
|
111890
|
-
var MAX_BYTES2 = 500 * 1024 * 1024;
|
|
111891
112441
|
var SUSPICIOUS_BYTES2 = 50 * 1024 * 1024;
|
|
111892
112442
|
var SUSPICIOUS_FILES2 = 1e4;
|
|
111893
112443
|
var PLACEHOLDER_PREFIXES = ["add-your-", "your-key-here"];
|
|
@@ -111961,7 +112511,7 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
111961
112511
|
await checkPlaceholders(ctx, config2);
|
|
111962
112512
|
await checkByoSecrets(ctx, config2);
|
|
111963
112513
|
const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
111964
|
-
if (tarball.bytes >
|
|
112514
|
+
if (tarball.bytes > MAX_TARBALL_BYTES) {
|
|
111965
112515
|
throw new PercherCoreError(`Repository is larger than 500 MB (current: ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB)`, {
|
|
111966
112516
|
code: "push.too-large",
|
|
111967
112517
|
hint: `Biggest files: ${tarball.topFiles.map((f) => f.path).join(", ")}`,
|
|
@@ -112116,10 +112666,10 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
112116
112666
|
const resetAt = extra.resetAt ?? "";
|
|
112117
112667
|
throw new PercherCoreError(`Daily ${kind}-deploy quota reached (${used}/${limit}). Resets at ${resetAt}.`, {
|
|
112118
112668
|
code: "push.quota-exceeded",
|
|
112119
|
-
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.`,
|
|
112120
112670
|
recovery: {
|
|
112121
112671
|
...recoveryAsk({
|
|
112122
|
-
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.`,
|
|
112123
112673
|
reasonCode: "quota_exceeded"
|
|
112124
112674
|
})
|
|
112125
112675
|
}
|
|
@@ -112273,10 +112823,10 @@ async function redeploy(ctx, input = {}) {
|
|
|
112273
112823
|
error: {
|
|
112274
112824
|
title: "Daily deploy quota reached",
|
|
112275
112825
|
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
112276
|
-
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.`
|
|
112277
112827
|
},
|
|
112278
112828
|
recovery: recoveryAsk({
|
|
112279
|
-
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.`,
|
|
112280
112830
|
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
112281
112831
|
reasonCode: "quota_exceeded"
|
|
112282
112832
|
}),
|
|
@@ -112289,7 +112839,7 @@ async function redeploy(ctx, input = {}) {
|
|
|
112289
112839
|
return failureResult({
|
|
112290
112840
|
title: "Deploy rate limit reached",
|
|
112291
112841
|
explanation: err.message,
|
|
112292
|
-
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."
|
|
112293
112843
|
});
|
|
112294
112844
|
case "DEPLOY_RATE_LIMITED": {
|
|
112295
112845
|
const extra = err.extra ?? {};
|
|
@@ -112516,6 +113066,8 @@ function failureResult(error48) {
|
|
|
112516
113066
|
};
|
|
112517
113067
|
}
|
|
112518
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";
|
|
112519
113071
|
var renameInputSchema = exports_external.object({
|
|
112520
113072
|
app: exports_external.string().optional(),
|
|
112521
113073
|
name: exports_external.string().min(3).max(40)
|
|
@@ -112528,12 +113080,33 @@ async function rename(ctx, input) {
|
|
|
112528
113080
|
hint: "Pass an app name or run from a directory with percher.toml."
|
|
112529
113081
|
});
|
|
112530
113082
|
}
|
|
112531
|
-
|
|
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
|
+
}
|
|
112532
113105
|
}
|
|
112533
113106
|
// ../core/dist/commands/reproduce.js
|
|
112534
|
-
import { mkdtempSync, writeFileSync as
|
|
113107
|
+
import { mkdtempSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
112535
113108
|
import { tmpdir } from "node:os";
|
|
112536
|
-
import { join as
|
|
113109
|
+
import { join as join23 } from "node:path";
|
|
112537
113110
|
var reproduceInputSchema = exports_external.object({
|
|
112538
113111
|
app: exports_external.string().optional().describe("App name. Defaults to the [app].name in the current directory's percher.toml."),
|
|
112539
113112
|
deployId: exports_external.string().describe("Deploy id (e.g. dep_abc123) to reproduce. Required — usually the failed deploy you want to debug.")
|
|
@@ -112617,9 +113190,9 @@ async function reproduce(ctx, input) {
|
|
|
112617
113190
|
const envMap = await ctx.client.env.list(app.id);
|
|
112618
113191
|
currentAppEnvKeys = Object.keys(envMap).sort();
|
|
112619
113192
|
} catch {}
|
|
112620
|
-
const tmpDir = mkdtempSync(
|
|
112621
|
-
const tarballPath =
|
|
112622
|
-
|
|
113193
|
+
const tmpDir = mkdtempSync(join23(tmpdir(), `percher-reproduce-${input.deployId}-`));
|
|
113194
|
+
const tarballPath = join23(tmpDir, "source.tar.gz");
|
|
113195
|
+
writeFileSync9(tarballPath, bytes);
|
|
112623
113196
|
let extracted = false;
|
|
112624
113197
|
try {
|
|
112625
113198
|
const proc = Bun.spawn(["tar", "-xzf", tarballPath, "-C", tmpDir], {
|
|
@@ -112857,7 +113430,7 @@ async function rollback(ctx, input, internals = {}) {
|
|
|
112857
113430
|
}
|
|
112858
113431
|
// ../core/dist/commands/sharing.js
|
|
112859
113432
|
var ShareRoleSchema = exports_external.enum(["viewer", "admin"]);
|
|
112860
|
-
function
|
|
113433
|
+
function resolveApp4(ctx, app) {
|
|
112861
113434
|
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
112862
113435
|
if (!name) {
|
|
112863
113436
|
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
@@ -112885,19 +113458,19 @@ var shareRevokeInputSchema = exports_external.object({
|
|
|
112885
113458
|
shareId: exports_external.string().min(1, "shareId is required")
|
|
112886
113459
|
});
|
|
112887
113460
|
async function shareList(ctx, input = {}) {
|
|
112888
|
-
const appRef =
|
|
113461
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
112889
113462
|
return ctx.client.sharing.list(appRef);
|
|
112890
113463
|
}
|
|
112891
113464
|
async function shareInvite(ctx, input) {
|
|
112892
|
-
const appRef =
|
|
113465
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
112893
113466
|
return ctx.client.sharing.create(appRef, { email: input.email, role: input.role });
|
|
112894
113467
|
}
|
|
112895
113468
|
async function shareSetRole(ctx, input) {
|
|
112896
|
-
const appRef =
|
|
113469
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
112897
113470
|
return ctx.client.sharing.setRole(appRef, input.shareId, input.role);
|
|
112898
113471
|
}
|
|
112899
113472
|
async function shareRevoke(ctx, input) {
|
|
112900
|
-
const appRef =
|
|
113473
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
112901
113474
|
await ctx.client.sharing.revoke(appRef, input.shareId);
|
|
112902
113475
|
}
|
|
112903
113476
|
var invitesListInputSchema = exports_external.object({});
|
|
@@ -112963,7 +113536,7 @@ function summarize2(verdict, health) {
|
|
|
112963
113536
|
return "API responded but reported a non-ok status";
|
|
112964
113537
|
}
|
|
112965
113538
|
// ../core/dist/commands/transfers.js
|
|
112966
|
-
function
|
|
113539
|
+
function resolveApp5(ctx, app) {
|
|
112967
113540
|
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
112968
113541
|
if (!name) {
|
|
112969
113542
|
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
@@ -112985,15 +113558,15 @@ var transferListForAppInputSchema = exports_external.object({
|
|
|
112985
113558
|
app: exports_external.string().optional()
|
|
112986
113559
|
});
|
|
112987
113560
|
async function transferInitiate(ctx, input) {
|
|
112988
|
-
const appRef =
|
|
113561
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
112989
113562
|
return ctx.client.transfers.initiate(appRef, { email: input.email });
|
|
112990
113563
|
}
|
|
112991
113564
|
async function transferCancel(ctx, input) {
|
|
112992
|
-
const appRef =
|
|
113565
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
112993
113566
|
await ctx.client.transfers.cancel(appRef, input.transferId);
|
|
112994
113567
|
}
|
|
112995
113568
|
async function transferListForApp(ctx, input = {}) {
|
|
112996
|
-
const appRef =
|
|
113569
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
112997
113570
|
return ctx.client.transfers.listForApp(appRef);
|
|
112998
113571
|
}
|
|
112999
113572
|
var transfersIncomingInputSchema = exports_external.object({});
|
|
@@ -113059,6 +113632,7 @@ var SOURCE_EXTS2 = new Set([
|
|
|
113059
113632
|
]);
|
|
113060
113633
|
var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
113061
113634
|
// ../core/dist/plans.js
|
|
113635
|
+
var PB_INITIAL_FOOTPRINT_MB = 256;
|
|
113062
113636
|
var PLAN_CATALOG = {
|
|
113063
113637
|
free: {
|
|
113064
113638
|
label: "Free",
|
|
@@ -113080,7 +113654,7 @@ var PLAN_CATALOG = {
|
|
|
113080
113654
|
logRetentionDays: 1,
|
|
113081
113655
|
buildLogRetentionDays: 3,
|
|
113082
113656
|
cronJobs: 1,
|
|
113083
|
-
storageMb:
|
|
113657
|
+
storageMb: PB_INITIAL_FOOTPRINT_MB,
|
|
113084
113658
|
instances: 1,
|
|
113085
113659
|
kvStorageMb: 10,
|
|
113086
113660
|
uptimeIntervalSec: 0,
|
|
@@ -113112,7 +113686,7 @@ var PLAN_CATALOG = {
|
|
|
113112
113686
|
logRetentionDays: 7,
|
|
113113
113687
|
buildLogRetentionDays: 14,
|
|
113114
113688
|
cronJobs: 3,
|
|
113115
|
-
storageMb:
|
|
113689
|
+
storageMb: PB_INITIAL_FOOTPRINT_MB,
|
|
113116
113690
|
instances: 1,
|
|
113117
113691
|
kvStorageMb: 50,
|
|
113118
113692
|
uptimeIntervalSec: 300,
|
|
@@ -113144,7 +113718,7 @@ var PLAN_CATALOG = {
|
|
|
113144
113718
|
logRetentionDays: 30,
|
|
113145
113719
|
buildLogRetentionDays: 30,
|
|
113146
113720
|
cronJobs: 10,
|
|
113147
|
-
storageMb:
|
|
113721
|
+
storageMb: PB_INITIAL_FOOTPRINT_MB,
|
|
113148
113722
|
instances: 2,
|
|
113149
113723
|
kvStorageMb: 200,
|
|
113150
113724
|
uptimeIntervalSec: 60,
|
|
@@ -113176,7 +113750,7 @@ var PLAN_CATALOG = {
|
|
|
113176
113750
|
logRetentionDays: 90,
|
|
113177
113751
|
buildLogRetentionDays: 90,
|
|
113178
113752
|
cronJobs: 25,
|
|
113179
|
-
storageMb:
|
|
113753
|
+
storageMb: PB_INITIAL_FOOTPRINT_MB,
|
|
113180
113754
|
instances: 4,
|
|
113181
113755
|
kvStorageMb: 1000,
|
|
113182
113756
|
uptimeIntervalSec: 30,
|
|
@@ -113290,6 +113864,18 @@ function formatStatus(status2) {
|
|
|
113290
113864
|
return import_picocolors.default.red(status2);
|
|
113291
113865
|
return import_picocolors.default.yellow(status2);
|
|
113292
113866
|
}
|
|
113867
|
+
function formatBytes(bytes) {
|
|
113868
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
113869
|
+
return "—";
|
|
113870
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
113871
|
+
let n = bytes;
|
|
113872
|
+
let i = 0;
|
|
113873
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
113874
|
+
n /= 1024;
|
|
113875
|
+
i += 1;
|
|
113876
|
+
}
|
|
113877
|
+
return `${i === 0 ? n : n.toFixed(1)} ${units[i]}`;
|
|
113878
|
+
}
|
|
113293
113879
|
function firstNonFlagAfter(argv, from, valueFlags = new Set) {
|
|
113294
113880
|
let i = from + 1;
|
|
113295
113881
|
while (i < argv.length) {
|
|
@@ -113329,6 +113915,9 @@ async function runDeploysInspect(ctx, input) {
|
|
|
113329
113915
|
await writeLine(ctx, `${import_picocolors.default.bold("Type:")} ${deploy.type}`);
|
|
113330
113916
|
}
|
|
113331
113917
|
await writeLine(ctx, `${import_picocolors.default.bold("Created:")} ${formatDeployTimestamp(deploy.createdAt)}`);
|
|
113918
|
+
if (typeof deploy.imageSizeBytes === "number") {
|
|
113919
|
+
await writeLine(ctx, `${import_picocolors.default.bold("Image:")} ${formatBytes(deploy.imageSizeBytes)}`);
|
|
113920
|
+
}
|
|
113332
113921
|
if (deploy.url) {
|
|
113333
113922
|
await writeLine(ctx, `${import_picocolors.default.bold("URL:")} ${deploy.url}`);
|
|
113334
113923
|
}
|
|
@@ -113789,6 +114378,9 @@ async function handlePublishResult(ctx, result, options = {}) {
|
|
|
113789
114378
|
await writeLine(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
|
|
113790
114379
|
}
|
|
113791
114380
|
}
|
|
114381
|
+
if (typeof result.deployment?.imageSizeBytes === "number") {
|
|
114382
|
+
await writeLine(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
|
|
114383
|
+
}
|
|
113792
114384
|
if (result.replacedPreview) {
|
|
113793
114385
|
await writeLine(ctx, import_picocolors4.default.dim(" (replaced previous preview — free plan keeps 1 preview slot per app)"));
|
|
113794
114386
|
}
|
|
@@ -113876,6 +114468,67 @@ function registerAppsCommands(cli, ctx) {
|
|
|
113876
114468
|
const timing = result.timing ? ` (${result.timing.totalSeconds.toFixed(0)}s)` : "";
|
|
113877
114469
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} Redeployed to ${result.url}${timing}`);
|
|
113878
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
|
+
});
|
|
113879
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) => {
|
|
113880
114533
|
const result = await push(ctx, {
|
|
113881
114534
|
dryRun: options.dryRun,
|
|
@@ -113897,12 +114550,20 @@ function registerAppsCommands(cli, ctx) {
|
|
|
113897
114550
|
} else if (status2 === "fresh") {
|
|
113898
114551
|
await writeLine(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
|
|
113899
114552
|
}
|
|
114553
|
+
if (typeof result.deployment?.imageSizeBytes === "number") {
|
|
114554
|
+
await writeLine(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
|
|
114555
|
+
}
|
|
113900
114556
|
}
|
|
113901
114557
|
});
|
|
113902
114558
|
cli.command("rename <name> [app]", "Rename an app (changes its subdomain)").action(async (name, app) => {
|
|
113903
114559
|
const result = await rename(ctx, { app, name });
|
|
113904
114560
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} Renamed to ${result.name} — now live at ${result.url}`);
|
|
113905
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
|
+
}
|
|
113906
114567
|
await writeLine(ctx, ` ${import_picocolors4.default.dim(`Next rename available in ${result.cooldownDays} days`)}`);
|
|
113907
114568
|
});
|
|
113908
114569
|
cli.command("open [app]", "Open the app URL in your browser").action(async (app) => {
|
|
@@ -113964,6 +114625,17 @@ function registerAppsCommands(cli, ctx) {
|
|
|
113964
114625
|
const result = await resume(ctx, { app });
|
|
113965
114626
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
|
|
113966
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
|
+
});
|
|
113967
114639
|
cli.command("doctor [app]", "Check app health and connectivity").option("--json", "Output as JSON").action(async (app, options) => {
|
|
113968
114640
|
const result = await doctor(ctx, { app, json: options.json });
|
|
113969
114641
|
if (options.json) {
|
|
@@ -114244,24 +114916,60 @@ function registerDeploysCommands(cli, ctx, argv) {
|
|
|
114244
114916
|
await writeLine(ctx, `${v.sha} ${v.timestamp} ${message}`);
|
|
114245
114917
|
}
|
|
114246
114918
|
});
|
|
114247
|
-
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
|
+
}
|
|
114248
114924
|
const appRef = options.app ?? readPercherTomlAppName(ctx.cwd);
|
|
114249
|
-
if (!appRef)
|
|
114925
|
+
if (!appRef) {
|
|
114250
114926
|
throw new Error("App name required. Use --app or run from a project directory.");
|
|
114927
|
+
}
|
|
114251
114928
|
if (action === "promote") {
|
|
114252
|
-
if (!
|
|
114929
|
+
if (!arg)
|
|
114253
114930
|
throw new Error("Deploy ID required. Usage: percher preview promote <id>");
|
|
114254
|
-
const result = await ctx.client.apps.promotePreview(appRef,
|
|
114931
|
+
const result = await ctx.client.apps.promotePreview(appRef, arg);
|
|
114255
114932
|
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview promoted to live. Status: ${result.status}`);
|
|
114256
114933
|
} else if (action === "discard") {
|
|
114257
|
-
if (!
|
|
114934
|
+
if (!arg)
|
|
114258
114935
|
throw new Error("Deploy ID required. Usage: percher preview discard <id>");
|
|
114259
|
-
await ctx.client.apps.discardPreview(appRef,
|
|
114936
|
+
await ctx.client.apps.discardPreview(appRef, arg);
|
|
114260
114937
|
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview discarded.`);
|
|
114261
114938
|
} else {
|
|
114262
|
-
throw new Error(`Unknown preview action "${action}". Use promote | discard.`);
|
|
114939
|
+
throw new Error(`Unknown preview action "${action}". Use promote | discard | branch.`);
|
|
114263
114940
|
}
|
|
114264
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
|
+
}
|
|
114265
114973
|
}
|
|
114266
114974
|
|
|
114267
114975
|
// src/commands/env.ts
|
|
@@ -114318,10 +115026,24 @@ function registerEnvCommands(cli, ctx) {
|
|
|
114318
115026
|
});
|
|
114319
115027
|
}
|
|
114320
115028
|
|
|
114321
|
-
// src/commands/
|
|
115029
|
+
// src/commands/export.ts
|
|
114322
115030
|
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
115031
|
+
function registerExportCommands(cli, ctx) {
|
|
115032
|
+
cli.command("export [app]", "Download an export bundle for an app (no secrets)").option("--out <path>", "Where to write the .tar.gz (defaults to ./{name}-{date}.tar.gz)").action(async (app, options) => {
|
|
115033
|
+
const result = await exportApp(ctx, {
|
|
115034
|
+
app,
|
|
115035
|
+
out: options.out,
|
|
115036
|
+
deliveryKind: "cli"
|
|
115037
|
+
});
|
|
115038
|
+
await writeLine(ctx, `${import_picocolors9.default.green("✓")} exported ${result.appName} → ${result.outPath} (${(result.bytes / 1024).toFixed(1)} KB)`);
|
|
115039
|
+
await writeLine(ctx, import_picocolors9.default.dim("Env values are not included. To export encrypted env values, use the dashboard's Settings → Export and confirm your password."));
|
|
115040
|
+
});
|
|
115041
|
+
}
|
|
115042
|
+
|
|
115043
|
+
// src/commands/github.ts
|
|
115044
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
114323
115045
|
async function printWebhookInstructions(ctx, instructions, branch) {
|
|
114324
|
-
await writeLine(ctx,
|
|
115046
|
+
await writeLine(ctx, import_picocolors10.default.bold("Add this webhook in GitHub to enable auto-deploy on push:"));
|
|
114325
115047
|
await writeLine(ctx, ` URL: ${instructions.settingsUrl}`);
|
|
114326
115048
|
await writeLine(ctx, "");
|
|
114327
115049
|
await writeLine(ctx, " Field Value");
|
|
@@ -114331,9 +115053,9 @@ async function printWebhookInstructions(ctx, instructions, branch) {
|
|
|
114331
115053
|
await writeLine(ctx, ` Secret ${instructions.secret}`);
|
|
114332
115054
|
await writeLine(ctx, " Which events? Just the push event");
|
|
114333
115055
|
await writeLine(ctx, "");
|
|
114334
|
-
await writeLine(ctx,
|
|
115056
|
+
await writeLine(ctx, import_picocolors10.default.yellow(" ⚠ Treat this secret like a password — save it now, it won't be shown again."));
|
|
114335
115057
|
await writeLine(ctx, "");
|
|
114336
|
-
await writeLine(ctx,
|
|
115058
|
+
await writeLine(ctx, import_picocolors10.default.dim(`After adding the webhook, every push to ${branch} will auto-deploy.`));
|
|
114337
115059
|
}
|
|
114338
115060
|
function registerGithubCommands(cli, ctx) {
|
|
114339
115061
|
cli.command("github [action] [repoUrl] [...rest]", "GitHub integration: connect | setup-webhook").option("--app <name>", "App name (overrides percher.toml)").option("--branch <branch>", "(connect only) Branch to track (default: main)").action(async (action, repoUrl, _rest, options) => {
|
|
@@ -114346,24 +115068,24 @@ function registerGithubCommands(cli, ctx) {
|
|
|
114346
115068
|
repoUrl,
|
|
114347
115069
|
branch: options.branch
|
|
114348
115070
|
});
|
|
114349
|
-
await writeLine(ctx, `${
|
|
114350
|
-
await writeLine(ctx, `${
|
|
115071
|
+
await writeLine(ctx, `${import_picocolors10.default.green("✓")} Connected to ${result.repoUrl} (branch: ${result.branch})`);
|
|
115072
|
+
await writeLine(ctx, `${import_picocolors10.default.green("✓")} Deploy started: ${result.deployId}`);
|
|
114351
115073
|
await writeLine(ctx, "");
|
|
114352
115074
|
if (result.webhookSetupFailed) {
|
|
114353
|
-
await writeLine(ctx,
|
|
114354
|
-
await writeLine(ctx,
|
|
114355
|
-
await writeLine(ctx, ` ${
|
|
115075
|
+
await writeLine(ctx, import_picocolors10.default.yellow(`⚠ Webhook setup failed: ${result.webhookSetupError ?? "unknown error"}`));
|
|
115076
|
+
await writeLine(ctx, import_picocolors10.default.yellow(" Auto-deploy will NOT work until you re-run webhook setup:"));
|
|
115077
|
+
await writeLine(ctx, ` ${import_picocolors10.default.bold(`percher github setup-webhook${options.app ? ` --app ${options.app}` : ""}`)}`);
|
|
114356
115078
|
return;
|
|
114357
115079
|
}
|
|
114358
115080
|
if (result.webhookInstructions) {
|
|
114359
115081
|
await printWebhookInstructions(ctx, result.webhookInstructions, result.branch);
|
|
114360
115082
|
}
|
|
114361
|
-
await writeLine(ctx,
|
|
115083
|
+
await writeLine(ctx, import_picocolors10.default.dim(`Track the initial deploy: percher deploys inspect ${result.deployId}`));
|
|
114362
115084
|
return;
|
|
114363
115085
|
}
|
|
114364
115086
|
if (action === "setup-webhook") {
|
|
114365
115087
|
const result = await githubSetupWebhook(ctx, { app: options.app });
|
|
114366
|
-
await writeLine(ctx, `${
|
|
115088
|
+
await writeLine(ctx, `${import_picocolors10.default.green("✓")} Webhook regenerated for ${result.app}`);
|
|
114367
115089
|
await writeLine(ctx, "");
|
|
114368
115090
|
await printWebhookInstructions(ctx, result.webhookInstructions, result.branch);
|
|
114369
115091
|
return;
|
|
@@ -114375,17 +115097,17 @@ function registerGithubCommands(cli, ctx) {
|
|
|
114375
115097
|
}
|
|
114376
115098
|
|
|
114377
115099
|
// src/commands/migrate.ts
|
|
114378
|
-
var
|
|
115100
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
114379
115101
|
function registerMigrateCommands(cli, ctx) {
|
|
114380
115102
|
cli.command("migrate-from-supabase", "Migrate a Supabase project to PocketBase (FUTURE9 Phase A read-only + Phase B preview writes)").option("--project <ref>", "Supabase project reference (the 20-char id from the project URL)").option("--token <token>", "Supabase personal access token (sbp_...). Read from SUPABASE_ACCESS_TOKEN if omitted.").option("--inspect-only", "Run schema introspection only (Phase A — read-only, no file writes)").option("--output-dir <dir>", "Where to write the Phase B preview files (default: ./migration-preview/)").option("--json", "Output the structured result as JSON").action(async (options) => {
|
|
114381
115103
|
const projectRef = options.project;
|
|
114382
115104
|
const token = options.token ?? ctx.env.SUPABASE_ACCESS_TOKEN;
|
|
114383
115105
|
if (!projectRef) {
|
|
114384
|
-
await writeLine(ctx, `${
|
|
115106
|
+
await writeLine(ctx, `${import_picocolors11.default.red("✗")} Missing --project. Pass your Supabase project ref (e.g. --project abcdefghijklmnopqrst).`);
|
|
114385
115107
|
throw new SilentExitError(1);
|
|
114386
115108
|
}
|
|
114387
115109
|
if (!token) {
|
|
114388
|
-
await writeLine(ctx, `${
|
|
115110
|
+
await writeLine(ctx, `${import_picocolors11.default.red("✗")} Missing --token. Pass --token sbp_... or set SUPABASE_ACCESS_TOKEN. Generate one at https://supabase.com/dashboard/account/tokens`);
|
|
114389
115111
|
throw new SilentExitError(1);
|
|
114390
115112
|
}
|
|
114391
115113
|
if (options.inspectOnly) {
|
|
@@ -114420,103 +115142,103 @@ function registerMigrateCommands(cli, ctx) {
|
|
|
114420
115142
|
async function renderInspectHuman(ctx, result) {
|
|
114421
115143
|
if (result.status === "failed") {
|
|
114422
115144
|
const err = result.error;
|
|
114423
|
-
await writeLine(ctx, `${
|
|
115145
|
+
await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Inspection failed"}`);
|
|
114424
115146
|
if (err?.explanation)
|
|
114425
115147
|
await writeLine(ctx, ` ${err.explanation}`);
|
|
114426
115148
|
if (err?.suggestion)
|
|
114427
|
-
await writeLine(ctx,
|
|
115149
|
+
await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
|
|
114428
115150
|
return;
|
|
114429
115151
|
}
|
|
114430
|
-
await writeLine(ctx, `${
|
|
115152
|
+
await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${result.summary}`);
|
|
114431
115153
|
await writeLine(ctx, "");
|
|
114432
|
-
await writeLine(ctx,
|
|
115154
|
+
await writeLine(ctx, import_picocolors11.default.bold("Tables"));
|
|
114433
115155
|
if (result.tables.length === 0) {
|
|
114434
|
-
await writeLine(ctx,
|
|
115156
|
+
await writeLine(ctx, import_picocolors11.default.dim(" (none in user schemas)"));
|
|
114435
115157
|
}
|
|
114436
115158
|
for (const table of result.tables) {
|
|
114437
115159
|
const pkCount = table.columns.filter((c) => c.isPrimaryKey).length;
|
|
114438
115160
|
const fkCount = table.columns.filter((c) => c.foreignKey !== null).length;
|
|
114439
115161
|
const policyCount = table.rlsPolicies.length;
|
|
114440
115162
|
const rowEstimate = table.estimatedRowCount.toLocaleString();
|
|
114441
|
-
await writeLine(ctx, ` ${
|
|
115163
|
+
await writeLine(ctx, ` ${import_picocolors11.default.cyan(`${table.schema}.${table.name}`)} ${import_picocolors11.default.dim(`cols=${table.columns.length} pk=${pkCount} fk=${fkCount} rls=${policyCount} rows≈${rowEstimate}`)}`);
|
|
114442
115164
|
}
|
|
114443
115165
|
if (result.storageBuckets.length > 0) {
|
|
114444
115166
|
await writeLine(ctx, "");
|
|
114445
|
-
await writeLine(ctx,
|
|
115167
|
+
await writeLine(ctx, import_picocolors11.default.bold("Storage buckets"));
|
|
114446
115168
|
for (const bucket of result.storageBuckets) {
|
|
114447
115169
|
const visibility = bucket.public ? "public" : "private";
|
|
114448
|
-
await writeLine(ctx, ` ${
|
|
115170
|
+
await writeLine(ctx, ` ${import_picocolors11.default.cyan(bucket.name)} ${import_picocolors11.default.dim(`(${visibility})`)}`);
|
|
114449
115171
|
}
|
|
114450
115172
|
}
|
|
114451
115173
|
if (result.authUsersCount !== null) {
|
|
114452
115174
|
await writeLine(ctx, "");
|
|
114453
|
-
await writeLine(ctx, `${
|
|
115175
|
+
await writeLine(ctx, `${import_picocolors11.default.bold("Auth users")}: ${result.authUsersCount}`);
|
|
114454
115176
|
}
|
|
114455
115177
|
if (result.warnings.length > 0) {
|
|
114456
115178
|
await writeLine(ctx, "");
|
|
114457
|
-
await writeLine(ctx,
|
|
115179
|
+
await writeLine(ctx, import_picocolors11.default.bold(`Warnings (${result.warnings.length})`));
|
|
114458
115180
|
for (const warning of result.warnings) {
|
|
114459
|
-
await writeLine(ctx, ` ${
|
|
115181
|
+
await writeLine(ctx, ` ${import_picocolors11.default.yellow("⚠")} ${warning}`);
|
|
114460
115182
|
}
|
|
114461
115183
|
}
|
|
114462
115184
|
await writeLine(ctx, "");
|
|
114463
|
-
await writeLine(ctx,
|
|
115185
|
+
await writeLine(ctx, import_picocolors11.default.dim("--inspect-only is read-only. Drop the flag to also write the Phase B PocketBase-schema preview."));
|
|
114464
115186
|
}
|
|
114465
115187
|
async function renderSchemaHuman(ctx, result) {
|
|
114466
115188
|
if (result.status === "failed") {
|
|
114467
115189
|
const err = result.error;
|
|
114468
|
-
await writeLine(ctx, `${
|
|
115190
|
+
await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Migration preview failed"}`);
|
|
114469
115191
|
if (err?.explanation)
|
|
114470
115192
|
await writeLine(ctx, ` ${err.explanation}`);
|
|
114471
115193
|
if (err?.suggestion)
|
|
114472
|
-
await writeLine(ctx,
|
|
115194
|
+
await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
|
|
114473
115195
|
return;
|
|
114474
115196
|
}
|
|
114475
|
-
await writeLine(ctx, `${
|
|
115197
|
+
await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${result.summary}`);
|
|
114476
115198
|
await writeLine(ctx, "");
|
|
114477
|
-
await writeLine(ctx,
|
|
115199
|
+
await writeLine(ctx, import_picocolors11.default.bold("Files written"));
|
|
114478
115200
|
for (const path of result.filesWritten) {
|
|
114479
|
-
await writeLine(ctx, ` ${
|
|
115201
|
+
await writeLine(ctx, ` ${import_picocolors11.default.cyan(path)}`);
|
|
114480
115202
|
}
|
|
114481
115203
|
const preview = result.preview;
|
|
114482
115204
|
if (!preview)
|
|
114483
115205
|
return;
|
|
114484
115206
|
if (preview.collections.length > 0) {
|
|
114485
115207
|
await writeLine(ctx, "");
|
|
114486
|
-
await writeLine(ctx,
|
|
115208
|
+
await writeLine(ctx, import_picocolors11.default.bold("Collections"));
|
|
114487
115209
|
for (const collection of preview.collections) {
|
|
114488
115210
|
const fields = collection.schema.length;
|
|
114489
115211
|
const relations = collection.schema.filter((f) => f.type === "relation").length;
|
|
114490
|
-
await writeLine(ctx, ` ${
|
|
115212
|
+
await writeLine(ctx, ` ${import_picocolors11.default.cyan(collection.name)} ${import_picocolors11.default.dim(`← ${collection.sourceTable} fields=${fields} relations=${relations}`)}`);
|
|
114491
115213
|
}
|
|
114492
115214
|
}
|
|
114493
115215
|
if (preview.warnings.length > 0) {
|
|
114494
115216
|
await writeLine(ctx, "");
|
|
114495
|
-
await writeLine(ctx,
|
|
115217
|
+
await writeLine(ctx, import_picocolors11.default.bold(`Warnings (${preview.warnings.length})`));
|
|
114496
115218
|
for (const warning of preview.warnings.slice(0, 10)) {
|
|
114497
|
-
await writeLine(ctx, ` ${
|
|
115219
|
+
await writeLine(ctx, ` ${import_picocolors11.default.yellow("⚠")} ${warning}`);
|
|
114498
115220
|
}
|
|
114499
115221
|
if (preview.warnings.length > 10) {
|
|
114500
|
-
await writeLine(ctx,
|
|
115222
|
+
await writeLine(ctx, import_picocolors11.default.dim(` …and ${preview.warnings.length - 10} more — see MIGRATION_NOTES.md`));
|
|
114501
115223
|
}
|
|
114502
115224
|
}
|
|
114503
115225
|
await writeLine(ctx, "");
|
|
114504
|
-
await writeLine(ctx,
|
|
115226
|
+
await writeLine(ctx, import_picocolors11.default.dim("Phase B is a preview. Phase C (data import) and Phase D (client rewrite) ship in later releases. Review MIGRATION_NOTES.md before any import."));
|
|
114505
115227
|
}
|
|
114506
115228
|
|
|
114507
115229
|
// src/commands/sharing.ts
|
|
114508
|
-
var
|
|
115230
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
114509
115231
|
function registerSharingCommands(cli, ctx) {
|
|
114510
115232
|
cli.command("share [action] [arg] [valueOrRole]", "Per-app collaborator invites: list | invite <email> | role <shareId> <role> | revoke <shareId>").option("--app <name>", "App name (overrides percher.toml)").option("--role <role>", "(invite only) viewer | admin — viewer is read-only, admin can deploy + change config").action(async (action, arg, valueOrRole, options) => {
|
|
114511
115233
|
if (action === "list" || action === undefined) {
|
|
114512
115234
|
const rows = await shareList(ctx, { app: options.app });
|
|
114513
115235
|
if (rows.length === 0) {
|
|
114514
|
-
await writeLine(ctx,
|
|
115236
|
+
await writeLine(ctx, import_picocolors12.default.dim("No collaborators."));
|
|
114515
115237
|
return;
|
|
114516
115238
|
}
|
|
114517
115239
|
for (const r of rows) {
|
|
114518
|
-
const state = r.accepted ?
|
|
114519
|
-
const roleStr = r.role === "admin" ?
|
|
115240
|
+
const state = r.accepted ? import_picocolors12.default.green("accepted") : import_picocolors12.default.yellow("pending");
|
|
115241
|
+
const roleStr = r.role === "admin" ? import_picocolors12.default.cyan("admin") : import_picocolors12.default.dim("viewer");
|
|
114520
115242
|
await writeLine(ctx, `${r.id} ${roleStr.padEnd(15)} ${state.padEnd(15)} ${r.email}`);
|
|
114521
115243
|
}
|
|
114522
115244
|
return;
|
|
@@ -114534,8 +115256,8 @@ function registerSharingCommands(cli, ctx) {
|
|
|
114534
115256
|
email: arg,
|
|
114535
115257
|
role: roleRaw
|
|
114536
115258
|
});
|
|
114537
|
-
await writeLine(ctx, `${
|
|
114538
|
-
await writeLine(ctx,
|
|
115259
|
+
await writeLine(ctx, `${import_picocolors12.default.green("✓")} Invited ${created.email} as ${created.role} (id ${created.id})`);
|
|
115260
|
+
await writeLine(ctx, import_picocolors12.default.dim(`Pending until accepted — invite expires ${new Date(created.expiresAt).toLocaleDateString()}.`));
|
|
114539
115261
|
return;
|
|
114540
115262
|
}
|
|
114541
115263
|
if (action === "role") {
|
|
@@ -114550,7 +115272,7 @@ function registerSharingCommands(cli, ctx) {
|
|
|
114550
115272
|
shareId: arg,
|
|
114551
115273
|
role: valueOrRole
|
|
114552
115274
|
});
|
|
114553
|
-
await writeLine(ctx, `${
|
|
115275
|
+
await writeLine(ctx, `${import_picocolors12.default.green("✓")} ${updated.email} is now ${import_picocolors12.default.cyan(updated.role)}`);
|
|
114554
115276
|
return;
|
|
114555
115277
|
}
|
|
114556
115278
|
if (action === "revoke") {
|
|
@@ -114558,7 +115280,7 @@ function registerSharingCommands(cli, ctx) {
|
|
|
114558
115280
|
throw new Error("Usage: percher share revoke <shareId> [--app <name>]");
|
|
114559
115281
|
}
|
|
114560
115282
|
await shareRevoke(ctx, { app: options.app, shareId: arg });
|
|
114561
|
-
await writeLine(ctx, `${
|
|
115283
|
+
await writeLine(ctx, `${import_picocolors12.default.green("✓")} Revoked share ${arg}`);
|
|
114562
115284
|
return;
|
|
114563
115285
|
}
|
|
114564
115286
|
throw new Error(`Unknown share action "${action}". Use list | invite <email> | role <shareId> <role> | revoke <shareId>.`);
|
|
@@ -114571,11 +115293,11 @@ function registerSharingCommands(cli, ctx) {
|
|
|
114571
115293
|
if (action === undefined || action === "list") {
|
|
114572
115294
|
const pending = await invitesList(ctx, {});
|
|
114573
115295
|
if (pending.length === 0) {
|
|
114574
|
-
await writeLine(ctx,
|
|
115296
|
+
await writeLine(ctx, import_picocolors12.default.dim("No pending invites."));
|
|
114575
115297
|
return;
|
|
114576
115298
|
}
|
|
114577
115299
|
for (const inv of pending) {
|
|
114578
|
-
const roleStr = inv.role === "admin" ?
|
|
115300
|
+
const roleStr = inv.role === "admin" ? import_picocolors12.default.cyan("admin") : import_picocolors12.default.dim("viewer");
|
|
114579
115301
|
await writeLine(ctx, `${inv.id} ${roleStr.padEnd(15)} ${inv.appName.padEnd(24)} from ${inv.invitedBy}`);
|
|
114580
115302
|
}
|
|
114581
115303
|
return;
|
|
@@ -114586,10 +115308,10 @@ function registerSharingCommands(cli, ctx) {
|
|
|
114586
115308
|
}
|
|
114587
115309
|
const accepted = await invitesAccept(ctx, { shareId });
|
|
114588
115310
|
if (accepted.alreadyAccepted) {
|
|
114589
|
-
await writeLine(ctx,
|
|
115311
|
+
await writeLine(ctx, import_picocolors12.default.dim(`Invite ${shareId} was already accepted.`));
|
|
114590
115312
|
return;
|
|
114591
115313
|
}
|
|
114592
|
-
await writeLine(ctx, `${
|
|
115314
|
+
await writeLine(ctx, `${import_picocolors12.default.green("✓")} Accepted — you now have ${import_picocolors12.default.cyan(accepted.role)} access to the shared app.`);
|
|
114593
115315
|
return;
|
|
114594
115316
|
}
|
|
114595
115317
|
if (action === "decline") {
|
|
@@ -114597,7 +115319,7 @@ function registerSharingCommands(cli, ctx) {
|
|
|
114597
115319
|
throw new Error("Usage: percher invites decline <shareId>");
|
|
114598
115320
|
}
|
|
114599
115321
|
await invitesDecline(ctx, { shareId });
|
|
114600
|
-
await writeLine(ctx, `${
|
|
115322
|
+
await writeLine(ctx, `${import_picocolors12.default.green("✓")} Declined invite ${shareId}.`);
|
|
114601
115323
|
return;
|
|
114602
115324
|
}
|
|
114603
115325
|
throw new Error(`Unknown invites action "${action}". Use list | accept <shareId> | decline <shareId>.`);
|
|
@@ -114608,7 +115330,7 @@ function registerSharingCommands(cli, ctx) {
|
|
|
114608
115330
|
}
|
|
114609
115331
|
|
|
114610
115332
|
// src/commands/status.ts
|
|
114611
|
-
var
|
|
115333
|
+
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
114612
115334
|
function registerStatusCommands(cli, ctx) {
|
|
114613
115335
|
cli.command("status", "Show Percher platform status (API + worker health)").option("--json", "Output JSON instead of formatted text").action(async (options) => {
|
|
114614
115336
|
const result = await status(ctx, { json: Boolean(options.json) });
|
|
@@ -114624,44 +115346,44 @@ function registerStatusCommands(cli, ctx) {
|
|
|
114624
115346
|
});
|
|
114625
115347
|
}
|
|
114626
115348
|
async function renderHumanStatus(ctx, result) {
|
|
114627
|
-
const symbol2 = result.verdict === "operational" ?
|
|
115349
|
+
const symbol2 = result.verdict === "operational" ? import_picocolors13.default.green("✓") : result.verdict === "worker-degraded" ? import_picocolors13.default.yellow("⚠") : import_picocolors13.default.red("✗");
|
|
114628
115350
|
await writeLine(ctx, `${symbol2} ${result.summary}`);
|
|
114629
115351
|
if (!result.health)
|
|
114630
115352
|
return;
|
|
114631
115353
|
const worker = result.health.worker;
|
|
114632
115354
|
if (!worker) {
|
|
114633
|
-
await writeLine(ctx,
|
|
115355
|
+
await writeLine(ctx, import_picocolors13.default.dim(" worker: not configured (local Docker socket)"));
|
|
114634
115356
|
return;
|
|
114635
115357
|
}
|
|
114636
115358
|
if (worker.health) {
|
|
114637
115359
|
const ok = worker.health.ok;
|
|
114638
|
-
const symbol3 = ok ?
|
|
115360
|
+
const symbol3 = ok ? import_picocolors13.default.green("✓") : import_picocolors13.default.red("✗");
|
|
114639
115361
|
const lastOk = worker.health.lastOkAt ?? "never";
|
|
114640
|
-
await writeLine(ctx, ` ${symbol3} worker health: ${ok ? "ok" : `failing (${worker.health.consecutiveErrs} consecutive)`} last-ok=${
|
|
115362
|
+
await writeLine(ctx, ` ${symbol3} worker health: ${ok ? "ok" : `failing (${worker.health.consecutiveErrs} consecutive)`} last-ok=${import_picocolors13.default.dim(lastOk)}`);
|
|
114641
115363
|
}
|
|
114642
115364
|
if (worker.circuitBreaker) {
|
|
114643
115365
|
const breaker = worker.circuitBreaker;
|
|
114644
|
-
const symbol3 = breaker.open ?
|
|
115366
|
+
const symbol3 = breaker.open ? import_picocolors13.default.red("✗") : import_picocolors13.default.green("✓");
|
|
114645
115367
|
const state = breaker.open ? "open (deploys fail fast)" : "closed";
|
|
114646
115368
|
await writeLine(ctx, ` ${symbol3} circuit breaker: ${state} ${breaker.failuresInWindow}/${breaker.threshold} failures in last ${Math.round(breaker.windowMs / 1000)}s`);
|
|
114647
115369
|
}
|
|
114648
115370
|
if (result.health.version) {
|
|
114649
|
-
await writeLine(ctx,
|
|
115371
|
+
await writeLine(ctx, import_picocolors13.default.dim(` api version: ${result.health.version}`));
|
|
114650
115372
|
}
|
|
114651
115373
|
}
|
|
114652
115374
|
|
|
114653
115375
|
// src/commands/transfers.ts
|
|
114654
|
-
var
|
|
115376
|
+
var import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
114655
115377
|
function registerTransfersCommands(cli, ctx) {
|
|
114656
115378
|
cli.command("transfer [emailOrTransferId]", "Transfer ownership of an app: `transfer <email>` initiates; `transfer --cancel <id>` cancels; `transfer --list` shows history").option("--app <name>", "App name (overrides percher.toml)").option("--cancel", "Cancel a pending transfer by id (positional arg is the transferId)").option("--list", "List the transfer history for this app").action(async (emailOrTransferId, options) => {
|
|
114657
115379
|
if (options.list) {
|
|
114658
115380
|
const rows = await transferListForApp(ctx, { app: options.app });
|
|
114659
115381
|
if (rows.length === 0) {
|
|
114660
|
-
await writeLine(ctx,
|
|
115382
|
+
await writeLine(ctx, import_picocolors14.default.dim("No transfers for this app."));
|
|
114661
115383
|
return;
|
|
114662
115384
|
}
|
|
114663
115385
|
for (const r of rows) {
|
|
114664
|
-
const status2 = r.status === "pending" ?
|
|
115386
|
+
const status2 = r.status === "pending" ? import_picocolors14.default.yellow("pending") : r.status === "accepted" ? import_picocolors14.default.green("accepted") : import_picocolors14.default.dim(r.status);
|
|
114665
115387
|
await writeLine(ctx, `${r.id} ${status2.padEnd(20)} → ${r.toEmail} (${r.createdAt})`);
|
|
114666
115388
|
}
|
|
114667
115389
|
return;
|
|
@@ -114674,7 +115396,7 @@ function registerTransfersCommands(cli, ctx) {
|
|
|
114674
115396
|
app: options.app,
|
|
114675
115397
|
transferId: emailOrTransferId
|
|
114676
115398
|
});
|
|
114677
|
-
await writeLine(ctx, `${
|
|
115399
|
+
await writeLine(ctx, `${import_picocolors14.default.green("✓")} Cancelled transfer ${emailOrTransferId}`);
|
|
114678
115400
|
return;
|
|
114679
115401
|
}
|
|
114680
115402
|
if (!emailOrTransferId) {
|
|
@@ -114684,17 +115406,17 @@ function registerTransfersCommands(cli, ctx) {
|
|
|
114684
115406
|
app: options.app,
|
|
114685
115407
|
email: emailOrTransferId
|
|
114686
115408
|
});
|
|
114687
|
-
await writeLine(ctx, `${
|
|
114688
|
-
await writeLine(ctx,
|
|
115409
|
+
await writeLine(ctx, `${import_picocolors14.default.green("✓")} Transfer initiated to ${initiated.toEmail} (id ${initiated.id})`);
|
|
115410
|
+
await writeLine(ctx, import_picocolors14.default.dim(`Expires ${new Date(initiated.expiresAt).toLocaleDateString()}. The recipient must accept via 'percher transfers accept ${initiated.id}'.`));
|
|
114689
115411
|
if (initiated.notes) {
|
|
114690
|
-
await writeLine(ctx,
|
|
115412
|
+
await writeLine(ctx, import_picocolors14.default.yellow(` ⚠ ${initiated.notes}`));
|
|
114691
115413
|
}
|
|
114692
115414
|
});
|
|
114693
115415
|
cli.command("transfers [action] [transferId]", "Incoming ownership transfers: list (default) | accept <transferId> | decline <transferId>").action(async (action, transferId) => {
|
|
114694
115416
|
if (action === undefined || action === "list") {
|
|
114695
115417
|
const rows = await transfersIncoming(ctx, {});
|
|
114696
115418
|
if (rows.length === 0) {
|
|
114697
|
-
await writeLine(ctx,
|
|
115419
|
+
await writeLine(ctx, import_picocolors14.default.dim("No pending incoming transfers."));
|
|
114698
115420
|
return;
|
|
114699
115421
|
}
|
|
114700
115422
|
for (const r of rows) {
|
|
@@ -114707,7 +115429,7 @@ function registerTransfersCommands(cli, ctx) {
|
|
|
114707
115429
|
throw new Error("Usage: percher transfers accept <transferId>");
|
|
114708
115430
|
}
|
|
114709
115431
|
const accepted = await transfersAccept(ctx, { transferId });
|
|
114710
|
-
await writeLine(ctx, `${
|
|
115432
|
+
await writeLine(ctx, `${import_picocolors14.default.green("✓")} You now own ${import_picocolors14.default.cyan(accepted.appName)}. Run 'percher open --app ${accepted.appName}' to see it.`);
|
|
114711
115433
|
return;
|
|
114712
115434
|
}
|
|
114713
115435
|
if (action === "decline") {
|
|
@@ -114715,7 +115437,7 @@ function registerTransfersCommands(cli, ctx) {
|
|
|
114715
115437
|
throw new Error("Usage: percher transfers decline <transferId>");
|
|
114716
115438
|
}
|
|
114717
115439
|
await transfersDecline(ctx, { transferId });
|
|
114718
|
-
await writeLine(ctx, `${
|
|
115440
|
+
await writeLine(ctx, `${import_picocolors14.default.green("✓")} Declined transfer ${transferId}.`);
|
|
114719
115441
|
return;
|
|
114720
115442
|
}
|
|
114721
115443
|
throw new Error(`Unknown transfers action "${action}". Use list | accept <id> | decline <id>.`);
|
|
@@ -114784,40 +115506,52 @@ async function createCliContext() {
|
|
|
114784
115506
|
}
|
|
114785
115507
|
|
|
114786
115508
|
// src/output.ts
|
|
114787
|
-
var
|
|
115509
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
114788
115510
|
async function renderError(stderr, error48) {
|
|
114789
115511
|
const writer = stderr.getWriter();
|
|
114790
115512
|
const encoder = new TextEncoder;
|
|
114791
115513
|
const lines = [];
|
|
114792
115514
|
if (error48 instanceof PercherTomlError) {
|
|
114793
|
-
lines.push(`${
|
|
115515
|
+
lines.push(`${import_picocolors15.default.red(`error (${error48.code}):`)} ${error48.message}`);
|
|
114794
115516
|
if (error48.tomlLine !== undefined) {
|
|
114795
|
-
lines.push(
|
|
115517
|
+
lines.push(import_picocolors15.default.dim(` at percher.toml line ${error48.tomlLine}`));
|
|
114796
115518
|
}
|
|
114797
115519
|
for (const issue2 of error48.issues ?? []) {
|
|
114798
|
-
lines.push(`${
|
|
115520
|
+
lines.push(`${import_picocolors15.default.dim(" · ") + import_picocolors15.default.yellow(issue2.path.join("."))}: ${issue2.message}`);
|
|
115521
|
+
}
|
|
115522
|
+
} else if (error48 instanceof SunsetError) {
|
|
115523
|
+
if (error48.gateCode === "platform_delete_gated") {
|
|
115524
|
+
lines.push(`${import_picocolors15.default.yellow("Percher is shutting down — deletes are paused.")}`);
|
|
115525
|
+
lines.push(error48.message);
|
|
115526
|
+
lines.push(import_picocolors15.default.dim(` To delete and permanently forfeit your export window, confirm with your password in the dashboard: ${error48.dashboardUrl}`));
|
|
115527
|
+
lines.push(import_picocolors15.default.dim(" The CLI cannot perform this delete during sunset — it must go through the dashboard."));
|
|
115528
|
+
} else {
|
|
115529
|
+
lines.push(`${import_picocolors15.default.yellow("Percher is in read-only mode ahead of shutdown.")}`);
|
|
115530
|
+
lines.push(error48.message);
|
|
115531
|
+
lines.push(import_picocolors15.default.dim(` Your apps keep running. Export them any time from the dashboard: ${error48.dashboardUrl}`));
|
|
114799
115532
|
}
|
|
115533
|
+
lines.push(import_picocolors15.default.dim(` Final data deletion: ${error48.finalDeletionDate}`));
|
|
114800
115534
|
} else if (error48 instanceof PercherApiError) {
|
|
114801
115535
|
if (error48.status === 502 || error48.status === 503) {
|
|
114802
|
-
lines.push(`${
|
|
115536
|
+
lines.push(`${import_picocolors15.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error48.status}). Try again in a few seconds.`);
|
|
114803
115537
|
} else {
|
|
114804
|
-
lines.push(`${
|
|
114805
|
-
lines.push(
|
|
115538
|
+
lines.push(`${import_picocolors15.default.red("error:")} ${error48.message}`);
|
|
115539
|
+
lines.push(import_picocolors15.default.dim(` ${error48.code} (HTTP ${error48.status})`));
|
|
114806
115540
|
}
|
|
114807
115541
|
if (error48.requestId)
|
|
114808
|
-
lines.push(
|
|
115542
|
+
lines.push(import_picocolors15.default.dim(` request: ${error48.requestId}`));
|
|
114809
115543
|
} else if (error48 instanceof PercherCoreError) {
|
|
114810
|
-
lines.push(`${
|
|
115544
|
+
lines.push(`${import_picocolors15.default.red(`error (${error48.code}):`)} ${error48.message}`);
|
|
114811
115545
|
if (error48.hint)
|
|
114812
|
-
lines.push(
|
|
115546
|
+
lines.push(import_picocolors15.default.dim(` ${error48.hint}`));
|
|
114813
115547
|
} else if (error48 instanceof Error) {
|
|
114814
|
-
lines.push(`${
|
|
115548
|
+
lines.push(`${import_picocolors15.default.red("unexpected error:")} ${error48.message}`);
|
|
114815
115549
|
if (error48.stack)
|
|
114816
|
-
lines.push(
|
|
115550
|
+
lines.push(import_picocolors15.default.dim(error48.stack.split(`
|
|
114817
115551
|
`).slice(1, 4).join(`
|
|
114818
115552
|
`)));
|
|
114819
115553
|
} else {
|
|
114820
|
-
lines.push(`${
|
|
115554
|
+
lines.push(`${import_picocolors15.default.red("unexpected error:")} ${String(error48)}`);
|
|
114821
115555
|
}
|
|
114822
115556
|
try {
|
|
114823
115557
|
await writer.write(encoder.encode(`${lines.join(`
|
|
@@ -114830,7 +115564,7 @@ async function renderError(stderr, error48) {
|
|
|
114830
115564
|
// package.json
|
|
114831
115565
|
var package_default = {
|
|
114832
115566
|
name: "percher",
|
|
114833
|
-
version: "0.
|
|
115567
|
+
version: "0.5.0",
|
|
114834
115568
|
description: "CLI for the Percher hosting platform — publish apps with one command",
|
|
114835
115569
|
type: "module",
|
|
114836
115570
|
bin: {
|
|
@@ -114911,23 +115645,23 @@ async function showTelemetryNotice() {
|
|
|
114911
115645
|
if (isDisabled())
|
|
114912
115646
|
return;
|
|
114913
115647
|
const { homedir: homedir2 } = await import("node:os");
|
|
114914
|
-
const { existsSync:
|
|
114915
|
-
const { join:
|
|
114916
|
-
const configDir =
|
|
114917
|
-
const markerFile =
|
|
114918
|
-
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))
|
|
114919
115653
|
return;
|
|
114920
115654
|
console.log("Percher sends non-account-linked usage stats (command, result, error type, OS, Bun version).");
|
|
114921
115655
|
console.log(`Disable with PERCHER_NO_TELEMETRY=1
|
|
114922
115656
|
`);
|
|
114923
115657
|
try {
|
|
114924
115658
|
mkdirSync6(configDir, { recursive: true });
|
|
114925
|
-
|
|
115659
|
+
writeFileSync10(markerFile, new Date().toISOString());
|
|
114926
115660
|
} catch {}
|
|
114927
115661
|
}
|
|
114928
115662
|
|
|
114929
115663
|
// src/version-check.ts
|
|
114930
|
-
var
|
|
115664
|
+
var import_picocolors16 = __toESM(require_picocolors(), 1);
|
|
114931
115665
|
async function checkForUpdates() {
|
|
114932
115666
|
try {
|
|
114933
115667
|
const res = await fetch("https://registry.npmjs.org/percher/latest", {
|
|
@@ -114938,9 +115672,9 @@ async function checkForUpdates() {
|
|
|
114938
115672
|
const data2 = await res.json();
|
|
114939
115673
|
const latest = data2.version;
|
|
114940
115674
|
if (latest && latest !== VERSION) {
|
|
114941
|
-
console.error(
|
|
115675
|
+
console.error(import_picocolors16.default.yellow(`
|
|
114942
115676
|
Update available: ${VERSION} → ${latest}`));
|
|
114943
|
-
console.error(
|
|
115677
|
+
console.error(import_picocolors16.default.dim(`Run: bunx percher@latest
|
|
114944
115678
|
`));
|
|
114945
115679
|
}
|
|
114946
115680
|
} catch {}
|
|
@@ -114953,6 +115687,7 @@ async function run(argv, ctxOverride) {
|
|
|
114953
115687
|
registerAppsCommands(cli, ctx);
|
|
114954
115688
|
registerDeploysCommands(cli, ctx, argv);
|
|
114955
115689
|
registerEnvCommands(cli, ctx);
|
|
115690
|
+
registerExportCommands(cli, ctx);
|
|
114956
115691
|
registerDataCommands(cli, ctx, argv);
|
|
114957
115692
|
registerAuthCommands(cli, ctx);
|
|
114958
115693
|
registerAccountCommands(cli, ctx);
|