percher 0.5.20 → 0.5.21

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.
@@ -2511,7 +2511,9 @@ class AppsResource {
2511
2511
  }
2512
2512
  async deploy(id, input) {
2513
2513
  const params = new URLSearchParams;
2514
- if (input.type === "preview")
2514
+ if (input.check)
2515
+ params.set("check", "1");
2516
+ else if (input.type === "preview")
2515
2517
  params.set("type", "preview");
2516
2518
  if (input.note)
2517
2519
  params.set("note", input.note);
@@ -2536,7 +2538,9 @@ class AppsResource {
2536
2538
  params.set("runtime", input.runtime);
2537
2539
  if (input.framework)
2538
2540
  params.set("framework", input.framework);
2539
- if (input.type === "preview")
2541
+ if (input.check)
2542
+ params.set("check", "1");
2543
+ else if (input.type === "preview")
2540
2544
  params.set("type", "preview");
2541
2545
  if (input.note)
2542
2546
  params.set("note", input.note);
@@ -2550,8 +2554,8 @@ class AppsResource {
2550
2554
  promotePreview(appRef, deployId) {
2551
2555
  return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/promote`, {});
2552
2556
  }
2553
- rerunDeploy(appRef, deployId) {
2554
- return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/rerun`, {});
2557
+ rerunDeploy(appRef, deployId, opts) {
2558
+ return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/rerun`, opts?.noCache ? { noCache: true } : {});
2555
2559
  }
2556
2560
  deployLatestCommit(appRef, input) {
2557
2561
  return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/from-latest-commit`, input ?? {});
@@ -2681,6 +2685,15 @@ class AppsResource {
2681
2685
  commitSha
2682
2686
  });
2683
2687
  }
2688
+ async lastGoodVersion(appRef) {
2689
+ try {
2690
+ return await this.http.get(`/apps/${encodeURIComponent(appRef)}/rollback/last-good`);
2691
+ } catch (err) {
2692
+ if (err instanceof PercherApiError && err.status === 404)
2693
+ return null;
2694
+ throw err;
2695
+ }
2696
+ }
2684
2697
  getChanges(appRef, opts = {}) {
2685
2698
  const params = new URLSearchParams;
2686
2699
  if (opts.base)
@@ -2893,6 +2906,10 @@ class DataResource {
2893
2906
  }
2894
2907
 
2895
2908
  // ../client/dist/resources/env.js
2909
+ function scopeQuery(opts) {
2910
+ return opts?.scope && opts.scope !== "all" ? `?scope=${opts.scope}` : "";
2911
+ }
2912
+
2896
2913
  class EnvResource {
2897
2914
  http;
2898
2915
  constructor(http) {
@@ -2901,11 +2918,15 @@ class EnvResource {
2901
2918
  list(appId) {
2902
2919
  return this.http.get(`/apps/${encodeURIComponent(appId)}/env`);
2903
2920
  }
2904
- set(appId, values) {
2905
- return this.http.put(`/apps/${encodeURIComponent(appId)}/env`, values);
2921
+ async listDetailed(appId) {
2922
+ const res = await this.http.get(`/apps/${encodeURIComponent(appId)}/env?detail=1`);
2923
+ return res.detailed;
2906
2924
  }
2907
- unset(appId, key) {
2908
- return this.http.delete(`/apps/${encodeURIComponent(appId)}/env/${encodeURIComponent(key)}`);
2925
+ set(appId, values, opts) {
2926
+ return this.http.put(`/apps/${encodeURIComponent(appId)}/env${scopeQuery(opts)}`, values);
2927
+ }
2928
+ unset(appId, key, opts) {
2929
+ return this.http.delete(`/apps/${encodeURIComponent(appId)}/env/${encodeURIComponent(key)}${scopeQuery(opts)}`);
2909
2930
  }
2910
2931
  }
2911
2932
 
@@ -2949,6 +2970,24 @@ class ExportResource {
2949
2970
  }
2950
2971
  }
2951
2972
 
2973
+ // ../client/dist/resources/hooks.js
2974
+ class HooksResource {
2975
+ http;
2976
+ constructor(http) {
2977
+ this.http = http;
2978
+ }
2979
+ create(appId, name) {
2980
+ return this.http.post(`/apps/${encodeURIComponent(appId)}/hooks`, { name });
2981
+ }
2982
+ async list(appId) {
2983
+ const res = await this.http.get(`/apps/${encodeURIComponent(appId)}/hooks`);
2984
+ return res.hooks;
2985
+ }
2986
+ revoke(appId, hookId) {
2987
+ return this.http.delete(`/apps/${encodeURIComponent(appId)}/hooks/${encodeURIComponent(hookId)}`);
2988
+ }
2989
+ }
2990
+
2952
2991
  // ../client/dist/sse.js
2953
2992
  async function* parseSseStream(stream, signal) {
2954
2993
  const reader = stream.getReader();
@@ -3213,6 +3252,7 @@ class PercherClient {
3213
3252
  env;
3214
3253
  envScan;
3215
3254
  export;
3255
+ hooks;
3216
3256
  logs;
3217
3257
  auth;
3218
3258
  billing;
@@ -3243,6 +3283,7 @@ class PercherClient {
3243
3283
  this.env = new EnvResource(http);
3244
3284
  this.envScan = new EnvScanResource(http);
3245
3285
  this.export = new ExportResource(http);
3286
+ this.hooks = new HooksResource(http);
3246
3287
  this.logs = new LogsResource(http);
3247
3288
  this.auth = new AuthResource(http);
3248
3289
  this.billing = new BillingResource(http);
@@ -105906,9 +105947,39 @@ var webSchema = exports_external.object({
105906
105947
  port: exports_external.number().int().min(1024).max(65535),
105907
105948
  health: exports_external.string().optional(),
105908
105949
  command: exports_external.string().min(1).max(1000).optional(),
105950
+ visibility: exports_external.enum(["public", "password", "private"]).optional(),
105909
105951
  password: exports_external.boolean().optional(),
105910
- spa: exports_external.boolean().optional()
105911
- }).strict();
105952
+ spa: exports_external.boolean().optional(),
105953
+ rate_limit: exports_external.object({
105954
+ rps: exports_external.number().int().min(1).max(100),
105955
+ burst: exports_external.number().int().min(1).max(1000)
105956
+ }).strict().optional()
105957
+ }).strict().superRefine((web, ctx) => {
105958
+ if (web.visibility === "public" && web.password === true) {
105959
+ ctx.addIssue({
105960
+ code: exports_external.ZodIssueCode.custom,
105961
+ path: ["visibility"],
105962
+ message: `password = true implies visibility "password" — don't set visibility = "public" alongside it`
105963
+ });
105964
+ }
105965
+ if (web.rate_limit) {
105966
+ const { rps, burst } = web.rate_limit;
105967
+ if (burst < rps) {
105968
+ ctx.addIssue({
105969
+ code: exports_external.ZodIssueCode.custom,
105970
+ path: ["rate_limit", "burst"],
105971
+ message: `rate_limit.burst (${burst}) must be >= rate_limit.rps (${rps}) — a burst below the sustained rate is nonsense`
105972
+ });
105973
+ }
105974
+ if (burst > rps * 10) {
105975
+ ctx.addIssue({
105976
+ code: exports_external.ZodIssueCode.custom,
105977
+ path: ["rate_limit", "burst"],
105978
+ message: `rate_limit.burst (${burst}) must be <= 10× rate_limit.rps (${rps * 10})`
105979
+ });
105980
+ }
105981
+ }
105982
+ });
105912
105983
  var convexConfigSchema = exports_external.object({
105913
105984
  deployment_url: exports_external.string().url()
105914
105985
  }).strict();
@@ -108168,6 +108239,11 @@ async function buildFailureError(ctx, app, deployment, signal) {
108168
108239
  async function classifyDeploymentFailure(opts) {
108169
108240
  const { ctx, app, deployment, publishInput } = opts;
108170
108241
  const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
108242
+ const isLiveDeploy = deployment.type !== "preview";
108243
+ if (isLiveDeploy && (app.status === "live" || app.status === "sleeping")) {
108244
+ error48.suggestion = `${error48.suggestion}
108245
+ To return to the last version that ran cleanly: \`bunx percher rollback --last-good\`.`;
108246
+ }
108171
108247
  const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
108172
108248
  missingEnvVars: error48.missingEnvVars,
108173
108249
  deployId: deployment.id,
@@ -108275,7 +108351,7 @@ async function deploysInspect(ctx, input) {
108275
108351
  throw err;
108276
108352
  }
108277
108353
  }
108278
- return { deploy, buildLog };
108354
+ return { deploy, buildLog, events: deploy.events ?? [] };
108279
108355
  }
108280
108356
  // ../core/dist/commands/dev.js
108281
108357
  import { existsSync as existsSync8 } from "node:fs";
@@ -108776,6 +108852,17 @@ async function waitForDeploy(ctx, input, opts) {
108776
108852
  summary
108777
108853
  };
108778
108854
  }
108855
+ if (deployment.isCheck && deployment.checkPassed) {
108856
+ return {
108857
+ status: "check_passed",
108858
+ app: { id: app.id, name: app.name, url: app.url },
108859
+ deployment,
108860
+ elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
108861
+ totalElapsedSeconds: secondsSinceCreated(deployment),
108862
+ recovery: RECOVERY_NONE,
108863
+ summary: `${app.name} check ${deployment.id} passed — would build and boot. The live app was not changed.`
108864
+ };
108865
+ }
108779
108866
  if (deployment.status === "replaced") {
108780
108867
  return await buildReplacedResult({ ctx, app, deployment, startedAt, signal: opts?.signal });
108781
108868
  }
@@ -109676,13 +109763,16 @@ async function domainRemove(ctx, input) {
109676
109763
  await ctx.client.apps.removeDomain(appRef, input.domain);
109677
109764
  }
109678
109765
  // ../core/dist/commands/env.js
109766
+ var envScopeEnum = exports_external.enum(["all", "live", "preview"]);
109679
109767
  var envSetInputSchema = exports_external.object({
109680
109768
  app: exports_external.string().optional(),
109681
- values: exports_external.record(exports_external.string(), exports_external.string())
109769
+ values: exports_external.record(exports_external.string(), exports_external.string()),
109770
+ scope: envScopeEnum.optional().describe("Env scope: 'all' (default) applies to the live app and every preview; 'preview' applies only to preview deploys and overrides 'all' there; 'live' applies only to the live app and overrides 'all' there.")
109682
109771
  });
109683
109772
  var envUnsetInputSchema = exports_external.object({
109684
109773
  app: exports_external.string().optional(),
109685
- key: exports_external.string().min(1)
109774
+ key: exports_external.string().min(1),
109775
+ scope: envScopeEnum.optional().describe("Which scoped value to remove: 'all' (default), 'live', or 'preview'. Use 'preview' to drop a preview-only override without touching the value live deploys see.")
109686
109776
  });
109687
109777
  var envListInputSchema = exports_external.object({
109688
109778
  app: exports_external.string().optional()
@@ -109705,21 +109795,30 @@ function mask(value) {
109705
109795
  async function envSet(ctx, input) {
109706
109796
  const name = resolveAppName2(ctx, input.app);
109707
109797
  const app = await ctx.client.apps.get(name);
109708
- await ctx.client.env.set(app.id, input.values);
109798
+ await ctx.client.env.set(app.id, input.values, { scope: input.scope });
109709
109799
  }
109710
109800
  async function envUnset(ctx, input) {
109711
109801
  const name = resolveAppName2(ctx, input.app);
109712
109802
  const app = await ctx.client.apps.get(name);
109713
- await ctx.client.env.unset(app.id, input.key);
109803
+ await ctx.client.env.unset(app.id, input.key, { scope: input.scope });
109714
109804
  }
109715
109805
  async function envList(ctx, input = {}) {
109716
109806
  const name = resolveAppName2(ctx, input.app);
109717
109807
  const app = await ctx.client.apps.get(name);
109718
- const raw = await ctx.client.env.list(app.id);
109719
- const masked = {};
109720
- for (const [k, v] of Object.entries(raw))
109721
- masked[k] = mask(v);
109722
- return masked;
109808
+ const [serverValues, rows] = await Promise.all([
109809
+ ctx.client.env.list(app.id),
109810
+ ctx.client.env.listDetailed(app.id)
109811
+ ]);
109812
+ const detailed = rows.map((r) => ({
109813
+ key: r.key,
109814
+ scope: r.scope,
109815
+ maskedValue: mask(r.maskedValue)
109816
+ }));
109817
+ const values = {};
109818
+ for (const [key, masked] of Object.entries(serverValues)) {
109819
+ values[key] = mask(masked);
109820
+ }
109821
+ return { values, detailed };
109723
109822
  }
109724
109823
  // ../core/dist/commands/export.js
109725
109824
  import { randomUUID } from "node:crypto";
@@ -110104,6 +110203,46 @@ async function githubSetupWebhook(ctx, input) {
110104
110203
  `)
110105
110204
  };
110106
110205
  }
110206
+ // ../core/dist/commands/hooks.js
110207
+ var hookCreateInputSchema = exports_external.object({
110208
+ app: exports_external.string().optional(),
110209
+ name: exports_external.string().trim().min(1).max(64).describe("Label for the hook, 1-64 chars — name it after the caller, e.g. 'sanity-publish' or 'nightly-cron' (shown in the list).")
110210
+ });
110211
+ var hookListInputSchema = exports_external.object({
110212
+ app: exports_external.string().optional()
110213
+ });
110214
+ var hookRevokeInputSchema = exports_external.object({
110215
+ app: exports_external.string().optional(),
110216
+ id: exports_external.string().min(1).describe("Hook id from `percher hook list`.")
110217
+ });
110218
+ function resolveAppName4(ctx, provided) {
110219
+ const name = provided ?? readPercherTomlAppName(ctx.cwd);
110220
+ if (!name) {
110221
+ throw new PercherCoreError("No app specified and no percher.toml found", {
110222
+ code: "hooks.no-app",
110223
+ hint: "Pass --app <name> or run from a directory with percher.toml."
110224
+ });
110225
+ }
110226
+ return name;
110227
+ }
110228
+ async function hookCreate(ctx, input) {
110229
+ const name = resolveAppName4(ctx, input.app);
110230
+ const app = await ctx.client.apps.get(name);
110231
+ ctx.status("Creating deploy hook…");
110232
+ return ctx.client.hooks.create(app.id, input.name);
110233
+ }
110234
+ async function hookList(ctx, input = {}) {
110235
+ const name = resolveAppName4(ctx, input.app);
110236
+ const app = await ctx.client.apps.get(name);
110237
+ ctx.status("Loading deploy hooks…");
110238
+ return ctx.client.hooks.list(app.id);
110239
+ }
110240
+ async function hookRevoke(ctx, input) {
110241
+ const name = resolveAppName4(ctx, input.app);
110242
+ const app = await ctx.client.apps.get(name);
110243
+ ctx.status("Revoking deploy hook…");
110244
+ await ctx.client.hooks.revoke(app.id, input.id);
110245
+ }
110107
110246
  // ../core/dist/commands/import-project.js
110108
110247
  import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
110109
110248
  import { join as join15 } from "node:path";
@@ -115168,6 +115307,11 @@ async function previewBranchShow(ctx, input = {}) {
115168
115307
  status: app.previewStatus ?? "disabled"
115169
115308
  };
115170
115309
  }
115310
+ async function previewList(ctx, input = {}) {
115311
+ const appRef = resolveApp3(ctx, input.app);
115312
+ const app = await ctx.client.apps.get(appRef);
115313
+ return { app: app.name, previews: app.previews ?? [] };
115314
+ }
115171
115315
  // ../core/dist/commands/publish.js
115172
115316
  init_dist();
115173
115317
  import { existsSync as existsSync15 } from "node:fs";
@@ -116090,6 +116234,7 @@ var publishInputSchema = exports_external.object({
116090
116234
  preview: exports_external.boolean().optional().describe("Deploy as preview (does not replace the live version)"),
116091
116235
  message: exports_external.string().optional().describe("Deploy note (visible in deploy history and version timeline)"),
116092
116236
  dryRun: exports_external.boolean().optional().describe("Show what would be deployed (file count, bytes, top files) without uploading or building. Useful for verifying the bundle before a real deploy."),
116237
+ check: exports_external.boolean().optional().describe("Validation deploy: build → start → health, then tear the container down (no swap, no route). Proves the app builds and boots without touching the live version. Consumes the PREVIEW daily quota; the app is NOT changed. Beyond a manifest-only dry run — it actually builds and runs. On success: status='check_passed'."),
116093
116238
  noCache: exports_external.boolean().optional().describe("Skip the image-cache lookup and force a fresh build. Use this when you suspect the cached image doesn't reflect your latest source — the new build's resulting image still gets cached, so future deploys benefit."),
116094
116239
  waitForLive: exports_external.boolean().optional().describe("If true (default), publish blocks until the deploy is live or failed. If false, publish returns as soon as the deploy is queued and the agent can resume with percher_wait_for_deploy. Recommended for AI agents — gives back control quickly with a deployId so they can decide between waiting, asking the user, or working on something else."),
116095
116240
  anonymous: exports_external.boolean().optional().describe("Publish WITHOUT an account. Creates a throwaway anonymous app that expires in 72h unless claimed (percher_claim_app). The app runs with no outbound network, no database, and no env vars until claimed. Only honored when the caller has no token; ignored once signed in. Server-gated — fails with ANON_DEPLOYS_DISABLED if the instance hasn't enabled it.")
@@ -116422,7 +116567,7 @@ async function executeDeploy(opts) {
116422
116567
  const tarballBytes = new Uint8Array(await new Response(tarball.stream).arrayBuffer());
116423
116568
  let cacheReusedDeployment = null;
116424
116569
  const contentHash = tarball.contentHash;
116425
- if (!input.noCache && !input.preview) {
116570
+ if (!input.noCache && !input.preview && !input.check) {
116426
116571
  try {
116427
116572
  const probe = await ctx.client.platform.cacheProbe({
116428
116573
  appId: app.id,
@@ -116445,6 +116590,7 @@ async function executeDeploy(opts) {
116445
116590
  call: (attempt) => ctx.client.apps.deploy(app.id, {
116446
116591
  tarball: tarballBytes,
116447
116592
  type: input.preview ? "preview" : undefined,
116593
+ check: input.check,
116448
116594
  note: input.message,
116449
116595
  noCache: input.noCache,
116450
116596
  idempotencyKey,
@@ -116590,6 +116736,31 @@ async function finalizeDeploy(opts) {
116590
116736
  };
116591
116737
  }
116592
116738
  const buildMs = Date.now() - buildStart;
116739
+ if (deployment.isCheck && deployment.checkPassed) {
116740
+ const totalSeconds2 = (Date.now() - t0) / 1000;
116741
+ const summary = `${app.name} check passed (${Math.round(totalSeconds2)}s) — would build and boot. The live app was not changed.`;
116742
+ ctx.status(summary);
116743
+ return {
116744
+ status: "check_passed",
116745
+ app,
116746
+ deployment,
116747
+ timing: {
116748
+ totalSeconds: Math.round(totalSeconds2),
116749
+ packageMs,
116750
+ uploadMs,
116751
+ buildMs
116752
+ },
116753
+ fileCount: tarball.fileCount,
116754
+ bytes: tarball.bytes,
116755
+ firstDeploy: firstDeploy || undefined,
116756
+ missingBuildEnvKeys: missingBuildEnvKeys.length > 0 ? missingBuildEnvKeys : undefined,
116757
+ traceId: deployment.id,
116758
+ recovery: RECOVERY_NONE,
116759
+ summary,
116760
+ configPath: tomlPathFor2(ctx.cwd),
116761
+ bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
116762
+ };
116763
+ }
116593
116764
  if (deployment.status === "replaced") {
116594
116765
  return await buildReplacedResult2({
116595
116766
  ctx,
@@ -116624,6 +116795,16 @@ async function finalizeDeploy(opts) {
116624
116795
  }
116625
116796
  const cacheHit = deployment.cacheHit;
116626
116797
  const cacheStatus = deployment.cacheStatus;
116798
+ const autoDeploys = resolveAutoDeploys(app);
116799
+ const liveSummary = buildLiveSummary({
116800
+ appName: app.name,
116801
+ url: url2 ?? app.url,
116802
+ totalSeconds: Math.round(totalSeconds),
116803
+ framework: config2.app.framework,
116804
+ preview: input.preview === true,
116805
+ replacedPreview,
116806
+ cacheHit: cacheHit === true
116807
+ });
116627
116808
  return {
116628
116809
  status: "live",
116629
116810
  url: url2,
@@ -116646,19 +116827,30 @@ async function finalizeDeploy(opts) {
116646
116827
  missingBuildEnvKeys: missingBuildEnvKeys.length > 0 ? missingBuildEnvKeys : undefined,
116647
116828
  traceId: deployment.id,
116648
116829
  recovery: RECOVERY_NONE,
116649
- summary: buildLiveSummary({
116650
- appName: app.name,
116651
- url: url2 ?? app.url,
116652
- totalSeconds: Math.round(totalSeconds),
116653
- framework: config2.app.framework,
116654
- preview: input.preview === true,
116655
- replacedPreview,
116656
- cacheHit: cacheHit === true
116657
- }),
116830
+ autoDeploys,
116831
+ summary: autoDeploys ? `${liveSummary}
116832
+ This app auto-deploys from ${autoDeploys.repo}@${autoDeploys.branch} — pushing there is the canonical path.` : liveSummary,
116658
116833
  configPath: tomlPathFor2(ctx.cwd),
116659
116834
  bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
116660
116835
  };
116661
116836
  }
116837
+ function resolveAutoDeploys(app) {
116838
+ if (app.githubRepoUrl) {
116839
+ return {
116840
+ repo: app.githubRepoUrl,
116841
+ branch: app.githubBranch ?? "main",
116842
+ provider: "github"
116843
+ };
116844
+ }
116845
+ if (app.forgejoRepoUrl) {
116846
+ return {
116847
+ repo: app.forgejoRepoUrl,
116848
+ branch: app.forgejoBranch ?? "main",
116849
+ provider: "forgejo"
116850
+ };
116851
+ }
116852
+ return;
116853
+ }
116662
116854
  async function handleUnauthorized(ctx, config2, tarball, input) {
116663
116855
  if (input.anonymous) {
116664
116856
  return handleAnonymousPublish(ctx, config2, input, tarball);
@@ -116901,6 +117093,7 @@ var publishInlineInputSchema = exports_external.object({
116901
117093
  encoding: exports_external.enum(["utf8", "base64"]).optional().describe("Content encoding. Default 'utf8' (text). Use 'base64' for binary assets (images, fonts).")
116902
117094
  })).min(1).describe("Inline source files. Include a percher.toml ([app].name + runtime), or omit it and Percher auto-detects the runtime from the files (then `name` is required)."),
116903
117095
  preview: exports_external.boolean().optional().describe("Deploy as a preview (does not replace the live version)."),
117096
+ check: exports_external.boolean().optional().describe("Validation deploy: build → start → health, then tear down (no swap, no route). Proves the app builds and boots without changing the live version. Consumes the preview quota. On success: status='check_passed'."),
116904
117097
  message: exports_external.string().optional().describe("Deploy note (visible in deploy history)."),
116905
117098
  noCache: exports_external.boolean().optional().describe("Skip the image cache and force a fresh build."),
116906
117099
  waitForLive: exports_external.boolean().optional().describe("If true (default), block until the deploy is live or failed. If false, return as soon as it is queued so the agent can resume with percher_wait_for_deploy.")
@@ -117222,7 +117415,8 @@ ${deployment.errorMessage}` : "";
117222
117415
  init_dist();
117223
117416
  var redeployInputSchema = exports_external.object({
117224
117417
  app: exports_external.string().optional().describe("App name. Defaults to the [app].name in the current directory's percher.toml."),
117225
- deployId: exports_external.string().optional().describe("Specific deploy id (e.g. dep_abc123). Defaults to the latest live deploy for the app.")
117418
+ deployId: exports_external.string().optional().describe("Specific deploy id (e.g. dep_abc123). Defaults to the latest live deploy for the app."),
117419
+ noCache: exports_external.boolean().optional().describe("Force a fresh build — skip the image cache. The stale-image escape hatch when a re-run keeps serving old bytes.")
117226
117420
  });
117227
117421
  async function redeploy(ctx, input = {}) {
117228
117422
  const t0 = Date.now();
@@ -117276,7 +117470,7 @@ async function redeploy(ctx, input = {}) {
117276
117470
  ctx.status(`Re-running ${sourceDeployId}...`);
117277
117471
  let initial;
117278
117472
  try {
117279
- initial = await ctx.client.apps.rerunDeploy(app.id, sourceDeployId);
117473
+ initial = await ctx.client.apps.rerunDeploy(app.id, sourceDeployId, input.noCache ? { noCache: true } : undefined);
117280
117474
  } catch (err) {
117281
117475
  if (err instanceof PercherApiError) {
117282
117476
  switch (err.code) {
@@ -117875,8 +118069,90 @@ async function resume(ctx, input = {}) {
117875
118069
  // ../core/dist/commands/rollback.js
117876
118070
  var rollbackInputSchema = exports_external.object({
117877
118071
  app: exports_external.string().optional(),
117878
- commitSha: exports_external.string().min(1)
118072
+ commitSha: exports_external.string().min(1).optional().describe("Version SHA to roll back to (from percher_versions / `percher versions`). Omit to pick from the most recent versions, or set toLastGood."),
118073
+ toLastGood: exports_external.boolean().optional().describe("Resolves server-side to the most recent version that went live and didn't crash-loop.")
117879
118074
  });
118075
+ var BARE_PICK_LIMIT = 5;
118076
+ function versionAge(iso, now = Date.now()) {
118077
+ const then = new Date(iso).getTime();
118078
+ if (Number.isNaN(then))
118079
+ return iso;
118080
+ const s = Math.max(0, Math.floor((now - then) / 1000));
118081
+ if (s < 60)
118082
+ return "just now";
118083
+ const m = Math.floor(s / 60);
118084
+ if (m < 60)
118085
+ return `${m}m ago`;
118086
+ const h = Math.floor(m / 60);
118087
+ if (h < 24)
118088
+ return `${h}h ago`;
118089
+ const d = Math.floor(h / 24);
118090
+ if (d < 30)
118091
+ return `${d}d ago`;
118092
+ return `${Math.floor(d / 30)}mo ago`;
118093
+ }
118094
+ async function writeLine(ctx, line) {
118095
+ const writer = ctx.stdout.getWriter();
118096
+ try {
118097
+ await writer.write(new TextEncoder().encode(`${line}
118098
+ `));
118099
+ } finally {
118100
+ writer.releaseLock();
118101
+ }
118102
+ }
118103
+ async function resolveCommitSha(ctx, name, input) {
118104
+ if (input.commitSha)
118105
+ return input.commitSha;
118106
+ if (input.toLastGood) {
118107
+ const lastGood = await ctx.client.apps.lastGoodVersion(name);
118108
+ if (!lastGood) {
118109
+ throw new PercherCoreError("No previous known-good version — deploy history has none that reached live without crash-looping.", {
118110
+ code: "rollback.no-last-good",
118111
+ hint: "Run `bunx percher versions` and pick a version explicitly: `bunx percher rollback <sha>`."
118112
+ });
118113
+ }
118114
+ return lastGood.commitSha;
118115
+ }
118116
+ const versions2 = await ctx.client.apps.listVersions(name);
118117
+ if (versions2.length === 0) {
118118
+ throw new PercherCoreError(`No versions to roll back to for "${name}"`, {
118119
+ code: "rollback.no-versions",
118120
+ hint: "Publish at least once before rolling back."
118121
+ });
118122
+ }
118123
+ const pick2 = versions2.slice(0, BARE_PICK_LIMIT);
118124
+ if (!ctx.stdinIsInteractive) {
118125
+ const appFlag = ` --app ${name}`;
118126
+ const lines = pick2.map((v) => {
118127
+ const note = v.message.split(`
118128
+ `)[0]?.trim() || "Published";
118129
+ return ` bunx percher rollback ${v.sha}${appFlag} # ${note} (${versionAge(v.timestamp)})`;
118130
+ });
118131
+ throw new PercherCoreError(`Rollback needs a version. The ${pick2.length} most recent for "${name}":
118132
+ ${lines.join(`
118133
+ `)}`, {
118134
+ code: "rollback.sha-required",
118135
+ hint: "Re-run with an explicit sha, or pass --last-good to pick the most recent known-good version."
118136
+ });
118137
+ }
118138
+ await writeLine(ctx, `Recent versions of ${name}:`);
118139
+ for (const [i, v] of pick2.entries()) {
118140
+ const note = v.message.split(`
118141
+ `)[0]?.trim() || "Published";
118142
+ const live = i === 0 ? " (live now)" : "";
118143
+ await writeLine(ctx, ` ${i + 1}. ${v.sha.slice(0, 10)} ${versionAge(v.timestamp)} ${note}${live}`);
118144
+ }
118145
+ const answer = await ctx.prompt(`Roll back to which version? [1-${pick2.length}]`, {});
118146
+ const n = Number.parseInt(answer.trim(), 10);
118147
+ const chosen = Number.isInteger(n) && n >= 1 && n <= pick2.length ? pick2[n - 1] : undefined;
118148
+ if (!chosen) {
118149
+ throw new PercherCoreError(`"${answer.trim()}" is not a number between 1 and ${pick2.length}`, {
118150
+ code: "rollback.sha-required",
118151
+ hint: "Re-run `percher rollback` and pick a listed number, or pass the sha directly."
118152
+ });
118153
+ }
118154
+ return chosen.sha;
118155
+ }
117880
118156
  async function rollback(ctx, input, internals = {}) {
117881
118157
  const name = input.app ?? readPercherTomlAppName(ctx.cwd);
117882
118158
  if (!name) {
@@ -117885,7 +118161,9 @@ async function rollback(ctx, input, internals = {}) {
117885
118161
  hint: "Pass --app <name> or run from a directory with percher.toml."
117886
118162
  });
117887
118163
  }
117888
- let deployment = await ctx.client.apps.rollback(name, input.commitSha);
118164
+ const commitSha = await resolveCommitSha(ctx, name, input);
118165
+ ctx.status(`Rolling back to ${commitSha.slice(0, 10)}...`);
118166
+ let deployment = await ctx.client.apps.rollback(name, commitSha);
117889
118167
  const pollInterval = internals.pollIntervalMs ?? 2000;
117890
118168
  const timeoutMs = internals.timeoutMs ?? TIMEOUTS.clientPublishPoll;
117891
118169
  const startedAt = Date.now();
@@ -117906,7 +118184,7 @@ async function rollback(ctx, input, internals = {}) {
117906
118184
  hint: "Run `percher logs` to see what went wrong."
117907
118185
  });
117908
118186
  }
117909
- return deployment;
118187
+ return { ...deployment, rolledBackTo: commitSha };
117910
118188
  }
117911
118189
  // ../core/dist/commands/sharing.js
117912
118190
  var ShareRoleSchema = exports_external.enum(["viewer", "admin"]);
@@ -118314,7 +118592,7 @@ class SilentExitError {
118314
118592
  this.exitCode = exitCode;
118315
118593
  }
118316
118594
  }
118317
- async function writeLine(ctx, line) {
118595
+ async function writeLine2(ctx, line) {
118318
118596
  const writer = ctx.stdout.getWriter();
118319
118597
  try {
118320
118598
  await writer.write(new TextEncoder().encode(`${line}
@@ -118379,56 +118657,115 @@ function firstNonFlagAfter(argv, from, valueFlags = new Set) {
118379
118657
  }
118380
118658
  return;
118381
118659
  }
118660
+ function foldStageTimeline(events) {
118661
+ const order = [];
118662
+ const info = new Map;
118663
+ for (const e of events) {
118664
+ if (e.subStage !== null)
118665
+ continue;
118666
+ let row = info.get(e.stage);
118667
+ if (!row) {
118668
+ row = { status: "running", durationMs: null };
118669
+ info.set(e.stage, row);
118670
+ order.push(e.stage);
118671
+ }
118672
+ const t = Date.parse(e.at);
118673
+ if (e.status === "started") {
118674
+ if (!Number.isNaN(t))
118675
+ row.startedAt = t;
118676
+ } else if (e.status === "ok" || e.status === "failed") {
118677
+ row.status = e.status;
118678
+ if (e.durationMs !== null)
118679
+ row.durationMs = e.durationMs;
118680
+ else if (!Number.isNaN(t))
118681
+ row.endedAt = t;
118682
+ }
118683
+ }
118684
+ return order.map((stage) => {
118685
+ const r = info.get(stage);
118686
+ let durationMs = r.durationMs;
118687
+ if (durationMs === null && r.startedAt !== undefined && r.endedAt !== undefined) {
118688
+ durationMs = Math.max(0, r.endedAt - r.startedAt);
118689
+ }
118690
+ return { stage, durationMs, status: r.status };
118691
+ });
118692
+ }
118693
+ function formatStageDuration(ms) {
118694
+ if (ms === null || !Number.isFinite(ms) || ms < 0)
118695
+ return "—";
118696
+ if (ms < 1000)
118697
+ return `${Math.round(ms)}ms`;
118698
+ if (ms < 60000)
118699
+ return `${(ms / 1000).toFixed(1)}s`;
118700
+ return `${(ms / 60000).toFixed(1)}m`;
118701
+ }
118702
+ async function writeStageTimeline(ctx, events) {
118703
+ if (events.length === 0)
118704
+ return;
118705
+ const rows = foldStageTimeline(events);
118706
+ if (rows.length === 0)
118707
+ return;
118708
+ const stageW = Math.max(...rows.map((r) => r.stage.length));
118709
+ const durW = Math.max(...rows.map((r) => formatStageDuration(r.durationMs).length));
118710
+ await writeLine2(ctx, "");
118711
+ await writeLine2(ctx, import_picocolors.default.bold("Stages:"));
118712
+ for (const r of rows) {
118713
+ const dur = formatStageDuration(r.durationMs).padStart(durW);
118714
+ const mark = r.status === "failed" ? import_picocolors.default.red("← failed") : r.status === "ok" ? import_picocolors.default.dim("ok") : import_picocolors.default.yellow("…");
118715
+ await writeLine2(ctx, ` ${r.stage.padEnd(stageW)} ${dur} ${mark}`);
118716
+ }
118717
+ }
118382
118718
  async function printSuperuserResetResult(ctx, result) {
118383
- await writeLine(ctx, `${import_picocolors.default.green("✓")} ${result.message}`);
118719
+ await writeLine2(ctx, `${import_picocolors.default.green("✓")} ${result.message}`);
118384
118720
  if (result.password) {
118385
- await writeLine(ctx, "");
118386
- await writeLine(ctx, ` ${import_picocolors.default.bold("Email:")} ${result.email}`);
118387
- await writeLine(ctx, ` ${import_picocolors.default.bold("Password:")} ${result.password}`);
118388
- await writeLine(ctx, "");
118389
- await writeLine(ctx, import_picocolors.default.yellow(" ⚠ This password is shown only once. Save it now if you need it."));
118721
+ await writeLine2(ctx, "");
118722
+ await writeLine2(ctx, ` ${import_picocolors.default.bold("Email:")} ${result.email}`);
118723
+ await writeLine2(ctx, ` ${import_picocolors.default.bold("Password:")} ${result.password}`);
118724
+ await writeLine2(ctx, "");
118725
+ await writeLine2(ctx, import_picocolors.default.yellow(" ⚠ This password is shown only once. Save it now if you need it."));
118390
118726
  }
118391
118727
  }
118392
118728
  async function runDeploysInspect(ctx, input) {
118393
118729
  const result = await deploysInspect(ctx, input);
118394
- const { deploy, buildLog } = result;
118395
- await writeLine(ctx, `${import_picocolors.default.bold("Deploy:")} ${deploy.id}`);
118396
- await writeLine(ctx, `${import_picocolors.default.bold("Status:")} ${formatStatus(deploy.status)}`);
118730
+ const { deploy, buildLog, events } = result;
118731
+ await writeLine2(ctx, `${import_picocolors.default.bold("Deploy:")} ${deploy.id}`);
118732
+ await writeLine2(ctx, `${import_picocolors.default.bold("Status:")} ${formatStatus(deploy.status)}`);
118397
118733
  if (deploy.type) {
118398
- await writeLine(ctx, `${import_picocolors.default.bold("Type:")} ${deploy.type}`);
118734
+ await writeLine2(ctx, `${import_picocolors.default.bold("Type:")} ${deploy.type}`);
118399
118735
  }
118400
- await writeLine(ctx, `${import_picocolors.default.bold("Created:")} ${formatDeployTimestamp(deploy.createdAt)}`);
118736
+ await writeLine2(ctx, `${import_picocolors.default.bold("Created:")} ${formatDeployTimestamp(deploy.createdAt)}`);
118401
118737
  if (typeof deploy.imageSizeBytes === "number") {
118402
- await writeLine(ctx, `${import_picocolors.default.bold("Image:")} ${formatBytes(deploy.imageSizeBytes)}`);
118738
+ await writeLine2(ctx, `${import_picocolors.default.bold("Image:")} ${formatBytes(deploy.imageSizeBytes)}`);
118403
118739
  }
118404
118740
  if (deploy.url) {
118405
- await writeLine(ctx, `${import_picocolors.default.bold("URL:")} ${deploy.url}`);
118741
+ await writeLine2(ctx, `${import_picocolors.default.bold("URL:")} ${deploy.url}`);
118406
118742
  }
118407
118743
  if (deploy.previewUrl) {
118408
- await writeLine(ctx, `${import_picocolors.default.bold("Preview:")} ${deploy.previewUrl}`);
118744
+ await writeLine2(ctx, `${import_picocolors.default.bold("Preview:")} ${deploy.previewUrl}`);
118409
118745
  }
118410
118746
  if (deploy.errorMessage) {
118411
- await writeLine(ctx, "");
118412
- await writeLine(ctx, import_picocolors.default.bold(import_picocolors.default.red("Error:")));
118747
+ await writeLine2(ctx, "");
118748
+ await writeLine2(ctx, import_picocolors.default.bold(import_picocolors.default.red("Error:")));
118413
118749
  for (const line of deploy.errorMessage.split(`
118414
118750
  `)) {
118415
- await writeLine(ctx, ` ${line}`);
118751
+ await writeLine2(ctx, ` ${line}`);
118416
118752
  }
118417
118753
  }
118418
118754
  if (buildLog) {
118419
- await writeLine(ctx, "");
118755
+ await writeLine2(ctx, "");
118420
118756
  const lines = buildLog.split(`
118421
118757
  `);
118422
118758
  const tail = lines.slice(-200);
118423
118759
  const header = lines.length > tail.length ? `${import_picocolors.default.bold("Build log")} ${import_picocolors.default.dim(`(last 200 of ${lines.length} lines)`)}` : import_picocolors.default.bold("Build log");
118424
- await writeLine(ctx, header);
118760
+ await writeLine2(ctx, header);
118425
118761
  for (const line of tail) {
118426
- await writeLine(ctx, ` ${line}`);
118762
+ await writeLine2(ctx, ` ${line}`);
118427
118763
  }
118428
118764
  } else if (deploy.status === "failed" || deploy.status === "live") {
118429
- await writeLine(ctx, "");
118430
- await writeLine(ctx, import_picocolors.default.dim("(no build log available for this deploy)"));
118765
+ await writeLine2(ctx, "");
118766
+ await writeLine2(ctx, import_picocolors.default.dim("(no build log available for this deploy)"));
118431
118767
  }
118768
+ await writeStageTimeline(ctx, events);
118432
118769
  }
118433
118770
 
118434
118771
  // src/commands/account.ts
@@ -118446,7 +118783,7 @@ function registerAccountCommands(cli, ctx) {
118446
118783
  cli.command("account [action] [...rest]", "Account management: export (GDPR data portability) | preview-delete (inspect deletion blockers) | delete (GDPR erasure)").option("--out <file>", "(export only) Output file path").option("--include-app-data", "(export only) Also download per-app PocketBase tarballs (creates a directory)").option("--confirm <email>", "(delete only) Your email — must match the signed-in account").option("--yes", "(delete only) Skip the interactive confirmation prompt").action(async (action, _rest, options) => {
118447
118784
  if (action === "export") {
118448
118785
  if (!ctx.client.token) {
118449
- await writeLine(ctx, `${import_picocolors2.default.red("✗")} Not logged in. Run: percher login`);
118786
+ await writeLine2(ctx, `${import_picocolors2.default.red("✗")} Not logged in. Run: percher login`);
118450
118787
  throw new SilentExitError;
118451
118788
  }
118452
118789
  const result = await accountExport(ctx, {
@@ -118454,69 +118791,69 @@ function registerAccountCommands(cli, ctx) {
118454
118791
  includeAppData: options.includeAppData
118455
118792
  });
118456
118793
  const kb = (result.bytes / 1024).toFixed(1);
118457
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} Exported ${kb} KB → ${result.path}`);
118794
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} Exported ${kb} KB → ${result.path}`);
118458
118795
  if (result.appTarballs && result.appTarballs.length > 0) {
118459
118796
  for (const t of result.appTarballs) {
118460
118797
  const mb = (t.bytes / 1024 / 1024).toFixed(1);
118461
- await writeLine(ctx, ` ${import_picocolors2.default.green("✓")} ${t.app}: ${mb} MB → ${t.path}`);
118798
+ await writeLine2(ctx, ` ${import_picocolors2.default.green("✓")} ${t.app}: ${mb} MB → ${t.path}`);
118462
118799
  }
118463
118800
  }
118464
118801
  if (result.appTarballsFailed && result.appTarballsFailed.length > 0) {
118465
118802
  for (const f of result.appTarballsFailed) {
118466
- await writeLine(ctx, ` ${import_picocolors2.default.yellow("⚠")} ${f.app}: ${f.error} (skipped)`);
118803
+ await writeLine2(ctx, ` ${import_picocolors2.default.yellow("⚠")} ${f.app}: ${f.error} (skipped)`);
118467
118804
  }
118468
- await writeLine(ctx, import_picocolors2.default.yellow(`Export is partial — ${result.appTarballsFailed.length} app(s) could not be downloaded. Re-run after the rate-limit window to get the rest.`));
118805
+ await writeLine2(ctx, import_picocolors2.default.yellow(`Export is partial — ${result.appTarballsFailed.length} app(s) could not be downloaded. Re-run after the rate-limit window to get the rest.`));
118469
118806
  }
118470
- await writeLine(ctx, import_picocolors2.default.dim("Note: encrypted env-var values are not included (only the keys + metadata). Build logs are excluded too — request via `percher logs` if needed."));
118807
+ await writeLine2(ctx, import_picocolors2.default.dim("Note: encrypted env-var values are not included (only the keys + metadata). Build logs are excluded too — request via `percher logs` if needed."));
118471
118808
  return;
118472
118809
  }
118473
118810
  if (action === "preview-delete") {
118474
118811
  if (!ctx.client.token) {
118475
- await writeLine(ctx, `${import_picocolors2.default.red("✗")} Not logged in. Run: percher login`);
118812
+ await writeLine2(ctx, `${import_picocolors2.default.red("✗")} Not logged in. Run: percher login`);
118476
118813
  throw new SilentExitError;
118477
118814
  }
118478
118815
  const preview = await accountDeletionPreview(ctx, {});
118479
118816
  if (preview.canDelete) {
118480
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} No blockers — your account is ready to delete. Run: percher account delete --confirm <your-email>`);
118817
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} No blockers — your account is ready to delete. Run: percher account delete --confirm <your-email>`);
118481
118818
  return;
118482
118819
  }
118483
- await writeLine(ctx, `${import_picocolors2.default.yellow("!")} Account deletion is blocked by ${preview.blockers.length} ${preview.blockers.length === 1 ? "category" : "categories"} (${preview.ownedAppCount} owned ${preview.ownedAppCount === 1 ? "app" : "apps"}):`);
118820
+ await writeLine2(ctx, `${import_picocolors2.default.yellow("!")} Account deletion is blocked by ${preview.blockers.length} ${preview.blockers.length === 1 ? "category" : "categories"} (${preview.ownedAppCount} owned ${preview.ownedAppCount === 1 ? "app" : "apps"}):`);
118484
118821
  for (const blocker of preview.blockers) {
118485
118822
  const action2 = BLOCKER_ACTIONS[blocker.kind] ?? "Clear via the dashboard or API.";
118486
- await writeLine(ctx, "");
118487
- await writeLine(ctx, ` ${import_picocolors2.default.bold(blocker.kind)} (${blocker.count})`);
118488
- await writeLine(ctx, ` → ${action2}`);
118823
+ await writeLine2(ctx, "");
118824
+ await writeLine2(ctx, ` ${import_picocolors2.default.bold(blocker.kind)} (${blocker.count})`);
118825
+ await writeLine2(ctx, ` → ${action2}`);
118489
118826
  const previewDetails = blocker.details.slice(0, 5);
118490
118827
  for (const detail of previewDetails) {
118491
118828
  const ref = detail.ref ? ` (${detail.ref})` : "";
118492
118829
  const name = detail.appName ?? detail.appId ?? "—";
118493
- await writeLine(ctx, ` · ${name}${ref}`);
118830
+ await writeLine2(ctx, ` · ${name}${ref}`);
118494
118831
  }
118495
118832
  if (blocker.details.length > previewDetails.length) {
118496
- await writeLine(ctx, import_picocolors2.default.dim(` … ${blocker.details.length - previewDetails.length} more`));
118833
+ await writeLine2(ctx, import_picocolors2.default.dim(` … ${blocker.details.length - previewDetails.length} more`));
118497
118834
  }
118498
118835
  }
118499
- await writeLine(ctx, import_picocolors2.default.dim("\nResolve each category above, then re-run `percher account preview-delete` to confirm. Once canDelete=true, run `percher account delete --confirm <your-email>`."));
118836
+ await writeLine2(ctx, import_picocolors2.default.dim("\nResolve each category above, then re-run `percher account preview-delete` to confirm. Once canDelete=true, run `percher account delete --confirm <your-email>`."));
118500
118837
  throw new SilentExitError(2);
118501
118838
  }
118502
118839
  if (action === "delete") {
118503
118840
  if (!ctx.client.token) {
118504
- await writeLine(ctx, `${import_picocolors2.default.red("✗")} Not logged in. Run: percher login`);
118841
+ await writeLine2(ctx, `${import_picocolors2.default.red("✗")} Not logged in. Run: percher login`);
118505
118842
  throw new SilentExitError;
118506
118843
  }
118507
118844
  if (!options.confirm) {
118508
- await writeLine(ctx, `${import_picocolors2.default.red("✗")} Pass --confirm <your-email> to proceed. Account deletion is fail-closed — owned apps, accepted collaborators, pending invites, open PR previews, pending transfers, in-flight deploys, and active restores must all be cleared first.`);
118845
+ await writeLine2(ctx, `${import_picocolors2.default.red("✗")} Pass --confirm <your-email> to proceed. Account deletion is fail-closed — owned apps, accepted collaborators, pending invites, open PR previews, pending transfers, in-flight deploys, and active restores must all be cleared first.`);
118509
118846
  throw new SilentExitError;
118510
118847
  }
118511
118848
  if (!options.yes) {
118512
118849
  const answer = await ctx.prompt(`${import_picocolors2.default.red("!")} This will request deletion of your account. The server will reject the request if any apps, collaborators, pending invites, PR previews, pending transfers, in-flight deploys, or active restores remain — clear those first. On a clean account the request is accepted asynchronously and finalization completes within seconds; your API tokens are revoked immediately. Type "delete my account" to confirm:`);
118513
118850
  if (answer.trim().toLowerCase() !== "delete my account") {
118514
- await writeLine(ctx, "Cancelled.");
118851
+ await writeLine2(ctx, "Cancelled.");
118515
118852
  return;
118516
118853
  }
118517
118854
  }
118518
118855
  const accepted = await accountDelete(ctx, { confirm: options.confirm });
118519
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} Account deletion started. Finalization runs asynchronously (job ${import_picocolors2.default.dim(accepted.finalizeJobId)}); usually completes within a few seconds. Your API tokens have already been revoked.`);
118856
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} Account deletion started. Finalization runs asynchronously (job ${import_picocolors2.default.dim(accepted.finalizeJobId)}); usually completes within a few seconds. Your API tokens have already been revoked.`);
118520
118857
  return;
118521
118858
  }
118522
118859
  throw new Error(`Unknown account action "${action ?? ""}". Use: export | preview-delete | delete.`);
@@ -118527,82 +118864,82 @@ function registerAccountCommands(cli, ctx) {
118527
118864
  cli.command("mcp", "Print the MCP server config for AI assistants (Claude Code, Cursor, Windsurf)").option("--json", "Output JSON only (paste-ready)").action(async (options) => {
118528
118865
  const result = mcp(ctx, {});
118529
118866
  if (options.json) {
118530
- await writeLine(ctx, JSON.stringify(result.config, null, 2));
118867
+ await writeLine2(ctx, JSON.stringify(result.config, null, 2));
118531
118868
  return;
118532
118869
  }
118533
- await writeLine(ctx, import_picocolors2.default.bold("Percher MCP server"));
118534
- await writeLine(ctx, "");
118535
- await writeLine(ctx, import_picocolors2.default.bold("Local (npx) — full tool set, reads your project files:"));
118536
- await writeLine(ctx, JSON.stringify(result.config, null, 2));
118537
- await writeLine(ctx, "");
118538
- await writeLine(ctx, import_picocolors2.default.bold("Hosted connector — add this URL, OAuth login in the browser (no token):"));
118539
- await writeLine(ctx, ` ${result.hosted.url}`);
118540
- await writeLine(ctx, "");
118541
- await writeLine(ctx, import_picocolors2.default.bold("Where to paste:"));
118870
+ await writeLine2(ctx, import_picocolors2.default.bold("Percher MCP server"));
118871
+ await writeLine2(ctx, "");
118872
+ await writeLine2(ctx, import_picocolors2.default.bold("Local (npx) — full tool set, reads your project files:"));
118873
+ await writeLine2(ctx, JSON.stringify(result.config, null, 2));
118874
+ await writeLine2(ctx, "");
118875
+ await writeLine2(ctx, import_picocolors2.default.bold("Hosted connector — add this URL, OAuth login in the browser (no token):"));
118876
+ await writeLine2(ctx, ` ${result.hosted.url}`);
118877
+ await writeLine2(ctx, "");
118878
+ await writeLine2(ctx, import_picocolors2.default.bold("Where to paste:"));
118542
118879
  for (const loc of result.installLocations) {
118543
- await writeLine(ctx, ` ${loc.client.padEnd(28)} ${import_picocolors2.default.dim(loc.path)}`);
118880
+ await writeLine2(ctx, ` ${loc.client.padEnd(28)} ${import_picocolors2.default.dim(loc.path)}`);
118544
118881
  if (loc.note) {
118545
- await writeLine(ctx, ` ${"".padEnd(28)} ${import_picocolors2.default.dim(loc.note)}`);
118882
+ await writeLine2(ctx, ` ${"".padEnd(28)} ${import_picocolors2.default.dim(loc.note)}`);
118546
118883
  }
118547
118884
  }
118548
- await writeLine(ctx, "");
118885
+ await writeLine2(ctx, "");
118549
118886
  for (const note of result.notes) {
118550
- await writeLine(ctx, import_picocolors2.default.dim(note));
118887
+ await writeLine2(ctx, import_picocolors2.default.dim(note));
118551
118888
  }
118552
- await writeLine(ctx, "");
118553
- await writeLine(ctx, import_picocolors2.default.dim("Once installed, your assistant gets percher_publish + 28 other tools."));
118889
+ await writeLine2(ctx, "");
118890
+ await writeLine2(ctx, import_picocolors2.default.dim("Once installed, your assistant gets percher_publish + 28 other tools."));
118554
118891
  });
118555
118892
  cli.command("ai-files <action>", "Manage AI-assistant rules files (install | update | status | remove)").option("--force", "Overwrite or remove user-modified / foreign files").option("--json", "Output JSON").action(async (action, options) => {
118556
118893
  if (action === "install") {
118557
118894
  const r = await aiFilesInstall(ctx, { force: options.force });
118558
118895
  if (options.json) {
118559
- await writeLine(ctx, JSON.stringify(r, null, 2));
118896
+ await writeLine2(ctx, JSON.stringify(r, null, 2));
118560
118897
  return;
118561
118898
  }
118562
118899
  for (const i of r.installed) {
118563
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} installed ${i.path} (v${i.templateVersion})`);
118900
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} installed ${i.path} (v${i.templateVersion})`);
118564
118901
  }
118565
118902
  for (const s of r.skipped) {
118566
118903
  const why = s.reason === "already-installed" ? "already installed" : s.reason === "modified" ? "user-modified — use --force to overwrite" : "foreign file — use --force to overwrite";
118567
- await writeLine(ctx, `${import_picocolors2.default.yellow("!")} skipped ${s.path}: ${why}`);
118904
+ await writeLine2(ctx, `${import_picocolors2.default.yellow("!")} skipped ${s.path}: ${why}`);
118568
118905
  }
118569
118906
  } else if (action === "update") {
118570
118907
  const r = await aiFilesUpdate(ctx, { force: options.force });
118571
118908
  if (options.json) {
118572
- await writeLine(ctx, JSON.stringify(r, null, 2));
118909
+ await writeLine2(ctx, JSON.stringify(r, null, 2));
118573
118910
  return;
118574
118911
  }
118575
118912
  for (const u of r.updated) {
118576
118913
  const tag = u.reason === "reinstalled" ? "reinstalled" : `${u.from ?? "new"} → ${u.to}`;
118577
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} updated ${u.path} (${tag})`);
118914
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} updated ${u.path} (${tag})`);
118578
118915
  }
118579
118916
  for (const s of r.skipped) {
118580
118917
  const why = s.reason === "up-to-date" ? "up to date" : s.reason === "modified" ? "user-modified — use --force to overwrite" : s.reason === "foreign" ? "foreign file" : "no manifest entry";
118581
- await writeLine(ctx, `${import_picocolors2.default.dim(`· ${s.path}: ${why}`)}`);
118918
+ await writeLine2(ctx, `${import_picocolors2.default.dim(`· ${s.path}: ${why}`)}`);
118582
118919
  }
118583
118920
  } else if (action === "status") {
118584
118921
  const r = await aiFilesStatus(ctx, {});
118585
118922
  if (options.json) {
118586
- await writeLine(ctx, JSON.stringify(r, null, 2));
118923
+ await writeLine2(ctx, JSON.stringify(r, null, 2));
118587
118924
  return;
118588
118925
  }
118589
- await writeLine(ctx, import_picocolors2.default.bold("AI files"));
118926
+ await writeLine2(ctx, import_picocolors2.default.bold("AI files"));
118590
118927
  for (const f of r.files) {
118591
118928
  const icon = f.status === "managed" ? import_picocolors2.default.green("✓") : f.status === "modified" ? import_picocolors2.default.yellow("!") : f.status === "missing" ? import_picocolors2.default.dim("·") : import_picocolors2.default.red("✗");
118592
118929
  const versionTag = f.installedVersion && f.installedVersion !== f.latestVersion ? import_picocolors2.default.yellow(` (v${f.installedVersion} → v${f.latestVersion})`) : f.installedVersion ? import_picocolors2.default.dim(` (v${f.installedVersion})`) : import_picocolors2.default.dim(` (latest v${f.latestVersion})`);
118593
- await writeLine(ctx, ` ${icon} ${f.path.padEnd(40)} ${f.status}${versionTag}`);
118930
+ await writeLine2(ctx, ` ${icon} ${f.path.padEnd(40)} ${f.status}${versionTag}`);
118594
118931
  }
118595
118932
  } else if (action === "remove") {
118596
118933
  const r = await aiFilesRemove(ctx, { force: options.force });
118597
118934
  if (options.json) {
118598
- await writeLine(ctx, JSON.stringify(r, null, 2));
118935
+ await writeLine2(ctx, JSON.stringify(r, null, 2));
118599
118936
  return;
118600
118937
  }
118601
118938
  for (const x of r.removed) {
118602
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} removed ${x.path}`);
118939
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} removed ${x.path}`);
118603
118940
  }
118604
118941
  for (const s of r.skipped) {
118605
- await writeLine(ctx, `${import_picocolors2.default.yellow("!")} skipped ${s.path}: ${s.reason}${s.reason === "modified" ? " — use --force to delete" : ""}`);
118942
+ await writeLine2(ctx, `${import_picocolors2.default.yellow("!")} skipped ${s.path}: ${s.reason}${s.reason === "modified" ? " — use --force to delete" : ""}`);
118606
118943
  }
118607
118944
  } else {
118608
118945
  throw new Error(`Unknown ai-files action "${action}". Use install | update | status | remove.`);
@@ -118613,7 +118950,7 @@ function registerAccountCommands(cli, ctx) {
118613
118950
  from: options.from
118614
118951
  });
118615
118952
  if (result.configGenerated) {
118616
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} Imported from ${result.provider} — percher.toml generated`);
118953
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} Imported from ${result.provider} — percher.toml generated`);
118617
118954
  }
118618
118955
  const mappedParts = [];
118619
118956
  if (result.mapped.redirects > 0) {
@@ -118623,25 +118960,25 @@ function registerAccountCommands(cli, ctx) {
118623
118960
  mappedParts.push(`headers for ${result.mapped.headerPaths} path${result.mapped.headerPaths === 1 ? "" : "s"}`);
118624
118961
  }
118625
118962
  if (mappedParts.length > 0) {
118626
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} Mapped ${mappedParts.join(" and ")} into percher.toml`);
118963
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} Mapped ${mappedParts.join(" and ")} into percher.toml`);
118627
118964
  }
118628
118965
  for (const file2 of result.scaffoldedFiles) {
118629
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} wrote ${file2}`);
118966
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} wrote ${file2}`);
118630
118967
  }
118631
118968
  for (const w of result.warnings) {
118632
- await writeLine(ctx, `${import_picocolors2.default.yellow("!")} ${w}`);
118969
+ await writeLine2(ctx, `${import_picocolors2.default.yellow("!")} ${w}`);
118633
118970
  }
118634
118971
  if (result.envVars.length > 0) {
118635
- await writeLine(ctx, "");
118636
- await writeLine(ctx, "Set these env vars before publishing:");
118972
+ await writeLine2(ctx, "");
118973
+ await writeLine2(ctx, "Set these env vars before publishing:");
118637
118974
  for (const v of result.envVars) {
118638
- await writeLine(ctx, ` bunx percher env set ${v}=your-value`);
118975
+ await writeLine2(ctx, ` bunx percher env set ${v}=your-value`);
118639
118976
  }
118640
118977
  }
118641
118978
  if (result.configGenerated) {
118642
- await writeLine(ctx, "");
118643
- await writeLine(ctx, "Next: review percher.toml, then run:");
118644
- await writeLine(ctx, " bunx percher publish");
118979
+ await writeLine2(ctx, "");
118980
+ await writeLine2(ctx, "Next: review percher.toml, then run:");
118981
+ await writeLine2(ctx, " bunx percher publish");
118645
118982
  }
118646
118983
  });
118647
118984
  cli.command("domains <action> [domain]", "Manage custom domains (add | verify | list | remove)").option("--app <name>", "App name (overrides percher.toml)").action(async (action, domain2, options) => {
@@ -118649,52 +118986,52 @@ function registerAccountCommands(cli, ctx) {
118649
118986
  if (!domain2)
118650
118987
  throw new Error("Domain is required. Usage: percher domains add <domain>");
118651
118988
  const result = await domainAdd(ctx, { app: options.app, domain: domain2 });
118652
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} Domain ${result.domain} added.`);
118653
- await writeLine(ctx, "");
118654
- await writeLine(ctx, `Add these DNS records ${result.dnsInstructions.kind === "apex" ? "(this is a root/apex domain — most providers reject CNAME on apex per RFC 1034, use A-record)" : "(this is a subdomain — CNAME works everywhere)"}:`);
118655
- await writeLine(ctx, ` TXT ${result.dnsInstructions.txt.name} ${result.dnsInstructions.txt.value}`);
118989
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} Domain ${result.domain} added.`);
118990
+ await writeLine2(ctx, "");
118991
+ await writeLine2(ctx, `Add these DNS records ${result.dnsInstructions.kind === "apex" ? "(this is a root/apex domain — most providers reject CNAME on apex per RFC 1034, use A-record)" : "(this is a subdomain — CNAME works everywhere)"}:`);
118992
+ await writeLine2(ctx, ` TXT ${result.dnsInstructions.txt.name} ${result.dnsInstructions.txt.value}`);
118656
118993
  const primary = result.dnsInstructions.primary;
118657
118994
  if (primary.type === "A") {
118658
- await writeLine(ctx, ` A ${primary.name} ${primary.values.join(", ")}`);
118995
+ await writeLine2(ctx, ` A ${primary.name} ${primary.values.join(", ")}`);
118659
118996
  } else {
118660
- await writeLine(ctx, ` CNAME ${primary.name} ${primary.value}`);
118997
+ await writeLine2(ctx, ` CNAME ${primary.name} ${primary.value}`);
118661
118998
  }
118662
118999
  if (result.dnsInstructions.alternative) {
118663
119000
  const alt = result.dnsInstructions.alternative;
118664
- await writeLine(ctx, "");
118665
- await writeLine(ctx, " Alternative (only for Cloudflare/Route53 with CNAME-flattening):");
119001
+ await writeLine2(ctx, "");
119002
+ await writeLine2(ctx, " Alternative (only for Cloudflare/Route53 with CNAME-flattening):");
118666
119003
  if (alt.type === "A") {
118667
- await writeLine(ctx, ` A ${alt.name} ${alt.values.join(", ")}`);
119004
+ await writeLine2(ctx, ` A ${alt.name} ${alt.values.join(", ")}`);
118668
119005
  } else {
118669
- await writeLine(ctx, ` CNAME ${alt.name} ${alt.value}`);
119006
+ await writeLine2(ctx, ` CNAME ${alt.name} ${alt.value}`);
118670
119007
  }
118671
119008
  }
118672
- await writeLine(ctx, "");
118673
- await writeLine(ctx, "Then run:");
118674
- await writeLine(ctx, ` percher domains verify ${result.domain}${options.app ? ` --app ${options.app}` : ""}`);
119009
+ await writeLine2(ctx, "");
119010
+ await writeLine2(ctx, "Then run:");
119011
+ await writeLine2(ctx, ` percher domains verify ${result.domain}${options.app ? ` --app ${options.app}` : ""}`);
118675
119012
  } else if (action === "verify") {
118676
119013
  if (!domain2)
118677
119014
  throw new Error("Domain is required. Usage: percher domains verify <domain>");
118678
119015
  const result = await domainVerify(ctx, { app: options.app, domain: domain2 });
118679
119016
  if (result.status === "verified") {
118680
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} Domain verified. ${result.domain} is now live.`);
119017
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} Domain verified. ${result.domain} is now live.`);
118681
119018
  } else {
118682
- await writeLine(ctx, `${import_picocolors2.default.yellow("!")} ${result.domain}: ${result.failureReason}`);
119019
+ await writeLine2(ctx, `${import_picocolors2.default.yellow("!")} ${result.domain}: ${result.failureReason}`);
118683
119020
  }
118684
119021
  } else if (action === "list") {
118685
119022
  const list = await domainList(ctx, { app: options.app });
118686
119023
  if (list.length === 0) {
118687
- await writeLine(ctx, "No custom domains.");
119024
+ await writeLine2(ctx, "No custom domains.");
118688
119025
  } else {
118689
119026
  for (const d of list) {
118690
- await writeLine(ctx, `${d.domain.padEnd(40)} ${d.status}`);
119027
+ await writeLine2(ctx, `${d.domain.padEnd(40)} ${d.status}`);
118691
119028
  }
118692
119029
  }
118693
119030
  } else if (action === "remove") {
118694
119031
  if (!domain2)
118695
119032
  throw new Error("Domain is required. Usage: percher domains remove <domain>");
118696
119033
  await domainRemove(ctx, { app: options.app, domain: domain2 });
118697
- await writeLine(ctx, `${import_picocolors2.default.green("✓")} Domain removed.`);
119034
+ await writeLine2(ctx, `${import_picocolors2.default.green("✓")} Domain removed.`);
118698
119035
  } else {
118699
119036
  throw new Error(`Unknown domains action "${action}". Use add | verify | list | remove.`);
118700
119037
  }
@@ -118707,7 +119044,7 @@ function registerAdminCommands(cli, ctx) {
118707
119044
  cli.command("admin [action] [...rest]", "Operator commands (admin only): reconcile-routes").option("--app <name>", "(reconcile-routes) Limit to a single app by name").option("--app-id <id>", "(reconcile-routes) Limit to a single app by id").option("--dry-run", "(reconcile-routes) Read-only diff — no Caddy mutations, returns missing/extra/host drift").option("--json", "(reconcile-routes) Output JSON instead of formatted text").action(async (action, _rest, options) => {
118708
119045
  if (action === "reconcile-routes") {
118709
119046
  if (!ctx.client.token) {
118710
- await writeLine(ctx, `${import_picocolors3.default.red("✗")} Not logged in. Run: percher login`);
119047
+ await writeLine2(ctx, `${import_picocolors3.default.red("✗")} Not logged in. Run: percher login`);
118711
119048
  throw new SilentExitError;
118712
119049
  }
118713
119050
  const result = await adminReconcileRoutes(ctx, {
@@ -118716,7 +119053,7 @@ function registerAdminCommands(cli, ctx) {
118716
119053
  dryRun: Boolean(options.dryRun)
118717
119054
  });
118718
119055
  if (options.json) {
118719
- await writeLine(ctx, JSON.stringify(result, null, 2));
119056
+ await writeLine2(ctx, JSON.stringify(result, null, 2));
118720
119057
  return;
118721
119058
  }
118722
119059
  if (options.dryRun) {
@@ -118725,25 +119062,25 @@ function registerAdminCommands(cli, ctx) {
118725
119062
  const extra = result.extraRoute ?? 0;
118726
119063
  const drift = result.dryRunDrift ?? [];
118727
119064
  if (missing === 0 && hostDrift === 0 && extra === 0) {
118728
- await writeLine(ctx, `${import_picocolors3.default.green("✓")} No drift — Caddy matches expected routes.`);
119065
+ await writeLine2(ctx, `${import_picocolors3.default.green("✓")} No drift — Caddy matches expected routes.`);
118729
119066
  return;
118730
119067
  }
118731
- await writeLine(ctx, `${import_picocolors3.default.yellow("!")} drift: ${missing} missing route(s), ${hostDrift} missing host(s), ${extra} extra route(s)`);
119068
+ await writeLine2(ctx, `${import_picocolors3.default.yellow("!")} drift: ${missing} missing route(s), ${hostDrift} missing host(s), ${extra} extra route(s)`);
118732
119069
  for (const d of drift) {
118733
119070
  const label = d.kind === "missing_route" ? import_picocolors3.default.red("MISSING") : d.kind === "missing_host" ? import_picocolors3.default.yellow("HOST_DRIFT") : import_picocolors3.default.dim("EXTRA");
118734
119071
  const target = d.appName ? `[${d.appName}] ` : "";
118735
- await writeLine(ctx, ` ${label} ${target}${d.routeId} (${d.routeType}) — ${d.hosts.join(", ")}`);
119072
+ await writeLine2(ctx, ` ${label} ${target}${d.routeId} (${d.routeType}) — ${d.hosts.join(", ")}`);
118736
119073
  }
118737
- await writeLine(ctx, import_picocolors3.default.dim(`Repair: percher admin reconcile-routes${options.app ? ` --app ${options.app}` : ""}`));
119074
+ await writeLine2(ctx, import_picocolors3.default.dim(`Repair: percher admin reconcile-routes${options.app ? ` --app ${options.app}` : ""}`));
118738
119075
  return;
118739
119076
  }
118740
119077
  if (result.errors === 0) {
118741
- await writeLine(ctx, `${import_picocolors3.default.green("✓")} reconciled ${result.reconciled}, recovered ${result.recovered}, stale ${result.stale}, errors 0`);
119078
+ await writeLine2(ctx, `${import_picocolors3.default.green("✓")} reconciled ${result.reconciled}, recovered ${result.recovered}, stale ${result.stale}, errors 0`);
118742
119079
  return;
118743
119080
  }
118744
- await writeLine(ctx, `${import_picocolors3.default.yellow("!")} reconciled ${result.reconciled}, errors ${result.errors} (app=${result.appRouteErrors} pb=${result.pbRouteErrors} sleeping=${result.sleepingRouteErrors})`);
119081
+ await writeLine2(ctx, `${import_picocolors3.default.yellow("!")} reconciled ${result.reconciled}, errors ${result.errors} (app=${result.appRouteErrors} pb=${result.pbRouteErrors} sleeping=${result.sleepingRouteErrors})`);
118745
119082
  for (const e of result.perAppErrors) {
118746
- await writeLine(ctx, ` ${import_picocolors3.default.red("✗")} [${e.appName}] ${e.routeId} (${e.routeType}) — ${e.errorCode}: ${e.cause}`);
119083
+ await writeLine2(ctx, ` ${import_picocolors3.default.red("✗")} [${e.appName}] ${e.routeId} (${e.routeType}) — ${e.errorCode}: ${e.cause}`);
118747
119084
  }
118748
119085
  throw new SilentExitError(1);
118749
119086
  }
@@ -118760,156 +119097,162 @@ function assertNever2(status2, summary) {
118760
119097
  async function handlePublishResult(ctx, result, options = {}) {
118761
119098
  switch (result.status) {
118762
119099
  case "dry_run": {
118763
- await writeLine(ctx, `Dry run: ${result.fileCount} files, ${(result.bytes / 1024).toFixed(1)} KB`);
119100
+ await writeLine2(ctx, `Dry run: ${result.fileCount} files, ${(result.bytes / 1024).toFixed(1)} KB`);
119101
+ return;
119102
+ }
119103
+ case "check_passed": {
119104
+ const timing = result.timing ? ` (${result.timing.totalSeconds}s)` : "";
119105
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} Check passed${timing} — would build and boot`);
119106
+ await writeLine2(ctx, import_picocolors4.default.dim(" The live app was not changed. Run `percher publish` to ship."));
118764
119107
  return;
