percher 0.4.2 → 0.5.1
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 +1521 -887
- package/dist/package.json +4 -4
- package/dist/src/commands/apps.d.ts.map +1 -1
- package/dist/src/commands/apps.js +149 -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/package.json +4 -4
- package/dist/context.d.ts +0 -3
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -31
- package/dist/context.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -385
- package/dist/index.js.map +0 -1
- package/dist/open.d.ts +0 -2
- package/dist/open.d.ts.map +0 -1
- package/dist/open.js +0 -16
- package/dist/open.js.map +0 -1
- package/dist/output.d.ts +0 -2
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -44
- package/dist/output.js.map +0 -1
- package/dist/prompt.d.ts +0 -4
- package/dist/prompt.d.ts.map +0 -1
- package/dist/prompt.js +0 -14
- package/dist/prompt.js.map +0 -1
- package/dist/templates/node/index.ts +0 -12
package/dist/bin/percher.js
CHANGED
|
@@ -103988,8 +103988,46 @@ function date4(params) {
|
|
|
103988
103988
|
|
|
103989
103989
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
103990
103990
|
config(en_default());
|
|
103991
|
-
// ../
|
|
103991
|
+
// ../shared/dist/app-name.js
|
|
103992
103992
|
var APP_NAME_RE = /^[a-z][a-z0-9-]{2,39}$/;
|
|
103993
|
+
// ../shared/dist/limits.js
|
|
103994
|
+
var MAX_TARBALL_BYTES = 500 * 1024 * 1024;
|
|
103995
|
+
// ../shared/dist/logs.js
|
|
103996
|
+
var MAX_LOG_SEARCH_LENGTH = 256;
|
|
103997
|
+
// ../shared/dist/platform-env.js
|
|
103998
|
+
var PERCHER_INJECTED_ENV_KEYS = new Set([
|
|
103999
|
+
"NODE_ENV",
|
|
104000
|
+
"PORT",
|
|
104001
|
+
"DATABASE_URL",
|
|
104002
|
+
"POCKETBASE_URL",
|
|
104003
|
+
"POCKETBASE_ADMIN_EMAIL",
|
|
104004
|
+
"POCKETBASE_ADMIN_PASSWORD",
|
|
104005
|
+
"PERCHER_APP_NAME",
|
|
104006
|
+
"PERCHER_APP_URL",
|
|
104007
|
+
"HTTP_PROXY",
|
|
104008
|
+
"HTTPS_PROXY",
|
|
104009
|
+
"NO_PROXY",
|
|
104010
|
+
"NODE_USE_ENV_PROXY"
|
|
104011
|
+
]);
|
|
104012
|
+
var PROXY_LOWERCASE_ALIASES = new Set([
|
|
104013
|
+
"http_proxy",
|
|
104014
|
+
"https_proxy",
|
|
104015
|
+
"no_proxy"
|
|
104016
|
+
]);
|
|
104017
|
+
// ../shared/dist/runtime.js
|
|
104018
|
+
var DEFAULT_NODE_VERSION_MAJOR = "24";
|
|
104019
|
+
var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
|
|
104020
|
+
// ../shared/dist/timeouts.js
|
|
104021
|
+
var TIMEOUTS = {
|
|
104022
|
+
workerBuildSubprocess: 10 * 60000,
|
|
104023
|
+
apiToWorkerBuildFetch: 11 * 60000,
|
|
104024
|
+
clientPublishPoll: 12 * 60000,
|
|
104025
|
+
workerHealthcheck: 5000,
|
|
104026
|
+
watchdogList: 15000,
|
|
104027
|
+
mcpWaitDeployDefault: 30000,
|
|
104028
|
+
mcpWaitDeployMax: 12 * 60000
|
|
104029
|
+
};
|
|
104030
|
+
// ../toml/dist/schema.js
|
|
103993
104031
|
var appSchema = exports_external.object({
|
|
103994
104032
|
name: exports_external.string().regex(APP_NAME_RE, "must be 3–40 chars, lowercase, start with a letter, [a-z0-9-] only"),
|
|
103995
104033
|
runtime: exports_external.enum(["node", "bun", "python", "static", "docker"]),
|
|
@@ -104030,6 +104068,7 @@ var buildSchema = exports_external.object({
|
|
|
104030
104068
|
var webSchema = exports_external.object({
|
|
104031
104069
|
port: exports_external.number().int().min(1024).max(65535),
|
|
104032
104070
|
health: exports_external.string().optional(),
|
|
104071
|
+
command: exports_external.string().min(1).max(1000).optional(),
|
|
104033
104072
|
password: exports_external.boolean().optional(),
|
|
104034
104073
|
spa: exports_external.boolean().optional()
|
|
104035
104074
|
}).strict();
|
|
@@ -104205,6 +104244,15 @@ var cronSchema = exports_external.object({
|
|
|
104205
104244
|
command: exports_external.string().min(1),
|
|
104206
104245
|
description: exports_external.string().max(200).optional()
|
|
104207
104246
|
}).strict();
|
|
104247
|
+
var cronsSchema = exports_external.record(exports_external.string(), cronSchema).superRefine((jobs, ctx) => {
|
|
104248
|
+
if (Object.hasOwn(jobs, "")) {
|
|
104249
|
+
ctx.addIssue({
|
|
104250
|
+
code: exports_external.ZodIssueCode.custom,
|
|
104251
|
+
path: [""],
|
|
104252
|
+
message: "cron job name must not be empty"
|
|
104253
|
+
});
|
|
104254
|
+
}
|
|
104255
|
+
});
|
|
104208
104256
|
var addonsSchema = exports_external.object({
|
|
104209
104257
|
pocketbase: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
|
|
104210
104258
|
cron: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
|
|
@@ -104229,7 +104277,7 @@ var percherConfigSchema = exports_external.object({
|
|
|
104229
104277
|
resources: resourcesSchema.optional(),
|
|
104230
104278
|
dev: devSchema.optional(),
|
|
104231
104279
|
required_env: requiredEnvSchema.optional(),
|
|
104232
|
-
crons:
|
|
104280
|
+
crons: cronsSchema.optional(),
|
|
104233
104281
|
addons: addonsSchema.optional(),
|
|
104234
104282
|
checks: exports_external.record(exports_external.string(), checkSchema).optional()
|
|
104235
104283
|
}).strict();
|
|
@@ -104900,229 +104948,43 @@ var billingUpgradeInputSchema = exports_external.object({
|
|
|
104900
104948
|
plan: exports_external.enum(["starter", "maker", "pro"])
|
|
104901
104949
|
});
|
|
104902
104950
|
var billingPortalInputSchema = exports_external.object({});
|
|
104903
|
-
// ../core/dist/commands/
|
|
104904
|
-
|
|
104905
|
-
|
|
104906
|
-
// ../core/dist/templates.js
|
|
104907
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
104908
|
-
import { basename, join as join5, normalize, relative, resolve as resolve3 } from "node:path";
|
|
104909
|
-
var __dirname = "C:\\dev\\Percher\\packages\\core\\dist";
|
|
104910
|
-
var TEMPLATES_DIR = resolve3(import.meta.dirname ?? __dirname, "../templates");
|
|
104911
|
-
var PLACEHOLDER = "__APP_NAME__";
|
|
104912
|
-
function listTemplates() {
|
|
104913
|
-
return readdirSync(TEMPLATES_DIR).filter((name) => {
|
|
104914
|
-
const full = join5(TEMPLATES_DIR, name);
|
|
104915
|
-
return statSync(full).isDirectory();
|
|
104916
|
-
});
|
|
104917
|
-
}
|
|
104918
|
-
function scaffoldTemplate(input) {
|
|
104919
|
-
const { template, appName, targetDir, force } = input;
|
|
104920
|
-
const available = listTemplates();
|
|
104921
|
-
if (!available.includes(template)) {
|
|
104922
|
-
throw new Error(`Unknown template "${template}". Available: ${available.join(", ")}`);
|
|
104923
|
-
}
|
|
104924
|
-
const absTarget = resolve3(targetDir);
|
|
104925
|
-
if (existsSync3(absTarget)) {
|
|
104926
|
-
const entries = readdirSync(absTarget);
|
|
104927
|
-
if (entries.length > 0 && !force) {
|
|
104928
|
-
throw new Error(`Target directory "${absTarget}" is not empty. Use --force to overwrite.`);
|
|
104929
|
-
}
|
|
104930
|
-
}
|
|
104931
|
-
const srcDir = join5(TEMPLATES_DIR, template);
|
|
104932
|
-
const files = [];
|
|
104933
|
-
function copyDir(src, dest) {
|
|
104934
|
-
mkdirSync3(dest, { recursive: true });
|
|
104935
|
-
for (const entry of readdirSync(src)) {
|
|
104936
|
-
const srcPath = join5(src, entry);
|
|
104937
|
-
const destPath = join5(dest, entry);
|
|
104938
|
-
const rel = relative(absTarget, normalize(destPath));
|
|
104939
|
-
if (rel.startsWith("..") || resolve3(destPath) === resolve3(absTarget, "..", basename(absTarget))) {
|
|
104940
|
-
throw new Error(`Template contains path traversal: ${entry}`);
|
|
104941
|
-
}
|
|
104942
|
-
if (statSync(srcPath).isDirectory()) {
|
|
104943
|
-
copyDir(srcPath, destPath);
|
|
104944
|
-
} else {
|
|
104945
|
-
if (existsSync3(destPath) && !force) {
|
|
104946
|
-
continue;
|
|
104947
|
-
}
|
|
104948
|
-
let content = readFileSync4(srcPath, "utf-8");
|
|
104949
|
-
content = content.replaceAll(PLACEHOLDER, appName);
|
|
104950
|
-
content = content.replace(/\r\n/g, `
|
|
104951
|
-
`);
|
|
104952
|
-
writeFileSync3(destPath, content, "utf-8");
|
|
104953
|
-
files.push(relative(absTarget, destPath).replace(/\\/g, "/"));
|
|
104954
|
-
}
|
|
104955
|
-
}
|
|
104956
|
-
}
|
|
104957
|
-
copyDir(srcDir, absTarget);
|
|
104958
|
-
return { files };
|
|
104959
|
-
}
|
|
104960
|
-
|
|
104961
|
-
// ../core/dist/commands/create.js
|
|
104962
|
-
var APP_NAME_RE2 = /^[a-z][a-z0-9-]{2,39}$/;
|
|
104963
|
-
var createInputSchema = exports_external.object({
|
|
104964
|
-
name: exports_external.string().regex(APP_NAME_RE2, "must be 3-40 chars, lowercase, start with a letter, [a-z0-9-] only"),
|
|
104965
|
-
template: exports_external.string().min(1, "Template is required"),
|
|
104966
|
-
directory: exports_external.string().optional(),
|
|
104967
|
-
force: exports_external.boolean().optional()
|
|
104968
|
-
});
|
|
104969
|
-
async function create(ctx, input) {
|
|
104970
|
-
const available = listTemplates();
|
|
104971
|
-
if (!available.includes(input.template)) {
|
|
104972
|
-
throw new PercherCoreError(`Unknown template "${input.template}". Available: ${available.join(", ")}`, {
|
|
104973
|
-
code: "create.invalid-template",
|
|
104974
|
-
hint: `Run "percher create --list-templates" to see available templates.`
|
|
104975
|
-
});
|
|
104976
|
-
}
|
|
104977
|
-
let targetDir;
|
|
104978
|
-
if (input.directory) {
|
|
104979
|
-
targetDir = resolve4(ctx.cwd, input.directory);
|
|
104980
|
-
if (!targetDir.startsWith(resolve4(ctx.cwd))) {
|
|
104981
|
-
throw new PercherCoreError("Directory must be within the current workspace", {
|
|
104982
|
-
code: "create.failed"
|
|
104983
|
-
});
|
|
104984
|
-
}
|
|
104985
|
-
} else {
|
|
104986
|
-
targetDir = join6(ctx.cwd, input.name);
|
|
104987
|
-
}
|
|
104988
|
-
try {
|
|
104989
|
-
const result = scaffoldTemplate({
|
|
104990
|
-
template: input.template,
|
|
104991
|
-
appName: input.name,
|
|
104992
|
-
targetDir,
|
|
104993
|
-
force: input.force
|
|
104994
|
-
});
|
|
104995
|
-
return {
|
|
104996
|
-
...result,
|
|
104997
|
-
name: input.name,
|
|
104998
|
-
template: input.template,
|
|
104999
|
-
directory: targetDir
|
|
105000
|
-
};
|
|
105001
|
-
} catch (err) {
|
|
105002
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
105003
|
-
if (msg.includes("not empty")) {
|
|
105004
|
-
throw new PercherCoreError(msg, {
|
|
105005
|
-
code: "create.dir-not-empty",
|
|
105006
|
-
hint: "Use --force to overwrite existing files."
|
|
105007
|
-
});
|
|
105008
|
-
}
|
|
105009
|
-
throw new PercherCoreError(msg, { code: "create.failed" });
|
|
105010
|
-
}
|
|
105011
|
-
}
|
|
105012
|
-
// ../core/dist/commands/dashboard.js
|
|
105013
|
-
var dashboardInputSchema = exports_external.object({
|
|
105014
|
-
app: exports_external.string().optional(),
|
|
105015
|
-
tab: exports_external.string().optional()
|
|
104951
|
+
// ../core/dist/commands/cache.js
|
|
104952
|
+
var cacheResetInputSchema = exports_external.object({
|
|
104953
|
+
app: exports_external.string().optional()
|
|
105016
104954
|
});
|
|
105017
|
-
|
|
105018
|
-
var TAB_SHAPE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
105019
|
-
async function dashboard(ctx, input = {}) {
|
|
104955
|
+
async function cacheReset(ctx, input = {}) {
|
|
105020
104956
|
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
105021
104957
|
if (!name) {
|
|
105022
104958
|
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
105023
|
-
code: "
|
|
104959
|
+
code: "env.no-app",
|
|
105024
104960
|
hint: "Pass an app name or run from a directory with percher.toml."
|
|
105025
104961
|
});
|
|
105026
104962
|
}
|
|
105027
|
-
|
|
105028
|
-
if (!TAB_SHAPE.test(tab)) {
|
|
105029
|
-
throw new PercherCoreError(`Invalid tab "${tab}"`, {
|
|
105030
|
-
code: "dashboard.invalid-tab",
|
|
105031
|
-
hint: "Tab must be lowercase letters, digits, and hyphens (≤32 chars)."
|
|
105032
|
-
});
|
|
105033
|
-
}
|
|
105034
|
-
const base = ctx.env.PERCHER_DASHBOARD_URL ?? DEFAULT_BASE;
|
|
105035
|
-
const url2 = `${base}/apps/${encodeURIComponent(name)}?tab=${tab}`;
|
|
105036
|
-
await ctx.openUrl(url2);
|
|
105037
|
-
return url2;
|
|
104963
|
+
return ctx.client.apps.resetCache(name);
|
|
105038
104964
|
}
|
|
105039
|
-
// ../core/dist/commands/
|
|
105040
|
-
var
|
|
105041
|
-
|
|
105042
|
-
|
|
105043
|
-
|
|
105044
|
-
|
|
105045
|
-
|
|
105046
|
-
|
|
105047
|
-
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
105048
|
-
code: "data.no-app",
|
|
105049
|
-
hint: "Pass an app name or run from a directory with percher.toml."
|
|
105050
|
-
});
|
|
105051
|
-
}
|
|
105052
|
-
const status = await ctx.client.data.getStatus(name);
|
|
105053
|
-
const adminLink = await ctx.client.data.getAdminLink(name);
|
|
105054
|
-
let collections = [];
|
|
105055
|
-
let stats = { collections: 0, totalRecords: 0 };
|
|
105056
|
-
let warning = null;
|
|
105057
|
-
try {
|
|
105058
|
-
const cols = await ctx.client.data.getCollections(name);
|
|
105059
|
-
collections = cols.map((c) => ({ name: c.name, type: c.type }));
|
|
105060
|
-
stats = await ctx.client.data.getStats(name);
|
|
105061
|
-
} catch (err) {
|
|
105062
|
-
const code = err.code;
|
|
105063
|
-
if (code === "PB_AUTH_FAILED") {
|
|
105064
|
-
warning = `Could not authenticate with PocketBase — collections + stats unavailable. Try \`percher data reset-superuser ${name}\` to rotate the credentials.`;
|
|
105065
|
-
} else {
|
|
105066
|
-
throw err;
|
|
104965
|
+
// ../core/dist/commands/capabilities.js
|
|
104966
|
+
var capabilitiesInputSchema = exports_external.object({});
|
|
104967
|
+
// ../client/dist/abortable-sleep.js
|
|
104968
|
+
function abortableSleep(ms, signal) {
|
|
104969
|
+
return new Promise((resolve3) => {
|
|
104970
|
+
if (!signal) {
|
|
104971
|
+
setTimeout(resolve3, ms);
|
|
104972
|
+
return;
|
|
105067
104973
|
}
|
|
105068
|
-
|
|
105069
|
-
|
|
105070
|
-
|
|
105071
|
-
|
|
105072
|
-
|
|
105073
|
-
|
|
105074
|
-
|
|
105075
|
-
|
|
105076
|
-
|
|
105077
|
-
|
|
105078
|
-
|
|
105079
|
-
|
|
105080
|
-
|
|
105081
|
-
|
|
105082
|
-
app: exports_external.string().optional().describe("App name or id. Defaults to the app in percher.toml."),
|
|
105083
|
-
out: exports_external.string().optional().describe("Output file path. Defaults to ./<app>-pb-<timestamp>.tar.gz in the current directory.")
|
|
105084
|
-
});
|
|
105085
|
-
async function dataExport(ctx, input) {
|
|
105086
|
-
const appName = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
105087
|
-
if (!appName) {
|
|
105088
|
-
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
105089
|
-
code: "data-export.no-app",
|
|
105090
|
-
hint: "Pass an app name with --app <name> or run from a directory with percher.toml."
|
|
105091
|
-
});
|
|
105092
|
-
}
|
|
105093
|
-
ctx.status?.(`Exporting ${appName} data...`);
|
|
105094
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
105095
|
-
const fileName = input.out ?? `${appName}-pb-${stamp}.tar.gz`;
|
|
105096
|
-
const path = resolve5(ctx.cwd, fileName);
|
|
105097
|
-
const res = await ctx.client.apps.streamDataExport(appName);
|
|
105098
|
-
const bytes = await writeResponseToFile(res, path);
|
|
105099
|
-
return {
|
|
105100
|
-
app: appName,
|
|
105101
|
-
path,
|
|
105102
|
-
bytes,
|
|
105103
|
-
exportedAt: new Date().toISOString()
|
|
105104
|
-
};
|
|
105105
|
-
}
|
|
105106
|
-
// ../core/dist/commands/delete.js
|
|
105107
|
-
var deleteInputSchema = exports_external.object({
|
|
105108
|
-
app: exports_external.string().optional().describe("App name to delete (defaults to percher.toml app name)"),
|
|
105109
|
-
confirm: exports_external.boolean().describe("Must be true to confirm deletion. This is a destructive operation that permanently removes the app, its container, database, domains, env vars, and deploy history.")
|
|
105110
|
-
});
|
|
105111
|
-
async function deleteApp(ctx, input) {
|
|
105112
|
-
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
105113
|
-
if (!name) {
|
|
105114
|
-
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
105115
|
-
code: "env.no-app",
|
|
105116
|
-
hint: "Pass an app name or run from a directory with percher.toml."
|
|
105117
|
-
});
|
|
105118
|
-
}
|
|
105119
|
-
if (!input.confirm) {
|
|
105120
|
-
throw new PercherCoreError(`Deletion requires explicit confirmation. Set confirm: true to delete "${name}" permanently.`, {
|
|
105121
|
-
code: "delete.confirmation-required",
|
|
105122
|
-
hint: `This will permanently delete the app "${name}" and all its data. Pass confirm: true to proceed.`
|
|
105123
|
-
});
|
|
105124
|
-
}
|
|
105125
|
-
await ctx.client.apps.delete(name);
|
|
104974
|
+
if (signal.aborted) {
|
|
104975
|
+
resolve3();
|
|
104976
|
+
return;
|
|
104977
|
+
}
|
|
104978
|
+
const onAbort = () => {
|
|
104979
|
+
clearTimeout(t);
|
|
104980
|
+
resolve3();
|
|
104981
|
+
};
|
|
104982
|
+
const t = setTimeout(() => {
|
|
104983
|
+
signal.removeEventListener("abort", onAbort);
|
|
104984
|
+
resolve3();
|
|
104985
|
+
}, ms);
|
|
104986
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
104987
|
+
});
|
|
105126
104988
|
}
|
|
105127
104989
|
// ../client/dist/build-env-prefixes.js
|
|
105128
104990
|
var AUTO_FORWARD_PREFIXES = [
|
|
@@ -105138,9 +105000,9 @@ function isAutoForwardedBuildKey(name) {
|
|
|
105138
105000
|
return AUTO_FORWARD_PREFIXES.some((p) => name.startsWith(p));
|
|
105139
105001
|
}
|
|
105140
105002
|
// ../client/dist/config.js
|
|
105141
|
-
import { existsSync as
|
|
105003
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
105142
105004
|
import { homedir } from "node:os";
|
|
105143
|
-
import { dirname as dirname3, join as
|
|
105005
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
105144
105006
|
var DEFAULT_API_URL = "https://api.percher.run";
|
|
105145
105007
|
function home() {
|
|
105146
105008
|
return process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
@@ -105148,15 +105010,15 @@ function home() {
|
|
|
105148
105010
|
function configPath() {
|
|
105149
105011
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
105150
105012
|
if (xdg)
|
|
105151
|
-
return
|
|
105152
|
-
return
|
|
105013
|
+
return join5(xdg, "percher", "config.json");
|
|
105014
|
+
return join5(home(), ".percher", "config.json");
|
|
105153
105015
|
}
|
|
105154
105016
|
function readFile2() {
|
|
105155
105017
|
const p = configPath();
|
|
105156
|
-
if (!
|
|
105018
|
+
if (!existsSync3(p))
|
|
105157
105019
|
return {};
|
|
105158
105020
|
try {
|
|
105159
|
-
const parsed = JSON.parse(
|
|
105021
|
+
const parsed = JSON.parse(readFileSync4(p, "utf8"));
|
|
105160
105022
|
return parsed ?? {};
|
|
105161
105023
|
} catch {
|
|
105162
105024
|
return {};
|
|
@@ -105168,16 +105030,17 @@ function loadConfig() {
|
|
|
105168
105030
|
const envToken = process.env.PERCHER_TOKEN;
|
|
105169
105031
|
return {
|
|
105170
105032
|
apiUrl: envUrl ?? file2.apiUrl ?? DEFAULT_API_URL,
|
|
105171
|
-
token: envToken ?? file2.token ?? null
|
|
105033
|
+
token: envToken ?? file2.token ?? null,
|
|
105034
|
+
anonClaim: file2.anonClaim ?? null
|
|
105172
105035
|
};
|
|
105173
105036
|
}
|
|
105174
105037
|
function saveConfig(partial2) {
|
|
105175
105038
|
const p = configPath();
|
|
105176
|
-
|
|
105039
|
+
mkdirSync3(dirname3(p), { recursive: true });
|
|
105177
105040
|
const current = readFile2();
|
|
105178
105041
|
const merged = { ...current, ...partial2 };
|
|
105179
105042
|
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
105180
|
-
|
|
105043
|
+
writeFileSync3(tmp, JSON.stringify(merged, null, 2), "utf8");
|
|
105181
105044
|
renameSync(tmp, p);
|
|
105182
105045
|
}
|
|
105183
105046
|
|
|
@@ -105196,6 +105059,21 @@ class PercherApiError extends Error {
|
|
|
105196
105059
|
this.extra = options.extra;
|
|
105197
105060
|
}
|
|
105198
105061
|
}
|
|
105062
|
+
|
|
105063
|
+
class NetworkError extends Error {
|
|
105064
|
+
constructor(message, options) {
|
|
105065
|
+
super(`network error: ${message}`, options);
|
|
105066
|
+
this.name = "NetworkError";
|
|
105067
|
+
}
|
|
105068
|
+
}
|
|
105069
|
+
var TRANSIENT_NETWORK_MESSAGE = /network error|fetch failed|ECONNRESET|ENOTFOUND|ETIMEDOUT/i;
|
|
105070
|
+
function isTransientNetworkError(err) {
|
|
105071
|
+
if (err instanceof NetworkError)
|
|
105072
|
+
return true;
|
|
105073
|
+
if (err instanceof PercherApiError)
|
|
105074
|
+
return false;
|
|
105075
|
+
return err instanceof Error && TRANSIENT_NETWORK_MESSAGE.test(err.message);
|
|
105076
|
+
}
|
|
105199
105077
|
var RESTORE_JOB_STATUSES_FOR_ERROR = [
|
|
105200
105078
|
"preparing",
|
|
105201
105079
|
"quiescing",
|
|
@@ -105244,6 +105122,11 @@ function isSunsetGateBody(body) {
|
|
|
105244
105122
|
}
|
|
105245
105123
|
|
|
105246
105124
|
// ../client/dist/http.js
|
|
105125
|
+
var BODY_SNIPPET_MAX = 120;
|
|
105126
|
+
function bodySnippet(body) {
|
|
105127
|
+
return body.length <= BODY_SNIPPET_MAX ? body : `${body.slice(0, BODY_SNIPPET_MAX)}…`;
|
|
105128
|
+
}
|
|
105129
|
+
|
|
105247
105130
|
class Http {
|
|
105248
105131
|
apiUrl;
|
|
105249
105132
|
token;
|
|
@@ -105257,8 +105140,8 @@ class Http {
|
|
|
105257
105140
|
this.clientVersion = options.clientVersion;
|
|
105258
105141
|
this.sessionId = options.sessionId;
|
|
105259
105142
|
}
|
|
105260
|
-
get(path) {
|
|
105261
|
-
return this.request("GET", path);
|
|
105143
|
+
get(path, opts) {
|
|
105144
|
+
return this.request("GET", path, undefined, opts?.signal);
|
|
105262
105145
|
}
|
|
105263
105146
|
post(path, body) {
|
|
105264
105147
|
return this.request("POST", path, body);
|
|
@@ -105296,7 +105179,7 @@ class Http {
|
|
|
105296
105179
|
try {
|
|
105297
105180
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, { method: "GET", headers });
|
|
105298
105181
|
} catch (err) {
|
|
105299
|
-
throw new
|
|
105182
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105300
105183
|
}
|
|
105301
105184
|
if (!res.ok)
|
|
105302
105185
|
throw await this.parseError(res);
|
|
@@ -105322,15 +105205,15 @@ class Http {
|
|
|
105322
105205
|
try {
|
|
105323
105206
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
105324
105207
|
} catch (err) {
|
|
105325
|
-
throw new
|
|
105208
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105326
105209
|
}
|
|
105327
105210
|
if (!res.ok)
|
|
105328
105211
|
throw await this.parseError(res);
|
|
105329
105212
|
return await res.json();
|
|
105330
105213
|
}
|
|
105331
|
-
async request(method, path, body) {
|
|
105214
|
+
async request(method, path, body, signal) {
|
|
105332
105215
|
const headers = this.buildHeaders();
|
|
105333
|
-
const init = { method, headers };
|
|
105216
|
+
const init = { method, headers, signal };
|
|
105334
105217
|
if (body !== undefined) {
|
|
105335
105218
|
headers["Content-Type"] = "application/json";
|
|
105336
105219
|
init.body = JSON.stringify(body);
|
|
@@ -105339,7 +105222,7 @@ class Http {
|
|
|
105339
105222
|
try {
|
|
105340
105223
|
res = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
105341
105224
|
} catch (err) {
|
|
105342
|
-
throw new
|
|
105225
|
+
throw new NetworkError(err.message, { cause: err });
|
|
105343
105226
|
}
|
|
105344
105227
|
if (!res.ok)
|
|
105345
105228
|
throw await this.parseError(res);
|
|
@@ -105350,7 +105233,10 @@ class Http {
|
|
|
105350
105233
|
return await res.json();
|
|
105351
105234
|
if (contentType.startsWith("text/"))
|
|
105352
105235
|
return await res.text();
|
|
105353
|
-
|
|
105236
|
+
const raw = await res.text();
|
|
105237
|
+
if (raw.length === 0)
|
|
105238
|
+
return;
|
|
105239
|
+
throw new PercherApiError(`unexpected content-type "${contentType || "none"}" on ${method} ${path}: ${bodySnippet(raw)}`, { status: res.status, code: "UNEXPECTED_CONTENT_TYPE" });
|
|
105354
105240
|
}
|
|
105355
105241
|
buildHeaders() {
|
|
105356
105242
|
const headers = {
|
|
@@ -105451,11 +105337,26 @@ class AlertsResource {
|
|
|
105451
105337
|
}
|
|
105452
105338
|
}
|
|
105453
105339
|
|
|
105454
|
-
// ../client/dist/resources/
|
|
105340
|
+
// ../client/dist/resources/anon.js
|
|
105341
|
+
class AnonResource {
|
|
105342
|
+
http;
|
|
105343
|
+
constructor(http) {
|
|
105344
|
+
this.http = http;
|
|
105345
|
+
}
|
|
105346
|
+
requestChallenge() {
|
|
105347
|
+
return this.http.get("/anon/challenge");
|
|
105348
|
+
}
|
|
105349
|
+
createAnonApp(input) {
|
|
105350
|
+
return this.http.post("/anon/apps", input);
|
|
105351
|
+
}
|
|
105352
|
+
}
|
|
105353
|
+
|
|
105354
|
+
// ../client/dist/resources/apps-types.js
|
|
105455
105355
|
function isDeployAlreadyInProgress(res) {
|
|
105456
105356
|
return res.status === "already_in_progress";
|
|
105457
105357
|
}
|
|
105458
105358
|
|
|
105359
|
+
// ../client/dist/resources/apps.js
|
|
105459
105360
|
class AppsResource {
|
|
105460
105361
|
http;
|
|
105461
105362
|
constructor(http) {
|
|
@@ -105470,8 +105371,20 @@ class AppsResource {
|
|
|
105470
105371
|
listLight() {
|
|
105471
105372
|
return this.http.get("/apps?light=1");
|
|
105472
105373
|
}
|
|
105473
|
-
get(id) {
|
|
105474
|
-
return this.http.get(`/apps/${encodeURIComponent(id)}
|
|
105374
|
+
get(id, opts) {
|
|
105375
|
+
return this.http.get(`/apps/${encodeURIComponent(id)}`, opts);
|
|
105376
|
+
}
|
|
105377
|
+
claim(appRef, input) {
|
|
105378
|
+
return this.http.post(`/apps/${encodeURIComponent(appRef)}/claim`, input);
|
|
105379
|
+
}
|
|
105380
|
+
createInspectLink(appRef) {
|
|
105381
|
+
return this.http.post(`/apps/${encodeURIComponent(appRef)}/inspect-link`, {});
|
|
105382
|
+
}
|
|
105383
|
+
inspectLinkStatus(appRef) {
|
|
105384
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/inspect-link`);
|
|
105385
|
+
}
|
|
105386
|
+
revokeInspectLink(appRef) {
|
|
105387
|
+
return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/inspect-link`);
|
|
105475
105388
|
}
|
|
105476
105389
|
async deploy(id, input) {
|
|
105477
105390
|
const params = new URLSearchParams;
|
|
@@ -105510,6 +105423,12 @@ class AppsResource {
|
|
|
105510
105423
|
enabled
|
|
105511
105424
|
});
|
|
105512
105425
|
}
|
|
105426
|
+
setPreviewBranch(appRef, branch) {
|
|
105427
|
+
return this.http.put(`/apps/${encodeURIComponent(appRef)}/preview-branch`, { branch });
|
|
105428
|
+
}
|
|
105429
|
+
unsetPreviewBranch(appRef) {
|
|
105430
|
+
return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/preview-branch`);
|
|
105431
|
+
}
|
|
105513
105432
|
setDescription(appRef, description) {
|
|
105514
105433
|
return this.http.patch(`/apps/${encodeURIComponent(appRef)}/description`, {
|
|
105515
105434
|
description
|
|
@@ -105548,8 +105467,8 @@ class AppsResource {
|
|
|
105548
105467
|
discardPreview(appRef, deployId) {
|
|
105549
105468
|
return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/discard`, {});
|
|
105550
105469
|
}
|
|
105551
|
-
getDeployment(appRef, deployId) {
|
|
105552
|
-
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}
|
|
105470
|
+
getDeployment(appRef, deployId, opts) {
|
|
105471
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}`, opts);
|
|
105553
105472
|
}
|
|
105554
105473
|
listDeploys(appRef, opts) {
|
|
105555
105474
|
const params = new URLSearchParams;
|
|
@@ -105558,32 +105477,36 @@ class AppsResource {
|
|
|
105558
105477
|
if (opts?.limit)
|
|
105559
105478
|
params.set("limit", String(opts.limit));
|
|
105560
105479
|
const qs = params.toString();
|
|
105561
|
-
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys${qs ? `?${qs}` : ""}
|
|
105480
|
+
return this.http.get(`/apps/${encodeURIComponent(appRef)}/deploys${qs ? `?${qs}` : ""}`, opts?.signal ? { signal: opts.signal } : undefined);
|
|
105562
105481
|
}
|
|
105563
|
-
async getBuildLog(appRef, deployId) {
|
|
105482
|
+
async getBuildLog(appRef, deployId, opts) {
|
|
105564
105483
|
const path = `/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/build-log`;
|
|
105565
105484
|
let attempt = 0;
|
|
105566
105485
|
const maxRetries = 5;
|
|
105567
105486
|
while (true) {
|
|
105568
105487
|
try {
|
|
105569
|
-
const res = await this.http.get(path);
|
|
105488
|
+
const res = await this.http.get(path, opts);
|
|
105570
105489
|
if (typeof res === "string")
|
|
105571
105490
|
return res;
|
|
105572
105491
|
return res?.buildLog ?? "";
|
|
105573
105492
|
} catch (err) {
|
|
105574
105493
|
attempt++;
|
|
105575
|
-
|
|
105576
|
-
const isNetwork = msg.startsWith("network error:");
|
|
105577
|
-
if (!isNetwork || attempt >= maxRetries)
|
|
105494
|
+
if (opts?.signal?.aborted || !isTransientNetworkError(err) || attempt >= maxRetries) {
|
|
105578
105495
|
throw err;
|
|
105496
|
+
}
|
|
105579
105497
|
const backoffMs = 1000 * 2 ** (attempt - 1);
|
|
105580
|
-
await
|
|
105498
|
+
await abortableSleep(backoffMs, opts?.signal);
|
|
105499
|
+
if (opts?.signal?.aborted)
|
|
105500
|
+
throw err;
|
|
105581
105501
|
}
|
|
105582
105502
|
}
|
|
105583
105503
|
}
|
|
105584
105504
|
unsuspend(appRef) {
|
|
105585
105505
|
return this.http.post(`/apps/${encodeURIComponent(appRef)}/unsuspend`, {});
|
|
105586
105506
|
}
|
|
105507
|
+
resetCache(appRef) {
|
|
105508
|
+
return this.http.post(`/apps/${encodeURIComponent(appRef)}/cache/reset`, {});
|
|
105509
|
+
}
|
|
105587
105510
|
delete(id) {
|
|
105588
105511
|
return this.http.delete(`/apps/${encodeURIComponent(id)}`);
|
|
105589
105512
|
}
|
|
@@ -105957,6 +105880,9 @@ class PlatformResource {
|
|
|
105957
105880
|
health() {
|
|
105958
105881
|
return this.http.get("/health");
|
|
105959
105882
|
}
|
|
105883
|
+
capabilities() {
|
|
105884
|
+
return this.http.get("/capabilities");
|
|
105885
|
+
}
|
|
105960
105886
|
preflight() {
|
|
105961
105887
|
return this.http.get("/publish/preflight");
|
|
105962
105888
|
}
|
|
@@ -106003,6 +105929,9 @@ class SecurityResource {
|
|
|
106003
105929
|
overview() {
|
|
106004
105930
|
return this.http.get("/admin/security/overview");
|
|
106005
105931
|
}
|
|
105932
|
+
platformBaseOverview() {
|
|
105933
|
+
return this.http.get("/admin/security/platform-base");
|
|
105934
|
+
}
|
|
106006
105935
|
rescanAllAsAdmin() {
|
|
106007
105936
|
return this.http.post("/admin/security/rescan", {});
|
|
106008
105937
|
}
|
|
@@ -106072,57 +106001,315 @@ class TransfersResource {
|
|
|
106072
106001
|
return this.http.post(`/transfers/${encodeURIComponent(transferId)}/decline`, {});
|
|
106073
106002
|
}
|
|
106074
106003
|
}
|
|
106075
|
-
|
|
106076
|
-
// ../client/dist/client.js
|
|
106077
|
-
class PercherClient {
|
|
106078
|
-
apiUrl;
|
|
106079
|
-
token;
|
|
106080
|
-
sessionId;
|
|
106081
|
-
apps;
|
|
106082
|
-
|
|
106083
|
-
|
|
106084
|
-
|
|
106085
|
-
|
|
106086
|
-
|
|
106087
|
-
|
|
106088
|
-
|
|
106089
|
-
|
|
106090
|
-
|
|
106091
|
-
|
|
106092
|
-
|
|
106093
|
-
|
|
106094
|
-
|
|
106095
|
-
|
|
106096
|
-
|
|
106097
|
-
|
|
106098
|
-
|
|
106099
|
-
|
|
106100
|
-
this.
|
|
106101
|
-
this.
|
|
106102
|
-
|
|
106103
|
-
|
|
106104
|
-
|
|
106105
|
-
|
|
106106
|
-
|
|
106107
|
-
|
|
106004
|
+
|
|
106005
|
+
// ../client/dist/client.js
|
|
106006
|
+
class PercherClient {
|
|
106007
|
+
apiUrl;
|
|
106008
|
+
token;
|
|
106009
|
+
sessionId;
|
|
106010
|
+
apps;
|
|
106011
|
+
anon;
|
|
106012
|
+
data;
|
|
106013
|
+
env;
|
|
106014
|
+
envScan;
|
|
106015
|
+
export;
|
|
106016
|
+
logs;
|
|
106017
|
+
auth;
|
|
106018
|
+
billing;
|
|
106019
|
+
account;
|
|
106020
|
+
alerts;
|
|
106021
|
+
platform;
|
|
106022
|
+
security;
|
|
106023
|
+
search;
|
|
106024
|
+
sharing;
|
|
106025
|
+
invites;
|
|
106026
|
+
transfers;
|
|
106027
|
+
constructor(options = {}) {
|
|
106028
|
+
const defaults = loadConfig();
|
|
106029
|
+
this.apiUrl = options.apiUrl ?? defaults.apiUrl;
|
|
106030
|
+
this.token = options.token !== undefined ? options.token : defaults.token;
|
|
106031
|
+
this.sessionId = options.sessionId;
|
|
106032
|
+
const http = new Http({
|
|
106033
|
+
apiUrl: this.apiUrl,
|
|
106034
|
+
token: this.token,
|
|
106035
|
+
fetchImpl: options.fetchImpl,
|
|
106036
|
+
clientVersion: options.clientVersion,
|
|
106037
|
+
sessionId: options.sessionId
|
|
106038
|
+
});
|
|
106039
|
+
this.apps = new AppsResource(http);
|
|
106040
|
+
this.anon = new AnonResource(http);
|
|
106041
|
+
this.data = new DataResource(http);
|
|
106042
|
+
this.env = new EnvResource(http);
|
|
106043
|
+
this.envScan = new EnvScanResource(http);
|
|
106044
|
+
this.export = new ExportResource(http);
|
|
106045
|
+
this.logs = new LogsResource(http);
|
|
106046
|
+
this.auth = new AuthResource(http);
|
|
106047
|
+
this.billing = new BillingResource(http);
|
|
106048
|
+
this.account = new AccountResource(http);
|
|
106049
|
+
this.alerts = new AlertsResource(http);
|
|
106050
|
+
this.platform = new PlatformResource(http);
|
|
106051
|
+
this.security = new SecurityResource(http);
|
|
106052
|
+
this.search = new SearchResource(http);
|
|
106053
|
+
this.sharing = new SharingResource(http);
|
|
106054
|
+
this.invites = new InvitesResource(http);
|
|
106055
|
+
this.transfers = new TransfersResource(http);
|
|
106056
|
+
}
|
|
106057
|
+
}
|
|
106058
|
+
// ../core/dist/commands/claim.js
|
|
106059
|
+
var claimInputSchema = exports_external.object({
|
|
106060
|
+
app: exports_external.string().optional().describe("App name to claim (defaults to the persisted anon state or percher.toml)"),
|
|
106061
|
+
claimToken: exports_external.string().optional().describe("The one-time claim token returned by the anonymous publish (pcl_…)")
|
|
106062
|
+
});
|
|
106063
|
+
async function claim(ctx, input = {}) {
|
|
106064
|
+
const state = loadConfig().anonClaim;
|
|
106065
|
+
const app = input.app ?? state?.app ?? readPercherTomlAppName(ctx.cwd) ?? undefined;
|
|
106066
|
+
const claimToken = input.claimToken ?? state?.claimToken;
|
|
106067
|
+
if (!app) {
|
|
106068
|
+
return {
|
|
106069
|
+
status: "needs_token",
|
|
106070
|
+
summary: "No app to claim. Pass --app (or run from the project that published it) and the claim token."
|
|
106071
|
+
};
|
|
106072
|
+
}
|
|
106073
|
+
if (!claimToken) {
|
|
106074
|
+
return {
|
|
106075
|
+
status: "needs_token",
|
|
106076
|
+
summary: "No claim token found. Pass claimToken explicitly — it was returned by the anonymous publish."
|
|
106077
|
+
};
|
|
106078
|
+
}
|
|
106079
|
+
const result = await ctx.client.apps.claim(app, { claimToken });
|
|
106080
|
+
if (state)
|
|
106081
|
+
saveConfig({ anonClaim: null });
|
|
106082
|
+
return {
|
|
106083
|
+
status: "claimed",
|
|
106084
|
+
app: result.name,
|
|
106085
|
+
url: result.url,
|
|
106086
|
+
summary: `Claimed ${result.name} — anonymous restrictions lift on the next deploy.`
|
|
106087
|
+
};
|
|
106088
|
+
}
|
|
106089
|
+
// ../core/dist/commands/create.js
|
|
106090
|
+
import { join as join7, resolve as resolve4 } from "node:path";
|
|
106091
|
+
|
|
106092
|
+
// ../core/dist/templates.js
|
|
106093
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
106094
|
+
import { basename, join as join6, normalize, relative, resolve as resolve3 } from "node:path";
|
|
106095
|
+
var __dirname = "C:\\dev\\Percher-lakebed-clean\\packages\\core\\dist";
|
|
106096
|
+
var TEMPLATES_DIR = resolve3(import.meta.dirname ?? __dirname, "../templates");
|
|
106097
|
+
var PLACEHOLDER = "__APP_NAME__";
|
|
106098
|
+
function listTemplates() {
|
|
106099
|
+
return readdirSync(TEMPLATES_DIR).filter((name) => {
|
|
106100
|
+
const full = join6(TEMPLATES_DIR, name);
|
|
106101
|
+
return statSync(full).isDirectory();
|
|
106102
|
+
});
|
|
106103
|
+
}
|
|
106104
|
+
function scaffoldTemplate(input) {
|
|
106105
|
+
const { template, appName, targetDir, force } = input;
|
|
106106
|
+
const available = listTemplates();
|
|
106107
|
+
if (!available.includes(template)) {
|
|
106108
|
+
throw new Error(`Unknown template "${template}". Available: ${available.join(", ")}`);
|
|
106109
|
+
}
|
|
106110
|
+
const absTarget = resolve3(targetDir);
|
|
106111
|
+
if (existsSync4(absTarget)) {
|
|
106112
|
+
const entries = readdirSync(absTarget);
|
|
106113
|
+
if (entries.length > 0 && !force) {
|
|
106114
|
+
throw new Error(`Target directory "${absTarget}" is not empty. Use --force to overwrite.`);
|
|
106115
|
+
}
|
|
106116
|
+
}
|
|
106117
|
+
const srcDir = join6(TEMPLATES_DIR, template);
|
|
106118
|
+
const files = [];
|
|
106119
|
+
function copyDir(src, dest) {
|
|
106120
|
+
mkdirSync4(dest, { recursive: true });
|
|
106121
|
+
for (const entry of readdirSync(src)) {
|
|
106122
|
+
const srcPath = join6(src, entry);
|
|
106123
|
+
const destPath = join6(dest, entry);
|
|
106124
|
+
const rel = relative(absTarget, normalize(destPath));
|
|
106125
|
+
if (rel.startsWith("..") || resolve3(destPath) === resolve3(absTarget, "..", basename(absTarget))) {
|
|
106126
|
+
throw new Error(`Template contains path traversal: ${entry}`);
|
|
106127
|
+
}
|
|
106128
|
+
if (statSync(srcPath).isDirectory()) {
|
|
106129
|
+
copyDir(srcPath, destPath);
|
|
106130
|
+
} else {
|
|
106131
|
+
if (existsSync4(destPath) && !force) {
|
|
106132
|
+
continue;
|
|
106133
|
+
}
|
|
106134
|
+
let content = readFileSync5(srcPath, "utf-8");
|
|
106135
|
+
content = content.replaceAll(PLACEHOLDER, appName);
|
|
106136
|
+
content = content.replace(/\r\n/g, `
|
|
106137
|
+
`);
|
|
106138
|
+
writeFileSync4(destPath, content, "utf-8");
|
|
106139
|
+
files.push(relative(absTarget, destPath).replace(/\\/g, "/"));
|
|
106140
|
+
}
|
|
106141
|
+
}
|
|
106142
|
+
}
|
|
106143
|
+
copyDir(srcDir, absTarget);
|
|
106144
|
+
return { files };
|
|
106145
|
+
}
|
|
106146
|
+
|
|
106147
|
+
// ../core/dist/commands/create.js
|
|
106148
|
+
var createInputSchema = exports_external.object({
|
|
106149
|
+
name: exports_external.string().regex(APP_NAME_RE, "must be 3-40 chars, lowercase, start with a letter, [a-z0-9-] only"),
|
|
106150
|
+
template: exports_external.string().min(1, "Template is required"),
|
|
106151
|
+
directory: exports_external.string().optional(),
|
|
106152
|
+
force: exports_external.boolean().optional()
|
|
106153
|
+
});
|
|
106154
|
+
async function create(ctx, input) {
|
|
106155
|
+
const available = listTemplates();
|
|
106156
|
+
if (!available.includes(input.template)) {
|
|
106157
|
+
throw new PercherCoreError(`Unknown template "${input.template}". Available: ${available.join(", ")}`, {
|
|
106158
|
+
code: "create.invalid-template",
|
|
106159
|
+
hint: `Run "percher create --list-templates" to see available templates.`
|
|
106160
|
+
});
|
|
106161
|
+
}
|
|
106162
|
+
let targetDir;
|
|
106163
|
+
if (input.directory) {
|
|
106164
|
+
targetDir = resolve4(ctx.cwd, input.directory);
|
|
106165
|
+
if (!targetDir.startsWith(resolve4(ctx.cwd))) {
|
|
106166
|
+
throw new PercherCoreError("Directory must be within the current workspace", {
|
|
106167
|
+
code: "create.failed"
|
|
106168
|
+
});
|
|
106169
|
+
}
|
|
106170
|
+
} else {
|
|
106171
|
+
targetDir = join7(ctx.cwd, input.name);
|
|
106172
|
+
}
|
|
106173
|
+
try {
|
|
106174
|
+
const result = scaffoldTemplate({
|
|
106175
|
+
template: input.template,
|
|
106176
|
+
appName: input.name,
|
|
106177
|
+
targetDir,
|
|
106178
|
+
force: input.force
|
|
106179
|
+
});
|
|
106180
|
+
return {
|
|
106181
|
+
...result,
|
|
106182
|
+
name: input.name,
|
|
106183
|
+
template: input.template,
|
|
106184
|
+
directory: targetDir
|
|
106185
|
+
};
|
|
106186
|
+
} catch (err) {
|
|
106187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
106188
|
+
if (msg.includes("not empty")) {
|
|
106189
|
+
throw new PercherCoreError(msg, {
|
|
106190
|
+
code: "create.dir-not-empty",
|
|
106191
|
+
hint: "Use --force to overwrite existing files."
|
|
106192
|
+
});
|
|
106193
|
+
}
|
|
106194
|
+
throw new PercherCoreError(msg, { code: "create.failed" });
|
|
106195
|
+
}
|
|
106196
|
+
}
|
|
106197
|
+
// ../core/dist/commands/dashboard.js
|
|
106198
|
+
var dashboardInputSchema = exports_external.object({
|
|
106199
|
+
app: exports_external.string().optional(),
|
|
106200
|
+
tab: exports_external.string().optional()
|
|
106201
|
+
});
|
|
106202
|
+
var DEFAULT_BASE = "https://percher.app";
|
|
106203
|
+
var TAB_SHAPE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
106204
|
+
var SETTINGS_ANCHOR_TABS = new Set(["github", "env", "domains", "crash-diagnosis"]);
|
|
106205
|
+
async function dashboard(ctx, input = {}) {
|
|
106206
|
+
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
106207
|
+
if (!name) {
|
|
106208
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
106209
|
+
code: "dashboard.no-app",
|
|
106210
|
+
hint: "Pass an app name or run from a directory with percher.toml."
|
|
106211
|
+
});
|
|
106212
|
+
}
|
|
106213
|
+
const tab = input.tab ?? "overview";
|
|
106214
|
+
if (!TAB_SHAPE.test(tab)) {
|
|
106215
|
+
throw new PercherCoreError(`Invalid tab "${tab}"`, {
|
|
106216
|
+
code: "dashboard.invalid-tab",
|
|
106217
|
+
hint: "Tab must be lowercase letters, digits, and hyphens (≤32 chars)."
|
|
106218
|
+
});
|
|
106219
|
+
}
|
|
106220
|
+
const base = ctx.env.PERCHER_DASHBOARD_URL ?? DEFAULT_BASE;
|
|
106221
|
+
const appBase = `${base}/apps/${encodeURIComponent(name)}`;
|
|
106222
|
+
const url2 = SETTINGS_ANCHOR_TABS.has(tab) ? `${appBase}/settings#${tab}` : `${appBase}?tab=${tab}`;
|
|
106223
|
+
await ctx.openUrl(url2);
|
|
106224
|
+
return url2;
|
|
106225
|
+
}
|
|
106226
|
+
// ../core/dist/commands/data.js
|
|
106227
|
+
var dataInputSchema = exports_external.object({
|
|
106228
|
+
app: exports_external.string().optional(),
|
|
106229
|
+
json: exports_external.boolean().optional()
|
|
106230
|
+
});
|
|
106231
|
+
async function data(ctx, input = {}) {
|
|
106232
|
+
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
106233
|
+
if (!name) {
|
|
106234
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
106235
|
+
code: "data.no-app",
|
|
106236
|
+
hint: "Pass an app name or run from a directory with percher.toml."
|
|
106237
|
+
});
|
|
106238
|
+
}
|
|
106239
|
+
const status = await ctx.client.data.getStatus(name);
|
|
106240
|
+
const adminLink = await ctx.client.data.getAdminLink(name);
|
|
106241
|
+
let collections = [];
|
|
106242
|
+
let stats = { collections: 0, totalRecords: 0 };
|
|
106243
|
+
let warning = null;
|
|
106244
|
+
try {
|
|
106245
|
+
const cols = await ctx.client.data.getCollections(name);
|
|
106246
|
+
collections = cols.map((c) => ({ name: c.name, type: c.type }));
|
|
106247
|
+
stats = await ctx.client.data.getStats(name);
|
|
106248
|
+
} catch (err) {
|
|
106249
|
+
const code = err.code;
|
|
106250
|
+
if (code === "PB_AUTH_FAILED") {
|
|
106251
|
+
warning = `Could not authenticate with PocketBase — collections + stats unavailable. Try \`percher data reset-superuser ${name}\` to rotate the credentials.`;
|
|
106252
|
+
} else {
|
|
106253
|
+
throw err;
|
|
106254
|
+
}
|
|
106255
|
+
}
|
|
106256
|
+
return {
|
|
106257
|
+
mode: status.mode,
|
|
106258
|
+
status: status.status,
|
|
106259
|
+
version: status.version ?? null,
|
|
106260
|
+
collections,
|
|
106261
|
+
stats,
|
|
106262
|
+
adminUrl: adminLink.adminUrl ?? null,
|
|
106263
|
+
warning
|
|
106264
|
+
};
|
|
106265
|
+
}
|
|
106266
|
+
// ../core/dist/commands/data-export.js
|
|
106267
|
+
import { resolve as resolve5 } from "node:path";
|
|
106268
|
+
var dataExportInputSchema = exports_external.object({
|
|
106269
|
+
app: exports_external.string().optional().describe("App name or id. Defaults to the app in percher.toml."),
|
|
106270
|
+
out: exports_external.string().optional().describe("Output file path. Defaults to ./<app>-pb-<timestamp>.tar.gz in the current directory.")
|
|
106271
|
+
});
|
|
106272
|
+
async function dataExport(ctx, input) {
|
|
106273
|
+
const appName = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
106274
|
+
if (!appName) {
|
|
106275
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
106276
|
+
code: "data-export.no-app",
|
|
106277
|
+
hint: "Pass an app name with --app <name> or run from a directory with percher.toml."
|
|
106278
|
+
});
|
|
106279
|
+
}
|
|
106280
|
+
ctx.status?.(`Exporting ${appName} data...`);
|
|
106281
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
106282
|
+
const fileName = input.out ?? `${appName}-pb-${stamp}.tar.gz`;
|
|
106283
|
+
const path = resolve5(ctx.cwd, fileName);
|
|
106284
|
+
const res = await ctx.client.apps.streamDataExport(appName);
|
|
106285
|
+
const bytes = await writeResponseToFile(res, path);
|
|
106286
|
+
return {
|
|
106287
|
+
app: appName,
|
|
106288
|
+
path,
|
|
106289
|
+
bytes,
|
|
106290
|
+
exportedAt: new Date().toISOString()
|
|
106291
|
+
};
|
|
106292
|
+
}
|
|
106293
|
+
// ../core/dist/commands/delete.js
|
|
106294
|
+
var deleteInputSchema = exports_external.object({
|
|
106295
|
+
app: exports_external.string().optional().describe("App name to delete (defaults to percher.toml app name)"),
|
|
106296
|
+
confirm: exports_external.boolean().describe("Must be true to confirm deletion. This is a destructive operation that permanently removes the app, its container, database, domains, env vars, and deploy history.")
|
|
106297
|
+
});
|
|
106298
|
+
async function deleteApp(ctx, input) {
|
|
106299
|
+
const name = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
106300
|
+
if (!name) {
|
|
106301
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
106302
|
+
code: "env.no-app",
|
|
106303
|
+
hint: "Pass an app name or run from a directory with percher.toml."
|
|
106304
|
+
});
|
|
106305
|
+
}
|
|
106306
|
+
if (!input.confirm) {
|
|
106307
|
+
throw new PercherCoreError(`Deletion requires explicit confirmation. Set confirm: true to delete "${name}" permanently.`, {
|
|
106308
|
+
code: "delete.confirmation-required",
|
|
106309
|
+
hint: `This will permanently delete the app "${name}" and all its data. Pass confirm: true to proceed.`
|
|
106108
106310
|
});
|
|
106109
|
-
this.apps = new AppsResource(http);
|
|
106110
|
-
this.data = new DataResource(http);
|
|
106111
|
-
this.env = new EnvResource(http);
|
|
106112
|
-
this.envScan = new EnvScanResource(http);
|
|
106113
|
-
this.export = new ExportResource(http);
|
|
106114
|
-
this.logs = new LogsResource(http);
|
|
106115
|
-
this.auth = new AuthResource(http);
|
|
106116
|
-
this.billing = new BillingResource(http);
|
|
106117
|
-
this.account = new AccountResource(http);
|
|
106118
|
-
this.alerts = new AlertsResource(http);
|
|
106119
|
-
this.platform = new PlatformResource(http);
|
|
106120
|
-
this.security = new SecurityResource(http);
|
|
106121
|
-
this.search = new SearchResource(http);
|
|
106122
|
-
this.sharing = new SharingResource(http);
|
|
106123
|
-
this.invites = new InvitesResource(http);
|
|
106124
|
-
this.transfers = new TransfersResource(http);
|
|
106125
106311
|
}
|
|
106312
|
+
await ctx.client.apps.delete(name);
|
|
106126
106313
|
}
|
|
106127
106314
|
// ../core/dist/commands/deploys.js
|
|
106128
106315
|
var deploysListInputSchema = exports_external.object({
|
|
@@ -106192,17 +106379,6 @@ async function deploysInspect(ctx, input) {
|
|
|
106192
106379
|
import { existsSync as existsSync7 } from "node:fs";
|
|
106193
106380
|
import { join as join10 } from "node:path";
|
|
106194
106381
|
|
|
106195
|
-
// ../shared/dist/timeouts.js
|
|
106196
|
-
var TIMEOUTS = {
|
|
106197
|
-
workerBuildSubprocess: 10 * 60000,
|
|
106198
|
-
apiToWorkerBuildFetch: 11 * 60000,
|
|
106199
|
-
clientPublishPoll: 12 * 60000,
|
|
106200
|
-
workerHealthcheck: 5000,
|
|
106201
|
-
watchdogList: 15000,
|
|
106202
|
-
mcpWaitDeployDefault: 30000,
|
|
106203
|
-
mcpWaitDeployMax: 12 * 60000
|
|
106204
|
-
};
|
|
106205
|
-
|
|
106206
106382
|
// ../core/dist/tarball.js
|
|
106207
106383
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
106208
106384
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -107159,15 +107335,17 @@ function trimMessage(raw) {
|
|
|
107159
107335
|
function tomlPathFor(cwd) {
|
|
107160
107336
|
return join11(cwd, "percher.toml");
|
|
107161
107337
|
}
|
|
107162
|
-
async function buildFailureError(ctx, app, deployment) {
|
|
107338
|
+
async function buildFailureError(ctx, app, deployment, signal) {
|
|
107163
107339
|
let buildLogTail;
|
|
107164
107340
|
let buildLogFetchFailed = false;
|
|
107165
107341
|
let fullLog = "";
|
|
107166
107342
|
try {
|
|
107167
|
-
fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id);
|
|
107168
|
-
if (!fullLog) {
|
|
107169
|
-
await
|
|
107170
|
-
|
|
107343
|
+
fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
|
|
107344
|
+
if (!fullLog && !signal?.aborted) {
|
|
107345
|
+
await abortableSleep(1500, signal);
|
|
107346
|
+
if (!signal?.aborted) {
|
|
107347
|
+
fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
|
|
107348
|
+
}
|
|
107171
107349
|
}
|
|
107172
107350
|
if (fullLog) {
|
|
107173
107351
|
const lines = fullLog.split(`
|
|
@@ -107213,7 +107391,7 @@ async function buildFailureError(ctx, app, deployment) {
|
|
|
107213
107391
|
}
|
|
107214
107392
|
async function classifyDeploymentFailure(opts) {
|
|
107215
107393
|
const { ctx, app, deployment, publishInput } = opts;
|
|
107216
|
-
const error48 = await buildFailureError(ctx, app, deployment);
|
|
107394
|
+
const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
|
|
107217
107395
|
const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
|
|
107218
107396
|
missingEnvVars: error48.missingEnvVars,
|
|
107219
107397
|
deployId: deployment.id,
|
|
@@ -107259,6 +107437,100 @@ var waitForDeployInputSchema = exports_external.object({
|
|
|
107259
107437
|
timeoutSeconds: exports_external.number().int().min(1).max(TIMEOUTS.mcpWaitDeployMax / 1000).optional().describe(`Maximum wall-clock seconds to wait before returning still_running (default ${TIMEOUTS.mcpWaitDeployDefault / 1000}, max ${TIMEOUTS.mcpWaitDeployMax / 1000}). Keeping this short is the point — the agent can resume in another short call without holding a long-lived connection.`),
|
|
107260
107438
|
pollIntervalSeconds: exports_external.number().int().min(1).max(30).optional().describe("Seconds between polls (default 2).")
|
|
107261
107439
|
});
|
|
107440
|
+
async function waitForDeploy(ctx, input, opts) {
|
|
107441
|
+
const timeoutSeconds = input.timeoutSeconds ?? TIMEOUTS.mcpWaitDeployDefault / 1000;
|
|
107442
|
+
const pollIntervalSeconds = input.pollIntervalSeconds ?? 2;
|
|
107443
|
+
const startedAt = Date.now();
|
|
107444
|
+
const deadlineMs = startedAt + timeoutSeconds * 1000;
|
|
107445
|
+
const controller = new AbortController;
|
|
107446
|
+
const abortTimer = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
107447
|
+
const onExternalAbort = () => controller.abort();
|
|
107448
|
+
opts?.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
107449
|
+
if (opts?.signal?.aborted)
|
|
107450
|
+
controller.abort();
|
|
107451
|
+
try {
|
|
107452
|
+
let app;
|
|
107453
|
+
let initial;
|
|
107454
|
+
try {
|
|
107455
|
+
[app, initial] = await Promise.all([
|
|
107456
|
+
ctx.client.apps.get(input.app, { signal: controller.signal }),
|
|
107457
|
+
ctx.client.apps.getDeployment(input.app, input.deployId, { signal: controller.signal })
|
|
107458
|
+
]);
|
|
107459
|
+
} catch (err) {
|
|
107460
|
+
if (controller.signal.aborted) {
|
|
107461
|
+
throw new PercherCoreError(`wait-for-deploy timed out after ${timeoutSeconds}s before the first status fetch completed`, {
|
|
107462
|
+
code: "wait.timeout",
|
|
107463
|
+
hint: "The API is slow or unreachable. Retry with a larger timeout, or run percher doctor."
|
|
107464
|
+
});
|
|
107465
|
+
}
|
|
107466
|
+
throw err;
|
|
107467
|
+
}
|
|
107468
|
+
let deployment = initial;
|
|
107469
|
+
while (!isTerminal(deployment.status)) {
|
|
107470
|
+
if (controller.signal.aborted || Date.now() >= deadlineMs) {
|
|
107471
|
+
return buildStillRunningResult(app, deployment, startedAt);
|
|
107472
|
+
}
|
|
107473
|
+
const sleepMs = Math.min(pollIntervalSeconds * 1000, deadlineMs - Date.now());
|
|
107474
|
+
await abortableSleep(sleepMs, controller.signal);
|
|
107475
|
+
if (controller.signal.aborted || Date.now() >= deadlineMs) {
|
|
107476
|
+
return buildStillRunningResult(app, deployment, startedAt);
|
|
107477
|
+
}
|
|
107478
|
+
try {
|
|
107479
|
+
deployment = await ctx.client.apps.getDeployment(input.app, input.deployId, {
|
|
107480
|
+
signal: controller.signal
|
|
107481
|
+
});
|
|
107482
|
+
} catch (err) {
|
|
107483
|
+
if (controller.signal.aborted) {
|
|
107484
|
+
return buildStillRunningResult(app, deployment, startedAt, "status fetch exceeded the wait budget");
|
|
107485
|
+
}
|
|
107486
|
+
if (isTransientNetworkError(err)) {
|
|
107487
|
+
const msg = err.message ?? String(err);
|
|
107488
|
+
return buildStillRunningResult(app, deployment, startedAt, msg);
|
|
107489
|
+
}
|
|
107490
|
+
throw err;
|
|
107491
|
+
}
|
|
107492
|
+
}
|
|
107493
|
+
if (deployment.status === "failed") {
|
|
107494
|
+
const { error: error48, recovery, summary } = await classifyDeploymentFailure({
|
|
107495
|
+
ctx,
|
|
107496
|
+
app,
|
|
107497
|
+
deployment,
|
|
107498
|
+
signal: opts?.signal
|
|
107499
|
+
});
|
|
107500
|
+
return {
|
|
107501
|
+
status: "failed",
|
|
107502
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107503
|
+
deployment,
|
|
107504
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107505
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107506
|
+
error: error48,
|
|
107507
|
+
recovery,
|
|
107508
|
+
summary
|
|
107509
|
+
};
|
|
107510
|
+
}
|
|
107511
|
+
if (deployment.status === "replaced") {
|
|
107512
|
+
return await buildReplacedResult({ ctx, app, deployment, startedAt, signal: opts?.signal });
|
|
107513
|
+
}
|
|
107514
|
+
const url2 = deployment.previewUrl ?? deployment.url ?? app.url;
|
|
107515
|
+
return {
|
|
107516
|
+
status: "live",
|
|
107517
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107518
|
+
deployment,
|
|
107519
|
+
url: url2,
|
|
107520
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107521
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107522
|
+
recovery: RECOVERY_NONE,
|
|
107523
|
+
summary: `${app.name} deploy ${deployment.id} is live → ${url2}`
|
|
107524
|
+
};
|
|
107525
|
+
} finally {
|
|
107526
|
+
clearTimeout(abortTimer);
|
|
107527
|
+
controller.abort();
|
|
107528
|
+
opts?.signal?.removeEventListener("abort", onExternalAbort);
|
|
107529
|
+
}
|
|
107530
|
+
}
|
|
107531
|
+
function isTerminal(status) {
|
|
107532
|
+
return status === "live" || status === "failed" || status === "replaced";
|
|
107533
|
+
}
|
|
107262
107534
|
async function resolveReplaced(opts) {
|
|
107263
107535
|
const { ctx, app, replacedDeployment } = opts;
|
|
107264
107536
|
const deployType = replacedDeployment.type ?? "live";
|
|
@@ -107266,7 +107538,8 @@ async function resolveReplaced(opts) {
|
|
|
107266
107538
|
try {
|
|
107267
107539
|
const deploys = await ctx.client.apps.listDeploys(app.name, {
|
|
107268
107540
|
limit: 5,
|
|
107269
|
-
type: deployType
|
|
107541
|
+
type: deployType,
|
|
107542
|
+
signal: opts.signal
|
|
107270
107543
|
});
|
|
107271
107544
|
latest = deploys.find((d) => d.id !== replacedDeployment.id);
|
|
107272
107545
|
} catch {
|
|
@@ -107313,6 +107586,46 @@ async function resolveReplaced(opts) {
|
|
|
107313
107586
|
summary: `${app.name} deploy ${replacedDeployment.id} was replaced; the latest deploy ${latest.id} is ${latest.status}. Surface to user.`
|
|
107314
107587
|
};
|
|
107315
107588
|
}
|
|
107589
|
+
async function buildReplacedResult(opts) {
|
|
107590
|
+
const { ctx, app, deployment, startedAt } = opts;
|
|
107591
|
+
const resolved = await resolveReplaced({
|
|
107592
|
+
ctx,
|
|
107593
|
+
app,
|
|
107594
|
+
replacedDeployment: deployment,
|
|
107595
|
+
signal: opts.signal
|
|
107596
|
+
});
|
|
107597
|
+
return {
|
|
107598
|
+
status: "replaced",
|
|
107599
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107600
|
+
deployment,
|
|
107601
|
+
url: resolved.url,
|
|
107602
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107603
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107604
|
+
recovery: resolved.recovery,
|
|
107605
|
+
summary: resolved.summary
|
|
107606
|
+
};
|
|
107607
|
+
}
|
|
107608
|
+
function buildStillRunningResult(app, deployment, startedAt, networkHint) {
|
|
107609
|
+
return {
|
|
107610
|
+
status: "still_running",
|
|
107611
|
+
app: { id: app.id, name: app.name, url: app.url },
|
|
107612
|
+
deployment,
|
|
107613
|
+
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
107614
|
+
totalElapsedSeconds: secondsSinceCreated(deployment),
|
|
107615
|
+
recovery: recoveryWait({
|
|
107616
|
+
app: app.name,
|
|
107617
|
+
deployId: deployment.id,
|
|
107618
|
+
reasonCode: deployment.status === "queued" ? "deploy_queued" : deployment.status === "building" ? "deploy_building" : deployment.status === "deploying" ? "deploy_deploying" : "deploy_timeout"
|
|
107619
|
+
}),
|
|
107620
|
+
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.`
|
|
107621
|
+
};
|
|
107622
|
+
}
|
|
107623
|
+
function secondsSinceCreated(deployment) {
|
|
107624
|
+
const t = Date.parse(deployment.createdAt);
|
|
107625
|
+
if (Number.isNaN(t))
|
|
107626
|
+
return 0;
|
|
107627
|
+
return Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
107628
|
+
}
|
|
107316
107629
|
|
|
107317
107630
|
// ../core/dist/commands/doctor.js
|
|
107318
107631
|
var doctorInputSchema = exports_external.object({
|
|
@@ -107440,17 +107753,19 @@ async function doctor(ctx, input = {}) {
|
|
|
107440
107753
|
message: diag.container.running ? "Running" : diag.container.state === "not-found" ? "No container found" : `State: ${diag.container.state}`
|
|
107441
107754
|
});
|
|
107442
107755
|
if (diag.containerHealth) {
|
|
107756
|
+
const latency = diag.containerHealth.latencyMs != null ? ` (${diag.containerHealth.latencyMs}ms)` : "";
|
|
107443
107757
|
checks3.push({
|
|
107444
107758
|
name: "Container health",
|
|
107445
107759
|
status: diag.containerHealth.healthy ? "pass" : "fail",
|
|
107446
|
-
message: diag.containerHealth.healthy ? `${diag.containerHealth.path} OK
|
|
107760
|
+
message: diag.containerHealth.healthy ? `${diag.containerHealth.path} OK${latency}` : `${diag.containerHealth.path} — not responding`
|
|
107447
107761
|
});
|
|
107448
107762
|
}
|
|
107449
107763
|
if (diag.publicRoute) {
|
|
107764
|
+
const latency = diag.publicRoute.latencyMs != null ? ` (${diag.publicRoute.latencyMs}ms)` : "";
|
|
107450
107765
|
checks3.push({
|
|
107451
107766
|
name: "Public route",
|
|
107452
107767
|
status: diag.publicRoute.healthy ? "pass" : "fail",
|
|
107453
|
-
message: diag.publicRoute.healthy ? `${diag.publicRoute.url}
|
|
107768
|
+
message: diag.publicRoute.healthy ? `${diag.publicRoute.url}${latency}` : `${diag.publicRoute.url} — not responding`
|
|
107454
107769
|
});
|
|
107455
107770
|
}
|
|
107456
107771
|
if (diag.pocketbase) {
|
|
@@ -107636,11 +107951,53 @@ function summarize(checks3, vctx = {}) {
|
|
|
107636
107951
|
summary: verdict.summary
|
|
107637
107952
|
};
|
|
107638
107953
|
}
|
|
107639
|
-
function
|
|
107954
|
+
function buildVerdictRuleContext(checks3, vctx) {
|
|
107640
107955
|
const findCheck = (name) => checks3.find((c) => c.name === name);
|
|
107641
|
-
const
|
|
107642
|
-
|
|
107956
|
+
const toml = findCheck("percher.toml");
|
|
107957
|
+
const verdictChecks = vctx.appProvided && toml?.status === "fail" ? checks3.filter((c) => c.name !== "percher.toml") : checks3;
|
|
107958
|
+
const containerDown = vctx.containerRunning === false && vctx.containerState !== undefined && vctx.containerState !== "not-found" || vctx.containerHealthy === false;
|
|
107959
|
+
return { checks: checks3, vctx, findCheck, verdictChecks, containerDown };
|
|
107960
|
+
}
|
|
107961
|
+
function runtimeCrashExplanation({ vctx, findCheck }) {
|
|
107962
|
+
const containerCheck = findCheck("Container");
|
|
107963
|
+
const healthCheck = findCheck("Container health");
|
|
107964
|
+
const lastCrashCheck = findCheck("Last crash");
|
|
107965
|
+
const explanationBits = [];
|
|
107966
|
+
if (vctx.appStatus === "crashed")
|
|
107967
|
+
explanationBits.push("App is in crashed state.");
|
|
107968
|
+
if (containerCheck?.status === "fail")
|
|
107969
|
+
explanationBits.push(containerCheck.message);
|
|
107970
|
+
if (healthCheck?.status === "fail")
|
|
107971
|
+
explanationBits.push(healthCheck.message);
|
|
107972
|
+
if (lastCrashCheck)
|
|
107973
|
+
explanationBits.push(lastCrashCheck.message);
|
|
107974
|
+
return explanationBits.length > 0 ? explanationBits.join(" ") : "Runtime is not responding — container or health check is failing.";
|
|
107975
|
+
}
|
|
107976
|
+
var FALLBACK_RULE = {
|
|
107977
|
+
id: "unknown-fallback",
|
|
107978
|
+
when: () => true,
|
|
107979
|
+
verdict: ({ verdictChecks }) => {
|
|
107980
|
+
const firstFailed = verdictChecks.find((c) => c.status === "fail");
|
|
107643
107981
|
return {
|
|
107982
|
+
status: "needs_action",
|
|
107983
|
+
diagnosis: firstFailed ? {
|
|
107984
|
+
title: firstFailed.name,
|
|
107985
|
+
explanation: firstFailed.message,
|
|
107986
|
+
reasonCode: "unknown"
|
|
107987
|
+
} : undefined,
|
|
107988
|
+
recovery: recoveryAsk({
|
|
107989
|
+
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.",
|
|
107990
|
+
reasonCode: "unknown"
|
|
107991
|
+
}),
|
|
107992
|
+
summary: firstFailed ? `${firstFailed.name} failed: ${firstFailed.message}` : "Doctor reported issues — review the checks list."
|
|
107993
|
+
};
|
|
107994
|
+
}
|
|
107995
|
+
};
|
|
107996
|
+
var VERDICT_RULES = [
|
|
107997
|
+
{
|
|
107998
|
+
id: "auth-missing",
|
|
107999
|
+
when: ({ findCheck }) => findCheck("Auth token")?.status === "fail",
|
|
108000
|
+
verdict: () => ({
|
|
107644
108001
|
status: "blocked",
|
|
107645
108002
|
diagnosis: {
|
|
107646
108003
|
title: "Authentication required",
|
|
@@ -107650,150 +108007,188 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107650
108007
|
},
|
|
107651
108008
|
recovery: recoveryLogin({ reasonCode: "auth_required" }),
|
|
107652
108009
|
summary: "Login required: run `percher login` (or set PERCHER_TOKEN)."
|
|
107653
|
-
}
|
|
107654
|
-
}
|
|
107655
|
-
|
|
107656
|
-
|
|
107657
|
-
|
|
107658
|
-
|
|
107659
|
-
|
|
107660
|
-
|
|
107661
|
-
|
|
107662
|
-
|
|
107663
|
-
|
|
107664
|
-
|
|
107665
|
-
|
|
107666
|
-
|
|
107667
|
-
|
|
107668
|
-
|
|
107669
|
-
|
|
107670
|
-
|
|
107671
|
-
|
|
107672
|
-
|
|
107673
|
-
|
|
107674
|
-
|
|
107675
|
-
|
|
107676
|
-
|
|
107677
|
-
|
|
107678
|
-
|
|
107679
|
-
|
|
107680
|
-
|
|
107681
|
-
|
|
107682
|
-
|
|
107683
|
-
|
|
107684
|
-
|
|
107685
|
-
|
|
107686
|
-
|
|
107687
|
-
|
|
107688
|
-
|
|
107689
|
-
|
|
107690
|
-
|
|
107691
|
-
|
|
107692
|
-
|
|
107693
|
-
|
|
107694
|
-
|
|
107695
|
-
|
|
107696
|
-
|
|
107697
|
-
|
|
107698
|
-
|
|
107699
|
-
|
|
107700
|
-
|
|
107701
|
-
|
|
107702
|
-
|
|
107703
|
-
|
|
107704
|
-
|
|
107705
|
-
|
|
107706
|
-
|
|
107707
|
-
|
|
107708
|
-
|
|
107709
|
-
|
|
107710
|
-
|
|
107711
|
-
|
|
107712
|
-
|
|
107713
|
-
|
|
107714
|
-
|
|
107715
|
-
|
|
107716
|
-
|
|
107717
|
-
|
|
107718
|
-
|
|
107719
|
-
|
|
107720
|
-
|
|
107721
|
-
|
|
107722
|
-
|
|
107723
|
-
|
|
107724
|
-
|
|
107725
|
-
|
|
107726
|
-
|
|
107727
|
-
|
|
107728
|
-
|
|
107729
|
-
|
|
107730
|
-
|
|
107731
|
-
|
|
107732
|
-
|
|
107733
|
-
|
|
107734
|
-
|
|
107735
|
-
|
|
107736
|
-
|
|
107737
|
-
|
|
107738
|
-
|
|
107739
|
-
|
|
107740
|
-
|
|
107741
|
-
|
|
108010
|
+
})
|
|
108011
|
+
},
|
|
108012
|
+
{
|
|
108013
|
+
id: "api-unreachable",
|
|
108014
|
+
when: ({ findCheck }) => findCheck("API reachability")?.status === "fail",
|
|
108015
|
+
verdict: ({ findCheck }) => {
|
|
108016
|
+
const message = findCheck("API reachability")?.message ?? "";
|
|
108017
|
+
return {
|
|
108018
|
+
status: "blocked",
|
|
108019
|
+
diagnosis: {
|
|
108020
|
+
title: "Percher API is unreachable",
|
|
108021
|
+
explanation: message,
|
|
108022
|
+
reasonCode: "infra_unavailable",
|
|
108023
|
+
phase: "infra"
|
|
108024
|
+
},
|
|
108025
|
+
recovery: recoveryAsk({
|
|
108026
|
+
prompt: `Percher API is unreachable (${message}). This is usually a transient network issue — wait a moment and try again, or check status.percher.app.`,
|
|
108027
|
+
reasonCode: "infra_unavailable",
|
|
108028
|
+
retryable: true
|
|
108029
|
+
}),
|
|
108030
|
+
summary: "Cannot reach the Percher API — try again in a moment."
|
|
108031
|
+
};
|
|
108032
|
+
}
|
|
108033
|
+
},
|
|
108034
|
+
{
|
|
108035
|
+
id: "toml-invalid",
|
|
108036
|
+
when: ({ vctx, findCheck }) => findCheck("percher.toml")?.status === "fail" && !vctx.appProvided,
|
|
108037
|
+
verdict: ({ findCheck }) => {
|
|
108038
|
+
const message = findCheck("percher.toml")?.message ?? "";
|
|
108039
|
+
return {
|
|
108040
|
+
status: "blocked",
|
|
108041
|
+
diagnosis: {
|
|
108042
|
+
title: "Invalid percher.toml",
|
|
108043
|
+
explanation: message,
|
|
108044
|
+
reasonCode: "config_invalid",
|
|
108045
|
+
phase: "config"
|
|
108046
|
+
},
|
|
108047
|
+
recovery: recoveryFixConfig({
|
|
108048
|
+
problems: [
|
|
108049
|
+
{
|
|
108050
|
+
file: "percher.toml",
|
|
108051
|
+
message
|
|
108052
|
+
}
|
|
108053
|
+
],
|
|
108054
|
+
reasonCode: "config_invalid"
|
|
108055
|
+
}),
|
|
108056
|
+
summary: "percher.toml is invalid — fix the issues and re-run."
|
|
108057
|
+
};
|
|
108058
|
+
}
|
|
108059
|
+
},
|
|
108060
|
+
{
|
|
108061
|
+
id: "app-unresolved",
|
|
108062
|
+
when: ({ findCheck }) => findCheck("App")?.status === "skip",
|
|
108063
|
+
verdict: ({ findCheck }) => {
|
|
108064
|
+
const message = findCheck("App")?.message ?? "";
|
|
108065
|
+
return {
|
|
108066
|
+
status: "blocked",
|
|
108067
|
+
diagnosis: {
|
|
108068
|
+
title: "No app specified",
|
|
108069
|
+
explanation: message,
|
|
108070
|
+
reasonCode: "config_missing",
|
|
108071
|
+
phase: "config"
|
|
108072
|
+
},
|
|
108073
|
+
recovery: recoveryAsk({
|
|
108074
|
+
prompt: `${message} — pass \`--app <name>\` or run \`percher init\` in a project directory to generate a percher.toml.`,
|
|
108075
|
+
reasonCode: "config_missing"
|
|
108076
|
+
}),
|
|
108077
|
+
summary: "No percher.toml found and no --app supplied — pass --app or run percher init."
|
|
108078
|
+
};
|
|
108079
|
+
}
|
|
108080
|
+
},
|
|
108081
|
+
{
|
|
108082
|
+
id: "deploy-transitional",
|
|
108083
|
+
when: ({ vctx }) => !!vctx.lastDeployStatus && TRANSITIONAL_DEPLOY_STATUSES.has(vctx.lastDeployStatus) || !!vctx.appStatus && TRANSITIONAL_APP_STATUSES.has(vctx.appStatus),
|
|
108084
|
+
verdict: ({ vctx }) => {
|
|
108085
|
+
const inTransitionalApp = !!vctx.appStatus && TRANSITIONAL_APP_STATUSES.has(vctx.appStatus);
|
|
108086
|
+
const reasonCode = vctx.lastDeployStatus === "queued" ? "deploy_queued" : vctx.lastDeployStatus === "building" ? "deploy_building" : vctx.lastDeployStatus === "deploying" ? "deploy_deploying" : "deploy_queued";
|
|
108087
|
+
const what = inTransitionalApp ? `App ${vctx.appName ?? ""} is provisioning` : `Last deploy is ${vctx.lastDeployStatus}`;
|
|
108088
|
+
const recovery = vctx.lastDeployId && vctx.appName ? recoveryWait({
|
|
108089
|
+
app: vctx.appName,
|
|
108090
|
+
deployId: vctx.lastDeployId,
|
|
108091
|
+
reasonCode
|
|
108092
|
+
}) : recoveryAsk({
|
|
108093
|
+
prompt: `${what.trim()} for ${vctx.appName ?? "this app"}. Wait for it to finish before retrying — there's no deployId to track yet.`,
|
|
108094
|
+
reasonCode
|
|
108095
|
+
});
|
|
108096
|
+
return {
|
|
108097
|
+
status: "in_progress",
|
|
108098
|
+
diagnosis: {
|
|
108099
|
+
title: what.trim(),
|
|
108100
|
+
explanation: vctx.lastDeployId ? `${what.trim()} (deploy ${vctx.lastDeployId}). Wait for it to finish before retrying.` : `${what.trim()}. Wait for it to finish before retrying.`,
|
|
108101
|
+
reasonCode,
|
|
108102
|
+
phase: "deploy"
|
|
108103
|
+
},
|
|
108104
|
+
recovery,
|
|
108105
|
+
summary: vctx.lastDeployId ? `${what.trim()} — wait for deploy ${vctx.lastDeployId}.` : `${what.trim()} — wait a moment and re-run.`
|
|
108106
|
+
};
|
|
108107
|
+
}
|
|
108108
|
+
},
|
|
108109
|
+
{
|
|
108110
|
+
id: "deploy-replaced",
|
|
108111
|
+
when: ({ vctx }) => !!vctx.replacedResolution,
|
|
108112
|
+
verdict: (ctx) => {
|
|
108113
|
+
const r = ctx.vctx.replacedResolution;
|
|
108114
|
+
if (!r)
|
|
108115
|
+
return FALLBACK_RULE.verdict(ctx);
|
|
108116
|
+
const isResolvedLive = r.recovery.nextAction === "none" && !!r.url;
|
|
108117
|
+
if (isResolvedLive) {
|
|
108118
|
+
return {
|
|
108119
|
+
status: "ok",
|
|
108120
|
+
recovery: { ...r.recovery, reasonCode: "replaced_by_newer" },
|
|
108121
|
+
summary: r.summary
|
|
108122
|
+
};
|
|
108123
|
+
}
|
|
108124
|
+
const status = r.recovery.nextAction === "wait_deploy" ? "in_progress" : "needs_action";
|
|
107742
108125
|
return {
|
|
107743
|
-
status
|
|
108126
|
+
status,
|
|
108127
|
+
diagnosis: {
|
|
108128
|
+
title: "Last deploy was replaced",
|
|
108129
|
+
explanation: r.summary,
|
|
108130
|
+
reasonCode: "replaced_by_newer",
|
|
108131
|
+
phase: "deploy"
|
|
108132
|
+
},
|
|
107744
108133
|
recovery: { ...r.recovery, reasonCode: "replaced_by_newer" },
|
|
107745
108134
|
summary: r.summary
|
|
107746
108135
|
};
|
|
107747
108136
|
}
|
|
107748
|
-
|
|
107749
|
-
|
|
107750
|
-
|
|
107751
|
-
|
|
107752
|
-
|
|
107753
|
-
|
|
107754
|
-
|
|
107755
|
-
|
|
107756
|
-
}
|
|
107757
|
-
|
|
107758
|
-
|
|
107759
|
-
|
|
107760
|
-
|
|
107761
|
-
|
|
107762
|
-
|
|
108137
|
+
},
|
|
108138
|
+
{
|
|
108139
|
+
id: "deploy-failed-missing-env",
|
|
108140
|
+
when: ({ vctx }) => {
|
|
108141
|
+
const cls = vctx.buildLogClassification;
|
|
108142
|
+
return vctx.lastDeployStatus === "failed" && vctx.inputMode === "deploy" && !!cls && cls.errorClass === "missing_env" && cls.missingEnvVars.length > 0;
|
|
108143
|
+
},
|
|
108144
|
+
verdict: (ctx) => {
|
|
108145
|
+
const { vctx } = ctx;
|
|
108146
|
+
const cls = vctx.buildLogClassification;
|
|
108147
|
+
if (!cls)
|
|
108148
|
+
return FALLBACK_RULE.verdict(ctx);
|
|
108149
|
+
return {
|
|
108150
|
+
status: "needs_action",
|
|
108151
|
+
diagnosis: {
|
|
108152
|
+
title: cls.title,
|
|
108153
|
+
explanation: cls.explanation,
|
|
108154
|
+
reasonCode: "missing_env",
|
|
108155
|
+
phase: "build"
|
|
108156
|
+
},
|
|
108157
|
+
recovery: recoveryEnv({
|
|
108158
|
+
app: vctx.appName,
|
|
108159
|
+
keys: cls.missingEnvVars
|
|
108160
|
+
}),
|
|
108161
|
+
summary: vctx.appName ? `Build failed — missing env vars on ${vctx.appName}: ${cls.missingEnvVars.join(", ")}.` : `Build failed — missing env vars: ${cls.missingEnvVars.join(", ")}.`
|
|
108162
|
+
};
|
|
108163
|
+
}
|
|
108164
|
+
},
|
|
108165
|
+
{
|
|
108166
|
+
id: "deploy-failed-file-problems",
|
|
108167
|
+
when: ({ vctx }) => vctx.lastDeployStatus === "failed" && vctx.inputMode === "deploy" && (vctx.buildProblems ?? []).filter((p) => p.file).length > 0,
|
|
108168
|
+
verdict: ({ vctx }) => {
|
|
107763
108169
|
const cls = vctx.buildLogClassification;
|
|
107764
|
-
if (cls && cls.errorClass === "missing_env" && cls.missingEnvVars.length > 0) {
|
|
107765
|
-
return {
|
|
107766
|
-
status: "needs_action",
|
|
107767
|
-
diagnosis: {
|
|
107768
|
-
title: cls.title,
|
|
107769
|
-
explanation: cls.explanation,
|
|
107770
|
-
reasonCode: "missing_env",
|
|
107771
|
-
phase: "build"
|
|
107772
|
-
},
|
|
107773
|
-
recovery: recoveryEnv({
|
|
107774
|
-
app: vctx.appName,
|
|
107775
|
-
keys: cls.missingEnvVars
|
|
107776
|
-
}),
|
|
107777
|
-
summary: vctx.appName ? `Build failed — missing env vars on ${vctx.appName}: ${cls.missingEnvVars.join(", ")}.` : `Build failed — missing env vars: ${cls.missingEnvVars.join(", ")}.`
|
|
107778
|
-
};
|
|
107779
|
-
}
|
|
107780
108170
|
const fileProblems = (vctx.buildProblems ?? []).filter((p) => p.file).map(buildProblemToRecoveryProblem);
|
|
107781
|
-
|
|
107782
|
-
|
|
107783
|
-
|
|
107784
|
-
|
|
107785
|
-
|
|
107786
|
-
|
|
107787
|
-
|
|
107788
|
-
|
|
107789
|
-
|
|
107790
|
-
|
|
107791
|
-
|
|
107792
|
-
|
|
107793
|
-
|
|
107794
|
-
|
|
107795
|
-
|
|
107796
|
-
|
|
108171
|
+
return {
|
|
108172
|
+
status: "needs_action",
|
|
108173
|
+
diagnosis: {
|
|
108174
|
+
title: cls?.title ?? "Build failed with file-located problems",
|
|
108175
|
+
explanation: cls?.explanation ?? `Build extracted ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} with file locations. Patch the files directly.`,
|
|
108176
|
+
reasonCode: "build_failed",
|
|
108177
|
+
phase: "build"
|
|
108178
|
+
},
|
|
108179
|
+
recovery: recoveryFixProblems({
|
|
108180
|
+
problems: fileProblems,
|
|
108181
|
+
reasonCode: "build_failed"
|
|
108182
|
+
}),
|
|
108183
|
+
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.`
|
|
108184
|
+
};
|
|
108185
|
+
}
|
|
108186
|
+
},
|
|
108187
|
+
{
|
|
108188
|
+
id: "deploy-failed-inspect-log",
|
|
108189
|
+
when: ({ vctx }) => vctx.lastDeployStatus === "failed" && vctx.inputMode === "deploy",
|
|
108190
|
+
verdict: ({ vctx }) => {
|
|
108191
|
+
const cls = vctx.buildLogClassification;
|
|
107797
108192
|
return {
|
|
107798
108193
|
status: "needs_action",
|
|
107799
108194
|
diagnosis: {
|
|
@@ -107810,7 +108205,11 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107810
108205
|
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — inspect the build log.` : "Last deploy failed — inspect the latest failed deploy's build log."
|
|
107811
108206
|
};
|
|
107812
108207
|
}
|
|
107813
|
-
|
|
108208
|
+
},
|
|
108209
|
+
{
|
|
108210
|
+
id: "deploy-failed-dispatch",
|
|
108211
|
+
when: ({ vctx }) => vctx.lastDeployStatus === "failed",
|
|
108212
|
+
verdict: ({ vctx }) => ({
|
|
107814
108213
|
status: "needs_action",
|
|
107815
108214
|
diagnosis: {
|
|
107816
108215
|
title: "Last deploy failed",
|
|
@@ -107830,10 +108229,12 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107830
108229
|
reasonCode: "build_failed"
|
|
107831
108230
|
},
|
|
107832
108231
|
summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — call percher_doctor with mode='deploy'.` : "Last deploy failed — call percher_doctor with mode='deploy'."
|
|
107833
|
-
}
|
|
107834
|
-
}
|
|
107835
|
-
|
|
107836
|
-
|
|
108232
|
+
})
|
|
108233
|
+
},
|
|
108234
|
+
{
|
|
108235
|
+
id: "app-suspended",
|
|
108236
|
+
when: ({ vctx }) => vctx.appStatus === "suspended",
|
|
108237
|
+
verdict: ({ vctx }) => ({
|
|
107837
108238
|
status: "needs_action",
|
|
107838
108239
|
diagnosis: {
|
|
107839
108240
|
title: "App is suspended",
|
|
@@ -107846,33 +108247,27 @@ function deriveVerdict(checks3, vctx) {
|
|
|
107846
108247
|
prompt: vctx.appName ? `${vctx.appName} is suspended. Resume the app from the dashboard (or call the unsuspend API) before retrying.` : "This app is suspended. Resume it from the dashboard before retrying."
|
|
107847
108248
|
}),
|
|
107848
108249
|
summary: vctx.appName ? `${vctx.appName} is suspended — resume the app before retrying.` : "App is suspended — resume it before retrying."
|
|
107849
|
-
}
|
|
107850
|
-
}
|
|
107851
|
-
|
|
107852
|
-
|
|
107853
|
-
|
|
107854
|
-
|
|
107855
|
-
status
|
|
107856
|
-
|
|
107857
|
-
|
|
107858
|
-
|
|
107859
|
-
|
|
107860
|
-
|
|
107861
|
-
|
|
107862
|
-
|
|
107863
|
-
|
|
107864
|
-
|
|
107865
|
-
|
|
107866
|
-
|
|
107867
|
-
|
|
107868
|
-
|
|
107869
|
-
|
|
107870
|
-
if (healthCheck?.status === "fail")
|
|
107871
|
-
explanationBits.push(healthCheck.message);
|
|
107872
|
-
if (lastCrashCheck)
|
|
107873
|
-
explanationBits.push(lastCrashCheck.message);
|
|
107874
|
-
const explanation = explanationBits.length > 0 ? explanationBits.join(" ") : "Runtime is not responding — container or health check is failing.";
|
|
107875
|
-
if (vctx.inputMode === "runtime") {
|
|
108250
|
+
})
|
|
108251
|
+
},
|
|
108252
|
+
{
|
|
108253
|
+
id: "all-healthy",
|
|
108254
|
+
when: ({ verdictChecks }) => verdictChecks.every((c) => c.status === "pass" || c.status === "skip" || c.status === "warn"),
|
|
108255
|
+
verdict: ({ vctx, verdictChecks }) => {
|
|
108256
|
+
const passing = verdictChecks.filter((c) => c.status === "pass").length;
|
|
108257
|
+
const noun = passing === 1 ? "check" : "checks";
|
|
108258
|
+
return {
|
|
108259
|
+
status: "ok",
|
|
108260
|
+
recovery: recoveryNone({ reasonCode: "none" }),
|
|
108261
|
+
summary: vctx.appName ? `All ${passing} ${noun} passed for ${vctx.appName}.` : `All ${passing} ${noun} passed.`
|
|
108262
|
+
};
|
|
108263
|
+
}
|
|
108264
|
+
},
|
|
108265
|
+
{
|
|
108266
|
+
id: "runtime-crashed-report",
|
|
108267
|
+
when: ({ vctx, containerDown }) => (vctx.appStatus === "crashed" || containerDown) && vctx.inputMode === "runtime",
|
|
108268
|
+
verdict: (ctx) => {
|
|
108269
|
+
const { vctx } = ctx;
|
|
108270
|
+
const explanation = runtimeCrashExplanation(ctx);
|
|
107876
108271
|
const cr = vctx.crashReport;
|
|
107877
108272
|
const hasAnalysis = cr && cr.analysisStatus === "completed";
|
|
107878
108273
|
const promptParts = [];
|
|
@@ -107909,58 +108304,62 @@ ${cr.logs.join(`
|
|
|
107909
108304
|
summary: vctx.appName ? hasAnalysis ? `${vctx.appName} crashed — ${cr.suggestion ?? cr.explanation ?? "surface the crash details to the user."}` : `${vctx.appName} runtime is unhealthy — surface the crash details to the user.` : "Runtime is unhealthy — surface the crash details to the user."
|
|
107910
108305
|
};
|
|
107911
108306
|
}
|
|
107912
|
-
|
|
107913
|
-
|
|
107914
|
-
|
|
107915
|
-
|
|
107916
|
-
|
|
107917
|
-
|
|
107918
|
-
|
|
107919
|
-
|
|
107920
|
-
|
|
107921
|
-
|
|
107922
|
-
|
|
107923
|
-
|
|
107924
|
-
|
|
107925
|
-
|
|
107926
|
-
|
|
107927
|
-
|
|
107928
|
-
|
|
107929
|
-
|
|
107930
|
-
|
|
107931
|
-
|
|
107932
|
-
|
|
107933
|
-
|
|
107934
|
-
|
|
107935
|
-
|
|
107936
|
-
|
|
107937
|
-
|
|
107938
|
-
|
|
107939
|
-
|
|
107940
|
-
|
|
107941
|
-
|
|
107942
|
-
|
|
107943
|
-
|
|
107944
|
-
|
|
107945
|
-
|
|
107946
|
-
|
|
107947
|
-
|
|
107948
|
-
|
|
107949
|
-
|
|
107950
|
-
|
|
107951
|
-
|
|
107952
|
-
|
|
107953
|
-
|
|
107954
|
-
|
|
107955
|
-
|
|
107956
|
-
|
|
107957
|
-
|
|
107958
|
-
|
|
107959
|
-
|
|
107960
|
-
|
|
107961
|
-
|
|
107962
|
-
|
|
107963
|
-
|
|
108307
|
+
},
|
|
108308
|
+
{
|
|
108309
|
+
id: "runtime-crashed-dispatch",
|
|
108310
|
+
when: ({ vctx, containerDown }) => vctx.appStatus === "crashed" || containerDown,
|
|
108311
|
+
verdict: (ctx) => {
|
|
108312
|
+
const { vctx } = ctx;
|
|
108313
|
+
const explanation = runtimeCrashExplanation(ctx);
|
|
108314
|
+
return {
|
|
108315
|
+
status: "needs_action",
|
|
108316
|
+
diagnosis: {
|
|
108317
|
+
title: vctx.appStatus === "crashed" ? "App crashed" : "Runtime not responding",
|
|
108318
|
+
explanation,
|
|
108319
|
+
reasonCode: "runtime_crashed",
|
|
108320
|
+
phase: "runtime"
|
|
108321
|
+
},
|
|
108322
|
+
recovery: {
|
|
108323
|
+
retryable: false,
|
|
108324
|
+
nextAction: "run_doctor",
|
|
108325
|
+
suggestedTool: "percher_doctor",
|
|
108326
|
+
args: { app: vctx.appName, mode: "runtime" },
|
|
108327
|
+
reasonCode: "runtime_crashed"
|
|
108328
|
+
},
|
|
108329
|
+
summary: vctx.appName ? `${vctx.appName} runtime needs investigation — call percher_doctor with mode='runtime'.` : "Runtime needs investigation — call percher_doctor with mode='runtime'."
|
|
108330
|
+
};
|
|
108331
|
+
}
|
|
108332
|
+
},
|
|
108333
|
+
{
|
|
108334
|
+
id: "public-route-unhealthy",
|
|
108335
|
+
when: ({ vctx }) => vctx.publicRouteHealthy === false,
|
|
108336
|
+
verdict: ({ vctx, findCheck }) => {
|
|
108337
|
+
const routeCheck = findCheck("Public route");
|
|
108338
|
+
return {
|
|
108339
|
+
status: "needs_action",
|
|
108340
|
+
diagnosis: {
|
|
108341
|
+
title: "Public route is not responding",
|
|
108342
|
+
explanation: routeCheck?.message ?? "Container looks healthy but the public URL isn't responding — route reconcile usually self-heals.",
|
|
108343
|
+
reasonCode: "infra_transient",
|
|
108344
|
+
phase: "infra"
|
|
108345
|
+
},
|
|
108346
|
+
recovery: {
|
|
108347
|
+
retryable: true,
|
|
108348
|
+
nextAction: "retry",
|
|
108349
|
+
suggestedTool: "percher_doctor",
|
|
108350
|
+
args: { app: vctx.appName },
|
|
108351
|
+
reasonCode: "infra_transient"
|
|
108352
|
+
},
|
|
108353
|
+
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."
|
|
108354
|
+
};
|
|
108355
|
+
}
|
|
108356
|
+
},
|
|
108357
|
+
FALLBACK_RULE
|
|
108358
|
+
];
|
|
108359
|
+
function deriveVerdict(checks3, vctx) {
|
|
108360
|
+
const ctx = buildVerdictRuleContext(checks3, vctx);
|
|
108361
|
+
const rule = VERDICT_RULES.find((r) => r.when(ctx)) ?? FALLBACK_RULE;
|
|
108362
|
+
return rule.verdict(ctx);
|
|
107964
108363
|
}
|
|
107965
108364
|
// ../core/dist/commands/domains.js
|
|
107966
108365
|
var domainAddInputSchema = exports_external.object({
|
|
@@ -108943,6 +109342,30 @@ async function init(ctx, input = {}) {
|
|
|
108943
109342
|
var insightsInputSchema = exports_external.object({
|
|
108944
109343
|
app: exports_external.string().optional()
|
|
108945
109344
|
});
|
|
109345
|
+
// ../core/dist/commands/inspect-link.js
|
|
109346
|
+
var inspectLinkInputSchema = exports_external.object({
|
|
109347
|
+
app: exports_external.string().optional().describe("App name (defaults to percher.toml)"),
|
|
109348
|
+
revoke: exports_external.boolean().optional().describe("Revoke the app's inspect link instead of creating one")
|
|
109349
|
+
});
|
|
109350
|
+
async function inspectLink(ctx, input = {}) {
|
|
109351
|
+
const app = input.app ?? readPercherTomlAppName(ctx.cwd);
|
|
109352
|
+
if (!app) {
|
|
109353
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
109354
|
+
code: "env.no-app",
|
|
109355
|
+
hint: "Pass an app name or run from a directory with percher.toml."
|
|
109356
|
+
});
|
|
109357
|
+
}
|
|
109358
|
+
if (input.revoke) {
|
|
109359
|
+
await ctx.client.apps.revokeInspectLink(app);
|
|
109360
|
+
return { revoked: true, app, summary: `Inspect link for ${app} revoked.` };
|
|
109361
|
+
}
|
|
109362
|
+
const { inspectUrl } = await ctx.client.apps.createInspectLink(app);
|
|
109363
|
+
return {
|
|
109364
|
+
inspectUrl,
|
|
109365
|
+
app,
|
|
109366
|
+
summary: `Share ${inspectUrl} — read-only status + deploy history, no login needed. Re-running rotates the link; --revoke kills it.`
|
|
109367
|
+
};
|
|
109368
|
+
}
|
|
108946
109369
|
// ../core/dist/commands/login.js
|
|
108947
109370
|
var loginInputSchema = exports_external.object({
|
|
108948
109371
|
token: exports_external.string().optional(),
|
|
@@ -108994,31 +109417,6 @@ async function login(ctx, input = {}) {
|
|
|
108994
109417
|
code: "login.expired"
|
|
108995
109418
|
});
|
|
108996
109419
|
}
|
|
108997
|
-
// ../shared/dist/logs.js
|
|
108998
|
-
var MAX_LOG_SEARCH_LENGTH = 256;
|
|
108999
|
-
// ../shared/dist/platform-env.js
|
|
109000
|
-
var PERCHER_INJECTED_ENV_KEYS = new Set([
|
|
109001
|
-
"NODE_ENV",
|
|
109002
|
-
"PORT",
|
|
109003
|
-
"DATABASE_URL",
|
|
109004
|
-
"POCKETBASE_URL",
|
|
109005
|
-
"POCKETBASE_ADMIN_EMAIL",
|
|
109006
|
-
"POCKETBASE_ADMIN_PASSWORD",
|
|
109007
|
-
"PERCHER_APP_NAME",
|
|
109008
|
-
"PERCHER_APP_URL",
|
|
109009
|
-
"HTTP_PROXY",
|
|
109010
|
-
"HTTPS_PROXY",
|
|
109011
|
-
"NO_PROXY",
|
|
109012
|
-
"NODE_USE_ENV_PROXY"
|
|
109013
|
-
]);
|
|
109014
|
-
var PROXY_LOWERCASE_ALIASES = new Set([
|
|
109015
|
-
"http_proxy",
|
|
109016
|
-
"https_proxy",
|
|
109017
|
-
"no_proxy"
|
|
109018
|
-
]);
|
|
109019
|
-
// ../shared/dist/runtime.js
|
|
109020
|
-
var DEFAULT_NODE_VERSION_MAJOR = "24";
|
|
109021
|
-
var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
|
|
109022
109420
|
// ../core/dist/commands/logs.js
|
|
109023
109421
|
var logsInputSchema = exports_external.object({
|
|
109024
109422
|
app: exports_external.string().optional(),
|
|
@@ -109967,24 +110365,23 @@ function topologicalSort(collections) {
|
|
|
109967
110365
|
continue;
|
|
109968
110366
|
if (!byName.has(target))
|
|
109969
110367
|
continue;
|
|
109970
|
-
dependsOn.get(c.name)
|
|
109971
|
-
dependents.get(target)
|
|
110368
|
+
dependsOn.get(c.name)?.add(target);
|
|
110369
|
+
dependents.get(target)?.add(c.name);
|
|
109972
110370
|
}
|
|
109973
110371
|
}
|
|
109974
110372
|
const inputOrder = collections.map((c) => c.name);
|
|
109975
110373
|
const inDegree = new Map;
|
|
109976
110374
|
for (const name of inputOrder)
|
|
109977
|
-
inDegree.set(name, dependsOn.get(name)
|
|
110375
|
+
inDegree.set(name, dependsOn.get(name)?.size ?? 0);
|
|
109978
110376
|
const ready = [];
|
|
109979
110377
|
for (const name of inputOrder) {
|
|
109980
110378
|
if (inDegree.get(name) === 0)
|
|
109981
110379
|
ready.push(name);
|
|
109982
110380
|
}
|
|
109983
110381
|
const out = [];
|
|
109984
|
-
|
|
109985
|
-
const name = ready.shift();
|
|
110382
|
+
for (let name = ready.shift();name !== undefined; name = ready.shift()) {
|
|
109986
110383
|
out.push(name);
|
|
109987
|
-
for (const dependent of dependents.get(name)) {
|
|
110384
|
+
for (const dependent of dependents.get(name) ?? []) {
|
|
109988
110385
|
const next = (inDegree.get(dependent) ?? 0) - 1;
|
|
109989
110386
|
inDegree.set(dependent, next);
|
|
109990
110387
|
if (next === 0)
|
|
@@ -110661,10 +111058,78 @@ async function open(ctx, input = {}) {
|
|
|
110661
111058
|
await ctx.openUrl(app.url);
|
|
110662
111059
|
return app.url;
|
|
110663
111060
|
}
|
|
111061
|
+
// ../core/dist/commands/preview-branch.js
|
|
111062
|
+
var previewBranchSetInputSchema = exports_external.object({
|
|
111063
|
+
app: exports_external.string().optional(),
|
|
111064
|
+
branch: exports_external.string().min(1, "Branch is required")
|
|
111065
|
+
});
|
|
111066
|
+
var previewBranchInputSchema = exports_external.object({
|
|
111067
|
+
app: exports_external.string().optional()
|
|
111068
|
+
});
|
|
111069
|
+
function resolveApp3(ctx, app) {
|
|
111070
|
+
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
111071
|
+
if (!name) {
|
|
111072
|
+
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
111073
|
+
code: "env.no-app",
|
|
111074
|
+
hint: "Pass an app name or run from a directory with percher.toml."
|
|
111075
|
+
});
|
|
111076
|
+
}
|
|
111077
|
+
return name;
|
|
111078
|
+
}
|
|
111079
|
+
async function previewBranchSet(ctx, input) {
|
|
111080
|
+
const appRef = resolveApp3(ctx, input.app);
|
|
111081
|
+
return ctx.client.apps.setPreviewBranch(appRef, input.branch);
|
|
111082
|
+
}
|
|
111083
|
+
async function previewBranchUnset(ctx, input = {}) {
|
|
111084
|
+
const appRef = resolveApp3(ctx, input.app);
|
|
111085
|
+
return ctx.client.apps.unsetPreviewBranch(appRef);
|
|
111086
|
+
}
|
|
111087
|
+
var setPreviewBranchToolInputSchema = exports_external.object({
|
|
111088
|
+
app: exports_external.string().optional(),
|
|
111089
|
+
branch: exports_external.string().nullable().describe("Branch to pin as the preview address; pass null to clear it.")
|
|
111090
|
+
});
|
|
111091
|
+
async function previewBranchShow(ctx, input = {}) {
|
|
111092
|
+
const appRef = resolveApp3(ctx, input.app);
|
|
111093
|
+
const app = await ctx.client.apps.get(appRef);
|
|
111094
|
+
return {
|
|
111095
|
+
previewBranch: app.previewBranch ?? null,
|
|
111096
|
+
previewUrl: app.previewUrl ?? null,
|
|
111097
|
+
status: app.previewStatus ?? "disabled"
|
|
111098
|
+
};
|
|
111099
|
+
}
|
|
110664
111100
|
// ../core/dist/commands/publish.js
|
|
110665
111101
|
import { existsSync as existsSync14 } from "node:fs";
|
|
110666
111102
|
import { join as join20 } from "node:path";
|
|
110667
111103
|
|
|
111104
|
+
// ../shared/dist/pow.js
|
|
111105
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
111106
|
+
function digest(nonce, solution) {
|
|
111107
|
+
return createHash3("sha256").update(`${nonce}:${solution}`).digest("hex");
|
|
111108
|
+
}
|
|
111109
|
+
function leadingZeroBits(hexDigest) {
|
|
111110
|
+
let bits = 0;
|
|
111111
|
+
for (const ch of hexDigest) {
|
|
111112
|
+
const nibble = Number.parseInt(ch, 16);
|
|
111113
|
+
if (nibble === 0) {
|
|
111114
|
+
bits += 4;
|
|
111115
|
+
continue;
|
|
111116
|
+
}
|
|
111117
|
+
bits += Math.clz32(nibble) - 28;
|
|
111118
|
+
break;
|
|
111119
|
+
}
|
|
111120
|
+
return bits;
|
|
111121
|
+
}
|
|
111122
|
+
function solvePow(nonce, difficulty, maxIterations = 50000000) {
|
|
111123
|
+
if (difficulty <= 0)
|
|
111124
|
+
return "0";
|
|
111125
|
+
for (let i = 0;i < maxIterations; i++) {
|
|
111126
|
+
const solution = i.toString(36);
|
|
111127
|
+
if (leadingZeroBits(digest(nonce, solution)) >= difficulty)
|
|
111128
|
+
return solution;
|
|
111129
|
+
}
|
|
111130
|
+
return null;
|
|
111131
|
+
}
|
|
111132
|
+
|
|
110668
111133
|
// ../core/dist/event-renderer.js
|
|
110669
111134
|
function renderDeployEvent(event) {
|
|
110670
111135
|
if (event.stage === "queue") {
|
|
@@ -110796,10 +111261,10 @@ async function pollDeployment(opts) {
|
|
|
110796
111261
|
polled = true;
|
|
110797
111262
|
} catch (err) {
|
|
110798
111263
|
attempt++;
|
|
110799
|
-
const
|
|
110800
|
-
const isNetwork = msg.startsWith("network error:");
|
|
111264
|
+
const isNetwork = isTransientNetworkError(err);
|
|
110801
111265
|
if (!isNetwork || attempt >= maxRetries) {
|
|
110802
111266
|
if (isNetwork) {
|
|
111267
|
+
const msg = err.message ?? String(err);
|
|
110803
111268
|
throw new Error(`${msg}
|
|
110804
111269
|
|
|
110805
111270
|
The deploy may already be live — your network connection dropped mid-poll. Check with: percher deploys list ${opts.appName}`, { cause: err });
|
|
@@ -110961,7 +111426,7 @@ function classifyDeployRetry(err, attempt) {
|
|
|
110961
111426
|
category: "non_retryable"
|
|
110962
111427
|
};
|
|
110963
111428
|
}
|
|
110964
|
-
if (
|
|
111429
|
+
if (isTransientNetworkError(err)) {
|
|
110965
111430
|
const delay = NETWORK_BACKOFF_MS[idx];
|
|
110966
111431
|
if (delay === undefined) {
|
|
110967
111432
|
return {
|
|
@@ -111096,6 +111561,12 @@ function* walkDir(dir, base) {
|
|
|
111096
111561
|
}
|
|
111097
111562
|
|
|
111098
111563
|
// ../core/dist/commands/publish-api-error.js
|
|
111564
|
+
var DEPLOY_GATE_CODES = new Set([
|
|
111565
|
+
"RETRY_LIMIT_REACHED",
|
|
111566
|
+
"REQUIRED_ENV_MISSING",
|
|
111567
|
+
"ENV_KEY_UNDECLARED",
|
|
111568
|
+
"DAILY_QUOTA_EXCEEDED"
|
|
111569
|
+
]);
|
|
111099
111570
|
function classifyPublishApiError(err, input) {
|
|
111100
111571
|
if (!(err instanceof PercherApiError))
|
|
111101
111572
|
return null;
|
|
@@ -111120,6 +111591,18 @@ function classifyPublishApiError(err, input) {
|
|
|
111120
111591
|
const auth = matchAuthError(err);
|
|
111121
111592
|
if (auth)
|
|
111122
111593
|
return buildResult(auth, input);
|
|
111594
|
+
const retryLimit = matchRetryLimitReached(err, input);
|
|
111595
|
+
if (retryLimit)
|
|
111596
|
+
return buildResult(retryLimit, input);
|
|
111597
|
+
const requiredEnv = matchRequiredEnvMissing(err, input);
|
|
111598
|
+
if (requiredEnv)
|
|
111599
|
+
return buildResult(requiredEnv, input);
|
|
111600
|
+
const undeclaredEnv = matchEnvKeyUndeclared(err, input);
|
|
111601
|
+
if (undeclaredEnv)
|
|
111602
|
+
return buildResult(undeclaredEnv, input);
|
|
111603
|
+
const dailyQuota = matchDailyQuotaExceeded(err, input);
|
|
111604
|
+
if (dailyQuota)
|
|
111605
|
+
return buildResult(dailyQuota, input);
|
|
111123
111606
|
const clientError = matchGenericClientError(err);
|
|
111124
111607
|
if (clientError)
|
|
111125
111608
|
return buildResult(clientError, input);
|
|
@@ -111128,12 +111611,17 @@ function classifyPublishApiError(err, input) {
|
|
|
111128
111611
|
function buildResult(c, input) {
|
|
111129
111612
|
return {
|
|
111130
111613
|
status: "failed",
|
|
111614
|
+
...input.app !== undefined ? { app: input.app } : {},
|
|
111131
111615
|
fileCount: input.bundle.fileCount,
|
|
111132
111616
|
bytes: input.bundle.bytes,
|
|
111133
111617
|
error: {
|
|
111134
111618
|
title: c.title,
|
|
111135
111619
|
explanation: c.explanation,
|
|
111136
|
-
suggestion: c.suggestion
|
|
111620
|
+
suggestion: c.suggestion,
|
|
111621
|
+
...c.errorClass !== undefined ? { errorClass: c.errorClass } : {},
|
|
111622
|
+
...c.phase !== undefined ? { phase: c.phase } : {},
|
|
111623
|
+
...c.missingEnvVars !== undefined ? { missingEnvVars: c.missingEnvVars } : {},
|
|
111624
|
+
...c.relevantFiles !== undefined ? { relevantFiles: c.relevantFiles } : {}
|
|
111137
111625
|
},
|
|
111138
111626
|
recovery: c.recovery,
|
|
111139
111627
|
summary: c.summary,
|
|
@@ -111152,9 +111640,9 @@ function matchPlanLimit(err) {
|
|
|
111152
111640
|
return {
|
|
111153
111641
|
title: "Plan limit reached",
|
|
111154
111642
|
explanation: err.message,
|
|
111155
|
-
suggestion: "Upgrade your plan at https://percher.app/
|
|
111643
|
+
suggestion: "Upgrade your plan at https://percher.app/account/billing, or delete unused apps with `percher delete <name>`.",
|
|
111156
111644
|
recovery: recoveryAsk({
|
|
111157
|
-
prompt: `${err.message}. Upgrade at https://percher.app/
|
|
111645
|
+
prompt: `${err.message}. Upgrade at https://percher.app/account/billing, or delete an unused app with \`percher delete <name>\`.`,
|
|
111158
111646
|
options: ["upgrade", "delete unused app", "cancel"],
|
|
111159
111647
|
reasonCode: "quota_exceeded"
|
|
111160
111648
|
}),
|
|
@@ -111172,7 +111660,7 @@ function matchRateLimit(err) {
|
|
|
111172
111660
|
return {
|
|
111173
111661
|
title: "Rate limit hit",
|
|
111174
111662
|
explanation: err.message,
|
|
111175
|
-
suggestion: "Wait a few minutes and try again. If you need a higher cap, upgrade at https://percher.app/
|
|
111663
|
+
suggestion: "Wait a few minutes and try again. If you need a higher cap, upgrade at https://percher.app/account/billing.",
|
|
111176
111664
|
recovery: recoveryAsk({
|
|
111177
111665
|
prompt: `Rate limit: ${err.message}. Wait a few minutes then retry, or upgrade your plan.`,
|
|
111178
111666
|
options: ["wait and retry", "upgrade", "cancel"],
|
|
@@ -111190,7 +111678,7 @@ function matchConcurrentLimit(err) {
|
|
|
111190
111678
|
explanation: err.message,
|
|
111191
111679
|
suggestion: "Wait for one of your in-flight deploys to finish, or upgrade your plan to raise the concurrency cap.",
|
|
111192
111680
|
recovery: recoveryAsk({
|
|
111193
|
-
prompt: `${err.message} Wait for one to finish, or upgrade at https://percher.app/
|
|
111681
|
+
prompt: `${err.message} Wait for one to finish, or upgrade at https://percher.app/account/billing.`,
|
|
111194
111682
|
options: ["wait", "upgrade", "cancel"],
|
|
111195
111683
|
reasonCode: "deploy_already_in_progress"
|
|
111196
111684
|
}),
|
|
@@ -111265,7 +111753,7 @@ function matchEmailNotVerified(err) {
|
|
|
111265
111753
|
return {
|
|
111266
111754
|
title: "Email not verified",
|
|
111267
111755
|
explanation: err.message,
|
|
111268
|
-
suggestion: "Open the verification link in your inbox, or visit https://percher.app/
|
|
111756
|
+
suggestion: "Open the verification link in your inbox, or visit https://percher.app/account to resend the email. Re-publish after verifying.",
|
|
111269
111757
|
recovery: recoveryAsk({
|
|
111270
111758
|
prompt: `${err.message} Verify the email, then retry publish.`,
|
|
111271
111759
|
options: ["check inbox", "open settings"],
|
|
@@ -111299,6 +111787,94 @@ function matchAuthError(err) {
|
|
|
111299
111787
|
summary: `Permission denied (${err.status} ${err.code || "—"}): ${err.message}`
|
|
111300
111788
|
};
|
|
111301
111789
|
}
|
|
111790
|
+
function matchRetryLimitReached(err, input) {
|
|
111791
|
+
if (err.code !== "RETRY_LIMIT_REACHED" || input.app === undefined)
|
|
111792
|
+
return null;
|
|
111793
|
+
const appName = input.app.name;
|
|
111794
|
+
const extra = err.extra ?? {};
|
|
111795
|
+
const resetAtStr = extra.resetAt ?? "";
|
|
111796
|
+
const resetAtForPrompt = resetAtStr || "the cooldown window expires";
|
|
111797
|
+
return {
|
|
111798
|
+
title: "Retry limit reached",
|
|
111799
|
+
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.",
|
|
111800
|
+
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.`,
|
|
111801
|
+
recovery: recoveryAsk({
|
|
111802
|
+
prompt: `Percher already retried once after an infrastructure failure for ${appName}. Wait until ${resetAtForPrompt} or surface to the user.`,
|
|
111803
|
+
options: ["wait before retrying", "inspect status"],
|
|
111804
|
+
reasonCode: "retry_limit_reached",
|
|
111805
|
+
retryable: false
|
|
111806
|
+
}),
|
|
111807
|
+
summary: `Retry limit reached for ${appName}${resetAtStr ? ` — wait until ${resetAtStr}` : ""}.`
|
|
111808
|
+
};
|
|
111809
|
+
}
|
|
111810
|
+
function matchRequiredEnvMissing(err, input) {
|
|
111811
|
+
if (err.code !== "REQUIRED_ENV_MISSING" || input.app === undefined)
|
|
111812
|
+
return null;
|
|
111813
|
+
const extra = err.extra ?? {};
|
|
111814
|
+
const keys = extra.keys ?? [];
|
|
111815
|
+
const source = extra.source ?? "contract";
|
|
111816
|
+
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";
|
|
111817
|
+
return {
|
|
111818
|
+
title: "Required env keys not set",
|
|
111819
|
+
explanation: `${keys.length} env ${keys.length === 1 ? "key is" : "keys are"} ${sourceText} but missing from the env store: ${keys.join(", ")}.`,
|
|
111820
|
+
suggestion: `Run \`bunx percher env set ${keys[0] ?? "KEY"}=value\` (and similarly for the rest) and re-publish.`,
|
|
111821
|
+
errorClass: "missing_env",
|
|
111822
|
+
phase: "config",
|
|
111823
|
+
missingEnvVars: keys,
|
|
111824
|
+
recovery: recoveryEnv({
|
|
111825
|
+
app: input.app.name,
|
|
111826
|
+
keys,
|
|
111827
|
+
reasonCode: "missing_env"
|
|
111828
|
+
}),
|
|
111829
|
+
summary: `Missing required env ${keys.length === 1 ? "key" : "keys"}: ${keys.join(", ")}.`
|
|
111830
|
+
};
|
|
111831
|
+
}
|
|
111832
|
+
function matchEnvKeyUndeclared(err, input) {
|
|
111833
|
+
if (err.code !== "ENV_KEY_UNDECLARED" || input.app === undefined)
|
|
111834
|
+
return null;
|
|
111835
|
+
const extra = err.extra ?? {};
|
|
111836
|
+
const apiProblems = extra.problems ?? [];
|
|
111837
|
+
const problems = apiProblems.map((p) => ({
|
|
111838
|
+
file: p.file,
|
|
111839
|
+
line: p.line,
|
|
111840
|
+
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)"}`
|
|
111841
|
+
}));
|
|
111842
|
+
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
111843
|
+
return {
|
|
111844
|
+
title: "Undeclared env key",
|
|
111845
|
+
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
111846
|
+
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.",
|
|
111847
|
+
errorClass: "config_invalid",
|
|
111848
|
+
phase: "config",
|
|
111849
|
+
relevantFiles: ["percher.toml"],
|
|
111850
|
+
recovery: recoveryFixConfig({
|
|
111851
|
+
problems,
|
|
111852
|
+
reasonCode: "env_key_undeclared"
|
|
111853
|
+
}),
|
|
111854
|
+
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`
|
|
111855
|
+
};
|
|
111856
|
+
}
|
|
111857
|
+
function matchDailyQuotaExceeded(err, input) {
|
|
111858
|
+
if (err.code !== "DAILY_QUOTA_EXCEEDED" || input.app === undefined)
|
|
111859
|
+
return null;
|
|
111860
|
+
const extra = err.extra ?? {};
|
|
111861
|
+
const kind = extra.kind ?? "live";
|
|
111862
|
+
const used = extra.used ?? 0;
|
|
111863
|
+
const limit = extra.limit ?? 0;
|
|
111864
|
+
const resetAt = extra.resetAt ?? "";
|
|
111865
|
+
const otherKind = kind === "live" ? "preview" : "live";
|
|
111866
|
+
return {
|
|
111867
|
+
title: "Daily deploy quota reached",
|
|
111868
|
+
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
111869
|
+
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/account/billing to raise this cap.`,
|
|
111870
|
+
recovery: recoveryAsk({
|
|
111871
|
+
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.`,
|
|
111872
|
+
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
111873
|
+
reasonCode: "quota_exceeded"
|
|
111874
|
+
}),
|
|
111875
|
+
summary: `Daily ${kind}-deploy quota reached (${used}/${limit}). Resets ${resetAt}.`
|
|
111876
|
+
};
|
|
111877
|
+
}
|
|
111302
111878
|
function matchGenericClientError(err) {
|
|
111303
111879
|
if (err.status < 400 || err.status >= 500)
|
|
111304
111880
|
return null;
|
|
@@ -111360,15 +111936,15 @@ var publishInputSchema = exports_external.object({
|
|
|
111360
111936
|
message: exports_external.string().optional().describe("Deploy note (visible in deploy history and version timeline)"),
|
|
111361
111937
|
dryRun: exports_external.boolean().optional().describe("Show what would be deployed (file count, bytes, top files) without uploading or building. Useful for verifying the bundle before a real deploy."),
|
|
111362
111938
|
noCache: exports_external.boolean().optional().describe("Skip the image-cache lookup and force a fresh build. Use this when you suspect the cached image doesn't reflect your latest source — the new build's resulting image still gets cached, so future deploys benefit."),
|
|
111363
|
-
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.")
|
|
111939
|
+
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."),
|
|
111940
|
+
anonymous: exports_external.boolean().optional().describe("Publish WITHOUT an account. Creates a throwaway anonymous app that expires in 72h unless claimed (percher_claim_app). The app runs with no outbound network, no database, and no env vars until claimed. Only honored when the caller has no token; ignored once signed in. Server-gated — fails with ANON_DEPLOYS_DISABLED if the instance hasn't enabled it.")
|
|
111364
111941
|
});
|
|
111365
|
-
var MAX_BYTES = 500 * 1024 * 1024;
|
|
111366
111942
|
var SUSPICIOUS_BYTES = 50 * 1024 * 1024;
|
|
111367
111943
|
var SUSPICIOUS_FILES = 1e4;
|
|
111368
111944
|
function tomlPathFor2(cwd) {
|
|
111369
111945
|
return join20(cwd, "percher.toml");
|
|
111370
111946
|
}
|
|
111371
|
-
async function
|
|
111947
|
+
async function buildReplacedResult2(opts) {
|
|
111372
111948
|
const { ctx, app, deployment, tarball, cwd } = opts;
|
|
111373
111949
|
const resolved = await resolveReplaced({
|
|
111374
111950
|
ctx,
|
|
@@ -111390,45 +111966,18 @@ async function buildReplacedResult(opts) {
|
|
|
111390
111966
|
}
|
|
111391
111967
|
function buildAlreadyInProgressResult(opts) {
|
|
111392
111968
|
const { active, app, tarball, cwd } = opts;
|
|
111393
|
-
const reasonCode = active.deployStatus === "queued" ? "deploy_queued" : active.deployStatus === "building" ? "deploy_building" : "deploy_deploying";
|
|
111394
|
-
return {
|
|
111395
|
-
status: "already_in_progress",
|
|
111396
|
-
app,
|
|
111397
|
-
fileCount: tarball.fileCount,
|
|
111398
|
-
bytes: tarball.bytes,
|
|
111399
|
-
recovery: recoveryWait({
|
|
111400
|
-
app: app.name,
|
|
111401
|
-
deployId: active.deployId,
|
|
111402
|
-
reasonCode
|
|
111403
|
-
}),
|
|
111404
|
-
summary: `${app.name} already has a deploy in progress (${active.deployId}, ${active.deployStatus}) — wait for it to finish instead of queueing a duplicate.`,
|
|
111405
|
-
configPath: tomlPathFor2(cwd),
|
|
111406
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111407
|
-
};
|
|
111408
|
-
}
|
|
111409
|
-
function buildRetryLimitReachedResult(opts) {
|
|
111410
|
-
const { err, app, tarball, cwd } = opts;
|
|
111411
|
-
const extra = err.extra ?? {};
|
|
111412
|
-
const resetAtStr = extra.resetAt ?? "";
|
|
111413
|
-
const resetAtForPrompt = resetAtStr || "the cooldown window expires";
|
|
111414
|
-
const prompt = `Percher already retried once after an infrastructure failure for ${app.name}. Wait until ${resetAtForPrompt} or surface to the user.`;
|
|
111969
|
+
const reasonCode = active.deployStatus === "queued" ? "deploy_queued" : active.deployStatus === "building" ? "deploy_building" : "deploy_deploying";
|
|
111415
111970
|
return {
|
|
111416
|
-
status: "
|
|
111971
|
+
status: "already_in_progress",
|
|
111417
111972
|
app,
|
|
111418
111973
|
fileCount: tarball.fileCount,
|
|
111419
111974
|
bytes: tarball.bytes,
|
|
111420
|
-
|
|
111421
|
-
|
|
111422
|
-
|
|
111423
|
-
|
|
111424
|
-
},
|
|
111425
|
-
recovery: recoveryAsk({
|
|
111426
|
-
prompt,
|
|
111427
|
-
options: ["wait before retrying", "inspect status"],
|
|
111428
|
-
reasonCode: "retry_limit_reached",
|
|
111429
|
-
retryable: false
|
|
111975
|
+
recovery: recoveryWait({
|
|
111976
|
+
app: app.name,
|
|
111977
|
+
deployId: active.deployId,
|
|
111978
|
+
reasonCode
|
|
111430
111979
|
}),
|
|
111431
|
-
summary:
|
|
111980
|
+
summary: `${app.name} already has a deploy in progress (${active.deployId}, ${active.deployStatus}) — wait for it to finish instead of queueing a duplicate.`,
|
|
111432
111981
|
configPath: tomlPathFor2(cwd),
|
|
111433
111982
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111434
111983
|
};
|
|
@@ -111555,7 +112104,7 @@ ${bullets}`,
|
|
|
111555
112104
|
ctx.status("[2/4] Packaging files...");
|
|
111556
112105
|
const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
111557
112106
|
const packageMs = Date.now() - t0;
|
|
111558
|
-
if (tarball.bytes >
|
|
112107
|
+
if (tarball.bytes > MAX_TARBALL_BYTES) {
|
|
111559
112108
|
return {
|
|
111560
112109
|
status: "failed",
|
|
111561
112110
|
fileCount: tarball.fileCount,
|
|
@@ -111650,12 +112199,26 @@ ${keyList}
|
|
|
111650
112199
|
ctx.status(`Build queue wait: ~${preflight.queueWaitEstimateSec}s — your build is queued behind ${preflight.queueDepth} other(s).`);
|
|
111651
112200
|
}
|
|
111652
112201
|
} catch {}
|
|
112202
|
+
return executeDeploy({
|
|
112203
|
+
ctx,
|
|
112204
|
+
config: config2,
|
|
112205
|
+
input,
|
|
112206
|
+
app,
|
|
112207
|
+
firstDeploy,
|
|
112208
|
+
tarball,
|
|
112209
|
+
t0,
|
|
112210
|
+
packageMs,
|
|
112211
|
+
missingBuildEnvKeys
|
|
112212
|
+
});
|
|
112213
|
+
}
|
|
112214
|
+
async function executeDeploy(opts) {
|
|
112215
|
+
const { ctx, config: config2, input, app, firstDeploy, tarball, t0, packageMs, missingBuildEnvKeys } = opts;
|
|
111653
112216
|
const uploadStart = Date.now();
|
|
111654
112217
|
ctx.status("[3/4] Uploading...");
|
|
111655
112218
|
const tarballBytes = new Uint8Array(await new Response(tarball.stream).arrayBuffer());
|
|
111656
112219
|
let cacheReusedDeployment = null;
|
|
111657
112220
|
const contentHash = tarball.contentHash;
|
|
111658
|
-
if (!input.noCache) {
|
|
112221
|
+
if (!input.noCache && !input.preview) {
|
|
111659
112222
|
try {
|
|
111660
112223
|
const probe = await ctx.client.platform.cacheProbe({
|
|
111661
112224
|
appId: app.id,
|
|
@@ -111701,99 +112264,14 @@ ${keyList}
|
|
|
111701
112264
|
}
|
|
111702
112265
|
deployment = deployResponse;
|
|
111703
112266
|
} catch (err) {
|
|
111704
|
-
if (err instanceof PercherApiError && err.code
|
|
111705
|
-
|
|
111706
|
-
err,
|
|
111707
|
-
app,
|
|
111708
|
-
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
111709
|
-
cwd: ctx.cwd
|
|
111710
|
-
});
|
|
111711
|
-
}
|
|
111712
|
-
if (err instanceof PercherApiError && err.code === "REQUIRED_ENV_MISSING") {
|
|
111713
|
-
const extra = err.extra ?? {};
|
|
111714
|
-
const keys = extra.keys ?? [];
|
|
111715
|
-
const source = extra.source ?? "contract";
|
|
111716
|
-
const sourceText = source === "contract" ? "declared in your [env].required" : source === "discovered" ? "learned from a previous build failure" : "both declared and learned from a previous build failure";
|
|
111717
|
-
return {
|
|
111718
|
-
status: "failed",
|
|
111719
|
-
app,
|
|
111720
|
-
fileCount: tarball.fileCount,
|
|
111721
|
-
bytes: tarball.bytes,
|
|
111722
|
-
error: {
|
|
111723
|
-
title: "Required env keys not set",
|
|
111724
|
-
explanation: `${keys.length} env ${keys.length === 1 ? "key is" : "keys are"} ${sourceText} but missing from the env store: ${keys.join(", ")}.`,
|
|
111725
|
-
suggestion: `Run \`bunx percher env set ${keys[0] ?? "KEY"}=value\` (and similarly for the rest) and re-publish.`,
|
|
111726
|
-
errorClass: "missing_env",
|
|
111727
|
-
phase: "config",
|
|
111728
|
-
missingEnvVars: keys
|
|
111729
|
-
},
|
|
111730
|
-
recovery: recoveryEnv({
|
|
111731
|
-
app: app.name,
|
|
111732
|
-
keys,
|
|
111733
|
-
reasonCode: "missing_env"
|
|
111734
|
-
}),
|
|
111735
|
-
summary: `Missing required env ${keys.length === 1 ? "key" : "keys"}: ${keys.join(", ")}.`,
|
|
112267
|
+
if (err instanceof PercherApiError && DEPLOY_GATE_CODES.has(err.code)) {
|
|
112268
|
+
const classified = classifyPublishApiError(err, {
|
|
111736
112269
|
configPath: tomlPathFor2(ctx.cwd),
|
|
111737
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111738
|
-
|
|
111739
|
-
|
|
111740
|
-
|
|
111741
|
-
|
|
111742
|
-
const apiProblems = extra.problems ?? [];
|
|
111743
|
-
const problems = apiProblems.map((p) => ({
|
|
111744
|
-
file: p.file,
|
|
111745
|
-
line: p.line,
|
|
111746
|
-
message: `Source references env key '${p.key}' which is not declared in [env]. Add it to [env].required, [env].optional, or [env].ignore in percher.toml. Context: ${p.context ?? "(unavailable)"}`
|
|
111747
|
-
}));
|
|
111748
|
-
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
111749
|
-
return {
|
|
111750
|
-
status: "failed",
|
|
111751
|
-
app,
|
|
111752
|
-
fileCount: tarball.fileCount,
|
|
111753
|
-
bytes: tarball.bytes,
|
|
111754
|
-
error: {
|
|
111755
|
-
title: "Undeclared env key",
|
|
111756
|
-
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
111757
|
-
suggestion: "Add each key to [env].required (must exist before deploy), [env].optional (may be referenced), or [env].ignore (intentionally unset). See https://percher.app/docs/env for the contract.",
|
|
111758
|
-
errorClass: "config_invalid",
|
|
111759
|
-
phase: "config",
|
|
111760
|
-
relevantFiles: ["percher.toml"]
|
|
111761
|
-
},
|
|
111762
|
-
recovery: recoveryFixConfig({
|
|
111763
|
-
problems,
|
|
111764
|
-
reasonCode: "env_key_undeclared"
|
|
111765
|
-
}),
|
|
111766
|
-
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`,
|
|
111767
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111768
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111769
|
-
};
|
|
111770
|
-
}
|
|
111771
|
-
if (err instanceof PercherApiError && err.code === "DAILY_QUOTA_EXCEEDED") {
|
|
111772
|
-
const extra = err.extra ?? {};
|
|
111773
|
-
const kind = extra.kind ?? "live";
|
|
111774
|
-
const used = extra.used ?? 0;
|
|
111775
|
-
const limit = extra.limit ?? 0;
|
|
111776
|
-
const resetAt = extra.resetAt ?? "";
|
|
111777
|
-
const otherKind = kind === "live" ? "preview" : "live";
|
|
111778
|
-
return {
|
|
111779
|
-
status: "failed",
|
|
111780
|
-
app,
|
|
111781
|
-
fileCount: tarball.fileCount,
|
|
111782
|
-
bytes: tarball.bytes,
|
|
111783
|
-
error: {
|
|
111784
|
-
title: "Daily deploy quota reached",
|
|
111785
|
-
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
111786
|
-
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/settings to raise this cap.`
|
|
111787
|
-
},
|
|
111788
|
-
recovery: recoveryAsk({
|
|
111789
|
-
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then, deploy a ${otherKind} instead, or upgrade at https://percher.app/settings.`,
|
|
111790
|
-
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
111791
|
-
reasonCode: "quota_exceeded"
|
|
111792
|
-
}),
|
|
111793
|
-
summary: `Daily ${kind}-deploy quota reached (${used}/${limit}). Resets ${resetAt}.`,
|
|
111794
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
111795
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
111796
|
-
};
|
|
112270
|
+
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
112271
|
+
app
|
|
112272
|
+
});
|
|
112273
|
+
if (classified)
|
|
112274
|
+
return classified;
|
|
111797
112275
|
}
|
|
111798
112276
|
throw err;
|
|
111799
112277
|
}
|
|
@@ -111891,7 +112369,7 @@ ${keyList}
|
|
|
111891
112369
|
}
|
|
111892
112370
|
const buildMs = Date.now() - buildStart;
|
|
111893
112371
|
if (deployment.status === "replaced") {
|
|
111894
|
-
return await
|
|
112372
|
+
return await buildReplacedResult2({
|
|
111895
112373
|
ctx,
|
|
111896
112374
|
app,
|
|
111897
112375
|
deployment,
|
|
@@ -111960,6 +112438,16 @@ ${keyList}
|
|
|
111960
112438
|
};
|
|
111961
112439
|
}
|
|
111962
112440
|
async function handleUnauthorized(ctx, config2, tarball, input) {
|
|
112441
|
+
if (input.anonymous) {
|
|
112442
|
+
return handleAnonymousPublish(ctx, config2, input, tarball);
|
|
112443
|
+
}
|
|
112444
|
+
const anonAlternative = {
|
|
112445
|
+
action: "retry",
|
|
112446
|
+
suggestedTool: "percher_publish",
|
|
112447
|
+
args: { ...input, anonymous: true },
|
|
112448
|
+
when: "to publish without an account (expires in 72h, claim later with percher_claim_app)"
|
|
112449
|
+
};
|
|
112450
|
+
const needsLoginRecovery = { ...RECOVERY_NEEDS_LOGIN, alternativeActions: [anonAlternative] };
|
|
111963
112451
|
if (!ctx.interactiveLogin) {
|
|
111964
112452
|
try {
|
|
111965
112453
|
const deviceClient = new PercherClient({
|
|
@@ -111974,7 +112462,7 @@ async function handleUnauthorized(ctx, config2, tarball, input) {
|
|
|
111974
112462
|
loginCode: userCode,
|
|
111975
112463
|
fileCount: tarball.fileCount,
|
|
111976
112464
|
bytes: tarball.bytes,
|
|
111977
|
-
recovery:
|
|
112465
|
+
recovery: needsLoginRecovery,
|
|
111978
112466
|
summary: `Login required: open ${loginUrl} (code: ${userCode}).`,
|
|
111979
112467
|
configPath: tomlPathFor2(ctx.cwd),
|
|
111980
112468
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
@@ -111989,7 +112477,7 @@ async function handleUnauthorized(ctx, config2, tarball, input) {
|
|
|
111989
112477
|
explanation: "No valid Percher token found.",
|
|
111990
112478
|
suggestion: "Run the percher_login tool first, or set PERCHER_TOKEN environment variable."
|
|
111991
112479
|
},
|
|
111992
|
-
recovery:
|
|
112480
|
+
recovery: needsLoginRecovery,
|
|
111993
112481
|
summary: "Login required — run percher_login or set PERCHER_TOKEN.",
|
|
111994
112482
|
configPath: tomlPathFor2(ctx.cwd),
|
|
111995
112483
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
@@ -112003,149 +112491,101 @@ async function handleUnauthorized(ctx, config2, tarball, input) {
|
|
|
112003
112491
|
token: newToken,
|
|
112004
112492
|
sessionId: ctx.client.sessionId
|
|
112005
112493
|
});
|
|
112494
|
+
const t0 = Date.now();
|
|
112006
112495
|
const freshTarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
112007
|
-
|
|
112008
|
-
|
|
112009
|
-
({
|
|
112010
|
-
|
|
112011
|
-
|
|
112012
|
-
|
|
112013
|
-
|
|
112496
|
+
const packageMs = Date.now() - t0;
|
|
112497
|
+
const { app, firstDeploy } = await ensureApp2(ctx, config2);
|
|
112498
|
+
return executeDeploy({
|
|
112499
|
+
ctx,
|
|
112500
|
+
config: config2,
|
|
112501
|
+
input,
|
|
112502
|
+
app,
|
|
112503
|
+
firstDeploy,
|
|
112504
|
+
tarball: freshTarball,
|
|
112505
|
+
t0,
|
|
112506
|
+
packageMs,
|
|
112507
|
+
missingBuildEnvKeys: []
|
|
112508
|
+
});
|
|
112509
|
+
}
|
|
112510
|
+
async function handleAnonymousPublish(ctx, config2, input, tarball) {
|
|
112511
|
+
const anonClient = new PercherClient({
|
|
112512
|
+
apiUrl: ctx.client.apiUrl,
|
|
112513
|
+
sessionId: ctx.client.sessionId
|
|
112514
|
+
});
|
|
112515
|
+
let created;
|
|
112014
112516
|
try {
|
|
112015
|
-
const
|
|
112016
|
-
|
|
112017
|
-
|
|
112018
|
-
|
|
112019
|
-
|
|
112020
|
-
|
|
112021
|
-
|
|
112022
|
-
|
|
112023
|
-
|
|
112024
|
-
onRetry: ({ attempt, decision }) => {
|
|
112025
|
-
ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
|
|
112026
|
-
}
|
|
112517
|
+
const ch = await anonClient.anon.requestChallenge();
|
|
112518
|
+
const solution = solvePow(ch.nonce, ch.difficulty);
|
|
112519
|
+
if (solution === null) {
|
|
112520
|
+
throw new Error("proof-of-work unsolvable (difficulty too high)");
|
|
112521
|
+
}
|
|
112522
|
+
created = await anonClient.anon.createAnonApp({
|
|
112523
|
+
name: config2.app.name,
|
|
112524
|
+
powChallengeId: ch.challengeId,
|
|
112525
|
+
powSolution: solution
|
|
112027
112526
|
});
|
|
112028
|
-
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
112029
|
-
return buildAlreadyInProgressResult({
|
|
112030
|
-
active: deployResponse,
|
|
112031
|
-
app,
|
|
112032
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
112033
|
-
cwd: ctx.cwd
|
|
112034
|
-
});
|
|
112035
|
-
}
|
|
112036
|
-
deployment = deployResponse;
|
|
112037
112527
|
} catch (err) {
|
|
112038
|
-
|
|
112039
|
-
|
|
112040
|
-
err,
|
|
112041
|
-
app,
|
|
112042
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
112043
|
-
cwd: ctx.cwd
|
|
112044
|
-
});
|
|
112045
|
-
}
|
|
112046
|
-
throw err;
|
|
112047
|
-
}
|
|
112048
|
-
const replacedPreview = deployment.replacedPreview === true;
|
|
112049
|
-
if (input.waitForLive === false) {
|
|
112050
|
-
ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
|
|
112528
|
+
const code = err.code;
|
|
112529
|
+
const detail = code === "ANON_CAPACITY" ? "Anonymous capacity is full right now." : "Anonymous deploys are not enabled on this instance.";
|
|
112051
112530
|
return {
|
|
112052
|
-
status: "
|
|
112053
|
-
|
|
112054
|
-
|
|
112055
|
-
|
|
112056
|
-
|
|
112057
|
-
|
|
112058
|
-
|
|
112059
|
-
|
|
112060
|
-
|
|
112061
|
-
|
|
112062
|
-
reasonCode: "deploy_queued"
|
|
112063
|
-
}),
|
|
112064
|
-
summary: `${app.name} deploy ${deployment.id} queued — resume with percher_wait_for_deploy.`,
|
|
112531
|
+
status: "needs_login",
|
|
112532
|
+
fileCount: tarball.fileCount,
|
|
112533
|
+
bytes: tarball.bytes,
|
|
112534
|
+
error: {
|
|
112535
|
+
title: "Anonymous publish unavailable",
|
|
112536
|
+
explanation: detail,
|
|
112537
|
+
suggestion: "Run the percher_login tool to publish with an account instead."
|
|
112538
|
+
},
|
|
112539
|
+
recovery: RECOVERY_NEEDS_LOGIN,
|
|
112540
|
+
summary: `${detail} Log in to publish instead.`,
|
|
112065
112541
|
configPath: tomlPathFor2(ctx.cwd),
|
|
112066
|
-
bundle: { fileCount:
|
|
112542
|
+
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
|
|
112067
112543
|
};
|
|
112068
112544
|
}
|
|
112069
|
-
|
|
112070
|
-
|
|
112071
|
-
|
|
112072
|
-
|
|
112073
|
-
|
|
112074
|
-
|
|
112075
|
-
app,
|
|
112076
|
-
deployment,
|
|
112077
|
-
fileCount: freshTarball.fileCount,
|
|
112078
|
-
bytes: freshTarball.bytes,
|
|
112079
|
-
error: {
|
|
112080
|
-
title: "Deploy timed out",
|
|
112081
|
-
explanation: "The deployment did not complete within 5 minutes.",
|
|
112082
|
-
suggestion: `Run \`percher doctor --app ${app.name}\` (CLI) or the percher_doctor tool (MCP) to diagnose the stall. Doctor will read deploy state + build log and return a concrete next step.`
|
|
112083
|
-
},
|
|
112084
|
-
recovery: recoveryDoctor({
|
|
112085
|
-
app: app.name,
|
|
112086
|
-
deployId: deployment.id,
|
|
112087
|
-
mode: "deploy",
|
|
112088
|
-
reasonCode: "deploy_stalled"
|
|
112089
|
-
}),
|
|
112090
|
-
summary: `Deploy timed out after 5 minutes (${deployment.id}).`,
|
|
112091
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
112092
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
112093
|
-
};
|
|
112545
|
+
saveConfig({
|
|
112546
|
+
token: created.token,
|
|
112547
|
+
anonClaim: {
|
|
112548
|
+
app: created.app.name,
|
|
112549
|
+
claimToken: created.claimToken,
|
|
112550
|
+
expiresAt: created.expiresAt
|
|
112094
112551
|
}
|
|
112095
|
-
|
|
112096
|
-
|
|
112097
|
-
|
|
112098
|
-
|
|
112099
|
-
|
|
112100
|
-
|
|
112101
|
-
|
|
112102
|
-
|
|
112103
|
-
|
|
112104
|
-
|
|
112105
|
-
|
|
112106
|
-
|
|
112107
|
-
}
|
|
112108
|
-
|
|
112109
|
-
|
|
112110
|
-
|
|
112111
|
-
|
|
112112
|
-
deployment,
|
|
112113
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
112114
|
-
input
|
|
112115
|
-
});
|
|
112116
|
-
}
|
|
112117
|
-
const totalSeconds = (Date.now() - start) / 1000;
|
|
112118
|
-
const url2 = deployment.previewUrl ?? deployment.url ?? app.url;
|
|
112119
|
-
ctx.status(`Live at ${url2} (${totalSeconds.toFixed(0)}s)`);
|
|
112120
|
-
return {
|
|
112121
|
-
status: "live",
|
|
112122
|
-
url: url2,
|
|
112552
|
+
});
|
|
112553
|
+
const newClient = new PercherClient({
|
|
112554
|
+
apiUrl: ctx.client.apiUrl,
|
|
112555
|
+
token: created.token,
|
|
112556
|
+
sessionId: ctx.client.sessionId
|
|
112557
|
+
});
|
|
112558
|
+
ctx.client = newClient;
|
|
112559
|
+
ctx.setClient?.(newClient);
|
|
112560
|
+
config2.app.name = created.app.name;
|
|
112561
|
+
const t0 = Date.now();
|
|
112562
|
+
const freshTarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
112563
|
+
const packageMs = Date.now() - t0;
|
|
112564
|
+
const { app, firstDeploy } = await ensureApp2(ctx, config2);
|
|
112565
|
+
const result = await executeDeploy({
|
|
112566
|
+
ctx,
|
|
112567
|
+
config: config2,
|
|
112568
|
+
input,
|
|
112123
112569
|
app,
|
|
112124
|
-
|
|
112125
|
-
|
|
112126
|
-
|
|
112127
|
-
|
|
112128
|
-
|
|
112129
|
-
|
|
112570
|
+
firstDeploy,
|
|
112571
|
+
tarball: freshTarball,
|
|
112572
|
+
t0,
|
|
112573
|
+
packageMs,
|
|
112574
|
+
missingBuildEnvKeys: []
|
|
112575
|
+
});
|
|
112576
|
+
return {
|
|
112577
|
+
...result,
|
|
112578
|
+
anonymous: true,
|
|
112579
|
+
claim: {
|
|
112580
|
+
claimUrl: created.claimUrl,
|
|
112581
|
+
claimToken: created.claimToken,
|
|
112582
|
+
expiresAt: created.expiresAt
|
|
112130
112583
|
},
|
|
112131
|
-
|
|
112132
|
-
|
|
112133
|
-
|
|
112134
|
-
|
|
112135
|
-
|
|
112136
|
-
cacheHit: deployment.cacheHit,
|
|
112137
|
-
traceId: deployment.id,
|
|
112138
|
-
summary: buildLiveSummary({
|
|
112139
|
-
appName: app.name,
|
|
112140
|
-
url: url2 ?? app.url,
|
|
112141
|
-
totalSeconds: Math.round(totalSeconds),
|
|
112142
|
-
framework: config2.app.framework,
|
|
112143
|
-
preview: input.preview === true,
|
|
112144
|
-
replacedPreview,
|
|
112145
|
-
cacheHit: deployment.cacheHit ?? false
|
|
112146
|
-
}),
|
|
112147
|
-
configPath: tomlPathFor2(ctx.cwd),
|
|
112148
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
|
|
112584
|
+
warnings: [
|
|
112585
|
+
...result.warnings ?? [],
|
|
112586
|
+
`Anonymous app: expires ${created.expiresAt} unless claimed — run percher_claim_app (claim token is in claim.claimToken).`,
|
|
112587
|
+
"No outbound network, no database, and no env vars until claimed."
|
|
112588
|
+
]
|
|
112149
112589
|
};
|
|
112150
112590
|
}
|
|
112151
112591
|
async function loadOrGenerate(ctx) {
|
|
@@ -112207,7 +112647,6 @@ var pushInputSchema = exports_external.object({
|
|
|
112207
112647
|
preview: exports_external.boolean().optional(),
|
|
112208
112648
|
noCache: exports_external.boolean().optional().describe("Skip the image-cache lookup and force a fresh build.")
|
|
112209
112649
|
});
|
|
112210
|
-
var MAX_BYTES2 = 500 * 1024 * 1024;
|
|
112211
112650
|
var SUSPICIOUS_BYTES2 = 50 * 1024 * 1024;
|
|
112212
112651
|
var SUSPICIOUS_FILES2 = 1e4;
|
|
112213
112652
|
var PLACEHOLDER_PREFIXES = ["add-your-", "your-key-here"];
|
|
@@ -112281,7 +112720,7 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
112281
112720
|
await checkPlaceholders(ctx, config2);
|
|
112282
112721
|
await checkByoSecrets(ctx, config2);
|
|
112283
112722
|
const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
|
|
112284
|
-
if (tarball.bytes >
|
|
112723
|
+
if (tarball.bytes > MAX_TARBALL_BYTES) {
|
|
112285
112724
|
throw new PercherCoreError(`Repository is larger than 500 MB (current: ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB)`, {
|
|
112286
112725
|
code: "push.too-large",
|
|
112287
112726
|
hint: `Biggest files: ${tarball.topFiles.map((f) => f.path).join(", ")}`,
|
|
@@ -112436,10 +112875,10 @@ async function push(ctx, input = {}, internals = {}) {
|
|
|
112436
112875
|
const resetAt = extra.resetAt ?? "";
|
|
112437
112876
|
throw new PercherCoreError(`Daily ${kind}-deploy quota reached (${used}/${limit}). Resets at ${resetAt}.`, {
|
|
112438
112877
|
code: "push.quota-exceeded",
|
|
112439
|
-
hint: `Wait until ${resetAt} or upgrade your plan at https://percher.app/
|
|
112878
|
+
hint: `Wait until ${resetAt} or upgrade your plan at https://percher.app/account/billing.`,
|
|
112440
112879
|
recovery: {
|
|
112441
112880
|
...recoveryAsk({
|
|
112442
|
-
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then or upgrade at https://percher.app/
|
|
112881
|
+
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then or upgrade at https://percher.app/account/billing.`,
|
|
112443
112882
|
reasonCode: "quota_exceeded"
|
|
112444
112883
|
})
|
|
112445
112884
|
}
|
|
@@ -112593,10 +113032,10 @@ async function redeploy(ctx, input = {}) {
|
|
|
112593
113032
|
error: {
|
|
112594
113033
|
title: "Daily deploy quota reached",
|
|
112595
113034
|
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
112596
|
-
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/
|
|
113035
|
+
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/account/billing to raise this cap.`
|
|
112597
113036
|
},
|
|
112598
113037
|
recovery: recoveryAsk({
|
|
112599
|
-
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then, deploy a ${otherKind} instead, or upgrade at https://percher.app/
|
|
113038
|
+
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then, deploy a ${otherKind} instead, or upgrade at https://percher.app/account/billing.`,
|
|
112600
113039
|
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
112601
113040
|
reasonCode: "quota_exceeded"
|
|
112602
113041
|
}),
|
|
@@ -112609,7 +113048,7 @@ async function redeploy(ctx, input = {}) {
|
|
|
112609
113048
|
return failureResult({
|
|
112610
113049
|
title: "Deploy rate limit reached",
|
|
112611
113050
|
explanation: err.message,
|
|
112612
|
-
suggestion: "Wait a minute or two and try again. Hourly limits scale with your plan; upgrade at https://percher.app/
|
|
113051
|
+
suggestion: "Wait a minute or two and try again. Hourly limits scale with your plan; upgrade at https://percher.app/account/billing to raise it."
|
|
112613
113052
|
});
|
|
112614
113053
|
case "DEPLOY_RATE_LIMITED": {
|
|
112615
113054
|
const extra = err.extra ?? {};
|
|
@@ -112836,6 +113275,8 @@ function failureResult(error48) {
|
|
|
112836
113275
|
};
|
|
112837
113276
|
}
|
|
112838
113277
|
// ../core/dist/commands/rename.js
|
|
113278
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
|
|
113279
|
+
import { join as join22 } from "node:path";
|
|
112839
113280
|
var renameInputSchema = exports_external.object({
|
|
112840
113281
|
app: exports_external.string().optional(),
|
|
112841
113282
|
name: exports_external.string().min(3).max(40)
|
|
@@ -112848,12 +113289,33 @@ async function rename(ctx, input) {
|
|
|
112848
113289
|
hint: "Pass an app name or run from a directory with percher.toml."
|
|
112849
113290
|
});
|
|
112850
113291
|
}
|
|
112851
|
-
|
|
113292
|
+
const result = await ctx.client.apps.rename(appRef, input.name);
|
|
113293
|
+
return {
|
|
113294
|
+
...result,
|
|
113295
|
+
tomlUpdated: updateLocalTomlName(ctx.cwd, result.previousName, result.name)
|
|
113296
|
+
};
|
|
113297
|
+
}
|
|
113298
|
+
function updateLocalTomlName(cwd, oldName, newName) {
|
|
113299
|
+
const p = join22(cwd, "percher.toml");
|
|
113300
|
+
if (!existsSync16(p))
|
|
113301
|
+
return false;
|
|
113302
|
+
try {
|
|
113303
|
+
const src = readFileSync12(p, "utf8");
|
|
113304
|
+
if (parse5(src).app.name !== oldName)
|
|
113305
|
+
return false;
|
|
113306
|
+
const next = src.replace(new RegExp(`(name\\s*=\\s*")${oldName}(")`), `$1${newName}$2`);
|
|
113307
|
+
if (next === src || parse5(next).app.name !== newName)
|
|
113308
|
+
return false;
|
|
113309
|
+
writeFileSync8(p, next);
|
|
113310
|
+
return true;
|
|
113311
|
+
} catch {
|
|
113312
|
+
return false;
|
|
113313
|
+
}
|
|
112852
113314
|
}
|
|
112853
113315
|
// ../core/dist/commands/reproduce.js
|
|
112854
|
-
import { mkdtempSync, writeFileSync as
|
|
113316
|
+
import { mkdtempSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
112855
113317
|
import { tmpdir } from "node:os";
|
|
112856
|
-
import { join as
|
|
113318
|
+
import { join as join23 } from "node:path";
|
|
112857
113319
|
var reproduceInputSchema = exports_external.object({
|
|
112858
113320
|
app: exports_external.string().optional().describe("App name. Defaults to the [app].name in the current directory's percher.toml."),
|
|
112859
113321
|
deployId: exports_external.string().describe("Deploy id (e.g. dep_abc123) to reproduce. Required — usually the failed deploy you want to debug.")
|
|
@@ -112937,9 +113399,9 @@ async function reproduce(ctx, input) {
|
|
|
112937
113399
|
const envMap = await ctx.client.env.list(app.id);
|
|
112938
113400
|
currentAppEnvKeys = Object.keys(envMap).sort();
|
|
112939
113401
|
} catch {}
|
|
112940
|
-
const tmpDir = mkdtempSync(
|
|
112941
|
-
const tarballPath =
|
|
112942
|
-
|
|
113402
|
+
const tmpDir = mkdtempSync(join23(tmpdir(), `percher-reproduce-${input.deployId}-`));
|
|
113403
|
+
const tarballPath = join23(tmpDir, "source.tar.gz");
|
|
113404
|
+
writeFileSync9(tarballPath, bytes);
|
|
112943
113405
|
let extracted = false;
|
|
112944
113406
|
try {
|
|
112945
113407
|
const proc = Bun.spawn(["tar", "-xzf", tarballPath, "-C", tmpDir], {
|
|
@@ -113177,7 +113639,7 @@ async function rollback(ctx, input, internals = {}) {
|
|
|
113177
113639
|
}
|
|
113178
113640
|
// ../core/dist/commands/sharing.js
|
|
113179
113641
|
var ShareRoleSchema = exports_external.enum(["viewer", "admin"]);
|
|
113180
|
-
function
|
|
113642
|
+
function resolveApp4(ctx, app) {
|
|
113181
113643
|
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
113182
113644
|
if (!name) {
|
|
113183
113645
|
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
@@ -113205,19 +113667,19 @@ var shareRevokeInputSchema = exports_external.object({
|
|
|
113205
113667
|
shareId: exports_external.string().min(1, "shareId is required")
|
|
113206
113668
|
});
|
|
113207
113669
|
async function shareList(ctx, input = {}) {
|
|
113208
|
-
const appRef =
|
|
113670
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113209
113671
|
return ctx.client.sharing.list(appRef);
|
|
113210
113672
|
}
|
|
113211
113673
|
async function shareInvite(ctx, input) {
|
|
113212
|
-
const appRef =
|
|
113674
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113213
113675
|
return ctx.client.sharing.create(appRef, { email: input.email, role: input.role });
|
|
113214
113676
|
}
|
|
113215
113677
|
async function shareSetRole(ctx, input) {
|
|
113216
|
-
const appRef =
|
|
113678
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113217
113679
|
return ctx.client.sharing.setRole(appRef, input.shareId, input.role);
|
|
113218
113680
|
}
|
|
113219
113681
|
async function shareRevoke(ctx, input) {
|
|
113220
|
-
const appRef =
|
|
113682
|
+
const appRef = resolveApp4(ctx, input.app);
|
|
113221
113683
|
await ctx.client.sharing.revoke(appRef, input.shareId);
|
|
113222
113684
|
}
|
|
113223
113685
|
var invitesListInputSchema = exports_external.object({});
|
|
@@ -113283,7 +113745,7 @@ function summarize2(verdict, health) {
|
|
|
113283
113745
|
return "API responded but reported a non-ok status";
|
|
113284
113746
|
}
|
|
113285
113747
|
// ../core/dist/commands/transfers.js
|
|
113286
|
-
function
|
|
113748
|
+
function resolveApp5(ctx, app) {
|
|
113287
113749
|
const name = app ?? readPercherTomlAppName(ctx.cwd);
|
|
113288
113750
|
if (!name) {
|
|
113289
113751
|
throw new PercherCoreError("No app specified and no percher.toml found", {
|
|
@@ -113305,15 +113767,15 @@ var transferListForAppInputSchema = exports_external.object({
|
|
|
113305
113767
|
app: exports_external.string().optional()
|
|
113306
113768
|
});
|
|
113307
113769
|
async function transferInitiate(ctx, input) {
|
|
113308
|
-
const appRef =
|
|
113770
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
113309
113771
|
return ctx.client.transfers.initiate(appRef, { email: input.email });
|
|
113310
113772
|
}
|
|
113311
113773
|
async function transferCancel(ctx, input) {
|
|
113312
|
-
const appRef =
|
|
113774
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
113313
113775
|
await ctx.client.transfers.cancel(appRef, input.transferId);
|
|
113314
113776
|
}
|
|
113315
113777
|
async function transferListForApp(ctx, input = {}) {
|
|
113316
|
-
const appRef =
|
|
113778
|
+
const appRef = resolveApp5(ctx, input.app);
|
|
113317
113779
|
return ctx.client.transfers.listForApp(appRef);
|
|
113318
113780
|
}
|
|
113319
113781
|
var transfersIncomingInputSchema = exports_external.object({});
|
|
@@ -113508,9 +113970,43 @@ var PLAN_CATALOG = {
|
|
|
113508
113970
|
vcpuPool: 4,
|
|
113509
113971
|
diskPoolMb: 75000
|
|
113510
113972
|
}
|
|
113973
|
+
},
|
|
113974
|
+
anonymous: {
|
|
113975
|
+
label: "Anonymous",
|
|
113976
|
+
priceEur: 0,
|
|
113977
|
+
hidden: true,
|
|
113978
|
+
limits: {
|
|
113979
|
+
apps: 1,
|
|
113980
|
+
deploysPerMonth: 20,
|
|
113981
|
+
deploysPerHour: 6,
|
|
113982
|
+
memory: "128mb",
|
|
113983
|
+
cpu: 0.25,
|
|
113984
|
+
pids: 64,
|
|
113985
|
+
activePreviewsPerApp: 0,
|
|
113986
|
+
customDomainsPerApp: 0,
|
|
113987
|
+
removesPercherBadge: false,
|
|
113988
|
+
autoSleep: true,
|
|
113989
|
+
inactivityWarningDays: 1,
|
|
113990
|
+
inactivityGraceDays: 1,
|
|
113991
|
+
deployRetention: 2,
|
|
113992
|
+
logRetentionDays: 1,
|
|
113993
|
+
buildLogRetentionDays: 1,
|
|
113994
|
+
cronJobs: 0,
|
|
113995
|
+
storageMb: 0,
|
|
113996
|
+
instances: 1,
|
|
113997
|
+
kvStorageMb: 0,
|
|
113998
|
+
uptimeIntervalSec: 0,
|
|
113999
|
+
dailyLiveDeploys: 10,
|
|
114000
|
+
dailyPreviewDeploys: 0,
|
|
114001
|
+
userConcurrentDeploys: 1,
|
|
114002
|
+
ramPoolMb: 128,
|
|
114003
|
+
vcpuPool: 0.25,
|
|
114004
|
+
diskPoolMb: 200
|
|
114005
|
+
}
|
|
113511
114006
|
}
|
|
113512
114007
|
};
|
|
113513
|
-
var
|
|
114008
|
+
var ALL_PLAN_IDS = Object.keys(PLAN_CATALOG);
|
|
114009
|
+
var PLAN_IDS = Object.entries(PLAN_CATALOG).filter(([, info]) => !info.hidden).map(([id]) => id);
|
|
113514
114010
|
|
|
113515
114011
|
// ../core/dist/index.js
|
|
113516
114012
|
init_qr();
|
|
@@ -114139,7 +114635,16 @@ async function handlePublishResult(ctx, result, options = {}) {
|
|
|
114139
114635
|
await writeLine(ctx, qr);
|
|
114140
114636
|
} catch {}
|
|
114141
114637
|
}
|
|
114142
|
-
if (result.
|
|
114638
|
+
if (result.anonymous && result.claim) {
|
|
114639
|
+
await writeLine(ctx, "");
|
|
114640
|
+
await writeLine(ctx, import_picocolors4.default.bold("Anonymous app — claim it to keep it:"));
|
|
114641
|
+
await writeLine(ctx, ` Expires: ${new Date(result.claim.expiresAt).toLocaleString()}`);
|
|
114642
|
+
await writeLine(ctx, ` Claim token: ${result.claim.claimToken}`);
|
|
114643
|
+
await writeLine(ctx, ` ${import_picocolors4.default.cyan("percher claim")} claim into your account (lifts the limits on next deploy)`);
|
|
114644
|
+
for (const w of result.warnings ?? []) {
|
|
114645
|
+
await writeLine(ctx, import_picocolors4.default.dim(` ${w}`));
|
|
114646
|
+
}
|
|
114647
|
+
} else if (result.firstDeploy) {
|
|
114143
114648
|
await writeLine(ctx, "");
|
|
114144
114649
|
await writeLine(ctx, import_picocolors4.default.bold("First deploy — next steps:"));
|
|
114145
114650
|
await writeLine(ctx, ` ${import_picocolors4.default.cyan("percher dev")} watch for changes and re-deploy`);
|
|
@@ -114164,7 +114669,7 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114164
114669
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} wrote ${file2}`);
|
|
114165
114670
|
}
|
|
114166
114671
|
});
|
|
114167
|
-
cli.command("publish", "Publish this project to Percher (recommended deploy command)").option("--force", "Skip size warnings").option("--preview", "Deploy as preview (does not replace the live version)").option("-m, --message <msg>", "Deploy note (visible in deploy history)").option("--dry-run", "Show what would be deployed (file count, bytes) without uploading").option("--no-cache", "Skip image-cache lookup and force a fresh build (use when you suspect cached image is stale)").action(async (options) => {
|
|
114672
|
+
cli.command("publish", "Publish this project to Percher (recommended deploy command)").option("--force", "Skip size warnings").option("--preview", "Deploy as preview (does not replace the live version)").option("-m, --message <msg>", "Deploy note (visible in deploy history)").option("--dry-run", "Show what would be deployed (file count, bytes) without uploading").option("--no-cache", "Skip image-cache lookup and force a fresh build (use when you suspect cached image is stale)").option("--anonymous", "Publish without an account — a 72h sandbox you can claim later (where enabled)").action(async (options) => {
|
|
114168
114673
|
const { detectCiBranchMessage: detectCiBranchMessage2 } = await Promise.resolve().then(() => exports_ci_env);
|
|
114169
114674
|
let messageOverride = options.message;
|
|
114170
114675
|
if (options.preview && !options.message) {
|
|
@@ -114179,7 +114684,8 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114179
114684
|
preview: options.preview,
|
|
114180
114685
|
message: messageOverride,
|
|
114181
114686
|
dryRun: options.dryRun,
|
|
114182
|
-
noCache: options.cache === false
|
|
114687
|
+
noCache: options.cache === false,
|
|
114688
|
+
anonymous: options.anonymous
|
|
114183
114689
|
});
|
|
114184
114690
|
await handlePublishResult(ctx, result, { preview: options.preview });
|
|
114185
114691
|
});
|
|
@@ -114215,6 +114721,67 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114215
114721
|
const timing = result.timing ? ` (${result.timing.totalSeconds.toFixed(0)}s)` : "";
|
|
114216
114722
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} Redeployed to ${result.url}${timing}`);
|
|
114217
114723
|
});
|
|
114724
|
+
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) => {
|
|
114725
|
+
const appName = options.app ?? readPercherTomlAppName(ctx.cwd);
|
|
114726
|
+
if (!appName) {
|
|
114727
|
+
await writeLine(ctx, `${import_picocolors4.default.red("✗")} No app specified — pass --app <name> or run from a directory with percher.toml.`);
|
|
114728
|
+
throw new SilentExitError(1);
|
|
114729
|
+
}
|
|
114730
|
+
const timeoutSec = Number.parseInt(options.timeout ?? "600", 10);
|
|
114731
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) {
|
|
114732
|
+
await writeLine(ctx, `${import_picocolors4.default.red("✗")} --timeout must be a positive number of seconds.`);
|
|
114733
|
+
throw new SilentExitError(1);
|
|
114734
|
+
}
|
|
114735
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
114736
|
+
let lastStatus = "";
|
|
114737
|
+
for (;; ) {
|
|
114738
|
+
const remainingMs = deadline - Date.now();
|
|
114739
|
+
if (remainingMs <= 0) {
|
|
114740
|
+
await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
|
|
114741
|
+
throw new SilentExitError(1);
|
|
114742
|
+
}
|
|
114743
|
+
let raceTimer;
|
|
114744
|
+
let result = null;
|
|
114745
|
+
const callController = new AbortController;
|
|
114746
|
+
try {
|
|
114747
|
+
result = await Promise.race([
|
|
114748
|
+
waitForDeploy(ctx, {
|
|
114749
|
+
app: appName,
|
|
114750
|
+
deployId,
|
|
114751
|
+
timeoutSeconds: Math.min(Math.ceil(remainingMs / 1000), 30)
|
|
114752
|
+
}, { signal: callController.signal }),
|
|
114753
|
+
new Promise((resolve7) => {
|
|
114754
|
+
raceTimer = setTimeout(() => resolve7(null), remainingMs);
|
|
114755
|
+
})
|
|
114756
|
+
]);
|
|
114757
|
+
} finally {
|
|
114758
|
+
clearTimeout(raceTimer);
|
|
114759
|
+
}
|
|
114760
|
+
if (result === null) {
|
|
114761
|
+
callController.abort();
|
|
114762
|
+
await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
|
|
114763
|
+
throw new SilentExitError(1);
|
|
114764
|
+
}
|
|
114765
|
+
if (result.deployment.status !== lastStatus) {
|
|
114766
|
+
lastStatus = result.deployment.status;
|
|
114767
|
+
await writeLine(ctx, import_picocolors4.default.dim(`${lastStatus}...`));
|
|
114768
|
+
}
|
|
114769
|
+
if (result.status === "live") {
|
|
114770
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
|
|
114771
|
+
return;
|
|
114772
|
+
}
|
|
114773
|
+
if (result.status === "failed" || result.status === "replaced") {
|
|
114774
|
+
const mark = result.status === "failed" ? import_picocolors4.default.red("✗") : import_picocolors4.default.yellow("!");
|
|
114775
|
+
await writeLine(ctx, `${mark} ${result.summary}`);
|
|
114776
|
+
if (result.error?.suggestion) {
|
|
114777
|
+
await writeLine(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
|
|
114778
|
+
}
|
|
114779
|
+
if (result.status === "failed")
|
|
114780
|
+
throw new SilentExitError(1);
|
|
114781
|
+
return;
|
|
114782
|
+
}
|
|
114783
|
+
}
|
|
114784
|
+
});
|
|
114218
114785
|
cli.command("push", "Package and deploy this project").option("--dry-run", "Show what would be deployed, don't upload").option("--force", "Skip size warnings").option("--preview", "Deploy as preview (does not replace the live version)").option("--no-cache", "Skip image-cache lookup and force a fresh build (use when you suspect cached image is stale)").action(async (options) => {
|
|
114219
114786
|
const result = await push(ctx, {
|
|
114220
114787
|
dryRun: options.dryRun,
|
|
@@ -114245,6 +114812,11 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114245
114812
|
const result = await rename(ctx, { app, name });
|
|
114246
114813
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} Renamed to ${result.name} — now live at ${result.url}`);
|
|
114247
114814
|
await writeLine(ctx, ` Previous name: ${result.previousName} (reserved for 60 days)`);
|
|
114815
|
+
if (result.tomlUpdated) {
|
|
114816
|
+
await writeLine(ctx, ` percher.toml updated: name = "${result.name}"`);
|
|
114817
|
+
} else {
|
|
114818
|
+
await writeLine(ctx, ` ${import_picocolors4.default.yellow("!")} Update percher.toml before the next publish: name = "${result.name}"`);
|
|
114819
|
+
}
|
|
114248
114820
|
await writeLine(ctx, ` ${import_picocolors4.default.dim(`Next rename available in ${result.cooldownDays} days`)}`);
|
|
114249
114821
|
});
|
|
114250
114822
|
cli.command("open [app]", "Open the app URL in your browser").action(async (app) => {
|
|
@@ -114306,6 +114878,32 @@ function registerAppsCommands(cli, ctx) {
|
|
|
114306
114878
|
const result = await resume(ctx, { app });
|
|
114307
114879
|
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
|
|
114308
114880
|
});
|
|
114881
|
+
cli.command("claim", "Claim an anonymous app into your account").option("--app <name>", "App name (defaults to the persisted anon state or percher.toml)").option("--claim-token <token>", "Claim token from the anonymous publish").action(async (options) => {
|
|
114882
|
+
const result = await claim(ctx, { app: options.app, claimToken: options.claimToken });
|
|
114883
|
+
if (result.status === "claimed") {
|
|
114884
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
|
|
114885
|
+
} else {
|
|
114886
|
+
await writeLine(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
|
|
114887
|
+
}
|
|
114888
|
+
});
|
|
114889
|
+
cli.command("inspect-link [app]", "Create a public read-only inspect link (or --revoke it)").option("--app <name>", "App name (defaults to percher.toml)").option("--revoke", "Revoke the app's inspect link instead of creating one").action(async (app, options) => {
|
|
114890
|
+
const result = await inspectLink(ctx, {
|
|
114891
|
+
app: app ?? options.app,
|
|
114892
|
+
revoke: options.revoke
|
|
114893
|
+
});
|
|
114894
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
|
|
114895
|
+
});
|
|
114896
|
+
cli.command("cache <action> [app]", "Manage the app's build cache (reset)").action(async (action, app) => {
|
|
114897
|
+
if (action !== "reset") {
|
|
114898
|
+
throw new Error(`Unknown cache action "${action}". Use: percher cache reset [app]`);
|
|
114899
|
+
}
|
|
114900
|
+
const result = await cacheReset(ctx, { app });
|
|
114901
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
|
|
114902
|
+
});
|
|
114903
|
+
cli.command("cache reset [app]", "Reset the app's build cache (next deploy runs cold)").action(async (app) => {
|
|
114904
|
+
const result = await cacheReset(ctx, { app });
|
|
114905
|
+
await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
|
|
114906
|
+
});
|
|
114309
114907
|
cli.command("doctor [app]", "Check app health and connectivity").option("--json", "Output as JSON").action(async (app, options) => {
|
|
114310
114908
|
const result = await doctor(ctx, { app, json: options.json });
|
|
114311
114909
|
if (options.json) {
|
|
@@ -114586,24 +115184,60 @@ function registerDeploysCommands(cli, ctx, argv) {
|
|
|
114586
115184
|
await writeLine(ctx, `${v.sha} ${v.timestamp} ${message}`);
|
|
114587
115185
|
}
|
|
114588
115186
|
});
|
|
114589
|
-
cli.command("preview <action> [
|
|
115187
|
+
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) => {
|
|
115188
|
+
if (action === "branch") {
|
|
115189
|
+
await runPreviewBranch(arg, arg2, options.app);
|
|
115190
|
+
return;
|
|
115191
|
+
}
|
|
114590
115192
|
const appRef = options.app ?? readPercherTomlAppName(ctx.cwd);
|
|
114591
|
-
if (!appRef)
|
|
115193
|
+
if (!appRef) {
|
|
114592
115194
|
throw new Error("App name required. Use --app or run from a project directory.");
|
|
115195
|
+
}
|
|
114593
115196
|
if (action === "promote") {
|
|
114594
|
-
if (!
|
|
115197
|
+
if (!arg)
|
|
114595
115198
|
throw new Error("Deploy ID required. Usage: percher preview promote <id>");
|
|
114596
|
-
const result = await ctx.client.apps.promotePreview(appRef,
|
|
115199
|
+
const result = await ctx.client.apps.promotePreview(appRef, arg);
|
|
114597
115200
|
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview promoted to live. Status: ${result.status}`);
|
|
114598
115201
|
} else if (action === "discard") {
|
|
114599
|
-
if (!
|
|
115202
|
+
if (!arg)
|
|
114600
115203
|
throw new Error("Deploy ID required. Usage: percher preview discard <id>");
|
|
114601
|
-
await ctx.client.apps.discardPreview(appRef,
|
|
115204
|
+
await ctx.client.apps.discardPreview(appRef, arg);
|
|
114602
115205
|
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview discarded.`);
|
|
114603
115206
|
} else {
|
|
114604
|
-
throw new Error(`Unknown preview action "${action}". Use promote | discard.`);
|
|
115207
|
+
throw new Error(`Unknown preview action "${action}". Use promote | discard | branch.`);
|
|
114605
115208
|
}
|
|
114606
115209
|
});
|
|
115210
|
+
async function runPreviewBranch(sub, branch, app) {
|
|
115211
|
+
if (sub === "set") {
|
|
115212
|
+
if (!branch)
|
|
115213
|
+
throw new Error("Branch required. Usage: percher preview branch set <branch>");
|
|
115214
|
+
const r = await previewBranchSet(ctx, { app, branch });
|
|
115215
|
+
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview branch set to "${branch}".`);
|
|
115216
|
+
if (r.previewUrl)
|
|
115217
|
+
await writeLine(ctx, ` Your preview address: ${import_picocolors7.default.bold(r.previewUrl)}`);
|
|
115218
|
+
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."));
|
|
115219
|
+
return;
|
|
115220
|
+
}
|
|
115221
|
+
if (sub === "unset") {
|
|
115222
|
+
await previewBranchUnset(ctx, { app });
|
|
115223
|
+
await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview branch cleared.`);
|
|
115224
|
+
return;
|
|
115225
|
+
}
|
|
115226
|
+
if (sub === "show" || sub === undefined) {
|
|
115227
|
+
const s = await previewBranchShow(ctx, { app });
|
|
115228
|
+
if (!s.previewBranch) {
|
|
115229
|
+
await writeLine(ctx, "No preview branch set.");
|
|
115230
|
+
await writeLine(ctx, import_picocolors7.default.dim(" Set one: percher preview branch set <branch>"));
|
|
115231
|
+
return;
|
|
115232
|
+
}
|
|
115233
|
+
await writeLine(ctx, `Preview branch: ${import_picocolors7.default.bold(s.previewBranch)}`);
|
|
115234
|
+
if (s.previewUrl)
|
|
115235
|
+
await writeLine(ctx, ` Address: ${s.previewUrl}`);
|
|
115236
|
+
await writeLine(ctx, ` Status: ${s.status}`);
|
|
115237
|
+
return;
|
|
115238
|
+
}
|
|
115239
|
+
throw new Error(`Unknown preview branch action "${sub}". Use set <branch> | unset | show.`);
|
|
115240
|
+
}
|
|
114607
115241
|
}
|
|
114608
115242
|
|
|
114609
115243
|
// src/commands/env.ts
|
|
@@ -115198,7 +115832,7 @@ async function renderError(stderr, error48) {
|
|
|
115198
115832
|
// package.json
|
|
115199
115833
|
var package_default = {
|
|
115200
115834
|
name: "percher",
|
|
115201
|
-
version: "0.
|
|
115835
|
+
version: "0.5.1",
|
|
115202
115836
|
description: "CLI for the Percher hosting platform — publish apps with one command",
|
|
115203
115837
|
type: "module",
|
|
115204
115838
|
bin: {
|
|
@@ -115222,9 +115856,9 @@ var package_default = {
|
|
|
115222
115856
|
typecheck: "tsc --noEmit"
|
|
115223
115857
|
},
|
|
115224
115858
|
dependencies: {
|
|
115225
|
-
"@percher/client": "^0.2.
|
|
115226
|
-
"@percher/core": "^0.4.
|
|
115227
|
-
"@percher/toml": "^0.1.
|
|
115859
|
+
"@percher/client": "^0.2.10",
|
|
115860
|
+
"@percher/core": "^0.4.4",
|
|
115861
|
+
"@percher/toml": "^0.1.5",
|
|
115228
115862
|
cac: "^7.0.0",
|
|
115229
115863
|
picocolors: "^1.1.1"
|
|
115230
115864
|
},
|
|
@@ -115279,18 +115913,18 @@ async function showTelemetryNotice() {
|
|
|
115279
115913
|
if (isDisabled())
|
|
115280
115914
|
return;
|
|
115281
115915
|
const { homedir: homedir2 } = await import("node:os");
|
|
115282
|
-
const { existsSync:
|
|
115283
|
-
const { join:
|
|
115284
|
-
const configDir =
|
|
115285
|
-
const markerFile =
|
|
115286
|
-
if (
|
|
115916
|
+
const { existsSync: existsSync17, mkdirSync: mkdirSync6, writeFileSync: writeFileSync10 } = await import("node:fs");
|
|
115917
|
+
const { join: join24 } = await import("node:path");
|
|
115918
|
+
const configDir = join24(homedir2(), ".percher");
|
|
115919
|
+
const markerFile = join24(configDir, ".telemetry-notice-shown");
|
|
115920
|
+
if (existsSync17(markerFile))
|
|
115287
115921
|
return;
|
|
115288
115922
|
console.log("Percher sends non-account-linked usage stats (command, result, error type, OS, Bun version).");
|
|
115289
115923
|
console.log(`Disable with PERCHER_NO_TELEMETRY=1
|
|
115290
115924
|
`);
|
|
115291
115925
|
try {
|
|
115292
115926
|
mkdirSync6(configDir, { recursive: true });
|
|
115293
|
-
|
|
115927
|
+
writeFileSync10(markerFile, new Date().toISOString());
|
|
115294
115928
|
} catch {}
|
|
115295
115929
|
}
|
|
115296
115930
|
|