percher 0.4.1 → 0.4.2

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.
@@ -8824,7 +8824,7 @@ var require_util = __commonJS((exports) => {
8824
8824
  }
8825
8825
  path = url2.path;
8826
8826
  }
8827
- var isAbsolute2 = exports.isAbsolute(path);
8827
+ var isAbsolute3 = exports.isAbsolute(path);
8828
8828
  var parts = path.split(/\/+/);
8829
8829
  for (var part, up = 0, i = parts.length - 1;i >= 0; i--) {
8830
8830
  part = parts[i];
@@ -8844,7 +8844,7 @@ var require_util = __commonJS((exports) => {
8844
8844
  }
8845
8845
  path = parts.join("/");
8846
8846
  if (path === "") {
8847
- path = isAbsolute2 ? "/" : ".";
8847
+ path = isAbsolute3 ? "/" : ".";
8848
8848
  }
8849
8849
  if (url2) {
8850
8850
  url2.path = path;
@@ -101395,7 +101395,7 @@ function finalize(ctx, schema) {
101395
101395
  result.$schema = "http://json-schema.org/draft-07/schema#";
101396
101396
  } else if (ctx.target === "draft-04") {
101397
101397
  result.$schema = "http://json-schema.org/draft-04/schema#";
101398
- } else if (ctx.target === "openapi-3.0") {} else {}
101398
+ } else if (ctx.target === "openapi-3.0") {}
101399
101399
  if (ctx.external?.uri) {
101400
101400
  const id = ctx.external.registry.get(schema)?.id;
101401
101401
  if (!id)
@@ -101643,7 +101643,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
101643
101643
  if (val === undefined) {
101644
101644
  if (ctx.unrepresentable === "throw") {
101645
101645
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
101646
- } else {}
101646
+ }
101647
101647
  } else if (typeof val === "bigint") {
101648
101648
  if (ctx.unrepresentable === "throw") {
101649
101649
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -104202,7 +104202,8 @@ var devSchema = exports_external.object({
104202
104202
  var requiredEnvSchema = exports_external.record(exports_external.string(), exports_external.enum(["secret", "url", "string"]));
104203
104203
  var cronSchema = exports_external.object({
104204
104204
  schedule: exports_external.string().min(1),
104205
- command: exports_external.string().min(1)
104205
+ command: exports_external.string().min(1),
104206
+ description: exports_external.string().max(200).optional()
104206
104207
  }).strict();
104207
104208
  var addonsSchema = exports_external.object({
104208
104209
  pocketbase: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
@@ -105221,6 +105222,27 @@ function isRestoreJobStatus(v) {
105221
105222
  return typeof v === "string" && RESTORE_JOB_STATUSES_FOR_ERROR.includes(v);
105222
105223
  }
105223
105224
 
105225
+ class SunsetError extends PercherApiError {
105226
+ gateCode;
105227
+ sunsetDate;
105228
+ finalDeletionDate;
105229
+ dashboardUrl;
105230
+ constructor(message, options) {
105231
+ super(message, options);
105232
+ this.name = "SunsetError";
105233
+ this.gateCode = options.gateCode;
105234
+ this.sunsetDate = options.sunsetDate;
105235
+ this.finalDeletionDate = options.finalDeletionDate;
105236
+ this.dashboardUrl = options.dashboardUrl;
105237
+ }
105238
+ }
105239
+ function isSunsetGateBody(body) {
105240
+ if (typeof body !== "object" || body === null)
105241
+ return false;
105242
+ const b = body;
105243
+ return (b.error === "platform_read_only" || b.error === "platform_delete_gated") && typeof b.sunsetDate === "string" && typeof b.finalDeletionDate === "string" && typeof b.dashboardUrl === "string";
105244
+ }
105245
+
105224
105246
  // ../client/dist/http.js
105225
105247
  class Http {
105226
105248
  apiUrl;
@@ -105342,10 +105364,23 @@ class Http {
105342
105364
  return headers;
105343
105365
  }
105344
105366
  async parseError(res) {
105345
- let body = {};
105367
+ let raw;
105346
105368
  try {
105347
- body = await res.json();
105369
+ raw = await res.json();
105348
105370
  } catch {}
105371
+ if (res.status === 503 && isSunsetGateBody(raw)) {
105372
+ const requestIdHdr = res.headers.get("x-request-id") ?? undefined;
105373
+ return new SunsetError(raw.message, {
105374
+ status: res.status,
105375
+ code: raw.error,
105376
+ requestId: requestIdHdr,
105377
+ gateCode: raw.error,
105378
+ sunsetDate: raw.sunsetDate,
105379
+ finalDeletionDate: raw.finalDeletionDate,
105380
+ dashboardUrl: raw.dashboardUrl
105381
+ });
105382
+ }
105383
+ const body = typeof raw === "object" && raw !== null ? raw : {};
105349
105384
  const code = body.error?.code ?? (res.status >= 500 ? "INTERNAL_ERROR" : "VALIDATION_ERROR");
105350
105385
  const message = body.error?.message ?? res.statusText ?? "request failed";
105351
105386
  const extra = {};
@@ -105385,6 +105420,12 @@ class AccountResource {
105385
105420
  getDiagnostics() {
105386
105421
  return this.http.get("/account/diagnostics");
105387
105422
  }
105423
+ listTokens() {
105424
+ return this.http.get("/account/tokens");
105425
+ }
105426
+ revokeToken(id) {
105427
+ return this.http.delete(`/account/tokens/${encodeURIComponent(id)}`);
105428
+ }
105388
105429
  exportData() {
105389
105430
  return this.http.get("/account/export");
105390
105431
  }
@@ -105426,6 +105467,9 @@ class AppsResource {
105426
105467
  list() {
105427
105468
  return this.http.get("/apps");
105428
105469
  }
105470
+ listLight() {
105471
+ return this.http.get("/apps?light=1");
105472
+ }
105429
105473
  get(id) {
105430
105474
  return this.http.get(`/apps/${encodeURIComponent(id)}`);
105431
105475
  }
@@ -105461,6 +105505,37 @@ class AppsResource {
105461
105505
  crashAnalysisEnabled: enabled
105462
105506
  });
105463
105507
  }
105508
+ setGalleryOptIn(appRef, enabled) {
105509
+ return this.http.put(`/apps/${encodeURIComponent(appRef)}/gallery-opt-in`, {
105510
+ enabled
105511
+ });
105512
+ }
105513
+ setDescription(appRef, description) {
105514
+ return this.http.patch(`/apps/${encodeURIComponent(appRef)}/description`, {
105515
+ description
105516
+ });
105517
+ }
105518
+ setIconColour(appRef, colour) {
105519
+ return this.http.patch(`/apps/${encodeURIComponent(appRef)}/icon`, {
105520
+ colour
105521
+ });
105522
+ }
105523
+ uploadIconImage(appRef, bytes, mime) {
105524
+ const ab = new ArrayBuffer(bytes.byteLength);
105525
+ new Uint8Array(ab).set(bytes);
105526
+ return this.http.postBinary(`/apps/${encodeURIComponent(appRef)}/icon/image`, new Blob([ab]), { "Content-Type": mime });
105527
+ }
105528
+ deleteIconImage(appRef) {
105529
+ return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/icon/image`);
105530
+ }
105531
+ uploadPreviewImage(appRef, bytes, mime) {
105532
+ const ab = new ArrayBuffer(bytes.byteLength);
105533
+ new Uint8Array(ab).set(bytes);
105534
+ return this.http.postBinary(`/apps/${encodeURIComponent(appRef)}/preview/image`, new Blob([ab]), { "Content-Type": mime });
105535
+ }
105536
+ deletePreviewImage(appRef) {
105537
+ return this.http.deleteJson(`/apps/${encodeURIComponent(appRef)}/preview/image`);
105538
+ }
105464
105539
  async downloadSource(appRef, deployId) {
105465
105540
  return this.http.getBinary(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/source`);
105466
105541
  }
@@ -105545,12 +105620,18 @@ class AppsResource {
105545
105620
  listJobs(appRef, limit = 20) {
105546
105621
  return this.http.get(`/apps/${encodeURIComponent(appRef)}/jobs?limit=${limit}`);
105547
105622
  }
105623
+ listCrons(appRef) {
105624
+ return this.http.get(`/apps/${encodeURIComponent(appRef)}/crons`);
105625
+ }
105548
105626
  getDiagnostics(appRef) {
105549
105627
  return this.http.get(`/apps/${encodeURIComponent(appRef)}/diagnostics`);
105550
105628
  }
105551
105629
  getTopology(appRef) {
105552
105630
  return this.http.get(`/apps/${encodeURIComponent(appRef)}/topology`);
105553
105631
  }
105632
+ getPocketBaseVolume(appRef) {
105633
+ return this.http.get(`/apps/${encodeURIComponent(appRef)}/pocketbase/volume`);
105634
+ }
105554
105635
  getResources(appRef, opts = {}) {
105555
105636
  const params = new URLSearchParams;
105556
105637
  if (opts.window)
@@ -105712,6 +105793,40 @@ class EnvResource {
105712
105793
  }
105713
105794
  }
105714
105795
 
105796
+ // ../client/dist/resources/env-scan.js
105797
+ class EnvScanResource {
105798
+ http;
105799
+ constructor(http) {
105800
+ this.http = http;
105801
+ }
105802
+ scan(appId) {
105803
+ return this.http.post(`/apps/${encodeURIComponent(appId)}/env-scan`);
105804
+ }
105805
+ }
105806
+
105807
+ // ../client/dist/resources/export.js
105808
+ class ExportResource {
105809
+ http;
105810
+ constructor(http) {
105811
+ this.http = http;
105812
+ }
105813
+ mint(appRef, options) {
105814
+ return this.http.post(`/apps/${encodeURIComponent(appRef)}/export/mint`, {
105815
+ deliveryKind: options?.deliveryKind
105816
+ });
105817
+ }
105818
+ revoke(appRef, tokenId) {
105819
+ return this.http.post(`/apps/${encodeURIComponent(appRef)}/export/tokens/${encodeURIComponent(tokenId)}/revoke`);
105820
+ }
105821
+ history(appRef) {
105822
+ return this.http.get(`/apps/${encodeURIComponent(appRef)}/export/history`);
105823
+ }
105824
+ async download(downloadUrl) {
105825
+ const path = downloadUrl.startsWith("http") ? new URL(downloadUrl).pathname + new URL(downloadUrl).search : downloadUrl;
105826
+ return this.http.stream(path);
105827
+ }
105828
+ }
105829
+
105715
105830
  // ../client/dist/sse.js
105716
105831
  async function* parseSseStream(stream, signal) {
105717
105832
  const reader = stream.getReader();
@@ -105859,6 +105974,43 @@ class PlatformResource {
105859
105974
  }
105860
105975
  }
105861
105976
 
105977
+ // ../client/dist/resources/search.js
105978
+ class SearchResource {
105979
+ http;
105980
+ constructor(http) {
105981
+ this.http = http;
105982
+ }
105983
+ query(q) {
105984
+ return this.http.get(`/search?q=${encodeURIComponent(q)}`);
105985
+ }
105986
+ }
105987
+
105988
+ // ../client/dist/resources/security.js
105989
+ class SecurityResource {
105990
+ http;
105991
+ constructor(http) {
105992
+ this.http = http;
105993
+ }
105994
+ forApp(appRef) {
105995
+ return this.http.get(`/apps/${encodeURIComponent(appRef)}/security`);
105996
+ }
105997
+ forAppAsAdmin(appRef) {
105998
+ return this.http.get(`/admin/security/apps/${encodeURIComponent(appRef)}`);
105999
+ }
106000
+ resendNotificationAsAdmin(appRef) {
106001
+ return this.http.post(`/admin/security/apps/${encodeURIComponent(appRef)}/notify`, {});
106002
+ }
106003
+ overview() {
106004
+ return this.http.get("/admin/security/overview");
106005
+ }
106006
+ rescanAllAsAdmin() {
106007
+ return this.http.post("/admin/security/rescan", {});
106008
+ }
106009
+ rescanAppAsAdmin(appRef) {
106010
+ return this.http.post(`/admin/security/apps/${encodeURIComponent(appRef)}/rescan`, {});
106011
+ }
106012
+ }
106013
+
105862
106014
  // ../client/dist/resources/sharing.js
105863
106015
  class SharingResource {
105864
106016
  http;
@@ -105929,12 +106081,16 @@ class PercherClient {
105929
106081
  apps;
105930
106082
  data;
105931
106083
  env;
106084
+ envScan;
106085
+ export;
105932
106086
  logs;
105933
106087
  auth;
105934
106088
  billing;
105935
106089
  account;
105936
106090
  alerts;
105937
106091
  platform;
106092
+ security;
106093
+ search;
105938
106094
  sharing;
105939
106095
  invites;
105940
106096
  transfers;
@@ -105953,12 +106109,16 @@ class PercherClient {
105953
106109
  this.apps = new AppsResource(http);
105954
106110
  this.data = new DataResource(http);
105955
106111
  this.env = new EnvResource(http);
106112
+ this.envScan = new EnvScanResource(http);
106113
+ this.export = new ExportResource(http);
105956
106114
  this.logs = new LogsResource(http);
105957
106115
  this.auth = new AuthResource(http);
105958
106116
  this.billing = new BillingResource(http);
105959
106117
  this.account = new AccountResource(http);
105960
106118
  this.alerts = new AlertsResource(http);
105961
106119
  this.platform = new PlatformResource(http);
106120
+ this.security = new SecurityResource(http);
106121
+ this.search = new SearchResource(http);
105962
106122
  this.sharing = new SharingResource(http);
105963
106123
  this.invites = new InvitesResource(http);
105964
106124
  this.transfers = new TransfersResource(http);
@@ -106418,6 +106578,19 @@ var patterns = [
106418
106578
  missingEnvVars: []
106419
106579
  }
106420
106580
  },
106581
+ {
106582
+ pattern: /Bad Gateway|HTTP 502|\b502\b/i,
106583
+ result: {
106584
+ title: "Gateway error (transient)",
106585
+ explanation: "A gateway in front of your app returned 502 Bad Gateway while the platform was routing the deploy. This is a transient platform-side hiccup, not your app code.",
106586
+ suggestion: "Wait 30 seconds and run percher publish again. A single retry almost always lands cleanly. If 502s persist across several spaced-out retries, check status.percher.app — that's a platform incident, not your app.",
106587
+ errorClass: "infra_unavailable",
106588
+ phase: "infra",
106589
+ cause: "bad_gateway",
106590
+ relevantFiles: [],
106591
+ missingEnvVars: []
106592
+ }
106593
+ },
106421
106594
  {
106422
106595
  pattern: /start command could not be found|no start command/i,
106423
106596
  result: {
@@ -106536,7 +106709,20 @@ var patterns = [
106536
106709
  }
106537
106710
  },
106538
106711
  {
106539
- pattern: /POCKETBASE|pocketbase.*error|pocketbase.*fail/i,
106712
+ pattern: /committed_budget|\bbox admission|platform (?:is )?at capacity/i,
106713
+ result: {
106714
+ title: "Platform at capacity",
106715
+ explanation: "The platform couldn't fit this deploy right now — the box is full, so the box-admission gate refused to start the container. This is a platform-side limit, not a problem with your app code or configuration.",
106716
+ suggestion: "Wait a few minutes and run percher publish again — capacity frees up as other deploys finish. This failure does not count against your app, and a retry usually lands once there's room. If it persists, contact the operator.",
106717
+ errorClass: "platform_at_capacity",
106718
+ phase: "infra",
106719
+ cause: "box_admission_rejected",
106720
+ relevantFiles: [],
106721
+ missingEnvVars: []
106722
+ }
106723
+ },
106724
+ {
106725
+ pattern: /pocketbase[^\n]{0,40}(?:error|fail|crash|panic|exit|unhealthy|could ?not|cannot|connection refused|timed out|not (?:start|ready|reachable))|(?:failed to (?:start|connect|reach)|error (?:starting|connecting to)|unable to (?:start|reach|connect to))[^\n]{0,40}pocketbase/i,
106540
106726
  result: {
106541
106727
  title: "PocketBase error",
106542
106728
  explanation: "PocketBase could not start or connect properly.",
@@ -106819,6 +107005,12 @@ function recoveryFromErrorClass(errorClass, failure = {}) {
106819
107005
  args: failure.publishInput ?? {},
106820
107006
  reasonCode: "infra_unavailable"
106821
107007
  });
107008
+ case "platform_at_capacity":
107009
+ return recoveryRetry({
107010
+ suggestedTool: "percher_publish",
107011
+ args: failure.publishInput ?? {},
107012
+ reasonCode: "infra_transient"
107013
+ });
106822
107014
  case "config_invalid":
106823
107015
  return recoveryFixConfig({
106824
107016
  problems: [
@@ -106987,7 +107179,7 @@ async function buildFailureError(ctx, app, deployment) {
106987
107179
  buildLogFetchFailed = true;
106988
107180
  }
106989
107181
  const errorMessage = deployment.errorMessage ?? "";
106990
- const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError("Deployment failed", fullLog) ?? classifyError(errorMessage, "");
107182
+ const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError(errorMessage || "Deployment failed", fullLog);
106991
107183
  const rawErrorMessage = errorMessage && !buildLogTail && !classified?.explanation.includes(errorMessage) ? errorMessage : undefined;
106992
107184
  const problems = deployment.problems && deployment.problems.length > 0 ? deployment.problems : undefined;
106993
107185
  if (classified) {
@@ -107858,6 +108050,82 @@ async function envList(ctx, input = {}) {
107858
108050
  masked[k] = mask(v);
107859
108051
  return masked;
107860
108052
  }
108053
+ // ../core/dist/commands/export.js
108054
+ import { isAbsolute, resolve as resolvePath } from "node:path";
108055
+ var exportInputSchema = exports_external.object({
108056
+ app: exports_external.string().optional(),
108057
+ out: exports_external.string().optional(),
108058
+ deliveryKind: exports_external.enum(["cli", "mcp"]).default("cli")
108059
+ });
108060
+ function resolveAppName3(ctx, provided) {
108061
+ const name = provided ?? readPercherTomlAppName(ctx.cwd);
108062
+ if (!name) {
108063
+ throw new PercherCoreError("No app specified and no percher.toml found", {
108064
+ code: "export.no-app",
108065
+ hint: "Pass --app <name> or run from a directory with percher.toml."
108066
+ });
108067
+ }
108068
+ return name;
108069
+ }
108070
+ async function streamToFile(outPath, source) {
108071
+ const file2 = Bun.file(outPath);
108072
+ const writer = file2.writer();
108073
+ let bytes = 0;
108074
+ const reader = source.getReader();
108075
+ try {
108076
+ while (true) {
108077
+ const { done, value } = await reader.read();
108078
+ if (done)
108079
+ break;
108080
+ if (value && value.length > 0) {
108081
+ writer.write(value);
108082
+ bytes += value.length;
108083
+ }
108084
+ }
108085
+ await writer.end();
108086
+ } catch (err) {
108087
+ try {
108088
+ await writer.end();
108089
+ } catch {}
108090
+ try {
108091
+ await Bun.write(outPath, "");
108092
+ await Bun.file(outPath).delete?.();
108093
+ } catch {}
108094
+ throw err;
108095
+ }
108096
+ return { bytes };
108097
+ }
108098
+ function resolveOutPath(ctxCwd, filename) {
108099
+ if (isAbsolute(filename))
108100
+ return filename;
108101
+ return resolvePath(ctxCwd, filename);
108102
+ }
108103
+ async function exportApp(ctx, input) {
108104
+ const name = resolveAppName3(ctx, input.app);
108105
+ const mint = await ctx.client.export.mint(name, { deliveryKind: input.deliveryKind });
108106
+ const dl = await ctx.client.export.download(mint.downloadUrl);
108107
+ if (!dl.body) {
108108
+ throw new PercherCoreError("Export download returned an empty body", {
108109
+ code: "export.empty-body",
108110
+ hint: "Try again — the link is single-use, so a new mint is required."
108111
+ });
108112
+ }
108113
+ const filenameFromHeader = (() => {
108114
+ const cd = dl.headers.get("Content-Disposition") ?? "";
108115
+ const m = cd.match(/filename="?([^";]+)"?/);
108116
+ return m?.[1];
108117
+ })();
108118
+ const filename = input.out ?? filenameFromHeader ?? `${name}-export.tar.gz`;
108119
+ const outPath = resolveOutPath(ctx.cwd, filename);
108120
+ const { bytes } = await streamToFile(outPath, dl.body);
108121
+ return {
108122
+ appName: name,
108123
+ outPath,
108124
+ bytes,
108125
+ expiresAt: mint.expiresAt,
108126
+ includedSecrets: false
108127
+ };
108128
+ }
107861
108129
  // ../core/dist/commands/forgejo.js
107862
108130
  var FORGEJO_URL_RE = /^https:\/\/[\w.-]+\/[\w.-]+\/[\w.-]+(\.git)?$/;
107863
108131
  var forgejoConnectInputSchema = exports_external.object({
@@ -108728,6 +108996,26 @@ async function login(ctx, input = {}) {
108728
108996
  }
108729
108997
  // ../shared/dist/logs.js
108730
108998
  var MAX_LOG_SEARCH_LENGTH = 256;
108999
+ // ../shared/dist/platform-env.js
109000
+ var PERCHER_INJECTED_ENV_KEYS = new Set([
109001
+ "NODE_ENV",
109002
+ "PORT",
109003
+ "DATABASE_URL",
109004
+ "POCKETBASE_URL",
109005
+ "POCKETBASE_ADMIN_EMAIL",
109006
+ "POCKETBASE_ADMIN_PASSWORD",
109007
+ "PERCHER_APP_NAME",
109008
+ "PERCHER_APP_URL",
109009
+ "HTTP_PROXY",
109010
+ "HTTPS_PROXY",
109011
+ "NO_PROXY",
109012
+ "NODE_USE_ENV_PROXY"
109013
+ ]);
109014
+ var PROXY_LOWERCASE_ALIASES = new Set([
109015
+ "http_proxy",
109016
+ "https_proxy",
109017
+ "no_proxy"
109018
+ ]);
108731
109019
  // ../shared/dist/runtime.js
108732
109020
  var DEFAULT_NODE_VERSION_MAJOR = "24";
108733
109021
  var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
@@ -109641,7 +109929,7 @@ function collectWarnings2(collections, rlsPolicies, unsupported) {
109641
109929
  }
109642
109930
  // ../core/dist/commands/migrate-supabase-schema.js
109643
109931
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
109644
- import { isAbsolute, join as join17, relative as relative3, resolve as resolve6 } from "node:path";
109932
+ import { isAbsolute as isAbsolute2, join as join17, relative as relative3, resolve as resolve6 } from "node:path";
109645
109933
 
109646
109934
  // ../core/dist/commands/migrate-supabase-scripts.js
109647
109935
  function generateDataImportScripts(preview) {
@@ -110154,7 +110442,7 @@ async function migrateSupabaseSchema(ctx, input) {
110154
110442
  }
110155
110443
  function resolveOutputDir(cwd, requested) {
110156
110444
  const raw = requested ?? "./migration-preview";
110157
- return isAbsolute(raw) ? raw : resolve6(cwd, raw);
110445
+ return isAbsolute2(raw) ? raw : resolve6(cwd, raw);
110158
110446
  }
110159
110447
  async function writePreviewFiles(ctx, outputDir, preview) {
110160
110448
  await mkdir2(outputDir, { recursive: true });
@@ -110927,14 +111215,46 @@ function matchSuspended(err) {
110927
111215
  function matchAppSuspended(err) {
110928
111216
  if (err.code !== "APP_SUSPENDED" || err.status !== 403)
110929
111217
  return null;
111218
+ const liftAfterRaw = typeof err.extra?.liftAfter === "string" ? err.extra.liftAfter : null;
111219
+ const liftableNow = err.extra?.liftableNow === true;
111220
+ const canLiftViaPublish = err.extra?.canLiftViaPublish === true;
111221
+ const fixInstructions = typeof err.extra?.fixInstructions === "string" && err.extra.fixInstructions.length > 0 ? err.extra.fixInstructions : null;
111222
+ let suggestion;
111223
+ let prompt;
111224
+ let options;
111225
+ if (canLiftViaPublish && liftableNow) {
111226
+ suggestion = fixInstructions ? `Fix the underlying cause (${fixInstructions}) and run \`percher publish\` — a successful deploy will lift the suspension automatically.` : "Fix the underlying cause and run `percher publish` — a successful deploy will lift the suspension automatically.";
111227
+ prompt = fixInstructions ? `${err.message}
111228
+
111229
+ The suspension window has passed. Fix the underlying cause per the admin's instructions (${fixInstructions}), then run \`percher publish\` — a successful deploy will lift the suspension.` : `${err.message}
111230
+
111231
+ The suspension window has passed. Fix the underlying cause, then run \`percher publish\` — a successful deploy will lift the suspension.`;
111232
+ options = ["fix and republish", "open dashboard", "contact support"];
111233
+ } else if (canLiftViaPublish && liftAfterRaw) {
111234
+ suggestion = fixInstructions ? `Wait until ${liftAfterRaw}, then fix the underlying cause (${fixInstructions}) and run \`percher publish\`. A successful deploy after the window passes will lift the suspension.` : `Wait until ${liftAfterRaw}, then run \`percher publish\`. A successful deploy after the window passes will lift the suspension.`;
111235
+ prompt = `${err.message}
111236
+
111237
+ You can try again after ${liftAfterRaw}. Until then, retrying will fail.`;
111238
+ options = ["wait and republish", "open dashboard", "contact support"];
111239
+ } else if (liftAfterRaw) {
111240
+ suggestion = `A Percher admin will review the suspension after ${liftAfterRaw}. Contact support if you need it lifted sooner.`;
111241
+ prompt = `${err.message}
111242
+
111243
+ Admin will review after ${liftAfterRaw}. Contact support at https://percher.app if you need it lifted sooner.`;
111244
+ options = ["wait", "contact support"];
111245
+ } else {
111246
+ suggestion = "Contact support at https://percher.app or check the dashboard for unsuspend options. Re-publishing won't unsuspend the app.";
111247
+ prompt = `${err.message} Open https://percher.app or contact support — re-publish won't help until the app is unsuspended.`;
111248
+ options = ["contact support", "open dashboard"];
111249
+ }
110930
111250
  return {
110931
111251
  title: "App suspended",
110932
111252
  explanation: err.message,
110933
- suggestion: "Contact support at https://percher.app or check the dashboard for unsuspend options. Re-publishing won't unsuspend the app.",
111253
+ suggestion,
110934
111254
  recovery: recoveryAsk({
110935
- prompt: `${err.message} Open https://percher.app or contact support — re-publish won't help until the app is unsuspended.`,
110936
- options: ["contact support", "open dashboard"],
110937
- reasonCode: "unknown"
111255
+ prompt,
111256
+ options,
111257
+ reasonCode: "app_suspended"
110938
111258
  }),
110939
111259
  summary: `App suspended: ${err.message}`
110940
111260
  };
@@ -113059,6 +113379,7 @@ var SOURCE_EXTS2 = new Set([
113059
113379
  ]);
113060
113380
  var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
113061
113381
  // ../core/dist/plans.js
113382
+ var PB_INITIAL_FOOTPRINT_MB = 256;
113062
113383
  var PLAN_CATALOG = {
113063
113384
  free: {
113064
113385
  label: "Free",
@@ -113080,7 +113401,7 @@ var PLAN_CATALOG = {
113080
113401
  logRetentionDays: 1,
113081
113402
  buildLogRetentionDays: 3,
113082
113403
  cronJobs: 1,
113083
- storageMb: 500,
113404
+ storageMb: PB_INITIAL_FOOTPRINT_MB,
113084
113405
  instances: 1,
113085
113406
  kvStorageMb: 10,
113086
113407
  uptimeIntervalSec: 0,
@@ -113112,7 +113433,7 @@ var PLAN_CATALOG = {
113112
113433
  logRetentionDays: 7,
113113
113434
  buildLogRetentionDays: 14,
113114
113435
  cronJobs: 3,
113115
- storageMb: 5000,
113436
+ storageMb: PB_INITIAL_FOOTPRINT_MB,
113116
113437
  instances: 1,
113117
113438
  kvStorageMb: 50,
113118
113439
  uptimeIntervalSec: 300,
@@ -113144,7 +113465,7 @@ var PLAN_CATALOG = {
113144
113465
  logRetentionDays: 30,
113145
113466
  buildLogRetentionDays: 30,
113146
113467
  cronJobs: 10,
113147
- storageMb: 25000,
113468
+ storageMb: PB_INITIAL_FOOTPRINT_MB,
113148
113469
  instances: 2,
113149
113470
  kvStorageMb: 200,
113150
113471
  uptimeIntervalSec: 60,
@@ -113176,7 +113497,7 @@ var PLAN_CATALOG = {
113176
113497
  logRetentionDays: 90,
113177
113498
  buildLogRetentionDays: 90,
113178
113499
  cronJobs: 25,
113179
- storageMb: 75000,
113500
+ storageMb: PB_INITIAL_FOOTPRINT_MB,
113180
113501
  instances: 4,
113181
113502
  kvStorageMb: 1000,
113182
113503
  uptimeIntervalSec: 30,
@@ -113290,6 +113611,18 @@ function formatStatus(status2) {
113290
113611
  return import_picocolors.default.red(status2);
113291
113612
  return import_picocolors.default.yellow(status2);
113292
113613
  }
113614
+ function formatBytes(bytes) {
113615
+ if (!Number.isFinite(bytes) || bytes < 0)
113616
+ return "—";
113617
+ const units = ["B", "KB", "MB", "GB", "TB"];
113618
+ let n = bytes;
113619
+ let i = 0;
113620
+ while (n >= 1024 && i < units.length - 1) {
113621
+ n /= 1024;
113622
+ i += 1;
113623
+ }
113624
+ return `${i === 0 ? n : n.toFixed(1)} ${units[i]}`;
113625
+ }
113293
113626
  function firstNonFlagAfter(argv, from, valueFlags = new Set) {
113294
113627
  let i = from + 1;
113295
113628
  while (i < argv.length) {
@@ -113329,6 +113662,9 @@ async function runDeploysInspect(ctx, input) {
113329
113662
  await writeLine(ctx, `${import_picocolors.default.bold("Type:")} ${deploy.type}`);
113330
113663
  }
113331
113664
  await writeLine(ctx, `${import_picocolors.default.bold("Created:")} ${formatDeployTimestamp(deploy.createdAt)}`);
113665
+ if (typeof deploy.imageSizeBytes === "number") {
113666
+ await writeLine(ctx, `${import_picocolors.default.bold("Image:")} ${formatBytes(deploy.imageSizeBytes)}`);
113667
+ }
113332
113668
  if (deploy.url) {
113333
113669
  await writeLine(ctx, `${import_picocolors.default.bold("URL:")} ${deploy.url}`);
113334
113670
  }
@@ -113789,6 +114125,9 @@ async function handlePublishResult(ctx, result, options = {}) {
113789
114125
  await writeLine(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
113790
114126
  }
113791
114127
  }
114128
+ if (typeof result.deployment?.imageSizeBytes === "number") {
114129
+ await writeLine(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
114130
+ }
113792
114131
  if (result.replacedPreview) {
113793
114132
  await writeLine(ctx, import_picocolors4.default.dim(" (replaced previous preview — free plan keeps 1 preview slot per app)"));
113794
114133
  }
@@ -113897,6 +114236,9 @@ function registerAppsCommands(cli, ctx) {
113897
114236
  } else if (status2 === "fresh") {
113898
114237
  await writeLine(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
113899
114238
  }
114239
+ if (typeof result.deployment?.imageSizeBytes === "number") {
114240
+ await writeLine(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
114241
+ }
113900
114242
  }
113901
114243
  });
113902
114244
  cli.command("rename <name> [app]", "Rename an app (changes its subdomain)").action(async (name, app) => {
@@ -114318,10 +114660,24 @@ function registerEnvCommands(cli, ctx) {
114318
114660
  });
114319
114661
  }
114320
114662
 
114321
- // src/commands/github.ts
114663
+ // src/commands/export.ts
114322
114664
  var import_picocolors9 = __toESM(require_picocolors(), 1);
114665
+ function registerExportCommands(cli, ctx) {
114666
+ cli.command("export [app]", "Download an export bundle for an app (no secrets)").option("--out <path>", "Where to write the .tar.gz (defaults to ./{name}-{date}.tar.gz)").action(async (app, options) => {
114667
+ const result = await exportApp(ctx, {
114668
+ app,
114669
+ out: options.out,
114670
+ deliveryKind: "cli"
114671
+ });
114672
+ await writeLine(ctx, `${import_picocolors9.default.green("✓")} exported ${result.appName} → ${result.outPath} (${(result.bytes / 1024).toFixed(1)} KB)`);
114673
+ await writeLine(ctx, import_picocolors9.default.dim("Env values are not included. To export encrypted env values, use the dashboard's Settings → Export and confirm your password."));
114674
+ });
114675
+ }
114676
+
114677
+ // src/commands/github.ts
114678
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
114323
114679
  async function printWebhookInstructions(ctx, instructions, branch) {
114324
- await writeLine(ctx, import_picocolors9.default.bold("Add this webhook in GitHub to enable auto-deploy on push:"));
114680
+ await writeLine(ctx, import_picocolors10.default.bold("Add this webhook in GitHub to enable auto-deploy on push:"));
114325
114681
  await writeLine(ctx, ` URL: ${instructions.settingsUrl}`);
114326
114682
  await writeLine(ctx, "");
114327
114683
  await writeLine(ctx, " Field Value");
@@ -114331,9 +114687,9 @@ async function printWebhookInstructions(ctx, instructions, branch) {
114331
114687
  await writeLine(ctx, ` Secret ${instructions.secret}`);
114332
114688
  await writeLine(ctx, " Which events? Just the push event");
114333
114689
  await writeLine(ctx, "");
114334
- await writeLine(ctx, import_picocolors9.default.yellow(" ⚠ Treat this secret like a password — save it now, it won't be shown again."));
114690
+ await writeLine(ctx, import_picocolors10.default.yellow(" ⚠ Treat this secret like a password — save it now, it won't be shown again."));
114335
114691
  await writeLine(ctx, "");
114336
- await writeLine(ctx, import_picocolors9.default.dim(`After adding the webhook, every push to ${branch} will auto-deploy.`));
114692
+ await writeLine(ctx, import_picocolors10.default.dim(`After adding the webhook, every push to ${branch} will auto-deploy.`));
114337
114693
  }
114338
114694
  function registerGithubCommands(cli, ctx) {
114339
114695
  cli.command("github [action] [repoUrl] [...rest]", "GitHub integration: connect | setup-webhook").option("--app <name>", "App name (overrides percher.toml)").option("--branch <branch>", "(connect only) Branch to track (default: main)").action(async (action, repoUrl, _rest, options) => {
@@ -114346,24 +114702,24 @@ function registerGithubCommands(cli, ctx) {
114346
114702
  repoUrl,
114347
114703
  branch: options.branch
114348
114704
  });
114349
- await writeLine(ctx, `${import_picocolors9.default.green("✓")} Connected to ${result.repoUrl} (branch: ${result.branch})`);
114350
- await writeLine(ctx, `${import_picocolors9.default.green("✓")} Deploy started: ${result.deployId}`);
114705
+ await writeLine(ctx, `${import_picocolors10.default.green("✓")} Connected to ${result.repoUrl} (branch: ${result.branch})`);
114706
+ await writeLine(ctx, `${import_picocolors10.default.green("✓")} Deploy started: ${result.deployId}`);
114351
114707
  await writeLine(ctx, "");
114352
114708
  if (result.webhookSetupFailed) {
114353
- await writeLine(ctx, import_picocolors9.default.yellow(`⚠ Webhook setup failed: ${result.webhookSetupError ?? "unknown error"}`));
114354
- await writeLine(ctx, import_picocolors9.default.yellow(" Auto-deploy will NOT work until you re-run webhook setup:"));
114355
- await writeLine(ctx, ` ${import_picocolors9.default.bold(`percher github setup-webhook${options.app ? ` --app ${options.app}` : ""}`)}`);
114709
+ await writeLine(ctx, import_picocolors10.default.yellow(`⚠ Webhook setup failed: ${result.webhookSetupError ?? "unknown error"}`));
114710
+ await writeLine(ctx, import_picocolors10.default.yellow(" Auto-deploy will NOT work until you re-run webhook setup:"));
114711
+ await writeLine(ctx, ` ${import_picocolors10.default.bold(`percher github setup-webhook${options.app ? ` --app ${options.app}` : ""}`)}`);
114356
114712
  return;
114357
114713
  }
114358
114714
  if (result.webhookInstructions) {
114359
114715
  await printWebhookInstructions(ctx, result.webhookInstructions, result.branch);
114360
114716
  }
114361
- await writeLine(ctx, import_picocolors9.default.dim(`Track the initial deploy: percher deploys inspect ${result.deployId}`));
114717
+ await writeLine(ctx, import_picocolors10.default.dim(`Track the initial deploy: percher deploys inspect ${result.deployId}`));
114362
114718
  return;
114363
114719
  }
114364
114720
  if (action === "setup-webhook") {
114365
114721
  const result = await githubSetupWebhook(ctx, { app: options.app });
114366
- await writeLine(ctx, `${import_picocolors9.default.green("✓")} Webhook regenerated for ${result.app}`);
114722
+ await writeLine(ctx, `${import_picocolors10.default.green("✓")} Webhook regenerated for ${result.app}`);
114367
114723
  await writeLine(ctx, "");
114368
114724
  await printWebhookInstructions(ctx, result.webhookInstructions, result.branch);
114369
114725
  return;
@@ -114375,17 +114731,17 @@ function registerGithubCommands(cli, ctx) {
114375
114731
  }
114376
114732
 
114377
114733
  // src/commands/migrate.ts
114378
- var import_picocolors10 = __toESM(require_picocolors(), 1);
114734
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
114379
114735
  function registerMigrateCommands(cli, ctx) {
114380
114736
  cli.command("migrate-from-supabase", "Migrate a Supabase project to PocketBase (FUTURE9 Phase A read-only + Phase B preview writes)").option("--project <ref>", "Supabase project reference (the 20-char id from the project URL)").option("--token <token>", "Supabase personal access token (sbp_...). Read from SUPABASE_ACCESS_TOKEN if omitted.").option("--inspect-only", "Run schema introspection only (Phase A — read-only, no file writes)").option("--output-dir <dir>", "Where to write the Phase B preview files (default: ./migration-preview/)").option("--json", "Output the structured result as JSON").action(async (options) => {
114381
114737
  const projectRef = options.project;
114382
114738
  const token = options.token ?? ctx.env.SUPABASE_ACCESS_TOKEN;
114383
114739
  if (!projectRef) {
114384
- await writeLine(ctx, `${import_picocolors10.default.red("✗")} Missing --project. Pass your Supabase project ref (e.g. --project abcdefghijklmnopqrst).`);
114740
+ await writeLine(ctx, `${import_picocolors11.default.red("✗")} Missing --project. Pass your Supabase project ref (e.g. --project abcdefghijklmnopqrst).`);
114385
114741
  throw new SilentExitError(1);
114386
114742
  }
114387
114743
  if (!token) {
114388
- await writeLine(ctx, `${import_picocolors10.default.red("✗")} Missing --token. Pass --token sbp_... or set SUPABASE_ACCESS_TOKEN. Generate one at https://supabase.com/dashboard/account/tokens`);
114744
+ await writeLine(ctx, `${import_picocolors11.default.red("✗")} Missing --token. Pass --token sbp_... or set SUPABASE_ACCESS_TOKEN. Generate one at https://supabase.com/dashboard/account/tokens`);
114389
114745
  throw new SilentExitError(1);
114390
114746
  }
114391
114747
  if (options.inspectOnly) {
@@ -114420,103 +114776,103 @@ function registerMigrateCommands(cli, ctx) {
114420
114776
  async function renderInspectHuman(ctx, result) {
114421
114777
  if (result.status === "failed") {
114422
114778
  const err = result.error;
114423
- await writeLine(ctx, `${import_picocolors10.default.red("✗")} ${err?.title ?? "Inspection failed"}`);
114779
+ await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Inspection failed"}`);
114424
114780
  if (err?.explanation)
114425
114781
  await writeLine(ctx, ` ${err.explanation}`);
114426
114782
  if (err?.suggestion)
114427
- await writeLine(ctx, import_picocolors10.default.dim(` ${err.suggestion}`));
114783
+ await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
114428
114784
  return;
114429
114785
  }
114430
- await writeLine(ctx, `${import_picocolors10.default.green("✓")} ${result.summary}`);
114786
+ await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${result.summary}`);
114431
114787
  await writeLine(ctx, "");
114432
- await writeLine(ctx, import_picocolors10.default.bold("Tables"));
114788
+ await writeLine(ctx, import_picocolors11.default.bold("Tables"));
114433
114789
  if (result.tables.length === 0) {
114434
- await writeLine(ctx, import_picocolors10.default.dim(" (none in user schemas)"));
114790
+ await writeLine(ctx, import_picocolors11.default.dim(" (none in user schemas)"));
114435
114791
  }
114436
114792
  for (const table of result.tables) {
114437
114793
  const pkCount = table.columns.filter((c) => c.isPrimaryKey).length;
114438
114794
  const fkCount = table.columns.filter((c) => c.foreignKey !== null).length;
114439
114795
  const policyCount = table.rlsPolicies.length;
114440
114796
  const rowEstimate = table.estimatedRowCount.toLocaleString();
114441
- await writeLine(ctx, ` ${import_picocolors10.default.cyan(`${table.schema}.${table.name}`)} ${import_picocolors10.default.dim(`cols=${table.columns.length} pk=${pkCount} fk=${fkCount} rls=${policyCount} rows≈${rowEstimate}`)}`);
114797
+ await writeLine(ctx, ` ${import_picocolors11.default.cyan(`${table.schema}.${table.name}`)} ${import_picocolors11.default.dim(`cols=${table.columns.length} pk=${pkCount} fk=${fkCount} rls=${policyCount} rows≈${rowEstimate}`)}`);
114442
114798
  }
114443
114799
  if (result.storageBuckets.length > 0) {
114444
114800
  await writeLine(ctx, "");
114445
- await writeLine(ctx, import_picocolors10.default.bold("Storage buckets"));
114801
+ await writeLine(ctx, import_picocolors11.default.bold("Storage buckets"));
114446
114802
  for (const bucket of result.storageBuckets) {
114447
114803
  const visibility = bucket.public ? "public" : "private";
114448
- await writeLine(ctx, ` ${import_picocolors10.default.cyan(bucket.name)} ${import_picocolors10.default.dim(`(${visibility})`)}`);
114804
+ await writeLine(ctx, ` ${import_picocolors11.default.cyan(bucket.name)} ${import_picocolors11.default.dim(`(${visibility})`)}`);
114449
114805
  }
114450
114806
  }
114451
114807
  if (result.authUsersCount !== null) {
114452
114808
  await writeLine(ctx, "");
114453
- await writeLine(ctx, `${import_picocolors10.default.bold("Auth users")}: ${result.authUsersCount}`);
114809
+ await writeLine(ctx, `${import_picocolors11.default.bold("Auth users")}: ${result.authUsersCount}`);
114454
114810
  }
114455
114811
  if (result.warnings.length > 0) {
114456
114812
  await writeLine(ctx, "");
114457
- await writeLine(ctx, import_picocolors10.default.bold(`Warnings (${result.warnings.length})`));
114813
+ await writeLine(ctx, import_picocolors11.default.bold(`Warnings (${result.warnings.length})`));
114458
114814
  for (const warning of result.warnings) {
114459
- await writeLine(ctx, ` ${import_picocolors10.default.yellow("⚠")} ${warning}`);
114815
+ await writeLine(ctx, ` ${import_picocolors11.default.yellow("⚠")} ${warning}`);
114460
114816
  }
114461
114817
  }
114462
114818
  await writeLine(ctx, "");
114463
- await writeLine(ctx, import_picocolors10.default.dim("--inspect-only is read-only. Drop the flag to also write the Phase B PocketBase-schema preview."));
114819
+ await writeLine(ctx, import_picocolors11.default.dim("--inspect-only is read-only. Drop the flag to also write the Phase B PocketBase-schema preview."));
114464
114820
  }
114465
114821
  async function renderSchemaHuman(ctx, result) {
114466
114822
  if (result.status === "failed") {
114467
114823
  const err = result.error;
114468
- await writeLine(ctx, `${import_picocolors10.default.red("✗")} ${err?.title ?? "Migration preview failed"}`);
114824
+ await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Migration preview failed"}`);
114469
114825
  if (err?.explanation)
114470
114826
  await writeLine(ctx, ` ${err.explanation}`);
114471
114827
  if (err?.suggestion)
114472
- await writeLine(ctx, import_picocolors10.default.dim(` ${err.suggestion}`));
114828
+ await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
114473
114829
  return;
114474
114830
  }
114475
- await writeLine(ctx, `${import_picocolors10.default.green("✓")} ${result.summary}`);
114831
+ await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${result.summary}`);
114476
114832
  await writeLine(ctx, "");
114477
- await writeLine(ctx, import_picocolors10.default.bold("Files written"));
114833
+ await writeLine(ctx, import_picocolors11.default.bold("Files written"));
114478
114834
  for (const path of result.filesWritten) {
114479
- await writeLine(ctx, ` ${import_picocolors10.default.cyan(path)}`);
114835
+ await writeLine(ctx, ` ${import_picocolors11.default.cyan(path)}`);
114480
114836
  }
114481
114837
  const preview = result.preview;
114482
114838
  if (!preview)
114483
114839
  return;
114484
114840
  if (preview.collections.length > 0) {
114485
114841
  await writeLine(ctx, "");
114486
- await writeLine(ctx, import_picocolors10.default.bold("Collections"));
114842
+ await writeLine(ctx, import_picocolors11.default.bold("Collections"));
114487
114843
  for (const collection of preview.collections) {
114488
114844
  const fields = collection.schema.length;
114489
114845
  const relations = collection.schema.filter((f) => f.type === "relation").length;
114490
- await writeLine(ctx, ` ${import_picocolors10.default.cyan(collection.name)} ${import_picocolors10.default.dim(`← ${collection.sourceTable} fields=${fields} relations=${relations}`)}`);
114846
+ await writeLine(ctx, ` ${import_picocolors11.default.cyan(collection.name)} ${import_picocolors11.default.dim(`← ${collection.sourceTable} fields=${fields} relations=${relations}`)}`);
114491
114847
  }
114492
114848
  }
114493
114849
  if (preview.warnings.length > 0) {
114494
114850
  await writeLine(ctx, "");
114495
- await writeLine(ctx, import_picocolors10.default.bold(`Warnings (${preview.warnings.length})`));
114851
+ await writeLine(ctx, import_picocolors11.default.bold(`Warnings (${preview.warnings.length})`));
114496
114852
  for (const warning of preview.warnings.slice(0, 10)) {
114497
- await writeLine(ctx, ` ${import_picocolors10.default.yellow("⚠")} ${warning}`);
114853
+ await writeLine(ctx, ` ${import_picocolors11.default.yellow("⚠")} ${warning}`);
114498
114854
  }
114499
114855
  if (preview.warnings.length > 10) {
114500
- await writeLine(ctx, import_picocolors10.default.dim(` …and ${preview.warnings.length - 10} more — see MIGRATION_NOTES.md`));
114856
+ await writeLine(ctx, import_picocolors11.default.dim(` …and ${preview.warnings.length - 10} more — see MIGRATION_NOTES.md`));
114501
114857
  }
114502
114858
  }
114503
114859
  await writeLine(ctx, "");
114504
- await writeLine(ctx, import_picocolors10.default.dim("Phase B is a preview. Phase C (data import) and Phase D (client rewrite) ship in later releases. Review MIGRATION_NOTES.md before any import."));
114860
+ await writeLine(ctx, import_picocolors11.default.dim("Phase B is a preview. Phase C (data import) and Phase D (client rewrite) ship in later releases. Review MIGRATION_NOTES.md before any import."));
114505
114861
  }
114506
114862
 
114507
114863
  // src/commands/sharing.ts
114508
- var import_picocolors11 = __toESM(require_picocolors(), 1);
114864
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
114509
114865
  function registerSharingCommands(cli, ctx) {
114510
114866
  cli.command("share [action] [arg] [valueOrRole]", "Per-app collaborator invites: list | invite <email> | role <shareId> <role> | revoke <shareId>").option("--app <name>", "App name (overrides percher.toml)").option("--role <role>", "(invite only) viewer | admin — viewer is read-only, admin can deploy + change config").action(async (action, arg, valueOrRole, options) => {
114511
114867
  if (action === "list" || action === undefined) {
114512
114868
  const rows = await shareList(ctx, { app: options.app });
114513
114869
  if (rows.length === 0) {
114514
- await writeLine(ctx, import_picocolors11.default.dim("No collaborators."));
114870
+ await writeLine(ctx, import_picocolors12.default.dim("No collaborators."));
114515
114871
  return;
114516
114872
  }
114517
114873
  for (const r of rows) {
114518
- const state = r.accepted ? import_picocolors11.default.green("accepted") : import_picocolors11.default.yellow("pending");
114519
- const roleStr = r.role === "admin" ? import_picocolors11.default.cyan("admin") : import_picocolors11.default.dim("viewer");
114874
+ const state = r.accepted ? import_picocolors12.default.green("accepted") : import_picocolors12.default.yellow("pending");
114875
+ const roleStr = r.role === "admin" ? import_picocolors12.default.cyan("admin") : import_picocolors12.default.dim("viewer");
114520
114876
  await writeLine(ctx, `${r.id} ${roleStr.padEnd(15)} ${state.padEnd(15)} ${r.email}`);
114521
114877
  }
114522
114878
  return;
@@ -114534,8 +114890,8 @@ function registerSharingCommands(cli, ctx) {
114534
114890
  email: arg,
114535
114891
  role: roleRaw
114536
114892
  });
114537
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} Invited ${created.email} as ${created.role} (id ${created.id})`);
114538
- await writeLine(ctx, import_picocolors11.default.dim(`Pending until accepted — invite expires ${new Date(created.expiresAt).toLocaleDateString()}.`));
114893
+ await writeLine(ctx, `${import_picocolors12.default.green("✓")} Invited ${created.email} as ${created.role} (id ${created.id})`);
114894
+ await writeLine(ctx, import_picocolors12.default.dim(`Pending until accepted — invite expires ${new Date(created.expiresAt).toLocaleDateString()}.`));
114539
114895
  return;
114540
114896
  }
114541
114897
  if (action === "role") {
@@ -114550,7 +114906,7 @@ function registerSharingCommands(cli, ctx) {
114550
114906
  shareId: arg,
114551
114907
  role: valueOrRole
114552
114908
  });
114553
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${updated.email} is now ${import_picocolors11.default.cyan(updated.role)}`);
114909
+ await writeLine(ctx, `${import_picocolors12.default.green("✓")} ${updated.email} is now ${import_picocolors12.default.cyan(updated.role)}`);
114554
114910
  return;
114555
114911
  }
114556
114912
  if (action === "revoke") {
@@ -114558,7 +114914,7 @@ function registerSharingCommands(cli, ctx) {
114558
114914
  throw new Error("Usage: percher share revoke <shareId> [--app <name>]");
114559
114915
  }
114560
114916
  await shareRevoke(ctx, { app: options.app, shareId: arg });
114561
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} Revoked share ${arg}`);
114917
+ await writeLine(ctx, `${import_picocolors12.default.green("✓")} Revoked share ${arg}`);
114562
114918
  return;
114563
114919
  }
114564
114920
  throw new Error(`Unknown share action "${action}". Use list | invite <email> | role <shareId> <role> | revoke <shareId>.`);
@@ -114571,11 +114927,11 @@ function registerSharingCommands(cli, ctx) {
114571
114927
  if (action === undefined || action === "list") {
114572
114928
  const pending = await invitesList(ctx, {});
114573
114929
  if (pending.length === 0) {
114574
- await writeLine(ctx, import_picocolors11.default.dim("No pending invites."));
114930
+ await writeLine(ctx, import_picocolors12.default.dim("No pending invites."));
114575
114931
  return;
114576
114932
  }
114577
114933
  for (const inv of pending) {
114578
- const roleStr = inv.role === "admin" ? import_picocolors11.default.cyan("admin") : import_picocolors11.default.dim("viewer");
114934
+ const roleStr = inv.role === "admin" ? import_picocolors12.default.cyan("admin") : import_picocolors12.default.dim("viewer");
114579
114935
  await writeLine(ctx, `${inv.id} ${roleStr.padEnd(15)} ${inv.appName.padEnd(24)} from ${inv.invitedBy}`);
114580
114936
  }
114581
114937
  return;
@@ -114586,10 +114942,10 @@ function registerSharingCommands(cli, ctx) {
114586
114942
  }
114587
114943
  const accepted = await invitesAccept(ctx, { shareId });
114588
114944
  if (accepted.alreadyAccepted) {
114589
- await writeLine(ctx, import_picocolors11.default.dim(`Invite ${shareId} was already accepted.`));
114945
+ await writeLine(ctx, import_picocolors12.default.dim(`Invite ${shareId} was already accepted.`));
114590
114946
  return;
114591
114947
  }
114592
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} Accepted — you now have ${import_picocolors11.default.cyan(accepted.role)} access to the shared app.`);
114948
+ await writeLine(ctx, `${import_picocolors12.default.green("✓")} Accepted — you now have ${import_picocolors12.default.cyan(accepted.role)} access to the shared app.`);
114593
114949
  return;
114594
114950
  }
114595
114951
  if (action === "decline") {
@@ -114597,7 +114953,7 @@ function registerSharingCommands(cli, ctx) {
114597
114953
  throw new Error("Usage: percher invites decline <shareId>");
114598
114954
  }
114599
114955
  await invitesDecline(ctx, { shareId });
114600
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} Declined invite ${shareId}.`);
114956
+ await writeLine(ctx, `${import_picocolors12.default.green("✓")} Declined invite ${shareId}.`);
114601
114957
  return;
114602
114958
  }
114603
114959
  throw new Error(`Unknown invites action "${action}". Use list | accept <shareId> | decline <shareId>.`);
@@ -114608,7 +114964,7 @@ function registerSharingCommands(cli, ctx) {
114608
114964
  }
114609
114965
 
114610
114966
  // src/commands/status.ts
114611
- var import_picocolors12 = __toESM(require_picocolors(), 1);
114967
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
114612
114968
  function registerStatusCommands(cli, ctx) {
114613
114969
  cli.command("status", "Show Percher platform status (API + worker health)").option("--json", "Output JSON instead of formatted text").action(async (options) => {
114614
114970
  const result = await status(ctx, { json: Boolean(options.json) });
@@ -114624,44 +114980,44 @@ function registerStatusCommands(cli, ctx) {
114624
114980
  });
114625
114981
  }
114626
114982
  async function renderHumanStatus(ctx, result) {
114627
- const symbol2 = result.verdict === "operational" ? import_picocolors12.default.green("✓") : result.verdict === "worker-degraded" ? import_picocolors12.default.yellow("⚠") : import_picocolors12.default.red("✗");
114983
+ const symbol2 = result.verdict === "operational" ? import_picocolors13.default.green("✓") : result.verdict === "worker-degraded" ? import_picocolors13.default.yellow("⚠") : import_picocolors13.default.red("✗");
114628
114984
  await writeLine(ctx, `${symbol2} ${result.summary}`);
114629
114985
  if (!result.health)
114630
114986
  return;
114631
114987
  const worker = result.health.worker;
114632
114988
  if (!worker) {
114633
- await writeLine(ctx, import_picocolors12.default.dim(" worker: not configured (local Docker socket)"));
114989
+ await writeLine(ctx, import_picocolors13.default.dim(" worker: not configured (local Docker socket)"));
114634
114990
  return;
114635
114991
  }
114636
114992
  if (worker.health) {
114637
114993
  const ok = worker.health.ok;
114638
- const symbol3 = ok ? import_picocolors12.default.green("✓") : import_picocolors12.default.red("✗");
114994
+ const symbol3 = ok ? import_picocolors13.default.green("✓") : import_picocolors13.default.red("✗");
114639
114995
  const lastOk = worker.health.lastOkAt ?? "never";
114640
- await writeLine(ctx, ` ${symbol3} worker health: ${ok ? "ok" : `failing (${worker.health.consecutiveErrs} consecutive)`} last-ok=${import_picocolors12.default.dim(lastOk)}`);
114996
+ await writeLine(ctx, ` ${symbol3} worker health: ${ok ? "ok" : `failing (${worker.health.consecutiveErrs} consecutive)`} last-ok=${import_picocolors13.default.dim(lastOk)}`);
114641
114997
  }
114642
114998
  if (worker.circuitBreaker) {
114643
114999
  const breaker = worker.circuitBreaker;
114644
- const symbol3 = breaker.open ? import_picocolors12.default.red("✗") : import_picocolors12.default.green("✓");
115000
+ const symbol3 = breaker.open ? import_picocolors13.default.red("✗") : import_picocolors13.default.green("✓");
114645
115001
  const state = breaker.open ? "open (deploys fail fast)" : "closed";
114646
115002
  await writeLine(ctx, ` ${symbol3} circuit breaker: ${state} ${breaker.failuresInWindow}/${breaker.threshold} failures in last ${Math.round(breaker.windowMs / 1000)}s`);
114647
115003
  }
114648
115004
  if (result.health.version) {
114649
- await writeLine(ctx, import_picocolors12.default.dim(` api version: ${result.health.version}`));
115005
+ await writeLine(ctx, import_picocolors13.default.dim(` api version: ${result.health.version}`));
114650
115006
  }
114651
115007
  }
114652
115008
 
114653
115009
  // src/commands/transfers.ts
114654
- var import_picocolors13 = __toESM(require_picocolors(), 1);
115010
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
114655
115011
  function registerTransfersCommands(cli, ctx) {
114656
115012
  cli.command("transfer [emailOrTransferId]", "Transfer ownership of an app: `transfer <email>` initiates; `transfer --cancel <id>` cancels; `transfer --list` shows history").option("--app <name>", "App name (overrides percher.toml)").option("--cancel", "Cancel a pending transfer by id (positional arg is the transferId)").option("--list", "List the transfer history for this app").action(async (emailOrTransferId, options) => {
114657
115013
  if (options.list) {
114658
115014
  const rows = await transferListForApp(ctx, { app: options.app });
114659
115015
  if (rows.length === 0) {
114660
- await writeLine(ctx, import_picocolors13.default.dim("No transfers for this app."));
115016
+ await writeLine(ctx, import_picocolors14.default.dim("No transfers for this app."));
114661
115017
  return;
114662
115018
  }
114663
115019
  for (const r of rows) {
114664
- const status2 = r.status === "pending" ? import_picocolors13.default.yellow("pending") : r.status === "accepted" ? import_picocolors13.default.green("accepted") : import_picocolors13.default.dim(r.status);
115020
+ const status2 = r.status === "pending" ? import_picocolors14.default.yellow("pending") : r.status === "accepted" ? import_picocolors14.default.green("accepted") : import_picocolors14.default.dim(r.status);
114665
115021
  await writeLine(ctx, `${r.id} ${status2.padEnd(20)} → ${r.toEmail} (${r.createdAt})`);
114666
115022
  }
114667
115023
  return;
@@ -114674,7 +115030,7 @@ function registerTransfersCommands(cli, ctx) {
114674
115030
  app: options.app,
114675
115031
  transferId: emailOrTransferId
114676
115032
  });
114677
- await writeLine(ctx, `${import_picocolors13.default.green("✓")} Cancelled transfer ${emailOrTransferId}`);
115033
+ await writeLine(ctx, `${import_picocolors14.default.green("✓")} Cancelled transfer ${emailOrTransferId}`);
114678
115034
  return;
114679
115035
  }
114680
115036
  if (!emailOrTransferId) {
@@ -114684,17 +115040,17 @@ function registerTransfersCommands(cli, ctx) {
114684
115040
  app: options.app,
114685
115041
  email: emailOrTransferId
114686
115042
  });
114687
- await writeLine(ctx, `${import_picocolors13.default.green("✓")} Transfer initiated to ${initiated.toEmail} (id ${initiated.id})`);
114688
- await writeLine(ctx, import_picocolors13.default.dim(`Expires ${new Date(initiated.expiresAt).toLocaleDateString()}. The recipient must accept via 'percher transfers accept ${initiated.id}'.`));
115043
+ await writeLine(ctx, `${import_picocolors14.default.green("✓")} Transfer initiated to ${initiated.toEmail} (id ${initiated.id})`);
115044
+ await writeLine(ctx, import_picocolors14.default.dim(`Expires ${new Date(initiated.expiresAt).toLocaleDateString()}. The recipient must accept via 'percher transfers accept ${initiated.id}'.`));
114689
115045
  if (initiated.notes) {
114690
- await writeLine(ctx, import_picocolors13.default.yellow(` ⚠ ${initiated.notes}`));
115046
+ await writeLine(ctx, import_picocolors14.default.yellow(` ⚠ ${initiated.notes}`));
114691
115047
  }
114692
115048
  });
114693
115049
  cli.command("transfers [action] [transferId]", "Incoming ownership transfers: list (default) | accept <transferId> | decline <transferId>").action(async (action, transferId) => {
114694
115050
  if (action === undefined || action === "list") {
114695
115051
  const rows = await transfersIncoming(ctx, {});
114696
115052
  if (rows.length === 0) {
114697
- await writeLine(ctx, import_picocolors13.default.dim("No pending incoming transfers."));
115053
+ await writeLine(ctx, import_picocolors14.default.dim("No pending incoming transfers."));
114698
115054
  return;
114699
115055
  }
114700
115056
  for (const r of rows) {
@@ -114707,7 +115063,7 @@ function registerTransfersCommands(cli, ctx) {
114707
115063
  throw new Error("Usage: percher transfers accept <transferId>");
114708
115064
  }
114709
115065
  const accepted = await transfersAccept(ctx, { transferId });
114710
- await writeLine(ctx, `${import_picocolors13.default.green("✓")} You now own ${import_picocolors13.default.cyan(accepted.appName)}. Run 'percher open --app ${accepted.appName}' to see it.`);
115066
+ await writeLine(ctx, `${import_picocolors14.default.green("✓")} You now own ${import_picocolors14.default.cyan(accepted.appName)}. Run 'percher open --app ${accepted.appName}' to see it.`);
114711
115067
  return;
114712
115068
  }
114713
115069
  if (action === "decline") {
@@ -114715,7 +115071,7 @@ function registerTransfersCommands(cli, ctx) {
114715
115071
  throw new Error("Usage: percher transfers decline <transferId>");
114716
115072
  }
114717
115073
  await transfersDecline(ctx, { transferId });
114718
- await writeLine(ctx, `${import_picocolors13.default.green("✓")} Declined transfer ${transferId}.`);
115074
+ await writeLine(ctx, `${import_picocolors14.default.green("✓")} Declined transfer ${transferId}.`);
114719
115075
  return;
114720
115076
  }
114721
115077
  throw new Error(`Unknown transfers action "${action}". Use list | accept <id> | decline <id>.`);
@@ -114784,40 +115140,52 @@ async function createCliContext() {
114784
115140
  }
114785
115141
 
114786
115142
  // src/output.ts
114787
- var import_picocolors14 = __toESM(require_picocolors(), 1);
115143
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
114788
115144
  async function renderError(stderr, error48) {
114789
115145
  const writer = stderr.getWriter();
114790
115146
  const encoder = new TextEncoder;
114791
115147
  const lines = [];
114792
115148
  if (error48 instanceof PercherTomlError) {
114793
- lines.push(`${import_picocolors14.default.red(`error (${error48.code}):`)} ${error48.message}`);
115149
+ lines.push(`${import_picocolors15.default.red(`error (${error48.code}):`)} ${error48.message}`);
114794
115150
  if (error48.tomlLine !== undefined) {
114795
- lines.push(import_picocolors14.default.dim(` at percher.toml line ${error48.tomlLine}`));
115151
+ lines.push(import_picocolors15.default.dim(` at percher.toml line ${error48.tomlLine}`));
114796
115152
  }
114797
115153
  for (const issue2 of error48.issues ?? []) {
114798
- lines.push(`${import_picocolors14.default.dim(" · ") + import_picocolors14.default.yellow(issue2.path.join("."))}: ${issue2.message}`);
115154
+ lines.push(`${import_picocolors15.default.dim(" · ") + import_picocolors15.default.yellow(issue2.path.join("."))}: ${issue2.message}`);
115155
+ }
115156
+ } else if (error48 instanceof SunsetError) {
115157
+ if (error48.gateCode === "platform_delete_gated") {
115158
+ lines.push(`${import_picocolors15.default.yellow("Percher is shutting down — deletes are paused.")}`);
115159
+ lines.push(error48.message);
115160
+ lines.push(import_picocolors15.default.dim(` To delete and permanently forfeit your export window, confirm with your password in the dashboard: ${error48.dashboardUrl}`));
115161
+ lines.push(import_picocolors15.default.dim(" The CLI cannot perform this delete during sunset — it must go through the dashboard."));
115162
+ } else {
115163
+ lines.push(`${import_picocolors15.default.yellow("Percher is in read-only mode ahead of shutdown.")}`);
115164
+ lines.push(error48.message);
115165
+ lines.push(import_picocolors15.default.dim(` Your apps keep running. Export them any time from the dashboard: ${error48.dashboardUrl}`));
114799
115166
  }
115167
+ lines.push(import_picocolors15.default.dim(` Final data deletion: ${error48.finalDeletionDate}`));
114800
115168
  } else if (error48 instanceof PercherApiError) {
114801
115169
  if (error48.status === 502 || error48.status === 503) {
114802
- lines.push(`${import_picocolors14.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error48.status}). Try again in a few seconds.`);
115170
+ lines.push(`${import_picocolors15.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error48.status}). Try again in a few seconds.`);
114803
115171
  } else {
114804
- lines.push(`${import_picocolors14.default.red("error:")} ${error48.message}`);
114805
- lines.push(import_picocolors14.default.dim(` ${error48.code} (HTTP ${error48.status})`));
115172
+ lines.push(`${import_picocolors15.default.red("error:")} ${error48.message}`);
115173
+ lines.push(import_picocolors15.default.dim(` ${error48.code} (HTTP ${error48.status})`));
114806
115174
  }
114807
115175
  if (error48.requestId)
114808
- lines.push(import_picocolors14.default.dim(` request: ${error48.requestId}`));
115176
+ lines.push(import_picocolors15.default.dim(` request: ${error48.requestId}`));
114809
115177
  } else if (error48 instanceof PercherCoreError) {
114810
- lines.push(`${import_picocolors14.default.red(`error (${error48.code}):`)} ${error48.message}`);
115178
+ lines.push(`${import_picocolors15.default.red(`error (${error48.code}):`)} ${error48.message}`);
114811
115179
  if (error48.hint)
114812
- lines.push(import_picocolors14.default.dim(` ${error48.hint}`));
115180
+ lines.push(import_picocolors15.default.dim(` ${error48.hint}`));
114813
115181
  } else if (error48 instanceof Error) {
114814
- lines.push(`${import_picocolors14.default.red("unexpected error:")} ${error48.message}`);
115182
+ lines.push(`${import_picocolors15.default.red("unexpected error:")} ${error48.message}`);
114815
115183
  if (error48.stack)
114816
- lines.push(import_picocolors14.default.dim(error48.stack.split(`
115184
+ lines.push(import_picocolors15.default.dim(error48.stack.split(`
114817
115185
  `).slice(1, 4).join(`
114818
115186
  `)));
114819
115187
  } else {
114820
- lines.push(`${import_picocolors14.default.red("unexpected error:")} ${String(error48)}`);
115188
+ lines.push(`${import_picocolors15.default.red("unexpected error:")} ${String(error48)}`);
114821
115189
  }
114822
115190
  try {
114823
115191
  await writer.write(encoder.encode(`${lines.join(`
@@ -114830,7 +115198,7 @@ async function renderError(stderr, error48) {
114830
115198
  // package.json
114831
115199
  var package_default = {
114832
115200
  name: "percher",
114833
- version: "0.4.1",
115201
+ version: "0.4.2",
114834
115202
  description: "CLI for the Percher hosting platform — publish apps with one command",
114835
115203
  type: "module",
114836
115204
  bin: {
@@ -114927,7 +115295,7 @@ async function showTelemetryNotice() {
114927
115295
  }
114928
115296
 
114929
115297
  // src/version-check.ts
114930
- var import_picocolors15 = __toESM(require_picocolors(), 1);
115298
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
114931
115299
  async function checkForUpdates() {
114932
115300
  try {
114933
115301
  const res = await fetch("https://registry.npmjs.org/percher/latest", {
@@ -114938,9 +115306,9 @@ async function checkForUpdates() {
114938
115306
  const data2 = await res.json();
114939
115307
  const latest = data2.version;
114940
115308
  if (latest && latest !== VERSION) {
114941
- console.error(import_picocolors15.default.yellow(`
115309
+ console.error(import_picocolors16.default.yellow(`
114942
115310
  Update available: ${VERSION} → ${latest}`));
114943
- console.error(import_picocolors15.default.dim(`Run: bunx percher@latest
115311
+ console.error(import_picocolors16.default.dim(`Run: bunx percher@latest
114944
115312
  `));
114945
115313
  }
114946
115314
  } catch {}
@@ -114953,6 +115321,7 @@ async function run(argv, ctxOverride) {
114953
115321
  registerAppsCommands(cli, ctx);
114954
115322
  registerDeploysCommands(cli, ctx, argv);
114955
115323
  registerEnvCommands(cli, ctx);
115324
+ registerExportCommands(cli, ctx);
114956
115325
  registerDataCommands(cli, ctx, argv);
114957
115326
  registerAuthCommands(cli, ctx);
114958
115327
  registerAccountCommands(cli, ctx);