118765
119108
  }
118766
119109
  case "needs_login": {
118767
119110
  if (result.loginUrl) {
118768
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Not logged in. Authorize here:`);
118769
- await writeLine(ctx, ` ${result.loginUrl}`);
119111
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} Not logged in. Authorize here:`);
119112
+ await writeLine2(ctx, ` ${result.loginUrl}`);
118770
119113
  } else {
118771
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} Not logged in. Run: percher login`);
119114
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} Not logged in. Run: percher login`);
118772
119115
  }
118773
119116
  throw new SilentExitError;
118774
119117
  }
118775
119118
  case "auth_pending": {
118776
119119
  if (result.loginUrl) {
118777
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Not logged in. Authorize here:`);
118778
- await writeLine(ctx, ` ${result.loginUrl}`);
119120
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} Not logged in. Authorize here:`);
119121
+ await writeLine2(ctx, ` ${result.loginUrl}`);
118779
119122
  } else {
118780
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} Not logged in. Run: percher login`);
119123
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} Not logged in. Run: percher login`);
118781
119124
  }
118782
119125
  throw new SilentExitError;
118783
119126
  }
118784
119127
  case "queued": {
118785
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
119128
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
118786
119129
  const deployId = result.deployment?.id;
118787
119130
  const appName = result.app?.name;
118788
119131
  if (deployId && appName) {
118789
- await writeLine(ctx, import_picocolors4.default.dim(` Next: percher wait-for-deploy ${deployId} --app ${appName}`));
119132
+ await writeLine2(ctx, import_picocolors4.default.dim(` Next: percher wait-for-deploy ${deployId} --app ${appName}`));
118790
119133
  } else if (deployId) {
118791
- await writeLine(ctx, import_picocolors4.default.dim(` Next: percher wait-for-deploy ${deployId}`));
119134
+ await writeLine2(ctx, import_picocolors4.default.dim(` Next: percher wait-for-deploy ${deployId}`));
118792
119135
  }
118793
119136
  return;
118794
119137
  }
