percher 0.5.9 → 0.5.11

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.
@@ -90873,10 +90873,46 @@ var CAC = class extends EventTarget {
90873
90873
  };
90874
90874
  var cac = (name = "") => new CAC(name);
90875
90875
 
90876
- // ../core/dist/app-name.js
90877
- import { existsSync, readFileSync } from "node:fs";
90878
- import { join } from "node:path";
90879
-
90876
+ // ../shared/dist/app-name.js
90877
+ var APP_NAME_RE = /^[a-z][a-z0-9-]{2,39}$/;
90878
+ // ../shared/dist/limits.js
90879
+ var MAX_TARBALL_BYTES = 500 * 1024 * 1024;
90880
+ var MAX_INLINE_PUBLISH_BYTES = 25 * 1024 * 1024;
90881
+ // ../shared/dist/logs.js
90882
+ var MAX_LOG_SEARCH_LENGTH = 256;
90883
+ // ../shared/dist/platform-env.js
90884
+ var PERCHER_INJECTED_ENV_KEYS = new Set([
90885
+ "NODE_ENV",
90886
+ "PORT",
90887
+ "DATABASE_URL",
90888
+ "POCKETBASE_URL",
90889
+ "POCKETBASE_ADMIN_EMAIL",
90890
+ "POCKETBASE_ADMIN_PASSWORD",
90891
+ "PERCHER_APP_NAME",
90892
+ "PERCHER_APP_URL",
90893
+ "HTTP_PROXY",
90894
+ "HTTPS_PROXY",
90895
+ "NO_PROXY",
90896
+ "NODE_USE_ENV_PROXY"
90897
+ ]);
90898
+ var PROXY_LOWERCASE_ALIASES = new Set([
90899
+ "http_proxy",
90900
+ "https_proxy",
90901
+ "no_proxy"
90902
+ ]);
90903
+ // ../shared/dist/runtime.js
90904
+ var DEFAULT_NODE_VERSION_MAJOR = "24";
90905
+ var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
90906
+ // ../shared/dist/timeouts.js
90907
+ var TIMEOUTS = {
90908
+ workerBuildSubprocess: 10 * 60000,
90909
+ apiToWorkerBuildFetch: 11 * 60000,
90910
+ clientPublishPoll: 12 * 60000,
90911
+ workerHealthcheck: 5000,
90912
+ watchdogList: 15000,
90913
+ mcpWaitDeployDefault: 30000,
90914
+ mcpWaitDeployMax: 12 * 60000
90915
+ };
90880
90916
  // ../toml/dist/errors.js
