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.
@@ -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
- // ../toml/dist/schema.js
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: exports_external.record(exports_external.string(), cronSchema).optional(),
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/create.js
104904
- import { join as join6, resolve as resolve4 } from "node:path";
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
- var DEFAULT_BASE = "https://percher.app";
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: "dashboard.no-app",
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
- const tab = input.tab ?? "overview";
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/data.js
105040
- var dataInputSchema = exports_external.object({
105041
- app: exports_external.string().optional(),
105042
- json: exports_external.boolean().optional()
105043
- });
105044
- async function data(ctx, input = {}) {
105045
- const name = input.app ?? readPercherTomlAppName(ctx.cwd);
105046
- if (!name) {
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
- return {
105070
- mode: status.mode,
105071
- status: status.status,
105072
- version: status.version ?? null,
105073
- collections,
105074
- stats,
105075
- adminUrl: adminLink.adminUrl ?? null,
105076
- warning
105077
- };
105078
- }
105079
- // ../core/dist/commands/data-export.js
105080
- import { resolve as resolve5 } from "node:path";
105081
- var dataExportInputSchema = exports_external.object({
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 existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync4 } from "node:fs";
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 join7 } from "node:path";
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 join7(xdg, "percher", "config.json");
105152
- return join7(home(), ".percher", "config.json");
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 (!existsSync4(p))
105018
+ if (!existsSync3(p))
105157
105019
  return {};
105158
105020
  try {
105159
- const parsed = JSON.parse(readFileSync5(p, "utf8"));
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
- mkdirSync4(dirname3(p), { recursive: true });
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
- writeFileSync4(tmp, JSON.stringify(merged, null, 2), "utf8");
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 Error(`network error: ${err.message}`, { cause: err });
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 Error(`network error: ${err.message}`, { cause: err });
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 Error(`network error: ${err.message}`, { cause: err });
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
- return;
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/apps.js
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
- const msg = err.message ?? String(err);
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 new Promise((r) => setTimeout(r, backoffMs));
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
- data;
106083
- env;
106084
- envScan;
106085
- export;
106086
- logs;
106087
- auth;
106088
- billing;
106089
- account;
106090
- alerts;
106091
- platform;
106092
- security;
106093
- search;
106094
- sharing;
106095
- invites;
106096
- transfers;
106097
- constructor(options = {}) {
106098
- const defaults = loadConfig();
106099
- this.apiUrl = options.apiUrl ?? defaults.apiUrl;
106100
- this.token = options.token !== undefined ? options.token : defaults.token;
106101
- this.sessionId = options.sessionId;
106102
- const http = new Http({
106103
- apiUrl: this.apiUrl,
106104
- token: this.token,
106105
- fetchImpl: options.fetchImpl,
106106
- clientVersion: options.clientVersion,
106107
- sessionId: options.sessionId
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 new Promise((r) => setTimeout(r, 1500));
107170
- fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id);
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 (${diag.containerHealth.latencyMs}ms)` : `${diag.containerHealth.path} — not responding`
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} (${diag.publicRoute.latencyMs}ms)` : `${diag.publicRoute.url} — not responding`
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 deriveVerdict(checks3, vctx) {
107954
+ function buildVerdictRuleContext(checks3, vctx) {
107640
107955
  const findCheck = (name) => checks3.find((c) => c.name === name);
107641
- const auth = findCheck("Auth token");
107642
- if (auth?.status === "fail") {
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
- const api2 = findCheck("API reachability");
107656
- if (api2?.status === "fail") {
107657
- return {
107658
- status: "blocked",
107659
- diagnosis: {
107660
- title: "Percher API is unreachable",
107661
- explanation: api2.message,
107662
- reasonCode: "infra_unavailable",
107663
- phase: "infra"
107664
- },
107665
- recovery: recoveryAsk({
107666
- prompt: `Percher API is unreachable (${api2.message}). This is usually a transient network issue — wait a moment and try again, or check status.percher.app.`,
107667
- reasonCode: "infra_unavailable",
107668
- retryable: true
107669
- }),
107670
- summary: "Cannot reach the Percher API — try again in a moment."
107671
- };
107672
- }
107673
- const toml = findCheck("percher.toml");
107674
- if (toml?.status === "fail" && !vctx.appProvided) {
107675
- return {
107676
- status: "blocked",
107677
- diagnosis: {
107678
- title: "Invalid percher.toml",
107679
- explanation: toml.message,
107680
- reasonCode: "config_invalid",
107681
- phase: "config"
107682
- },
107683
- recovery: recoveryFixConfig({
107684
- problems: [
107685
- {
107686
- file: "percher.toml",
107687
- message: toml.message
107688
- }
107689
- ],
107690
- reasonCode: "config_invalid"
107691
- }),
107692
- summary: "percher.toml is invalid — fix the issues and re-run."
107693
- };
107694
- }
107695
- const app = findCheck("App");
107696
- if (app?.status === "skip") {
107697
- return {
107698
- status: "blocked",
107699
- diagnosis: {
107700
- title: "No app specified",
107701
- explanation: app.message,
107702
- reasonCode: "config_missing",
107703
- phase: "config"
107704
- },
107705
- recovery: recoveryAsk({
107706
- prompt: `${app.message} — pass \`--app <name>\` or run \`percher init\` in a project directory to generate a percher.toml.`,
107707
- reasonCode: "config_missing"
107708
- }),
107709
- summary: "No percher.toml found and no --app supplied — pass --app or run percher init."
107710
- };
107711
- }
107712
- const inTransitionalDeploy = !!vctx.lastDeployStatus && TRANSITIONAL_DEPLOY_STATUSES.has(vctx.lastDeployStatus);
107713
- const inTransitionalApp = !!vctx.appStatus && TRANSITIONAL_APP_STATUSES.has(vctx.appStatus);
107714
- if (inTransitionalDeploy || inTransitionalApp) {
107715
- const reasonCode = vctx.lastDeployStatus === "queued" ? "deploy_queued" : vctx.lastDeployStatus === "building" ? "deploy_building" : vctx.lastDeployStatus === "deploying" ? "deploy_deploying" : "deploy_queued";
107716
- const what = inTransitionalApp ? `App ${vctx.appName ?? ""} is provisioning` : `Last deploy is ${vctx.lastDeployStatus}`;
107717
- const recovery = vctx.lastDeployId && vctx.appName ? recoveryWait({
107718
- app: vctx.appName,
107719
- deployId: vctx.lastDeployId,
107720
- reasonCode
107721
- }) : recoveryAsk({
107722
- prompt: `${what.trim()} for ${vctx.appName ?? "this app"}. Wait for it to finish before retrying — there's no deployId to track yet.`,
107723
- reasonCode
107724
- });
107725
- return {
107726
- status: "in_progress",
107727
- diagnosis: {
107728
- title: what.trim(),
107729
- explanation: vctx.lastDeployId ? `${what.trim()} (deploy ${vctx.lastDeployId}). Wait for it to finish before retrying.` : `${what.trim()}. Wait for it to finish before retrying.`,
107730
- reasonCode,
107731
- phase: "deploy"
107732
- },
107733
- recovery,
107734
- summary: vctx.lastDeployId ? `${what.trim()} — wait for deploy ${vctx.lastDeployId}.` : `${what.trim()} — wait a moment and re-run.`
107735
- };
107736
- }
107737
- const verdictChecks = vctx.appProvided && toml?.status === "fail" ? checks3.filter((c) => c.name !== "percher.toml") : checks3;
107738
- if (vctx.replacedResolution) {
107739
- const r = vctx.replacedResolution;
107740
- const isResolvedLive = r.recovery.nextAction === "none" && !!r.url;
107741
- if (isResolvedLive) {
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: "ok",
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
- const status = r.recovery.nextAction === "wait_deploy" ? "in_progress" : "needs_action";
107749
- return {
107750
- status,
107751
- diagnosis: {
107752
- title: "Last deploy was replaced",
107753
- explanation: r.summary,
107754
- reasonCode: "replaced_by_newer",
107755
- phase: "deploy"
107756
- },
107757
- recovery: { ...r.recovery, reasonCode: "replaced_by_newer" },
107758
- summary: r.summary
107759
- };
107760
- }
107761
- if (vctx.lastDeployStatus === "failed") {
107762
- if (vctx.inputMode === "deploy") {
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
- if (fileProblems.length > 0) {
107782
- return {
107783
- status: "needs_action",
107784
- diagnosis: {
107785
- title: cls?.title ?? "Build failed with file-located problems",
107786
- explanation: cls?.explanation ?? `Build extracted ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} with file locations. Patch the files directly.`,
107787
- reasonCode: "build_failed",
107788
- phase: "build"
107789
- },
107790
- recovery: recoveryFixProblems({
107791
- problems: fileProblems,
107792
- reasonCode: "build_failed"
107793
- }),
107794
- summary: vctx.lastDeployId ? `Deploy ${vctx.lastDeployId} failed — ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} to patch.` : `Build failed — ${fileProblems.length} structured problem${fileProblems.length === 1 ? "" : "s"} to patch.`
107795
- };
107796
- }
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
- return {
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
- if (vctx.appStatus === "suspended") {
107836
- return {
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
- if (verdictChecks.every((c) => c.status === "pass" || c.status === "skip" || c.status === "warn")) {
107852
- const passing = verdictChecks.filter((c) => c.status === "pass").length;
107853
- const noun = passing === 1 ? "check" : "checks";
107854
- return {
107855
- status: "ok",
107856
- recovery: recoveryNone({ reasonCode: "none" }),
107857
- summary: vctx.appName ? `All ${passing} ${noun} passed for ${vctx.appName}.` : `All ${passing} ${noun} passed.`
107858
- };
107859
- }
107860
- const containerDown = vctx.containerRunning === false && vctx.containerState !== undefined && vctx.containerState !== "not-found" || vctx.containerHealthy === false;
107861
- if (vctx.appStatus === "crashed" || containerDown) {
107862
- const containerCheck = findCheck("Container");
107863
- const healthCheck = findCheck("Container health");
107864
- const lastCrashCheck = findCheck("Last crash");
107865
- const explanationBits = [];
107866
- if (vctx.appStatus === "crashed")
107867
- explanationBits.push("App is in crashed state.");
107868
- if (containerCheck?.status === "fail")
107869
- explanationBits.push(containerCheck.message);
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
- return {
107913
- status: "needs_action",
107914
- diagnosis: {
107915
- title: vctx.appStatus === "crashed" ? "App crashed" : "Runtime not responding",
107916
- explanation,
107917
- reasonCode: "runtime_crashed",
107918
- phase: "runtime"
107919
- },
107920
- recovery: {
107921
- retryable: false,
107922
- nextAction: "run_doctor",
107923
- suggestedTool: "percher_doctor",
107924
- args: { app: vctx.appName, mode: "runtime" },
107925
- reasonCode: "runtime_crashed"
107926
- },
107927
- summary: vctx.appName ? `${vctx.appName} runtime needs investigation — call percher_doctor with mode='runtime'.` : "Runtime needs investigation — call percher_doctor with mode='runtime'."
107928
- };
107929
- }
107930
- if (vctx.publicRouteHealthy === false) {
107931
- const routeCheck = findCheck("Public route");
107932
- return {
107933
- status: "needs_action",
107934
- diagnosis: {
107935
- title: "Public route is not responding",
107936
- explanation: routeCheck?.message ?? "Container looks healthy but the public URL isn't responding — route reconcile usually self-heals.",
107937
- reasonCode: "infra_transient",
107938
- phase: "infra"
107939
- },
107940
- recovery: {
107941
- retryable: true,
107942
- nextAction: "retry",
107943
- suggestedTool: "percher_doctor",
107944
- args: { app: vctx.appName },
107945
- reasonCode: "infra_transient"
107946
- },
107947
- 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."
107948
- };
107949
- }
107950
- const firstFailed = verdictChecks.find((c) => c.status === "fail");
107951
- return {
107952
- status: "needs_action",
107953
- diagnosis: firstFailed ? {
107954
- title: firstFailed.name,
107955
- explanation: firstFailed.message,
107956
- reasonCode: "unknown"
107957
- } : undefined,
107958
- recovery: recoveryAsk({
107959
- 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.",
107960
- reasonCode: "unknown"
107961
- }),
107962
- summary: firstFailed ? `${firstFailed.name} failed: ${firstFailed.message}` : "Doctor reported issues — review the checks list."
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).add(target);
109971
- dependents.get(target).add(c.name);
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).size);
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
- while (ready.length > 0) {
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 msg = err.message ?? String(err);
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 (err instanceof Error && /network error|fetch failed|ECONNRESET|ENOTFOUND|ETIMEDOUT/i.test(err.message)) {
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/settings, or delete unused apps with `percher delete <name>`.",
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/settings, or delete an unused app with \`percher delete <name>\`.`,
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/settings.",
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/settings.`,
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/settings to resend the email. Re-publish after verifying.",
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 buildReplacedResult(opts) {
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: "failed",
111971
+ status: "already_in_progress",
111417
111972
  app,
111418
111973
  fileCount: tarball.fileCount,
111419
111974
  bytes: tarball.bytes,
111420
- error: {
111421
- title: "Retry limit reached",
111422
- explanation: "Percher already auto-retried once after an `infra_unavailable` failure for this app within the last 10 minutes. We won't auto-retry again — the next attempt should be a deliberate user decision.",
111423
- suggestion: `Wait until ${resetAtForPrompt}, then re-run \`percher publish\`. If the failure persists past that, the underlying issue is probably not transient — run \`percher doctor\` to diagnose.`
111424
- },
111425
- recovery: recoveryAsk({
111426
- prompt,
111427
- options: ["wait before retrying", "inspect status"],
111428
- reasonCode: "retry_limit_reached",
111429
- retryable: false
111975
+ recovery: recoveryWait({
111976
+ app: app.name,
111977
+ deployId: active.deployId,
111978
+ reasonCode
111430
111979
  }),
111431
- summary: `Retry limit reached for ${app.name}${resetAtStr ? ` — wait until ${resetAtStr}` : ""}.`,
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 > MAX_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 === "RETRY_LIMIT_REACHED") {
111705
- return buildRetryLimitReachedResult({
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
- if (err instanceof PercherApiError && err.code === "ENV_KEY_UNDECLARED") {
111741
- const extra = err.extra ?? {};
111742
- const apiProblems = extra.problems ?? [];
111743
- const problems = apiProblems.map((p) => ({
111744
- file: p.file,
111745
- line: p.line,
111746
- message: `Source references env key '${p.key}' which is not declared in [env]. Add it to [env].required, [env].optional, or [env].ignore in percher.toml. Context: ${p.context ?? "(unavailable)"}`
111747
- }));
111748
- const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
111749
- return {
111750
- status: "failed",
111751
- app,
111752
- fileCount: tarball.fileCount,
111753
- bytes: tarball.bytes,
111754
- error: {
111755
- title: "Undeclared env key",
111756
- explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
111757
- suggestion: "Add each key to [env].required (must exist before deploy), [env].optional (may be referenced), or [env].ignore (intentionally unset). See https://percher.app/docs/env for the contract.",
111758
- errorClass: "config_invalid",
111759
- phase: "config",
111760
- relevantFiles: ["percher.toml"]
111761
- },
111762
- recovery: recoveryFixConfig({
111763
- problems,
111764
- reasonCode: "env_key_undeclared"
111765
- }),
111766
- summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`,
111767
- configPath: tomlPathFor2(ctx.cwd),
111768
- bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
111769
- };
111770
- }
111771
- if (err instanceof PercherApiError && err.code === "DAILY_QUOTA_EXCEEDED") {
111772
- const extra = err.extra ?? {};
111773
- const kind = extra.kind ?? "live";
111774
- const used = extra.used ?? 0;
111775
- const limit = extra.limit ?? 0;
111776
- const resetAt = extra.resetAt ?? "";
111777
- const otherKind = kind === "live" ? "preview" : "live";
111778
- return {
111779
- status: "failed",
111780
- app,
111781
- fileCount: tarball.fileCount,
111782
- bytes: tarball.bytes,
111783
- error: {
111784
- title: "Daily deploy quota reached",
111785
- explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
111786
- suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/settings to raise this cap.`
111787
- },
111788
- recovery: recoveryAsk({
111789
- prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then, deploy a ${otherKind} instead, or upgrade at https://percher.app/settings.`,
111790
- options: ["wait", `deploy ${otherKind}`, "upgrade"],
111791
- reasonCode: "quota_exceeded"
111792
- }),
111793
- summary: `Daily ${kind}-deploy quota reached (${used}/${limit}). Resets ${resetAt}.`,
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 buildReplacedResult({
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: RECOVERY_NEEDS_LOGIN,
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: RECOVERY_NEEDS_LOGIN,
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
- let firstDeployRetry = false;
112008
- let app;
112009
- ({ app, firstDeploy: firstDeployRetry } = await ensureApp2(ctx, config2));
112010
- ctx.status("Uploading...");
112011
- const freshTarballBytes = new Uint8Array(await new Response(freshTarball.stream).arrayBuffer());
112012
- const freshIdempotencyKey = crypto.randomUUID();
112013
- let deployment;
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 { result: deployResponse } = await runDeployWithRetry({
112016
- call: (attempt) => ctx.client.apps.deploy(app.id, {
112017
- tarball: freshTarballBytes,
112018
- type: input.preview ? "preview" : undefined,
112019
- note: input.message,
112020
- noCache: input.noCache,
112021
- idempotencyKey: freshIdempotencyKey,
112022
- retriedAttempts: attempt - 1
112023
- }),
112024
- onRetry: ({ attempt, decision }) => {
112025
- ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
112026
- }
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
- if (err instanceof PercherApiError && err.code === "RETRY_LIMIT_REACHED") {
112039
- return buildRetryLimitReachedResult({
112040
- err,
112041
- app,
112042
- tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
112043
- cwd: ctx.cwd
112044
- });
112045
- }
112046
- throw err;
112047
- }
112048
- const replacedPreview = deployment.replacedPreview === true;
112049
- if (input.waitForLive === false) {
112050
- ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
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: "queued",
112053
- app,
112054
- deployment,
112055
- fileCount: freshTarball.fileCount,
112056
- bytes: freshTarball.bytes,
112057
- replacedPreview: replacedPreview || undefined,
112058
- firstDeploy: firstDeployRetry || undefined,
112059
- recovery: recoveryWait({
112060
- app: app.name,
112061
- deployId: deployment.id,
112062
- reasonCode: "deploy_queued"
112063
- }),
112064
- summary: `${app.name} deploy ${deployment.id} queued — resume with percher_wait_for_deploy.`,
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: freshTarball.fileCount, bytes: freshTarball.bytes }
112542
+ bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
112067
112543
  };
112068
112544
  }
112069
- const timeoutMs = TIMEOUTS.clientPublishPoll;
112070
- const start = Date.now();
112071
- while (deployment.status !== "live" && deployment.status !== "failed" && deployment.status !== "replaced") {
112072
- if (Date.now() - start > timeoutMs) {
112073
- return {
112074
- status: "failed",
112075
- app,
112076
- deployment,
112077
- fileCount: freshTarball.fileCount,
112078
- bytes: freshTarball.bytes,
112079
- error: {
112080
- title: "Deploy timed out",
112081
- explanation: "The deployment did not complete within 5 minutes.",
112082
- suggestion: `Run \`percher doctor --app ${app.name}\` (CLI) or the percher_doctor tool (MCP) to diagnose the stall. Doctor will read deploy state + build log and return a concrete next step.`
112083
- },
112084
- recovery: recoveryDoctor({
112085
- app: app.name,
112086
- deployId: deployment.id,
112087
- mode: "deploy",
112088
- reasonCode: "deploy_stalled"
112089
- }),
112090
- summary: `Deploy timed out after 5 minutes (${deployment.id}).`,
112091
- configPath: tomlPathFor2(ctx.cwd),
112092
- bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
112093
- };
112545
+ saveConfig({
112546
+ token: created.token,
112547
+ anonClaim: {
112548
+ app: created.app.name,
112549
+ claimToken: created.claimToken,
112550
+ expiresAt: created.expiresAt
112094
112551
  }
112095
- ctx.status(`${deployment.status}...`);
112096
- await new Promise((r) => setTimeout(r, 2000));
112097
- deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
112098
- }
112099
- if (deployment.status === "replaced") {
112100
- return await buildReplacedResult({
112101
- ctx,
112102
- app,
112103
- deployment,
112104
- tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
112105
- cwd: ctx.cwd
112106
- });
112107
- }
112108
- if (deployment.status === "failed") {
112109
- return buildFailureResult({
112110
- ctx,
112111
- app,
112112
- deployment,
112113
- tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
112114
- input
112115
- });
112116
- }
112117
- const totalSeconds = (Date.now() - start) / 1000;
112118
- const url2 = deployment.previewUrl ?? deployment.url ?? app.url;
112119
- ctx.status(`Live at ${url2} (${totalSeconds.toFixed(0)}s)`);
112120
- return {
112121
- status: "live",
112122
- url: url2,
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
- deployment,
112125
- timing: {
112126
- totalSeconds: Math.round(totalSeconds),
112127
- packageMs: 0,
112128
- uploadMs: 0,
112129
- buildMs: 0
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
- fileCount: freshTarball.fileCount,
112132
- bytes: freshTarball.bytes,
112133
- replacedPreview,
112134
- firstDeploy: firstDeployRetry || undefined,
112135
- recovery: RECOVERY_NONE,
112136
- cacheHit: deployment.cacheHit,
112137
- traceId: deployment.id,
112138
- summary: buildLiveSummary({
112139
- appName: app.name,
112140
- url: url2 ?? app.url,
112141
- totalSeconds: Math.round(totalSeconds),
112142
- framework: config2.app.framework,
112143
- preview: input.preview === true,
112144
- replacedPreview,
112145
- cacheHit: deployment.cacheHit ?? false
112146
- }),
112147
- configPath: tomlPathFor2(ctx.cwd),
112148
- bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes }
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 > MAX_BYTES2) {
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/settings.`,
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/settings.`,
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/settings to raise this cap.`
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/settings.`,
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/settings to raise it."
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
- return ctx.client.apps.rename(appRef, input.name);
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 writeFileSync8 } from "node:fs";
113316
+ import { mkdtempSync, writeFileSync as writeFileSync9 } from "node:fs";
112855
113317
  import { tmpdir } from "node:os";
112856
- import { join as join22 } from "node:path";
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(join22(tmpdir(), `percher-reproduce-${input.deployId}-`));
112941
- const tarballPath = join22(tmpDir, "source.tar.gz");
112942
- writeFileSync8(tarballPath, bytes);
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 resolveApp3(ctx, app) {
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 = resolveApp3(ctx, input.app);
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 = resolveApp3(ctx, input.app);
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 = resolveApp3(ctx, input.app);
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 = resolveApp3(ctx, input.app);
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 resolveApp4(ctx, app) {
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 = resolveApp4(ctx, input.app);
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 = resolveApp4(ctx, input.app);
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 = resolveApp4(ctx, input.app);
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 PLAN_IDS = Object.keys(PLAN_CATALOG);
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.firstDeploy) {
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> [deployId]", "Manage preview deploys (promote | discard)").option("--app <name>", "App name (overrides percher.toml)").action(async (action, deployId, options) => {
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 (!deployId)
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, deployId);
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 (!deployId)
115202
+ if (!arg)
114600
115203
  throw new Error("Deploy ID required. Usage: percher preview discard <id>");
114601
- await ctx.client.apps.discardPreview(appRef, deployId);
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.4.2",
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.7",
115226
- "@percher/core": "^0.4.0",
115227
- "@percher/toml": "^0.1.3",
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: existsSync16, mkdirSync: mkdirSync6, writeFileSync: writeFileSync9 } = await import("node:fs");
115283
- const { join: join23 } = await import("node:path");
115284
- const configDir = join23(homedir2(), ".percher");
115285
- const markerFile = join23(configDir, ".telemetry-notice-shown");
115286
- if (existsSync16(markerFile))
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
- writeFileSync9(markerFile, new Date().toISOString());
115927
+ writeFileSync10(markerFile, new Date().toISOString());
115294
115928
  } catch {}
115295
115929
  }
115296
115930