118795
119138
  case "already_in_progress": {
118796
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
119139
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
118797
119140
  if (result.recovery.nextAction === "wait_deploy") {
118798
119141
  const args = result.recovery.args;
118799
119142
  if (args.app && args.deployId) {
118800
- await writeLine(ctx, import_picocolors4.default.dim(` Next: percher wait-for-deploy ${args.deployId} --app ${args.app}`));
119143
+ await writeLine2(ctx, import_picocolors4.default.dim(` Next: percher wait-for-deploy ${args.deployId} --app ${args.app}`));
118801
119144
  }
118802
119145
  }
118803
119146
  return;
118804
119147
  }
118805
119148
  case "replaced": {
118806
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
119149
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
118807
119150
  if (result.url) {
118808
- await writeLine(ctx, import_picocolors4.default.dim(` Live URL: ${result.url}`));
119151
+ await writeLine2(ctx, import_picocolors4.default.dim(` Live URL: ${result.url}`));
118809
119152
  }
118810
119153
  if (result.recovery.nextAction === "wait_deploy") {
118811
119154
  const args = result.recovery.args;
118812
119155
  if (args.app && args.deployId) {
118813
- await writeLine(ctx, import_picocolors4.default.dim(` Next: percher deploys inspect ${args.deployId} --app ${args.app}`));
119156
+ await writeLine2(ctx, import_picocolors4.default.dim(` Next: percher deploys inspect ${args.deployId} --app ${args.app}`));
118814
119157
  }
118815
119158
  } else if (result.recovery.nextAction === "ask_user" || result.recovery.nextAction === "run_doctor") {
118816
119159
  const appName = result.app?.name;
118817
119160
  if (appName) {
118818
- await writeLine(ctx, import_picocolors4.default.dim(` Next: percher doctor --app ${appName}`));
119161
+ await writeLine2(ctx, import_picocolors4.default.dim(` Next: percher doctor --app ${appName}`));
118819
119162
  } else {
118820
- await writeLine(ctx, import_picocolors4.default.dim(" Next: percher doctor"));
119163
+ await writeLine2(ctx, import_picocolors4.default.dim(" Next: percher doctor"));
118821
119164
  }
118822
119165
  }
118823
119166
  return;
118824
119167
  }
118825
119168
  case "failed": {
118826
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} ${result.error?.title ?? "Deployment failed"}`);
119169
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} ${result.error?.title ?? "Deployment failed"}`);
118827
119170
  if (result.error?.explanation) {
118828
- await writeLine(ctx, ` ${result.error.explanation}`);
119171
+ await writeLine2(ctx, ` ${result.error.explanation}`);
118829
119172
  }
118830
119173
  if (result.error?.rawErrorMessage) {
118831
- await writeLine(ctx, ` ${import_picocolors4.default.dim(`Details: ${result.error.rawErrorMessage}`)}`);
119174
+ await writeLine2(ctx, ` ${import_picocolors4.default.dim(`Details: ${result.error.rawErrorMessage}`)}`);
118832
119175
  }
118833
119176
  if (result.error?.suggestion) {
118834
- await writeLine(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
119177
+ await writeLine2(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
118835
119178
  }
118836
119179
  if (result.error?.missingEnvVars && result.error.missingEnvVars.length > 0) {
118837
- await writeLine(ctx, "");
118838
- await writeLine(ctx, import_picocolors4.default.bold("Missing env vars:"));
119180
+ await writeLine2(ctx, "");
119181
+ await writeLine2(ctx, import_picocolors4.default.bold("Missing env vars:"));
118839
119182
  for (const k of result.error.missingEnvVars) {
118840
- await writeLine(ctx, ` ${k}`);
119183
+ await writeLine2(ctx, ` ${k}`);
118841
119184
  }
118842
- await writeLine(ctx, import_picocolors4.default.dim(` Set with: percher env set ${result.error.missingEnvVars[0]}=...`));
119185
+ await writeLine2(ctx, import_picocolors4.default.dim(` Set with: percher env set ${result.error.missingEnvVars[0]}=...`));
118843
119186
  }
118844
119187
  if (result.error?.relevantFiles && result.error.relevantFiles.length > 0) {
118845
- await writeLine(ctx, "");
118846
- await writeLine(ctx, import_picocolors4.default.bold("Check these files:"));
119188
+ await writeLine2(ctx, "");
119189
+ await writeLine2(ctx, import_picocolors4.default.bold("Check these files:"));
118847
119190
  for (const f of result.error.relevantFiles) {
118848
- await writeLine(ctx, ` ${f}`);
119191
+ await writeLine2(ctx, ` ${f}`);
118849
119192
  }
118850
119193
  }
118851
119194
  if (result.error?.buildLogTail) {
118852
- await writeLine(ctx, "");
119195
+ await writeLine2(ctx, "");
118853
119196
  const lines = result.error.buildLogTail.split(`
118854
119197
  `);
118855
- await writeLine(ctx, `${import_picocolors4.default.bold("Build log")} ${import_picocolors4.default.dim(`(last ${lines.length} lines)`)}`);
119198
+ await writeLine2(ctx, `${import_picocolors4.default.bold("Build log")} ${import_picocolors4.default.dim(`(last ${lines.length} lines)`)}`);
118856
119199
  for (const line of lines) {
118857
- await writeLine(ctx, ` ${line}`);
119200
+ await writeLine2(ctx, ` ${line}`);
118858
119201
  }
118859
119202
  if (result.app?.id && result.deployment?.id) {
118860
- await writeLine(ctx, "");
118861
- await writeLine(ctx, import_picocolors4.default.dim(`Full log: percher deploys inspect ${result.deployment.id} --app ${result.app.name}`));
119203
+ await writeLine2(ctx, "");
119204
+ await writeLine2(ctx, import_picocolors4.default.dim(`Full log: percher deploys inspect ${result.deployment.id} --app ${result.app.name}`));
118862
119205
  }
118863
119206
  }
118864
119207
  throw new SilentExitError;
118865
119208
  }
118866
119209
  case "live": {
118867
119210
  const timing = result.timing ? ` (${result.timing.totalSeconds}s)` : "";
118868
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} Published to ${result.url}${timing}`);
119211
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} Published to ${result.url}${timing}`);
118869
119212
  if (result.traceId) {
118870
- await writeLine(ctx, import_picocolors4.default.dim(` Trace: ${result.traceId}`));
119213
+ await writeLine2(ctx, import_picocolors4.default.dim(` Trace: ${result.traceId}`));
118871
119214
  }