90881
90917
  class PercherTomlError extends Error {
90882
90918
  code;
@@ -104444,45 +104480,6 @@ function date4(params) {
104444
104480
 
104445
104481
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
104446
104482
  config(en_default());
104447
- // ../shared/dist/app-name.js
104448
- var APP_NAME_RE = /^[a-z][a-z0-9-]{2,39}$/;
104449
- // ../shared/dist/limits.js
104450
- var MAX_TARBALL_BYTES = 500 * 1024 * 1024;
104451
- // ../shared/dist/logs.js
104452
- var MAX_LOG_SEARCH_LENGTH = 256;
104453
- // ../shared/dist/platform-env.js
104454
- var PERCHER_INJECTED_ENV_KEYS = new Set([
104455
- "NODE_ENV",
104456
- "PORT",
104457
- "DATABASE_URL",
104458
- "POCKETBASE_URL",
104459
- "POCKETBASE_ADMIN_EMAIL",
104460
- "POCKETBASE_ADMIN_PASSWORD",
104461
- "PERCHER_APP_NAME",
104462
- "PERCHER_APP_URL",
104463
- "HTTP_PROXY",
104464
- "HTTPS_PROXY",
104465
- "NO_PROXY",
104466
- "NODE_USE_ENV_PROXY"
104467
- ]);
104468
- var PROXY_LOWERCASE_ALIASES = new Set([
104469
- "http_proxy",
104470
- "https_proxy",
104471
- "no_proxy"
104472
- ]);
104473
- // ../shared/dist/runtime.js
104474
- var DEFAULT_NODE_VERSION_MAJOR = "24";
104475
- var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
104476
- // ../shared/dist/timeouts.js
104477
- var TIMEOUTS = {
104478
- workerBuildSubprocess: 10 * 60000,
104479
- apiToWorkerBuildFetch: 11 * 60000,
104480
- clientPublishPoll: 12 * 60000,
104481
- workerHealthcheck: 5000,
104482
- watchdogList: 15000,
104483
- mcpWaitDeployDefault: 30000,
104484
- mcpWaitDeployMax: 12 * 60000
104485
- };
104486
104483
  // ../toml/dist/schema.js
104487
104484
  var appSchema = exports_external.object({
104488
104485
  name: exports_external.string().regex(APP_NAME_RE, "must be 3–40 chars, lowercase, start with a letter, [a-z0-9-] only"),
@@ -104801,6 +104798,8 @@ function serialize(config2) {
104801
104798
  return import_toml2.default.stringify(config2);
104802
104799
  }
104803
104800
  // ../core/dist/app-name.js
104801
+ import { existsSync, readFileSync } from "node:fs";
104802
+ import { join } from "node:path";
104804
104803
  function readPercherTomlAppName(cwd) {
104805
104804
  const p = join(cwd, "percher.toml");
104806
104805
  if (!existsSync(p))
@@ -105526,7 +105525,7 @@ async function appstoreKit(ctx, input = {}) {
105526
105525
  const themeHex = themeColorForToken(null);
105527
105526
  const base = resolve3(ctx.cwd, input.outDir ?? ".");
105528
105527
  const baseRel = relative(ctx.cwd, base);
105529
- if (baseRel.startsWith("..") || isAbsolute(baseRel)) {
105528
+ if (baseRel === ".." || baseRel.startsWith(`..${sep}`) || isAbsolute(baseRel)) {
105530
105529
  throw new PercherCoreError("Output directory must stay inside the project", {
105531
105530
  code: "appstore-kit.bad-out-dir",
105532
105531
  hint: "Pass a path inside the current project (e.g. --out public), not an absolute or parent path."
@@ -105965,6 +105964,12 @@ class AccountResource {
105965
105964
  revokeToken(id) {
105966
105965
  return this.http.delete(`/account/tokens/${encodeURIComponent(id)}`);
105967
105966
  }
105967
+ listOAuthGrants() {
105968
+ return this.http.get("/account/oauth-grants");
105969
+ }
105970
+ revokeOAuthGrant(grantId) {
105971
+ return this.http.delete(`/account/oauth-grants/${encodeURIComponent(grantId)}`);
105972
+ }
105968
105973
  exportData() {
105969
105974
  return this.http.get("/account/export");
105970
105975
  }
@@ -106060,12 +106065,32 @@ class AppsResource {
106060
106065
  }
106061
106066
  return this.http.postBinary(`/apps/${encodeURIComponent(id)}/deploys${qs ? `?${qs}` : ""}`, input.tarball, headers);
106062
106067
  }
106068
+ publishInline(input) {
106069
+ const params = new URLSearchParams;
106070
+ params.set("name", input.name);
106071
+ params.set("runtime", input.runtime);
106072
+ if (input.framework)
106073
+ params.set("framework", input.framework);
106074
+ if (input.type === "preview")
106075
+ params.set("type", "preview");
106076
+ if (input.note)
106077
+ params.set("note", input.note);
106078
+ if (input.noCache)
106079
+ params.set("noCache", "1");
106080
+ const headers = {};
106081
+ if (input.tarballHash)
106082
+ headers["X-Tarball-Hash"] = input.tarballHash;
106083
+ return this.http.postBinary(`/apps/publish?${params.toString()}`, input.tarball, headers);
106084
+ }
106063
106085
  promotePreview(appRef, deployId) {
106064
106086
  return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/promote`, {});
106065
106087
  }
106066
106088
  rerunDeploy(appRef, deployId) {
106067
106089
  return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/rerun`, {});
106068
106090
  }
106091
+ deployLatestCommit(appRef, input) {
106092
+ return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/from-latest-commit`, input ?? {});
106093
+ }
106069
106094
  setCrashAnalysisEnabled(appRef, enabled) {
106070
106095
  return this.http.put(`/apps/${encodeURIComponent(appRef)}/crash-analysis`, {
106071
106096
  crashAnalysisEnabled: enabled
@@ -106704,9 +106729,10 @@ class PercherClient {
106704
106729
  invites;
106705
106730
  transfers;
106706
106731
  constructor(options = {}) {
106707
- const defaults = loadConfig();
106708
- this.apiUrl = options.apiUrl ?? defaults.apiUrl;
106709
- this.token = options.token !== undefined ? options.token : defaults.token;
106732
+ const bothExplicit = options.apiUrl !== undefined && options.token !== undefined;
106733
+ const defaults = bothExplicit ? undefined : loadConfig();
106734
+ this.apiUrl = options.apiUrl ?? defaults?.apiUrl ?? "";
106735
+ this.token = options.token !== undefined ? options.token : defaults?.token ?? null;
106710
106736
  this.sessionId = options.sessionId;
106711
106737
  const http = new Http({
106712
106738
  apiUrl: this.apiUrl,
@@ -106990,427 +107016,278 @@ async function deleteApp(ctx, input) {
106990
107016
  }
106991
107017
  await ctx.client.apps.delete(name);
106992
107018
  }
106993
- // ../core/dist/commands/deploys.js
106994
- var deploysListInputSchema = exports_external.object({
106995
- app: exports_external.string().optional(),
106996
- limit: exports_external.number().int().positive().max(100).optional()
106997
- });
106998
- var deploysInspectInputSchema = exports_external.object({
106999
- app: exports_external.string().optional(),
107000
- id: exports_external.string().optional(),
107001
- latestFailed: exports_external.boolean().optional()
107002
- }).superRefine((val, ctx) => {
107003
- if (!val.id && !val.latestFailed) {
107004
- ctx.addIssue({
107005
- code: exports_external.ZodIssueCode.custom,
107006
- message: "either `id` or `latestFailed: true` must be provided"
107007
- });
107008
- }
107009
- });
107010
- function resolveAppName(ctx, app) {
107011
- const name = app ?? readPercherTomlAppName(ctx.cwd);
107012
- if (!name) {
107013
- throw new PercherCoreError("No app specified and no percher.toml found", {
107014
- code: "deploys.no-app",
107015
- hint: "Pass --app <name> or run from a directory with percher.toml."
107016
- });
107019
+ // ../core/dist/poll-deployment.js
107020
+ async function pollDeployment(opts) {
107021
+ const terminal = opts.terminal ?? ["live", "failed"];
107022
+ const intervalMs = opts.intervalMs ?? 2000;
107023
+ const timeoutMs = opts.timeoutMs ?? TIMEOUTS.clientPublishPoll;
107024
+ const maxRetries = opts.maxRetries ?? 5;
107025
+ let deployment = opts.initial;
107026
+ const startedAt = Date.now();
107027
+ const seenEventIds = new Set;
107028
+ if (opts.onEvent) {
107029
+ await drainNewEvents(opts, deployment.id, seenEventIds);
107017
107030
  }
107018
- return name;
107019
- }
107020
- async function deploysList(ctx, input = {}) {
107021
- const name = resolveAppName(ctx, input.app);
107022
- return ctx.client.apps.listDeploys(name, { limit: input.limit ?? 10 });
107023
- }
107024
- async function deploysInspect(ctx, input) {
107025
- const name = resolveAppName(ctx, input.app);
107026
- let deployId = input.id;
107027
- if (!deployId && input.latestFailed) {
107028
- const recent = await ctx.client.apps.listDeploys(name, { limit: 25 });
107029
- const failed = recent.find((d) => d.status === "failed");
107030
- if (!failed) {
107031
- throw new PercherCoreError(`No failed deploys found for "${name}"`, {
107032
- code: "deploys.no-failed",
107033
- hint: "Try `percher deploys` to list recent deploys, or omit --latest-failed."
107034
- });
107031
+ while (!terminal.includes(deployment.status)) {
107032
+ if (Date.now() - startedAt > timeoutMs) {
107033
+ opts.onTimeout(deployment);
107035
107034
  }
107036
- deployId = failed.id;
107037
- }
107038
- if (!deployId) {
107039
- throw new PercherCoreError("Deploy ID is required", {
107040
- code: "deploys.no-id"
107041
- });
107042
- }
107043
- const deploy = await ctx.client.apps.getDeployment(name, deployId);
107044
- let buildLog = null;
107045
- try {
107046
- const log = await ctx.client.apps.getBuildLog(name, deployId);
107047
- buildLog = log.length > 0 ? log : null;
107048
- } catch (err) {
107049
- if (err instanceof PercherApiError && err.status === 404) {
107050
- buildLog = null;
107051
- } else {
107052
- throw err;
107035
+ opts.onTick?.(deployment);
107036
+ await new Promise((r) => setTimeout(r, intervalMs));
107037
+ let attempt = 0;
107038
+ let polled = false;
107039
+ while (!polled) {
107040
+ try {
107041
+ deployment = await opts.ctx.client.apps.getDeployment(opts.appId, deployment.id);
107042
+ polled = true;
107043
+ } catch (err) {
107044
+ attempt++;
107045
+ const isNetwork = isTransientNetworkError(err);
107046
+ if (!isNetwork || attempt >= maxRetries) {
107047
+ if (isNetwork) {
107048
+ const msg = err.message ?? String(err);
107049
+ throw new Error(`${msg}
107050
+
107051
+ The deploy may already be live — your network connection dropped mid-poll. Check with: percher deploys list ${opts.appName}`, { cause: err });
107052
+ }
107053
+ throw err;
107054
+ }
107055
+ const backoff = (opts.backoffMs ?? ((n) => 1000 * 2 ** (n - 1)))(attempt);
107056
+ await new Promise((r) => setTimeout(r, backoff));
107057
+ }
107058
+ }
107059
+ if (opts.onEvent) {
107060
+ await drainNewEvents(opts, deployment.id, seenEventIds);
107053
107061
  }
107054
107062
  }
107055
- return { deploy, buildLog };
107063
+ return deployment;
107056
107064
  }
107057
- // ../core/dist/commands/dev.js
107058
- import { existsSync as existsSync8 } from "node:fs";
107059
- import { join as join11 } from "node:path";
107060
-
107061
- // ../core/dist/tarball.js
107062
- var import_ignore = __toESM(require_ignore(), 1);
107063
- import { createHash as createHash2 } from "node:crypto";
107064
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
107065
- import { join as join9, relative as relative3, sep as sep2 } from "node:path";
107066
- import { Readable } from "node:stream";
107067
- import { createGzip } from "node:zlib";
107068
-
107069
- // ../../node_modules/.bun/tar-stream@3.1.8/node_modules/tar-stream/index.js
107070
- var $extract = require_extract();
107071
- var $pack = require_pack();
107072
-
107073
- // ../core/dist/tarball.js
107074
- var DEFAULT_IGNORE = [
107075
- "node_modules",
107076
- ".git",
107077
- "dist",
107078
- ".next",
107079
- ".svelte-kit",
107080
- "build",
107081
- "coverage",
107082
- ".env",
107083
- ".env.*",
107084
- ".bun",
107085
- "*.log",
107086
- ".DS_Store"
107087
- ];
107088
- function* walk(dir, baseDir, ig) {
107089
- const entries = readdirSync2(dir);
107090
- for (const name of entries) {
107091
- const full = join9(dir, name);
107092
- const rel = relative3(baseDir, full).split(sep2).join("/");
107093
- if (ig.ignores(rel))
107094
- continue;
107095
- const stats = statSync2(full);
107096
- if (stats.isDirectory()) {
107097
- if (ig.ignores(`${rel}/`))
107065
+ async function drainNewEvents(opts, deployId, seenEventIds) {
107066
+ if (!opts.onEvent)
107067
+ return;
107068
+ try {
107069
+ const { events } = await opts.ctx.client.apps.getDeployEvents(opts.appId, deployId);
107070
+ for (const event of events) {
107071
+ if (seenEventIds.has(event.id))
107098
107072
  continue;
107099
- yield* walk(full, baseDir, ig);
107100
- } else if (stats.isFile()) {
107101
- yield rel;
107073
+ seenEventIds.add(event.id);
107074
+ opts.onEvent(event);
107102
107075
  }
107103
- }
107076
+ } catch {}
107104
107077
  }
107105
- async function createTarball(options) {
107106
- const ig = import_ignore.default();
107107
- ig.add(DEFAULT_IGNORE);
107108
- const gitignorePath = join9(options.cwd, ".gitignore");
107109
- if (existsSync6(gitignorePath)) {
107110
- ig.add(readFileSync6(gitignorePath, "utf8"));
107111
- }
107112
- if (options.extraIgnore)
107113
- ig.add(options.extraIgnore);
107114
- const fileBuffers = [];
107115
- for (const rel of walk(options.cwd, options.cwd, ig)) {
107116
- const data2 = readFileSync6(join9(options.cwd, rel));
107117
- fileBuffers.push({ path: rel, size: data2.length, data: data2 });
107118
- }
107119
- fileBuffers.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
107120
- const totalBytes = fileBuffers.reduce((sum, f) => sum + f.size, 0);
107121
- const contentHasher = createHash2("sha256");
107122
- for (const f of fileBuffers) {
107123
- contentHasher.update(`PATH:${f.path.length}:${f.path}
107124
- `);
107125
- contentHasher.update(`SIZE:${f.size}
107126
- `);
107127
- contentHasher.update(f.data);
107128
- contentHasher.update(`
107129
- `);
107130
- }
107131
- const contentHash = contentHasher.digest("hex");
107132
- const pack = $pack();
107133
- (async () => {
107134
- for (const file2 of fileBuffers) {
107135
- await new Promise((resolve7, reject) => {
107136
- pack.entry({ name: file2.path, size: file2.data.length }, file2.data, (err) => err ? reject(err) : resolve7());
107137
- });
107138
- }
107139
- pack.finalize();
107140
- })();
107141
- const gzip = createGzip();
107142
- const nodeStream = pack.pipe(gzip);
107143
- const webStream = Readable.toWeb(nodeStream);
107144
- const topFiles = [...fileBuffers].sort((a, b) => b.size - a.size).slice(0, 5).map((f) => ({ path: f.path, size: f.size }));
107078
+
107079
+ // ../core/dist/recovery.js
107080
+ function recoveryNone(opts = {}) {
107145
107081
  return {
107146
- stream: webStream,
107147
- bytes: totalBytes,
107148
- fileCount: fileBuffers.length,
107149
- topFiles,
107150
- contentHash
107082
+ retryable: false,
107083
+ nextAction: "none",
107084
+ reasonCode: opts.reasonCode ?? "none",
107085
+ ...opts.url !== undefined ? { url: opts.url } : {}
107151
107086
  };
107152
107087
  }
107153
-
107154
- // ../core/dist/watcher.js
107155
- var import_ignore2 = __toESM(require_ignore(), 1);
107156
- import { existsSync as existsSync7, readFileSync as readFileSync7, watch } from "node:fs";
107157
- import { join as join10, sep as sep3 } from "node:path";
107158
- var DEFAULT_IGNORE2 = [
107159
- "node_modules",
107160
- ".git",
107161
- "dist",
107162
- ".next",
107163
- ".svelte-kit",
107164
- "build",
107165
- "coverage",
107166
- ".env",
107167
- ".env.*",
107168
- ".bun",
107169
- "*.log",
107170
- ".DS_Store",
107171
- "__pycache__"
107172
- ];
107173
- function createWatcher(options) {
107174
- const debounceMs = options.debounceMs ?? 500;
107175
- const ig = import_ignore2.default();
107176
- ig.add(DEFAULT_IGNORE2);
107177
- const gitignorePath = join10(options.cwd, ".gitignore");
107178
- if (existsSync7(gitignorePath)) {
107179
- ig.add(readFileSync7(gitignorePath, "utf8"));
107180
- }
107181
- if (options.extraIgnore)
107182
- ig.add(options.extraIgnore);
107183
- let timer = null;
107184
- let pending = new Set;
107185
- const fsWatcher = watch(options.cwd, { recursive: true }, (_event, filename) => {
107186
- if (!filename)
107187
- return;
107188
- const rel = filename.split(sep3).join("/");
107189
- if (ig.ignores(rel))
107190
- return;
107191
- pending.add(rel);
107192
- if (timer)
107193
- clearTimeout(timer);
107194
- timer = setTimeout(() => {
107195
- const files = [...pending];
107196
- pending = new Set;
107197
- timer = null;
107198
- if (files.length > 0) {
107199
- options.onChange(files);
107200
- }
107201
- }, debounceMs);
107202
- });
107088
+ function recoveryLogin(opts = {}) {
107203
107089
  return {
107204
- close() {
107205
- if (timer)
107206
- clearTimeout(timer);
107207
- fsWatcher.close();
107208
- }
107090
+ retryable: false,
107091
+ nextAction: "open_login",
107092
+ suggestedTool: "percher_login",
107093
+ args: {},
107094
+ reasonCode: opts.reasonCode ?? "auth_required"
107209
107095
  };
107210
107096
  }
107211
-
107212
- // ../core/dist/commands/dev.js
107213
- var devInputSchema = exports_external.object({
107214
- app: exports_external.string().optional()
107215
- });
107216
- async function loadConfig2(ctx, appOverride) {
107217
- const tomlPath = join11(ctx.cwd, "percher.toml");
107218
- if (!existsSync8(tomlPath)) {
107219
- throw new PercherCoreError("No percher.toml found. Run: percher init", {
107220
- code: "dev.no-app",
107221
- hint: "Create a percher.toml first with `percher init`."
107222
- });
107223
- }
107224
- const config2 = await parseFile(tomlPath);
107225
- if (appOverride)
107226
- return { ...config2, app: { ...config2.app, name: appOverride } };
107227
- return config2;
107097
+ function recoveryWait(opts) {
107098
+ return {
107099
+ retryable: false,
107100
+ nextAction: "wait_deploy",
107101
+ suggestedTool: "percher_wait_for_deploy",
107102
+ args: {
107103
+ app: opts.app,
107104
+ deployId: opts.deployId,
107105
+ timeoutSeconds: opts.timeoutSeconds ?? TIMEOUTS.mcpWaitDeployDefault / 1000
107106
+ },
107107
+ reasonCode: opts.reasonCode
107108
+ };
107228
107109
  }
107229
- async function ensureApp(ctx, config2) {
107230
- try {
107231
- return await ctx.client.apps.get(config2.app.name);
107232
- } catch (err) {
107233
- if (err.code === "APP_NOT_FOUND") {
107234
- return ctx.client.apps.create({
107235
- name: config2.app.name,
107236
- runtime: config2.app.runtime,
107237
- framework: config2.app.framework
107238
- });
107239
- }
107240
- throw err;
107241
- }
107110
+ function recoveryDoctor(opts) {
107111
+ const args = {};
107112
+ if (opts.app !== undefined)
107113
+ args.app = opts.app;
107114
+ if (opts.deployId !== undefined)
107115
+ args.deployId = opts.deployId;
107116
+ if (opts.mode !== undefined)
107117
+ args.mode = opts.mode;
107118
+ return {
107119
+ retryable: false,
107120
+ nextAction: "run_doctor",
107121
+ suggestedTool: "percher_doctor",
107122
+ args,
107123
+ reasonCode: opts.reasonCode,
107124
+ ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107125
+ };
107242
107126
  }
107243
- async function doPush(ctx, config2, callbacks) {
107244
- const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
107245
- const app = await ensureApp(ctx, config2);
107246
- await callbacks.writeLine(`Pushing ${tarball.fileCount} files (${(tarball.bytes / 1024).toFixed(0)} KB)...`);
107247
- const deployResponse = await ctx.client.apps.deploy(app.id, { tarball: tarball.stream });
107248
- if (deployResponse.status === "already_in_progress") {
107249
- const active = deployResponse;
107250
- await callbacks.writeLine(` Skipped — deploy already in progress (${active.deployId}, ${active.deployStatus}).`);
107251
- return { success: false, appId: app.id };
107252
- }
107253
- let deployment = deployResponse;
107254
- const startedAt = Date.now();
107255
- const timeoutMs = TIMEOUTS.clientPublishPoll;
107256
- const pollInterval = 2000;
107257
- let lastStatus = "";
107258
- while (deployment.status !== "live" && deployment.status !== "failed") {
107259
- if (Date.now() - startedAt > timeoutMs) {
107260
- await callbacks.writeLine("Deploy timed out");
107261
- return { success: false, appId: app.id };
107262
- }
107263
- if (callbacks.signal.aborted)
107264
- return { success: false, appId: app.id };
107265
- if (deployment.status !== lastStatus) {
107266
- lastStatus = deployment.status;
107267
- await callbacks.writeLine(` ${deployment.status}...`);
107268
- }
107269
- await new Promise((r) => setTimeout(r, pollInterval));
107270
- deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
107271
- }
107272
- if (deployment.status === "failed") {
107273
- try {
107274
- const insp = await ctx.client.apps.getDeployment(app.id, deployment.id);
107275
- if (insp.errorMessage) {
107276
- await callbacks.writeLine("Deploy failed:");
107277
- for (const line of insp.errorMessage.split(`
107278
- `).slice(0, 30)) {
107279
- await callbacks.writeLine(` ${line}`);
107280
- }
107281
- } else {
107282
- await callbacks.writeLine("Deploy failed.");
107283
- }
107284
- } catch {
107285
- await callbacks.writeLine("Deploy failed.");
107286
- }
107287
- return { success: false, appId: app.id };
107288
- }
107289
- await callbacks.writeLine(`Live at ${deployment.url ?? ""}`);
107290
- return { success: true, appId: app.id };
107127
+ function recoveryEnv(opts) {
107128
+ const args = {};
107129
+ if (opts.app !== undefined)
107130
+ args.app = opts.app;
107131
+ if (opts.keys.length > 0)
107132
+ args.keys = opts.keys;
107133
+ return {
107134
+ retryable: false,
107135
+ nextAction: "set_env_vars",
107136
+ suggestedTool: "percher_env_set",
107137
+ args,
107138
+ reasonCode: opts.reasonCode ?? "missing_env"
107139
+ };
107291
107140
  }
107292
- function formatLogLine(l) {
107293
- const level = l.level.toUpperCase().padEnd(5);
107294
- const src = l.source === "runtime" ? "" : `[${l.source}] `;
107295
- return ` ${src}${level} ${l.message}`;
107141
+ function recoveryFixConfig(opts) {
107142
+ return {
107143
+ retryable: false,
107144
+ nextAction: "fix_config",
107145
+ problems: opts.problems,
107146
+ reasonCode: opts.reasonCode ?? "config_invalid"
107147
+ };
107296
107148
  }
107297
- async function streamLogs(ctx, appId, callbacks, signal) {
107298
- try {
107299
- for await (const line of ctx.client.logs.stream(appId, { signal })) {
107300
- if (signal.aborted)
107301
- return;
107302
- await callbacks.writeLine(formatLogLine(line));
107303
- }
107304
- } catch (err) {
107305
- if (signal.aborted)
107306
- return;
107307
- await callbacks.writeLine(`Log stream error: ${err.message}`);
107308
- }
107149
+ function recoveryFixProblems(opts) {
107150
+ return {
107151
+ retryable: false,
107152
+ nextAction: "fix_problems",
107153
+ problems: opts.problems,
107154
+ reasonCode: opts.reasonCode ?? "build_failed",
107155
+ ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107156
+ };
107309
107157
  }
107310
- async function dev(ctx, input, callbacks) {
107311
- const config2 = await loadConfig2(ctx, input.app);
107312
- const app = await ensureApp(ctx, config2);
107313
- await callbacks.writeLine(`App: ${app.url}`);
107314
- await callbacks.writeLine("Watching for changes (logs streaming below)...");
107315
- await callbacks.writeLine("");
107316
- const logStream = { ctrl: null };
107317
- function startLogStream(appId) {
107318
- logStream.ctrl?.abort();
107319
- logStream.ctrl = new AbortController;
107320
- const linkedSignal = logStream.ctrl.signal;
107321
- streamLogs(ctx, appId, callbacks, linkedSignal);
107158
+ function recoveryRetry(opts) {
107159
+ return {
107160
+ retryable: true,
107161
+ nextAction: "retry",
107162
+ suggestedTool: opts.suggestedTool,
107163
+ args: opts.args,
107164
+ reasonCode: opts.reasonCode
107165
+ };
107166
+ }
107167
+ function recoveryAsk(opts) {
107168
+ if (!opts.prompt || opts.prompt.trim().length === 0) {
107169
+ throw new Error("recoveryAsk requires a non-empty prompt");
107322
107170
  }
107323
- startLogStream(app.id);
107324
- let busy = false;
107325
- let queued = null;
107326
- async function handleChange(files) {
107327
- if (busy) {
107328
- queued = files;
107329
- return;
107330
- }
107331
- busy = true;
107332
- try {
107333
- const shortList = files.length <= 3 ? files.join(", ") : `${files.slice(0, 3).join(", ")} +${files.length - 3} more`;
107334
- await callbacks.writeLine(`Change detected: ${shortList}`);
107335
- logStream.ctrl?.abort();
107336
- const shouldPush = await callbacks.onFilesChanged(files);
107337
- if (shouldPush) {
107338
- try {
107339
- const freshConfig = await loadConfig2(ctx, input.app);
107340
- const result = await doPush(ctx, freshConfig, callbacks);
107341
- if (result.success) {
107342
- startLogStream(result.appId);
107343
- } else {
107344
- startLogStream(app.id);
107171
+ return {
107172
+ retryable: opts.retryable ?? false,
107173
+ nextAction: "ask_user",
107174
+ prompt: opts.prompt,
107175
+ ...opts.options !== undefined ? { options: opts.options } : {},
107176
+ reasonCode: opts.reasonCode
107177
+ };
107178
+ }
107179
+ function recoveryInspectBuildLog(opts) {
107180
+ const args = opts.deployId ? { id: opts.deployId, app: opts.app } : { latestFailed: true, app: opts.app };
107181
+ return {
107182
+ retryable: false,
107183
+ nextAction: "inspect_build_log",
107184
+ suggestedTool: "percher_deploys_inspect",
107185
+ args,
107186
+ reasonCode: opts.reasonCode ?? "build_failed",
107187
+ ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107188
+ };
107189
+ }
107190
+ var RECOVERY_NONE = recoveryNone();
107191
+ var RECOVERY_NEEDS_LOGIN = recoveryLogin();
107192
+ function buildProblemToRecoveryProblem(p) {
107193
+ const file2 = p.file ?? "";
107194
+ const message = p.hint && p.hint.length > 0 ? `${p.message}
107195
+ Hint: ${p.hint}` : p.message;
107196
+ return {
107197
+ file: file2,
107198
+ ...typeof p.line === "number" ? { line: p.line } : {},
107199
+ ...typeof p.column === "number" ? { column: p.column } : {},
107200
+ message
107201
+ };
107202
+ }
107203
+ function recoveryFromErrorClass(errorClass, failure = {}) {
107204
+ switch (errorClass) {
107205
+ case "missing_env":
107206
+ return recoveryEnv({
107207
+ keys: failure.missingEnvVars ?? [],
107208
+ reasonCode: "missing_env"
107209
+ });
107210
+ case "infra_unavailable":
107211
+ return recoveryRetry({
107212
+ suggestedTool: "percher_publish",
107213
+ args: failure.publishInput ?? {},
107214
+ reasonCode: "infra_unavailable"
107215
+ });
107216
+ case "platform_at_capacity":
107217
+ return recoveryRetry({
107218
+ suggestedTool: "percher_publish",
107219
+ args: failure.publishInput ?? {},
107220
+ reasonCode: "infra_transient"
107221
+ });
107222
+ case "config_invalid":
107223
+ return recoveryFixConfig({
107224
+ problems: [
107225
+ {
107226
+ file: "percher.toml",
107227
+ message: "percher.toml failed validation — fix the file and re-run."
107345
107228
  }
107346
- } catch (err) {
107347
- await callbacks.writeLine(`Push error: ${err.message}`);
107348
- startLogStream(app.id);
107349
- }
107350
- } else {
107351
- startLogStream(app.id);
107229
+ ],
107230
+ reasonCode: "config_invalid"
107231
+ });
107232
+ case "build_failed":
107233
+ case "missing_dependency":
107234
+ case "health_check_timeout":
107235
+ case "port_mismatch":
107236
+ case "permission_denied":
107237
+ case "runtime_crash":
107238
+ case "oom_killed": {
107239
+ const fileProblems = (failure.problems ?? []).filter((p) => p.file);
107240
+ const reproduceAlt = failure.deployId ? {
107241
+ action: "reproduce_locally",
107242
+ suggestedTool: "percher_reproduce",
107243
+ args: { deployId: failure.deployId, app: failure.appName },
107244
+ when: "after the primary path didn't surface a clear fix and the user has nixpacks installed locally"
107245
+ } : undefined;
107246
+ if (fileProblems.length > 0) {
107247
+ return recoveryFixProblems({
107248
+ problems: fileProblems.map(buildProblemToRecoveryProblem),
107249
+ reasonCode: "build_failed",
107250
+ ...reproduceAlt ? {
107251
+ alternativeActions: [
107252
+ {
107253
+ ...reproduceAlt,
107254
+ when: "after fixing the file-located problems didn't make the build pass and the user has nixpacks installed locally"
107255
+ }
107256
+ ]
107257
+ } : {}
107258
+ });
107352
107259
  }
107353
- } finally {
107354
- busy = false;
107355
- if (queued) {
107356
- const next = queued;
107357
- queued = null;
107358
- await handleChange(next);
107260
+ const inspectAlt = {
107261
+ action: "inspect_build_log",
107262
+ suggestedTool: "percher_deploys_inspect",
107263
+ args: failure.deployId ? { id: failure.deployId, app: failure.appName } : { latestFailed: true, app: failure.appName },
107264
+ when: "after percher_doctor's recovery didn't yield a fix and you want to read the raw build log directly"
107265
+ };
107266
+ const alternativeActions = [inspectAlt];
107267
+ if (reproduceAlt) {
107268
+ alternativeActions.push({
107269
+ ...reproduceAlt,
107270
+ when: "after percher_doctor + percher_deploys_inspect didn't surface a clear fix and the user has nixpacks installed locally"
107271
+ });
107359
107272
  }
107273
+ return recoveryDoctor({
107274
+ app: failure.appName,
107275
+ deployId: failure.deployId,
107276
+ mode: "deploy",
107277
+ reasonCode: "build_failed",
107278
+ alternativeActions
107279
+ });
107360
107280
  }
107281
+ default:
107282
+ return recoveryAsk({
107283
+ prompt: "Unexpected failure — Percher couldn't classify this error. Try `percher doctor` for diagnostics, or surface the error to the user.",
107284
+ reasonCode: "unknown"
107285
+ });
107361
107286
  }
107362
- const devConfig = config2.dev;
107363
- const watcher = createWatcher({
107364
- cwd: ctx.cwd,
107365
- extraIgnore: devConfig?.ignore,
107366
- debounceMs: devConfig?.debounce,
107367
- onChange(files) {
107368
- handleChange(files);
107369
- }
107370
- });
107371
- try {
107372
- await new Promise((resolve7) => {
107373
- if (callbacks.signal.aborted) {
107374
- resolve7();
107375
- return;
107376
- }
107377
- callbacks.signal.addEventListener("abort", () => resolve7(), { once: true });
107378
- });
107379
- } finally {
107380
- logStream.ctrl?.abort();
107381
- watcher.close();
107382
- }
107383
- }
107384
- // ../core/dist/commands/diagnose.js
107385
- var diagnoseInputSchema = exports_external.object({
107386
- app: exports_external.string().optional(),
107387
- logLines: exports_external.number().int().min(0).max(200).optional(),
107388
- recencyHours: exports_external.number().positive().max(24 * 30).optional()
107389
- });
107390
- // ../core/dist/commands/diff.js
107391
- var diffInputSchema = exports_external.object({
107392
- app: exports_external.string().optional(),
107393
- base: exports_external.string().optional(),
107394
- head: exports_external.string().optional(),
107395
- patch: exports_external.boolean().optional()
107396
- });
107397
- async function diff(ctx, input = {}) {
107398
- const name = input.app ?? readPercherTomlAppName(ctx.cwd);
107399
- if (!name) {
107400
- throw new PercherCoreError("No app specified and no percher.toml found", {
107401
- code: "diff.no-app",
107402
- hint: "Pass --app <name> or run from a directory with percher.toml."
107403
- });
107404
- }
107405
- return ctx.client.apps.getChanges(name, {
107406
- base: input.base,
107407
- head: input.head,
107408
- patch: input.patch
107409
- });
107410
107287
  }
107411
- // ../core/dist/commands/doctor.js
107412
- import { existsSync as existsSync9 } from "node:fs";
107413
- import { join as join13 } from "node:path";
107288
+
107289
+ // ../core/dist/commands/publish-failure.js
107290
+ import { join as join9 } from "node:path";
107414
107291
 
107415
107292
  // ../core/dist/error-classifier.js
107416
107293
  function extractEnvVarsFromLog(combined) {
@@ -107744,219 +107621,6 @@ ${buildLog ?? ""}`;
107744
107621
  return null;
107745
107622
  }
107746
107623
 
107747
- // ../core/dist/recovery.js
107748
- function recoveryNone(opts = {}) {
107749
- return {
107750
- retryable: false,
107751
- nextAction: "none",
107752
- reasonCode: opts.reasonCode ?? "none",
107753
- ...opts.url !== undefined ? { url: opts.url } : {}
107754
- };
107755
- }
107756
- function recoveryLogin(opts = {}) {
107757
- return {
107758
- retryable: false,
107759
- nextAction: "open_login",
107760
- suggestedTool: "percher_login",
107761
- args: {},
107762
- reasonCode: opts.reasonCode ?? "auth_required"
107763
- };
107764
- }
107765
- function recoveryWait(opts) {
107766
- return {
107767
- retryable: false,
107768
- nextAction: "wait_deploy",
107769
- suggestedTool: "percher_wait_for_deploy",
107770
- args: {
107771
- app: opts.app,
107772
- deployId: opts.deployId,
107773
- timeoutSeconds: opts.timeoutSeconds ?? TIMEOUTS.mcpWaitDeployDefault / 1000
107774
- },
107775
- reasonCode: opts.reasonCode
107776
- };
107777
- }
107778
- function recoveryDoctor(opts) {
107779
- const args = {};
107780
- if (opts.app !== undefined)
107781
- args.app = opts.app;
107782
- if (opts.deployId !== undefined)
107783
- args.deployId = opts.deployId;
107784
- if (opts.mode !== undefined)
107785
- args.mode = opts.mode;
107786
- return {
107787
- retryable: false,
107788
- nextAction: "run_doctor",
107789
- suggestedTool: "percher_doctor",
107790
- args,
107791
- reasonCode: opts.reasonCode,
107792
- ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107793
- };
107794
- }
107795
- function recoveryEnv(opts) {
107796
- const args = {};
107797
- if (opts.app !== undefined)
107798
- args.app = opts.app;
107799
- if (opts.keys.length > 0)
107800
- args.keys = opts.keys;
107801
- return {
107802
- retryable: false,
107803
- nextAction: "set_env_vars",
107804
- suggestedTool: "percher_env_set",
107805
- args,
107806
- reasonCode: opts.reasonCode ?? "missing_env"
107807
- };
107808
- }
107809
- function recoveryFixConfig(opts) {
107810
- return {
107811
- retryable: false,
107812
- nextAction: "fix_config",
107813
- problems: opts.problems,
107814
- reasonCode: opts.reasonCode ?? "config_invalid"
107815
- };
107816
- }
107817
- function recoveryFixProblems(opts) {
107818
- return {
107819
- retryable: false,
107820
- nextAction: "fix_problems",
107821
- problems: opts.problems,
107822
- reasonCode: opts.reasonCode ?? "build_failed",
107823
- ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107824
- };
107825
- }
107826
- function recoveryRetry(opts) {
107827
- return {
107828
- retryable: true,
107829
- nextAction: "retry",
107830
- suggestedTool: opts.suggestedTool,
107831
- args: opts.args,
107832
- reasonCode: opts.reasonCode
107833
- };
107834
- }
107835
- function recoveryAsk(opts) {
107836
- if (!opts.prompt || opts.prompt.trim().length === 0) {
107837
- throw new Error("recoveryAsk requires a non-empty prompt");
107838
- }
107839
- return {
107840
- retryable: opts.retryable ?? false,
107841
- nextAction: "ask_user",
107842
- prompt: opts.prompt,
107843
- ...opts.options !== undefined ? { options: opts.options } : {},
107844
- reasonCode: opts.reasonCode
107845
- };
107846
- }
107847
- function recoveryInspectBuildLog(opts) {
107848
- const args = opts.deployId ? { id: opts.deployId, app: opts.app } : { latestFailed: true, app: opts.app };
107849
- return {
107850
- retryable: false,
107851
- nextAction: "inspect_build_log",
107852
- suggestedTool: "percher_deploys_inspect",
107853
- args,
107854
- reasonCode: opts.reasonCode ?? "build_failed",
107855
- ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107856
- };
107857
- }
107858
- var RECOVERY_NONE = recoveryNone();
107859
- var RECOVERY_NEEDS_LOGIN = recoveryLogin();
107860
- function buildProblemToRecoveryProblem(p) {
107861
- const file2 = p.file ?? "";
107862
- const message = p.hint && p.hint.length > 0 ? `${p.message}
107863
- Hint: ${p.hint}` : p.message;
107864
- return {
107865
- file: file2,
107866
- ...typeof p.line === "number" ? { line: p.line } : {},
107867
- ...typeof p.column === "number" ? { column: p.column } : {},
107868
- message
107869
- };
107870
- }
107871
- function recoveryFromErrorClass(errorClass, failure = {}) {
107872
- switch (errorClass) {
107873
- case "missing_env":
107874
- return recoveryEnv({
107875
- keys: failure.missingEnvVars ?? [],
107876
- reasonCode: "missing_env"
107877
- });
107878
- case "infra_unavailable":
107879
- return recoveryRetry({
107880
- suggestedTool: "percher_publish",
107881
- args: failure.publishInput ?? {},
107882
- reasonCode: "infra_unavailable"
107883
- });
107884
- case "platform_at_capacity":
107885
- return recoveryRetry({
107886
- suggestedTool: "percher_publish",
107887
- args: failure.publishInput ?? {},
107888
- reasonCode: "infra_transient"
107889
- });
107890
- case "config_invalid":
107891
- return recoveryFixConfig({
107892
- problems: [
107893
- {
107894
- file: "percher.toml",
107895
- message: "percher.toml failed validation — fix the file and re-run."
107896
- }
107897
- ],
107898
- reasonCode: "config_invalid"
107899
- });
107900
- case "build_failed":
107901
- case "missing_dependency":
107902
- case "health_check_timeout":
107903
- case "port_mismatch":
107904
- case "permission_denied":
107905
- case "runtime_crash":
107906
- case "oom_killed": {
107907
- const fileProblems = (failure.problems ?? []).filter((p) => p.file);
107908
- const reproduceAlt = failure.deployId ? {
107909
- action: "reproduce_locally",
107910
- suggestedTool: "percher_reproduce",
107911
- args: { deployId: failure.deployId, app: failure.appName },
107912
- when: "after the primary path didn't surface a clear fix and the user has nixpacks installed locally"
107913
- } : undefined;
107914
- if (fileProblems.length > 0) {
107915
- return recoveryFixProblems({
107916
- problems: fileProblems.map(buildProblemToRecoveryProblem),
107917
- reasonCode: "build_failed",
107918
- ...reproduceAlt ? {
107919
- alternativeActions: [
107920
- {
107921
- ...reproduceAlt,
107922
- when: "after fixing the file-located problems didn't make the build pass and the user has nixpacks installed locally"
107923
- }
107924
- ]
107925
- } : {}
107926
- });
107927
- }
107928
- const inspectAlt = {
107929
- action: "inspect_build_log",
107930
- suggestedTool: "percher_deploys_inspect",
107931
- args: failure.deployId ? { id: failure.deployId, app: failure.appName } : { latestFailed: true, app: failure.appName },
107932
- when: "after percher_doctor's recovery didn't yield a fix and you want to read the raw build log directly"
107933
- };
107934
- const alternativeActions = [inspectAlt];
107935
- if (reproduceAlt) {
107936
- alternativeActions.push({
107937
- ...reproduceAlt,
107938
- when: "after percher_doctor + percher_deploys_inspect didn't surface a clear fix and the user has nixpacks installed locally"
107939
- });
107940
- }
107941
- return recoveryDoctor({
107942
- app: failure.appName,
107943
- deployId: failure.deployId,
107944
- mode: "deploy",
107945
- reasonCode: "build_failed",
107946
- alternativeActions
107947
- });
107948
- }
107949
- default:
107950
- return recoveryAsk({
107951
- prompt: "Unexpected failure — Percher couldn't classify this error. Try `percher doctor` for diagnostics, or surface the error to the user.",
107952
- reasonCode: "unknown"
107953
- });
107954
- }
107955
- }
107956
-
107957
- // ../core/dist/commands/publish-failure.js
107958
- import { join as join12 } from "node:path";
107959
-
107960
107624
  // ../core/dist/structured-error-codes.js
107961
107625
  var MAPPINGS = {
107962
107626
  WORKER_UNAVAILABLE: {
@@ -108033,7 +107697,7 @@ function trimMessage(raw) {
108033
107697
 
108034
107698
  // ../core/dist/commands/publish-failure.js
108035
107699
  function tomlPathFor(cwd) {
108036
- return join12(cwd, "percher.toml");
107700
+ return join9(cwd, "percher.toml");
108037
107701
  }
108038
107702
  async function buildFailureError(ctx, app, deployment, signal) {
108039
107703
  let buildLogTail;
@@ -108047,88 +107711,560 @@ async function buildFailureError(ctx, app, deployment, signal) {
108047
107711
  fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
108048
107712
  }
108049
107713
  }
108050
- if (fullLog) {
108051
- const lines = fullLog.split(`
108052
- `);
108053
- buildLogTail = lines.slice(-50).join(`
108054
- `);
108055
- }
108056
- } catch {
108057
- buildLogFetchFailed = true;
107714
+ if (fullLog) {
107715
+ const lines = fullLog.split(`
107716
+ `);
107717
+ buildLogTail = lines.slice(-50).join(`
107718
+ `);
107719
+ }
107720
+ } catch {
107721
+ buildLogFetchFailed = true;
107722
+ }
107723
+ const errorMessage = deployment.errorMessage ?? "";
107724
+ const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError(errorMessage || "Deployment failed", fullLog);
107725
+ const rawErrorMessage = errorMessage && !buildLogTail && !classified?.explanation.includes(errorMessage) ? errorMessage : undefined;
107726
+ const problems = deployment.problems && deployment.problems.length > 0 ? deployment.problems : undefined;
107727
+ if (classified) {
107728
+ return {
107729
+ title: classified.title,
107730
+ explanation: classified.explanation,
107731
+ suggestion: classified.suggestion,
107732
+ buildLogTail,
107733
+ rawErrorMessage,
107734
+ errorClass: classified.errorClass,
107735
+ phase: classified.phase,
107736
+ cause: classified.cause,
107737
+ relevantFiles: classified.relevantFiles,
107738
+ missingEnvVars: classified.missingEnvVars,
107739
+ problems
107740
+ };
107741
+ }
107742
+ return {
107743
+ title: "Deployment failed",
107744
+ explanation: errorMessage ? `The deployment failed: ${errorMessage}` : "The deployment failed. See the build log for details.",
107745
+ suggestion: buildLogTail ? "Check the build log below for errors. Run your build command locally to reproduce, fix, and publish again." : buildLogFetchFailed ? `The deploy failed and we could not retrieve the build log (network/API error after retries). The log itself may exist on the server — fetch it directly with: percher logs ${app.name} --build ${deployment.id}` : errorMessage ? `The deploy failed with: ${errorMessage.split(`
107746
+ `)[0]}. The build log column is empty — try \`percher logs ${app.name} --build ${deployment.id}\` to fetch it directly. If it remains empty, run \`percher deploys inspect ${deployment.id}\` for full details.` : `No build log was produced — the failure happened before the build started. This usually means a Percher infra issue. Retry once, and if it persists, fetch the deploy details with: percher deploys inspect ${deployment.id}`,
107747
+ buildLogTail,
107748
+ errorClass: "build_failed",
107749
+ phase: "build",
107750
+ cause: "unknown",
107751
+ relevantFiles: [],
107752
+ missingEnvVars: [],
107753
+ problems
107754
+ };
107755
+ }
107756
+ async function classifyDeploymentFailure(opts) {
107757
+ const { ctx, app, deployment, publishInput } = opts;
107758
+ const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
107759
+ const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
107760
+ missingEnvVars: error48.missingEnvVars,
107761
+ deployId: deployment.id,
107762
+ appName: app.name,
107763
+ publishInput,
107764
+ problems: error48.problems
107765
+ }) : recoveryAsk({
107766
+ prompt: `${app.name} deploy ${deployment.id} failed: ${error48.title}. ${error48.suggestion}`.trim(),
107767
+ reasonCode: "build_failed"
107768
+ });
107769
+ return {
107770
+ error: error48,
107771
+ recovery,
107772
+ summary: `${error48.title} (${app.name}, deploy ${deployment.id}).`
107773
+ };
107774
+ }
107775
+ async function buildFailureResult(opts) {
107776
+ const { ctx, app, deployment, tarball, input } = opts;
107777
+ const { error: error48, recovery, summary } = await classifyDeploymentFailure({
107778
+ ctx,
107779
+ app,
107780
+ deployment,
107781
+ publishInput: input
107782
+ });
107783
+ return {
107784
+ status: "failed",
107785
+ app,
107786
+ deployment,
107787
+ fileCount: tarball.fileCount,
107788
+ bytes: tarball.bytes,
107789
+ error: error48,
107790
+ recovery,
107791
+ summary,
107792
+ configPath: tomlPathFor(ctx.cwd),
107793
+ bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
107794
+ };
107795
+ }
107796
+
107797
+ // ../core/dist/commands/deploy-repo.js
107798
+ var deployRepoInputSchema = exports_external.object({
107799
+ app: exports_external.string().describe("App name or id (required — hosted has no cwd)."),
107800
+ branch: exports_external.string().optional().describe("Branch to deploy. Defaults to the app's tracked branch."),
107801
+ note: exports_external.string().optional().describe("Optional note recorded against the deploy.")
107802
+ });
107803
+ // ../core/dist/commands/deploys.js
107804
+ var deploysListInputSchema = exports_external.object({
107805
+ app: exports_external.string().optional(),
107806
+ limit: exports_external.number().int().positive().max(100).optional()
107807
+ });
107808
+ var deploysInspectInputSchema = exports_external.object({
107809
+ app: exports_external.string().optional(),
107810
+ id: exports_external.string().optional(),
107811
+ latestFailed: exports_external.boolean().optional()
107812
+ }).superRefine((val, ctx) => {
107813
+ if (!val.id && !val.latestFailed) {
107814
+ ctx.addIssue({
107815
+ code: exports_external.ZodIssueCode.custom,
107816
+ message: "either `id` or `latestFailed: true` must be provided"
107817
+ });
107818
+ }
107819
+ });
107820
+ function resolveAppName(ctx, app) {
107821
+ const name = app ?? readPercherTomlAppName(ctx.cwd);
107822
+ if (!name) {
107823
+ throw new PercherCoreError("No app specified and no percher.toml found", {
107824
+ code: "deploys.no-app",
107825
+ hint: "Pass --app <name> or run from a directory with percher.toml."
107826
+ });
107827
+ }
107828
+ return name;
107829
+ }
107830
+ async function deploysList(ctx, input = {}) {
107831
+ const name = resolveAppName(ctx, input.app);
107832
+ return ctx.client.apps.listDeploys(name, { limit: input.limit ?? 10 });
107833
+ }
107834
+ async function deploysInspect(ctx, input) {
107835
+ const name = resolveAppName(ctx, input.app);
107836
+ let deployId = input.id;
107837
+ if (!deployId && input.latestFailed) {
107838
+ const recent = await ctx.client.apps.listDeploys(name, { limit: 25 });
107839
+ const failed = recent.find((d) => d.status === "failed");
107840
+ if (!failed) {
107841
+ throw new PercherCoreError(`No failed deploys found for "${name}"`, {
107842
+ code: "deploys.no-failed",
107843
+ hint: "Try `percher deploys` to list recent deploys, or omit --latest-failed."
107844
+ });
107845
+ }
107846
+ deployId = failed.id;
107847
+ }
107848
+ if (!deployId) {
107849
+ throw new PercherCoreError("Deploy ID is required", {
107850
+ code: "deploys.no-id"
107851
+ });
107852
+ }
107853
+ const deploy = await ctx.client.apps.getDeployment(name, deployId);
107854
+ let buildLog = null;
107855
+ try {
107856
+ const log = await ctx.client.apps.getBuildLog(name, deployId);
107857
+ buildLog = log.length > 0 ? log : null;
107858
+ } catch (err) {
107859
+ if (err instanceof PercherApiError && err.status === 404) {
107860
+ buildLog = null;
107861
+ } else {
107862
+ throw err;
107863
+ }
107864
+ }
107865
+ return { deploy, buildLog };
107866
+ }
107867
+ // ../core/dist/commands/dev.js
107868
+ import { existsSync as existsSync8 } from "node:fs";
107869
+ import { join as join12 } from "node:path";
107870
+
107871
+ // ../core/dist/tarball.js
107872
+ var import_ignore = __toESM(require_ignore(), 1);
107873
+ import { createHash as createHash2 } from "node:crypto";
107874
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
107875
+ import { join as join10, relative as relative3, sep as sep2 } from "node:path";
107876
+ import { Readable } from "node:stream";
107877
+ import { createGzip } from "node:zlib";
107878
+
107879
+ // ../../node_modules/.bun/tar-stream@3.1.8/node_modules/tar-stream/index.js
107880
+ var $extract = require_extract();
107881
+ var $pack = require_pack();
107882
+
107883
+ // ../core/dist/tarball.js
107884
+ function isFilesOptions(options) {
107885
+ return "files" in options;
107886
+ }
107887
+ var DEFAULT_IGNORE = [
107888
+ "node_modules",
107889
+ ".git",
107890
+ "dist",
107891
+ ".next",
107892
+ ".svelte-kit",
107893
+ "build",
107894
+ "coverage",
107895
+ ".env",
107896
+ ".env.*",
107897
+ ".bun",
107898
+ "*.log",
107899
+ ".DS_Store"
107900
+ ];
107901
+ function* walk(dir, baseDir, ig) {
107902
+ const entries = readdirSync2(dir);
107903
+ for (const name of entries) {
107904
+ const full = join10(dir, name);
107905
+ const rel = relative3(baseDir, full).split(sep2).join("/");
107906
+ if (ig.ignores(rel))
107907
+ continue;
107908
+ const stats = statSync2(full);
107909
+ if (stats.isDirectory()) {
107910
+ if (ig.ignores(`${rel}/`))
107911
+ continue;
107912
+ yield* walk(full, baseDir, ig);
107913
+ } else if (stats.isFile()) {
107914
+ yield rel;
107915
+ }
107916
+ }
107917
+ }
107918
+ function normalizeInlinePath(raw) {
107919
+ const path = raw.replace(/\\/g, "/");
107920
+ if (path.length === 0) {
107921
+ throw new Error("file path must not be empty");
107922
+ }
107923
+ if (path.startsWith("/")) {
107924
+ throw new Error(`file path must be relative, got absolute path: ${raw}`);
107925
+ }
107926
+ if (/^[a-zA-Z]:/.test(path)) {
107927
+ throw new Error(`file path must be relative, got drive path: ${raw}`);
107928
+ }
107929
+ const segments = path.split("/");
107930
+ if (segments.some((s) => s === "..")) {
107931
+ throw new Error(`file path must not contain "..": ${raw}`);
107932
+ }
107933
+ const cleaned = segments.filter((s) => s !== "" && s !== ".").join("/");
107934
+ if (cleaned.length === 0) {
107935
+ throw new Error(`file path resolves to nothing: ${raw}`);
107936
+ }
107937
+ return cleaned;
107938
+ }
107939
+ function buildTarball(fileBuffers) {
107940
+ fileBuffers.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
107941
+ const totalBytes = fileBuffers.reduce((sum, f) => sum + f.size, 0);
107942
+ const contentHasher = createHash2("sha256");
107943
+ for (const f of fileBuffers) {
107944
+ contentHasher.update(`PATH:${f.path.length}:${f.path}
107945
+ `);
107946
+ contentHasher.update(`SIZE:${f.size}
107947
+ `);
107948
+ contentHasher.update(f.data);
107949
+ contentHasher.update(`
107950
+ `);
107951
+ }
107952
+ const contentHash = contentHasher.digest("hex");
107953
+ const pack = $pack();
107954
+ (async () => {
107955
+ for (const file2 of fileBuffers) {
107956
+ await new Promise((resolve7, reject) => {
107957
+ pack.entry({ name: file2.path, size: file2.data.length }, file2.data, (err) => err ? reject(err) : resolve7());
107958
+ });
107959
+ }
107960
+ pack.finalize();
107961
+ })();
107962
+ const gzip = createGzip();
107963
+ const nodeStream = pack.pipe(gzip);
107964
+ const webStream = Readable.toWeb(nodeStream);
107965
+ const topFiles = [...fileBuffers].sort((a, b) => b.size - a.size).slice(0, 5).map((f) => ({ path: f.path, size: f.size }));
107966
+ return {
107967
+ stream: webStream,
107968
+ bytes: totalBytes,
107969
+ fileCount: fileBuffers.length,
107970
+ topFiles,
107971
+ contentHash
107972
+ };
107973
+ }
107974
+ async function createTarball(options) {
107975
+ if (isFilesOptions(options)) {
107976
+ return createTarballFromFiles(options.files, options.config);
107977
+ }
107978
+ const ig = import_ignore.default();
107979
+ ig.add(DEFAULT_IGNORE);
107980
+ const gitignorePath = join10(options.cwd, ".gitignore");
107981
+ if (existsSync6(gitignorePath)) {
107982
+ ig.add(readFileSync6(gitignorePath, "utf8"));
107983
+ }
107984
+ if (options.extraIgnore)
107985
+ ig.add(options.extraIgnore);
107986
+ const fileBuffers = [];
107987
+ for (const rel of walk(options.cwd, options.cwd, ig)) {
107988
+ const data2 = readFileSync6(join10(options.cwd, rel));
107989
+ fileBuffers.push({ path: rel, size: data2.length, data: data2 });
107990
+ }
107991
+ return buildTarball(fileBuffers);
107992
+ }
107993
+ function createTarballFromFiles(files, _config) {
107994
+ const seen = new Set;
107995
+ const fileBuffers = [];
107996
+ for (const file2 of files) {
107997
+ const path = normalizeInlinePath(file2.path);
107998
+ if (seen.has(path)) {
107999
+ throw new Error(`duplicate file path in inline source: ${path}`);
108000
+ }
108001
+ seen.add(path);
108002
+ const data2 = Buffer.from(file2.data);
108003
+ fileBuffers.push({ path, size: data2.length, data: data2 });
108004
+ }
108005
+ return buildTarball(fileBuffers);
108006
+ }
108007
+
108008
+ // ../core/dist/watcher.js
108009
+ var import_ignore2 = __toESM(require_ignore(), 1);
108010
+ import { existsSync as existsSync7, readFileSync as readFileSync7, watch } from "node:fs";
108011
+ import { join as join11, sep as sep3 } from "node:path";
108012
+ var DEFAULT_IGNORE2 = [
108013
+ "node_modules",
108014
+ ".git",
108015
+ "dist",
108016
+ ".next",
108017
+ ".svelte-kit",
108018
+ "build",
108019
+ "coverage",
108020
+ ".env",
108021
+ ".env.*",
108022
+ ".bun",
108023
+ "*.log",
108024
+ ".DS_Store",
108025
+ "__pycache__"
108026
+ ];
108027
+ function createWatcher(options) {
108028
+ const debounceMs = options.debounceMs ?? 500;
108029
+ const ig = import_ignore2.default();
108030
+ ig.add(DEFAULT_IGNORE2);
108031
+ const gitignorePath = join11(options.cwd, ".gitignore");
108032
+ if (existsSync7(gitignorePath)) {
108033
+ ig.add(readFileSync7(gitignorePath, "utf8"));
108034
+ }
108035
+ if (options.extraIgnore)
108036
+ ig.add(options.extraIgnore);
108037
+ let timer = null;
108038
+ let pending = new Set;
108039
+ const fsWatcher = watch(options.cwd, { recursive: true }, (_event, filename) => {
108040
+ if (!filename)
108041
+ return;
108042
+ const rel = filename.split(sep3).join("/");
108043
+ if (ig.ignores(rel))
108044
+ return;
108045
+ pending.add(rel);
108046
+ if (timer)
108047
+ clearTimeout(timer);
108048
+ timer = setTimeout(() => {
108049
+ const files = [...pending];
108050
+ pending = new Set;
108051
+ timer = null;
108052
+ if (files.length > 0) {
108053
+ options.onChange(files);
108054
+ }
108055
+ }, debounceMs);
108056
+ });
108057
+ return {
108058
+ close() {
108059
+ if (timer)
108060
+ clearTimeout(timer);
108061
+ fsWatcher.close();
108062
+ }
108063
+ };
108064
+ }
108065
+
108066
+ // ../core/dist/commands/dev.js
108067
+ var devInputSchema = exports_external.object({
108068
+ app: exports_external.string().optional()
108069
+ });
108070
+ async function loadConfig2(ctx, appOverride) {
108071
+ const tomlPath = join12(ctx.cwd, "percher.toml");
108072
+ if (!existsSync8(tomlPath)) {
108073
+ throw new PercherCoreError("No percher.toml found. Run: percher init", {
108074
+ code: "dev.no-app",
108075
+ hint: "Create a percher.toml first with `percher init`."
108076
+ });
108077
+ }
108078
+ const config2 = await parseFile(tomlPath);
108079
+ if (appOverride)
108080
+ return { ...config2, app: { ...config2.app, name: appOverride } };
108081
+ return config2;
108082
+ }
108083
+ async function ensureApp(ctx, config2) {
108084
+ try {
108085
+ return await ctx.client.apps.get(config2.app.name);
108086
+ } catch (err) {
108087
+ if (err.code === "APP_NOT_FOUND") {
108088
+ return ctx.client.apps.create({
108089
+ name: config2.app.name,
108090
+ runtime: config2.app.runtime,
108091
+ framework: config2.app.framework
108092
+ });
108093
+ }
108094
+ throw err;
108095
+ }
108096
+ }
108097
+ async function doPush(ctx, config2, callbacks) {
108098
+ const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
108099
+ const app = await ensureApp(ctx, config2);
108100
+ await callbacks.writeLine(`Pushing ${tarball.fileCount} files (${(tarball.bytes / 1024).toFixed(0)} KB)...`);
108101
+ const deployResponse = await ctx.client.apps.deploy(app.id, { tarball: tarball.stream });
108102
+ if (deployResponse.status === "already_in_progress") {
108103
+ const active = deployResponse;
108104
+ await callbacks.writeLine(` Skipped — deploy already in progress (${active.deployId}, ${active.deployStatus}).`);
108105
+ return { success: false, appId: app.id };
108106
+ }
108107
+ let deployment = deployResponse;
108108
+ const startedAt = Date.now();
108109
+ const timeoutMs = TIMEOUTS.clientPublishPoll;
108110
+ const pollInterval = 2000;
108111
+ let lastStatus = "";
108112
+ while (deployment.status !== "live" && deployment.status !== "failed") {
108113
+ if (Date.now() - startedAt > timeoutMs) {
108114
+ await callbacks.writeLine("Deploy timed out");
108115
+ return { success: false, appId: app.id };
108116
+ }
108117
+ if (callbacks.signal.aborted)
108118
+ return { success: false, appId: app.id };
108119
+ if (deployment.status !== lastStatus) {
108120
+ lastStatus = deployment.status;
108121
+ await callbacks.writeLine(` ${deployment.status}...`);
108122
+ }
108123
+ await new Promise((r) => setTimeout(r, pollInterval));
108124
+ deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
108125
+ }
108126
+ if (deployment.status === "failed") {
108127
+ try {
108128
+ const insp = await ctx.client.apps.getDeployment(app.id, deployment.id);
108129
+ if (insp.errorMessage) {
108130
+ await callbacks.writeLine("Deploy failed:");
108131
+ for (const line of insp.errorMessage.split(`
108132
+ `).slice(0, 30)) {
108133
+ await callbacks.writeLine(` ${line}`);
108134
+ }
108135
+ } else {
108136
+ await callbacks.writeLine("Deploy failed.");
108137
+ }
108138
+ } catch {
108139
+ await callbacks.writeLine("Deploy failed.");
108140
+ }
108141
+ return { success: false, appId: app.id };
108142
+ }
108143
+ await callbacks.writeLine(`Live at ${deployment.url ?? ""}`);
108144
+ return { success: true, appId: app.id };
108145
+ }
108146
+ function formatLogLine(l) {
108147
+ const level = l.level.toUpperCase().padEnd(5);
108148
+ const src = l.source === "runtime" ? "" : `[${l.source}] `;
108149
+ return ` ${src}${level} ${l.message}`;
108150
+ }
108151
+ async function streamLogs(ctx, appId, callbacks, signal) {
108152
+ try {
108153
+ for await (const line of ctx.client.logs.stream(appId, { signal })) {
108154
+ if (signal.aborted)
108155
+ return;
108156
+ await callbacks.writeLine(formatLogLine(line));
108157
+ }
108158
+ } catch (err) {
108159
+ if (signal.aborted)
108160
+ return;
108161
+ await callbacks.writeLine(`Log stream error: ${err.message}`);
108162
+ }
108163
+ }
108164
+ async function dev(ctx, input, callbacks) {
108165
+ const config2 = await loadConfig2(ctx, input.app);
108166
+ const app = await ensureApp(ctx, config2);
108167
+ await callbacks.writeLine(`App: ${app.url}`);
108168
+ await callbacks.writeLine("Watching for changes (logs streaming below)...");
108169
+ await callbacks.writeLine("");
108170
+ const logStream = { ctrl: null };
108171
+ function startLogStream(appId) {
108172
+ logStream.ctrl?.abort();
108173
+ logStream.ctrl = new AbortController;
108174
+ const linkedSignal = logStream.ctrl.signal;
108175
+ streamLogs(ctx, appId, callbacks, linkedSignal);
108176
+ }
108177
+ startLogStream(app.id);
108178
+ let busy = false;
108179
+ let queued = null;
108180
+ async function handleChange(files) {
108181
+ if (busy) {
108182
+ queued = files;
108183
+ return;
108184
+ }
108185
+ busy = true;
108186
+ try {
108187
+ const shortList = files.length <= 3 ? files.join(", ") : `${files.slice(0, 3).join(", ")} +${files.length - 3} more`;
108188
+ await callbacks.writeLine(`Change detected: ${shortList}`);
108189
+ logStream.ctrl?.abort();
108190
+ const shouldPush = await callbacks.onFilesChanged(files);
108191
+ if (shouldPush) {
108192
+ try {
108193
+ const freshConfig = await loadConfig2(ctx, input.app);
108194
+ const result = await doPush(ctx, freshConfig, callbacks);
108195
+ if (result.success) {
108196
+ startLogStream(result.appId);
108197
+ } else {
108198
+ startLogStream(app.id);
108199
+ }
108200
+ } catch (err) {
108201
+ await callbacks.writeLine(`Push error: ${err.message}`);
108202
+ startLogStream(app.id);
108203
+ }
108204
+ } else {
108205
+ startLogStream(app.id);
108206
+ }
108207
+ } finally {
108208
+ busy = false;
108209
+ if (queued) {
108210
+ const next = queued;
108211
+ queued = null;
108212
+ await handleChange(next);
108213
+ }
108214
+ }
108058
108215
  }
108059
- const errorMessage = deployment.errorMessage ?? "";
108060
- const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError(errorMessage || "Deployment failed", fullLog);
108061
- const rawErrorMessage = errorMessage && !buildLogTail && !classified?.explanation.includes(errorMessage) ? errorMessage : undefined;
108062
- const problems = deployment.problems && deployment.problems.length > 0 ? deployment.problems : undefined;
108063
- if (classified) {
108064
- return {
108065
- title: classified.title,
108066
- explanation: classified.explanation,
108067
- suggestion: classified.suggestion,
108068
- buildLogTail,
108069
- rawErrorMessage,
108070
- errorClass: classified.errorClass,
108071
- phase: classified.phase,
108072
- cause: classified.cause,
108073
- relevantFiles: classified.relevantFiles,
108074
- missingEnvVars: classified.missingEnvVars,
108075
- problems
108076
- };
108077
- }
108078
- return {
108079
- title: "Deployment failed",
108080
- explanation: errorMessage ? `The deployment failed: ${errorMessage}` : "The deployment failed. See the build log for details.",
108081
- suggestion: buildLogTail ? "Check the build log below for errors. Run your build command locally to reproduce, fix, and publish again." : buildLogFetchFailed ? `The deploy failed and we could not retrieve the build log (network/API error after retries). The log itself may exist on the server — fetch it directly with: percher logs ${app.name} --build ${deployment.id}` : errorMessage ? `The deploy failed with: ${errorMessage.split(`
108082
- `)[0]}. The build log column is empty — try \`percher logs ${app.name} --build ${deployment.id}\` to fetch it directly. If it remains empty, run \`percher deploys inspect ${deployment.id}\` for full details.` : `No build log was produced — the failure happened before the build started. This usually means a Percher infra issue. Retry once, and if it persists, fetch the deploy details with: percher deploys inspect ${deployment.id}`,
108083
- buildLogTail,
108084
- errorClass: "build_failed",
108085
- phase: "build",
108086
- cause: "unknown",
108087
- relevantFiles: [],
108088
- missingEnvVars: [],
108089
- problems
108090
- };
108091
- }
108092
- async function classifyDeploymentFailure(opts) {
108093
- const { ctx, app, deployment, publishInput } = opts;
108094
- const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
108095
- const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
108096
- missingEnvVars: error48.missingEnvVars,
108097
- deployId: deployment.id,
108098
- appName: app.name,
108099
- publishInput,
108100
- problems: error48.problems
108101
- }) : recoveryAsk({
108102
- prompt: `${app.name} deploy ${deployment.id} failed: ${error48.title}. ${error48.suggestion}`.trim(),
108103
- reasonCode: "build_failed"
108216
+ const devConfig = config2.dev;
108217
+ const watcher = createWatcher({
108218
+ cwd: ctx.cwd,
108219
+ extraIgnore: devConfig?.ignore,
108220
+ debounceMs: devConfig?.debounce,
108221
+ onChange(files) {
108222
+ handleChange(files);
108223
+ }
108104
108224
  });
108105
- return {
108106
- error: error48,
108107
- recovery,
108108
- summary: `${error48.title} (${app.name}, deploy ${deployment.id}).`
108109
- };
108225
+ try {
108226
+ await new Promise((resolve7) => {
108227
+ if (callbacks.signal.aborted) {
108228
+ resolve7();
108229
+ return;
108230
+ }
108231
+ callbacks.signal.addEventListener("abort", () => resolve7(), { once: true });
108232
+ });
108233
+ } finally {
108234
+ logStream.ctrl?.abort();
108235
+ watcher.close();
108236
+ }
108110
108237
  }
108111
- async function buildFailureResult(opts) {
108112
- const { ctx, app, deployment, tarball, input } = opts;
108113
- const { error: error48, recovery, summary } = await classifyDeploymentFailure({
108114
- ctx,
108115
- app,
108116
- deployment,
108117
- publishInput: input
108238
+ // ../core/dist/commands/diagnose.js
108239
+ var diagnoseInputSchema = exports_external.object({
108240
+ app: exports_external.string().optional(),
108241
+ logLines: exports_external.number().int().min(0).max(200).optional(),
108242
+ recencyHours: exports_external.number().positive().max(24 * 30).optional()
108243
+ });
108244
+ // ../core/dist/commands/diff.js
108245
+ var diffInputSchema = exports_external.object({
108246
+ app: exports_external.string().optional(),
108247
+ base: exports_external.string().optional(),
108248
+ head: exports_external.string().optional(),
108249
+ patch: exports_external.boolean().optional()
108250
+ });
108251
+ async function diff(ctx, input = {}) {
108252
+ const name = input.app ?? readPercherTomlAppName(ctx.cwd);
108253
+ if (!name) {
108254
+ throw new PercherCoreError("No app specified and no percher.toml found", {
108255
+ code: "diff.no-app",
108256
+ hint: "Pass --app <name> or run from a directory with percher.toml."
108257
+ });
108258
+ }
108259
+ return ctx.client.apps.getChanges(name, {
108260
+ base: input.base,
108261
+ head: input.head,
108262
+ patch: input.patch
108118
108263
  });
108119
- return {
108120
- status: "failed",
108121
- app,
108122
- deployment,
108123
- fileCount: tarball.fileCount,
108124
- bytes: tarball.bytes,
108125
- error: error48,
108126
- recovery,
108127
- summary,
108128
- configPath: tomlPathFor(ctx.cwd),
108129
- bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
108130
- };
108131
108264
  }
108265
+ // ../core/dist/commands/doctor.js
108266
+ import { existsSync as existsSync9 } from "node:fs";
108267
+ import { join as join13 } from "node:path";
108132
108268
 
108133
108269
  // ../core/dist/commands/wait-deploy.js
108134
108270
  var waitForDeployInputSchema = exports_external.object({
@@ -109566,7 +109702,8 @@ FROM caddy:2-alpine AS caddy-source
109566
109702
  FROM alpine:3.21
109567
109703
  COPY --from=caddy-source /usr/bin/caddy /usr/bin/caddy
109568
109704
  RUN apk add --no-cache libcap \\
109569
- && setcap -r /usr/bin/caddy \\
109705
+ && (setcap -r /usr/bin/caddy 2>/dev/null || true) \\
109706
+ && test -z "$(getcap /usr/bin/caddy)" \\
109570
109707
  && apk del libcap \\
109571
109708
  && rm -rf /var/cache/apk/*
109572
109709
  COPY Caddyfile /etc/caddy/Caddyfile
@@ -110226,6 +110363,7 @@ async function logs(ctx, input = {}) {
110226
110363
  }
110227
110364
  // ../core/dist/commands/mcp.js
110228
110365
  var mcpInputSchema = exports_external.object({});
110366
+ var HOSTED_MCP_URL = "https://mcp.percher.app/mcp";
110229
110367
  function mcp(_ctx, _input = {}) {
110230
110368
  return {
110231
110369
  config: {
@@ -110236,19 +110374,43 @@ function mcp(_ctx, _input = {}) {
110236
110374
  }
110237
110375
  }
110238
110376
  },
110377
+ hosted: {
110378
+ config: {
110379
+ mcpServers: {
110380
+ percher: {
110381
+ transport: "http",
110382
+ url: HOSTED_MCP_URL
110383
+ }
110384
+ }
110385
+ },
110386
+ url: HOSTED_MCP_URL,
110387
+ auth: "oauth"
110388
+ },
110239
110389
  installLocations: [
110240
110390
  {
110241
110391
  client: "Claude Code",
110242
- path: "~/.claude.json (under mcpServers, or a per-project .mcp.json)"
110392
+ path: "~/.claude.json (under mcpServers, or a per-project .mcp.json)",
110393
+ note: "Local stdio — recommended for Claude Code (full tool set, incl. publish from local files)."
110394
+ },
110395
+ {
110396
+ client: "Claude.ai (web/desktop)",
110397
+ path: `Settings → Connectors → Add custom connector → ${HOSTED_MCP_URL}`,
110398
+ note: "Hosted — OAuth, no token to paste. Remote-safe subset (operate existing apps + deploy connected repos)."
110243
110399
  },
110244
110400
  {
110245
110401
  client: "Cursor / Windsurf",
110246
- path: "settings → MCP → add server (use the same npx command)"
110402
+ path: "settings → MCP → add server (local: the same npx command; hosted: add the URL as a connector)"
110247
110403
  },
110248
110404
  {
110249
110405
  client: "VS Code (Continue / Cline)",
110250
- path: "extension settings → MCP servers"
110406
+ path: "extension settings → MCP servers (local npx command)"
110251
110407
  }
110408
+ ],
110409
+ notes: [
110410
+ "Two ways to connect:",
110411
+ " • Local (npx): full tool set, reads your project files — required for first publish from local files. Recommended for Claude Code.",
110412
+ ` • Hosted connector: add ${HOSTED_MCP_URL} as a custom connector. OAuth login in the browser — no token to paste. Remote-safe subset (operate existing apps + deploy connected repos); first-publish-from-local-files stays on the npx path.`,
110413
+ "Manage or revoke hosted connections from the Percher dashboard."
110252
110414
  ]
110253
110415
  };
110254
110416
  }
@@ -114210,66 +114372,6 @@ function withDuration(label, durationMs) {
114210
114372
  return `${label} (${seconds}s)`;
114211
114373
  }
114212
114374
 
114213
- // ../core/dist/poll-deployment.js
114214
- async function pollDeployment(opts) {
114215
- const terminal = opts.terminal ?? ["live", "failed"];
114216
- const intervalMs = opts.intervalMs ?? 2000;
114217
- const timeoutMs = opts.timeoutMs ?? TIMEOUTS.clientPublishPoll;
114218
- const maxRetries = opts.maxRetries ?? 5;
114219
- let deployment = opts.initial;
114220
- const startedAt = Date.now();
114221
- const seenEventIds = new Set;
114222
- if (opts.onEvent) {
114223
- await drainNewEvents(opts, deployment.id, seenEventIds);
114224
- }
114225
- while (!terminal.includes(deployment.status)) {
114226
- if (Date.now() - startedAt > timeoutMs) {
114227
- opts.onTimeout(deployment);
114228
- }
114229
- opts.onTick?.(deployment);
114230
- await new Promise((r) => setTimeout(r, intervalMs));
114231
- let attempt = 0;
114232
- let polled = false;
114233
- while (!polled) {
114234
- try {
114235
- deployment = await opts.ctx.client.apps.getDeployment(opts.appId, deployment.id);
114236
- polled = true;
114237
- } catch (err) {
114238
- attempt++;
114239
- const isNetwork = isTransientNetworkError(err);
114240
- if (!isNetwork || attempt >= maxRetries) {
114241
- if (isNetwork) {
114242
- const msg = err.message ?? String(err);
114243
- throw new Error(`${msg}
114244
-
114245
- The deploy may already be live — your network connection dropped mid-poll. Check with: percher deploys list ${opts.appName}`, { cause: err });
114246
- }
114247
- throw err;
114248
- }
114249
- const backoff = (opts.backoffMs ?? ((n) => 1000 * 2 ** (n - 1)))(attempt);
114250
- await new Promise((r) => setTimeout(r, backoff));
114251
- }
114252
- }
114253
- if (opts.onEvent) {
114254
- await drainNewEvents(opts, deployment.id, seenEventIds);
114255
- }
114256
- }
114257
- return deployment;
114258
- }
114259
- async function drainNewEvents(opts, deployId, seenEventIds) {
114260
- if (!opts.onEvent)
114261
- return;
114262
- try {
114263
- const { events } = await opts.ctx.client.apps.getDeployEvents(opts.appId, deployId);
114264
- for (const event of events) {
114265
- if (seenEventIds.has(event.id))
114266
- continue;
114267
- seenEventIds.add(event.id);
114268
- opts.onEvent(event);
114269
- }
114270
- } catch {}
114271
- }
114272
-
114273
114375
  // ../core/dist/publish-retry.js
114274
114376
  var NON_RETRYABLE_CODES = new Set([
114275
114377
  "WORKER_UNAVAILABLE_PERSISTENT",
@@ -114449,6 +114551,17 @@ async function runDeployWithRetry(opts) {
114449
114551
  throw lastErr;
114450
114552
  }
114451
114553
 
114554
+ // ../core/dist/toml-recovery.js
114555
+ function tomlErrorToRecoveryProblems(err) {
114556
+ if (err.issues?.length) {
114557
+ return err.issues.map((i) => ({
114558
+ file: "percher.toml",
114559
+ message: `${i.path.join(".") || "(root)"}: ${i.message}`
114560
+ }));
114561
+ }
114562
+ return [{ file: "percher.toml", message: err.message }];
114563
+ }
114564
+
114452
114565
  // ../core/dist/commands/env-scan.js
114453
114566
  import { readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync3 } from "node:fs";
114454
114567
  import { join as join22 } from "node:path";
@@ -114964,9 +115077,9 @@ function buildLiveSummary(opts) {
114964
115077
  const replaced = opts.replacedPreview ? " — replaced previous preview" : "";
114965
115078
  return `${opts.appName} ${verb}${fwSuffix} → ${opts.url}${replaced}`;
114966
115079
  }
114967
- async function publish(ctx, input = {}) {
115080
+ async function publish(ctx, input = {}, preParsedConfig) {
114968
115081
  try {
114969
- return await publishInner(ctx, input);
115082
+ return await publishInner(ctx, input, preParsedConfig);
114970
115083
  } catch (err) {
114971
115084
  const classified = classifyPublishApiError(err, {
114972
115085
  configPath: tomlPathFor2(ctx.cwd),
@@ -114995,7 +115108,7 @@ async function publish(ctx, input = {}) {
114995
115108
  };
114996
115109
  }
114997
115110
  }
114998
- async function publishInner(ctx, input) {
115111
+ async function publishInner(ctx, input, preParsedConfig) {
114999
115112
  const t0 = Date.now();
115000
115113
  if (!input.dryRun) {
115001
115114
  try {
@@ -115006,7 +115119,7 @@ async function publishInner(ctx, input) {
115006
115119
  ctx.status("[1/4] Reading config...");
115007
115120
  let config2;
115008
115121
  try {
115009
- config2 = await loadOrGenerate(ctx);
115122
+ config2 = await loadOrGenerate(ctx, preParsedConfig);
115010
115123
  } catch (err) {
115011
115124
  if (err instanceof PercherTomlError) {
115012
115125
  if (err.code === "PARSE_ERROR") {
@@ -115020,7 +115133,7 @@ async function publishInner(ctx, input) {
115020
115133
  suggestion: "Fix the syntax error and retry publish."
115021
115134
  },
115022
115135
  recovery: recoveryFixConfig({
115023
- problems: [{ file: "percher.toml", message: err.message }],
115136
+ problems: tomlErrorToRecoveryProblems(err),
115024
115137
  reasonCode: "config_invalid"
115025
115138
  }),
115026
115139
  summary: "percher.toml has invalid TOML syntax — fix and retry.",
@@ -115029,10 +115142,7 @@ async function publishInner(ctx, input) {
115029
115142
  };
115030
115143
  }
115031
115144
  if (err.code === "VALIDATION_ERROR") {
115032
- const problems = (err.issues ?? []).map((i) => ({
115033
- file: "percher.toml",
115034
- message: `${i.path.join(".") || "(root)"}: ${i.message}`
115035
- }));
115145
+ const problems = tomlErrorToRecoveryProblems(err);
115036
115146
  const bullets = problems.map((p) => ` - ${p.message}`).join(`
115037
115147
  `);
115038
115148
  const count = problems.length;
@@ -115076,6 +115186,30 @@ ${bullets}`,
115076
115186
  bundle: { fileCount: 0, bytes: 0 }
115077
115187
  };
115078
115188
  }
115189
+ if (preParsedConfig && !input.dryRun && !existsSync15(join24(ctx.cwd, "percher.toml"))) {
115190
+ return {
115191
+ status: "failed",
115192
+ fileCount: 0,
115193
+ bytes: 0,
115194
+ error: {
115195
+ title: "percher.toml missing from the project",
115196
+ explanation: "A pre-parsed config was supplied, but no percher.toml exists in the project directory. The file is packaged into the upload, so it must be present on disk for a real deploy.",
115197
+ suggestion: "Run `percher init` (or write percher.toml) before publishing."
115198
+ },
115199
+ recovery: recoveryFixConfig({
115200
+ problems: [
115201
+ {
115202
+ file: "percher.toml",
115203
+ message: "percher.toml must exist on disk for a real upload (it ships in the tarball)."
115204
+ }
115205
+ ],
115206
+ reasonCode: "config_missing"
115207
+ }),
115208
+ summary: "percher.toml missing — required on disk for a real upload.",
115209
+ configPath: tomlPathFor2(ctx.cwd),
115210
+ bundle: { fileCount: 0, bytes: 0 }
115211
+ };
115212
+ }
115079
115213
  ctx.status("[2/4] Packaging files...");
115080
115214
  const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
115081
115215
  const packageMs = Date.now() - t0;
@@ -115251,11 +115385,29 @@ async function executeDeploy(opts) {
115251
115385
  throw err;
115252
115386
  }
115253
115387
  }
115388
+ const uploadMs = Date.now() - uploadStart;
115389
+ return finalizeDeploy({
115390
+ ctx,
115391
+ config: config2,
115392
+ input,
115393
+ app,
115394
+ firstDeploy,
115395
+ tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
115396
+ deployment,
115397
+ t0,
115398
+ packageMs,
115399
+ uploadMs,
115400
+ missingBuildEnvKeys,
115401
+ cacheReused: cacheReusedDeployment !== null
115402
+ });
115403
+ }
115404
+ async function finalizeDeploy(opts) {
115405
+ const { ctx, config: config2, input, app, firstDeploy, tarball, t0, packageMs, uploadMs, missingBuildEnvKeys, cacheReused } = opts;
115406
+ let deployment = opts.deployment;
115254
115407
  const replacedPreview = deployment.replacedPreview === true;
115255
115408
  if (replacedPreview) {
115256
115409
  ctx.status("Replaced previous preview...");
115257
115410
  }
115258
- const uploadMs = Date.now() - uploadStart;
115259
115411
  if (input.waitForLive === false) {
115260
115412
  ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
115261
115413
  return {
@@ -115395,7 +115547,7 @@ async function executeDeploy(opts) {
115395
115547
  firstDeploy: firstDeploy || undefined,
115396
115548
  cacheHit,
115397
115549
  cacheStatus,
115398
- cacheReused: cacheReusedDeployment !== null || undefined,
115550
+ cacheReused: cacheReused || undefined,
115399
115551
  missingBuildEnvKeys: missingBuildEnvKeys.length > 0 ? missingBuildEnvKeys : undefined,
115400
115552
  traceId: deployment.id,
115401
115553
  recovery: RECOVERY_NONE,
@@ -115581,11 +115733,14 @@ async function handleAnonymousPublish(ctx, config2, input, tarball) {
115581
115733
  ]
115582
115734
  };
115583
115735
  }
115584
- async function loadOrGenerate(ctx) {
115736
+ async function loadOrGenerate(ctx, preParsed) {
115737
+ if (preParsed)
115738
+ return structuredClone(preParsed);
115585
115739
  const tomlPath = join24(ctx.cwd, "percher.toml");
115586
115740
  if (!existsSync15(tomlPath)) {
115587
115741
  ctx.status("No percher.toml found — detecting framework...");
115588
- await init(ctx);
115742
+ const generated = await init(ctx);
115743
+ return validate(generated.config);
115589
115744
  }
115590
115745
  return parseFile(tomlPath);
115591
115746
  }
@@ -115631,6 +115786,18 @@ function emitPreflight(ctx, config2, tarball) {
115631
115786
  ctx.status(line);
115632
115787
  }
115633
115788
  }
115789
+ // ../core/dist/commands/publish-inline.js
115790
+ var publishInlineInputSchema = exports_external.object({
115791
+ name: exports_external.string().optional().describe("Override the app name from percher.toml. Optional when percher.toml sets [app].name."),
115792
+ files: exports_external.array(exports_external.object({
115793
+ path: exports_external.string().min(1).describe("Repo-relative path (no leading / and no ..)."),
115794
+ content: exports_external.string().describe("UTF-8 file contents.")
115795
+ })).min(1).describe("Inline source files. MUST include a percher.toml."),
115796
+ preview: exports_external.boolean().optional().describe("Deploy as a preview (does not replace the live version)."),
115797
+ message: exports_external.string().optional().describe("Deploy note (visible in deploy history)."),
115798
+ noCache: exports_external.boolean().optional().describe("Skip the image cache and force a fresh build."),
115799
+ 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.")
115800
+ });
115634
115801
  // ../core/dist/commands/push.js
115635
115802
  import { existsSync as existsSync16 } from "node:fs";
115636
115803
  import { join as join25 } from "node:path";
@@ -117280,13 +117447,23 @@ function registerAccountCommands(cli, ctx) {
117280
117447
  return;
117281
117448
  }
117282
117449
  await writeLine(ctx, import_picocolors2.default.bold("Percher MCP server"));
117283
- await writeLine(ctx, import_picocolors2.default.dim("Add this entry to your MCP-aware client:"));
117284
117450
  await writeLine(ctx, "");
117451
+ await writeLine(ctx, import_picocolors2.default.bold("Local (npx) — full tool set, reads your project files:"));
117285
117452
  await writeLine(ctx, JSON.stringify(result.config, null, 2));
117286
117453
  await writeLine(ctx, "");
117454
+ await writeLine(ctx, import_picocolors2.default.bold("Hosted connector — add this URL, OAuth login in the browser (no token):"));
117455
+ await writeLine(ctx, ` ${result.hosted.url}`);
117456
+ await writeLine(ctx, "");
117287
117457
  await writeLine(ctx, import_picocolors2.default.bold("Where to paste:"));
117288
117458
  for (const loc of result.installLocations) {
117289
117459
  await writeLine(ctx, ` ${loc.client.padEnd(28)} ${import_picocolors2.default.dim(loc.path)}`);
117460
+ if (loc.note) {
117461
+ await writeLine(ctx, ` ${"".padEnd(28)} ${import_picocolors2.default.dim(loc.note)}`);
117462
+ }
117463
+ }
117464
+ await writeLine(ctx, "");
117465
+ for (const note of result.notes) {
117466
+ await writeLine(ctx, import_picocolors2.default.dim(note));
117290
117467
  }
117291
117468
  await writeLine(ctx, "");
117292
117469
  await writeLine(ctx, import_picocolors2.default.dim("Once installed, your assistant gets percher_publish + 28 other tools."));
@@ -119014,7 +119191,7 @@ async function renderError(stderr, error48) {
119014
119191
  // package.json
119015
119192
  var package_default = {
119016
119193
  name: "percher",
119017
- version: "0.5.9",
119194
+ version: "0.5.11",
119018
119195
  description: "CLI for the Percher hosting platform — publish apps with one command",
119019
119196
  type: "module",
119020
119197
  bin: {