percher 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4751,7 +4751,7 @@ var init_build_env_prefixes = __esm(() => {
4751
4751
  });
4752
4752
 
4753
4753
  // ../client/dist/config.js
4754
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync4 } from "node:fs";
4754
+ import { chmodSync, closeSync, existsSync as existsSync4, fchmodSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
4755
4755
  import { homedir } from "node:os";
4756
4756
  import { dirname as dirname4, join as join6 } from "node:path";
4757
4757
  function home() {
@@ -4800,12 +4800,32 @@ function readAnonClaims() {
4800
4800
  }
4801
4801
  function saveConfig(partial2) {
4802
4802
  const p = configPath();
4803
- mkdirSync4(dirname4(p), { recursive: true });
4803
+ mkdirSync4(dirname4(p), { recursive: true, mode: 448 });
4804
+ if (process.platform !== "win32") {
4805
+ chmodSync(dirname4(p), 448);
4806
+ if (existsSync4(p))
4807
+ chmodSync(p, 384);
4808
+ }
4804
4809
  const current = readFile2();
4805
4810
  const merged = { ...current, ...partial2 };
4806
4811
  const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
4807
- writeFileSync4(tmp, JSON.stringify(merged, null, 2), "utf8");
4808
- renameSync(tmp, p);
4812
+ let fd;
4813
+ let created = false;
4814
+ try {
4815
+ fd = openSync(tmp, "wx", 384);
4816
+ created = true;
4817
+ if (process.platform !== "win32")
4818
+ fchmodSync(fd, 384);
4819
+ writeFileSync4(fd, JSON.stringify(merged, null, 2), "utf8");
4820
+ closeSync(fd);
4821
+ fd = undefined;
4822
+ renameSync(tmp, p);
4823
+ } finally {
4824
+ if (fd !== undefined)
4825
+ closeSync(fd);
4826
+ if (created)
4827
+ rmSync2(tmp, { force: true });
4828
+ }
4809
4829
  }
4810
4830
  function getAnonClaim(app) {
4811
4831
  return readAnonClaims()[app] ?? null;
@@ -4830,7 +4850,7 @@ var package_default;
4830
4850
  var init_package = __esm(() => {
4831
4851
  package_default = {
4832
4852
  name: "@percher/client",
4833
- version: "1.1.0",
4853
+ version: "1.2.0",
4834
4854
  description: "Official TypeScript SDK for Percher: deploy apps, manage env vars, stream logs, and automate hosting workflows.",
4835
4855
  keywords: [
4836
4856
  "percher",
@@ -5384,10 +5404,11 @@ class AppsResource {
5384
5404
  deleteIconImage(appRef) {
5385
5405
  return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/icon/image`);
5386
5406
  }
5387
- uploadPreviewImage(appRef, bytes, mime) {
5407
+ uploadPreviewImage(appRef, bytes, mime, orientation) {
5388
5408
  const ab = new ArrayBuffer(bytes.byteLength);
5389
5409
  new Uint8Array(ab).set(bytes);
5390
- return this.http.postBinary(`/apps/${encodeURIComponent(appRef)}/preview/image`, new Blob([ab]), { "Content-Type": mime });
5410
+ const query = orientation ? `?orientation=${orientation}` : "";
5411
+ return this.http.postBinary(`/apps/${encodeURIComponent(appRef)}/preview/image${query}`, new Blob([ab]), { "Content-Type": mime });
5391
5412
  }
5392
5413
  deletePreviewImage(appRef) {
5393
5414
  return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/preview/image`);
@@ -6262,6 +6283,55 @@ var init_env_file = __esm(() => {
6262
6283
  };
6263
6284
  });
6264
6285
 
6286
+ // ../client/dist/telemetry-consent.js
6287
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
6288
+ import { dirname as dirname5, join as join7 } from "node:path";
6289
+ function consentPath() {
6290
+ return join7(dirname5(configPath()), "telemetry.json");
6291
+ }
6292
+ function envOptOut() {
6293
+ if (process.env.PERCHER_NO_TELEMETRY === "1" || process.env.PERCHER_NO_TELEMETRY === "true") {
6294
+ return "PERCHER_NO_TELEMETRY";
6295
+ }
6296
+ if (process.env.DO_NOT_TRACK === "1")
6297
+ return "DO_NOT_TRACK";
6298
+ return null;
6299
+ }
6300
+ function readStored() {
6301
+ try {
6302
+ const parsed = JSON.parse(readFileSync5(consentPath(), "utf8"));
6303
+ if (typeof parsed?.enabled === "boolean" && typeof parsed.decidedAt === "string" && typeof parsed.version === "string") {
6304
+ return { enabled: parsed.enabled, decidedAt: parsed.decidedAt, version: parsed.version };
6305
+ }
6306
+ } catch {}
6307
+ return null;
6308
+ }
6309
+ function telemetryState() {
6310
+ const optOut = envOptOut();
6311
+ const stored = readStored();
6312
+ return { enabled: optOut === null && stored?.enabled === true, envOptOut: optOut, stored };
6313
+ }
6314
+ function saveTelemetryAnswer(enabled, version2) {
6315
+ const p = consentPath();
6316
+ mkdirSync5(dirname5(p), { recursive: true, mode: 448 });
6317
+ if (process.platform !== "win32")
6318
+ chmodSync2(dirname5(p), 448);
6319
+ const answer = { enabled, decidedAt: new Date().toISOString(), version: version2 };
6320
+ const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
6321
+ try {
6322
+ writeFileSync5(tmp, `${JSON.stringify(answer, null, 2)}
6323
+ `, { mode: 384, flag: "wx" });
6324
+ if (process.platform !== "win32")
6325
+ chmodSync2(tmp, 384);
6326
+ renameSync2(tmp, p);
6327
+ } finally {
6328
+ rmSync3(tmp, { force: true });
6329
+ }
6330
+ }
6331
+ var init_telemetry_consent = __esm(() => {
6332
+ init_config();
6333
+ });
6334
+
6265
6335
  // ../client/dist/index.js
6266
6336
  var init_dist = __esm(() => {
6267
6337
  init_build_env_prefixes();
@@ -6272,6 +6342,7 @@ var init_dist = __esm(() => {
6272
6342
  init_apps();
6273
6343
  init_auth();
6274
6344
  init_logs();
6345
+ init_telemetry_consent();
6275
6346
  });
6276
6347
 
6277
6348
  // ../../node_modules/.bun/ignore@7.0.5/node_modules/ignore/index.js
@@ -10582,7 +10653,7 @@ var require_util = __commonJS((exports) => {
10582
10653
  return path;
10583
10654
  }
10584
10655
  exports.normalize = normalize2;
10585
- function join18(aRoot, aPath) {
10656
+ function join19(aRoot, aPath) {
10586
10657
  if (aRoot === "") {
10587
10658
  aRoot = ".";
10588
10659
  }
@@ -10614,7 +10685,7 @@ var require_util = __commonJS((exports) => {
10614
10685
  }
10615
10686
  return joined;
10616
10687
  }
10617
- exports.join = join18;
10688
+ exports.join = join19;
10618
10689
  exports.isAbsolute = function(aPath) {
10619
10690
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
10620
10691
  };
@@ -10787,7 +10858,7 @@ var require_util = __commonJS((exports) => {
10787
10858
  parsed.path = parsed.path.substring(0, index + 1);
10788
10859
  }
10789
10860
  }
10790
- sourceURL = join18(urlGenerate(parsed), sourceURL);
10861
+ sourceURL = join19(urlGenerate(parsed), sourceURL);
10791
10862
  }
10792
10863
  return normalize2(sourceURL);
10793
10864
  }
@@ -92667,13 +92738,13 @@ function iconColourHex(token) {
92667
92738
  }
92668
92739
  // ../shared/dist/legal.js
92669
92740
  var LEGAL_DOCUMENT_VERSIONS = {
92670
- terms: "1.0.2",
92741
+ terms: "1.0.3",
92671
92742
  aup: "1.0.1",
92672
- privacy: "1.0.2",
92673
- cookies: "1.0.3",
92674
- dpa: "1.0",
92675
- subprocessors: "1.0.2",
92676
- dataPortability: "1.0"
92743
+ privacy: "1.0.4",
92744
+ cookies: "1.0.4",
92745
+ dpa: "1.1",
92746
+ subprocessors: "1.2",
92747
+ dataPortability: "1.0.4"
92677
92748
  };
92678
92749
  var CURRENT_TERMS_VERSION = LEGAL_DOCUMENT_VERSIONS.terms;
92679
92750
  // ../shared/dist/legal-changes.js
@@ -92796,6 +92867,7 @@ var SWITCHES = [
92796
92867
  },
92797
92868
  bool("PERCHER_ALLOW_NO_CAPTCHA", ["dashboard"], "allow production without Turnstile", ONE),
92798
92869
  bool("PERCHER_ALLOW_UNVERIFIED_SIGNUP", ["dashboard"], "allow production without Resend", ONE),
92870
+ bool("PERCHER_DASHBOARD_SECRET_BUILD", ["dashboard"], "allow an ephemeral BetterAuth key during `next build` only, never at runtime", ONE),
92799
92871
  bool("PERCHER_ALLOW_NO_ALERTING", ["api"], "boot without an alert channel", ONE),
92800
92872
  {
92801
92873
  kind: "presence",
@@ -106437,7 +106509,7 @@ var ENV_NAME_SHAPE = "UPPER_SNAKE_CASE, at most 256 characters";
106437
106509
  var buildSchema = exports_external.object({
106438
106510
  command: exports_external.string().optional(),
106439
106511
  output: exports_external.string().refine((v) => !v.startsWith("/"), "must be a relative path (no leading slash)").optional(),
106440
- pass_env: exports_external.array(exports_external.string().regex(ENV_NAME_RE2, `must be a valid env var name (${ENV_NAME_SHAPE})`).refine((name) => !name.startsWith("BUILDKIT_"), "BUILDKIT_* names are reserved by Percher and cannot be passed to the build")).max(50, "at most 50 build env vars can be exposed").optional()
106512
+ pass_env: exports_external.array(exports_external.string().regex(ENV_NAME_RE2, `must be a valid env var name (${ENV_NAME_SHAPE})`).refine((name) => !name.startsWith("BUILDKIT_") && name !== "PERCHER_BUILD_SECRET_DIGEST", "BUILDKIT_* and PERCHER_BUILD_SECRET_DIGEST are reserved by Percher and cannot be passed to the build")).max(50, "at most 50 build env vars can be exposed").optional()
106441
106513
  }).strict();
106442
106514
  var webSchema = exports_external.object({
106443
106515
  port: exports_external.number().int().min(1024).max(65535),
@@ -107774,20 +107846,32 @@ function manifestStarter({ name, themeColorHex }) {
107774
107846
  theme_color: themeColorHex
107775
107847
  }, null, 2);
107776
107848
  }
107777
- function serviceWorkerStarter() {
107778
- return `// sw.js — minimal offline shell. Drop in at your app root and serve from /sw.js.
107779
- const CACHE = "app-shell-v1";
107780
- const SHELL = ["/"];
107849
+ function serviceWorkerStarter(assets = []) {
107850
+ if (assets.some((path) => !path.startsWith("/") || path.startsWith("//") || path.includes("\\"))) {
107851
+ throw new Error("Offline assets must be same-origin absolute paths");
107852
+ }
107853
+ return `// sw.js — public offline shell. Serve from /sw.js.
107854
+ // Add public assets, e.g. ["/offline.html", "/app.js", "/app.css"]. Never add private pages or API data.
107855
+ // Include "/" only if the page is public and identical for signed-in and anonymous visitors.
107856
+ // Bump CACHE whenever the asset list or its content changes. Test an offline reload.
107857
+ const CACHE = "percher-public-shell-v1";
107858
+ const SHELL = ${JSON.stringify(assets)};
107781
107859
 
107782
107860
  self.addEventListener("install", (event) => {
107783
- event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)));
107861
+ event.waitUntil(caches.open(CACHE).then((cache) => Promise.all(SHELL.map(async (path) => {
107862
+ const response = await fetch(path, { credentials: "omit", redirect: "error" });
107863
+ if (!response.ok || /private|no-store/i.test(response.headers.get("cache-control") || "")) {
107864
+ throw new Error("Offline asset is unavailable or private: " + path);
107865
+ }
107866
+ await cache.put(path, response);
107867
+ }))));
107784
107868
  self.skipWaiting();
107785
107869
  });
107786
107870
 
107787
107871
  self.addEventListener("activate", (event) => {
107788
107872
  event.waitUntil(
107789
107873
  caches.keys().then((keys) =>
107790
- Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
107874
+ Promise.all(keys.filter((k) => k.startsWith("percher-public-shell-") && k !== CACHE).map((k) => caches.delete(k))),
107791
107875
  ),
107792
107876
  );
107793
107877
  self.clients.claim();
@@ -107795,8 +107879,10 @@ self.addEventListener("activate", (event) => {
107795
107879
 
107796
107880
  self.addEventListener("fetch", (event) => {
107797
107881
  if (event.request.method !== "GET") return;
107882
+ const url = new URL(event.request.url);
107883
+ if (url.origin !== self.location.origin || !SHELL.includes(url.pathname + url.search)) return;
107798
107884
  event.respondWith(
107799
- caches.match(event.request).then((cached) => cached || fetch(event.request)),
107885
+ caches.open(CACHE).then((cache) => cache.match(event.request)).then((cached) => cached || fetch(event.request)),
107800
107886
  );
107801
107887
  });`;
107802
107888
  }
@@ -107965,17 +108051,17 @@ async function claim(ctx, input = {}) {
107965
108051
  };
107966
108052
  }
107967
108053
  // ../core/dist/commands/create.js
107968
- import { join as join8, resolve as resolve5 } from "node:path";
108054
+ import { join as join9, resolve as resolve5 } from "node:path";
107969
108055
 
107970
108056
  // ../core/dist/templates.js
107971
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync5 } from "node:fs";
107972
- import { basename as basename2, join as join7, normalize, relative as relative2, resolve as resolve4 } from "node:path";
107973
- var __dirname = "C:\\dev\\Percher\\packages\\core\\dist";
108057
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync6 } from "node:fs";
108058
+ import { basename as basename2, join as join8, normalize, relative as relative2, resolve as resolve4 } from "node:path";
108059
+ var __dirname = "D:\\dev\\Percher-codex-clone\\packages\\core\\dist";
107974
108060
  var TEMPLATES_DIR = resolve4(import.meta.dirname ?? __dirname, "../templates");
107975
108061
  var PLACEHOLDER = "__APP_NAME__";
107976
108062
  function listTemplates() {
107977
108063
  return readdirSync(TEMPLATES_DIR).filter((name) => {
107978
- const full = join7(TEMPLATES_DIR, name);
108064
+ const full = join8(TEMPLATES_DIR, name);
107979
108065
  return statSync(full).isDirectory();
107980
108066
  });
107981
108067
  }
@@ -107992,13 +108078,13 @@ function scaffoldTemplate(input) {
107992
108078
  throw new Error(`Target directory "${absTarget}" is not empty. Use --force to overwrite.`);
107993
108079
  }
107994
108080
  }
107995
- const srcDir = join7(TEMPLATES_DIR, template);
108081
+ const srcDir = join8(TEMPLATES_DIR, template);
107996
108082
  const files = [];
107997
108083
  function copyDir(src, dest) {
107998
- mkdirSync5(dest, { recursive: true });
108084
+ mkdirSync6(dest, { recursive: true });
107999
108085
  for (const entry of readdirSync(src)) {
108000
- const srcPath = join7(src, entry);
108001
- const destPath = join7(dest, entry);
108086
+ const srcPath = join8(src, entry);
108087
+ const destPath = join8(dest, entry);
108002
108088
  const rel = relative2(absTarget, normalize(destPath));
108003
108089
  if (rel.startsWith("..") || resolve4(destPath) === resolve4(absTarget, "..", basename2(absTarget))) {
108004
108090
  throw new Error(`Template contains path traversal: ${entry}`);
@@ -108009,11 +108095,11 @@ function scaffoldTemplate(input) {
108009
108095
  if (existsSync5(destPath) && !force) {
108010
108096
  continue;
108011
108097
  }
108012
- let content = readFileSync5(srcPath, "utf-8");
108098
+ let content = readFileSync6(srcPath, "utf-8");
108013
108099
  content = content.replaceAll(PLACEHOLDER, appName);
108014
108100
  content = content.replace(/\r\n/g, `
108015
108101
  `);
108016
- writeFileSync5(destPath, content, "utf-8");
108102
+ writeFileSync6(destPath, content, "utf-8");
108017
108103
  files.push(relative2(absTarget, destPath).replace(/\\/g, "/"));
108018
108104
  }
108019
108105
  }
@@ -108046,7 +108132,7 @@ async function create(ctx, input) {
108046
108132
  });
108047
108133
  }
108048
108134
  } else {
108049
- targetDir = join8(ctx.cwd, input.name);
108135
+ targetDir = join9(ctx.cwd, input.name);
108050
108136
  }
108051
108137
  try {
108052
108138
  const result = scaffoldTemplate({
@@ -108101,6 +108187,144 @@ async function dashboard(ctx, input = {}) {
108101
108187
  await ctx.openUrl(url2);
108102
108188
  return url2;
108103
108189
  }
108190
+ // ../core/dist/untrusted.js
108191
+ import { relative as relative3 } from "node:path";
108192
+ function boundedSeconds(value, maxSeconds = 86400) {
108193
+ if (typeof value !== "number" || !Number.isFinite(value))
108194
+ return null;
108195
+ if (value <= 0 || value > maxSeconds)
108196
+ return null;
108197
+ return Math.ceil(value);
108198
+ }
108199
+ function boundedCount(value, max = 1e6) {
108200
+ if (typeof value !== "number" || !Number.isInteger(value))
108201
+ return null;
108202
+ if (value < 0 || value > max)
108203
+ return null;
108204
+ return value;
108205
+ }
108206
+ var ISO_INSTANT = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|([+-])(\d{2}):(\d{2}))$/;
108207
+ function daysInMonth(year, month) {
108208
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
108209
+ }
108210
+ function isoInstant(value, opts = {}) {
108211
+ if (typeof value !== "string")
108212
+ return null;
108213
+ const match = ISO_INSTANT.exec(value);
108214
+ if (!match)
108215
+ return null;
108216
+ const [, y, mo, d, h, mi, s, , offH, offM] = match;
108217
+ const year = Number(y);
108218
+ const month = Number(mo);
108219
+ const day = Number(d);
108220
+ if (month < 1 || month > 12)
108221
+ return null;
108222
+ if (day < 1 || day > daysInMonth(year, month))
108223
+ return null;
108224
+ if (Number(h) > 23 || Number(mi) > 59)
108225
+ return null;
108226
+ if (s !== undefined && Number(s) > 59)
108227
+ return null;
108228
+ if (offH !== undefined && (Number(offH) > 14 || Number(offM) > 59))
108229
+ return null;
108230
+ const ms = new Date(value).getTime();
108231
+ if (Number.isNaN(ms))
108232
+ return null;
108233
+ const now = (opts.now ?? Date.now)();
108234
+ const delta = ms - now;
108235
+ if (delta < -(opts.maxPastMs ?? 60 * 60000))
108236
+ return null;
108237
+ if (delta > (opts.maxFutureMs ?? 48 * 60 * 60000))
108238
+ return null;
108239
+ return value;
108240
+ }
108241
+ var ERROR_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
108242
+ function errorCode(value) {
108243
+ return typeof value === "string" && ERROR_CODE.test(value) ? value : null;
108244
+ }
108245
+ function quotaKind(value) {
108246
+ return value === "live" || value === "preview" ? value : null;
108247
+ }
108248
+ function isEnvKey(value) {
108249
+ return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value);
108250
+ }
108251
+ function envKeyList(value, max = 50) {
108252
+ if (!Array.isArray(value))
108253
+ return [];
108254
+ return [...new Set(value.filter(isEnvKey))].slice(0, max);
108255
+ }
108256
+ var MINTED_ID = /^[a-z0-9][a-z0-9_-]{0,39}$/;
108257
+ function idToken(value) {
108258
+ return typeof value === "string" && MINTED_ID.test(value) ? value : null;
108259
+ }
108260
+ function appRef(value) {
108261
+ return idToken(value);
108262
+ }
108263
+ var CONFIG_KEY_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/;
108264
+ function configKeyPath(segments, maxDepth = 8) {
108265
+ if (!Array.isArray(segments) || segments.length === 0)
108266
+ return null;
108267
+ const parts = [];
108268
+ for (const segment of segments.slice(0, maxDepth)) {
108269
+ if (typeof segment === "number") {
108270
+ if (!Number.isInteger(segment) || segment < 0 || segment > 9999)
108271
+ break;
108272
+ parts.push(String(segment));
108273
+ continue;
108274
+ }
108275
+ if (typeof segment !== "string" || !CONFIG_KEY_SEGMENT.test(segment))
108276
+ break;
108277
+ parts.push(segment);
108278
+ }
108279
+ return parts.length > 0 ? parts.join(".") : null;
108280
+ }
108281
+ function boundedLine(value, max = 1e7) {
108282
+ if (typeof value !== "number" || !Number.isInteger(value))
108283
+ return null;
108284
+ if (value < 1 || value > max)
108285
+ return null;
108286
+ return value;
108287
+ }
108288
+ function parseRemoteEnvProblems(value, max = 50) {
108289
+ if (!Array.isArray(value))
108290
+ return [];
108291
+ const out = [];
108292
+ const scanLimit = Math.min(value.length, max * 20);
108293
+ for (let i = 0;i < scanLimit; i++) {
108294
+ const entry = value[i];
108295
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
108296
+ continue;
108297
+ const { key, file: file2, line } = entry;
108298
+ if (!isEnvKey(key) || !isSafeRelativePath(file2))
108299
+ continue;
108300
+ const parsedLine = boundedLine(line);
108301
+ out.push({ key, file: file2, ...parsedLine !== null ? { line: parsedLine } : {} });
108302
+ if (out.length >= max)
108303
+ break;
108304
+ }
108305
+ return out;
108306
+ }
108307
+ var PATH_SEGMENT = /^[A-Za-z0-9._@+ ()'[\]-]+$/;
108308
+ var RESERVED_DEVICE_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
108309
+ function isSafeRelativePath(value) {
108310
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024)
108311
+ return false;
108312
+ if (value.includes("\x00"))
108313
+ return false;
108314
+ const segments = value.replace(/\\/g, "/").split("/");
108315
+ return segments.every((s) => {
108316
+ if (s === "." || s === ".." || !PATH_SEGMENT.test(s))
108317
+ return false;
108318
+ if (/[ .]$/.test(s))
108319
+ return false;
108320
+ return !RESERVED_DEVICE_NAME.test(s);
108321
+ });
108322
+ }
108323
+ function projectRelative(cwd, abs) {
108324
+ const rel = relative3(cwd, abs);
108325
+ return isSafeRelativePath(rel) ? rel : null;
108326
+ }
108327
+
108104
108328
  // ../core/dist/commands/data.js
108105
108329
  var dataInputSchema = exports_external.object({
108106
108330
  app: exports_external.string().optional(),
@@ -108126,7 +108350,9 @@ async function data(ctx, input = {}) {
108126
108350
  } catch (err) {
108127
108351
  const code = err.code;
108128
108352
  if (code === "PB_AUTH_FAILED") {
108129
- warning = `Could not authenticate with PocketBase — collections + stats unavailable. Try \`percher data reset-superuser ${name}\` to rotate the credentials.`;
108353
+ const ref = appRef(name) ?? "<app>";
108354
+ const verdict = err.extra?.verdict;
108355
+ warning = verdict === "rejected" ? `Percher has no working PocketBase admin credentials for this app; collections and counts are unavailable. The app owner can issue new ones with \`percher data reset-superuser ${ref}\`.` : `PocketBase authentication failed; collections and counts are unavailable. Retry \`percher data ${ref}\`; if this continues, contact support.`;
108130
108356
  } else {
108131
108357
  throw err;
108132
108358
  }
@@ -108253,7 +108479,7 @@ function isTransientPollError(err) {
108253
108479
  if (isTransientNetworkError(err))
108254
108480
  return true;
108255
108481
  if (err instanceof PercherApiError)
108256
- return err.status === 502 || err.status === 503 || err.status === 504;
108482
+ return err.status === 429 || err.status === 502 || err.status === 503 || err.status === 504;
108257
108483
  return err instanceof TypeError && /terminated/i.test(err.message);
108258
108484
  }
108259
108485
  async function pollDeployment(opts) {
@@ -108287,7 +108513,7 @@ async function pollDeployment(opts) {
108287
108513
  throw err;
108288
108514
  if (attempt === 1) {
108289
108515
  streakStartedAt = Date.now();
108290
- opts.ctx.status("Lost contact with Percher; retrying while the deploy continues...");
108516
+ opts.ctx.status(err instanceof PercherApiError && err.status === 429 ? "Percher asked this client to slow down; retrying while the deploy continues..." : "Lost contact with Percher; retrying while the deploy continues...");
108291
108517
  }
108292
108518
  if (Date.now() - startedAt > timeoutMs)
108293
108519
  opts.onTimeout(deployment);
@@ -108321,143 +108547,204 @@ async function drainNewEvents(opts, deployId, seenEventIds) {
108321
108547
  } catch {}
108322
108548
  }
108323
108549
 
108324
- // ../core/dist/untrusted.js
108325
- import { relative as relative3 } from "node:path";
108326
- function boundedSeconds(value, maxSeconds = 86400) {
108327
- if (typeof value !== "number" || !Number.isFinite(value))
108328
- return null;
108329
- if (value <= 0 || value > maxSeconds)
108330
- return null;
108331
- return Math.ceil(value);
108332
- }
108333
- function boundedCount(value, max = 1e6) {
108334
- if (typeof value !== "number" || !Number.isInteger(value))
108335
- return null;
108336
- if (value < 0 || value > max)
108337
- return null;
108338
- return value;
108339
- }
108340
- var ISO_INSTANT = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|([+-])(\d{2}):(\d{2}))$/;
108341
- function daysInMonth(year, month) {
108342
- return new Date(Date.UTC(year, month, 0)).getUTCDate();
108343
- }
108344
- function isoInstant(value, opts = {}) {
108345
- if (typeof value !== "string")
108346
- return null;
108347
- const match = ISO_INSTANT.exec(value);
108348
- if (!match)
108349
- return null;
108350
- const [, y, mo, d, h, mi, s, , offH, offM] = match;
108351
- const year = Number(y);
108352
- const month = Number(mo);
108353
- const day = Number(d);
108354
- if (month < 1 || month > 12)
108355
- return null;
108356
- if (day < 1 || day > daysInMonth(year, month))
108357
- return null;
108358
- if (Number(h) > 23 || Number(mi) > 59)
108359
- return null;
108360
- if (s !== undefined && Number(s) > 59)
108361
- return null;
108362
- if (offH !== undefined && (Number(offH) > 14 || Number(offM) > 59))
108363
- return null;
108364
- const ms = new Date(value).getTime();
108365
- if (Number.isNaN(ms))
108366
- return null;
108367
- const now = (opts.now ?? Date.now)();
108368
- const delta = ms - now;
108369
- if (delta < -(opts.maxPastMs ?? 60 * 60000))
108370
- return null;
108371
- if (delta > (opts.maxFutureMs ?? 48 * 60 * 60000))
108372
- return null;
108373
- return value;
108374
- }
108375
- var ERROR_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
108376
- function errorCode(value) {
108377
- return typeof value === "string" && ERROR_CODE.test(value) ? value : null;
108378
- }
108379
- function quotaKind(value) {
108380
- return value === "live" || value === "preview" ? value : null;
108381
- }
108382
- function isEnvKey(value) {
108383
- return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value);
108384
- }
108385
- function envKeyList(value, max = 50) {
108386
- if (!Array.isArray(value))
108387
- return [];
108388
- return [...new Set(value.filter(isEnvKey))].slice(0, max);
108389
- }
108390
- var MINTED_ID = /^[a-z0-9][a-z0-9_-]{0,39}$/;
108391
- function idToken(value) {
108392
- return typeof value === "string" && MINTED_ID.test(value) ? value : null;
108393
- }
108394
- function appRef(value) {
108395
- return idToken(value);
108396
- }
108397
- var CONFIG_KEY_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/;
108398
- function configKeyPath(segments, maxDepth = 8) {
108399
- if (!Array.isArray(segments) || segments.length === 0)
108400
- return null;
108401
- const parts = [];
108402
- for (const segment of segments.slice(0, maxDepth)) {
108403
- if (typeof segment === "number") {
108404
- if (!Number.isInteger(segment) || segment < 0 || segment > 9999)
108405
- break;
108406
- parts.push(String(segment));
108407
- continue;
108408
- }
108409
- if (typeof segment !== "string" || !CONFIG_KEY_SEGMENT.test(segment))
108410
- break;
108411
- parts.push(segment);
108550
+ // ../core/dist/structured-error-codes.js
108551
+ var MAPPINGS = {
108552
+ PB_PROVISION_FAILED: {
108553
+ errorClass: "pb_provision_failed",
108554
+ phase: "infra",
108555
+ cause: "pb_provision_failed",
108556
+ title: "Database admin setup failed",
108557
+ explanation: () => "The platform could not set up the database admin account.",
108558
+ suggestion: "Publishing again after the platform failure is resolved is safe."
108559
+ },
108560
+ BUILD_SECRET_KEY_UNAVAILABLE: {
108561
+ errorClass: "build_secrets_unavailable",
108562
+ phase: "infra",
108563
+ cause: "build_secret_key_unavailable",
108564
+ title: "Percher could not protect build secrets",
108565
+ explanation: () => "Percher's key for build secrets is unavailable, so it refused the build rather than run it without them. This is a platform issue and does not count against your app.",
108566
+ suggestion: "Contact support. Removing the non-public keys from [build] pass_env lets the build run without them."
108567
+ },
108568
+ BUILD_SECRETS_UNSUPPORTED_NIXPACKS: {
108569
+ errorClass: "build_secrets_unavailable",
108570
+ phase: "infra",
108571
+ cause: "build_secrets_unsupported_nixpacks",
108572
+ title: "This builder cannot mount build secrets",
108573
+ explanation: () => "Percher is building this app with its fallback builder, which cannot keep build secrets out of the image, so it refused the build. It does not count against your app.",
108574
+ suggestion: "Remove the non-public keys from [build] pass_env, or deploy again later."
108575
+ },
108576
+ SHARED_NETWORK_FORBIDDEN: {
108577
+ errorClass: "infra_unavailable",
108578
+ phase: "infra",
108579
+ cause: "shared_network_forbidden",
108580
+ title: "Percher refused unsafe network placement",
108581
+ explanation: () => "This app is still on Percher's shared bridge. Percher refused to start another container there. This is a platform issue and does not count against your app.",
108582
+ suggestion: "Contact support. A Percher operator must migrate the app to its own network before you retry."
108583
+ },
108584
+ PLATFORM_DISK_LOW: {
108585
+ errorClass: "platform_at_capacity",
108586
+ phase: "infra",
108587
+ cause: "disk_headroom_exhausted",
108588
+ title: "Platform storage is too low",
108589
+ explanation: () => "Percher paused new deploys: the platform is nearly out of disk. Not your app — nothing to change. Deployed apps keep running; only new deploys are paused.",
108590
+ suggestion: "The operator is alerted automatically. Run `percher publish` again in ~15 min; this failure does not count against your app. Still failing after an hour: https://percher.app/status"
108591
+ },
108592
+ PLATFORM_DISK_UNKNOWN: {
108593
+ errorClass: "platform_at_capacity",
108594
+ phase: "infra",
108595
+ cause: "disk_state_unreadable",
108596
+ title: "Platform storage state is unknown",
108597
+ explanation: () => "Percher paused new deploys: it can't read how much disk is left, and won't assume there's room. Not your app. Deployed apps have not been stopped by this.",
108598
+ suggestion: "Run `percher publish` again in ~5 min; this failure does not count against your app. Still failing after 15 min: https://percher.app/status"
108599
+ },
108600
+ PLAN_POOL_EXCEEDED: {
108601
+ errorClass: "plan_pool_exceeded",
108602
+ phase: "start",
108603
+ cause: "plan_pool_exceeded",
108604
+ title: "Plan resource pool is full",
108605
+ explanation: (raw) => raw ? `${trimMessage(raw)} The platform is fine — the account is at its plan's limit.` : "This deploy would commit more RAM, vCPU or disk than your plan's pool allows across all your apps. The platform is fine — the account is at its limit.",
108606
+ suggestion: "Reduce `[resources]` in this app's percher.toml, delete an app you no longer need, or upgrade at https://percher.app/account/billing. Retrying unchanged will fail the same way."
108607
+ },
108608
+ PLACEMENT_UNDETERMINED: {
108609
+ errorClass: "infra_unavailable",
108610
+ phase: "infra",
108611
+ cause: "placement_undetermined",
108612
+ title: "Percher could not place your app's container",
108613
+ explanation: (raw) => raw ? `${trimMessage(raw)} This is a platform-side read of Percher's own container placement, not anything in your app.` : "Percher could not read which internal network this app's current containers are on, and refused to guess rather than risk splitting the app from its database across two networks.",
108614
+ suggestion: "Nothing to change in your app. Retry in a minute; if it persists, the platform is having trouble reaching its container daemon and Percher's operators are alerted."
108615
+ },
108616
+ PLATFORM_MEMORY_LOW: {
108617
+ errorClass: "platform_at_capacity",
108618
+ phase: "infra",
108619
+ cause: "box_admission_rejected",
108620
+ title: "Platform is at capacity",
108621
+ explanation: (raw) => raw ? `Percher couldn't fit this deploy on the box right now: ${trimMessage(raw)} Not your app — nothing to change. Deployed apps keep running.` : "Percher couldn't fit this deploy on the box right now — the admission gate refused to start the container. Not your app — nothing to change. Deployed apps keep running.",
108622
+ suggestion: "Run `percher publish` again in a few minutes; capacity frees up as other deploys finish, and this failure does not count against your app. Still failing after an hour: https://percher.app/status"
108623
+ },
108624
+ PB_BRIDGE_UNAVAILABLE: {
108625
+ errorClass: "infra_unavailable",
108626
+ phase: "infra",
108627
+ cause: "pb_bridge_unreachable",
108628
+ title: "PocketBase is unreachable from the platform",
108629
+ explanation: (raw) => raw ? `Percher could not reach your app's database through its internal bridge: ${trimMessage(raw)}. This is a platform-side issue — your data and your code are both fine.` : "Percher could not reach your app's database through its internal bridge. This is a platform-side issue — your data and your code are both fine.",
108630
+ suggestion: "Wait a minute and run `percher publish` again; this failure does not count against your app. Your data was not touched — the platform could not read it, which is not the same as it being missing. Still failing after 15 min: https://percher.app/status"
108631
+ },
108632
+ PB_ROTATION_UNVERIFIED: {
108633
+ errorClass: "infra_unavailable",
108634
+ phase: "infra",
108635
+ cause: "pb_rotation_unverified",
108636
+ title: "PocketBase admin password was rotated but not confirmed",
108637
+ explanation: (raw) => raw ? `${trimMessage(raw)} The rotation itself ran, so the previously stored admin password may now be stale. Your app data was not modified — only the admin credential changed.` : "Percher rotated your app's PocketBase admin password, then could not confirm the rotation. The previously stored admin password may now be stale. Your app data was not modified — only the admin credential changed.",
108638
+ suggestion: "Run the superuser reset again (`percher data reset-superuser` or the dashboard's reset button) — the rotation is idempotent, and a successful re-run stores a fresh working credential. This failure does not count against your app."
108639
+ },
108640
+ PB_OPERATION_BUSY: {
108641
+ errorClass: "pb_operation_busy",
108642
+ phase: "start",
108643
+ cause: "pb_backup_restore_busy",
108644
+ title: "PocketBase is finishing another data operation",
108645
+ explanation: (raw) => raw ? `${trimMessage(raw)} The operation is scoped to this app and must finish before Percher can create the deploy checkpoint.` : "PocketBase is already creating a backup or restoring data for this app, so Percher could not create the deploy checkpoint yet.",
108646
+ suggestion: "Wait for the current backup, export or restore to finish, then publish again. If no operation is visible after a few minutes, contact support before retrying repeatedly."
108647
+ },
108648
+ CANARY_FAILED: {
108649
+ errorClass: "runtime_crash",
108650
+ phase: "start",
108651
+ cause: "canary_failed",
108652
+ title: "Your app went live, then failed its post-deploy check",
108653
+ explanation: (raw) => raw ? `${trimMessage(raw)} The container started and passed its health check, so this is something that only shows up once the app is serving real requests.` : "Percher swapped your new version in, then its own public URL did not answer correctly. The container started and passed its health check, so this is something that only shows up once the app is serving real requests.",
108654
+ suggestion: "Run `percher doctor` — it re-probes the container and the public route and will say which half is failing. The container log from the moment it was rolled back is in the build log below. Retrying unchanged will usually fail the same way."
108655
+ },
108656
+ APP_DATA_MOUNT_CONFLICT: {
108657
+ errorClass: "app_data_mount_conflict",
108658
+ phase: "start",
108659
+ cause: "app_data_mount_conflict",
108660
+ title: "Your image has a file where Percher mounts /app/data",
108661
+ explanation: () => "The image built, but Docker could not start the container: every Percher app gets its persistent data volume mounted at /app/data, and this image has a file (or a link to one) at /app or /app/data, so that directory cannot be created.",
108662
+ suggestion: "Install the binary somewhere else, such as /usr/local/bin/app, and point ENTRYPOINT/CMD at it. If the project has a file named `data` at its root, rename it. Retrying unchanged will fail the same way."
108663
+ },
108664
+ WORKER_UNAVAILABLE: {
108665
+ errorClass: "infra_unavailable",
108666
+ phase: "infra",
108667
+ cause: "worker_fetch_failed",
108668
+ title: "Build worker is unreachable",
108669
+ explanation: (raw) => raw ? `The Percher build worker did not respond in time: ${trimMessage(raw)}. This is a platform-side issue, not your code.` : "The Percher build worker did not respond in time. This is a platform-side issue, not your code.",
108670
+ suggestion: "Wait 30 seconds and run `percher publish` again. If retry #2 also fails, pause 5 minutes before the next attempt — back-to-back retries during a partial outage just waste your time. After 3 spaced-out retries, check status.percher.app or contact support. Your build never started."
108671
+ },
108672
+ WORKER_UNAVAILABLE_PERSISTENT: {
108673
+ errorClass: "infra_unavailable",
108674
+ phase: "infra",
108675
+ cause: "worker_circuit_breaker_open",
108676
+ title: "Build worker is in a crash loop",
108677
+ explanation: () => "The Percher build worker has failed repeatedly in the last minute and the platform's circuit breaker has flipped open. Retrying immediately would just queue more failures.",
108678
+ suggestion: "Wait 2-3 minutes for the platform to recover, then retry. If it persists past 5 minutes check status.percher.app or contact support — this is a platform incident, not your code."
108679
+ },
108680
+ BUILD_TIMEOUT: {
108681
+ errorClass: "build_failed",
108682
+ phase: "build",
108683
+ cause: "build_timeout",
108684
+ title: "Build timed out",
108685
+ explanation: () => "The build ran longer than the 10-minute platform limit and was aborted.",
108686
+ suggestion: "Common causes: a slow `postinstall` hook, very large dependency tree, or a Dockerfile that pulls a huge base image. Try `bun install` locally and check what's slow. If it's legitimately a heavy build, contact support to discuss raising the limit for your account."
108687
+ },
108688
+ BUILD_FAILED: {
108689
+ errorClass: "build_failed",
108690
+ phase: "build",
108691
+ cause: "build_nonzero_exit",
108692
+ title: "Build failed",
108693
+ explanation: (raw) => raw ? `The build process returned a non-zero exit code: ${trimMessage(raw)}. Read the build log below for the exact error.` : "The build process returned a non-zero exit code. Read the build log below for the exact error.",
108694
+ suggestion: "Run your build locally to reproduce: `bun install && bun run build`. The error in the build log usually points to a missing dependency, a TypeScript/syntax error, or a misconfigured framework setting."
108695
+ },
108696
+ WORKER_ERROR: {
108697
+ errorClass: "infra_unavailable",
108698
+ phase: "infra",
108699
+ cause: "worker_internal_error",
108700
+ title: "Platform error inside the build worker",
108701
+ explanation: (raw) => raw ? `The build worker hit an internal error: ${trimMessage(raw)}. This is a Percher bug, not your code.` : "The build worker hit an internal error. This is a Percher bug, not your code.",
108702
+ suggestion: "Retry once — transient platform errors usually clear within a minute. If it repeats, please report it: include the deploy ID and we'll investigate. Your account is not at fault."
108703
+ },
108704
+ WORKER_ENDPOINT_MISSING: {
108705
+ errorClass: "infra_unavailable",
108706
+ phase: "infra",
108707
+ cause: "worker_endpoint_missing",
108708
+ title: "Platform worker is mid-upgrade",
108709
+ explanation: (raw) => raw ? `${trimMessage(raw)} The platform's worker process predates an endpoint this request needs — a rolling-deploy skew, not your code.` : "The platform's worker process predates an endpoint this request needs — a rolling-deploy skew, not your code.",
108710
+ suggestion: "Retry in a few minutes; the operator finishes the platform deploy worker-first. This failure does not count against your app."
108711
+ },
108712
+ DEPLOY_STALLED: {
108713
+ errorClass: "infra_unavailable",
108714
+ phase: "infra",
108715
+ cause: "deploy_stalled",
108716
+ title: "Deploy stalled",
108717
+ explanation: () => "The platform abandoned this deploy — likely because the API or build worker restarted mid-build. Your code is fine; the build never finished.",
108718
+ suggestion: "Run `percher publish` again. The new deploy will start fresh. If you see this repeatedly across multiple publishes, check status.percher.app — it implies a recurring platform issue."
108412
108719
  }
108413
- return parts.length > 0 ? parts.join(".") : null;
108414
- }
108415
- function boundedLine(value, max = 1e7) {
108416
- if (typeof value !== "number" || !Number.isInteger(value))
108720
+ };
108721
+ function classifyByErrorCode(code, rawMessage) {
108722
+ if (!code)
108417
108723
  return null;
108418
- if (value < 1 || value > max)
108724
+ const mapping = MAPPINGS[code];
108725
+ if (!mapping)
108419
108726
  return null;
108420
- return value;
108421
- }
108422
- function parseRemoteEnvProblems(value, max = 50) {
108423
- if (!Array.isArray(value))
108424
- return [];
108425
- const out = [];
108426
- const scanLimit = Math.min(value.length, max * 20);
108427
- for (let i = 0;i < scanLimit; i++) {
108428
- const entry = value[i];
108429
- if (typeof entry !== "object" || entry === null || Array.isArray(entry))
108430
- continue;
108431
- const { key, file: file2, line } = entry;
108432
- if (!isEnvKey(key) || !isSafeRelativePath(file2))
108433
- continue;
108434
- const parsedLine = boundedLine(line);
108435
- out.push({ key, file: file2, ...parsedLine !== null ? { line: parsedLine } : {} });
108436
- if (out.length >= max)
108437
- break;
108438
- }
108439
- return out;
108440
- }
108441
- var PATH_SEGMENT = /^[A-Za-z0-9._@+ ()'[\]-]+$/;
108442
- var RESERVED_DEVICE_NAME = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
108443
- function isSafeRelativePath(value) {
108444
- if (typeof value !== "string" || value.length === 0 || value.length > 1024)
108445
- return false;
108446
- if (value.includes("\x00"))
108447
- return false;
108448
- const segments = value.replace(/\\/g, "/").split("/");
108449
- return segments.every((s) => {
108450
- if (s === "." || s === ".." || !PATH_SEGMENT.test(s))
108451
- return false;
108452
- if (/[ .]$/.test(s))
108453
- return false;
108454
- return !RESERVED_DEVICE_NAME.test(s);
108455
- });
108727
+ return {
108728
+ title: mapping.title,
108729
+ explanation: mapping.explanation(rawMessage),
108730
+ suggestion: mapping.suggestion,
108731
+ errorClass: mapping.errorClass,
108732
+ phase: mapping.phase,
108733
+ cause: mapping.cause,
108734
+ relevantFiles: [],
108735
+ missingEnvVars: []
108736
+ };
108456
108737
  }
108457
- function projectRelative(cwd, abs) {
108458
- const rel = relative3(cwd, abs);
108459
- return isSafeRelativePath(rel) ? rel : null;
108738
+ function trimMessage(raw) {
108739
+ const firstLine = raw.split(`
108740
+ `)[0] ?? "";
108741
+ return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine;
108460
108742
  }
108743
+ var PB_PROVISION_FAILED_ASK = {
108744
+ prompt: "The platform could not set up the database admin account. Publishing again after the platform failure is resolved is safe.",
108745
+ reasonCode: "pb_provision_failed",
108746
+ retryable: true
108747
+ };
108461
108748
 
108462
108749
  // ../core/dist/recovery.js
108463
108750
  function recoveryNone(opts = {}) {
@@ -108688,12 +108975,25 @@ function unprovenPlatformClaim(failure) {
108688
108975
  }
108689
108976
  function recoveryFromErrorClass(errorClass, failure) {
108690
108977
  switch (errorClass) {
108978
+ case "build_secrets_unavailable":
108979
+ return recoveryAsk({
108980
+ prompt: "Percher cannot protect build secrets for this build. Should I remove the non-public keys from [build] pass_env and publish again, or wait and publish later?",
108981
+ options: [
108982
+ "remove non-public keys from [build] pass_env and publish again",
108983
+ "wait and publish later"
108984
+ ],
108985
+ reasonCode: "build_secrets_unavailable"
108986
+ });
108691
108987
  case "missing_env":
108692
108988
  return recoveryEnv({
108693
108989
  app: failure.appName,
108694
108990
  keys: failure.missingEnvVars ?? [],
108695
108991
  reasonCode: "missing_env"
108696
108992
  });
108993
+ case "pb_provision_failed":
108994
+ if (failure.classSource !== "code")
108995
+ return unprovenPlatformClaim(failure);
108996
+ return recoveryAsk(PB_PROVISION_FAILED_ASK);
108697
108997
  case "infra_unavailable":
108698
108998
  if (failure.classSource !== "code")
108699
108999
  return unprovenPlatformClaim(failure);
@@ -108786,7 +109086,7 @@ function recoveryFromErrorClass(errorClass, failure) {
108786
109086
 
108787
109087
  // ../core/dist/commands/publish-failure.js
108788
109088
  init_dist();
108789
- import { join as join9 } from "node:path";
109089
+ import { join as join10 } from "node:path";
108790
109090
 
108791
109091
  // ../core/dist/error-classifier.js
108792
109092
  function extractEnvVarsFromLog(combined) {
@@ -108804,6 +109104,19 @@ function extractModuleFromLog(combined) {
108804
109104
  return match?.[1];
108805
109105
  }
108806
109106
  var patterns = [
109107
+ {
109108
+ pattern: /PB_PROVISION_FAILED|the platform could not set up the database admin account/i,
109109
+ result: {
109110
+ title: "Database admin setup failed",
109111
+ explanation: "The platform could not set up the database admin account.",
109112
+ suggestion: "Publishing again after the platform failure is resolved is safe.",
109113
+ errorClass: "pb_provision_failed",
109114
+ phase: "infra",
109115
+ cause: "pb_provision_failed",
109116
+ relevantFiles: [],
109117
+ missingEnvVars: []
109118
+ }
109119
+ },
108807
109120
  {
108808
109121
  pattern: /DEPLOY_STALLED:/,
108809
109122
  result: {
@@ -109172,171 +109485,9 @@ ${buildLog ?? ""}`;
109172
109485
  return null;
109173
109486
  }
109174
109487
 
109175
- // ../core/dist/structured-error-codes.js
109176
- var MAPPINGS = {
109177
- PLATFORM_DISK_LOW: {
109178
- errorClass: "platform_at_capacity",
109179
- phase: "infra",
109180
- cause: "disk_headroom_exhausted",
109181
- title: "Platform storage is too low",
109182
- explanation: () => "Percher paused new deploys: the platform is nearly out of disk. Not your app — nothing to change. Deployed apps keep running; only new deploys are paused.",
109183
- suggestion: "The operator is alerted automatically. Run `percher publish` again in ~15 min; this failure does not count against your app. Still failing after an hour: https://percher.app/status"
109184
- },
109185
- PLATFORM_DISK_UNKNOWN: {
109186
- errorClass: "platform_at_capacity",
109187
- phase: "infra",
109188
- cause: "disk_state_unreadable",
109189
- title: "Platform storage state is unknown",
109190
- explanation: () => "Percher paused new deploys: it can't read how much disk is left, and won't assume there's room. Not your app. Deployed apps have not been stopped by this.",
109191
- suggestion: "Run `percher publish` again in ~5 min; this failure does not count against your app. Still failing after 15 min: https://percher.app/status"
109192
- },
109193
- PLAN_POOL_EXCEEDED: {
109194
- errorClass: "plan_pool_exceeded",
109195
- phase: "start",
109196
- cause: "plan_pool_exceeded",
109197
- title: "Plan resource pool is full",
109198
- explanation: (raw) => raw ? `${trimMessage(raw)} The platform is fine — the account is at its plan's limit.` : "This deploy would commit more RAM, vCPU or disk than your plan's pool allows across all your apps. The platform is fine — the account is at its limit.",
109199
- suggestion: "Reduce `[resources]` in this app's percher.toml, delete an app you no longer need, or upgrade at https://percher.app/account/billing. Retrying unchanged will fail the same way."
109200
- },
109201
- PLACEMENT_UNDETERMINED: {
109202
- errorClass: "infra_unavailable",
109203
- phase: "infra",
109204
- cause: "placement_undetermined",
109205
- title: "Percher could not place your app's container",
109206
- explanation: (raw) => raw ? `${trimMessage(raw)} This is a platform-side read of Percher's own container placement, not anything in your app.` : "Percher could not read which internal network this app's current containers are on, and refused to guess rather than risk splitting the app from its database across two networks.",
109207
- suggestion: "Nothing to change in your app. Retry in a minute; if it persists, the platform is having trouble reaching its container daemon and Percher's operators are alerted."
109208
- },
109209
- PLATFORM_MEMORY_LOW: {
109210
- errorClass: "platform_at_capacity",
109211
- phase: "infra",
109212
- cause: "box_admission_rejected",
109213
- title: "Platform is at capacity",
109214
- explanation: (raw) => raw ? `Percher couldn't fit this deploy on the box right now: ${trimMessage(raw)} Not your app — nothing to change. Deployed apps keep running.` : "Percher couldn't fit this deploy on the box right now — the admission gate refused to start the container. Not your app — nothing to change. Deployed apps keep running.",
109215
- suggestion: "Run `percher publish` again in a few minutes; capacity frees up as other deploys finish, and this failure does not count against your app. Still failing after an hour: https://percher.app/status"
109216
- },
109217
- PB_BRIDGE_UNAVAILABLE: {
109218
- errorClass: "infra_unavailable",
109219
- phase: "infra",
109220
- cause: "pb_bridge_unreachable",
109221
- title: "PocketBase is unreachable from the platform",
109222
- explanation: (raw) => raw ? `Percher could not reach your app's database through its internal bridge: ${trimMessage(raw)}. This is a platform-side issue — your data and your code are both fine.` : "Percher could not reach your app's database through its internal bridge. This is a platform-side issue — your data and your code are both fine.",
109223
- suggestion: "Wait a minute and run `percher publish` again; this failure does not count against your app. Your data was not touched — the platform could not read it, which is not the same as it being missing. Still failing after 15 min: https://percher.app/status"
109224
- },
109225
- PB_ROTATION_UNVERIFIED: {
109226
- errorClass: "infra_unavailable",
109227
- phase: "infra",
109228
- cause: "pb_rotation_unverified",
109229
- title: "PocketBase admin password was rotated but not confirmed",
109230
- explanation: (raw) => raw ? `${trimMessage(raw)} The rotation itself ran, so the previously stored admin password may now be stale. Your app data was not modified — only the admin credential changed.` : "Percher rotated your app's PocketBase admin password, then could not confirm the rotation. The previously stored admin password may now be stale. Your app data was not modified — only the admin credential changed.",
109231
- suggestion: "Run the superuser reset again (`percher data reset-superuser` or the dashboard's reset button) — the rotation is idempotent, and a successful re-run stores a fresh working credential. This failure does not count against your app."
109232
- },
109233
- PB_OPERATION_BUSY: {
109234
- errorClass: "pb_operation_busy",
109235
- phase: "start",
109236
- cause: "pb_backup_restore_busy",
109237
- title: "PocketBase is finishing another data operation",
109238
- explanation: (raw) => raw ? `${trimMessage(raw)} The operation is scoped to this app and must finish before Percher can create the deploy checkpoint.` : "PocketBase is already creating a backup or restoring data for this app, so Percher could not create the deploy checkpoint yet.",
109239
- suggestion: "Wait for the current backup, export or restore to finish, then publish again. If no operation is visible after a few minutes, contact support before retrying repeatedly."
109240
- },
109241
- CANARY_FAILED: {
109242
- errorClass: "runtime_crash",
109243
- phase: "start",
109244
- cause: "canary_failed",
109245
- title: "Your app went live, then failed its post-deploy check",
109246
- explanation: (raw) => raw ? `${trimMessage(raw)} The container started and passed its health check, so this is something that only shows up once the app is serving real requests.` : "Percher swapped your new version in, then its own public URL did not answer correctly. The container started and passed its health check, so this is something that only shows up once the app is serving real requests.",
109247
- suggestion: "Run `percher doctor` — it re-probes the container and the public route and will say which half is failing. The container log from the moment it was rolled back is in the build log below. Retrying unchanged will usually fail the same way."
109248
- },
109249
- APP_DATA_MOUNT_CONFLICT: {
109250
- errorClass: "app_data_mount_conflict",
109251
- phase: "start",
109252
- cause: "app_data_mount_conflict",
109253
- title: "Your image has a file where Percher mounts /app/data",
109254
- explanation: () => "The image built, but Docker could not start the container: every Percher app gets its persistent data volume mounted at /app/data, and this image has a file (or a link to one) at /app or /app/data, so that directory cannot be created.",
109255
- suggestion: "Install the binary somewhere else, such as /usr/local/bin/app, and point ENTRYPOINT/CMD at it. If the project has a file named `data` at its root, rename it. Retrying unchanged will fail the same way."
109256
- },
109257
- WORKER_UNAVAILABLE: {
109258
- errorClass: "infra_unavailable",
109259
- phase: "infra",
109260
- cause: "worker_fetch_failed",
109261
- title: "Build worker is unreachable",
109262
- explanation: (raw) => raw ? `The Percher build worker did not respond in time: ${trimMessage(raw)}. This is a platform-side issue, not your code.` : "The Percher build worker did not respond in time. This is a platform-side issue, not your code.",
109263
- suggestion: "Wait 30 seconds and run `percher publish` again. If retry #2 also fails, pause 5 minutes before the next attempt — back-to-back retries during a partial outage just waste your time. After 3 spaced-out retries, check status.percher.app or contact support. Your build never started."
109264
- },
109265
- WORKER_UNAVAILABLE_PERSISTENT: {
109266
- errorClass: "infra_unavailable",
109267
- phase: "infra",
109268
- cause: "worker_circuit_breaker_open",
109269
- title: "Build worker is in a crash loop",
109270
- explanation: () => "The Percher build worker has failed repeatedly in the last minute and the platform's circuit breaker has flipped open. Retrying immediately would just queue more failures.",
109271
- suggestion: "Wait 2-3 minutes for the platform to recover, then retry. If it persists past 5 minutes check status.percher.app or contact support — this is a platform incident, not your code."
109272
- },
109273
- BUILD_TIMEOUT: {
109274
- errorClass: "build_failed",
109275
- phase: "build",
109276
- cause: "build_timeout",
109277
- title: "Build timed out",
109278
- explanation: () => "The build ran longer than the 10-minute platform limit and was aborted.",
109279
- suggestion: "Common causes: a slow `postinstall` hook, very large dependency tree, or a Dockerfile that pulls a huge base image. Try `bun install` locally and check what's slow. If it's legitimately a heavy build, contact support to discuss raising the limit for your account."
109280
- },
109281
- BUILD_FAILED: {
109282
- errorClass: "build_failed",
109283
- phase: "build",
109284
- cause: "build_nonzero_exit",
109285
- title: "Build failed",
109286
- explanation: (raw) => raw ? `The build process returned a non-zero exit code: ${trimMessage(raw)}. Read the build log below for the exact error.` : "The build process returned a non-zero exit code. Read the build log below for the exact error.",
109287
- suggestion: "Run your build locally to reproduce: `bun install && bun run build`. The error in the build log usually points to a missing dependency, a TypeScript/syntax error, or a misconfigured framework setting."
109288
- },
109289
- WORKER_ERROR: {
109290
- errorClass: "infra_unavailable",
109291
- phase: "infra",
109292
- cause: "worker_internal_error",
109293
- title: "Platform error inside the build worker",
109294
- explanation: (raw) => raw ? `The build worker hit an internal error: ${trimMessage(raw)}. This is a Percher bug, not your code.` : "The build worker hit an internal error. This is a Percher bug, not your code.",
109295
- suggestion: "Retry once — transient platform errors usually clear within a minute. If it repeats, please report it: include the deploy ID and we'll investigate. Your account is not at fault."
109296
- },
109297
- WORKER_ENDPOINT_MISSING: {
109298
- errorClass: "infra_unavailable",
109299
- phase: "infra",
109300
- cause: "worker_endpoint_missing",
109301
- title: "Platform worker is mid-upgrade",
109302
- explanation: (raw) => raw ? `${trimMessage(raw)} The platform's worker process predates an endpoint this request needs — a rolling-deploy skew, not your code.` : "The platform's worker process predates an endpoint this request needs — a rolling-deploy skew, not your code.",
109303
- suggestion: "Retry in a few minutes; the operator finishes the platform deploy worker-first. This failure does not count against your app."
109304
- },
109305
- DEPLOY_STALLED: {
109306
- errorClass: "infra_unavailable",
109307
- phase: "infra",
109308
- cause: "deploy_stalled",
109309
- title: "Deploy stalled",
109310
- explanation: () => "The platform abandoned this deploy — likely because the API or build worker restarted mid-build. Your code is fine; the build never finished.",
109311
- suggestion: "Run `percher publish` again. The new deploy will start fresh. If you see this repeatedly across multiple publishes, check status.percher.app — it implies a recurring platform issue."
109312
- }
109313
- };
109314
- function classifyByErrorCode(code, rawMessage) {
109315
- if (!code)
109316
- return null;
109317
- const mapping = MAPPINGS[code];
109318
- if (!mapping)
109319
- return null;
109320
- return {
109321
- title: mapping.title,
109322
- explanation: mapping.explanation(rawMessage),
109323
- suggestion: mapping.suggestion,
109324
- errorClass: mapping.errorClass,
109325
- phase: mapping.phase,
109326
- cause: mapping.cause,
109327
- relevantFiles: [],
109328
- missingEnvVars: []
109329
- };
109330
- }
109331
- function trimMessage(raw) {
109332
- const firstLine = raw.split(`
109333
- `)[0] ?? "";
109334
- return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine;
109335
- }
109336
-
109337
109488
  // ../core/dist/commands/publish-failure.js
109338
109489
  function tomlPathFor(cwd) {
109339
- return join9(cwd, "percher.toml");
109490
+ return join10(cwd, "percher.toml");
109340
109491
  }
109341
109492
  async function buildFailureError(ctx, app, deployment, signal) {
109342
109493
  let buildLogTail;
@@ -109521,13 +109672,13 @@ async function deploysInspect(ctx, input) {
109521
109672
  }
109522
109673
  // ../core/dist/commands/dev.js
109523
109674
  import { existsSync as existsSync8 } from "node:fs";
109524
- import { join as join12 } from "node:path";
109675
+ import { join as join13 } from "node:path";
109525
109676
 
109526
109677
  // ../core/dist/tarball.js
109527
109678
  var import_ignore = __toESM(require_ignore(), 1);
109528
109679
  import { createHash as createHash2 } from "node:crypto";
109529
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
109530
- import { join as join10, relative as relative4, sep as sep2 } from "node:path";
109680
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
109681
+ import { join as join11, relative as relative4, sep as sep2 } from "node:path";
109531
109682
  import { Readable } from "node:stream";
109532
109683
  import { createGzip } from "node:zlib";
109533
109684
  function isFilesOptions(options) {
@@ -109551,7 +109702,7 @@ var ALWAYS_IGNORE_SECRETS = [".env", ".env.*"];
109551
109702
  function* walk(dir, baseDir, ig) {
109552
109703
  const entries = readdirSync2(dir);
109553
109704
  for (const name of entries) {
109554
- const full = join10(dir, name);
109705
+ const full = join11(dir, name);
109555
109706
  const rel = relative4(baseDir, full).split(sep2).join("/");
109556
109707
  if (ig.ignores(rel))
109557
109708
  continue;
@@ -109640,20 +109791,20 @@ async function createTarball(options) {
109640
109791
  }
109641
109792
  const ig = import_ignore.default();
109642
109793
  ig.add(DEFAULT_IGNORE);
109643
- const gitignorePath = join10(options.cwd, ".gitignore");
109794
+ const gitignorePath = join11(options.cwd, ".gitignore");
109644
109795
  if (existsSync6(gitignorePath)) {
109645
- ig.add(readFileSync6(gitignorePath, "utf8"));
109796
+ ig.add(readFileSync7(gitignorePath, "utf8"));
109646
109797
  }
109647
- const percherignorePath = join10(options.cwd, ".percherignore");
109798
+ const percherignorePath = join11(options.cwd, ".percherignore");
109648
109799
  if (existsSync6(percherignorePath)) {
109649
- ig.add(readFileSync6(percherignorePath, "utf8"));
109800
+ ig.add(readFileSync7(percherignorePath, "utf8"));
109650
109801
  }
109651
109802
  if (options.extraIgnore)
109652
109803
  ig.add(options.extraIgnore);
109653
109804
  ig.add(ALWAYS_IGNORE_SECRETS);
109654
109805
  const fileBuffers = [];
109655
109806
  for (const rel of walk(options.cwd, options.cwd, ig)) {
109656
- const data2 = readFileSync6(join10(options.cwd, rel));
109807
+ const data2 = readFileSync7(join11(options.cwd, rel));
109657
109808
  fileBuffers.push({ path: rel, size: data2.length, data: data2 });
109658
109809
  }
109659
109810
  return buildTarball(fileBuffers);
@@ -109675,8 +109826,8 @@ function createTarballFromFiles(files, _config) {
109675
109826
 
109676
109827
  // ../core/dist/watcher.js
109677
109828
  var import_ignore2 = __toESM(require_ignore(), 1);
109678
- import { existsSync as existsSync7, readFileSync as readFileSync7, watch } from "node:fs";
109679
- import { join as join11, sep as sep3 } from "node:path";
109829
+ import { existsSync as existsSync7, readFileSync as readFileSync8, watch } from "node:fs";
109830
+ import { join as join12, sep as sep3 } from "node:path";
109680
109831
  var DEFAULT_IGNORE2 = [
109681
109832
  "node_modules",
109682
109833
  ".git",
@@ -109696,9 +109847,9 @@ function createWatcher(options) {
109696
109847
  const debounceMs = options.debounceMs ?? 500;
109697
109848
  const ig = import_ignore2.default();
109698
109849
  ig.add(DEFAULT_IGNORE2);
109699
- const gitignorePath = join11(options.cwd, ".gitignore");
109850
+ const gitignorePath = join12(options.cwd, ".gitignore");
109700
109851
  if (existsSync7(gitignorePath)) {
109701
- ig.add(readFileSync7(gitignorePath, "utf8"));
109852
+ ig.add(readFileSync8(gitignorePath, "utf8"));
109702
109853
  }
109703
109854
  if (options.extraIgnore)
109704
109855
  ig.add(options.extraIgnore);
@@ -109736,7 +109887,7 @@ var devInputSchema = exports_external.object({
109736
109887
  app: exports_external.string().optional()
109737
109888
  });
109738
109889
  async function loadConfig2(ctx, appOverride) {
109739
- const tomlPath = join12(ctx.cwd, "percher.toml");
109890
+ const tomlPath = join13(ctx.cwd, "percher.toml");
109740
109891
  if (!existsSync8(tomlPath)) {
109741
109892
  throw new PercherCoreError("No percher.toml found. Run: percher init", {
109742
109893
  code: "dev.no-app",
@@ -109932,7 +110083,7 @@ async function diff(ctx, input = {}) {
109932
110083
  }
109933
110084
  // ../core/dist/commands/doctor.js
109934
110085
  import { existsSync as existsSync9 } from "node:fs";
109935
- import { join as join13 } from "node:path";
110086
+ import { join as join14 } from "node:path";
109936
110087
 
109937
110088
  // ../core/dist/toml-recovery.js
109938
110089
  function tomlErrorToRecoveryProblems(err) {
@@ -110233,7 +110384,7 @@ async function doctor(ctx, input = {}) {
110233
110384
  });
110234
110385
  return summarize(checks3, { appProvided: !!input.app });
110235
110386
  }
110236
- const tomlPath = join13(ctx.cwd, "percher.toml");
110387
+ const tomlPath = join14(ctx.cwd, "percher.toml");
110237
110388
  const tomlExists = existsSync9(tomlPath);
110238
110389
  let tomlValid = false;
110239
110390
  let tomlProblems;
@@ -111249,8 +111400,8 @@ var forgejoSetupWebhookInputSchema = exports_external.object({
111249
111400
  app: exports_external.string().optional().describe("App name (overrides percher.toml)")
111250
111401
  });
111251
111402
  // ../core/dist/commands/generate.js
111252
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
111253
- import { join as join14 } from "node:path";
111403
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
111404
+ import { join as join15 } from "node:path";
111254
111405
  var generateInputSchema = exports_external.object({
111255
111406
  cwd: exports_external.string().optional()
111256
111407
  });
@@ -111336,7 +111487,7 @@ function generateTypeScript(config2) {
111336
111487
  }
111337
111488
  async function generate(ctx, input = {}) {
111338
111489
  const cwd = input.cwd ?? ctx.cwd;
111339
- const tomlPath = join14(cwd, "percher.toml");
111490
+ const tomlPath = join15(cwd, "percher.toml");
111340
111491
  if (!existsSync10(tomlPath)) {
111341
111492
  throw new PercherCoreError("No percher.toml found", {
111342
111493
  code: "dev.no-app",
@@ -111346,14 +111497,14 @@ async function generate(ctx, input = {}) {
111346
111497
  const config2 = await parseFile(tomlPath);
111347
111498
  const files = [];
111348
111499
  const envExample = generateEnvExample(config2);
111349
- writeFileSync6(join14(cwd, ".env.example"), envExample);
111500
+ writeFileSync7(join15(cwd, ".env.example"), envExample);
111350
111501
  files.push(".env.example");
111351
111502
  const tsTypes = generateTypeScript(config2);
111352
- writeFileSync6(join14(cwd, "percher.generated.d.ts"), tsTypes);
111503
+ writeFileSync7(join15(cwd, "percher.generated.d.ts"), tsTypes);
111353
111504
  files.push("percher.generated.d.ts");
111354
111505
  const agentInstructions = generateAgentInstructions(config2);
111355
- mkdirSync6(join14(cwd, ".percher"), { recursive: true });
111356
- writeFileSync6(join14(cwd, ".percher", "agent-instructions.md"), agentInstructions);
111506
+ mkdirSync7(join15(cwd, ".percher"), { recursive: true });
111507
+ writeFileSync7(join15(cwd, ".percher", "agent-instructions.md"), agentInstructions);
111357
111508
  files.push(".percher/agent-instructions.md");
111358
111509
  return { files };
111359
111510
  }
@@ -111559,11 +111710,17 @@ async function hookRevoke(ctx, input) {
111559
111710
  await ctx.client.hooks.revoke(app.id, input.id);
111560
111711
  }
111561
111712
  // ../core/dist/commands/import-project.js
111562
- import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
111563
- import { join as join15 } from "node:path";
111713
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
111714
+ import { join as join16 } from "node:path";
111564
111715
 
111565
111716
  // ../core/dist/static-docker.js
111566
111717
  var STATIC_DOCKER_PORT = 8080;
111718
+ var INIT_TEMPLATE_FRAMEWORKS = new Set([
111719
+ "astro",
111720
+ "preact",
111721
+ "solid",
111722
+ "vite"
111723
+ ]);
111567
111724
  function renderStaticFrameworkDockerfile(input) {
111568
111725
  return `# Multi-stage build: a Bun stage compiles the static bundle, then a
111569
111726
  # minimal Caddy image serves it. This project uses runtime = "docker"
@@ -111581,7 +111738,7 @@ RUN bun install && bun run build
111581
111738
  # Build pinned Caddy source with refreshed security modules. The scratch
111582
111739
  # runtime carries no shell, package manager, or file capabilities and listens
111583
111740
  # on the unprivileged port :${STATIC_DOCKER_PORT}.
111584
- FROM golang:1.26.7-alpine3.23@sha256:b17af760035fc2f338eed92d448a6c67f2d45438844fc6c60678fa5f99e44b57 AS caddy-source
111741
+ FROM golang:1.26.9-alpine3.23@sha256:6750308561953d451210ed1a8086d36c8c8bdcbf672d38d9d0d424b8a7a0216c AS caddy-source
111585
111742
  ADD --checksum=sha256:78e660d838e45c0b8c89338b325946238f153641aa9925df9ef2ad5e8eced31b \\
111586
111743
  https://github.com/caddyserver/caddy/commit/b2693fb63a30e6d7be0972c3645e9a2c0a500e93.patch /tmp/caddy-cel-go.patch
111587
111744
  RUN go mod download github.com/caddyserver/caddy/v2@v2.11.4 \\
@@ -111591,9 +111748,15 @@ WORKDIR /src
111591
111748
  RUN apk add --no-cache patch \\
111592
111749
  && patch -p1 < /tmp/caddy-cel-go.patch \\
111593
111750
  && go get github.com/go-chi/chi/v5@v5.3.0 github.com/google/cel-go@v0.29.2 \\
111594
- github.com/klauspost/compress@v1.18.7 go.opentelemetry.io/otel@v1.44.0 \\
111595
- golang.org/x/crypto@v0.55.0 golang.org/x/net@v0.58.0 \\
111596
- golang.org/x/text@v0.41.0 google.golang.org/grpc@v1.83.2 \\
111751
+ github.com/klauspost/compress@v1.18.7 go.opentelemetry.io/otel@v1.45.0 \\
111752
+ go.opentelemetry.io/otel/sdk/log@v0.21.0 \\
111753
+ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc@v0.21.0 \\
111754
+ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@v0.21.0 \\
111755
+ go.opentelemetry.io/otel/exporters/stdout/stdoutlog@v0.21.0 \\
111756
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@v1.45.0 \\
111757
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@v1.45.0 \\
111758
+ golang.org/x/crypto@v0.57.0 golang.org/x/net@v0.60.0 \\
111759
+ golang.org/x/text@v0.42.0 google.golang.org/grpc@v1.83.2 \\
111597
111760
  && go mod tidy \\
111598
111761
  && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/caddy ./cmd/caddy \\
111599
111762
  && printf '%s\\n' \\
@@ -111631,6 +111794,15 @@ ${body}
111631
111794
  }
111632
111795
  `;
111633
111796
  }
111797
+ var TEMPLATE_GO_PINS = readGoPins(renderStaticFrameworkDockerfile({ outputDir: "dist" }));
111798
+ function readGoPins(dockerfile) {
111799
+ const modules = {};
111800
+ for (const m of dockerfile.matchAll(/([a-z0-9-]+(?:\.[a-z0-9-]+)+\/[^\s@\\]+)@v(\d[^\s\\]*)/g)) {
111801
+ if (m[1] && m[2])
111802
+ modules[m[1]] = m[2];
111803
+ }
111804
+ return { go: /^FROM golang:(\d+\.\d+\.\d+)/m.exec(dockerfile)?.[1] ?? null, modules };
111805
+ }
111634
111806
 
111635
111807
  // ../core/dist/commands/import-project.js
111636
111808
  var importInputSchema = exports_external.object({
@@ -111638,9 +111810,9 @@ var importInputSchema = exports_external.object({
111638
111810
  app: exports_external.string().optional().describe("App name to import into")
111639
111811
  });
111640
111812
  function detectProvider(cwd) {
111641
- if (existsSync11(join15(cwd, "vercel.json")))
111813
+ if (existsSync11(join16(cwd, "vercel.json")))
111642
111814
  return "vercel";
111643
- if (existsSync11(join15(cwd, "netlify.toml")))
111815
+ if (existsSync11(join16(cwd, "netlify.toml")))
111644
111816
  return "netlify";
111645
111817
  return null;
111646
111818
  }
@@ -111976,7 +112148,7 @@ function mapVercelFramework(framework) {
111976
112148
  return map2[framework ?? ""] ?? { runtime: "node" };
111977
112149
  }
111978
112150
  function parseVercelJson(cwd) {
111979
- const raw = readFileSync8(join15(cwd, "vercel.json"), "utf-8");
112151
+ const raw = readFileSync9(join16(cwd, "vercel.json"), "utf-8");
111980
112152
  const config2 = JSON.parse(raw);
111981
112153
  const warnings = [];
111982
112154
  const envVars = [];
@@ -112032,12 +112204,12 @@ health = "/api/health"
112032
112204
  return { toml, warnings, envVars, staticBuild, redirects, headers };
112033
112205
  }
112034
112206
  function parseNetlifyToml(cwd) {
112035
- const raw = readFileSync8(join15(cwd, "netlify.toml"), "utf-8");
112207
+ const raw = readFileSync9(join16(cwd, "netlify.toml"), "utf-8");
112036
112208
  const warnings = [];
112037
112209
  const envVars = [];
112038
112210
  const buildCommand = raw.match(/command\s*=\s*"([^"]+)"/)?.[1] ?? "bun run build";
112039
112211
  const publishDir = raw.match(/publish\s*=\s*"([^"]+)"/)?.[1] ?? "dist";
112040
- const hasPackageJson = existsSync11(join15(cwd, "package.json"));
112212
+ const hasPackageJson = existsSync11(join16(cwd, "package.json"));
112041
112213
  const looksLikeStaticOutput = publishDir === "dist" || publishDir === "build" || publishDir === "public";
112042
112214
  let toml = `[app]
112043
112215
  name = "imported-app"
@@ -112085,23 +112257,23 @@ health = "/health"
112085
112257
  }
112086
112258
  }
112087
112259
  const sidecarPath = (name) => {
112088
- const root = join15(cwd, name);
112260
+ const root = join16(cwd, name);
112089
112261
  if (existsSync11(root))
112090
112262
  return root;
112091
- const published = join15(cwd, publishDir, name);
112263
+ const published = join16(cwd, publishDir, name);
112092
112264
  return existsSync11(published) ? published : null;
112093
112265
  };
112094
112266
  const redirectCollector = newRedirectCollector();
112095
112267
  const redirectsPath = sidecarPath("_redirects");
112096
112268
  if (redirectsPath) {
112097
- parseNetlifyRedirectsFile(readFileSync8(redirectsPath, "utf-8"), redirectCollector, warnings);
112269
+ parseNetlifyRedirectsFile(readFileSync9(redirectsPath, "utf-8"), redirectCollector, warnings);
112098
112270
  }
112099
112271
  parseNetlifyTomlRedirects(raw, redirectCollector, warnings);
112100
112272
  const redirects = finishRedirects(redirectCollector, warnings, "netlify");
112101
112273
  const headerTable = {};
112102
112274
  const headersPath = sidecarPath("_headers");
112103
112275
  if (headersPath) {
112104
- parseNetlifyHeadersFile(readFileSync8(headersPath, "utf-8"), headerTable, warnings);
112276
+ parseNetlifyHeadersFile(readFileSync9(headersPath, "utf-8"), headerTable, warnings);
112105
112277
  }
112106
112278
  const headers = finishHeaders(headerTable, warnings, "_headers");
112107
112279
  if (/^\s*\[\[headers\]\]/m.test(raw)) {
@@ -112170,11 +112342,11 @@ ${serialize(extras)}`;
112170
112342
  mapped.headerPaths = Object.keys(headers).length;
112171
112343
  }
112172
112344
  }
112173
- const tomlPath = join15(ctx.cwd, "percher.toml");
112345
+ const tomlPath = join16(ctx.cwd, "percher.toml");
112174
112346
  if (existsSync11(tomlPath)) {
112175
112347
  result.warnings.unshift("percher.toml already exists — overwriting with imported config");
112176
112348
  }
112177
- writeFileSync7(tomlPath, toml);
112349
+ writeFileSync8(tomlPath, toml);
112178
112350
  const scaffoldedFiles = [];
112179
112351
  if (result.staticBuild) {
112180
112352
  const targets = [
@@ -112188,12 +112360,12 @@ ${serialize(extras)}`;
112188
112360
  }
112189
112361
  ];
112190
112362
  for (const { name, content } of targets) {
112191
- const target = join15(ctx.cwd, name);
112363
+ const target = join16(ctx.cwd, name);
112192
112364
  if (existsSync11(target)) {
112193
112365
  result.warnings.push(`${name} already exists — left untouched. The imported config expects a multi-stage static build; merge by hand if needed.`);
112194
112366
  continue;
112195
112367
  }
112196
- writeFileSync7(target, content, "utf-8");
112368
+ writeFileSync8(target, content, "utf-8");
112197
112369
  scaffoldedFiles.push(name);
112198
112370
  }
112199
112371
  }
@@ -112215,12 +112387,12 @@ function parsesAsPercherToml(source) {
112215
112387
  }
112216
112388
  }
112217
112389
  // ../core/dist/commands/init.js
112218
- import { existsSync as existsSync13, writeFileSync as writeFileSync8 } from "node:fs";
112219
- import { basename as basename3, join as join17 } from "node:path";
112390
+ import { existsSync as existsSync13, writeFileSync as writeFileSync9 } from "node:fs";
112391
+ import { basename as basename3, join as join18 } from "node:path";
112220
112392
 
112221
112393
  // ../core/dist/detect.js
112222
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
112223
- import { join as join16 } from "node:path";
112394
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
112395
+ import { join as join17 } from "node:path";
112224
112396
  function readPackageJson(src) {
112225
112397
  const raw = src.text("package.json");
112226
112398
  if (raw === null)
@@ -112392,10 +112564,10 @@ function detectFromSource(src) {
112392
112564
  }
112393
112565
  async function detectFramework(cwd) {
112394
112566
  const src = {
112395
- has: (...names) => names.some((n) => existsSync12(join16(cwd, n))),
112567
+ has: (...names) => names.some((n) => existsSync12(join17(cwd, n))),
112396
112568
  text: (name) => {
112397
112569
  try {
112398
- return readFileSync9(join16(cwd, name), "utf8");
112570
+ return readFileSync10(join17(cwd, name), "utf8");
112399
112571
  } catch {
112400
112572
  return null;
112401
112573
  }
@@ -112404,7 +112576,7 @@ async function detectFramework(cwd) {
112404
112576
  return detectFromSource(src);
112405
112577
  }
112406
112578
  function refineBunRuntime(detected, cwd) {
112407
- if (detected.runtime === "node" && (existsSync12(join16(cwd, "bun.lockb")) || existsSync12(join16(cwd, "bun.lock")))) {
112579
+ if (detected.runtime === "node" && (existsSync12(join17(cwd, "bun.lockb")) || existsSync12(join17(cwd, "bun.lock")))) {
112408
112580
  return { ...detected, runtime: "bun" };
112409
112581
  }
112410
112582
  return detected;
@@ -112549,7 +112721,7 @@ function scanEnvVars(_cwd) {
112549
112721
  return {};
112550
112722
  }
112551
112723
  async function init(ctx, input = {}) {
112552
- const path = join17(ctx.cwd, "percher.toml");
112724
+ const path = join18(ctx.cwd, "percher.toml");
112553
112725
  const exists = existsSync13(path);
112554
112726
  if (exists && !input.force) {
112555
112727
  throw new PercherCoreError("percher.toml already exists", {
@@ -112594,7 +112766,7 @@ async function init(ctx, input = {}) {
112594
112766
  if (Object.keys(envVars).length > 0) {
112595
112767
  config2.env = envVars;
112596
112768
  }
112597
- writeFileSync8(path, serialize(config2), "utf8");
112769
+ writeFileSync9(path, serialize(config2), "utf8");
112598
112770
  const scaffoldedFiles = [];
112599
112771
  if (detected?.staticBuild) {
112600
112772
  const targets = [
@@ -112610,10 +112782,10 @@ async function init(ctx, input = {}) {
112610
112782
  }
112611
112783
  ];
112612
112784
  for (const { name: name2, content } of targets) {
112613
- const target = join17(ctx.cwd, name2);
112785
+ const target = join18(ctx.cwd, name2);
112614
112786
  if (existsSync13(target) && !input.force)
112615
112787
  continue;
112616
- writeFileSync8(target, content, "utf8");
112788
+ writeFileSync9(target, content, "utf8");
112617
112789
  scaffoldedFiles.push(name2);
112618
112790
  }
112619
112791
  }
@@ -113690,7 +113862,7 @@ function collectWarnings2(collections, rlsPolicies, unsupported) {
113690
113862
  }
113691
113863
  // ../core/dist/commands/migrate-supabase-rewrite.js
113692
113864
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
113693
- import { dirname as dirname6, join as join20, relative as relative7, resolve as resolve9, sep as sep5 } from "node:path";
113865
+ import { dirname as dirname7, join as join21, relative as relative7, resolve as resolve9, sep as sep5 } from "node:path";
113694
113866
 
113695
113867
  // ../core/dist/commands/migrate-supabase-recipes.js
113696
113868
  var AUTH_RECIPES = {
@@ -113956,7 +114128,7 @@ function isSupabaseClientModule(source) {
113956
114128
  // ../core/dist/commands/migrate-supabase-status.js
113957
114129
  var import_cross_spawn = __toESM(require_cross_spawn(), 1);
113958
114130
  import { readFile as readFile4 } from "node:fs/promises";
113959
- import { join as join19, relative as relative6, resolve as resolve8 } from "node:path";
114131
+ import { join as join20, relative as relative6, resolve as resolve8 } from "node:path";
113960
114132
 
113961
114133
  // ../core/dist/commands/migrate-supabase-sdk.js
113962
114134
  var import_jscodeshift2 = __toESM(require_core3(), 1);
@@ -114893,7 +115065,7 @@ function applyRewrite(path, replacement, kind, pattern, note, flags, state) {
114893
115065
  // ../core/dist/commands/migrate-supabase-walker.js
114894
115066
  var import_ignore3 = __toESM(require_ignore(), 1);
114895
115067
  import { mkdir as mkdir2, readdir, readFile as readFile3, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
114896
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join18, relative as relative5, resolve as resolve7, sep as sep4 } from "node:path";
115068
+ import { dirname as dirname6, isAbsolute as isAbsolute3, join as join19, relative as relative5, resolve as resolve7, sep as sep4 } from "node:path";
114897
115069
  var REWRITABLE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
114898
115070
  var DEFAULT_EXCLUDES = [
114899
115071
  "node_modules",
@@ -115045,7 +115217,7 @@ async function* walkTree(dir, rootAbs, ig) {
115045
115217
  return;
115046
115218
  }
115047
115219
  for (const name of entries) {
115048
- const abs = join18(dir, name);
115220
+ const abs = join19(dir, name);
115049
115221
  const rel = relative5(rootAbs, abs).split(sep4).join("/");
115050
115222
  if (rel.length > 0 && ig.ignores(rel))
115051
115223
  continue;
@@ -115207,7 +115379,7 @@ async function writeWalkResult(input) {
115207
115379
  if (rel.length === 0 || isParentTraversal || isAbsolute3(rel)) {
115208
115380
  throw new Error(`writeWalkResult refused path-traversing file "${file2.relativePath}" → "${abs}" (would escape targetDir "${targetAbs}").`);
115209
115381
  }
115210
- await mkdir2(dirname5(abs), { recursive: true });
115382
+ await mkdir2(dirname6(abs), { recursive: true });
115211
115383
  await writeFile2(abs, file2.rewritten, "utf8");
115212
115384
  written.push(file2.relativePath);
115213
115385
  }
@@ -115482,7 +115654,7 @@ function buildPrompt(state, failing, warning, build) {
115482
115654
  async function readManualRlsCount(rootAbs) {
115483
115655
  for (const dir of ["migration-preview", "."]) {
115484
115656
  try {
115485
- const notes = await readFile4(join19(rootAbs, dir, "MIGRATION_NOTES.md"), "utf8");
115657
+ const notes = await readFile4(join20(rootAbs, dir, "MIGRATION_NOTES.md"), "utf8");
115486
115658
  const m = notes.match(/\*\*RLS policies:\*\*\s*\d+\s*\((\d+)\s*need manual review\)/);
115487
115659
  if (m?.[1] !== undefined)
115488
115660
  return Number.parseInt(m[1], 10);
@@ -115494,7 +115666,7 @@ async function readManualRlsCount(rootAbs) {
115494
115666
  async function runProjectBuild(rootAbs) {
115495
115667
  let hasBuildScript = false;
115496
115668
  try {
115497
- const pkg = JSON.parse(await readFile4(join19(rootAbs, "package.json"), "utf8"));
115669
+ const pkg = JSON.parse(await readFile4(join20(rootAbs, "package.json"), "utf8"));
115498
115670
  hasBuildScript = typeof pkg.scripts?.build === "string";
115499
115671
  } catch {
115500
115672
  return { ran: false, passed: false, detail: "No readable package.json — nothing to build." };
@@ -115526,7 +115698,7 @@ ${tail}` : ""}`
115526
115698
  async function detectPackageManager(rootAbs) {
115527
115699
  const has = async (name) => {
115528
115700
  try {
115529
- await readFile4(join19(rootAbs, name), "utf8");
115701
+ await readFile4(join20(rootAbs, name), "utf8");
115530
115702
  return true;
115531
115703
  } catch {
115532
115704
  return false;
@@ -115656,8 +115828,8 @@ async function rewriteSupabaseClient(ctx, input) {
115656
115828
  flags.push(...scaffold.flags);
115657
115829
  }
115658
115830
  const reportNeeded = hadRewrites || flags.length > 0 || walk2.skipped.length > 0;
115659
- const backupDir = apply && changed.length > 0 ? join20(rootAbs, "migration-preview", "backup") : null;
115660
- const rewriteTarget = apply ? rootAbs : input.outputDir ? resolve9(ctx.cwd, input.outputDir) : join20(rootAbs, "migration-preview", "rewritten");
115831
+ const backupDir = apply && changed.length > 0 ? join21(rootAbs, "migration-preview", "backup") : null;
115832
+ const rewriteTarget = apply ? rootAbs : input.outputDir ? resolve9(ctx.cwd, input.outputDir) : join21(rootAbs, "migration-preview", "rewritten");
115661
115833
  const filesWritten = [];
115662
115834
  let clientModuleWritten = null;
115663
115835
  try {
@@ -115677,19 +115849,19 @@ async function rewriteSupabaseClient(ctx, input) {
115677
115849
  changedOnly: true
115678
115850
  });
115679
115851
  for (const rel of out.written) {
115680
- filesWritten.push(relative7(ctx.cwd, join20(rewriteTarget, rel)));
115852
+ filesWritten.push(relative7(ctx.cwd, join21(rewriteTarget, rel)));
115681
115853
  }
115682
115854
  if (scaffold?.clientModule) {
115683
- const moduleAbs = join20(rewriteTarget, scaffold.clientModule.relativePath);
115684
- await mkdir3(dirname6(moduleAbs), { recursive: true });
115855
+ const moduleAbs = join21(rewriteTarget, scaffold.clientModule.relativePath);
115856
+ await mkdir3(dirname7(moduleAbs), { recursive: true });
115685
115857
  await writeFile3(moduleAbs, scaffold.clientModule.content, "utf8");
115686
115858
  clientModuleWritten = relative7(ctx.cwd, moduleAbs).split(sep5).join("/");
115687
115859
  filesWritten.push(clientModuleWritten);
115688
115860
  }
115689
115861
  }
115690
115862
  if (reportNeeded) {
115691
- const reportAbs = join20(rootAbs, "migration-preview", "REWRITE_NOTES.md");
115692
- await mkdir3(dirname6(reportAbs), { recursive: true });
115863
+ const reportAbs = join21(rootAbs, "migration-preview", "REWRITE_NOTES.md");
115864
+ await mkdir3(dirname7(reportAbs), { recursive: true });
115693
115865
  const analysis = analyzeMigration(walk2.files.map((f) => ({
115694
115866
  relativePath: f.relativePath,
115695
115867
  kind: f.kind,
@@ -115941,7 +116113,7 @@ function failure3(rootAbs, mode, err, suggestion) {
115941
116113
  }
115942
116114
  // ../core/dist/commands/migrate-supabase-schema.js
115943
116115
  import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
115944
- import { isAbsolute as isAbsolute4, join as join21, relative as relative8, resolve as resolve10 } from "node:path";
116116
+ import { isAbsolute as isAbsolute4, join as join22, relative as relative8, resolve as resolve10 } from "node:path";
115945
116117
 
115946
116118
  // ../core/dist/commands/migrate-supabase-scripts.js
115947
116119
  function generateDataImportScripts(preview) {
@@ -116461,10 +116633,10 @@ async function writePreviewFiles(ctx, outputDir, preview) {
116461
116633
  const schemaJson = renderSchemaJson(preview);
116462
116634
  const notesMd = renderMigrationNotes(preview);
116463
116635
  const scripts = generateDataImportScripts(preview);
116464
- const schemaPath = join21(outputDir, "pb_schema.json");
116465
- const notesPath = join21(outputDir, "MIGRATION_NOTES.md");
116466
- const migratePath = join21(outputDir, "pb_migrate.js");
116467
- const storagePath = join21(outputDir, "pb_migrate_storage.js");
116636
+ const schemaPath = join22(outputDir, "pb_schema.json");
116637
+ const notesPath = join22(outputDir, "MIGRATION_NOTES.md");
116638
+ const migratePath = join22(outputDir, "pb_migrate.js");
116639
+ const storagePath = join22(outputDir, "pb_migrate_storage.js");
116468
116640
  await writeFile4(schemaPath, schemaJson, "utf8");
116469
116641
  await writeFile4(notesPath, notesMd, "utf8");
116470
116642
  await writeFile4(migratePath, scripts.pbMigrate, "utf8");
@@ -116697,7 +116869,7 @@ async function previewList(ctx, input = {}) {
116697
116869
  // ../core/dist/commands/publish.js
116698
116870
  init_dist();
116699
116871
  import { existsSync as existsSync15 } from "node:fs";
116700
- import { join as join24 } from "node:path";
116872
+ import { join as join25 } from "node:path";
116701
116873
 
116702
116874
  // ../shared/dist/pow.js
116703
116875
  import { createHash as createHash3 } from "node:crypto";
@@ -117101,8 +117273,8 @@ async function runDeployWithRetry(opts) {
117101
117273
 
117102
117274
  // ../core/dist/commands/env-scan.js
117103
117275
  init_dist();
117104
- import { readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync3 } from "node:fs";
117105
- import { join as join22 } from "node:path";
117276
+ import { readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
117277
+ import { join as join23 } from "node:path";
117106
117278
  function platformInjectedClientEnvKeys(config2) {
117107
117279
  const data2 = config2.data;
117108
117280
  if (!data2)
@@ -117160,7 +117332,7 @@ function scanForMissingBuildEnvRefs(cwd, existingKeys) {
117160
117332
  continue;
117161
117333
  let content;
117162
117334
  try {
117163
- content = readFileSync10(join22(cwd, rel), "utf8");
117335
+ content = readFileSync11(join23(cwd, rel), "utf8");
117164
117336
  } catch {
117165
117337
  continue;
117166
117338
  }
@@ -117192,7 +117364,7 @@ function* walkDir(dir, base) {
117192
117364
  for (const name of entries) {
117193
117365
  if (SKIP_DIRS.has(name))
117194
117366
  continue;
117195
- const full = join22(dir, name);
117367
+ const full = join23(dir, name);
117196
117368
  let st;
117197
117369
  try {
117198
117370
  st = statSync3(full);
@@ -117209,6 +117381,47 @@ function* walkDir(dir, base) {
117209
117381
 
117210
117382
  // ../core/dist/commands/publish-api-error.js
117211
117383
  init_dist();
117384
+
117385
+ // ../core/dist/commands/publish-rate-limit.js
117386
+ var RATE_LIMIT_CODES = new Set([
117387
+ "DEPLOY_RATE_LIMIT",
117388
+ "DEPLOY_RATE_LIMITED",
117389
+ "RATE_LIMIT_EXCEEDED"
117390
+ ]);
117391
+ function matchRateLimit(err) {
117392
+ if (!RATE_LIMIT_CODES.has(err.code))
117393
+ return null;
117394
+ const wait = boundedSeconds(err.extra?.retryAfterSec);
117395
+ return {
117396
+ title: "Rate limit hit",
117397
+ explanation: err.message,
117398
+ suggestion: "Wait a few minutes and try again. If you need a higher cap, upgrade at https://percher.app/account/billing.",
117399
+ recovery: recoveryAsk({
117400
+ prompt: `Percher is rate-limiting deploys for this app${wait ? `; wait at least ${wait}s` : " — the window is up to an hour"}. This is a burst/hourly limit, not the daily deploy quota. Wait and retry, or upgrade the plan for a higher cap — which would you like?`,
117401
+ options: ["wait and retry", "upgrade", "cancel"],
117402
+ reasonCode: "quota_exceeded"
117403
+ }),
117404
+ summary: `Rate limit hit: ${err.message}`
117405
+ };
117406
+ }
117407
+ function matchConnectionRateLimit(err) {
117408
+ if (err.code !== "OAUTH_GRANT_RATE_LIMITED")
117409
+ return null;
117410
+ const wait = boundedSeconds(err.extra?.retryAfterSec) ?? 60;
117411
+ return {
117412
+ title: "Connection rate limit hit",
117413
+ explanation: err.message,
117414
+ suggestion: `Wait ${wait}s, then publish again. Nothing in the app needs to change.`,
117415
+ recovery: recoveryAsk({
117416
+ prompt: `Percher is limiting requests on this connection; wait at least ${wait}s, then publish again. Nothing in the app needs to change. Wait and retry, or cancel?`,
117417
+ options: ["wait and retry", "cancel"],
117418
+ reasonCode: "quota_exceeded"
117419
+ }),
117420
+ summary: `Connection rate limit hit. Retry in ${wait}s.`
117421
+ };
117422
+ }
117423
+
117424
+ // ../core/dist/commands/publish-api-error.js
117212
117425
  var DEPLOY_GATE_CODES = new Set([
117213
117426
  "RETRY_LIMIT_REACHED",
117214
117427
  "REQUIRED_ENV_MISSING",
@@ -117228,6 +117441,9 @@ function classifyPublishApiError(err, input) {
117228
117441
  const rateLimit = matchRateLimit(err);
117229
117442
  if (rateLimit)
117230
117443
  return buildResult(rateLimit, input);
117444
+ const connectionLimit = matchConnectionRateLimit(err);
117445
+ if (connectionLimit)
117446
+ return buildResult(connectionLimit, input);
117231
117447
  const concurrent = matchConcurrentLimit(err);
117232
117448
  if (concurrent)
117233
117449
  return buildResult(concurrent, input);
@@ -117313,27 +117529,6 @@ function matchPlanLimit(err) {
117313
117529
  summary: `Plan limit reached: ${err.message}`
117314
117530
  };
117315
117531
  }
117316
- var RATE_LIMIT_CODES = new Set([
117317
- "DEPLOY_RATE_LIMIT",
117318
- "DEPLOY_RATE_LIMITED",
117319
- "RATE_LIMIT_EXCEEDED"
117320
- ]);
117321
- function matchRateLimit(err) {
117322
- if (!RATE_LIMIT_CODES.has(err.code))
117323
- return null;
117324
- const wait = boundedSeconds(err.extra?.retryAfterSec);
117325
- return {
117326
- title: "Rate limit hit",
117327
- explanation: err.message,
117328
- suggestion: "Wait a few minutes and try again. If you need a higher cap, upgrade at https://percher.app/account/billing.",
117329
- recovery: recoveryAsk({
117330
- prompt: `Percher is rate-limiting deploys for this app${wait ? `; wait at least ${wait}s` : " — the window is up to an hour"}. This is a burst/hourly limit, not the daily deploy quota. Wait and retry, or upgrade the plan for a higher cap — which would you like?`,
117331
- options: ["wait and retry", "upgrade", "cancel"],
117332
- reasonCode: "quota_exceeded"
117333
- }),
117334
- summary: `Rate limit hit: ${err.message}`
117335
- };
117336
- }
117337
117532
  var PLATFORM_DISK_CODES2 = new Set(["PLATFORM_DISK_LOW", "PLATFORM_DISK_UNKNOWN"]);
117338
117533
  function matchPlatformDisk(err) {
117339
117534
  if (!PLATFORM_DISK_CODES2.has(err.code))
@@ -117691,22 +117886,22 @@ function inFlightPublishResult(p, why) {
117691
117886
  }
117692
117887
 
117693
117888
  // ../core/dist/commands/publish-node.js
117694
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
117695
- import { join as join23 } from "node:path";
117889
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "node:fs";
117890
+ import { join as join24 } from "node:path";
117696
117891
  function resolveNodeVersion(cwd) {
117697
- const nvmrc = join23(cwd, ".nvmrc");
117892
+ const nvmrc = join24(cwd, ".nvmrc");
117698
117893
  if (existsSync14(nvmrc)) {
117699
117894
  try {
117700
- const v = readFileSync11(nvmrc, "utf8").trim();
117895
+ const v = readFileSync12(nvmrc, "utf8").trim();
117701
117896
  if (v)
117702
117897
  return { value: v, source: ".nvmrc" };
117703
117898
  } catch {}
117704
117899
  }
117705
- const pkgPath = join23(cwd, "package.json");
117900
+ const pkgPath = join24(cwd, "package.json");
117706
117901
  if (!existsSync14(pkgPath))
117707
117902
  return null;
117708
117903
  try {
117709
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
117904
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
117710
117905
  const engines = pkg?.engines?.node;
117711
117906
  if (typeof engines === "string" && engines.length > 0) {
117712
117907
  return { value: engines, source: "engines.node" };
@@ -117714,6 +117909,34 @@ function resolveNodeVersion(cwd) {
117714
117909
  } catch {}
117715
117910
  return { value: DEFAULT_NODE_VERSION_LABEL, source: "default" };
117716
117911
  }
117912
+ function emitPreflight(ctx, config2, tarball) {
117913
+ const lines = [];
117914
+ const fw = config2.app.framework ?? (config2.app.runtime === "docker" ? "Dockerfile" : config2.app.runtime);
117915
+ lines.push(`Detected: ${fw} (runtime=${config2.app.runtime})`);
117916
+ if (config2.app.runtime === "bun") {
117917
+ lines.push("Bun: 1.x (default)");
117918
+ } else if (config2.app.runtime === "node") {
117919
+ const node = resolveNodeVersion(tarball.cwd);
117920
+ if (node) {
117921
+ lines.push(`Node: ${node.value} (${node.source})`);
117922
+ }
117923
+ }
117924
+ if (config2.build?.command) {
117925
+ lines.push(`Build: ${config2.build.command}`);
117926
+ }
117927
+ if (config2.web?.health) {
117928
+ lines.push(`Health: ${config2.web.health}`);
117929
+ }
117930
+ if (config2.web?.port) {
117931
+ lines.push(`Port: ${config2.web.port}`);
117932
+ }
117933
+ const sizeKb = (tarball.bytes / 1024).toFixed(1);
117934
+ lines.push(`Bundle: ${tarball.fileCount} files, ${sizeKb} KB`);
117935
+ lines.push(`Target: ${config2.app.name}`);
117936
+ for (const line of lines) {
117937
+ ctx.status(line);
117938
+ }
117939
+ }
117717
117940
 
117718
117941
  // ../core/dist/commands/publish.js
117719
117942
  var publishInputSchema = exports_external.object({
@@ -117729,7 +117952,7 @@ var publishInputSchema = exports_external.object({
117729
117952
  var SUSPICIOUS_BYTES = 50 * 1024 * 1024;
117730
117953
  var SUSPICIOUS_FILES = 1e4;
117731
117954
  function tomlPathFor2(cwd) {
117732
- return join24(cwd, "percher.toml");
117955
+ return join25(cwd, "percher.toml");
117733
117956
  }
117734
117957
  async function buildReplacedResult2(opts) {
117735
117958
  const { ctx, app, deployment, tarball, cwd } = opts;
@@ -117885,7 +118108,7 @@ ${bullets}`,
117885
118108
  bundle: { fileCount: 0, bytes: 0 }
117886
118109
  };
117887
118110
  }
117888
- if (preParsedConfig && !input.dryRun && !existsSync15(join24(ctx.cwd, "percher.toml"))) {
118111
+ if (preParsedConfig && !input.dryRun && !existsSync15(join25(ctx.cwd, "percher.toml"))) {
117889
118112
  return {
117890
118113
  status: "failed",
117891
118114
  fileCount: 0,
@@ -118202,7 +118425,8 @@ async function finalizeDeploy(opts) {
118202
118425
  }
118203
118426
  });
118204
118427
  } catch (err) {
118205
- if (err instanceof DeployPollLostContactError)
118428
+ const serverError = err instanceof PercherApiError && err.status >= 500 && err.status < 600;
118429
+ if (err instanceof DeployPollLostContactError || serverError)
118206
118430
  return inFlightPublishResult(inFlight(), "lost_contact");
118207
118431
  if (!timedOut)
118208
118432
  throw err;
@@ -118521,7 +118745,7 @@ async function handleAnonymousPublish(ctx, config2, input, tarball) {
118521
118745
  async function loadOrGenerate(ctx, preParsed) {
118522
118746
  if (preParsed)
118523
118747
  return structuredClone(preParsed);
118524
- const tomlPath = join24(ctx.cwd, "percher.toml");
118748
+ const tomlPath = join25(ctx.cwd, "percher.toml");
118525
118749
  if (!existsSync15(tomlPath)) {
118526
118750
  ctx.status("No percher.toml found — detecting framework...");
118527
118751
  const generated = await init(ctx);
@@ -118545,32 +118769,6 @@ async function ensureApp2(ctx, config2) {
118545
118769
  throw err;
118546
118770
  }
118547
118771
  }
118548
- function emitPreflight(ctx, config2, tarball) {
118549
- const lines = [];
118550
- const fw = config2.app.framework ?? (config2.app.runtime === "docker" ? "Dockerfile" : config2.app.runtime);
118551
- lines.push(`Detected: ${fw} (runtime=${config2.app.runtime})`);
118552
- if (config2.app.runtime === "node" || config2.app.runtime === "bun") {
118553
- const node = resolveNodeVersion(tarball.cwd);
118554
- if (node) {
118555
- lines.push(`Node: ${node.value} (${node.source})`);
118556
- }
118557
- }
118558
- if (config2.build?.command) {
118559
- lines.push(`Build: ${config2.build.command}`);
118560
- }
118561
- if (config2.web?.health) {
118562
- lines.push(`Health: ${config2.web.health}`);
118563
- }
118564
- if (config2.web?.port) {
118565
- lines.push(`Port: ${config2.web.port}`);
118566
- }
118567
- const sizeKb = (tarball.bytes / 1024).toFixed(1);
118568
- lines.push(`Bundle: ${tarball.fileCount} files, ${sizeKb} KB`);
118569
- lines.push(`Target: ${config2.app.name}`);
118570
- for (const line of lines) {
118571
- ctx.status(line);
118572
- }
118573
- }
118574
118772
  // ../core/dist/commands/publish-from-repo.js
118575
118773
  init_dist();
118576
118774
  var publishFromRepoInputSchema = exports_external.object({
@@ -118601,7 +118799,7 @@ var publishInlineInputSchema = exports_external.object({
118601
118799
  // ../core/dist/commands/push.js
118602
118800
  init_dist();
118603
118801
  import { existsSync as existsSync16 } from "node:fs";
118604
- import { join as join25 } from "node:path";
118802
+ import { join as join26 } from "node:path";
118605
118803
  var pushInputSchema = exports_external.object({
118606
118804
  dryRun: exports_external.boolean().optional(),
118607
118805
  force: exports_external.boolean().optional(),
@@ -118612,7 +118810,7 @@ var SUSPICIOUS_BYTES2 = 50 * 1024 * 1024;
118612
118810
  var SUSPICIOUS_FILES2 = 1e4;
118613
118811
  var PLACEHOLDER_PREFIXES = ["add-your-", "your-key-here"];
118614
118812
  async function loadOrGenerate2(ctx) {
118615
- const tomlPath = join25(ctx.cwd, "percher.toml");
118813
+ const tomlPath = join26(ctx.cwd, "percher.toml");
118616
118814
  if (!existsSync16(tomlPath)) {
118617
118815
  ctx.status("No percher.toml found — detecting framework...");
118618
118816
  const result = await init(ctx);
@@ -118724,7 +118922,7 @@ async function push(ctx, input = {}, internals = {}) {
118724
118922
  fileCount: tarball.fileCount,
118725
118923
  recovery: RECOVERY_NONE,
118726
118924
  summary: `Dry run for ${config2.app.name}: ${tarball.fileCount} files, ${(tarball.bytes / 1024).toFixed(1)} KB — no deploy performed.`,
118727
- configPath: join25(ctx.cwd, "percher.toml"),
118925
+ configPath: join26(ctx.cwd, "percher.toml"),
118728
118926
  bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
118729
118927
  };
118730
118928
  }
@@ -118912,7 +119110,7 @@ ${deployment.errorMessage}` : "";
118912
119110
  fileCount: tarball.fileCount,
118913
119111
  recovery: RECOVERY_NONE,
118914
119112
  summary,
118915
- configPath: join25(ctx.cwd, "percher.toml"),
119113
+ configPath: join26(ctx.cwd, "percher.toml"),
118916
119114
  bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
118917
119115
  };
118918
119116
  }
@@ -119241,8 +119439,8 @@ function failureResult(error48) {
119241
119439
  };
119242
119440
  }
119243
119441
  // ../core/dist/commands/rename.js
119244
- import { existsSync as existsSync17, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
119245
- import { join as join26 } from "node:path";
119442
+ import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "node:fs";
119443
+ import { join as join27 } from "node:path";
119246
119444
  var renameInputSchema = exports_external.object({
119247
119445
  app: exports_external.string().optional(),
119248
119446
  name: exports_external.string().min(3).max(40)
@@ -119262,17 +119460,17 @@ async function rename3(ctx, input) {
119262
119460
  };
119263
119461
  }
119264
119462
  function updateLocalTomlName(cwd, oldName, newName) {
119265
- const p = join26(cwd, "percher.toml");
119463
+ const p = join27(cwd, "percher.toml");
119266
119464
  if (!existsSync17(p))
119267
119465
  return false;
119268
119466
  try {
119269
- const src = readFileSync12(p, "utf8");
119467
+ const src = readFileSync13(p, "utf8");
119270
119468
  if (parse5(src).app.name !== oldName)
119271
119469
  return false;
119272
119470
  const next = src.replace(new RegExp(`(name\\s*=\\s*")${oldName}(")`), `$1${newName}$2`);
119273
119471
  if (next === src || parse5(next).app.name !== newName)
119274
119472
  return false;
119275
- writeFileSync9(p, next);
119473
+ writeFileSync10(p, next);
119276
119474
  return true;
119277
119475
  } catch {
119278
119476
  return false;
@@ -119281,9 +119479,9 @@ function updateLocalTomlName(cwd, oldName, newName) {
119281
119479
  // ../core/dist/commands/reproduce.js
119282
119480
  init_dist();
119283
119481
  var import_cross_spawn3 = __toESM(require_cross_spawn(), 1);
119284
- import { mkdtempSync, writeFileSync as writeFileSync10 } from "node:fs";
119482
+ import { mkdtempSync, writeFileSync as writeFileSync11 } from "node:fs";
119285
119483
  import { tmpdir } from "node:os";
119286
- import { join as join27 } from "node:path";
119484
+ import { join as join28 } from "node:path";
119287
119485
  function runProcessExit(cmd, args, opts = {}) {
119288
119486
  return new Promise((resolve11, reject) => {
119289
119487
  const child = import_cross_spawn3.default(cmd, args, {
@@ -119377,9 +119575,9 @@ async function reproduce(ctx, input) {
119377
119575
  const envMap = await ctx.client.env.list(app.id);
119378
119576
  currentAppEnvKeys = Object.keys(envMap).sort();
119379
119577
  } catch {}
119380
- const tmpDir = mkdtempSync(join27(tmpdir(), `percher-reproduce-${input.deployId}-`));
119381
- const tarballPath = join27(tmpDir, "source.tar.gz");
119382
- writeFileSync10(tarballPath, bytes);
119578
+ const tmpDir = mkdtempSync(join28(tmpdir(), `percher-reproduce-${input.deployId}-`));
119579
+ const tarballPath = join28(tmpDir, "source.tar.gz");
119580
+ writeFileSync11(tarballPath, bytes);
119383
119581
  let extracted = false;
119384
119582
  try {
119385
119583
  extracted = await runProcessExit("tar", ["-xzf", tarballPath, "-C", tmpDir]) === 0;
@@ -119977,6 +120175,7 @@ var SOURCE_EXTS2 = new Set([
119977
120175
  ".py"
119978
120176
  ]);
119979
120177
  var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
120178
+ var IMPORT_META_CONSTANTS = new Set(["DEV", "PROD", "MODE", "BASE_URL", "SSR"]);
119980
120179
  // ../core/dist/plans.js
119981
120180
  var PB_INITIAL_FOOTPRINT_MB = 256;
119982
120181
  var EGRESS_GB_PER_MONTH = {
@@ -120183,6 +120382,15 @@ async function writeLine2(ctx, line) {
120183
120382
  writer.releaseLock();
120184
120383
  }
120185
120384
  }
120385
+ async function writeErrLine(ctx, line) {
120386
+ const writer = ctx.stderr.getWriter();
120387
+ try {
120388
+ await writer.write(new TextEncoder().encode(`${line}
120389
+ `));
120390
+ } finally {
120391
+ writer.releaseLock();
120392
+ }
120393
+ }
120186
120394
  async function writeUnknownCommand(ctx, name) {
120187
120395
  const writer = ctx.stderr.getWriter();
120188
120396
  try {
@@ -121578,6 +121786,7 @@ function registerDeploysCommands(cli, ctx, argv) {
121578
121786
  rebuild: Boolean(options.rebuild)
121579
121787
  });
121580
121788
  await writeLine2(ctx, `${import_picocolors8.default.green("✓")} rolled back to ${result.rolledBackTo.slice(0, 10)} (status: ${result.status})`);
121789
+ await writeLine2(ctx, "Env vars are not rolled back; the current values stay.");
121581
121790
  });
121582
121791
  cli.command("reproduce <deployId>", "Download a deploy's source and reproduce the build locally").option("--app <name>", "App name (defaults to percher.toml app.name)").action(async (deployId, options) => {
121583
121792
  const result = await reproduce(ctx, {
@@ -121656,6 +121865,8 @@ function registerDeploysCommands(cli, ctx, argv) {
121656
121865
  for (const v of list) {
121657
121866
  const message = v.message.split(`
121658
121867
  `)[0]?.trim() ?? "";
121868
+ const note = Array.from((v.note ?? "").replace(/[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/gu, " ").replace(/\s+/g, " ").trim());
121869
+ const noteLabel = note.length > 80 ? `${note.slice(0, 79).join("")}…` : note.join("");
121659
121870
  const channelLabels = {
121660
121871
  cli: "CLI",
121661
121872
  mcp: "MCP",
@@ -121670,7 +121881,7 @@ function registerDeploysCommands(cli, ctx, argv) {
121670
121881
  const marker = target && target.mode !== "in-window" ? target.mode === "rebuild" ? "[rebuild required]" : "[not a rollback target]" : null;
121671
121882
  if (marker && target)
121672
121883
  reasons.set(marker, target.explanation);
121673
- await writeLine2(ctx, `${v.sha} ${v.timestamp} ${message}${suffix}${marker ? ` ${marker}` : ""}`);
121884
+ await writeLine2(ctx, `${v.sha} ${v.timestamp} ${message}${noteLabel ? ` · ${noteLabel}` : ""}${suffix}${marker ? ` ${marker}` : ""}`);
121674
121885
  }
121675
121886
  for (const [marker, explanation] of reasons) {
121676
121887
  await writeLine2(ctx, `
@@ -122376,8 +122587,209 @@ async function renderHumanStatus(ctx, result) {
122376
122587
  }
122377
122588
  }
122378
122589
 
122379
- // src/commands/token.ts
122590
+ // src/commands/telemetry.ts
122591
+ init_dist();
122380
122592
  var import_picocolors16 = __toESM(require_picocolors(), 1);
122593
+
122594
+ // src/telemetry.ts
122595
+ init_dist();
122596
+ import { createInterface } from "node:readline";
122597
+ // package.json
122598
+ var package_default2 = {
122599
+ name: "percher",
122600
+ version: "1.2.0",
122601
+ description: "CLI for the Percher hosting platform — publish apps with one command",
122602
+ type: "module",
122603
+ bin: {
122604
+ percher: "./dist/bin/percher.js"
122605
+ },
122606
+ main: "./dist/src/index.js",
122607
+ types: "./dist/src/index.d.ts",
122608
+ exports: {
122609
+ ".": {
122610
+ types: "./dist/src/index.d.ts",
122611
+ import: "./dist/src/index.js",
122612
+ default: "./dist/src/index.js"
122613
+ }
122614
+ },
122615
+ files: [
122616
+ "dist",
122617
+ "THIRD-PARTY-NOTICES.txt"
122618
+ ],
122619
+ publishConfig: {
122620
+ access: "public"
122621
+ },
122622
+ scripts: {
122623
+ build: `tsc -p tsconfig.json && tsc-alias -p tsconfig.json && bun build bin/percher.ts --outfile dist/bin/percher.js --target node --format esm --metafile=bundle-meta.json && node ../../scripts/bundle-notices.mjs . bundle-meta.json && node scripts/copy-templates.mjs && node -e "require('fs').chmodSync('dist/bin/percher.js',0o755)"`,
122624
+ test: "bun test",
122625
+ typecheck: "tsc --noEmit"
122626
+ },
122627
+ dependencies: {
122628
+ "@percher/client": "^1.2.0",
122629
+ "@percher/core": "^1.2.0",
122630
+ "@percher/toml": "^1.0.3",
122631
+ cac: "^7.0.0",
122632
+ picocolors: "^1.1.1"
122633
+ },
122634
+ devDependencies: {
122635
+ "bun-types": "^1.1.0",
122636
+ "tsc-alias": "^1.8.17"
122637
+ },
122638
+ engines: {
122639
+ bun: ">=1.0.0",
122640
+ node: ">=22.0.0"
122641
+ },
122642
+ license: "FSL-1.1-MIT",
122643
+ author: "Mattias Malmborg",
122644
+ repository: {
122645
+ type: "git",
122646
+ url: "https://github.com/mattiasmalmborg/percher"
122647
+ },
122648
+ keywords: [
122649
+ "hosting",
122650
+ "deploy",
122651
+ "cli",
122652
+ "paas",
122653
+ "bun",
122654
+ "ai"
122655
+ ]
122656
+ };
122657
+
122658
+ // src/telemetry.ts
122659
+ var VERSION = package_default2.version;
122660
+ function trackEvent(command, result, errorType) {
122661
+ if (!telemetryState().enabled)
122662
+ return;
122663
+ const body = JSON.stringify({
122664
+ cliVersion: VERSION,
122665
+ command,
122666
+ result,
122667
+ errorType,
122668
+ os: `${process.platform}-${process.arch}`,
122669
+ bunVersion: typeof Bun !== "undefined" ? Bun.version : undefined
122670
+ });
122671
+ fetch(`${resolveApiUrl()}/telemetry/cli`, {
122672
+ method: "POST",
122673
+ headers: { "Content-Type": "application/json" },
122674
+ body,
122675
+ signal: AbortSignal.timeout(3000)
122676
+ }).catch(() => {});
122677
+ }
122678
+ function lineAsker(input, output, onInterrupt = () => process.exit(130)) {
122679
+ return (question, signal) => new Promise((resolve11) => {
122680
+ const rl = createInterface({
122681
+ input,
122682
+ output,
122683
+ terminal: input.isTTY === true
122684
+ });
122685
+ let settled = false;
122686
+ const finish = (answer) => {
122687
+ if (settled)
122688
+ return;
122689
+ settled = true;
122690
+ signal.removeEventListener("abort", onAbort);
122691
+ if (answer === null)
122692
+ output.write(`
122693
+ `);
122694
+ resolve11(answer);
122695
+ rl.close();
122696
+ };
122697
+ const onAbort = () => finish(null);
122698
+ signal.addEventListener("abort", onAbort, { once: true });
122699
+ rl.on("line", (line) => finish(line));
122700
+ rl.on("close", () => finish(null));
122701
+ rl.on("SIGINT", () => {
122702
+ finish(null);
122703
+ onInterrupt();
122704
+ });
122705
+ rl.setPrompt(question);
122706
+ rl.prompt();
122707
+ if (signal.aborted)
122708
+ finish(null);
122709
+ });
122710
+ }
122711
+ var askOnStdin = lineAsker(process.stdin, process.stderr);
122712
+ var ANSWER_TIMEOUT_MS = 20000;
122713
+ var CI_VARS = ["CONTINUOUS_INTEGRATION", "BUILD_NUMBER", "TF_BUILD", "JENKINS_URL"];
122714
+ var AGENT_VARS = [
122715
+ "CLAUDECODE",
122716
+ "CURSOR_AGENT",
122717
+ "CURSOR_TRACE_ID",
122718
+ "CODEX_SANDBOX",
122719
+ "CODEX_CI",
122720
+ "GITHUB_ACTIONS"
122721
+ ];
122722
+ function isUnattended(env) {
122723
+ const ci = env.CI;
122724
+ if (ci !== undefined && ci !== "" && ci !== "0" && ci !== "false")
122725
+ return true;
122726
+ return [...CI_VARS, ...AGENT_VARS].some((key) => Boolean(env[key]));
122727
+ }
122728
+ var realTimer = (fire, ms) => {
122729
+ const handle = setTimeout(fire, ms);
122730
+ return () => clearTimeout(handle);
122731
+ };
122732
+ async function askTelemetryConsentOnce(ctx, argv, command, question) {
122733
+ if (command === "telemetry" || argv.includes("--json"))
122734
+ return;
122735
+ if (ctx.stdinIsInteractive !== true || !question.stderrIsTTY)
122736
+ return;
122737
+ if (isUnattended(ctx.env))
122738
+ return;
122739
+ const state = telemetryState();
122740
+ if (state.envOptOut || state.stored)
122741
+ return;
122742
+ const timeout = new AbortController;
122743
+ const cancel = (question.setTimer ?? realTimer)(() => timeout.abort(), ANSWER_TIMEOUT_MS);
122744
+ let answer;
122745
+ try {
122746
+ answer = await question.ask("Send anonymous usage statistics to Percher? (y/N) ", timeout.signal);
122747
+ } finally {
122748
+ cancel();
122749
+ }
122750
+ if (answer === null)
122751
+ return;
122752
+ const enabled = /^y(es)?$/i.test(answer.trim());
122753
+ try {
122754
+ saveTelemetryAnswer(enabled, VERSION);
122755
+ } catch {
122756
+ await writeErrLine(ctx, "Could not save the answer; you will be asked again.");
122757
+ return;
122758
+ }
122759
+ await writeErrLine(ctx, enabled ? "Usage statistics on. Turn off: percher telemetry off" : "Usage statistics off. Turn on: percher telemetry on");
122760
+ }
122761
+ function telemetryStatusLine(state = telemetryState()) {
122762
+ const stored = state.stored ? `stored answer: ${state.stored.enabled ? "on" : "off"}` : null;
122763
+ if (state.envOptOut) {
122764
+ return `Telemetry: off (env opt-out: ${state.envOptOut}${stored ? `; ${stored}` : ""})`;
122765
+ }
122766
+ if (state.stored)
122767
+ return `Telemetry: ${state.stored.enabled ? "on" : "off"} (stored answer)`;
122768
+ return "Telemetry: off (undecided)";
122769
+ }
122770
+
122771
+ // src/commands/telemetry.ts
122772
+ function registerTelemetryCommands(cli, ctx) {
122773
+ cli.command("telemetry [action]", "Anonymous usage statistics: on | off | status").action(async (action) => {
122774
+ if (action === "on" || action === "off") {
122775
+ try {
122776
+ saveTelemetryAnswer(action === "on", VERSION);
122777
+ } catch {
122778
+ await writeErrLine(ctx, `${import_picocolors16.default.red("✗")} Could not save the answer.`);
122779
+ throw new SilentExitError;
122780
+ }
122781
+ } else if (action !== undefined && action !== "status") {
122782
+ await writeErrLine(ctx, `${import_picocolors16.default.red("✗")} Usage: percher telemetry on|off|status`);
122783
+ throw new SilentExitError;
122784
+ }
122785
+ const state = telemetryState();
122786
+ const overridden = action === "on" && state.envOptOut !== null;
122787
+ await writeLine2(ctx, `${telemetryStatusLine(state)}${overridden ? "; the environment variable still wins." : ""}`);
122788
+ });
122789
+ }
122790
+
122791
+ // src/commands/token.ts
122792
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
122381
122793
  var VALID_SCOPES = ["deploy", "env", "read", "full"];
122382
122794
  function scopeLabel(scopes, appName) {
122383
122795
  const s = scopes && scopes.length > 0 ? scopes.join("+") : "full";
@@ -122386,22 +122798,22 @@ function scopeLabel(scopes, appName) {
122386
122798
  function registerTokenCommands(cli, ctx) {
122387
122799
  cli.command("token [action] [arg]", "Manage API tokens: create | list | revoke").option("--scope <scope>", "(create) deploy | env | read | full (default: full, account-wide unless --app)").option("--app <name>", "(create) bind the token to a single app").option("--ttl-days <n>", "(create) token lifetime in days (default 90, max 365)").action(async (action, arg, options) => {
122388
122800
  if (!ctx.client.token) {
122389
- await writeLine2(ctx, `${import_picocolors16.default.red("✗")} Not logged in. Run: percher login`);
122801
+ await writeLine2(ctx, `${import_picocolors17.default.red("✗")} Not logged in. Run: percher login`);
122390
122802
  throw new SilentExitError;
122391
122803
  }
122392
122804
  if (action === "create") {
122393
122805
  if (!arg) {
122394
- await writeLine2(ctx, `${import_picocolors16.default.red("✗")} A name is required. Usage: percher token create <name> [--scope deploy] [--app <name>]`);
122806
+ await writeLine2(ctx, `${import_picocolors17.default.red("✗")} A name is required. Usage: percher token create <name> [--scope deploy] [--app <name>]`);
122395
122807
  throw new SilentExitError;
122396
122808
  }
122397
122809
  const scope = options.scope ?? "full";
122398
122810
  if (!VALID_SCOPES.includes(scope)) {
122399
- await writeLine2(ctx, `${import_picocolors16.default.red("✗")} Invalid --scope "${options.scope}". Use one of: ${VALID_SCOPES.join(", ")}.`);
122811
+ await writeLine2(ctx, `${import_picocolors17.default.red("✗")} Invalid --scope "${options.scope}". Use one of: ${VALID_SCOPES.join(", ")}.`);
122400
122812
  throw new SilentExitError;
122401
122813
  }
122402
122814
  const ttlDays = options.ttlDays !== undefined ? Number(options.ttlDays) : undefined;
122403
122815
  if (ttlDays !== undefined && (!Number.isInteger(ttlDays) || ttlDays <= 0)) {
122404
- await writeLine2(ctx, `${import_picocolors16.default.red("✗")} --ttl-days must be a positive whole number.`);
122816
+ await writeLine2(ctx, `${import_picocolors17.default.red("✗")} --ttl-days must be a positive whole number.`);
122405
122817
  throw new SilentExitError;
122406
122818
  }
122407
122819
  const created = await tokenCreate(ctx, {
@@ -122410,36 +122822,36 @@ function registerTokenCommands(cli, ctx) {
122410
122822
  app: options.app,
122411
122823
  ttlDays
122412
122824
  });
122413
- await writeLine2(ctx, `${import_picocolors16.default.green("✓")} Token created: ${import_picocolors16.default.bold(created.name)} ${import_picocolors16.default.dim(`(${scopeLabel(created.scopes, created.appName)})`)}`);
122825
+ await writeLine2(ctx, `${import_picocolors17.default.green("✓")} Token created: ${import_picocolors17.default.bold(created.name)} ${import_picocolors17.default.dim(`(${scopeLabel(created.scopes, created.appName)})`)}`);
122414
122826
  await writeLine2(ctx, "");
122415
- await writeLine2(ctx, ` ${import_picocolors16.default.bold(created.token)}`);
122416
- await writeLine2(ctx, import_picocolors16.default.yellow(" ↑ Store this now — the secret is shown only once and can't be retrieved."));
122827
+ await writeLine2(ctx, ` ${import_picocolors17.default.bold(created.token)}`);
122828
+ await writeLine2(ctx, import_picocolors17.default.yellow(" ↑ Store this now — the secret is shown only once and can't be retrieved."));
122417
122829
  await writeLine2(ctx, "");
122418
- await writeLine2(ctx, import_picocolors16.default.dim(" Use it in CI / an agent:"));
122419
- await writeLine2(ctx, import_picocolors16.default.dim(` PERCHER_TOKEN=${created.token} bunx percher publish`));
122830
+ await writeLine2(ctx, import_picocolors17.default.dim(" Use it in CI / an agent:"));
122831
+ await writeLine2(ctx, import_picocolors17.default.dim(` PERCHER_TOKEN=${created.token} bunx percher publish`));
122420
122832
  return;
122421
122833
  }
122422
122834
  if (action === "list") {
122423
122835
  const tokens = await tokenList(ctx);
122424
122836
  if (tokens.length === 0) {
122425
- await writeLine2(ctx, import_picocolors16.default.dim("No API tokens."));
122837
+ await writeLine2(ctx, import_picocolors17.default.dim("No API tokens."));
122426
122838
  return;
122427
122839
  }
122428
122840
  for (const t of tokens) {
122429
122841
  const expires = t.expiresAt ? `expires ${new Date(t.expiresAt).toISOString().slice(0, 10)}` : "no expiry";
122430
- await writeLine2(ctx, `${import_picocolors16.default.bold(t.id)} ${t.name.padEnd(20)} ${import_picocolors16.default.dim(scopeLabel(t.scopes, t.appName))} ${import_picocolors16.default.dim(expires)}`);
122842
+ await writeLine2(ctx, `${import_picocolors17.default.bold(t.id)} ${t.name.padEnd(20)} ${import_picocolors17.default.dim(scopeLabel(t.scopes, t.appName))} ${import_picocolors17.default.dim(expires)}`);
122431
122843
  }
122432
122844
  await writeLine2(ctx, "");
122433
- await writeLine2(ctx, import_picocolors16.default.dim("Revoke one with: percher token revoke <id>"));
122845
+ await writeLine2(ctx, import_picocolors17.default.dim("Revoke one with: percher token revoke <id>"));
122434
122846
  return;
122435
122847
  }
122436
122848
  if (action === "revoke") {
122437
122849
  if (!arg) {
122438
- await writeLine2(ctx, `${import_picocolors16.default.red("✗")} A token id is required. Usage: percher token revoke <id> (list ids with \`percher token list\`).`);
122850
+ await writeLine2(ctx, `${import_picocolors17.default.red("✗")} A token id is required. Usage: percher token revoke <id> (list ids with \`percher token list\`).`);
122439
122851
  throw new SilentExitError;
122440
122852
  }
122441
122853
  await tokenRevoke(ctx, { id: arg });
122442
- await writeLine2(ctx, `${import_picocolors16.default.green("✓")} Token revoked.`);
122854
+ await writeLine2(ctx, `${import_picocolors17.default.green("✓")} Token revoked.`);
122443
122855
  return;
122444
122856
  }
122445
122857
  throw new Error(`Unknown token action "${action ?? ""}". Use: create | list | revoke.`);
@@ -122450,17 +122862,17 @@ function registerTokenCommands(cli, ctx) {
122450
122862
  }
122451
122863
 
122452
122864
  // src/commands/transfers.ts
122453
- var import_picocolors17 = __toESM(require_picocolors(), 1);
122865
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
122454
122866
  function registerTransfersCommands(cli, ctx) {
122455
122867
  cli.command("transfer [emailOrTransferId]", "Transfer ownership of an app: `transfer <email>` initiates; `transfer --cancel <id>` cancels; `transfer --list` shows history").option("--app <name>", "App name (overrides percher.toml)").option("--cancel", "Cancel a pending transfer by id (positional arg is the transferId)").option("--list", "List the transfer history for this app").action(async (emailOrTransferId, options) => {
122456
122868
  if (options.list) {
122457
122869
  const rows = await transferListForApp(ctx, { app: options.app });
122458
122870
  if (rows.length === 0) {
122459
- await writeLine2(ctx, import_picocolors17.default.dim("No transfers for this app."));
122871
+ await writeLine2(ctx, import_picocolors18.default.dim("No transfers for this app."));
122460
122872
  return;
122461
122873
  }
122462
122874
  for (const r of rows) {
122463
- const status2 = r.status === "pending" ? import_picocolors17.default.yellow("pending") : r.status === "accepted" ? import_picocolors17.default.green("accepted") : import_picocolors17.default.dim(r.status);
122875
+ const status2 = r.status === "pending" ? import_picocolors18.default.yellow("pending") : r.status === "accepted" ? import_picocolors18.default.green("accepted") : import_picocolors18.default.dim(r.status);
122464
122876
  await writeLine2(ctx, `${r.id} ${status2.padEnd(20)} → ${r.toEmail} (${r.createdAt})`);
122465
122877
  }
122466
122878
  return;
@@ -122473,7 +122885,7 @@ function registerTransfersCommands(cli, ctx) {
122473
122885
  app: options.app,
122474
122886
  transferId: emailOrTransferId
122475
122887
  });
122476
- await writeLine2(ctx, `${import_picocolors17.default.green("✓")} Cancelled transfer ${emailOrTransferId}`);
122888
+ await writeLine2(ctx, `${import_picocolors18.default.green("✓")} Cancelled transfer ${emailOrTransferId}`);
122477
122889
  return;
122478
122890
  }
122479
122891
  if (!emailOrTransferId) {
@@ -122483,17 +122895,17 @@ function registerTransfersCommands(cli, ctx) {
122483
122895
  app: options.app,
122484
122896
  email: emailOrTransferId
122485
122897
  });
122486
- await writeLine2(ctx, `${import_picocolors17.default.green("✓")} Transfer initiated to ${initiated.toEmail} (id ${initiated.id})`);
122487
- await writeLine2(ctx, import_picocolors17.default.dim(`Expires ${new Date(initiated.expiresAt).toLocaleDateString()}. The recipient must accept via 'percher transfers accept ${initiated.id}'.`));
122898
+ await writeLine2(ctx, `${import_picocolors18.default.green("✓")} Transfer initiated to ${initiated.toEmail} (id ${initiated.id})`);
122899
+ await writeLine2(ctx, import_picocolors18.default.dim(`Expires ${new Date(initiated.expiresAt).toLocaleDateString()}. The recipient must accept via 'percher transfers accept ${initiated.id}'.`));
122488
122900
  if (initiated.notes) {
122489
- await writeLine2(ctx, import_picocolors17.default.yellow(` ⚠ ${initiated.notes}`));
122901
+ await writeLine2(ctx, import_picocolors18.default.yellow(` ⚠ ${initiated.notes}`));
122490
122902
  }
122491
122903
  });
122492
122904
  cli.command("transfers [action] [transferId]", "Incoming ownership transfers: list (default) | accept <transferId> | decline <transferId>").action(async (action, transferId) => {
122493
122905
  if (action === undefined || action === "list") {
122494
122906
  const rows = await transfersIncoming(ctx, {});
122495
122907
  if (rows.length === 0) {
122496
- await writeLine2(ctx, import_picocolors17.default.dim("No pending incoming transfers."));
122908
+ await writeLine2(ctx, import_picocolors18.default.dim("No pending incoming transfers."));
122497
122909
  return;
122498
122910
  }
122499
122911
  for (const r of rows) {
@@ -122506,7 +122918,7 @@ function registerTransfersCommands(cli, ctx) {
122506
122918
  throw new Error("Usage: percher transfers accept <transferId>");
122507
122919
  }
122508
122920
  const accepted = await transfersAccept(ctx, { transferId });
122509
- await writeLine2(ctx, `${import_picocolors17.default.green("✓")} You now own ${import_picocolors17.default.cyan(accepted.appName)}. Run 'percher open ${accepted.appName}' to see it.`);
122921
+ await writeLine2(ctx, `${import_picocolors18.default.green("✓")} You now own ${import_picocolors18.default.cyan(accepted.appName)}. Run 'percher open ${accepted.appName}' to see it.`);
122510
122922
  return;
122511
122923
  }
122512
122924
  if (action === "decline") {
@@ -122514,7 +122926,7 @@ function registerTransfersCommands(cli, ctx) {
122514
122926
  throw new Error("Usage: percher transfers decline <transferId>");
122515
122927
  }
122516
122928
  await transfersDecline(ctx, { transferId });
122517
- await writeLine2(ctx, `${import_picocolors17.default.green("✓")} Declined transfer ${transferId}.`);
122929
+ await writeLine2(ctx, `${import_picocolors18.default.green("✓")} Declined transfer ${transferId}.`);
122518
122930
  return;
122519
122931
  }
122520
122932
  throw new Error(`Unknown transfers action "${action}". Use list | accept <id> | decline <id>.`);
@@ -122543,9 +122955,9 @@ async function openUrl(url2) {
122543
122955
 
122544
122956
  // src/prompt.ts
122545
122957
  import { stdin as input, stdout as output } from "node:process";
122546
- import { createInterface } from "node:readline/promises";
122958
+ import { createInterface as createInterface2 } from "node:readline/promises";
122547
122959
  async function prompt(question, options) {
122548
- const rl = createInterface({ input, output });
122960
+ const rl = createInterface2({ input, output });
122549
122961
  try {
122550
122962
  const suffix = options?.default ? ` [${options.default}]` : "";
122551
122963
  const answer = await rl.question(`${question}${suffix} `);
@@ -122555,111 +122967,6 @@ async function prompt(question, options) {
122555
122967
  }
122556
122968
  }
122557
122969
 
122558
- // src/telemetry.ts
122559
- init_dist();
122560
- // package.json
122561
- var package_default2 = {
122562
- name: "percher",
122563
- version: "1.1.0",
122564
- description: "CLI for the Percher hosting platform — publish apps with one command",
122565
- type: "module",
122566
- bin: {
122567
- percher: "./dist/bin/percher.js"
122568
- },
122569
- main: "./dist/src/index.js",
122570
- types: "./dist/src/index.d.ts",
122571
- exports: {
122572
- ".": {
122573
- types: "./dist/src/index.d.ts",
122574
- import: "./dist/src/index.js",
122575
- default: "./dist/src/index.js"
122576
- }
122577
- },
122578
- files: [
122579
- "dist",
122580
- "THIRD-PARTY-NOTICES.txt"
122581
- ],
122582
- publishConfig: {
122583
- access: "public"
122584
- },
122585
- scripts: {
122586
- build: `tsc -p tsconfig.json && tsc-alias -p tsconfig.json && bun build bin/percher.ts --outfile dist/bin/percher.js --target node --format esm --metafile=bundle-meta.json && node ../../scripts/bundle-notices.mjs . bundle-meta.json && node scripts/copy-templates.mjs && node -e "require('fs').chmodSync('dist/bin/percher.js',0o755)"`,
122587
- test: "bun test",
122588
- typecheck: "tsc --noEmit"
122589
- },
122590
- dependencies: {
122591
- "@percher/client": "^1.1.0",
122592
- "@percher/core": "^1.1.0",
122593
- "@percher/toml": "^1.0.3",
122594
- cac: "^7.0.0",
122595
- picocolors: "^1.1.1"
122596
- },
122597
- devDependencies: {
122598
- "bun-types": "^1.1.0",
122599
- "tsc-alias": "^1.8.17"
122600
- },
122601
- engines: {
122602
- bun: ">=1.0.0",
122603
- node: ">=22.0.0"
122604
- },
122605
- license: "FSL-1.1-MIT",
122606
- author: "Mattias Malmborg",
122607
- repository: {
122608
- type: "git",
122609
- url: "https://github.com/mattiasmalmborg/percher"
122610
- },
122611
- keywords: [
122612
- "hosting",
122613
- "deploy",
122614
- "cli",
122615
- "paas",
122616
- "bun",
122617
- "ai"
122618
- ]
122619
- };
122620
-
122621
- // src/telemetry.ts
122622
- var VERSION = package_default2.version;
122623
- function isDisabled() {
122624
- return process.env.PERCHER_NO_TELEMETRY === "1" || process.env.PERCHER_NO_TELEMETRY === "true" || process.env.DO_NOT_TRACK === "1";
122625
- }
122626
- function trackEvent(command, result, errorType) {
122627
- if (isDisabled())
122628
- return;
122629
- const body = JSON.stringify({
122630
- cliVersion: VERSION,
122631
- command,
122632
- result,
122633
- errorType,
122634
- os: `${process.platform}-${process.arch}`,
122635
- bunVersion: typeof Bun !== "undefined" ? Bun.version : undefined
122636
- });
122637
- fetch(`${resolveApiUrl()}/telemetry/cli`, {
122638
- method: "POST",
122639
- headers: { "Content-Type": "application/json" },
122640
- body,
122641
- signal: AbortSignal.timeout(3000)
122642
- }).catch(() => {});
122643
- }
122644
- async function showTelemetryNotice() {
122645
- if (isDisabled())
122646
- return;
122647
- const { homedir: homedir2 } = await import("node:os");
122648
- const { existsSync: existsSync18, mkdirSync: mkdirSync7, writeFileSync: writeFileSync11 } = await import("node:fs");
122649
- const { join: join28 } = await import("node:path");
122650
- const configDir = join28(homedir2(), ".percher");
122651
- const markerFile = join28(configDir, ".telemetry-notice-shown");
122652
- if (existsSync18(markerFile))
122653
- return;
122654
- console.error("Percher sends non-account-linked usage stats (CLI version, command, result, error type, OS, Bun version).");
122655
- console.error(`Disable with PERCHER_NO_TELEMETRY=1 or DO_NOT_TRACK=1
122656
- `);
122657
- try {
122658
- mkdirSync7(configDir, { recursive: true });
122659
- writeFileSync11(markerFile, new Date().toISOString());
122660
- } catch {}
122661
- }
122662
-
122663
122970
  // src/context.ts
122664
122971
  async function createCliContext() {
122665
122972
  const client = new PercherClient({ clientVersion: `percher-cli/${VERSION}` });
@@ -122693,52 +123000,52 @@ async function createCliContext() {
122693
123000
 
122694
123001
  // src/output.ts
122695
123002
  init_dist();
122696
- var import_picocolors18 = __toESM(require_picocolors(), 1);
123003
+ var import_picocolors19 = __toESM(require_picocolors(), 1);
122697
123004
  async function renderError(stderr, error48) {
122698
123005
  const writer = stderr.getWriter();
122699
123006
  const encoder = new TextEncoder;
122700
123007
  const lines = [];
122701
123008
  if (error48 instanceof PercherTomlError) {
122702
- lines.push(`${import_picocolors18.default.red(`error (${error48.code}):`)} ${error48.message}`);
123009
+ lines.push(`${import_picocolors19.default.red(`error (${error48.code}):`)} ${error48.message}`);
122703
123010
  if (error48.tomlLine !== undefined) {
122704
- lines.push(import_picocolors18.default.dim(` at percher.toml line ${error48.tomlLine}`));
123011
+ lines.push(import_picocolors19.default.dim(` at percher.toml line ${error48.tomlLine}`));
122705
123012
  }
122706
123013
  for (const issue2 of error48.issues ?? []) {
122707
- lines.push(`${import_picocolors18.default.dim(" · ") + import_picocolors18.default.yellow(issue2.path.join("."))}: ${issue2.message}`);
123014
+ lines.push(`${import_picocolors19.default.dim(" · ") + import_picocolors19.default.yellow(issue2.path.join("."))}: ${issue2.message}`);
122708
123015
  }
122709
123016
  } else if (error48 instanceof SunsetError) {
122710
123017
  if (error48.gateCode === "platform_delete_gated") {
122711
- lines.push(`${import_picocolors18.default.yellow("Percher is shutting down — deletes are paused.")}`);
123018
+ lines.push(`${import_picocolors19.default.yellow("Percher is shutting down — deletes are paused.")}`);
122712
123019
  lines.push(error48.message);
122713
- lines.push(import_picocolors18.default.dim(` To delete and permanently forfeit your export window, confirm with your password in the dashboard: ${error48.dashboardUrl}`));
122714
- lines.push(import_picocolors18.default.dim(" The CLI cannot perform this delete during sunset — it must go through the dashboard."));
123020
+ lines.push(import_picocolors19.default.dim(` To delete and permanently forfeit your export window, confirm with your password in the dashboard: ${error48.dashboardUrl}`));
123021
+ lines.push(import_picocolors19.default.dim(" The CLI cannot perform this delete during sunset — it must go through the dashboard."));
122715
123022
  } else {
122716
- lines.push(`${import_picocolors18.default.yellow("Percher is in read-only mode ahead of shutdown.")}`);
123023
+ lines.push(`${import_picocolors19.default.yellow("Percher is in read-only mode ahead of shutdown.")}`);
122717
123024
  lines.push(error48.message);
122718
- lines.push(import_picocolors18.default.dim(` Your apps keep running. Export them any time from the dashboard: ${error48.dashboardUrl}`));
123025
+ lines.push(import_picocolors19.default.dim(` Your apps keep running. Export them any time from the dashboard: ${error48.dashboardUrl}`));
122719
123026
  }
122720
- lines.push(import_picocolors18.default.dim(` Final data deletion: ${error48.finalDeletionDate}`));
123027
+ lines.push(import_picocolors19.default.dim(` Final data deletion: ${error48.finalDeletionDate}`));
122721
123028
  } else if (error48 instanceof PercherApiError) {
122722
123029
  if (error48.status === 502 || error48.status === 503) {
122723
- lines.push(`${import_picocolors18.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error48.status}). Try again in a few seconds.`);
123030
+ lines.push(`${import_picocolors19.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error48.status}). Try again in a few seconds.`);
122724
123031
  } else {
122725
- lines.push(`${import_picocolors18.default.red("error:")} ${error48.message}`);
122726
- lines.push(import_picocolors18.default.dim(` ${error48.code} (HTTP ${error48.status})`));
123032
+ lines.push(`${import_picocolors19.default.red("error:")} ${error48.message}`);
123033
+ lines.push(import_picocolors19.default.dim(` ${error48.code} (HTTP ${error48.status})`));
122727
123034
  }
122728
123035
  if (error48.requestId)
122729
- lines.push(import_picocolors18.default.dim(` request: ${error48.requestId}`));
123036
+ lines.push(import_picocolors19.default.dim(` request: ${error48.requestId}`));
122730
123037
  } else if (error48 instanceof PercherCoreError) {
122731
- lines.push(`${import_picocolors18.default.red(`error (${error48.code}):`)} ${error48.message}`);
123038
+ lines.push(`${import_picocolors19.default.red(`error (${error48.code}):`)} ${error48.message}`);
122732
123039
  if (error48.hint)
122733
- lines.push(import_picocolors18.default.dim(` ${error48.hint}`));
123040
+ lines.push(import_picocolors19.default.dim(` ${error48.hint}`));
122734
123041
  } else if (error48 instanceof Error) {
122735
- lines.push(`${import_picocolors18.default.red("unexpected error:")} ${error48.message}`);
123042
+ lines.push(`${import_picocolors19.default.red("unexpected error:")} ${error48.message}`);
122736
123043
  if (error48.stack)
122737
- lines.push(import_picocolors18.default.dim(error48.stack.split(`
123044
+ lines.push(import_picocolors19.default.dim(error48.stack.split(`
122738
123045
  `).slice(1, 4).join(`
122739
123046
  `)));
122740
123047
  } else {
122741
- lines.push(`${import_picocolors18.default.red("unexpected error:")} ${String(error48)}`);
123048
+ lines.push(`${import_picocolors19.default.red("unexpected error:")} ${String(error48)}`);
122742
123049
  }
122743
123050
  try {
122744
123051
  await writer.write(encoder.encode(`${lines.join(`
@@ -122750,7 +123057,7 @@ async function renderError(stderr, error48) {
122750
123057
  }
122751
123058
 
122752
123059
  // src/version-check.ts
122753
- var import_picocolors19 = __toESM(require_picocolors(), 1);
123060
+ var import_picocolors20 = __toESM(require_picocolors(), 1);
122754
123061
  function isNewerStableVersion(latest, current) {
122755
123062
  const stable = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
122756
123063
  if (typeof latest !== "string" || latest.length > 64 || current.length > 64 || !stable.test(latest) || !stable.test(current))
@@ -122777,16 +123084,16 @@ async function checkForUpdates() {
122777
123084
  const data2 = await res.json();
122778
123085
  const latest = data2.version;
122779
123086
  if (isNewerStableVersion(latest, VERSION)) {
122780
- console.error(import_picocolors19.default.yellow(`
123087
+ console.error(import_picocolors20.default.yellow(`
122781
123088
  Update available: ${VERSION} → ${latest}`));
122782
- console.error(import_picocolors19.default.dim(`Run: bunx percher@latest
123089
+ console.error(import_picocolors20.default.dim(`Run: bunx percher@latest
122783
123090
  `));
122784
123091
  }
122785
123092
  } catch {}
122786
123093
  }
122787
123094
 
122788
123095
  // src/index.ts
122789
- async function run(argv, ctxOverride) {
123096
+ async function run(argv, ctxOverride, options = {}) {
122790
123097
  const cli = cac("percher");
122791
123098
  const ctx = ctxOverride ?? await createCliContext();
122792
123099
  registerAppsCommands(cli, ctx);
@@ -122802,6 +123109,7 @@ async function run(argv, ctxOverride) {
122802
123109
  registerMigrateCommands(cli, ctx);
122803
123110
  registerSharingCommands(cli, ctx);
122804
123111
  registerStatusCommands(cli, ctx);
123112
+ registerTelemetryCommands(cli, ctx);
122805
123113
  registerTokenCommands(cli, ctx);
122806
123114
  registerTransfersCommands(cli, ctx);
122807
123115
  registerAdminCommands(cli, ctx);
@@ -122809,10 +123117,14 @@ async function run(argv, ctxOverride) {
122809
123117
  cli.version(VERSION);
122810
123118
  const command = argv[0] ?? "help";
122811
123119
  try {
122812
- await showTelemetryNotice();
122813
123120
  cli.parse(["percher", "percher", ...argv], { run: false });
122814
123121
  const requested = argv[0];
122815
123122
  if (cli.matchedCommand) {
123123
+ await askTelemetryConsentOnce(ctx, argv, cli.matchedCommand.name, {
123124
+ ask: options.askTelemetry ?? askOnStdin,
123125
+ stderrIsTTY: options.stderrIsTTY ?? process.stderr.isTTY === true,
123126
+ setTimer: options.setTimer
123127
+ });
122816
123128
  await cli.runMatchedCommand();
122817
123129
  } else if (requested === undefined || requested === "help") {
122818
123130
  cli.outputHelp();