118872
119215
  if (result.cacheReused) {
118873
- await writeLine(ctx, import_picocolors4.default.dim(" Build cache: reused — identical bundle, redeployed without re-upload (--no-cache to force a fresh build)"));
119216
+ await writeLine2(ctx, import_picocolors4.default.dim(" Build cache: reused — identical bundle, redeployed without re-upload (--no-cache to force a fresh build)"));
118874
119217
  } else {
118875
119218
  const status2 = result.cacheStatus ?? (result.cacheHit === true ? "image_reused" : result.cacheHit === false ? "fresh" : null);
118876
119219
  if (status2 === "image_reused") {
118877
- await writeLine(ctx, import_picocolors4.default.dim(" Build cache: hit (reused image — re-run with --no-cache if your changes aren't reflected)"));
119220
+ await writeLine2(ctx, import_picocolors4.default.dim(" Build cache: hit (reused image — re-run with --no-cache if your changes aren't reflected)"));
118878
119221
  } else if (status2 === "layer_hit") {
118879
- await writeLine(ctx, import_picocolors4.default.dim(" Build cache: layer hit (Docker reused unchanged layers — full nixpacks/build skipped)"));
119222
+ await writeLine2(ctx, import_picocolors4.default.dim(" Build cache: layer hit (Docker reused unchanged layers — full nixpacks/build skipped)"));
118880
119223
  } else if (status2 === "fresh") {
118881
- await writeLine(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
119224
+ await writeLine2(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
118882
119225
  }
118883
119226
  }
118884
119227
  if (typeof result.deployment?.imageSizeBytes === "number") {
118885
- await writeLine(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
119228
+ await writeLine2(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
118886
119229
  }
118887
119230
  if (result.replacedPreview) {
118888
- await writeLine(ctx, import_picocolors4.default.dim(" (replaced previous preview — free plan keeps 1 preview slot per app)"));
119231
+ await writeLine2(ctx, import_picocolors4.default.dim(" (replaced previous preview — free plan keeps 1 preview slot per app)"));
118889
119232
  }
118890
119233
  if (options.preview && result.url) {
118891
119234
  try {
118892
119235
  const qr = await qrToTerminal(result.url);
118893
- await writeLine(ctx, "");
118894
- await writeLine(ctx, import_picocolors4.default.dim("Scan to preview on mobile:"));
118895
- await writeLine(ctx, qr);
119236
+ await writeLine2(ctx, "");
119237
+ await writeLine2(ctx, import_picocolors4.default.dim("Scan to preview on mobile:"));
119238
+ await writeLine2(ctx, qr);
118896
119239
  } catch {}
118897
119240
  }
118898
119241
  if (result.anonymous && result.claim) {
118899
- await writeLine(ctx, "");
118900
- await writeLine(ctx, import_picocolors4.default.bold("Anonymous app — claim it to keep it:"));
118901
- await writeLine(ctx, ` Expires: ${new Date(result.claim.expiresAt).toLocaleString()}`);
118902
- await writeLine(ctx, ` Claim token: ${result.claim.claimToken}`);
118903
- await writeLine(ctx, ` ${import_picocolors4.default.cyan("percher claim")} claim into your account (lifts the limits on next deploy)`);
119242
+ await writeLine2(ctx, "");
119243
+ await writeLine2(ctx, import_picocolors4.default.bold("Anonymous app — claim it to keep it:"));
119244
+ await writeLine2(ctx, ` Expires: ${new Date(result.claim.expiresAt).toLocaleString()}`);
119245
+ await writeLine2(ctx, ` Claim token: ${result.claim.claimToken}`);
119246
+ await writeLine2(ctx, ` ${import_picocolors4.default.cyan("percher claim")} claim into your account (lifts the limits on next deploy)`);
118904
119247
  for (const w of result.warnings ?? []) {
118905
- await writeLine(ctx, import_picocolors4.default.dim(` ${w}`));
119248
+ await writeLine2(ctx, import_picocolors4.default.dim(` ${w}`));
118906
119249
  }
118907
119250
  } else if (result.firstDeploy) {
118908
- await writeLine(ctx, "");
118909
- await writeLine(ctx, import_picocolors4.default.bold("First deploy — next steps:"));
118910
- await writeLine(ctx, ` ${import_picocolors4.default.cyan("percher dev")} watch for changes and re-deploy`);
118911
- await writeLine(ctx, ` ${import_picocolors4.default.cyan("percher env set KEY")} add environment variables`);
118912
- await writeLine(ctx, ` ${import_picocolors4.default.cyan("percher logs")} stream live logs`);
119251
+ await writeLine2(ctx, "");
119252
+ await writeLine2(ctx, import_picocolors4.default.bold("First deploy — next steps:"));
119253
+ await writeLine2(ctx, ` ${import_picocolors4.default.cyan("percher dev")} watch for changes and re-deploy`);
119254
+ await writeLine2(ctx, ` ${import_picocolors4.default.cyan("percher env set KEY")} add environment variables`);
119255
+ await writeLine2(ctx, ` ${import_picocolors4.default.cyan("percher logs")} stream live logs`);
118913
119256
  }
118914
119257
  return;
118915
119258
  }
@@ -118924,9 +119267,9 @@ function registerAppsCommands(cli, ctx) {
118924
119267
  name: options.name,
118925
119268
  interactive: true
118926
119269
  });
118927
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} wrote ${result.path}`);
119270
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} wrote ${result.path}`);
118928
119271
  for (const file2 of result.scaffoldedFiles) {
118929
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} wrote ${file2}`);
119272
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} wrote ${file2}`);
118930
119273
  }
118931
119274
  });
118932
119275
  cli.command("appstore-kit", "Write app-store starter files (PWA manifest, TWA/Capacitor, asset links)").option("--name <name>", "App name (defaults to percher.toml / directory)").option("--url <url>", "App URL (defaults to https://<name>.percher.run)").option("--package <pkg>", "Android package name for assetlinks.json").option("--fingerprint <sha256>", "Signing-cert SHA-256 for assetlinks.json").option("--team-id <id>", "Apple Team ID for apple-app-site-association").option("--bundle-id <id>", "Apple bundle ID for apple-app-site-association").option("--out <dir>", "Output directory (defaults to the current directory)").option("--force", "Overwrite files that already exist").action(async (options) => {
@@ -118941,32 +119284,32 @@ function registerAppsCommands(cli, ctx) {
118941
119284
  force: options.force
118942
119285
  });
118943
119286
  for (const file2 of result.written) {
118944
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} wrote ${file2}`);
119287
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} wrote ${file2}`);
118945
119288
  }
118946
119289
  for (const file2 of result.skipped) {
118947
- await writeLine(ctx, `${import_picocolors4.default.dim("•")} skipped ${file2} (exists — pass --force to overwrite)`);
118948
- }
118949
- await writeLine(ctx, "");
118950
- await writeLine(ctx, `${import_picocolors4.default.bold("Make it an installable PWA:")} link the manifest from your HTML`);
118951
- await writeLine(ctx, ` <link rel="manifest" href="/manifest.json">`);
118952
- await writeLine(ctx, "and register the service worker from your app entry:");
118953
- await writeLine(ctx, result.swRegistration);
118954
- await writeLine(ctx, "");
118955
- await writeLine(ctx, `${import_picocolors4.default.bold("Wrap for Google Play (TWA):")}`);
118956
- await writeLine(ctx, result.bubblewrapCommand);
118957
- await writeLine(ctx, "");
118958
- await writeLine(ctx, `The two ${import_picocolors4.default.dim(".well-known")} files are trust assertions for your origin — add real values`);
118959
- await writeLine(ctx, `after you sign the apps, then serve them yourself or let Percher serve them from the`);
118960
- await writeLine(ctx, "App store kit at https://percher.app. Full guide: /docs/app-stores");
119290
+ await writeLine2(ctx, `${import_picocolors4.default.dim("•")} skipped ${file2} (exists — pass --force to overwrite)`);
119291
+ }
119292
+ await writeLine2(ctx, "");
119293
+ await writeLine2(ctx, `${import_picocolors4.default.bold("Make it an installable PWA:")} link the manifest from your HTML`);
119294
+ await writeLine2(ctx, ` <link rel="manifest" href="/manifest.json">`);
119295
+ await writeLine2(ctx, "and register the service worker from your app entry:");
119296
+ await writeLine2(ctx, result.swRegistration);
119297
+ await writeLine2(ctx, "");
119298
+ await writeLine2(ctx, `${import_picocolors4.default.bold("Wrap for Google Play (TWA):")}`);
119299
+ await writeLine2(ctx, result.bubblewrapCommand);
119300
+ await writeLine2(ctx, "");
119301
+ await writeLine2(ctx, `The two ${import_picocolors4.default.dim(".well-known")} files are trust assertions for your origin — add real values`);
119302
+ await writeLine2(ctx, `after you sign the apps, then serve them yourself or let Percher serve them from the`);
119303
+ await writeLine2(ctx, "App store kit at https://percher.app. Full guide: /docs/app-stores");
118961
119304
  });
118962
- cli.command("publish", "Publish this project to Percher (recommended deploy command)").option("--force", "Skip size warnings").option("--preview", "Deploy as preview (does not replace the live version)").option("-m, --message <msg>", "Deploy note (visible in deploy history)").option("--dry-run", "Show what would be deployed (file count, bytes) without uploading").option("--no-cache", "Skip image-cache lookup and force a fresh build (use when you suspect cached image is stale)").option("--anonymous", "Publish without an account — a 72h sandbox you can claim later (where enabled)").action(async (options) => {
119305
+ cli.command("publish", "Publish this project to Percher (recommended deploy command)").option("--force", "Skip size warnings").option("--preview", "Deploy as preview (does not replace the live version)").option("-m, --message <msg>", "Deploy note (visible in deploy history)").option("--dry-run", "Show what would be deployed (file count, bytes) without uploading").option("--check", "Validation deploy: build → start → health, then tear down (no swap, no route). The live app is not changed. Consumes the preview quota.").option("--no-cache", "Skip image-cache lookup and force a fresh build (use when you suspect cached image is stale)").option("--anonymous", "Publish without an account — a 72h sandbox you can claim later (where enabled)").action(async (options) => {
118963
119306
  const { detectCiBranchMessage: detectCiBranchMessage2 } = await Promise.resolve().then(() => exports_ci_env);
118964
119307
  let messageOverride = options.message;
118965
119308
  if (options.preview && !options.message) {
118966
119309
  const ci = detectCiBranchMessage2(process.env);
118967
119310
  if (ci) {
118968
119311
  messageOverride = ci.message;
118969
- await writeLine(ctx, import_picocolors4.default.dim(`Auto-detected ${ci.source} branch — using deploy note "${ci.message}"`));
119312
+ await writeLine2(ctx, import_picocolors4.default.dim(`Auto-detected ${ci.source} branch — using deploy note "${ci.message}"`));
118970
119313
  }
118971
119314
  }
118972
119315
  const result = await publish(ctx, {
@@ -118974,52 +119317,54 @@ function registerAppsCommands(cli, ctx) {
118974
119317
  preview: options.preview,
118975
119318
  message: messageOverride,
118976
119319
  dryRun: options.dryRun,
119320
+ check: options.check,
118977
119321
  noCache: options.cache === false,
118978
119322
  anonymous: options.anonymous
118979
119323
  });
118980
119324
  await handlePublishResult(ctx, result, { preview: options.preview });
118981
119325
  });
118982
- cli.command("redeploy [deployId]", "Re-run an existing deploy without re-uploading source").option("--app <name>", "App name (defaults to percher.toml app.name)").action(async (deployId, options) => {
119326
+ cli.command("redeploy [deployId]", "Re-run an existing deploy without re-uploading source").option("--app <name>", "App name (defaults to percher.toml app.name)").option("--no-cache", "Force a fresh build of that deploy's source (skip the image cache — the stale-image escape hatch)").action(async (deployId, options) => {
118983
119327
  const result = await redeploy(ctx, {
118984
119328
  app: options.app,
118985
- deployId
119329
+ deployId,
119330
+ noCache: options.cache === false
118986
119331
  });
118987
119332
  if (result.status === "needs_login") {
118988
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} Not logged in. Run: percher login`);
119333
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} Not logged in. Run: percher login`);
118989
119334
  throw new SilentExitError;
118990
119335
  }
118991
119336
  if (result.status === "failed") {
118992
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} ${result.error?.title ?? "Redeploy failed"}`);
119337
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} ${result.error?.title ?? "Redeploy failed"}`);
118993
119338
  if (result.error?.explanation) {
118994
- await writeLine(ctx, ` ${result.error.explanation}`);
119339
+ await writeLine2(ctx, ` ${result.error.explanation}`);
118995
119340
  }
118996
119341
  if (result.error?.suggestion) {
118997
- await writeLine(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
119342
+ await writeLine2(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
118998
119343
  }
118999
119344
  if (result.error?.problems && result.error.problems.length > 0) {
119000
- await writeLine(ctx, "");
119001
- await writeLine(ctx, import_picocolors4.default.bold("Build problems:"));
119345
+ await writeLine2(ctx, "");
119346
+ await writeLine2(ctx, import_picocolors4.default.bold("Build problems:"));
119002
119347
  for (const p of result.error.problems) {
119003
119348
  const loc = p.file ? ` ${import_picocolors4.default.dim(`${p.file}${p.line ? `:${p.line}` : ""}${p.column ? `:${p.column}` : ""}`)}` : "";
119004
- await writeLine(ctx, ` ${import_picocolors4.default.red(p.code)}${loc} ${p.message}`);
119349
+ await writeLine2(ctx, ` ${import_picocolors4.default.red(p.code)}${loc} ${p.message}`);
119005
119350
  if (p.hint)
119006
- await writeLine(ctx, ` ${import_picocolors4.default.dim(`→ ${p.hint}`)}`);
119351
+ await writeLine2(ctx, ` ${import_picocolors4.default.dim(`→ ${p.hint}`)}`);
119007
119352
  }
119008
119353
  }
119009
119354
  throw new SilentExitError;
119010
119355
  }
119011
119356
  const timing = result.timing ? ` (${result.timing.totalSeconds.toFixed(0)}s)` : "";
119012
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} Redeployed to ${result.url}${timing}`);
119357
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} Redeployed to ${result.url}${timing}`);
119013
119358
  });
119014
119359
  cli.command("wait-for-deploy <deployId>", "Wait for an in-flight deploy to finish").option("--app <name>", "App name (defaults to percher.toml app.name)").option("--timeout <seconds>", "Max seconds to wait (default: 600)").action(async (deployId, options) => {
119015
119360
  const appName = options.app ?? readPercherTomlAppName(ctx.cwd);
119016
119361
  if (!appName) {
119017
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} No app specified — pass --app <name> or run from a directory with percher.toml.`);
119362
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} No app specified — pass --app <name> or run from a directory with percher.toml.`);
119018
119363
  throw new SilentExitError(1);
119019
119364
  }
119020
119365
  const timeoutSec = Number.parseInt(options.timeout ?? "600", 10);
119021
119366
  if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) {
119022
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} --timeout must be a positive number of seconds.`);
119367
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} --timeout must be a positive number of seconds.`);
119023
119368
  throw new SilentExitError(1);
119024
119369
  }
119025
119370
  const deadline = Date.now() + timeoutSec * 1000;
@@ -119027,7 +119372,7 @@ function registerAppsCommands(cli, ctx) {
119027
119372
  for (;; ) {
119028
119373
  const remainingMs = deadline - Date.now();
119029
119374
  if (remainingMs <= 0) {
119030
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
119375
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
119031
119376
  throw new SilentExitError(1);
119032
119377
  }
119033
119378
  let raceTimer;
@@ -119049,22 +119394,22 @@ function registerAppsCommands(cli, ctx) {
119049
119394
  }
119050
119395
  if (result === null) {
119051
119396
  callController.abort();
119052
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
119397
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} Still running after the timeout — re-run: percher wait-for-deploy ${deployId} --app ${appName}`);
119053
119398
  throw new SilentExitError(1);
119054
119399
  }
119055
119400
  if (result.deployment.status !== lastStatus) {
119056
119401
  lastStatus = result.deployment.status;
119057
- await writeLine(ctx, import_picocolors4.default.dim(`${lastStatus}...`));
119402
+ await writeLine2(ctx, import_picocolors4.default.dim(`${lastStatus}...`));
119058
119403
  }
119059
119404
  if (result.status === "live") {
119060
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
119405
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
119061
119406
  return;
119062
119407
  }
119063
119408
  if (result.status === "failed" || result.status === "replaced") {
119064
119409
  const mark = result.status === "failed" ? import_picocolors4.default.red("✗") : import_picocolors4.default.yellow("!");
119065
- await writeLine(ctx, `${mark} ${result.summary}`);
119410
+ await writeLine2(ctx, `${mark} ${result.summary}`);
119066
119411
  if (result.error?.suggestion) {
119067
- await writeLine(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
119412
+ await writeLine2(ctx, ` ${import_picocolors4.default.dim(result.error.suggestion)}`);
119068
119413
  }
119069
119414
  if (result.status === "failed")
119070
119415
  throw new SilentExitError(1);
@@ -119080,49 +119425,49 @@ function registerAppsCommands(cli, ctx) {
119080
119425
  noCache: options.cache === false
119081
119426
  });
119082
119427
  if (result.dryRun) {
119083
- await writeLine(ctx, `Dry run: ${result.fileCount} files, ${(result.bytes / 1024).toFixed(1)} KB`);
119428
+ await writeLine2(ctx, `Dry run: ${result.fileCount} files, ${(result.bytes / 1024).toFixed(1)} KB`);
119084
119429
  } else {
119085
119430
  const url2 = result.deployment?.previewUrl ?? result.deployment?.url ?? result.app.url;
119086
119431
  const label = result.deployment?.previewUrl ? "preview live at" : "deployed to";
119087
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${label} ${url2}`);
119432
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} ${label} ${url2}`);
119088
119433
  const status2 = result.deployment?.cacheStatus ?? (result.deployment?.cacheHit === true ? "image_reused" : result.deployment?.cacheHit === false ? "fresh" : null);
119089
119434
  if (status2 === "image_reused") {
119090
- await writeLine(ctx, import_picocolors4.default.dim(" Build cache: hit (reused image — re-run with --no-cache if your changes aren't reflected)"));
119435
+ await writeLine2(ctx, import_picocolors4.default.dim(" Build cache: hit (reused image — re-run with --no-cache if your changes aren't reflected)"));
119091
119436
  } else if (status2 === "layer_hit") {
119092
- await writeLine(ctx, import_picocolors4.default.dim(" Build cache: layer hit (Docker reused unchanged layers — full nixpacks/build skipped)"));
119437
+ await writeLine2(ctx, import_picocolors4.default.dim(" Build cache: layer hit (Docker reused unchanged layers — full nixpacks/build skipped)"));
119093
119438
  } else if (status2 === "fresh") {
119094
- await writeLine(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
119439
+ await writeLine2(ctx, import_picocolors4.default.dim(" Build cache: miss (fresh build)"));
119095
119440
  }
119096
119441
  if (typeof result.deployment?.imageSizeBytes === "number") {
119097
- await writeLine(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
119442
+ await writeLine2(ctx, import_picocolors4.default.dim(` Image size: ${formatBytes(result.deployment.imageSizeBytes)}`));
119098
119443
  }
119099
119444
  }
119100
119445
  });
119101
119446
  cli.command("rename <name> [app]", "Rename an app (changes its subdomain)").action(async (name, app) => {
119102
119447
  const result = await rename2(ctx, { app, name });
119103
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} Renamed to ${result.name} — now live at ${result.url}`);
119104
- await writeLine(ctx, ` Previous name: ${result.previousName} (reserved for 60 days)`);
119448
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} Renamed to ${result.name} — now live at ${result.url}`);
119449
+ await writeLine2(ctx, ` Previous name: ${result.previousName} (reserved for 60 days)`);
119105
119450
  if (result.tomlUpdated) {
119106
- await writeLine(ctx, ` percher.toml updated: name = "${result.name}"`);
119451
+ await writeLine2(ctx, ` percher.toml updated: name = "${result.name}"`);
119107
119452
  } else {
119108
- await writeLine(ctx, ` ${import_picocolors4.default.yellow("!")} Update percher.toml before the next publish: name = "${result.name}"`);
119453
+ await writeLine2(ctx, ` ${import_picocolors4.default.yellow("!")} Update percher.toml before the next publish: name = "${result.name}"`);
119109
119454
  }
119110
- await writeLine(ctx, ` ${import_picocolors4.default.dim(`Next rename available in ${result.cooldownDays} days`)}`);
119455
+ await writeLine2(ctx, ` ${import_picocolors4.default.dim(`Next rename available in ${result.cooldownDays} days`)}`);
119111
119456
  });
119112
119457
  cli.command("open [app]", "Open the app URL in your browser").action(async (app) => {
119113
119458
  const url2 = await open(ctx, { app });
119114
- await writeLine(ctx, url2);
119459
+ await writeLine2(ctx, url2);
119115
119460
  });
119116
119461
  cli.command("dashboard [tab]", "Open the dashboard for this app (optionally on a specific tab)").option("--app <name>", "App name (overrides percher.toml)").action(async (tab, options) => {
119117
119462
  const url2 = await dashboard(ctx, { app: options.app, tab });
119118
- await writeLine(ctx, url2);
119463
+ await writeLine2(ctx, url2);
119119
119464
  });
119120
119465
  cli.command("create [name]", "Create a new app from a template").option("--template <template>", "Template to use (node, python, static, nextjs, hono, express)").option("--force", "Overwrite existing files in target directory").option("--list-templates", "List available templates").action(async (name, options) => {
119121
119466
  if (options.listTemplates) {
119122
119467
  const templates = listTemplates();
119123
- await writeLine(ctx, import_picocolors4.default.bold("Available templates:"));
119468
+ await writeLine2(ctx, import_picocolors4.default.bold("Available templates:"));
119124
119469
  for (const t of templates) {
119125
- await writeLine(ctx, ` ${t}`);
119470
+ await writeLine2(ctx, ` ${t}`);
119126
119471
  }
119127
119472
  return;
119128
119473
  }
@@ -119131,7 +119476,7 @@ function registerAppsCommands(cli, ctx) {
119131
119476
  }
119132
119477
  if (!options.template) {
119133
119478
  const templates = listTemplates();
119134
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} --template is required. Available: ${templates.join(", ")}`);
119479
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} --template is required. Available: ${templates.join(", ")}`);
119135
119480
  return;
119136
119481
  }
119137
119482
  const result = await create(ctx, {
@@ -119139,41 +119484,41 @@ function registerAppsCommands(cli, ctx) {
119139
119484
  template: options.template,
119140
119485
  force: options.force
119141
119486
  });
119142
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} created ${result.name} from ${result.template}`);
119487
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} created ${result.name} from ${result.template}`);
119143
119488
  for (const f of result.files) {
119144
- await writeLine(ctx, ` ${import_picocolors4.default.dim(f)}`);
119489
+ await writeLine2(ctx, ` ${import_picocolors4.default.dim(f)}`);
119145
119490
  }
119146
- await writeLine(ctx, "");
119147
- await writeLine(ctx, "Next steps:");
119148
- await writeLine(ctx, ` cd ${result.name}`);
119149
- await writeLine(ctx, " bunx percher publish");
119491
+ await writeLine2(ctx, "");
119492
+ await writeLine2(ctx, "Next steps:");
119493
+ await writeLine2(ctx, ` cd ${result.name}`);
119494
+ await writeLine2(ctx, " bunx percher publish");
119150
119495
  });
119151
119496
  cli.command("delete [app]", "Delete an app permanently").option("--yes", "Skip confirmation prompt").action(async (app, options) => {
119152
119497
  const name = app ?? readPercherTomlAppName(ctx.cwd);
119153
119498
  if (!name) {
119154
- await writeLine(ctx, `${import_picocolors4.default.red("✗")} No app specified and no percher.toml found.`);
119499
+ await writeLine2(ctx, `${import_picocolors4.default.red("✗")} No app specified and no percher.toml found.`);
119155
119500
  throw new SilentExitError;
119156
119501
  }
119157
119502
  if (!options.yes) {
119158
119503
  const answer = await ctx.prompt(`${import_picocolors4.default.red("!")} This will permanently delete ${import_picocolors4.default.bold(name)} and all its data. Type the app name to confirm:`);
119159
119504
  if (answer.trim() !== name) {
119160
- await writeLine(ctx, "Cancelled.");
119505
+ await writeLine2(ctx, "Cancelled.");
119161
119506
  return;
119162
119507
  }
119163
119508
  }
119164
119509
  await deleteApp(ctx, { app: name, confirm: true });
119165
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} Deleted ${name}`);
119510
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} Deleted ${name}`);
119166
119511
  });
119167
119512
  cli.command("resume [app]", "Resume an app suspended for inactivity").action(async (app) => {
119168
119513
  const result = await resume(ctx, { app });
119169
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
119514
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
119170
119515
  });
119171
119516
  cli.command("claim", "Claim an anonymous app into your account").option("--app <name>", "App name (defaults to the persisted anon state or percher.toml)").option("--claim-token <token>", "Claim token from the anonymous publish").action(async (options) => {
119172
119517
  const result = await claim(ctx, { app: options.app, claimToken: options.claimToken });
119173
119518
  if (result.status === "claimed") {
119174
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
119519
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
119175
119520
  } else {
119176
- await writeLine(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
119521
+ await writeLine2(ctx, `${import_picocolors4.default.yellow("!")} ${result.summary}`);
119177
119522
  }
119178
119523
  });
119179
119524
  cli.command("inspect-link [app]", "Create a public read-only inspect link (or --revoke it)").option("--app <name>", "App name (defaults to percher.toml)").option("--revoke", "Revoke the app's inspect link instead of creating one").action(async (app, options) => {
@@ -119181,49 +119526,49 @@ function registerAppsCommands(cli, ctx) {
119181
119526
  app: app ?? options.app,
119182
119527
  revoke: options.revoke
119183
119528
  });
119184
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
119529
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} ${result.summary}`);
119185
119530
  });
119186
119531
  cli.command("cache <action> [app]", "Manage the app's build cache (reset)").action(async (action, app) => {
119187
119532
  if (action !== "reset") {
119188
119533
  throw new Error(`Unknown cache action "${action}". Use: percher cache reset [app]`);
119189
119534
  }
119190
119535
  const result = await cacheReset(ctx, { app });
119191
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
119536
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
119192
119537
  });
119193
119538
  cli.command("cache reset [app]", "Reset the app's build cache (next deploy runs cold)").action(async (app) => {
119194
119539
  const result = await cacheReset(ctx, { app });
119195
- await writeLine(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
119540
+ await writeLine2(ctx, `${import_picocolors4.default.green("✓")} ${result.message}`);
119196
119541
  });
119197
119542
  cli.command("doctor [app]", "Check app health and connectivity").option("--json", "Output as JSON").action(async (app, options) => {
119198
119543
  const result = await doctor(ctx, { app, json: options.json });
119199
119544
  if (options.json) {
119200
- await writeLine(ctx, JSON.stringify(result, null, 2));
119545
+ await writeLine2(ctx, JSON.stringify(result, null, 2));
119201
119546
  } else {
119202
- await writeLine(ctx, `${import_picocolors4.default.bold("Percher Doctor")}`);
119203
- await writeLine(ctx, "──────────────");
119204
- await writeLine(ctx, "");
119547
+ await writeLine2(ctx, `${import_picocolors4.default.bold("Percher Doctor")}`);
119548
+ await writeLine2(ctx, "──────────────");
119549
+ await writeLine2(ctx, "");
119205
119550
  for (const check2 of result.checks) {
119206
119551
  const icon = check2.status === "pass" ? import_picocolors4.default.green("✓") : check2.status === "fail" ? import_picocolors4.default.red("✗") : check2.status === "warn" ? import_picocolors4.default.yellow("!") : import_picocolors4.default.dim("—");
119207
- await writeLine(ctx, `${icon} ${import_picocolors4.default.bold(check2.name.padEnd(20))} ${check2.message}`);
119552
+ await writeLine2(ctx, `${icon} ${import_picocolors4.default.bold(check2.name.padEnd(20))} ${check2.message}`);
119208
119553
  }
119209
- await writeLine(ctx, "");
119554
+ await writeLine2(ctx, "");
119210
119555
  const counter = `${result.passed}/${result.total} checks passed`;
119211
119556
  const counterColor = result.status === "ok" ? import_picocolors4.default.green : result.status === "in_progress" ? import_picocolors4.default.yellow : import_picocolors4.default.red;
119212
- await writeLine(ctx, counterColor(counter));
119213
- await writeLine(ctx, result.summary);
119557
+ await writeLine2(ctx, counterColor(counter));
119558
+ await writeLine2(ctx, result.summary);
119214
119559
  if (result.suspectDeploy) {
119215
119560
  const s = result.suspectDeploy;
119216
119561
  const head = (s.commitSha ?? s.deployId).slice(0, 10);
119217
- await writeLine(ctx, "");
119218
- await writeLine(ctx, import_picocolors4.default.bold(`Likely broken by a deploy (${s.confidence} confidence):`));
119219
- await writeLine(ctx, ` Suspect: ${head}`);
119562
+ await writeLine2(ctx, "");
119563
+ await writeLine2(ctx, import_picocolors4.default.bold(`Likely broken by a deploy (${s.confidence} confidence):`));
119564
+ await writeLine2(ctx, ` Suspect: ${head}`);
119220
119565
  if (s.lastHealthyCommitSha) {
119221
119566
  const base = s.lastHealthyCommitSha.slice(0, 10);
119222
- await writeLine(ctx, ` Last working: ${base}`);
119223
- await writeLine(ctx, import_picocolors4.default.dim(` See what changed: percher diff ${base} ${head}${app ? ` --app ${app}` : ""}`));
119567
+ await writeLine2(ctx, ` Last working: ${base}`);
119568
+ await writeLine2(ctx, import_picocolors4.default.dim(` See what changed: percher diff ${base} ${head}${app ? ` --app ${app}` : ""}`));
119224
119569
  }
119225
119570
  for (const e of s.evidence)
119226
- await writeLine(ctx, import_picocolors4.default.dim(` · ${e}`));
119571
+ await writeLine2(ctx, import_picocolors4.default.dim(` · ${e}`));
119227
119572
  }
119228
119573
  }
119229
119574
  if (result.status === "blocked" || result.status === "needs_action") {
@@ -119235,15 +119580,15 @@ function registerAppsCommands(cli, ctx) {
119235
119580
  const onSignal = () => controller.abort();
119236
119581
  process.on("SIGINT", onSignal);
119237
119582
  process.on("SIGTERM", onSignal);
119238
- await writeLine(ctx, import_picocolors4.default.bold("Percher Dev"));
119239
- await writeLine(ctx, "───────────");
119240
- await writeLine(ctx, "");
119583
+ await writeLine2(ctx, import_picocolors4.default.bold("Percher Dev"));
119584
+ await writeLine2(ctx, "───────────");
119585
+ await writeLine2(ctx, "");
119241
119586
  try {
119242
119587
  await dev(ctx, { app }, {
119243
119588
  signal: controller.signal,
119244
119589
  async writeLine(line) {
119245
119590
  const timestamp = new Date().toLocaleTimeString("sv-SE", { hour12: false });
119246
- await writeLine(ctx, `${import_picocolors4.default.dim(`[${timestamp}]`)} ${line}`);
119591
+ await writeLine2(ctx, `${import_picocolors4.default.dim(`[${timestamp}]`)} ${line}`);
119247
119592
  },
119248
119593
  async onFilesChanged(_files) {
119249
119594
  if (options.confirm) {
@@ -119257,8 +119602,8 @@ function registerAppsCommands(cli, ctx) {
119257
119602
  process.removeListener("SIGINT", onSignal);
119258
119603
  process.removeListener("SIGTERM", onSignal);
119259
119604
  }
119260
- await writeLine(ctx, "");
119261
- await writeLine(ctx, import_picocolors4.default.dim("Dev session ended."));
119605
+ await writeLine2(ctx, "");
119606
+ await writeLine2(ctx, import_picocolors4.default.dim("Dev session ended."));
119262
119607
  });
119263
119608
  }
119264
119609
 
@@ -119270,11 +119615,11 @@ function registerAuthCommands(cli, ctx) {
119270
119615
  });
119271
119616
  cli.command("whoami", "Show the signed-in user").action(async () => {
119272
119617
  if (!ctx.client.token) {
119273
- await writeLine(ctx, `${import_picocolors5.default.red("✗")} Not logged in. Run: percher login`);
119618
+ await writeLine2(ctx, `${import_picocolors5.default.red("✗")} Not logged in. Run: percher login`);
119274
119619
  throw new SilentExitError;
119275
119620
  }
119276
119621
  const me = await whoami(ctx);
119277
- await writeLine(ctx, `Signed in as ${me.email} (${me.userId})`);
119622
+ await writeLine2(ctx, `Signed in as ${me.email} (${me.userId})`);
119278
119623
  });
119279
119624
  }
119280
119625
 
@@ -119297,9 +119642,9 @@ function registerDataCommands(cli, ctx, argv) {
119297
119642
  const out = outIdx >= 0 ? argv[outIdx + 1] : undefined;
119298
119643
  const result2 = await dataExport(ctx, { app: realApp, out });
119299
119644
  const mb = (result2.bytes / 1024 / 1024).toFixed(1);
119300
- await writeLine(ctx, `${import_picocolors6.default.green("✓")} Exported ${result2.app}`);
119301
- await writeLine(ctx, ` Path: ${result2.path}`);
119302
- await writeLine(ctx, ` Size: ${mb} MB`);
119645
+ await writeLine2(ctx, `${import_picocolors6.default.green("✓")} Exported ${result2.app}`);
119646
+ await writeLine2(ctx, ` Path: ${result2.path}`);
119647
+ await writeLine2(ctx, ` Size: ${mb} MB`);
119303
119648
  return;
119304
119649
  }
119305
119650
  if (app === "oauth") {
@@ -119313,48 +119658,48 @@ function registerDataCommands(cli, ctx, argv) {
119313
119658
  remove: options.remove
119314
119659
  });
119315
119660
  if (result2.action === "removed") {
119316
- await writeLine(ctx, `${import_picocolors6.default.green("✓")} Removed ${result2.provider} sign-in.`);
119661
+ await writeLine2(ctx, `${import_picocolors6.default.green("✓")} Removed ${result2.provider} sign-in.`);
119317
119662
  } else if (result2.action === "set") {
119318
- await writeLine(ctx, `${import_picocolors6.default.green("✓")} ${result2.provider} sign-in enabled.`);
119319
- await writeLine(ctx, "");
119320
- await writeLine(ctx, import_picocolors6.default.bold("One more step — add this redirect URI in the provider console:"));
119321
- await writeLine(ctx, ` ${result2.redirectUri}`);
119663
+ await writeLine2(ctx, `${import_picocolors6.default.green("✓")} ${result2.provider} sign-in enabled.`);
119664
+ await writeLine2(ctx, "");
119665
+ await writeLine2(ctx, import_picocolors6.default.bold("One more step — add this redirect URI in the provider console:"));
119666
+ await writeLine2(ctx, ` ${result2.redirectUri}`);
119322
119667
  } else {
119323
119668
  if (!result2.providers?.length) {
119324
- await writeLine(ctx, import_picocolors6.default.dim("No OAuth providers configured."));
119669
+ await writeLine2(ctx, import_picocolors6.default.dim("No OAuth providers configured."));
119325
119670
  } else {
119326
- await writeLine(ctx, import_picocolors6.default.bold("Configured providers"));
119671
+ await writeLine2(ctx, import_picocolors6.default.bold("Configured providers"));
119327
119672
  for (const p of result2.providers) {
119328
- await writeLine(ctx, ` ${p.provider.padEnd(12)} ${import_picocolors6.default.dim(p.clientId)}`);
119673
+ await writeLine2(ctx, ` ${p.provider.padEnd(12)} ${import_picocolors6.default.dim(p.clientId)}`);
119329
119674
  }
119330
119675
  }
119331
- await writeLine(ctx, "");
119332
- await writeLine(ctx, `Redirect URI: ${result2.redirectUri}`);
119676
+ await writeLine2(ctx, "");
119677
+ await writeLine2(ctx, `Redirect URI: ${result2.redirectUri}`);
119333
119678
  }
119334
119679
  return;
119335
119680
  }
119336
119681
  const result = await data(ctx, { app, json: options.json });
119337
119682
  if (options.json) {
119338
- await writeLine(ctx, JSON.stringify(result, null, 2));
119683
+ await writeLine2(ctx, JSON.stringify(result, null, 2));
119339
119684
  } else {
119340
119685
  const version2 = result.version ? ` Version: ${result.version}` : "";
119341
- await writeLine(ctx, `${import_picocolors6.default.bold("Data")} Mode: ${result.mode} Status: ${result.status}${version2}`);
119342
- await writeLine(ctx, "");
119686
+ await writeLine2(ctx, `${import_picocolors6.default.bold("Data")} Mode: ${result.mode} Status: ${result.status}${version2}`);
119687
+ await writeLine2(ctx, "");
119343
119688
  if (result.collections.length > 0) {
119344
- await writeLine(ctx, import_picocolors6.default.bold("Collections"));
119689
+ await writeLine2(ctx, import_picocolors6.default.bold("Collections"));
119345
119690
  for (const col of result.collections) {
119346
- await writeLine(ctx, ` ${col.name.padEnd(20)} ${import_picocolors6.default.dim(col.type)}`);
119691
+ await writeLine2(ctx, ` ${col.name.padEnd(20)} ${import_picocolors6.default.dim(col.type)}`);
119347
119692
  }
119348
- await writeLine(ctx, "");
119693
+ await writeLine2(ctx, "");
119349
119694
  }
119350
- await writeLine(ctx, `Collections: ${result.stats.collections} Records: ${result.stats.totalRecords}`);
119695
+ await writeLine2(ctx, `Collections: ${result.stats.collections} Records: ${result.stats.totalRecords}`);
119351
119696
  if (result.adminUrl) {
119352
- await writeLine(ctx, "");
119353
- await writeLine(ctx, `Admin: ${result.adminUrl}`);
119697
+ await writeLine2(ctx, "");
119698
+ await writeLine2(ctx, `Admin: ${result.adminUrl}`);
119354
119699
  }
119355
119700
  if (result.warning) {
119356
- await writeLine(ctx, "");
119357
- await writeLine(ctx, import_picocolors6.default.yellow(`⚠ ${result.warning}`));
119701
+ await writeLine2(ctx, "");
119702
+ await writeLine2(ctx, import_picocolors6.default.yellow(`⚠ ${result.warning}`));
119358
119703
  }
119359
119704
  }
119360
119705
  });
@@ -119367,19 +119712,19 @@ function registerDataCommands(cli, ctx, argv) {
119367
119712
  cli.command("backups [app]", "List available backups for an app").action(async (app) => {
119368
119713
  const { backups, skipped } = await listBackups(ctx, { app });
119369
119714
  if (backups.length === 0) {
119370
- await writeLine(ctx, `${import_picocolors6.default.dim("No backups found.")}`);
119715
+ await writeLine2(ctx, `${import_picocolors6.default.dim("No backups found.")}`);
119371
119716
  if (skipped.length > 0) {
119372
- await writeLine(ctx, import_picocolors6.default.yellow(`(${skipped.length} sidecar(s) skipped — see ops logs)`));
119717
+ await writeLine2(ctx, import_picocolors6.default.yellow(`(${skipped.length} sidecar(s) skipped — see ops logs)`));
119373
119718
  }
119374
119719
  return;
119375
119720
  }
119376
119721
  for (const b of backups) {
119377
119722
  const components = b.apps[0]?.components.join(", ") ?? "—";
119378
119723
  const size = b.tarball_size_bytes < 1024 * 1024 ? `${Math.round(b.tarball_size_bytes / 1024)} KB` : `${(b.tarball_size_bytes / (1024 * 1024)).toFixed(1)} MB`;
119379
- await writeLine(ctx, `${import_picocolors6.default.green("●")} ${import_picocolors6.default.bold(b.backup_id)} ${import_picocolors6.default.dim(size.padStart(10))} ${import_picocolors6.default.dim(`[${components}]`)}`);
119724
+ await writeLine2(ctx, `${import_picocolors6.default.green("●")} ${import_picocolors6.default.bold(b.backup_id)} ${import_picocolors6.default.dim(size.padStart(10))} ${import_picocolors6.default.dim(`[${components}]`)}`);
119380
119725
  }
119381
119726
  if (skipped.length > 0) {
119382
- await writeLine(ctx, import_picocolors6.default.yellow(`
119727
+ await writeLine2(ctx, import_picocolors6.default.yellow(`
119383
119728
  ${skipped.length} sidecar(s) skipped — older backups predating the manifest schema.`));
119384
119729
  }
119385
119730
  });
@@ -119392,14 +119737,14 @@ ${skipped.length} sidecar(s) skipped — older backups predating the manifest sc
119392
119737
  dryRun: options.dryRun ?? false
119393
119738
  });
119394
119739
  if ("kind" in result) {
119395
- await writeLine(ctx, `${import_picocolors6.default.cyan("dry-run")} would restore ${import_picocolors6.default.bold(result.app)} from ${import_picocolors6.default.bold(result.resolvedBackup.backup_id)}`);
119396
- await writeLine(ctx, ` components: ${import_picocolors6.default.dim(result.components.join(", "))}`);
119397
- await writeLine(ctx, ` sha: ${import_picocolors6.default.dim(`${result.resolvedBackup.tarball_sha256.slice(0, 16)}…`)}`);
119398
- await writeLine(ctx, import_picocolors6.default.dim(`
119740
+ await writeLine2(ctx, `${import_picocolors6.default.cyan("dry-run")} would restore ${import_picocolors6.default.bold(result.app)} from ${import_picocolors6.default.bold(result.resolvedBackup.backup_id)}`);
119741
+ await writeLine2(ctx, ` components: ${import_picocolors6.default.dim(result.components.join(", "))}`);
119742
+ await writeLine2(ctx, ` sha: ${import_picocolors6.default.dim(`${result.resolvedBackup.tarball_sha256.slice(0, 16)}…`)}`);
119743
+ await writeLine2(ctx, import_picocolors6.default.dim(`
119399
119744
  Run without --dry-run to actually restore. Expect 30–120s of downtime.`));
119400
119745
  return;
119401
119746
  }
119402
- await writeLine(ctx, `${import_picocolors6.default.green("✓")} restored from ${result.sourceBackupId} (job ${result.id.slice(0, 12)})`);
119747
+ await writeLine2(ctx, `${import_picocolors6.default.green("✓")} restored from ${result.sourceBackupId} (job ${result.id.slice(0, 12)})`);
119403
119748
  });
119404
119749
  }
119405
119750
 
@@ -119420,10 +119765,10 @@ function registerDeploysCommands(cli, ctx, argv) {
119420
119765
  const limit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
119421
119766
  const list = await deploysList(ctx, { app, limit });
119422
119767
  if (list.length === 0) {
119423
- await writeLine(ctx, import_picocolors7.default.dim("No deploys yet."));
119768
+ await writeLine2(ctx, import_picocolors7.default.dim("No deploys yet."));
119424
119769
  return;
119425
119770
  }
119426
- await writeLine(ctx, `${import_picocolors7.default.bold("ID".padEnd(22))} ${import_picocolors7.default.bold("STATUS".padEnd(10))} ${import_picocolors7.default.bold("CREATED".padEnd(20))} ${import_picocolors7.default.bold("ERROR")}`);
119771
+ await writeLine2(ctx, `${import_picocolors7.default.bold("ID".padEnd(22))} ${import_picocolors7.default.bold("STATUS".padEnd(10))} ${import_picocolors7.default.bold("CREATED".padEnd(20))} ${import_picocolors7.default.bold("ERROR")}`);
119427
119772
  for (const d of list) {
119428
119773
  const id = d.id.padEnd(22);
119429
119774
  const status2 = d.status.padEnd(10);
@@ -119431,15 +119776,19 @@ function registerDeploysCommands(cli, ctx, argv) {
119431
119776
  const err = d.errorMessage ? d.errorMessage.split(`
119432
119777
  `)[0]?.slice(0, 60) ?? "—" : import_picocolors7.default.dim("—");
119433
119778
  const colored = d.status === "live" ? import_picocolors7.default.green(status2) : d.status === "failed" ? import_picocolors7.default.red(status2) : import_picocolors7.default.yellow(status2);
119434
- await writeLine(ctx, `${import_picocolors7.default.dim(id)} ${colored} ${import_picocolors7.default.dim(created)} ${err}`);
119779
+ await writeLine2(ctx, `${import_picocolors7.default.dim(id)} ${colored} ${import_picocolors7.default.dim(created)} ${err}`);
119435
119780
  }
119436
119781
  });
119437
119782
  cli.command("deploys inspect [id]", "Inspect a deploy: status, error, build log").option("--app <name>", "App name (defaults to percher.toml in cwd)").option("--latest-failed", "Resolve to the most recent failed deploy").action(async (id, options) => {
119438
119783
  await runDeploysInspect(ctx, { id, app: options.app, latestFailed: options.latestFailed });
119439
119784
  });
119440
- cli.command("rollback <sha> [app]", "Roll back to a previous version").action(async (sha, app) => {
119441
- const result = await rollback(ctx, { app, commitSha: sha });
119442
- await writeLine(ctx, `${import_picocolors7.default.green("✓")} rolled back to ${sha.slice(0, 10)} (status: ${result.status})`);
119785
+ cli.command("rollback [sha] [app]", "Roll back to a previous version (bare: pick from the 5 most recent)").option("--last-good", "Roll back to the most recent version that went live without crash-looping").option("--app <name>", "App name (defaults to percher.toml in cwd)").action(async (sha, app, options) => {
119786
+ const result = await rollback(ctx, {
119787
+ app: options.app ?? app,
119788
+ commitSha: sha,
119789
+ toLastGood: Boolean(options.lastGood)
119790
+ });
119791
+ await writeLine2(ctx, `${import_picocolors7.default.green("✓")} rolled back to ${result.rolledBackTo.slice(0, 10)} (status: ${result.status})`);
119443
119792
  });
119444
119793
  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) => {
119445
119794
  const result = await reproduce(ctx, {
@@ -119447,27 +119796,27 @@ function registerDeploysCommands(cli, ctx, argv) {
119447
119796
  deployId
119448
119797
  });
119449
119798
  if (result.status === "needs_login") {
119450
- await writeLine(ctx, `${import_picocolors7.default.red("✗")} Not logged in. Run: percher login`);
119799
+ await writeLine2(ctx, `${import_picocolors7.default.red("✗")} Not logged in. Run: percher login`);
119451
119800
  throw new SilentExitError;
119452
119801
  }
119453
119802
  if (result.status === "failed") {
119454
- await writeLine(ctx, `${import_picocolors7.default.red("✗")} ${result.error?.title ?? "Reproduce failed"}`);
119803
+ await writeLine2(ctx, `${import_picocolors7.default.red("✗")} ${result.error?.title ?? "Reproduce failed"}`);
119455
119804
  if (result.error?.explanation) {
119456
- await writeLine(ctx, ` ${result.error.explanation}`);
119805
+ await writeLine2(ctx, ` ${result.error.explanation}`);
119457
119806
  }
119458
119807
  if (result.error?.suggestion) {
119459
- await writeLine(ctx, ` ${import_picocolors7.default.dim(result.error.suggestion)}`);
119808
+ await writeLine2(ctx, ` ${import_picocolors7.default.dim(result.error.suggestion)}`);
119460
119809
  }
119461
119810
  throw new SilentExitError;
119462
119811
  }
119463
- await writeLine(ctx, `${import_picocolors7.default.green("✓")} ${result.summary}`);
119812
+ await writeLine2(ctx, `${import_picocolors7.default.green("✓")} ${result.summary}`);
119464
119813
  if (result.currentAppEnvKeys && result.currentAppEnvKeys.length > 0) {
119465
- await writeLine(ctx, "");
119466
- await writeLine(ctx, import_picocolors7.default.bold("Current app env keys:"));
119814
+ await writeLine2(ctx, "");
119815
+ await writeLine2(ctx, import_picocolors7.default.bold("Current app env keys:"));
119467
119816
  for (const k of result.currentAppEnvKeys) {
119468
- await writeLine(ctx, ` ${k}`);
119817
+ await writeLine2(ctx, ` ${k}`);
119469
119818
  }
119470
- await writeLine(ctx, import_picocolors7.default.dim(" (values not exfiltrated — set them locally before building. Note: this is the CURRENT app env, not a deploy-time snapshot.)"));
119819
+ await writeLine2(ctx, import_picocolors7.default.dim(" (values not exfiltrated — set them locally before building. Note: this is the CURRENT app env, not a deploy-time snapshot.)"));
119471
119820
  }
119472
119821
  });
119473
119822
  cli.command("logs [app]", "Stream application logs").option("--tail <n>", "Return the last N lines and exit", { type: [Number] }).option("--no-follow", "Don't stream new lines").option("--build [deployId]", "Show the BUILD log instead of runtime logs (defaults to latest failed deploy)").action(async (app, options) => {
@@ -119479,30 +119828,30 @@ function registerDeploysCommands(cli, ctx, argv) {
119479
119828
  latestFailed: deployId ? undefined : true
119480
119829
  });
119481
119830
  const { deploy, buildLog } = result;
119482
- await writeLine(ctx, `${import_picocolors7.default.bold("Deploy:")} ${deploy.id}`);
119483
- await writeLine(ctx, `${import_picocolors7.default.bold("Status:")} ${formatStatus(deploy.status)}`);
119484
- await writeLine(ctx, `${import_picocolors7.default.bold("Created:")} ${formatDeployTimestamp(deploy.createdAt)}`);
119831
+ await writeLine2(ctx, `${import_picocolors7.default.bold("Deploy:")} ${deploy.id}`);
119832
+ await writeLine2(ctx, `${import_picocolors7.default.bold("Status:")} ${formatStatus(deploy.status)}`);
119833
+ await writeLine2(ctx, `${import_picocolors7.default.bold("Created:")} ${formatDeployTimestamp(deploy.createdAt)}`);
119485
119834
  if (deploy.errorMessage) {
119486
- await writeLine(ctx, "");
119487
- await writeLine(ctx, import_picocolors7.default.bold(import_picocolors7.default.red("Error:")));
119835
+ await writeLine2(ctx, "");
119836
+ await writeLine2(ctx, import_picocolors7.default.bold(import_picocolors7.default.red("Error:")));
119488
119837
  for (const line of deploy.errorMessage.split(`
119489
119838
  `)) {
119490
- await writeLine(ctx, ` ${line}`);
119839
+ await writeLine2(ctx, ` ${line}`);
119491
119840
  }
119492
119841
  }
119493
119842
  if (buildLog) {
119494
- await writeLine(ctx, "");
119843
+ await writeLine2(ctx, "");
119495
119844
  const lines = buildLog.split(`
119496
119845
  `);
119497
119846
  const tailN = options.tail ? Number(options.tail) : 200;
119498
119847
  const tail = lines.slice(-tailN);
119499
119848
  const header = lines.length > tail.length ? `${import_picocolors7.default.bold("Build log")} ${import_picocolors7.default.dim(`(last ${tail.length} of ${lines.length} lines)`)}` : import_picocolors7.default.bold("Build log");
119500
- await writeLine(ctx, header);
119849
+ await writeLine2(ctx, header);
119501
119850
  for (const line of tail) {
119502
- await writeLine(ctx, ` ${line}`);
119851
+ await writeLine2(ctx, ` ${line}`);
119503
119852
  }
119504
119853
  } else {
119505
- await writeLine(ctx, import_picocolors7.default.dim("(no build log available for this deploy)"));
119854
+ await writeLine2(ctx, import_picocolors7.default.dim("(no build log available for this deploy)"));
119506
119855
  }
119507
119856
  return;
119508
119857
  }
@@ -119526,7 +119875,7 @@ function registerDeploysCommands(cli, ctx, argv) {
119526
119875
  };
119527
119876
  const label = v.channel ? channelLabels[v.channel] ?? v.channel : null;
119528
119877
  const suffix = label ? ` · via ${label}` : "";
119529
- await writeLine(ctx, `${v.sha} ${v.timestamp} ${message}${suffix}`);
119878
+ await writeLine2(ctx, `${v.sha} ${v.timestamp} ${message}${suffix}`);
119530
119879
  }
119531
119880
  });
119532
119881
  cli.command("diff [base] [head]", "Show what changed between two deploy versions").option("--app <name>", "App name (overrides percher.toml)").option("--patch", "Include the unified diff per file (owner-only)").action(async (base, head, options) => {
@@ -119537,23 +119886,27 @@ function registerDeploysCommands(cli, ctx, argv) {
119537
119886
  patch: options.patch
119538
119887
  });
119539
119888
  const { summary, files } = result;
119540
- await writeLine(ctx, `${summary.filesChanged} file${summary.filesChanged === 1 ? "" : "s"} changed · ${import_picocolors7.default.green(`+${summary.additions}`)} ${import_picocolors7.default.red(`−${summary.deletions}`)} (${result.base.slice(0, 10)} → ${result.head.slice(0, 10)})`);
119889
+ await writeLine2(ctx, `${summary.filesChanged} file${summary.filesChanged === 1 ? "" : "s"} changed · ${import_picocolors7.default.green(`+${summary.additions}`)} ${import_picocolors7.default.red(`−${summary.deletions}`)} (${result.base.slice(0, 10)} → ${result.head.slice(0, 10)})`);
119541
119890
  for (const f of files) {
119542
119891
  const stat2 = f.binary ? import_picocolors7.default.dim("binary") : `${import_picocolors7.default.green(`+${f.additions}`)} ${import_picocolors7.default.red(`−${f.deletions}`)}`;
119543
- await writeLine(ctx, ` ${f.status} ${f.path} ${stat2}${f.truncated ? import_picocolors7.default.dim(" (truncated)") : ""}`);
119892
+ await writeLine2(ctx, ` ${f.status} ${f.path} ${stat2}${f.truncated ? import_picocolors7.default.dim(" (truncated)") : ""}`);
119544
119893
  if (options.patch && f.patch) {
119545
- await writeLine(ctx, f.patch.replace(/\n$/, ""));
119894
+ await writeLine2(ctx, f.patch.replace(/\n$/, ""));
119546
119895
  }
119547
119896
  }
119548
119897
  if (result.truncated) {
119549
- await writeLine(ctx, import_picocolors7.default.dim("… some patches omitted (response size limit)"));
119898
+ await writeLine2(ctx, import_picocolors7.default.dim("… some patches omitted (response size limit)"));
119550
119899
  }
119551
119900
  });
119552
- cli.command("preview <action> [arg] [arg2]", "Preview deploys: promote <id> | discard <id> | branch set <branch> | branch unset | branch show").option("--app <name>", "App name (overrides percher.toml)").action(async (action, arg, arg2, options) => {
119901
+ cli.command("preview <action> [arg] [arg2]", "Preview deploys: list | promote <id> | discard <id> | branch set <branch> | branch unset | branch show").option("--app <name>", "App name (overrides percher.toml)").action(async (action, arg, arg2, options) => {
119553
119902
  if (action === "branch") {
119554
119903
  await runPreviewBranch(arg, arg2, options.app);
119555
119904
  return;
119556
119905
  }
119906
+ if (action === "list") {
119907
+ await runPreviewList(options.app);
119908
+ return;
119909
+ }
119557
119910
  const appRef = options.app ?? readPercherTomlAppName(ctx.cwd);
119558
119911
  if (!appRef) {
119559
119912
  throw new Error("App name required. Use --app or run from a project directory.");
@@ -119562,43 +119915,59 @@ function registerDeploysCommands(cli, ctx, argv) {
119562
119915
  if (!arg)
119563
119916
  throw new Error("Deploy ID required. Usage: percher preview promote <id>");
119564
119917
  const result = await ctx.client.apps.promotePreview(appRef, arg);
119565
- await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview promoted to live. Status: ${result.status}`);
119918
+ await writeLine2(ctx, `${import_picocolors7.default.green("✓")} Preview promoted to live. Status: ${result.status}`);
119566
119919
  } else if (action === "discard") {
119567
119920
  if (!arg)
119568
119921
  throw new Error("Deploy ID required. Usage: percher preview discard <id>");
119569
119922
  await ctx.client.apps.discardPreview(appRef, arg);
119570
- await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview discarded.`);
119923
+ await writeLine2(ctx, `${import_picocolors7.default.green("✓")} Preview discarded.`);
119571
119924
  } else {
119572
- throw new Error(`Unknown preview action "${action}". Use promote | discard | branch.`);
119925
+ throw new Error(`Unknown preview action "${action}". Use list | promote | discard | branch.`);
119573
119926
  }
119574
119927
  });
119928
+ async function runPreviewList(app) {
119929
+ const { app: appName, previews } = await previewList(ctx, { app });
119930
+ await writeLine2(ctx, `Preview surfaces for ${import_picocolors7.default.bold(appName)}:`);
119931
+ if (previews.length === 0) {
119932
+ await writeLine2(ctx, import_picocolors7.default.dim(" (no preview surfaces reported by the server)"));
119933
+ return;
119934
+ }
119935
+ for (const p of previews) {
119936
+ const label = p.kind.padEnd(14);
119937
+ const statusColour = p.status === "ready" || p.status === "enabled" || p.status === "available" || p.status === "active" ? import_picocolors7.default.green(p.status) : p.status === "error" ? import_picocolors7.default.red(p.status) : import_picocolors7.default.dim(p.status);
119938
+ await writeLine2(ctx, ` ${import_picocolors7.default.bold(label)} ${statusColour}`);
119939
+ if (p.url)
119940
+ await writeLine2(ctx, ` url: ${p.url}`);
119941
+ await writeLine2(ctx, import_picocolors7.default.dim(` how: ${p.howTriggered}`));
119942
+ }
119943
+ }
119575
119944
  async function runPreviewBranch(sub, branch, app) {
119576
119945
  if (sub === "set") {
119577
119946
  if (!branch)
119578
119947
  throw new Error("Branch required. Usage: percher preview branch set <branch>");
119579
119948
  const r = await previewBranchSet(ctx, { app, branch });
119580
- await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview branch set to "${branch}".`);
119949
+ await writeLine2(ctx, `${import_picocolors7.default.green("✓")} Preview branch set to "${branch}".`);
119581
119950
  if (r.previewUrl)
119582
- await writeLine(ctx, ` Your preview address: ${import_picocolors7.default.bold(r.previewUrl)}`);
119583
- await writeLine(ctx, r.status === "error" ? import_picocolors7.default.yellow(" ⚠ The initial deploy failed — push to the branch to retry.") : import_picocolors7.default.dim(" Building now — live within ~90s."));
119951
+ await writeLine2(ctx, ` Your preview address: ${import_picocolors7.default.bold(r.previewUrl)}`);
119952
+ await writeLine2(ctx, r.status === "error" ? import_picocolors7.default.yellow(" ⚠ The initial deploy failed — push to the branch to retry.") : import_picocolors7.default.dim(" Building now — live within ~90s."));
119584
119953
  return;
119585
119954
  }
119586
119955
  if (sub === "unset") {
119587
119956
  await previewBranchUnset(ctx, { app });
119588
- await writeLine(ctx, `${import_picocolors7.default.green("✓")} Preview branch cleared.`);
119957
+ await writeLine2(ctx, `${import_picocolors7.default.green("✓")} Preview branch cleared.`);
119589
119958
  return;
119590
119959
  }
119591
119960
  if (sub === "show" || sub === undefined) {
119592
119961
  const s = await previewBranchShow(ctx, { app });
119593
119962
  if (!s.previewBranch) {
119594
- await writeLine(ctx, "No preview branch set.");
119595
- await writeLine(ctx, import_picocolors7.default.dim(" Set one: percher preview branch set <branch>"));
119963
+ await writeLine2(ctx, "No preview branch set.");
119964
+ await writeLine2(ctx, import_picocolors7.default.dim(" Set one: percher preview branch set <branch>"));
119596
119965
  return;
119597
119966
  }
119598
- await writeLine(ctx, `Preview branch: ${import_picocolors7.default.bold(s.previewBranch)}`);
119967
+ await writeLine2(ctx, `Preview branch: ${import_picocolors7.default.bold(s.previewBranch)}`);
119599
119968
  if (s.previewUrl)
119600
- await writeLine(ctx, ` Address: ${s.previewUrl}`);
119601
- await writeLine(ctx, ` Status: ${s.status}`);
119969
+ await writeLine2(ctx, ` Address: ${s.previewUrl}`);
119970
+ await writeLine2(ctx, ` Status: ${s.status}`);
119602
119971
  return;
119603
119972
  }
119604
119973
  throw new Error(`Unknown preview branch action "${sub}". Use set <branch> | unset | show.`);
@@ -119607,8 +119976,25 @@ function registerDeploysCommands(cli, ctx, argv) {
119607
119976
 
119608
119977
  // src/commands/env.ts
119609
119978
  var import_picocolors8 = __toESM(require_picocolors(), 1);
119979
+ var ENV_SCOPES = ["all", "live", "preview"];
119980
+ function isEnvScope(value) {
119981
+ return ENV_SCOPES.includes(value);
119982
+ }
119610
119983
  function registerEnvCommands(cli, ctx) {
119611
- cli.command("env <action> [...args]", "Manage env vars (set KEY=VALUE | set KEY --from-stdin | set KEY --from-file <path> | set --from-env-file <path> | set KEY (prompts) | unset KEY | list)").option("--app <name>", "App name (overrides percher.toml)").option("--from-stdin", "(set) Read value for the single KEY from stdin").option("--from-file <path>", "(set) Read value for the single KEY from file").option("--from-env-file <path>", "(set) Set every KEY=VALUE line from a .env-format file").action(async (action, args, options) => {
119984
+ cli.command("env <action> [...args]", "Manage env vars (set KEY=VALUE | set KEY --from-stdin | set KEY --from-file <path> | set --from-env-file <path> | set KEY (prompts) | unset KEY | list). set/unset take --scope all|live|preview").option("--app <name>", "App name (overrides percher.toml)").option("--from-stdin", "(set) Read value for the single KEY from stdin").option("--from-file <path>", "(set) Read value for the single KEY from file").option("--from-env-file <path>", "(set) Set every KEY=VALUE line from a .env-format file").option("--scope <scope>", "(set/unset) all (default) applies everywhere; live/preview apply only to that deploy type and override all").action(async (action, args, options) => {
119985
+ let scope;
119986
+ if (options.scope !== undefined) {
119987
+ const raw = String(options.scope);
119988
+ if (!isEnvScope(raw)) {
119989
+ await writeLine2(ctx, `${import_picocolors8.default.red("✗")} Invalid --scope "${raw}". Use all, live, or preview.`);
119990
+ throw new SilentExitError;
119991
+ }
119992
+ if (action === "list") {
119993
+ await writeLine2(ctx, `${import_picocolors8.default.red("✗")} --scope is not supported with "list" — env list already shows every scope ([live]/[preview] annotations).`);
119994
+ throw new SilentExitError;
119995
+ }
119996
+ scope = raw;
119997
+ }
119612
119998
  if (action === "set") {
119613
119999
  const { resolveEnvSetInputs: resolveEnvSetInputs2, EnvSetInputError: EnvSetInputError2 } = await Promise.resolve().then(() => (init_env_set_input(), exports_env_set_input));
119614
120000
  const { promptHidden: promptHidden2 } = await Promise.resolve().then(() => (init_prompt_hidden(), exports_prompt_hidden));
@@ -119628,11 +120014,12 @@ function registerEnvCommands(cli, ctx) {
119628
120014
  readFile: async (p) => readFile5(p, "utf8"),
119629
120015
  promptHidden: promptHidden2
119630
120016
  });
119631
- await envSet(ctx, { app: options.app, values });
119632
- await writeLine(ctx, `${import_picocolors8.default.green("✓")} env vars updated (${Object.keys(values).length})`);
120017
+ await envSet(ctx, { app: options.app, values, scope });
120018
+ const scopeNote = scope && scope !== "all" ? ` [${scope}]` : "";
120019
+ await writeLine2(ctx, `${import_picocolors8.default.green("✓")} env vars updated (${Object.keys(values).length})${scopeNote}`);
119633
120020
  } catch (err) {
119634
120021
  if (err instanceof EnvSetInputError2) {
119635
- await writeLine(ctx, `${import_picocolors8.default.red("✗")} ${err.message}`);
120022
+ await writeLine2(ctx, `${import_picocolors8.default.red("✗")} ${err.message}`);
119636
120023
  throw new SilentExitError;
119637
120024
  }
119638
120025
  throw err;
@@ -119641,12 +120028,14 @@ function registerEnvCommands(cli, ctx) {
119641
120028
  const key = args[0];
119642
120029
  if (!key)
119643
120030
  throw new Error("Expected key to unset");
119644
- await envUnset(ctx, { app: options.app, key });
119645
- await writeLine(ctx, `${import_picocolors8.default.green("✓")} unset ${key}`);
120031
+ await envUnset(ctx, { app: options.app, key, scope });
120032
+ const scopeNote = scope && scope !== "all" ? ` [${scope}]` : "";
120033
+ await writeLine2(ctx, `${import_picocolors8.default.green("✓")} unset ${key}${scopeNote}`);
119646
120034
  } else if (action === "list") {
119647
120035
  const env = await envList(ctx, { app: options.app });
119648
- for (const [k, v] of Object.entries(env)) {
119649
- await writeLine(ctx, `${k}=${v}`);
120036
+ for (const row of env.detailed) {
120037
+ const suffix = row.scope === "all" ? "" : ` [${row.scope}]`;
120038
+ await writeLine2(ctx, `${row.key}=${row.maskedValue}${suffix}`);
119650
120039
  }
119651
120040
  } else {
119652
120041
  throw new Error(`Unknown env action "${action}". Use set | unset | list.`);
@@ -119655,7 +120044,7 @@ function registerEnvCommands(cli, ctx) {
119655
120044
  cli.command("generate", "Generate .env.example and TypeScript types from percher.toml").action(async () => {
119656
120045
  const result = await generate(ctx);
119657
120046
  for (const f of result.files) {
119658
- await writeLine(ctx, `${import_picocolors8.default.green("✓")} ${f}`);
120047
+ await writeLine2(ctx, `${import_picocolors8.default.green("✓")} ${f}`);
119659
120048
  }
119660
120049
  });
119661
120050
  }
@@ -119666,8 +120055,8 @@ function registerExportCommands(cli, ctx) {
119666
120055
  cli.command("export [app]", "Download an export bundle for an app (no secrets)").option("--out <path>", "Where to write the file (defaults to ./{name}-{date}.tar.gz)").option("--git", "Export the whole deploy history as a git bundle (.bundle) — re-clone with `git clone`").action(async (app, options) => {
119667
120056
  if (options.git) {
119668
120057
  const result2 = await exportGitBundle(ctx, { app, out: options.out });
119669
- await writeLine(ctx, `${import_picocolors9.default.green("✓")} exported ${result2.appName} deploy history → ${result2.outPath} (${(result2.bytes / 1024).toFixed(1)} KB)`);
119670
- await writeLine(ctx, import_picocolors9.default.dim(`Re-clone it anywhere with: git clone ${result2.outPath}`));
120058
+ await writeLine2(ctx, `${import_picocolors9.default.green("✓")} exported ${result2.appName} deploy history → ${result2.outPath} (${(result2.bytes / 1024).toFixed(1)} KB)`);
120059
+ await writeLine2(ctx, import_picocolors9.default.dim(`Re-clone it anywhere with: git clone ${result2.outPath}`));
119671
120060
  return;
119672
120061
  }
119673
120062
  const result = await exportApp(ctx, {
@@ -119675,27 +120064,27 @@ function registerExportCommands(cli, ctx) {
119675
120064
  out: options.out,
119676
120065
  deliveryKind: "cli"
119677
120066
  });
119678
- await writeLine(ctx, `${import_picocolors9.default.green("✓")} exported ${result.appName} → ${result.outPath} (${(result.bytes / 1024).toFixed(1)} KB)`);
119679
- 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."));
120067
+ await writeLine2(ctx, `${import_picocolors9.default.green("✓")} exported ${result.appName} → ${result.outPath} (${(result.bytes / 1024).toFixed(1)} KB)`);
120068
+ await writeLine2(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."));
119680
120069
  });
119681
120070
  }
119682
120071
 
119683
120072
  // src/commands/github.ts
119684
120073
  var import_picocolors10 = __toESM(require_picocolors(), 1);
119685
120074
  async function printWebhookInstructions(ctx, instructions, branch) {
119686
- await writeLine(ctx, import_picocolors10.default.bold("Add this webhook in GitHub to enable auto-deploy on push:"));
119687
- await writeLine(ctx, ` URL: ${instructions.settingsUrl}`);
119688
- await writeLine(ctx, "");
119689
- await writeLine(ctx, " Field Value");
119690
- await writeLine(ctx, " ─────────────────────────────────────────────────");
119691
- await writeLine(ctx, ` Payload URL ${instructions.payloadUrl}`);
119692
- await writeLine(ctx, " Content type application/json");
119693
- await writeLine(ctx, ` Secret ${instructions.secret}`);
119694
- await writeLine(ctx, " Which events? Just the push event");
119695
- await writeLine(ctx, "");
119696
- await writeLine(ctx, import_picocolors10.default.yellow(" ⚠ Treat this secret like a password — save it now, it won't be shown again."));
119697
- await writeLine(ctx, "");
119698
- await writeLine(ctx, import_picocolors10.default.dim(`After adding the webhook, every push to ${branch} will auto-deploy.`));
120075
+ await writeLine2(ctx, import_picocolors10.default.bold("Add this webhook in GitHub to enable auto-deploy on push:"));
120076
+ await writeLine2(ctx, ` URL: ${instructions.settingsUrl}`);
120077
+ await writeLine2(ctx, "");
120078
+ await writeLine2(ctx, " Field Value");
120079
+ await writeLine2(ctx, " ─────────────────────────────────────────────────");
120080
+ await writeLine2(ctx, ` Payload URL ${instructions.payloadUrl}`);
120081
+ await writeLine2(ctx, " Content type application/json");
120082
+ await writeLine2(ctx, ` Secret ${instructions.secret}`);
120083
+ await writeLine2(ctx, " Which events? Just the push event");
120084
+ await writeLine2(ctx, "");
120085
+ await writeLine2(ctx, import_picocolors10.default.yellow(" ⚠ Treat this secret like a password — save it now, it won't be shown again."));
120086
+ await writeLine2(ctx, "");
120087
+ await writeLine2(ctx, import_picocolors10.default.dim(`After adding the webhook, every push to ${branch} will auto-deploy.`));
119699
120088
  }
119700
120089
  function registerGithubCommands(cli, ctx) {
119701
120090
  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) => {
@@ -119708,25 +120097,25 @@ function registerGithubCommands(cli, ctx) {
119708
120097
  repoUrl,
119709
120098
  branch: options.branch
119710
120099
  });
119711
- await writeLine(ctx, `${import_picocolors10.default.green("✓")} Connected to ${result.repoUrl} (branch: ${result.branch})`);
119712
- await writeLine(ctx, `${import_picocolors10.default.green("✓")} Deploy started: ${result.deployId}`);
119713
- await writeLine(ctx, "");
120100
+ await writeLine2(ctx, `${import_picocolors10.default.green("✓")} Connected to ${result.repoUrl} (branch: ${result.branch})`);
120101
+ await writeLine2(ctx, `${import_picocolors10.default.green("✓")} Deploy started: ${result.deployId}`);
120102
+ await writeLine2(ctx, "");
119714
120103
  if (result.webhookSetupFailed) {
119715
- await writeLine(ctx, import_picocolors10.default.yellow(`⚠ Webhook setup failed: ${result.webhookSetupError ?? "unknown error"}`));
119716
- await writeLine(ctx, import_picocolors10.default.yellow(" Auto-deploy will NOT work until you re-run webhook setup:"));
119717
- await writeLine(ctx, ` ${import_picocolors10.default.bold(`percher github setup-webhook${options.app ? ` --app ${options.app}` : ""}`)}`);
120104
+ await writeLine2(ctx, import_picocolors10.default.yellow(`⚠ Webhook setup failed: ${result.webhookSetupError ?? "unknown error"}`));
120105
+ await writeLine2(ctx, import_picocolors10.default.yellow(" Auto-deploy will NOT work until you re-run webhook setup:"));
120106
+ await writeLine2(ctx, ` ${import_picocolors10.default.bold(`percher github setup-webhook${options.app ? ` --app ${options.app}` : ""}`)}`);
119718
120107
  return;
119719
120108
  }
119720
120109
  if (result.webhookInstructions) {
119721
120110
  await printWebhookInstructions(ctx, result.webhookInstructions, result.branch);
119722
120111
  }
119723
- await writeLine(ctx, import_picocolors10.default.dim(`Track the initial deploy: percher deploys inspect ${result.deployId}`));
120112
+ await writeLine2(ctx, import_picocolors10.default.dim(`Track the initial deploy: percher deploys inspect ${result.deployId}`));
119724
120113
  return;
119725
120114
  }
119726
120115
  if (action === "setup-webhook") {
119727
120116
  const result = await githubSetupWebhook(ctx, { app: options.app });
119728
- await writeLine(ctx, `${import_picocolors10.default.green("✓")} Webhook regenerated for ${result.app}`);
119729
- await writeLine(ctx, "");
120117
+ await writeLine2(ctx, `${import_picocolors10.default.green("✓")} Webhook regenerated for ${result.app}`);
120118
+ await writeLine2(ctx, "");
119730
120119
  await printWebhookInstructions(ctx, result.webhookInstructions, result.branch);
119731
120120
  return;
119732
120121
  }
@@ -119736,8 +120125,64 @@ function registerGithubCommands(cli, ctx) {
119736
120125
  cli.command("github setup-webhook", "Regenerate the GitHub webhook secret for an app (use after a failed `github connect`)").option("--app <name>", "App name (overrides percher.toml)").action(async () => {});
119737
120126
  }
119738
120127
 
119739
- // src/commands/migrate.ts
120128
+ // src/commands/hooks.ts
119740
120129
  var import_picocolors11 = __toESM(require_picocolors(), 1);
120130
+ function whenLabel(iso) {
120131
+ return iso ? new Date(iso).toISOString().slice(0, 10) : "never";
120132
+ }
120133
+ function registerHooksCommands(cli, ctx) {
120134
+ cli.command("hook [action] [arg]", "Manage deploy hooks: create | list | revoke").option("--app <name>", "App name (default: read from percher.toml)").action(async (action, arg, options) => {
120135
+ if (!ctx.client.token) {
120136
+ await writeLine2(ctx, `${import_picocolors11.default.red("✗")} Not logged in. Run: percher login`);
120137
+ throw new SilentExitError;
120138
+ }
120139
+ if (action === "create") {
120140
+ if (!arg) {
120141
+ await writeLine2(ctx, `${import_picocolors11.default.red("✗")} A name is required. Usage: percher hook create <name> [--app <name>]`);
120142
+ throw new SilentExitError;
120143
+ }
120144
+ const created = await hookCreate(ctx, { name: arg, app: options.app });
120145
+ await writeLine2(ctx, `${import_picocolors11.default.green("✓")} Deploy hook created: ${import_picocolors11.default.bold(created.name)}`);
120146
+ await writeLine2(ctx, "");
120147
+ await writeLine2(ctx, ` ${import_picocolors11.default.bold(created.hookUrl)}`);
120148
+ await writeLine2(ctx, import_picocolors11.default.yellow(" ↑ Shown once — store it in your CMS/CI secret store now. It can't be retrieved again."));
120149
+ await writeLine2(ctx, import_picocolors11.default.yellow(" The URL is a secret: anyone holding it can trigger builds (6/min)."));
120150
+ await writeLine2(ctx, "");
120151
+ await writeLine2(ctx, import_picocolors11.default.dim(" Trigger a rebuild with:"));
120152
+ await writeLine2(ctx, import_picocolors11.default.dim(` curl -X POST ${created.hookUrl}`));
120153
+ return;
120154
+ }
120155
+ if (action === "list") {
120156
+ const hooks = await hookList(ctx, { app: options.app });
120157
+ if (hooks.length === 0) {
120158
+ await writeLine2(ctx, import_picocolors11.default.dim("No deploy hooks. Create one with: percher hook create <name>"));
120159
+ return;
120160
+ }
120161
+ for (const h of hooks) {
120162
+ await writeLine2(ctx, `${import_picocolors11.default.bold(h.id)} ${h.name.padEnd(20)} ${import_picocolors11.default.dim(`created ${whenLabel(h.createdAt)}`)} ${import_picocolors11.default.dim(`last used ${whenLabel(h.lastUsedAt)}`)}`);
120163
+ }
120164
+ await writeLine2(ctx, "");
120165
+ await writeLine2(ctx, import_picocolors11.default.dim("Revoke one with: percher hook revoke <id>"));
120166
+ return;
120167
+ }
120168
+ if (action === "revoke") {
120169
+ if (!arg) {
120170
+ await writeLine2(ctx, `${import_picocolors11.default.red("✗")} A hook id is required. Usage: percher hook revoke <id> (list ids with \`percher hook list\`).`);
120171
+ throw new SilentExitError;
120172
+ }
120173
+ await hookRevoke(ctx, { id: arg, app: options.app });
120174
+ await writeLine2(ctx, `${import_picocolors11.default.green("✓")} Deploy hook revoked.`);
120175
+ return;
120176
+ }
120177
+ throw new Error(`Unknown hook action "${action ?? ""}". Use: create | list | revoke.`);
120178
+ });
120179
+ cli.command("hook create <name>", "Create a deploy hook (the URL is shown once)").option("--app <name>", "App name (default: read from percher.toml)").action(async () => {});
120180
+ cli.command("hook list", "List deploy hooks with created + last-used dates").option("--app <name>", "App name (default: read from percher.toml)").action(async () => {});
120181
+ cli.command("hook revoke <id>", "Revoke a deploy hook by id").option("--app <name>", "App name (default: read from percher.toml)").action(async () => {});
120182
+ }
120183
+
120184
+ // src/commands/migrate.ts
120185
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
119741
120186
  function registerMigrateCommands(cli, ctx) {
119742
120187
  cli.command("migrate-from-supabase", "Migrate a Supabase project to PocketBase: schema + data-import script, optionally rewrite SDK calls").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", "Only print the schema that would migrate — read-only, no file writes").option("--output-dir <dir>", "Where to write the preview files (default: ./migration-preview/)").option("--rewrite-client", "Also convert your Supabase SDK calls to PocketBase across the project").option("--apply", "With --rewrite-client: overwrite files in place (originals backed up first). Default: preview only.").option("--root <dir>", "Project root to rewrite with --rewrite-client (default: current directory)").option("--no-scaffold-client", "With --rewrite-client: don't generate a PocketBase client module / insert pb imports (default: scaffold one)").option("--status", "Show a migration scoreboard for the local project — what's still on Supabase vs PocketBase. Local scan, no Supabase token needed.").option("--build", "With --status: also run the project build as a final gate").option("--pb-identifier <name>", "With --status: the PocketBase client identifier to check for (default: pb). Match what the rewrite used.").option("--json", "Output the structured result as JSON").action(async (options) => {
119743
120188
  if (options.status) {
@@ -119747,7 +120192,7 @@ function registerMigrateCommands(cli, ctx) {
119747
120192
  pbIdentifier: options.pbIdentifier
119748
120193
  });
119749
120194
  if (options.json) {
119750
- await writeLine(ctx, JSON.stringify(result2, null, 2));
120195
+ await writeLine2(ctx, JSON.stringify(result2, null, 2));
119751
120196
  if (result2.status === "failed")
119752
120197
  throw new SilentExitError(1);
119753
120198
  return;
@@ -119760,21 +120205,21 @@ function registerMigrateCommands(cli, ctx) {
119760
120205
  const projectRef = options.project;
119761
120206
  const token = options.token ?? ctx.env.SUPABASE_ACCESS_TOKEN;
119762
120207
  if (options.apply && !options.rewriteClient) {
119763
- await writeLine(ctx, `${import_picocolors11.default.red("✗")} --apply only applies to the client rewrite. Pass --rewrite-client --apply together.`);
120208
+ await writeLine2(ctx, `${import_picocolors12.default.red("✗")} --apply only applies to the client rewrite. Pass --rewrite-client --apply together.`);
119764
120209
  throw new SilentExitError(1);
119765
120210
  }
119766
120211
  if (!projectRef) {
119767
- await writeLine(ctx, `${import_picocolors11.default.red("✗")} Missing --project. Pass your Supabase project ref (e.g. --project abcdefghijklmnopqrst).`);
120212
+ await writeLine2(ctx, `${import_picocolors12.default.red("✗")} Missing --project. Pass your Supabase project ref (e.g. --project abcdefghijklmnopqrst).`);
119768
120213
  throw new SilentExitError(1);
119769
120214
  }
119770
120215
  if (!token) {
119771
- 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`);
120216
+ await writeLine2(ctx, `${import_picocolors12.default.red("✗")} Missing --token. Pass --token sbp_... or set SUPABASE_ACCESS_TOKEN. Generate one at https://supabase.com/dashboard/account/tokens`);
119772
120217
  throw new SilentExitError(1);
119773
120218
  }
119774
120219
  if (options.inspectOnly) {
119775
120220
  const result2 = await inspectSupabase(ctx, { projectRef, token });
119776
120221
  if (options.json) {
119777
- await writeLine(ctx, JSON.stringify(result2, null, 2));
120222
+ await writeLine2(ctx, JSON.stringify(result2, null, 2));
119778
120223
  if (result2.status === "failed")
119779
120224
  throw new SilentExitError(1);
119780
120225
  return;
@@ -119798,7 +120243,7 @@ function registerMigrateCommands(cli, ctx) {
119798
120243
  });
119799
120244
  }
119800
120245
  if (options.json) {
119801
- await writeLine(ctx, JSON.stringify(rewrite ? { schema: result, rewrite } : result, null, 2));
120246
+ await writeLine2(ctx, JSON.stringify(rewrite ? { schema: result, rewrite } : result, null, 2));
119802
120247
  if (result.status === "failed" || rewrite?.status === "failed") {
119803
120248
  throw new SilentExitError(1);
119804
120249
  }
@@ -119817,183 +120262,183 @@ function registerMigrateCommands(cli, ctx) {
119817
120262
  async function renderInspectHuman(ctx, result) {
119818
120263
  if (result.status === "failed") {
119819
120264
  const err = result.error;
119820
- await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Inspection failed"}`);
120265
+ await writeLine2(ctx, `${import_picocolors12.default.red("✗")} ${err?.title ?? "Inspection failed"}`);
119821
120266
  if (err?.explanation)
119822
- await writeLine(ctx, ` ${err.explanation}`);
120267
+ await writeLine2(ctx, ` ${err.explanation}`);
119823
120268
  if (err?.suggestion)
119824
- await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
120269
+ await writeLine2(ctx, import_picocolors12.default.dim(` ${err.suggestion}`));
119825
120270
  return;
119826
120271
  }
119827
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${result.summary}`);
119828
- await writeLine(ctx, "");
119829
- await writeLine(ctx, import_picocolors11.default.bold("Tables"));
120272
+ await writeLine2(ctx, `${import_picocolors12.default.green("✓")} ${result.summary}`);
120273
+ await writeLine2(ctx, "");
120274
+ await writeLine2(ctx, import_picocolors12.default.bold("Tables"));
119830
120275
  if (result.tables.length === 0) {
119831
- await writeLine(ctx, import_picocolors11.default.dim(" (none in user schemas)"));
120276
+ await writeLine2(ctx, import_picocolors12.default.dim(" (none in user schemas)"));
119832
120277
  }
119833
120278
  for (const table of result.tables) {
119834
120279
  const pkCount = table.columns.filter((c) => c.isPrimaryKey).length;
119835
120280
  const fkCount = table.columns.filter((c) => c.foreignKey !== null).length;
119836
120281
  const policyCount = table.rlsPolicies.length;
119837
120282
  const rowEstimate = table.estimatedRowCount.toLocaleString();
119838
- 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}`)}`);
120283
+ await writeLine2(ctx, ` ${import_picocolors12.default.cyan(`${table.schema}.${table.name}`)} ${import_picocolors12.default.dim(`cols=${table.columns.length} pk=${pkCount} fk=${fkCount} rls=${policyCount} rows≈${rowEstimate}`)}`);
119839
120284
  }
119840
120285
  if (result.storageBuckets.length > 0) {
119841
- await writeLine(ctx, "");
119842
- await writeLine(ctx, import_picocolors11.default.bold("Storage buckets"));
120286
+ await writeLine2(ctx, "");
120287
+ await writeLine2(ctx, import_picocolors12.default.bold("Storage buckets"));
119843
120288
  for (const bucket of result.storageBuckets) {
119844
120289
  const visibility = bucket.public ? "public" : "private";
119845
- await writeLine(ctx, ` ${import_picocolors11.default.cyan(bucket.name)} ${import_picocolors11.default.dim(`(${visibility})`)}`);
120290
+ await writeLine2(ctx, ` ${import_picocolors12.default.cyan(bucket.name)} ${import_picocolors12.default.dim(`(${visibility})`)}`);
119846
120291
  }
119847
120292
  }
119848
120293
  if (result.authUsersCount !== null) {
119849
- await writeLine(ctx, "");
119850
- await writeLine(ctx, `${import_picocolors11.default.bold("Auth users")}: ${result.authUsersCount}`);
120294
+ await writeLine2(ctx, "");
120295
+ await writeLine2(ctx, `${import_picocolors12.default.bold("Auth users")}: ${result.authUsersCount}`);
119851
120296
  }
119852
120297
  if (result.warnings.length > 0) {
119853
- await writeLine(ctx, "");
119854
- await writeLine(ctx, import_picocolors11.default.bold(`Warnings (${result.warnings.length})`));
120298
+ await writeLine2(ctx, "");
120299
+ await writeLine2(ctx, import_picocolors12.default.bold(`Warnings (${result.warnings.length})`));
119855
120300
  for (const warning of result.warnings) {
119856
- await writeLine(ctx, ` ${import_picocolors11.default.yellow("⚠")} ${warning}`);
120301
+ await writeLine2(ctx, ` ${import_picocolors12.default.yellow("⚠")} ${warning}`);
119857
120302
  }
119858
120303
  }
119859
- await writeLine(ctx, "");
119860
- await writeLine(ctx, import_picocolors11.default.dim("--inspect-only is read-only. Drop the flag to also write the PocketBase schema preview + pb_migrate.js data-import script to ./migration-preview/."));
120304
+ await writeLine2(ctx, "");
120305
+ await writeLine2(ctx, import_picocolors12.default.dim("--inspect-only is read-only. Drop the flag to also write the PocketBase schema preview + pb_migrate.js data-import script to ./migration-preview/."));
119861
120306
  }
119862
120307
  async function renderSchemaHuman(ctx, result, opts) {
119863
120308
  if (result.status === "failed") {
119864
120309
  const err = result.error;
119865
- await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Migration preview failed"}`);
120310
+ await writeLine2(ctx, `${import_picocolors12.default.red("✗")} ${err?.title ?? "Migration preview failed"}`);
119866
120311
  if (err?.explanation)
119867
- await writeLine(ctx, ` ${err.explanation}`);
120312
+ await writeLine2(ctx, ` ${err.explanation}`);
119868
120313
  if (err?.suggestion)
119869
- await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
120314
+ await writeLine2(ctx, import_picocolors12.default.dim(` ${err.suggestion}`));
119870
120315
  return;
119871
120316
  }
119872
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${result.summary}`);
119873
- await writeLine(ctx, "");
119874
- await writeLine(ctx, import_picocolors11.default.bold("Files written"));
120317
+ await writeLine2(ctx, `${import_picocolors12.default.green("✓")} ${result.summary}`);
120318
+ await writeLine2(ctx, "");
120319
+ await writeLine2(ctx, import_picocolors12.default.bold("Files written"));
119875
120320
  for (const path of result.filesWritten) {
119876
- await writeLine(ctx, ` ${import_picocolors11.default.cyan(path)}`);
120321
+ await writeLine2(ctx, ` ${import_picocolors12.default.cyan(path)}`);
119877
120322
  }
119878
120323
  const preview = result.preview;
119879
120324
  if (!preview)
119880
120325
  return;
119881
120326
  if (preview.collections.length > 0) {
119882
- await writeLine(ctx, "");
119883
- await writeLine(ctx, import_picocolors11.default.bold("Collections"));
120327
+ await writeLine2(ctx, "");
120328
+ await writeLine2(ctx, import_picocolors12.default.bold("Collections"));
119884
120329
  for (const collection of preview.collections) {
119885
120330
  const fields = collection.schema.length;
119886
120331
  const relations = collection.schema.filter((f) => f.type === "relation").length;
119887
- await writeLine(ctx, ` ${import_picocolors11.default.cyan(collection.name)} ${import_picocolors11.default.dim(`← ${collection.sourceTable} fields=${fields} relations=${relations}`)}`);
120332
+ await writeLine2(ctx, ` ${import_picocolors12.default.cyan(collection.name)} ${import_picocolors12.default.dim(`← ${collection.sourceTable} fields=${fields} relations=${relations}`)}`);
119888
120333
  }
119889
120334
  }
119890
120335
  if (preview.warnings.length > 0) {
119891
- await writeLine(ctx, "");
119892
- await writeLine(ctx, import_picocolors11.default.bold(`Warnings (${preview.warnings.length})`));
120336
+ await writeLine2(ctx, "");
120337
+ await writeLine2(ctx, import_picocolors12.default.bold(`Warnings (${preview.warnings.length})`));
119893
120338
  for (const warning of preview.warnings.slice(0, 10)) {
119894
- await writeLine(ctx, ` ${import_picocolors11.default.yellow("⚠")} ${warning}`);
120339
+ await writeLine2(ctx, ` ${import_picocolors12.default.yellow("⚠")} ${warning}`);
119895
120340
  }
119896
120341
  if (preview.warnings.length > 10) {
119897
- await writeLine(ctx, import_picocolors11.default.dim(` …and ${preview.warnings.length - 10} more — see MIGRATION_NOTES.md`));
120342
+ await writeLine2(ctx, import_picocolors12.default.dim(` …and ${preview.warnings.length - 10} more — see MIGRATION_NOTES.md`));
119898
120343
  }
119899
120344
  }
119900
- await writeLine(ctx, "");
119901
- await writeLine(ctx, import_picocolors11.default.dim("Preview only — nothing has touched your PocketBase yet. ./migration-preview/ includes pb_migrate.js, the data-import script: review MIGRATION_NOTES.md, then run it against your app's PocketBase to copy your rows over."));
120345
+ await writeLine2(ctx, "");
120346
+ await writeLine2(ctx, import_picocolors12.default.dim("Preview only — nothing has touched your PocketBase yet. ./migration-preview/ includes pb_migrate.js, the data-import script: review MIGRATION_NOTES.md, then run it against your app's PocketBase to copy your rows over."));
119902
120347
  if (!opts.clientRewriteRequested) {
119903
- await writeLine(ctx, import_picocolors11.default.dim("Add --rewrite-client to also convert your Supabase SDK calls to PocketBase (preview first; --apply writes in place)."));
120348
+ await writeLine2(ctx, import_picocolors12.default.dim("Add --rewrite-client to also convert your Supabase SDK calls to PocketBase (preview first; --apply writes in place)."));
119904
120349
  }
119905
120350
  }
119906
120351
  async function renderRewriteHuman(ctx, result) {
119907
- await writeLine(ctx, "");
120352
+ await writeLine2(ctx, "");
119908
120353
  if (result.status === "failed") {
119909
120354
  const err = result.error;
119910
- await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Client rewrite failed"}`);
120355
+ await writeLine2(ctx, `${import_picocolors12.default.red("✗")} ${err?.title ?? "Client rewrite failed"}`);
119911
120356
  if (err?.explanation)
119912
- await writeLine(ctx, ` ${err.explanation}`);
120357
+ await writeLine2(ctx, ` ${err.explanation}`);
119913
120358
  if (err?.suggestion)
119914
- await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
120359
+ await writeLine2(ctx, import_picocolors12.default.dim(` ${err.suggestion}`));
119915
120360
  return;
119916
120361
  }
119917
- await writeLine(ctx, `${import_picocolors11.default.green("✓")} ${result.summary}`);
120362
+ await writeLine2(ctx, `${import_picocolors12.default.green("✓")} ${result.summary}`);
119918
120363
  if (result.filesWritten.length > 0) {
119919
- await writeLine(ctx, "");
119920
- await writeLine(ctx, import_picocolors11.default.bold("Files written"));
120364
+ await writeLine2(ctx, "");
120365
+ await writeLine2(ctx, import_picocolors12.default.bold("Files written"));
119921
120366
  for (const path of result.filesWritten.slice(0, 20)) {
119922
- await writeLine(ctx, ` ${import_picocolors11.default.cyan(path)}`);
120367
+ await writeLine2(ctx, ` ${import_picocolors12.default.cyan(path)}`);
119923
120368
  }
119924
120369
  if (result.filesWritten.length > 20) {
119925
- await writeLine(ctx, import_picocolors11.default.dim(` …and ${result.filesWritten.length - 20} more`));
120370
+ await writeLine2(ctx, import_picocolors12.default.dim(` …and ${result.filesWritten.length - 20} more`));
119926
120371
  }
119927
120372
  }
119928
120373
  if (result.flags.length > 0) {
119929
- await writeLine(ctx, "");
119930
- await writeLine(ctx, import_picocolors11.default.bold("Needs review (see migration-preview/REWRITE_NOTES.md)"));
120374
+ await writeLine2(ctx, "");
120375
+ await writeLine2(ctx, import_picocolors12.default.bold("Needs review (see migration-preview/REWRITE_NOTES.md)"));
119931
120376
  for (const flag of result.flags.slice(0, 15)) {
119932
120377
  const loc = flag.line !== undefined ? `:${flag.line}` : "";
119933
120378
  const tag = flag.kind === "manual_review" ? "rewritten, verify semantics" : "left untouched";
119934
- await writeLine(ctx, ` ${import_picocolors11.default.yellow("⚠")} ${import_picocolors11.default.cyan(`${flag.file}${loc}`)} ${flag.pattern} ${import_picocolors11.default.dim(`(${tag}) — ${flag.note}`)}`);
120379
+ await writeLine2(ctx, ` ${import_picocolors12.default.yellow("⚠")} ${import_picocolors12.default.cyan(`${flag.file}${loc}`)} ${flag.pattern} ${import_picocolors12.default.dim(`(${tag}) — ${flag.note}`)}`);
119935
120380
  }
119936
120381
  if (result.flags.length > 15) {
119937
- await writeLine(ctx, import_picocolors11.default.dim(` …and ${result.flags.length - 15} more — see REWRITE_NOTES.md`));
120382
+ await writeLine2(ctx, import_picocolors12.default.dim(` …and ${result.flags.length - 15} more — see REWRITE_NOTES.md`));
119938
120383
  }
119939
120384
  }
119940
120385
  if (result.skipped.length > 0) {
119941
- await writeLine(ctx, import_picocolors11.default.dim(`
120386
+ await writeLine2(ctx, import_picocolors12.default.dim(`
119942
120387
  ${result.skipped.length} file(s) couldn't be parsed and were skipped — listed in REWRITE_NOTES.md.`));
119943
120388
  }
119944
- await writeLine(ctx, "");
120389
+ await writeLine2(ctx, "");
119945
120390
  if (result.mode === "apply") {
119946
- await writeLine(ctx, import_picocolors11.default.dim(result.changedCount > 0 ? "Files rewritten in place. Originals are in ./migration-preview/backup/ — diff against them (or git) before committing." : "No files were rewritten — the work left is the manual review above."));
120391
+ await writeLine2(ctx, import_picocolors12.default.dim(result.changedCount > 0 ? "Files rewritten in place. Originals are in ./migration-preview/backup/ — diff against them (or git) before committing." : "No files were rewritten — the work left is the manual review above."));
119947
120392
  } else {
119948
- await writeLine(ctx, import_picocolors11.default.dim(result.changedCount > 0 ? "Preview only — diff ./migration-preview/rewritten/ against your project, then re-run with --rewrite-client --apply to write in place." : "Preview only — no files needed automatic rewriting; work through the manual review above."));
120393
+ await writeLine2(ctx, import_picocolors12.default.dim(result.changedCount > 0 ? "Preview only — diff ./migration-preview/rewritten/ against your project, then re-run with --rewrite-client --apply to write in place." : "Preview only — no files needed automatic rewriting; work through the manual review above."));
119949
120394
  }
119950
120395
  }
119951
120396
  async function renderStatusHuman(ctx, result) {
119952
120397
  if (result.status === "failed") {
119953
120398
  const err = result.error;
119954
- await writeLine(ctx, `${import_picocolors11.default.red("✗")} ${err?.title ?? "Migration status scan failed"}`);
120399
+ await writeLine2(ctx, `${import_picocolors12.default.red("✗")} ${err?.title ?? "Migration status scan failed"}`);
119955
120400
  if (err?.explanation)
119956
- await writeLine(ctx, ` ${err.explanation}`);
120401
+ await writeLine2(ctx, ` ${err.explanation}`);
119957
120402
  if (err?.suggestion)
119958
- await writeLine(ctx, import_picocolors11.default.dim(` ${err.suggestion}`));
120403
+ await writeLine2(ctx, import_picocolors12.default.dim(` ${err.suggestion}`));
119959
120404
  return;
119960
120405
  }
119961
- await writeLine(ctx, import_picocolors11.default.bold("Migration status"));
119962
- await writeLine(ctx, "");
120406
+ await writeLine2(ctx, import_picocolors12.default.bold("Migration status"));
120407
+ await writeLine2(ctx, "");
119963
120408
  for (const check2 of result.checks) {
119964
- const mark = check2.status === "pass" ? import_picocolors11.default.green("✓") : check2.status === "fail" ? import_picocolors11.default.red("✗") : check2.status === "warn" ? import_picocolors11.default.yellow("⚠") : import_picocolors11.default.dim("•");
119965
- const count = check2.count > 0 ? import_picocolors11.default.dim(` (${check2.count})`) : "";
119966
- await writeLine(ctx, ` ${mark} ${check2.label}${count} ${import_picocolors11.default.dim(`— ${check2.detail}`)}`);
120409
+ const mark = check2.status === "pass" ? import_picocolors12.default.green("✓") : check2.status === "fail" ? import_picocolors12.default.red("✗") : check2.status === "warn" ? import_picocolors12.default.yellow("⚠") : import_picocolors12.default.dim("•");
120410
+ const count = check2.count > 0 ? import_picocolors12.default.dim(` (${check2.count})`) : "";
120411
+ await writeLine2(ctx, ` ${mark} ${check2.label}${count} ${import_picocolors12.default.dim(`— ${check2.detail}`)}`);
119967
120412
  for (const file2 of (check2.files ?? []).slice(0, 10)) {
119968
- await writeLine(ctx, import_picocolors11.default.dim(` - ${file2}`));
120413
+ await writeLine2(ctx, import_picocolors12.default.dim(` - ${file2}`));
119969
120414
  }
119970
120415
  if ((check2.files?.length ?? 0) > 10) {
119971
- await writeLine(ctx, import_picocolors11.default.dim(` …and ${(check2.files?.length ?? 0) - 10} more`));
120416
+ await writeLine2(ctx, import_picocolors12.default.dim(` …and ${(check2.files?.length ?? 0) - 10} more`));
119972
120417
  }
119973
120418
  }
119974
120419
  if (result.build) {
119975
- await writeLine(ctx, "");
119976
- const mark = !result.build.ran ? import_picocolors11.default.dim("•") : result.build.passed ? import_picocolors11.default.green("✓") : import_picocolors11.default.red("✗");
119977
- await writeLine(ctx, ` ${mark} Build ${import_picocolors11.default.dim(`— ${result.build.detail}`)}`);
120420
+ await writeLine2(ctx, "");
120421
+ const mark = !result.build.ran ? import_picocolors12.default.dim("•") : result.build.passed ? import_picocolors12.default.green("✓") : import_picocolors12.default.red("✗");
120422
+ await writeLine2(ctx, ` ${mark} Build ${import_picocolors12.default.dim(`— ${result.build.detail}`)}`);
119978
120423
  }
119979
- await writeLine(ctx, "");
119980
- await writeLine(ctx, import_picocolors11.default.dim(result.summary));
120424
+ await writeLine2(ctx, "");
120425
+ await writeLine2(ctx, import_picocolors12.default.dim(result.summary));
119981
120426
  }
119982
120427
 
119983
120428
  // src/commands/sharing.ts
119984
- var import_picocolors12 = __toESM(require_picocolors(), 1);
120429
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
119985
120430
  function registerSharingCommands(cli, ctx) {
119986
120431
  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) => {
119987
120432
  if (action === "list" || action === undefined) {
119988
120433
  const rows = await shareList(ctx, { app: options.app });
119989
120434
  if (rows.length === 0) {
119990
- await writeLine(ctx, import_picocolors12.default.dim("No collaborators."));
120435
+ await writeLine2(ctx, import_picocolors13.default.dim("No collaborators."));
119991
120436
  return;
119992
120437
  }
119993
120438
  for (const r of rows) {
119994
- const state = r.accepted ? import_picocolors12.default.green("accepted") : import_picocolors12.default.yellow("pending");
119995
- const roleStr = r.role === "admin" ? import_picocolors12.default.cyan("admin") : import_picocolors12.default.dim("viewer");
119996
- await writeLine(ctx, `${r.id} ${roleStr.padEnd(15)} ${state.padEnd(15)} ${r.email}`);
120439
+ const state = r.accepted ? import_picocolors13.default.green("accepted") : import_picocolors13.default.yellow("pending");
120440
+ const roleStr = r.role === "admin" ? import_picocolors13.default.cyan("admin") : import_picocolors13.default.dim("viewer");
120441
+ await writeLine2(ctx, `${r.id} ${roleStr.padEnd(15)} ${state.padEnd(15)} ${r.email}`);
119997
120442
  }
119998
120443
  return;
119999
120444
  }
@@ -120010,8 +120455,8 @@ function registerSharingCommands(cli, ctx) {
120010
120455
  email: arg,
120011
120456
  role: roleRaw
120012
120457
  });
120013
- await writeLine(ctx, `${import_picocolors12.default.green("✓")} Invited ${created.email} as ${created.role} (id ${created.id})`);
120014
- await writeLine(ctx, import_picocolors12.default.dim(`Pending until accepted — invite expires ${new Date(created.expiresAt).toLocaleDateString()}.`));
120458
+ await writeLine2(ctx, `${import_picocolors13.default.green("✓")} Invited ${created.email} as ${created.role} (id ${created.id})`);
120459
+ await writeLine2(ctx, import_picocolors13.default.dim(`Pending until accepted — invite expires ${new Date(created.expiresAt).toLocaleDateString()}.`));
120015
120460
  return;
120016
120461
  }
120017
120462
  if (action === "role") {
@@ -120026,7 +120471,7 @@ function registerSharingCommands(cli, ctx) {
120026
120471
  shareId: arg,
120027
120472
  role: valueOrRole
120028
120473
  });
120029
- await writeLine(ctx, `${import_picocolors12.default.green("✓")} ${updated.email} is now ${import_picocolors12.default.cyan(updated.role)}`);
120474
+ await writeLine2(ctx, `${import_picocolors13.default.green("✓")} ${updated.email} is now ${import_picocolors13.default.cyan(updated.role)}`);
120030
120475
  return;
120031
120476
  }
120032
120477
  if (action === "revoke") {
@@ -120034,7 +120479,7 @@ function registerSharingCommands(cli, ctx) {
120034
120479
  throw new Error("Usage: percher share revoke <shareId> [--app <name>]");
120035
120480
  }
120036
120481
  await shareRevoke(ctx, { app: options.app, shareId: arg });
120037
- await writeLine(ctx, `${import_picocolors12.default.green("✓")} Revoked share ${arg}`);
120482
+ await writeLine2(ctx, `${import_picocolors13.default.green("✓")} Revoked share ${arg}`);
120038
120483
  return;
120039
120484
  }
120040
120485
  throw new Error(`Unknown share action "${action}". Use list | invite <email> | role <shareId> <role> | revoke <shareId>.`);
@@ -120047,12 +120492,12 @@ function registerSharingCommands(cli, ctx) {
120047
120492
  if (action === undefined || action === "list") {
120048
120493
  const pending = await invitesList(ctx, {});
120049
120494
  if (pending.length === 0) {
120050
- await writeLine(ctx, import_picocolors12.default.dim("No pending invites."));
120495
+ await writeLine2(ctx, import_picocolors13.default.dim("No pending invites."));
120051
120496
  return;
120052
120497
  }
120053
120498
  for (const inv of pending) {
120054
- const roleStr = inv.role === "admin" ? import_picocolors12.default.cyan("admin") : import_picocolors12.default.dim("viewer");
120055
- await writeLine(ctx, `${inv.id} ${roleStr.padEnd(15)} ${inv.appName.padEnd(24)} from ${inv.invitedBy}`);
120499
+ const roleStr = inv.role === "admin" ? import_picocolors13.default.cyan("admin") : import_picocolors13.default.dim("viewer");
120500
+ await writeLine2(ctx, `${inv.id} ${roleStr.padEnd(15)} ${inv.appName.padEnd(24)} from ${inv.invitedBy}`);
120056
120501
  }
120057
120502
  return;
120058
120503
  }
@@ -120062,10 +120507,10 @@ function registerSharingCommands(cli, ctx) {
120062
120507
  }
120063
120508
  const accepted = await invitesAccept(ctx, { shareId });
120064
120509
  if (accepted.alreadyAccepted) {
120065
- await writeLine(ctx, import_picocolors12.default.dim(`Invite ${shareId} was already accepted.`));
120510
+ await writeLine2(ctx, import_picocolors13.default.dim(`Invite ${shareId} was already accepted.`));
120066
120511
  return;
120067
120512
  }
120068
- await writeLine(ctx, `${import_picocolors12.default.green("✓")} Accepted — you now have ${import_picocolors12.default.cyan(accepted.role)} access to the shared app.`);
120513
+ await writeLine2(ctx, `${import_picocolors13.default.green("✓")} Accepted — you now have ${import_picocolors13.default.cyan(accepted.role)} access to the shared app.`);
120069
120514
  return;
120070
120515
  }
120071
120516
  if (action === "decline") {
@@ -120073,7 +120518,7 @@ function registerSharingCommands(cli, ctx) {
120073
120518
  throw new Error("Usage: percher invites decline <shareId>");
120074
120519
  }
120075
120520
  await invitesDecline(ctx, { shareId });
120076
- await writeLine(ctx, `${import_picocolors12.default.green("✓")} Declined invite ${shareId}.`);
120521
+ await writeLine2(ctx, `${import_picocolors13.default.green("✓")} Declined invite ${shareId}.`);
120077
120522
  return;
120078
120523
  }
120079
120524
  throw new Error(`Unknown invites action "${action}". Use list | accept <shareId> | decline <shareId>.`);
@@ -120084,12 +120529,12 @@ function registerSharingCommands(cli, ctx) {
120084
120529
  }
120085
120530
 
120086
120531
  // src/commands/status.ts
120087
- var import_picocolors13 = __toESM(require_picocolors(), 1);
120532
+ var import_picocolors14 = __toESM(require_picocolors(), 1);
120088
120533
  function registerStatusCommands(cli, ctx) {
120089
120534
  cli.command("status", "Show Percher platform status (API + worker health)").option("--json", "Output JSON instead of formatted text").action(async (options) => {
120090
120535
  const result = await status(ctx, { json: Boolean(options.json) });
120091
120536
  if (options.json) {
120092
- await writeLine(ctx, JSON.stringify(result, null, 2));
120537
+ await writeLine2(ctx, JSON.stringify(result, null, 2));
120093
120538
  if (result.verdict === "unreachable")
120094
120539
  throw new SilentExitError(1);
120095
120540
  return;
@@ -120100,34 +120545,34 @@ function registerStatusCommands(cli, ctx) {
120100
120545
  });
120101
120546
  }
120102
120547
  async function renderHumanStatus(ctx, result) {
120103
- const symbol2 = result.verdict === "operational" ? import_picocolors13.default.green("✓") : result.verdict === "worker-degraded" ? import_picocolors13.default.yellow("⚠") : import_picocolors13.default.red("✗");
120104
- await writeLine(ctx, `${symbol2} ${result.summary}`);
120548
+ const symbol2 = result.verdict === "operational" ? import_picocolors14.default.green("✓") : result.verdict === "worker-degraded" ? import_picocolors14.default.yellow("⚠") : import_picocolors14.default.red("✗");
120549
+ await writeLine2(ctx, `${symbol2} ${result.summary}`);
120105
120550
  if (!result.health)
120106
120551
  return;
120107
120552
  const worker = result.health.worker;
120108
120553
  if (!worker) {
120109
- await writeLine(ctx, import_picocolors13.default.dim(" worker: not configured (local Docker socket)"));
120554
+ await writeLine2(ctx, import_picocolors14.default.dim(" worker: not configured (local Docker socket)"));
120110
120555
  return;
120111
120556
  }
120112
120557
  if (worker.health) {
120113
120558
  const ok = worker.health.ok;
120114
- const symbol3 = ok ? import_picocolors13.default.green("✓") : import_picocolors13.default.red("✗");
120559
+ const symbol3 = ok ? import_picocolors14.default.green("✓") : import_picocolors14.default.red("✗");
120115
120560
  const lastOk = worker.health.lastOkAt ?? "never";
120116
- await writeLine(ctx, ` ${symbol3} worker health: ${ok ? "ok" : `failing (${worker.health.consecutiveErrs} consecutive)`} last-ok=${import_picocolors13.default.dim(lastOk)}`);
120561
+ await writeLine2(ctx, ` ${symbol3} worker health: ${ok ? "ok" : `failing (${worker.health.consecutiveErrs} consecutive)`} last-ok=${import_picocolors14.default.dim(lastOk)}`);
120117
120562
  }
120118
120563
  if (worker.circuitBreaker) {
120119
120564
  const breaker = worker.circuitBreaker;
120120
- const symbol3 = breaker.open ? import_picocolors13.default.red("✗") : import_picocolors13.default.green("✓");
120565
+ const symbol3 = breaker.open ? import_picocolors14.default.red("✗") : import_picocolors14.default.green("✓");
120121
120566
  const state = breaker.open ? "open (deploys fail fast)" : "closed";
120122
- await writeLine(ctx, ` ${symbol3} circuit breaker: ${state} ${breaker.failuresInWindow}/${breaker.threshold} failures in last ${Math.round(breaker.windowMs / 1000)}s`);
120567
+ await writeLine2(ctx, ` ${symbol3} circuit breaker: ${state} ${breaker.failuresInWindow}/${breaker.threshold} failures in last ${Math.round(breaker.windowMs / 1000)}s`);
120123
120568
  }
120124
120569
  if (result.health.version) {
120125
- await writeLine(ctx, import_picocolors13.default.dim(` api version: ${result.health.version}`));
120570
+ await writeLine2(ctx, import_picocolors14.default.dim(` api version: ${result.health.version}`));
120126
120571
  }
120127
120572
  }
120128
120573
 
120129
120574
  // src/commands/token.ts
120130
- var import_picocolors14 = __toESM(require_picocolors(), 1);
120575
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
120131
120576
  var VALID_SCOPES = ["deploy", "env", "read", "full"];
120132
120577
  function scopeLabel(scopes, appName) {
120133
120578
  const s = scopes && scopes.length > 0 ? scopes.join("+") : "full";
@@ -120136,22 +120581,22 @@ function scopeLabel(scopes, appName) {
120136
120581
  function registerTokenCommands(cli, ctx) {
120137
120582
  cli.command("token [action] [arg]", "Manage API tokens: create | list | revoke").option("--scope <scope>", "(create) deploy | env | read | full (default: full)").option("--app <name>", "(create) bind the token to a single app").option("--ttl-days <n>", "(create) token lifetime in days").action(async (action, arg, options) => {
120138
120583
  if (!ctx.client.token) {
120139
- await writeLine(ctx, `${import_picocolors14.default.red("✗")} Not logged in. Run: percher login`);
120584
+ await writeLine2(ctx, `${import_picocolors15.default.red("✗")} Not logged in. Run: percher login`);
120140
120585
  throw new SilentExitError;
120141
120586
  }
120142
120587
  if (action === "create") {
120143
120588
  if (!arg) {
120144
- await writeLine(ctx, `${import_picocolors14.default.red("✗")} A name is required. Usage: percher token create <name> [--scope deploy] [--app <name>]`);
120589
+ await writeLine2(ctx, `${import_picocolors15.default.red("✗")} A name is required. Usage: percher token create <name> [--scope deploy] [--app <name>]`);
120145
120590
  throw new SilentExitError;
120146
120591
  }
120147
120592
  const scope = options.scope ?? "full";
120148
120593
  if (!VALID_SCOPES.includes(scope)) {
120149
- await writeLine(ctx, `${import_picocolors14.default.red("✗")} Invalid --scope "${options.scope}". Use one of: ${VALID_SCOPES.join(", ")}.`);
120594
+ await writeLine2(ctx, `${import_picocolors15.default.red("✗")} Invalid --scope "${options.scope}". Use one of: ${VALID_SCOPES.join(", ")}.`);
120150
120595
  throw new SilentExitError;
120151
120596
  }
120152
120597
  const ttlDays = options.ttlDays !== undefined ? Number(options.ttlDays) : undefined;
120153
120598
  if (ttlDays !== undefined && (!Number.isInteger(ttlDays) || ttlDays <= 0)) {
120154
- await writeLine(ctx, `${import_picocolors14.default.red("✗")} --ttl-days must be a positive whole number.`);
120599
+ await writeLine2(ctx, `${import_picocolors15.default.red("✗")} --ttl-days must be a positive whole number.`);
120155
120600
  throw new SilentExitError;
120156
120601
  }
120157
120602
  const created = await tokenCreate(ctx, {
@@ -120160,36 +120605,36 @@ function registerTokenCommands(cli, ctx) {
120160
120605
  app: options.app,
120161
120606
  ttlDays
120162
120607
  });
120163
- await writeLine(ctx, `${import_picocolors14.default.green("✓")} Token created: ${import_picocolors14.default.bold(created.name)} ${import_picocolors14.default.dim(`(${scopeLabel(created.scopes, created.appName)})`)}`);
120164
- await writeLine(ctx, "");
120165
- await writeLine(ctx, ` ${import_picocolors14.default.bold(created.token)}`);
120166
- await writeLine(ctx, import_picocolors14.default.yellow(" ↑ Store this now — the secret is shown only once and can't be retrieved."));
120167
- await writeLine(ctx, "");
120168
- await writeLine(ctx, import_picocolors14.default.dim(" Use it in CI / an agent:"));
120169
- await writeLine(ctx, import_picocolors14.default.dim(` PERCHER_TOKEN=${created.token} bunx percher publish`));
120608
+ await writeLine2(ctx, `${import_picocolors15.default.green("✓")} Token created: ${import_picocolors15.default.bold(created.name)} ${import_picocolors15.default.dim(`(${scopeLabel(created.scopes, created.appName)})`)}`);
120609
+ await writeLine2(ctx, "");
120610
+ await writeLine2(ctx, ` ${import_picocolors15.default.bold(created.token)}`);
120611
+ await writeLine2(ctx, import_picocolors15.default.yellow(" ↑ Store this now — the secret is shown only once and can't be retrieved."));
120612
+ await writeLine2(ctx, "");
120613
+ await writeLine2(ctx, import_picocolors15.default.dim(" Use it in CI / an agent:"));
120614
+ await writeLine2(ctx, import_picocolors15.default.dim(` PERCHER_TOKEN=${created.token} bunx percher publish`));
120170
120615
  return;
120171
120616
  }
120172
120617
  if (action === "list") {
120173
120618
  const tokens = await tokenList(ctx);
120174
120619
  if (tokens.length === 0) {
120175
- await writeLine(ctx, import_picocolors14.default.dim("No API tokens."));
120620
+ await writeLine2(ctx, import_picocolors15.default.dim("No API tokens."));
120176
120621
  return;
120177
120622
  }
120178
120623
  for (const t of tokens) {
120179
120624
  const expires = t.expiresAt ? `expires ${new Date(t.expiresAt).toISOString().slice(0, 10)}` : "no expiry";
120180
- await writeLine(ctx, `${import_picocolors14.default.bold(t.id)} ${t.name.padEnd(20)} ${import_picocolors14.default.dim(scopeLabel(t.scopes, t.appName))} ${import_picocolors14.default.dim(expires)}`);
120625
+ await writeLine2(ctx, `${import_picocolors15.default.bold(t.id)} ${t.name.padEnd(20)} ${import_picocolors15.default.dim(scopeLabel(t.scopes, t.appName))} ${import_picocolors15.default.dim(expires)}`);
120181
120626
  }
120182
- await writeLine(ctx, "");
120183
- await writeLine(ctx, import_picocolors14.default.dim("Revoke one with: percher token revoke <id>"));
120627
+ await writeLine2(ctx, "");
120628
+ await writeLine2(ctx, import_picocolors15.default.dim("Revoke one with: percher token revoke <id>"));
120184
120629
  return;
120185
120630
  }
120186
120631
  if (action === "revoke") {
120187
120632
  if (!arg) {
120188
- await writeLine(ctx, `${import_picocolors14.default.red("✗")} A token id is required. Usage: percher token revoke <id> (list ids with \`percher token list\`).`);
120633
+ await writeLine2(ctx, `${import_picocolors15.default.red("✗")} A token id is required. Usage: percher token revoke <id> (list ids with \`percher token list\`).`);
120189
120634
  throw new SilentExitError;
120190
120635
  }
120191
120636
  await tokenRevoke(ctx, { id: arg });
120192
- await writeLine(ctx, `${import_picocolors14.default.green("✓")} Token revoked.`);
120637
+ await writeLine2(ctx, `${import_picocolors15.default.green("✓")} Token revoked.`);
120193
120638
  return;
120194
120639
  }
120195
120640
  throw new Error(`Unknown token action "${action ?? ""}". Use: create | list | revoke.`);
@@ -120200,18 +120645,18 @@ function registerTokenCommands(cli, ctx) {
120200
120645
  }
120201
120646
 
120202
120647
  // src/commands/transfers.ts
120203
- var import_picocolors15 = __toESM(require_picocolors(), 1);
120648
+ var import_picocolors16 = __toESM(require_picocolors(), 1);
120204
120649
  function registerTransfersCommands(cli, ctx) {
120205
120650
  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) => {
120206
120651
  if (options.list) {
120207
120652
  const rows = await transferListForApp(ctx, { app: options.app });
120208
120653
  if (rows.length === 0) {
120209
- await writeLine(ctx, import_picocolors15.default.dim("No transfers for this app."));
120654
+ await writeLine2(ctx, import_picocolors16.default.dim("No transfers for this app."));
120210
120655
  return;
120211
120656
  }
120212
120657
  for (const r of rows) {
120213
- const status2 = r.status === "pending" ? import_picocolors15.default.yellow("pending") : r.status === "accepted" ? import_picocolors15.default.green("accepted") : import_picocolors15.default.dim(r.status);
120214
- await writeLine(ctx, `${r.id} ${status2.padEnd(20)} → ${r.toEmail} (${r.createdAt})`);
120658
+ const status2 = r.status === "pending" ? import_picocolors16.default.yellow("pending") : r.status === "accepted" ? import_picocolors16.default.green("accepted") : import_picocolors16.default.dim(r.status);
120659
+ await writeLine2(ctx, `${r.id} ${status2.padEnd(20)} → ${r.toEmail} (${r.createdAt})`);
120215
120660
  }
120216
120661
  return;
120217
120662
  }
@@ -120223,7 +120668,7 @@ function registerTransfersCommands(cli, ctx) {
120223
120668
  app: options.app,
120224
120669
  transferId: emailOrTransferId
120225
120670
  });
120226
- await writeLine(ctx, `${import_picocolors15.default.green("✓")} Cancelled transfer ${emailOrTransferId}`);
120671
+ await writeLine2(ctx, `${import_picocolors16.default.green("✓")} Cancelled transfer ${emailOrTransferId}`);
120227
120672
  return;
120228
120673
  }
120229
120674
  if (!emailOrTransferId) {
@@ -120233,21 +120678,21 @@ function registerTransfersCommands(cli, ctx) {
120233
120678
  app: options.app,
120234
120679
  email: emailOrTransferId
120235
120680
  });
120236
- await writeLine(ctx, `${import_picocolors15.default.green("✓")} Transfer initiated to ${initiated.toEmail} (id ${initiated.id})`);
120237
- await writeLine(ctx, import_picocolors15.default.dim(`Expires ${new Date(initiated.expiresAt).toLocaleDateString()}. The recipient must accept via 'percher transfers accept ${initiated.id}'.`));
120681
+ await writeLine2(ctx, `${import_picocolors16.default.green("✓")} Transfer initiated to ${initiated.toEmail} (id ${initiated.id})`);
120682
+ await writeLine2(ctx, import_picocolors16.default.dim(`Expires ${new Date(initiated.expiresAt).toLocaleDateString()}. The recipient must accept via 'percher transfers accept ${initiated.id}'.`));
120238
120683
  if (initiated.notes) {
120239
- await writeLine(ctx, import_picocolors15.default.yellow(` ⚠ ${initiated.notes}`));
120684
+ await writeLine2(ctx, import_picocolors16.default.yellow(` ⚠ ${initiated.notes}`));
120240
120685
  }
120241
120686
  });
120242
120687
  cli.command("transfers [action] [transferId]", "Incoming ownership transfers: list (default) | accept <transferId> | decline <transferId>").action(async (action, transferId) => {
120243
120688
  if (action === undefined || action === "list") {
120244
120689
  const rows = await transfersIncoming(ctx, {});
120245
120690
  if (rows.length === 0) {
120246
- await writeLine(ctx, import_picocolors15.default.dim("No pending incoming transfers."));
120691
+ await writeLine2(ctx, import_picocolors16.default.dim("No pending incoming transfers."));
120247
120692
  return;
120248
120693
  }
120249
120694
  for (const r of rows) {
120250
- await writeLine(ctx, `${r.id} ${r.appName.padEnd(24)} from ${r.fromEmail} (expires ${new Date(r.expiresAt).toLocaleDateString()})`);
120695
+ await writeLine2(ctx, `${r.id} ${r.appName.padEnd(24)} from ${r.fromEmail} (expires ${new Date(r.expiresAt).toLocaleDateString()})`);
120251
120696
  }
120252
120697
  return;
120253
120698
  }
@@ -120256,7 +120701,7 @@ function registerTransfersCommands(cli, ctx) {
120256
120701
  throw new Error("Usage: percher transfers accept <transferId>");
120257
120702
  }
120258
120703
  const accepted = await transfersAccept(ctx, { transferId });
120259
- await writeLine(ctx, `${import_picocolors15.default.green("✓")} You now own ${import_picocolors15.default.cyan(accepted.appName)}. Run 'percher open --app ${accepted.appName}' to see it.`);
120704
+ await writeLine2(ctx, `${import_picocolors16.default.green("✓")} You now own ${import_picocolors16.default.cyan(accepted.appName)}. Run 'percher open --app ${accepted.appName}' to see it.`);
120260
120705
  return;
120261
120706
  }
120262
120707
  if (action === "decline") {
@@ -120264,7 +120709,7 @@ function registerTransfersCommands(cli, ctx) {
120264
120709
  throw new Error("Usage: percher transfers decline <transferId>");
120265
120710
  }
120266
120711
  await transfersDecline(ctx, { transferId });
120267
- await writeLine(ctx, `${import_picocolors15.default.green("✓")} Declined transfer ${transferId}.`);
120712
+ await writeLine2(ctx, `${import_picocolors16.default.green("✓")} Declined transfer ${transferId}.`);
120268
120713
  return;
120269
120714
  }
120270
120715
  throw new Error(`Unknown transfers action "${action}". Use list | accept <id> | decline <id>.`);
@@ -120328,6 +120773,7 @@ async function createCliContext() {
120328
120773
  prompt,
120329
120774
  openUrl,
120330
120775
  interactiveLogin: true,
120776
+ stdinIsInteractive: process.stdin.isTTY === true && process.stdout.isTTY === true,
120331
120777
  status(message) {
120332
120778
  process.stderr.write(encoder.encode(`${message}
120333
120779
  `));
@@ -120337,52 +120783,52 @@ async function createCliContext() {
120337
120783
 
120338
120784
  // src/output.ts
120339
120785
  init_dist();
120340
- var import_picocolors16 = __toESM(require_picocolors(), 1);
120786
+ var import_picocolors17 = __toESM(require_picocolors(), 1);
120341
120787
  async function renderError(stderr, error48) {
120342
120788
  const writer = stderr.getWriter();
120343
120789
  const encoder = new TextEncoder;
120344
120790
  const lines = [];
120345
120791
  if (error48 instanceof PercherTomlError) {
120346
- lines.push(`${import_picocolors16.default.red(`error (${error48.code}):`)} ${error48.message}`);
120792
+ lines.push(`${import_picocolors17.default.red(`error (${error48.code}):`)} ${error48.message}`);
120347
120793
  if (error48.tomlLine !== undefined) {
120348
- lines.push(import_picocolors16.default.dim(` at percher.toml line ${error48.tomlLine}`));
120794
+ lines.push(import_picocolors17.default.dim(` at percher.toml line ${error48.tomlLine}`));
120349
120795
  }
120350
120796
  for (const issue2 of error48.issues ?? []) {
120351
- lines.push(`${import_picocolors16.default.dim(" · ") + import_picocolors16.default.yellow(issue2.path.join("."))}: ${issue2.message}`);
120797
+ lines.push(`${import_picocolors17.default.dim(" · ") + import_picocolors17.default.yellow(issue2.path.join("."))}: ${issue2.message}`);
120352
120798
  }
120353
120799
  } else if (error48 instanceof SunsetError) {
120354
120800
  if (error48.gateCode === "platform_delete_gated") {
120355
- lines.push(`${import_picocolors16.default.yellow("Percher is shutting down — deletes are paused.")}`);
120801
+ lines.push(`${import_picocolors17.default.yellow("Percher is shutting down — deletes are paused.")}`);
120356
120802
  lines.push(error48.message);
120357
- lines.push(import_picocolors16.default.dim(` To delete and permanently forfeit your export window, confirm with your password in the dashboard: ${error48.dashboardUrl}`));
120358
- lines.push(import_picocolors16.default.dim(" The CLI cannot perform this delete during sunset — it must go through the dashboard."));
120803
+ lines.push(import_picocolors17.default.dim(` To delete and permanently forfeit your export window, confirm with your password in the dashboard: ${error48.dashboardUrl}`));
120804
+ lines.push(import_picocolors17.default.dim(" The CLI cannot perform this delete during sunset — it must go through the dashboard."));
120359
120805
  } else {
120360
- lines.push(`${import_picocolors16.default.yellow("Percher is in read-only mode ahead of shutdown.")}`);
120806
+ lines.push(`${import_picocolors17.default.yellow("Percher is in read-only mode ahead of shutdown.")}`);
120361
120807
  lines.push(error48.message);
120362
- lines.push(import_picocolors16.default.dim(` Your apps keep running. Export them any time from the dashboard: ${error48.dashboardUrl}`));
120808
+ lines.push(import_picocolors17.default.dim(` Your apps keep running. Export them any time from the dashboard: ${error48.dashboardUrl}`));
120363
120809
  }
120364
- lines.push(import_picocolors16.default.dim(` Final data deletion: ${error48.finalDeletionDate}`));
120810
+ lines.push(import_picocolors17.default.dim(` Final data deletion: ${error48.finalDeletionDate}`));
120365
120811
  } else if (error48 instanceof PercherApiError) {
120366
120812
  if (error48.status === 502 || error48.status === 503) {
120367
- lines.push(`${import_picocolors16.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error48.status}). Try again in a few seconds.`);
120813
+ lines.push(`${import_picocolors17.default.red("error:")} Percher API is temporarily unavailable (HTTP ${error48.status}). Try again in a few seconds.`);
120368
120814
  } else {
120369
- lines.push(`${import_picocolors16.default.red("error:")} ${error48.message}`);
120370
- lines.push(import_picocolors16.default.dim(` ${error48.code} (HTTP ${error48.status})`));
120815
+ lines.push(`${import_picocolors17.default.red("error:")} ${error48.message}`);
120816
+ lines.push(import_picocolors17.default.dim(` ${error48.code} (HTTP ${error48.status})`));
120371
120817
  }
120372
120818
  if (error48.requestId)
120373
- lines.push(import_picocolors16.default.dim(` request: ${error48.requestId}`));
120819
+ lines.push(import_picocolors17.default.dim(` request: ${error48.requestId}`));
120374
120820
  } else if (error48 instanceof PercherCoreError) {
120375
- lines.push(`${import_picocolors16.default.red(`error (${error48.code}):`)} ${error48.message}`);
120821
+ lines.push(`${import_picocolors17.default.red(`error (${error48.code}):`)} ${error48.message}`);
120376
120822
  if (error48.hint)
120377
- lines.push(import_picocolors16.default.dim(` ${error48.hint}`));
120823
+ lines.push(import_picocolors17.default.dim(` ${error48.hint}`));
120378
120824
  } else if (error48 instanceof Error) {
120379
- lines.push(`${import_picocolors16.default.red("unexpected error:")} ${error48.message}`);
120825
+ lines.push(`${import_picocolors17.default.red("unexpected error:")} ${error48.message}`);
120380
120826
  if (error48.stack)
120381
- lines.push(import_picocolors16.default.dim(error48.stack.split(`
120827
+ lines.push(import_picocolors17.default.dim(error48.stack.split(`
120382
120828
  `).slice(1, 4).join(`
120383
120829
  `)));
120384
120830
  } else {
120385
- lines.push(`${import_picocolors16.default.red("unexpected error:")} ${String(error48)}`);
120831
+ lines.push(`${import_picocolors17.default.red("unexpected error:")} ${String(error48)}`);
120386
120832
  }
120387
120833
  try {
120388
120834
  await writer.write(encoder.encode(`${lines.join(`
@@ -120395,7 +120841,7 @@ async function renderError(stderr, error48) {
120395
120841
  // package.json
120396
120842
  var package_default = {
120397
120843
  name: "percher",
120398
- version: "0.5.20",
120844
+ version: "0.5.21",
120399
120845
  description: "CLI for the Percher hosting platform — publish apps with one command",
120400
120846
  type: "module",
120401
120847
  bin: {
@@ -120422,9 +120868,9 @@ var package_default = {
120422
120868
  typecheck: "tsc --noEmit"
120423
120869
  },
120424
120870
  dependencies: {
120425
- "@percher/client": "^0.2.19",
120426
- "@percher/core": "^0.4.23",
120427
- "@percher/toml": "^0.1.8",
120871
+ "@percher/client": "^0.2.20",
120872
+ "@percher/core": "^0.4.24",
120873
+ "@percher/toml": "^0.1.9",
120428
120874
  cac: "^7.0.0",
120429
120875
  picocolors: "^1.1.1"
120430
120876
  },
@@ -120495,7 +120941,7 @@ async function showTelemetryNotice() {
120495
120941
  }
120496
120942
 
120497
120943
  // src/version-check.ts
120498
- var import_picocolors17 = __toESM(require_picocolors(), 1);
120944
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
120499
120945
  async function checkForUpdates() {
120500
120946
  try {
120501
120947
  const res = await fetch("https://registry.npmjs.org/percher/latest", {
@@ -120506,9 +120952,9 @@ async function checkForUpdates() {
120506
120952
  const data2 = await res.json();
120507
120953
  const latest = data2.version;
120508
120954
  if (latest && latest !== VERSION) {
120509
- console.error(import_picocolors17.default.yellow(`
120955
+ console.error(import_picocolors18.default.yellow(`
120510
120956
  Update available: ${VERSION} → ${latest}`));
120511
- console.error(import_picocolors17.default.dim(`Run: bunx percher@latest
120957
+ console.error(import_picocolors18.default.dim(`Run: bunx percher@latest
120512
120958
  `));
120513
120959
  }
120514
120960
  } catch {}
@@ -120526,6 +120972,7 @@ async function run(argv, ctxOverride) {
120526
120972
  registerAuthCommands(cli, ctx);
120527
120973
  registerAccountCommands(cli, ctx);
120528
120974
  registerGithubCommands(cli, ctx);
120975
+ registerHooksCommands(cli, ctx);
120529
120976
  registerMigrateCommands(cli, ctx);
120530
120977
  registerSharingCommands(cli, ctx);
120531
120978
  registerStatusCommands(cli, ctx);