percher 0.5.10 → 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,6 +90873,46 @@ var CAC = class extends EventTarget {
90873
90873
  };
90874
90874
  var cac = (name = "") => new CAC(name);
90875
90875
 
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
+ };
90876
90916
  // ../toml/dist/errors.js
90877
90917
  class PercherTomlError extends Error {
90878
90918
  code;
@@ -104440,45 +104480,6 @@ function date4(params) {
104440
104480
 
104441
104481
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
104442
104482
  config(en_default());
104443
- // ../shared/dist/app-name.js
104444
- var APP_NAME_RE = /^[a-z][a-z0-9-]{2,39}$/;
104445
- // ../shared/dist/limits.js
104446
- var MAX_TARBALL_BYTES = 500 * 1024 * 1024;
104447
- // ../shared/dist/logs.js
104448
- var MAX_LOG_SEARCH_LENGTH = 256;
104449
- // ../shared/dist/platform-env.js
104450
- var PERCHER_INJECTED_ENV_KEYS = new Set([
104451
- "NODE_ENV",
104452
- "PORT",
104453
- "DATABASE_URL",
104454
- "POCKETBASE_URL",
104455
- "POCKETBASE_ADMIN_EMAIL",
104456
- "POCKETBASE_ADMIN_PASSWORD",
104457
- "PERCHER_APP_NAME",
104458
- "PERCHER_APP_URL",
104459
- "HTTP_PROXY",
104460
- "HTTPS_PROXY",
104461
- "NO_PROXY",
104462
- "NODE_USE_ENV_PROXY"
104463
- ]);
104464
- var PROXY_LOWERCASE_ALIASES = new Set([
104465
- "http_proxy",
104466
- "https_proxy",
104467
- "no_proxy"
104468
- ]);
104469
- // ../shared/dist/runtime.js
104470
- var DEFAULT_NODE_VERSION_MAJOR = "24";
104471
- var DEFAULT_NODE_VERSION_LABEL = `${DEFAULT_NODE_VERSION_MAJOR} LTS`;
104472
- // ../shared/dist/timeouts.js
104473
- var TIMEOUTS = {
104474
- workerBuildSubprocess: 10 * 60000,
104475
- apiToWorkerBuildFetch: 11 * 60000,
104476
- clientPublishPoll: 12 * 60000,
104477
- workerHealthcheck: 5000,
104478
- watchdogList: 15000,
104479
- mcpWaitDeployDefault: 30000,
104480
- mcpWaitDeployMax: 12 * 60000
104481
- };
104482
104483
  // ../toml/dist/schema.js
104483
104484
  var appSchema = exports_external.object({
104484
104485
  name: exports_external.string().regex(APP_NAME_RE, "must be 3–40 chars, lowercase, start with a letter, [a-z0-9-] only"),
@@ -105963,6 +105964,12 @@ class AccountResource {
105963
105964
  revokeToken(id) {
105964
105965
  return this.http.delete(`/account/tokens/${encodeURIComponent(id)}`);
105965
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
+ }
105966
105973
  exportData() {
105967
105974
  return this.http.get("/account/export");
105968
105975
  }
@@ -106058,12 +106065,32 @@ class AppsResource {
106058
106065
  }
106059
106066
  return this.http.postBinary(`/apps/${encodeURIComponent(id)}/deploys${qs ? `?${qs}` : ""}`, input.tarball, headers);
106060
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
+ }
106061
106085
  promotePreview(appRef, deployId) {
106062
106086
  return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/promote`, {});
106063
106087
  }
106064
106088
  rerunDeploy(appRef, deployId) {
106065
106089
  return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/${encodeURIComponent(deployId)}/rerun`, {});
106066
106090
  }
106091
+ deployLatestCommit(appRef, input) {
106092
+ return this.http.post(`/apps/${encodeURIComponent(appRef)}/deploys/from-latest-commit`, input ?? {});
106093
+ }
106067
106094
  setCrashAnalysisEnabled(appRef, enabled) {
106068
106095
  return this.http.put(`/apps/${encodeURIComponent(appRef)}/crash-analysis`, {
106069
106096
  crashAnalysisEnabled: enabled
@@ -106702,9 +106729,10 @@ class PercherClient {
106702
106729
  invites;
106703
106730
  transfers;
106704
106731
  constructor(options = {}) {
106705
- const defaults = loadConfig();
106706
- this.apiUrl = options.apiUrl ?? defaults.apiUrl;
106707
- 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;
106708
106736
  this.sessionId = options.sessionId;
106709
106737
  const http = new Http({
106710
106738
  apiUrl: this.apiUrl,
@@ -106988,427 +107016,278 @@ async function deleteApp(ctx, input) {
106988
107016
  }
106989
107017
  await ctx.client.apps.delete(name);
106990
107018
  }
106991
- // ../core/dist/commands/deploys.js
106992
- var deploysListInputSchema = exports_external.object({
106993
- app: exports_external.string().optional(),
106994
- limit: exports_external.number().int().positive().max(100).optional()
106995
- });
106996
- var deploysInspectInputSchema = exports_external.object({
106997
- app: exports_external.string().optional(),
106998
- id: exports_external.string().optional(),
106999
- latestFailed: exports_external.boolean().optional()
107000
- }).superRefine((val, ctx) => {
107001
- if (!val.id && !val.latestFailed) {
107002
- ctx.addIssue({
107003
- code: exports_external.ZodIssueCode.custom,
107004
- message: "either `id` or `latestFailed: true` must be provided"
107005
- });
107006
- }
107007
- });
107008
- function resolveAppName(ctx, app) {
107009
- const name = app ?? readPercherTomlAppName(ctx.cwd);
107010
- if (!name) {
107011
- throw new PercherCoreError("No app specified and no percher.toml found", {
107012
- code: "deploys.no-app",
107013
- hint: "Pass --app <name> or run from a directory with percher.toml."
107014
- });
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);
107015
107030
  }
107016
- return name;
107017
- }
107018
- async function deploysList(ctx, input = {}) {
107019
- const name = resolveAppName(ctx, input.app);
107020
- return ctx.client.apps.listDeploys(name, { limit: input.limit ?? 10 });
107021
- }
107022
- async function deploysInspect(ctx, input) {
107023
- const name = resolveAppName(ctx, input.app);
107024
- let deployId = input.id;
107025
- if (!deployId && input.latestFailed) {
107026
- const recent = await ctx.client.apps.listDeploys(name, { limit: 25 });
107027
- const failed = recent.find((d) => d.status === "failed");
107028
- if (!failed) {
107029
- throw new PercherCoreError(`No failed deploys found for "${name}"`, {
107030
- code: "deploys.no-failed",
107031
- hint: "Try `percher deploys` to list recent deploys, or omit --latest-failed."
107032
- });
107031
+ while (!terminal.includes(deployment.status)) {
107032
+ if (Date.now() - startedAt > timeoutMs) {
107033
+ opts.onTimeout(deployment);
107033
107034
  }
107034
- deployId = failed.id;
107035
- }
107036
- if (!deployId) {
107037
- throw new PercherCoreError("Deploy ID is required", {
107038
- code: "deploys.no-id"
107039
- });
107040
- }
107041
- const deploy = await ctx.client.apps.getDeployment(name, deployId);
107042
- let buildLog = null;
107043
- try {
107044
- const log = await ctx.client.apps.getBuildLog(name, deployId);
107045
- buildLog = log.length > 0 ? log : null;
107046
- } catch (err) {
107047
- if (err instanceof PercherApiError && err.status === 404) {
107048
- buildLog = null;
107049
- } else {
107050
- 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);
107051
107061
  }
107052
107062
  }
107053
- return { deploy, buildLog };
107063
+ return deployment;
107054
107064
  }
107055
- // ../core/dist/commands/dev.js
107056
- import { existsSync as existsSync8 } from "node:fs";
107057
- import { join as join11 } from "node:path";
107058
-
107059
- // ../core/dist/tarball.js
107060
- var import_ignore = __toESM(require_ignore(), 1);
107061
- import { createHash as createHash2 } from "node:crypto";
107062
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
107063
- import { join as join9, relative as relative3, sep as sep2 } from "node:path";
107064
- import { Readable } from "node:stream";
107065
- import { createGzip } from "node:zlib";
107066
-
107067
- // ../../node_modules/.bun/tar-stream@3.1.8/node_modules/tar-stream/index.js
107068
- var $extract = require_extract();
107069
- var $pack = require_pack();
107070
-
107071
- // ../core/dist/tarball.js
107072
- var DEFAULT_IGNORE = [
107073
- "node_modules",
107074
- ".git",
107075
- "dist",
107076
- ".next",
107077
- ".svelte-kit",
107078
- "build",
107079
- "coverage",
107080
- ".env",
107081
- ".env.*",
107082
- ".bun",
107083
- "*.log",
107084
- ".DS_Store"
107085
- ];
107086
- function* walk(dir, baseDir, ig) {
107087
- const entries = readdirSync2(dir);
107088
- for (const name of entries) {
107089
- const full = join9(dir, name);
107090
- const rel = relative3(baseDir, full).split(sep2).join("/");
107091
- if (ig.ignores(rel))
107092
- continue;
107093
- const stats = statSync2(full);
107094
- if (stats.isDirectory()) {
107095
- 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))
107096
107072
  continue;
107097
- yield* walk(full, baseDir, ig);
107098
- } else if (stats.isFile()) {
107099
- yield rel;
107073
+ seenEventIds.add(event.id);
107074
+ opts.onEvent(event);
107100
107075
  }
107101
- }
107076
+ } catch {}
107102
107077
  }
107103
- async function createTarball(options) {
107104
- const ig = import_ignore.default();
107105
- ig.add(DEFAULT_IGNORE);
107106
- const gitignorePath = join9(options.cwd, ".gitignore");
107107
- if (existsSync6(gitignorePath)) {
107108
- ig.add(readFileSync6(gitignorePath, "utf8"));
107109
- }
107110
- if (options.extraIgnore)
107111
- ig.add(options.extraIgnore);
107112
- const fileBuffers = [];
107113
- for (const rel of walk(options.cwd, options.cwd, ig)) {
107114
- const data2 = readFileSync6(join9(options.cwd, rel));
107115
- fileBuffers.push({ path: rel, size: data2.length, data: data2 });
107116
- }
107117
- fileBuffers.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
107118
- const totalBytes = fileBuffers.reduce((sum, f) => sum + f.size, 0);
107119
- const contentHasher = createHash2("sha256");
107120
- for (const f of fileBuffers) {
107121
- contentHasher.update(`PATH:${f.path.length}:${f.path}
107122
- `);
107123
- contentHasher.update(`SIZE:${f.size}
107124
- `);
107125
- contentHasher.update(f.data);
107126
- contentHasher.update(`
107127
- `);
107128
- }
107129
- const contentHash = contentHasher.digest("hex");
107130
- const pack = $pack();
107131
- (async () => {
107132
- for (const file2 of fileBuffers) {
107133
- await new Promise((resolve7, reject) => {
107134
- pack.entry({ name: file2.path, size: file2.data.length }, file2.data, (err) => err ? reject(err) : resolve7());
107135
- });
107136
- }
107137
- pack.finalize();
107138
- })();
107139
- const gzip = createGzip();
107140
- const nodeStream = pack.pipe(gzip);
107141
- const webStream = Readable.toWeb(nodeStream);
107142
- 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 = {}) {
107143
107081
  return {
107144
- stream: webStream,
107145
- bytes: totalBytes,
107146
- fileCount: fileBuffers.length,
107147
- topFiles,
107148
- contentHash
107082
+ retryable: false,
107083
+ nextAction: "none",
107084
+ reasonCode: opts.reasonCode ?? "none",
107085
+ ...opts.url !== undefined ? { url: opts.url } : {}
107149
107086
  };
107150
107087
  }
107151
-
107152
- // ../core/dist/watcher.js
107153
- var import_ignore2 = __toESM(require_ignore(), 1);
107154
- import { existsSync as existsSync7, readFileSync as readFileSync7, watch } from "node:fs";
107155
- import { join as join10, sep as sep3 } from "node:path";
107156
- var DEFAULT_IGNORE2 = [
107157
- "node_modules",
107158
- ".git",
107159
- "dist",
107160
- ".next",
107161
- ".svelte-kit",
107162
- "build",
107163
- "coverage",
107164
- ".env",
107165
- ".env.*",
107166
- ".bun",
107167
- "*.log",
107168
- ".DS_Store",
107169
- "__pycache__"
107170
- ];
107171
- function createWatcher(options) {
107172
- const debounceMs = options.debounceMs ?? 500;
107173
- const ig = import_ignore2.default();
107174
- ig.add(DEFAULT_IGNORE2);
107175
- const gitignorePath = join10(options.cwd, ".gitignore");
107176
- if (existsSync7(gitignorePath)) {
107177
- ig.add(readFileSync7(gitignorePath, "utf8"));
107178
- }
107179
- if (options.extraIgnore)
107180
- ig.add(options.extraIgnore);
107181
- let timer = null;
107182
- let pending = new Set;
107183
- const fsWatcher = watch(options.cwd, { recursive: true }, (_event, filename) => {
107184
- if (!filename)
107185
- return;
107186
- const rel = filename.split(sep3).join("/");
107187
- if (ig.ignores(rel))
107188
- return;
107189
- pending.add(rel);
107190
- if (timer)
107191
- clearTimeout(timer);
107192
- timer = setTimeout(() => {
107193
- const files = [...pending];
107194
- pending = new Set;
107195
- timer = null;
107196
- if (files.length > 0) {
107197
- options.onChange(files);
107198
- }
107199
- }, debounceMs);
107200
- });
107088
+ function recoveryLogin(opts = {}) {
107201
107089
  return {
107202
- close() {
107203
- if (timer)
107204
- clearTimeout(timer);
107205
- fsWatcher.close();
107206
- }
107090
+ retryable: false,
107091
+ nextAction: "open_login",
107092
+ suggestedTool: "percher_login",
107093
+ args: {},
107094
+ reasonCode: opts.reasonCode ?? "auth_required"
107207
107095
  };
107208
107096
  }
107209
-
107210
- // ../core/dist/commands/dev.js
107211
- var devInputSchema = exports_external.object({
107212
- app: exports_external.string().optional()
107213
- });
107214
- async function loadConfig2(ctx, appOverride) {
107215
- const tomlPath = join11(ctx.cwd, "percher.toml");
107216
- if (!existsSync8(tomlPath)) {
107217
- throw new PercherCoreError("No percher.toml found. Run: percher init", {
107218
- code: "dev.no-app",
107219
- hint: "Create a percher.toml first with `percher init`."
107220
- });
107221
- }
107222
- const config2 = await parseFile(tomlPath);
107223
- if (appOverride)
107224
- return { ...config2, app: { ...config2.app, name: appOverride } };
107225
- 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
+ };
107226
107109
  }
107227
- async function ensureApp(ctx, config2) {
107228
- try {
107229
- return await ctx.client.apps.get(config2.app.name);
107230
- } catch (err) {
107231
- if (err.code === "APP_NOT_FOUND") {
107232
- return ctx.client.apps.create({
107233
- name: config2.app.name,
107234
- runtime: config2.app.runtime,
107235
- framework: config2.app.framework
107236
- });
107237
- }
107238
- throw err;
107239
- }
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
+ };
107240
107126
  }
107241
- async function doPush(ctx, config2, callbacks) {
107242
- const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
107243
- const app = await ensureApp(ctx, config2);
107244
- await callbacks.writeLine(`Pushing ${tarball.fileCount} files (${(tarball.bytes / 1024).toFixed(0)} KB)...`);
107245
- const deployResponse = await ctx.client.apps.deploy(app.id, { tarball: tarball.stream });
107246
- if (deployResponse.status === "already_in_progress") {
107247
- const active = deployResponse;
107248
- await callbacks.writeLine(` Skipped — deploy already in progress (${active.deployId}, ${active.deployStatus}).`);
107249
- return { success: false, appId: app.id };
107250
- }
107251
- let deployment = deployResponse;
107252
- const startedAt = Date.now();
107253
- const timeoutMs = TIMEOUTS.clientPublishPoll;
107254
- const pollInterval = 2000;
107255
- let lastStatus = "";
107256
- while (deployment.status !== "live" && deployment.status !== "failed") {
107257
- if (Date.now() - startedAt > timeoutMs) {
107258
- await callbacks.writeLine("Deploy timed out");
107259
- return { success: false, appId: app.id };
107260
- }
107261
- if (callbacks.signal.aborted)
107262
- return { success: false, appId: app.id };
107263
- if (deployment.status !== lastStatus) {
107264
- lastStatus = deployment.status;
107265
- await callbacks.writeLine(` ${deployment.status}...`);
107266
- }
107267
- await new Promise((r) => setTimeout(r, pollInterval));
107268
- deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
107269
- }
107270
- if (deployment.status === "failed") {
107271
- try {
107272
- const insp = await ctx.client.apps.getDeployment(app.id, deployment.id);
107273
- if (insp.errorMessage) {
107274
- await callbacks.writeLine("Deploy failed:");
107275
- for (const line of insp.errorMessage.split(`
107276
- `).slice(0, 30)) {
107277
- await callbacks.writeLine(` ${line}`);
107278
- }
107279
- } else {
107280
- await callbacks.writeLine("Deploy failed.");
107281
- }
107282
- } catch {
107283
- await callbacks.writeLine("Deploy failed.");
107284
- }
107285
- return { success: false, appId: app.id };
107286
- }
107287
- await callbacks.writeLine(`Live at ${deployment.url ?? ""}`);
107288
- 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
+ };
107289
107140
  }
107290
- function formatLogLine(l) {
107291
- const level = l.level.toUpperCase().padEnd(5);
107292
- const src = l.source === "runtime" ? "" : `[${l.source}] `;
107293
- 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
+ };
107294
107148
  }
107295
- async function streamLogs(ctx, appId, callbacks, signal) {
107296
- try {
107297
- for await (const line of ctx.client.logs.stream(appId, { signal })) {
107298
- if (signal.aborted)
107299
- return;
107300
- await callbacks.writeLine(formatLogLine(line));
107301
- }
107302
- } catch (err) {
107303
- if (signal.aborted)
107304
- return;
107305
- await callbacks.writeLine(`Log stream error: ${err.message}`);
107306
- }
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
+ };
107307
107157
  }
107308
- async function dev(ctx, input, callbacks) {
107309
- const config2 = await loadConfig2(ctx, input.app);
107310
- const app = await ensureApp(ctx, config2);
107311
- await callbacks.writeLine(`App: ${app.url}`);
107312
- await callbacks.writeLine("Watching for changes (logs streaming below)...");
107313
- await callbacks.writeLine("");
107314
- const logStream = { ctrl: null };
107315
- function startLogStream(appId) {
107316
- logStream.ctrl?.abort();
107317
- logStream.ctrl = new AbortController;
107318
- const linkedSignal = logStream.ctrl.signal;
107319
- 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");
107320
107170
  }
107321
- startLogStream(app.id);
107322
- let busy = false;
107323
- let queued = null;
107324
- async function handleChange(files) {
107325
- if (busy) {
107326
- queued = files;
107327
- return;
107328
- }
107329
- busy = true;
107330
- try {
107331
- const shortList = files.length <= 3 ? files.join(", ") : `${files.slice(0, 3).join(", ")} +${files.length - 3} more`;
107332
- await callbacks.writeLine(`Change detected: ${shortList}`);
107333
- logStream.ctrl?.abort();
107334
- const shouldPush = await callbacks.onFilesChanged(files);
107335
- if (shouldPush) {
107336
- try {
107337
- const freshConfig = await loadConfig2(ctx, input.app);
107338
- const result = await doPush(ctx, freshConfig, callbacks);
107339
- if (result.success) {
107340
- startLogStream(result.appId);
107341
- } else {
107342
- 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."
107343
107228
  }
107344
- } catch (err) {
107345
- await callbacks.writeLine(`Push error: ${err.message}`);
107346
- startLogStream(app.id);
107347
- }
107348
- } else {
107349
- 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
+ });
107350
107259
  }
107351
- } finally {
107352
- busy = false;
107353
- if (queued) {
107354
- const next = queued;
107355
- queued = null;
107356
- 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
+ });
107357
107272
  }
107273
+ return recoveryDoctor({
107274
+ app: failure.appName,
107275
+ deployId: failure.deployId,
107276
+ mode: "deploy",
107277
+ reasonCode: "build_failed",
107278
+ alternativeActions
107279
+ });
107358
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
+ });
107359
107286
  }
107360
- const devConfig = config2.dev;
107361
- const watcher = createWatcher({
107362
- cwd: ctx.cwd,
107363
- extraIgnore: devConfig?.ignore,
107364
- debounceMs: devConfig?.debounce,
107365
- onChange(files) {
107366
- handleChange(files);
107367
- }
107368
- });
107369
- try {
107370
- await new Promise((resolve7) => {
107371
- if (callbacks.signal.aborted) {
107372
- resolve7();
107373
- return;
107374
- }
107375
- callbacks.signal.addEventListener("abort", () => resolve7(), { once: true });
107376
- });
107377
- } finally {
107378
- logStream.ctrl?.abort();
107379
- watcher.close();
107380
- }
107381
- }
107382
- // ../core/dist/commands/diagnose.js
107383
- var diagnoseInputSchema = exports_external.object({
107384
- app: exports_external.string().optional(),
107385
- logLines: exports_external.number().int().min(0).max(200).optional(),
107386
- recencyHours: exports_external.number().positive().max(24 * 30).optional()
107387
- });
107388
- // ../core/dist/commands/diff.js
107389
- var diffInputSchema = exports_external.object({
107390
- app: exports_external.string().optional(),
107391
- base: exports_external.string().optional(),
107392
- head: exports_external.string().optional(),
107393
- patch: exports_external.boolean().optional()
107394
- });
107395
- async function diff(ctx, input = {}) {
107396
- const name = input.app ?? readPercherTomlAppName(ctx.cwd);
107397
- if (!name) {
107398
- throw new PercherCoreError("No app specified and no percher.toml found", {
107399
- code: "diff.no-app",
107400
- hint: "Pass --app <name> or run from a directory with percher.toml."
107401
- });
107402
- }
107403
- return ctx.client.apps.getChanges(name, {
107404
- base: input.base,
107405
- head: input.head,
107406
- patch: input.patch
107407
- });
107408
107287
  }
107409
- // ../core/dist/commands/doctor.js
107410
- import { existsSync as existsSync9 } from "node:fs";
107411
- import { join as join13 } from "node:path";
107288
+
107289
+ // ../core/dist/commands/publish-failure.js
107290
+ import { join as join9 } from "node:path";
107412
107291
 
107413
107292
  // ../core/dist/error-classifier.js
107414
107293
  function extractEnvVarsFromLog(combined) {
@@ -107742,391 +107621,650 @@ ${buildLog ?? ""}`;
107742
107621
  return null;
107743
107622
  }
107744
107623
 
107745
- // ../core/dist/recovery.js
107746
- function recoveryNone(opts = {}) {
107747
- return {
107748
- retryable: false,
107749
- nextAction: "none",
107750
- reasonCode: opts.reasonCode ?? "none",
107751
- ...opts.url !== undefined ? { url: opts.url } : {}
107752
- };
107753
- }
107754
- function recoveryLogin(opts = {}) {
107755
- return {
107756
- retryable: false,
107757
- nextAction: "open_login",
107758
- suggestedTool: "percher_login",
107759
- args: {},
107760
- reasonCode: opts.reasonCode ?? "auth_required"
107761
- };
107762
- }
107763
- function recoveryWait(opts) {
107764
- return {
107765
- retryable: false,
107766
- nextAction: "wait_deploy",
107767
- suggestedTool: "percher_wait_for_deploy",
107768
- args: {
107769
- app: opts.app,
107770
- deployId: opts.deployId,
107771
- timeoutSeconds: opts.timeoutSeconds ?? TIMEOUTS.mcpWaitDeployDefault / 1000
107772
- },
107773
- reasonCode: opts.reasonCode
107774
- };
107775
- }
107776
- function recoveryDoctor(opts) {
107777
- const args = {};
107778
- if (opts.app !== undefined)
107779
- args.app = opts.app;
107780
- if (opts.deployId !== undefined)
107781
- args.deployId = opts.deployId;
107782
- if (opts.mode !== undefined)
107783
- args.mode = opts.mode;
107784
- return {
107785
- retryable: false,
107786
- nextAction: "run_doctor",
107787
- suggestedTool: "percher_doctor",
107788
- args,
107789
- reasonCode: opts.reasonCode,
107790
- ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107791
- };
107792
- }
107793
- function recoveryEnv(opts) {
107794
- const args = {};
107795
- if (opts.app !== undefined)
107796
- args.app = opts.app;
107797
- if (opts.keys.length > 0)
107798
- args.keys = opts.keys;
107799
- return {
107800
- retryable: false,
107801
- nextAction: "set_env_vars",
107802
- suggestedTool: "percher_env_set",
107803
- args,
107804
- reasonCode: opts.reasonCode ?? "missing_env"
107805
- };
107806
- }
107807
- function recoveryFixConfig(opts) {
107624
+ // ../core/dist/structured-error-codes.js
107625
+ var MAPPINGS = {
107626
+ WORKER_UNAVAILABLE: {
107627
+ errorClass: "infra_unavailable",
107628
+ phase: "infra",
107629
+ cause: "worker_fetch_failed",
107630
+ title: "Build worker is unreachable",
107631
+ explanation: (raw) => raw ? `The Percher build worker did not respond in time: ${trimMessage(raw)}. This is a platform-side issue, not your code.` : "The Percher build worker did not respond in time. This is a platform-side issue, not your code.",
107632
+ suggestion: "Wait 30 seconds and run `percher publish` again. If retry #2 also fails, pause 5 minutes before the next attempt — back-to-back retries during a partial outage just waste your time. After 3 spaced-out retries, check status.percher.app or contact support. Your build never started."
107633
+ },
107634
+ WORKER_UNAVAILABLE_PERSISTENT: {
107635
+ errorClass: "infra_unavailable",
107636
+ phase: "infra",
107637
+ cause: "worker_circuit_breaker_open",
107638
+ title: "Build worker is in a crash loop",
107639
+ explanation: () => "The Percher build worker has failed repeatedly in the last minute and the platform's circuit breaker has flipped open. Retrying immediately would just queue more failures.",
107640
+ suggestion: "Wait 2-3 minutes for the platform to recover, then retry. If it persists past 5 minutes check status.percher.app or contact support — this is a platform incident, not your code."
107641
+ },
107642
+ BUILD_TIMEOUT: {
107643
+ errorClass: "build_failed",
107644
+ phase: "build",
107645
+ cause: "build_timeout",
107646
+ title: "Build timed out",
107647
+ explanation: () => "The build ran longer than the 10-minute platform limit and was aborted.",
107648
+ suggestion: "Common causes: a slow `postinstall` hook, very large dependency tree, or a Dockerfile that pulls a huge base image. Try `bun install` locally and check what's slow. If it's legitimately a heavy build, contact support to discuss raising the limit for your account."
107649
+ },
107650
+ BUILD_FAILED: {
107651
+ errorClass: "build_failed",
107652
+ phase: "build",
107653
+ cause: "build_nonzero_exit",
107654
+ title: "Build failed",
107655
+ explanation: (raw) => raw ? `The build process returned a non-zero exit code: ${trimMessage(raw)}. Read the build log below for the exact error.` : "The build process returned a non-zero exit code. Read the build log below for the exact error.",
107656
+ suggestion: "Run your build locally to reproduce: `bun install && bun run build`. The error in the build log usually points to a missing dependency, a TypeScript/syntax error, or a misconfigured framework setting."
107657
+ },
107658
+ WORKER_ERROR: {
107659
+ errorClass: "infra_unavailable",
107660
+ phase: "infra",
107661
+ cause: "worker_internal_error",
107662
+ title: "Platform error inside the build worker",
107663
+ explanation: (raw) => raw ? `The build worker hit an internal error: ${trimMessage(raw)}. This is a Percher bug, not your code.` : "The build worker hit an internal error. This is a Percher bug, not your code.",
107664
+ suggestion: "Retry once — transient platform errors usually clear within a minute. If it repeats, please report it: include the deploy ID and we'll investigate. Your account is not at fault."
107665
+ },
107666
+ DEPLOY_STALLED: {
107667
+ errorClass: "infra_unavailable",
107668
+ phase: "infra",
107669
+ cause: "deploy_stalled",
107670
+ title: "Deploy stalled",
107671
+ explanation: () => "The platform abandoned this deploy — likely because the API or build worker restarted mid-build. Your code is fine; the build never finished.",
107672
+ suggestion: "Run `percher publish` again. The new deploy will start fresh. If you see this repeatedly across multiple publishes, check status.percher.app — it implies a recurring platform issue."
107673
+ }
107674
+ };
107675
+ function classifyByErrorCode(code, rawMessage) {
107676
+ if (!code)
107677
+ return null;
107678
+ const mapping = MAPPINGS[code];
107679
+ if (!mapping)
107680
+ return null;
107808
107681
  return {
107809
- retryable: false,
107810
- nextAction: "fix_config",
107811
- problems: opts.problems,
107812
- reasonCode: opts.reasonCode ?? "config_invalid"
107682
+ title: mapping.title,
107683
+ explanation: mapping.explanation(rawMessage),
107684
+ suggestion: mapping.suggestion,
107685
+ errorClass: mapping.errorClass,
107686
+ phase: mapping.phase,
107687
+ cause: mapping.cause,
107688
+ relevantFiles: [],
107689
+ missingEnvVars: []
107813
107690
  };
107814
107691
  }
107815
- function recoveryFixProblems(opts) {
107816
- return {
107817
- retryable: false,
107818
- nextAction: "fix_problems",
107819
- problems: opts.problems,
107820
- reasonCode: opts.reasonCode ?? "build_failed",
107821
- ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107822
- };
107692
+ function trimMessage(raw) {
107693
+ const firstLine = raw.split(`
107694
+ `)[0] ?? "";
107695
+ return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine;
107823
107696
  }
107824
- function recoveryRetry(opts) {
107825
- return {
107826
- retryable: true,
107827
- nextAction: "retry",
107828
- suggestedTool: opts.suggestedTool,
107829
- args: opts.args,
107830
- reasonCode: opts.reasonCode
107831
- };
107697
+
107698
+ // ../core/dist/commands/publish-failure.js
107699
+ function tomlPathFor(cwd) {
107700
+ return join9(cwd, "percher.toml");
107832
107701
  }
107833
- function recoveryAsk(opts) {
107834
- if (!opts.prompt || opts.prompt.trim().length === 0) {
107835
- throw new Error("recoveryAsk requires a non-empty prompt");
107702
+ async function buildFailureError(ctx, app, deployment, signal) {
107703
+ let buildLogTail;
107704
+ let buildLogFetchFailed = false;
107705
+ let fullLog = "";
107706
+ try {
107707
+ fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
107708
+ if (!fullLog && !signal?.aborted) {
107709
+ await abortableSleep(1500, signal);
107710
+ if (!signal?.aborted) {
107711
+ fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
107712
+ }
107713
+ }
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
+ };
107836
107741
  }
107837
107742
  return {
107838
- retryable: opts.retryable ?? false,
107839
- nextAction: "ask_user",
107840
- prompt: opts.prompt,
107841
- ...opts.options !== undefined ? { options: opts.options } : {},
107842
- reasonCode: opts.reasonCode
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
107843
107754
  };
107844
107755
  }
107845
- function recoveryInspectBuildLog(opts) {
107846
- const args = opts.deployId ? { id: opts.deployId, app: opts.app } : { latestFailed: true, app: opts.app };
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
+ });
107847
107769
  return {
107848
- retryable: false,
107849
- nextAction: "inspect_build_log",
107850
- suggestedTool: "percher_deploys_inspect",
107851
- args,
107852
- reasonCode: opts.reasonCode ?? "build_failed",
107853
- ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107770
+ error: error48,
107771
+ recovery,
107772
+ summary: `${error48.title} (${app.name}, deploy ${deployment.id}).`
107854
107773
  };
107855
107774
  }
107856
- var RECOVERY_NONE = recoveryNone();
107857
- var RECOVERY_NEEDS_LOGIN = recoveryLogin();
107858
- function buildProblemToRecoveryProblem(p) {
107859
- const file2 = p.file ?? "";
107860
- const message = p.hint && p.hint.length > 0 ? `${p.message}
107861
- Hint: ${p.hint}` : p.message;
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
+ });
107862
107783
  return {
107863
- file: file2,
107864
- ...typeof p.line === "number" ? { line: p.line } : {},
107865
- ...typeof p.column === "number" ? { column: p.column } : {},
107866
- message
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 }
107867
107794
  };
107868
107795
  }
107869
- function recoveryFromErrorClass(errorClass, failure = {}) {
107870
- switch (errorClass) {
107871
- case "missing_env":
107872
- return recoveryEnv({
107873
- keys: failure.missingEnvVars ?? [],
107874
- reasonCode: "missing_env"
107875
- });
107876
- case "infra_unavailable":
107877
- return recoveryRetry({
107878
- suggestedTool: "percher_publish",
107879
- args: failure.publishInput ?? {},
107880
- reasonCode: "infra_unavailable"
107881
- });
107882
- case "platform_at_capacity":
107883
- return recoveryRetry({
107884
- suggestedTool: "percher_publish",
107885
- args: failure.publishInput ?? {},
107886
- reasonCode: "infra_transient"
107887
- });
107888
- case "config_invalid":
107889
- return recoveryFixConfig({
107890
- problems: [
107891
- {
107892
- file: "percher.toml",
107893
- message: "percher.toml failed validation — fix the file and re-run."
107894
- }
107895
- ],
107896
- reasonCode: "config_invalid"
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."
107897
107844
  });
107898
- case "build_failed":
107899
- case "missing_dependency":
107900
- case "health_check_timeout":
107901
- case "port_mismatch":
107902
- case "permission_denied":
107903
- case "runtime_crash":
107904
- case "oom_killed": {
107905
- const fileProblems = (failure.problems ?? []).filter((p) => p.file);
107906
- const reproduceAlt = failure.deployId ? {
107907
- action: "reproduce_locally",
107908
- suggestedTool: "percher_reproduce",
107909
- args: { deployId: failure.deployId, app: failure.appName },
107910
- when: "after the primary path didn't surface a clear fix and the user has nixpacks installed locally"
107911
- } : undefined;
107912
- if (fileProblems.length > 0) {
107913
- return recoveryFixProblems({
107914
- problems: fileProblems.map(buildProblemToRecoveryProblem),
107915
- reasonCode: "build_failed",
107916
- ...reproduceAlt ? {
107917
- alternativeActions: [
107918
- {
107919
- ...reproduceAlt,
107920
- when: "after fixing the file-located problems didn't make the build pass and the user has nixpacks installed locally"
107921
- }
107922
- ]
107923
- } : {}
107924
- });
107925
- }
107926
- const inspectAlt = {
107927
- action: "inspect_build_log",
107928
- suggestedTool: "percher_deploys_inspect",
107929
- args: failure.deployId ? { id: failure.deployId, app: failure.appName } : { latestFailed: true, app: failure.appName },
107930
- when: "after percher_doctor's recovery didn't yield a fix and you want to read the raw build log directly"
107931
- };
107932
- const alternativeActions = [inspectAlt];
107933
- if (reproduceAlt) {
107934
- alternativeActions.push({
107935
- ...reproduceAlt,
107936
- when: "after percher_doctor + percher_deploys_inspect didn't surface a clear fix and the user has nixpacks installed locally"
107937
- });
107938
- }
107939
- return recoveryDoctor({
107940
- app: failure.appName,
107941
- deployId: failure.deployId,
107942
- mode: "deploy",
107943
- reasonCode: "build_failed",
107944
- alternativeActions
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());
107945
107958
  });
107946
107959
  }
107947
- default:
107948
- return recoveryAsk({
107949
- prompt: "Unexpected failure — Percher couldn't classify this error. Try `percher doctor` for diagnostics, or surface the error to the user.",
107950
- reasonCode: "unknown"
107951
- });
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 });
107952
108004
  }
108005
+ return buildTarball(fileBuffers);
107953
108006
  }
107954
108007
 
107955
- // ../core/dist/commands/publish-failure.js
107956
- import { join as join12 } from "node:path";
107957
-
107958
- // ../core/dist/structured-error-codes.js
107959
- var MAPPINGS = {
107960
- WORKER_UNAVAILABLE: {
107961
- errorClass: "infra_unavailable",
107962
- phase: "infra",
107963
- cause: "worker_fetch_failed",
107964
- title: "Build worker is unreachable",
107965
- explanation: (raw) => raw ? `The Percher build worker did not respond in time: ${trimMessage(raw)}. This is a platform-side issue, not your code.` : "The Percher build worker did not respond in time. This is a platform-side issue, not your code.",
107966
- suggestion: "Wait 30 seconds and run `percher publish` again. If retry #2 also fails, pause 5 minutes before the next attempt — back-to-back retries during a partial outage just waste your time. After 3 spaced-out retries, check status.percher.app or contact support. Your build never started."
107967
- },
107968
- WORKER_UNAVAILABLE_PERSISTENT: {
107969
- errorClass: "infra_unavailable",
107970
- phase: "infra",
107971
- cause: "worker_circuit_breaker_open",
107972
- title: "Build worker is in a crash loop",
107973
- explanation: () => "The Percher build worker has failed repeatedly in the last minute and the platform's circuit breaker has flipped open. Retrying immediately would just queue more failures.",
107974
- suggestion: "Wait 2-3 minutes for the platform to recover, then retry. If it persists past 5 minutes check status.percher.app or contact support — this is a platform incident, not your code."
107975
- },
107976
- BUILD_TIMEOUT: {
107977
- errorClass: "build_failed",
107978
- phase: "build",
107979
- cause: "build_timeout",
107980
- title: "Build timed out",
107981
- explanation: () => "The build ran longer than the 10-minute platform limit and was aborted.",
107982
- suggestion: "Common causes: a slow `postinstall` hook, very large dependency tree, or a Dockerfile that pulls a huge base image. Try `bun install` locally and check what's slow. If it's legitimately a heavy build, contact support to discuss raising the limit for your account."
107983
- },
107984
- BUILD_FAILED: {
107985
- errorClass: "build_failed",
107986
- phase: "build",
107987
- cause: "build_nonzero_exit",
107988
- title: "Build failed",
107989
- explanation: (raw) => raw ? `The build process returned a non-zero exit code: ${trimMessage(raw)}. Read the build log below for the exact error.` : "The build process returned a non-zero exit code. Read the build log below for the exact error.",
107990
- suggestion: "Run your build locally to reproduce: `bun install && bun run build`. The error in the build log usually points to a missing dependency, a TypeScript/syntax error, or a misconfigured framework setting."
107991
- },
107992
- WORKER_ERROR: {
107993
- errorClass: "infra_unavailable",
107994
- phase: "infra",
107995
- cause: "worker_internal_error",
107996
- title: "Platform error inside the build worker",
107997
- explanation: (raw) => raw ? `The build worker hit an internal error: ${trimMessage(raw)}. This is a Percher bug, not your code.` : "The build worker hit an internal error. This is a Percher bug, not your code.",
107998
- suggestion: "Retry once — transient platform errors usually clear within a minute. If it repeats, please report it: include the deploy ID and we'll investigate. Your account is not at fault."
107999
- },
108000
- DEPLOY_STALLED: {
108001
- errorClass: "infra_unavailable",
108002
- phase: "infra",
108003
- cause: "deploy_stalled",
108004
- title: "Deploy stalled",
108005
- explanation: () => "The platform abandoned this deploy — likely because the API or build worker restarted mid-build. Your code is fine; the build never finished.",
108006
- suggestion: "Run `percher publish` again. The new deploy will start fresh. If you see this repeatedly across multiple publishes, check status.percher.app — it implies a recurring platform issue."
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"));
108007
108034
  }
108008
- };
108009
- function classifyByErrorCode(code, rawMessage) {
108010
- if (!code)
108011
- return null;
108012
- const mapping = MAPPINGS[code];
108013
- if (!mapping)
108014
- return null;
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
+ });
108015
108057
  return {
108016
- title: mapping.title,
108017
- explanation: mapping.explanation(rawMessage),
108018
- suggestion: mapping.suggestion,
108019
- errorClass: mapping.errorClass,
108020
- phase: mapping.phase,
108021
- cause: mapping.cause,
108022
- relevantFiles: [],
108023
- missingEnvVars: []
108058
+ close() {
108059
+ if (timer)
108060
+ clearTimeout(timer);
108061
+ fsWatcher.close();
108062
+ }
108024
108063
  };
108025
108064
  }
108026
- function trimMessage(raw) {
108027
- const firstLine = raw.split(`
108028
- `)[0] ?? "";
108029
- return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine;
108030
- }
108031
108065
 
108032
- // ../core/dist/commands/publish-failure.js
108033
- function tomlPathFor(cwd) {
108034
- return join12(cwd, "percher.toml");
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;
108035
108082
  }
108036
- async function buildFailureError(ctx, app, deployment, signal) {
108037
- let buildLogTail;
108038
- let buildLogFetchFailed = false;
108039
- let fullLog = "";
108083
+ async function ensureApp(ctx, config2) {
108040
108084
  try {
108041
- fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
108042
- if (!fullLog && !signal?.aborted) {
108043
- await abortableSleep(1500, signal);
108044
- if (!signal?.aborted) {
108045
- fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
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.");
108046
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;
108047
108184
  }
108048
- if (fullLog) {
108049
- const lines = fullLog.split(`
108050
- `);
108051
- buildLogTail = lines.slice(-50).join(`
108052
- `);
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
+ }
108053
108214
  }
108054
- } catch {
108055
- buildLogFetchFailed = true;
108056
- }
108057
- const errorMessage = deployment.errorMessage ?? "";
108058
- const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError(errorMessage || "Deployment failed", fullLog);
108059
- const rawErrorMessage = errorMessage && !buildLogTail && !classified?.explanation.includes(errorMessage) ? errorMessage : undefined;
108060
- const problems = deployment.problems && deployment.problems.length > 0 ? deployment.problems : undefined;
108061
- if (classified) {
108062
- return {
108063
- title: classified.title,
108064
- explanation: classified.explanation,
108065
- suggestion: classified.suggestion,
108066
- buildLogTail,
108067
- rawErrorMessage,
108068
- errorClass: classified.errorClass,
108069
- phase: classified.phase,
108070
- cause: classified.cause,
108071
- relevantFiles: classified.relevantFiles,
108072
- missingEnvVars: classified.missingEnvVars,
108073
- problems
108074
- };
108075
108215
  }
108076
- return {
108077
- title: "Deployment failed",
108078
- explanation: errorMessage ? `The deployment failed: ${errorMessage}` : "The deployment failed. See the build log for details.",
108079
- 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(`
108080
- `)[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}`,
108081
- buildLogTail,
108082
- errorClass: "build_failed",
108083
- phase: "build",
108084
- cause: "unknown",
108085
- relevantFiles: [],
108086
- missingEnvVars: [],
108087
- problems
108088
- };
108089
- }
108090
- async function classifyDeploymentFailure(opts) {
108091
- const { ctx, app, deployment, publishInput } = opts;
108092
- const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
108093
- const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
108094
- missingEnvVars: error48.missingEnvVars,
108095
- deployId: deployment.id,
108096
- appName: app.name,
108097
- publishInput,
108098
- problems: error48.problems
108099
- }) : recoveryAsk({
108100
- prompt: `${app.name} deploy ${deployment.id} failed: ${error48.title}. ${error48.suggestion}`.trim(),
108101
- 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
+ }
108102
108224
  });
108103
- return {
108104
- error: error48,
108105
- recovery,
108106
- summary: `${error48.title} (${app.name}, deploy ${deployment.id}).`
108107
- };
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
+ }
108108
108237
  }
108109
- async function buildFailureResult(opts) {
108110
- const { ctx, app, deployment, tarball, input } = opts;
108111
- const { error: error48, recovery, summary } = await classifyDeploymentFailure({
108112
- ctx,
108113
- app,
108114
- deployment,
108115
- 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
108116
108263
  });
108117
- return {
108118
- status: "failed",
108119
- app,
108120
- deployment,
108121
- fileCount: tarball.fileCount,
108122
- bytes: tarball.bytes,
108123
- error: error48,
108124
- recovery,
108125
- summary,
108126
- configPath: tomlPathFor(ctx.cwd),
108127
- bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
108128
- };
108129
108264
  }
108265
+ // ../core/dist/commands/doctor.js
108266
+ import { existsSync as existsSync9 } from "node:fs";
108267
+ import { join as join13 } from "node:path";
108130
108268
 
108131
108269
  // ../core/dist/commands/wait-deploy.js
108132
108270
  var waitForDeployInputSchema = exports_external.object({
@@ -110225,6 +110363,7 @@ async function logs(ctx, input = {}) {
110225
110363
  }
110226
110364
  // ../core/dist/commands/mcp.js
110227
110365
  var mcpInputSchema = exports_external.object({});
110366
+ var HOSTED_MCP_URL = "https://mcp.percher.app/mcp";
110228
110367
  function mcp(_ctx, _input = {}) {
110229
110368
  return {
110230
110369
  config: {
@@ -110235,19 +110374,43 @@ function mcp(_ctx, _input = {}) {
110235
110374
  }
110236
110375
  }
110237
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
+ },
110238
110389
  installLocations: [
110239
110390
  {
110240
110391
  client: "Claude Code",
110241
- 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)."
110242
110399
  },
110243
110400
  {
110244
110401
  client: "Cursor / Windsurf",
110245
- 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)"
110246
110403
  },
110247
110404
  {
110248
110405
  client: "VS Code (Continue / Cline)",
110249
- path: "extension settings → MCP servers"
110406
+ path: "extension settings → MCP servers (local npx command)"
110250
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."
110251
110414
  ]
110252
110415
  };
110253
110416
  }
@@ -114209,66 +114372,6 @@ function withDuration(label, durationMs) {
114209
114372
  return `${label} (${seconds}s)`;
114210
114373
  }
114211
114374
 
114212
- // ../core/dist/poll-deployment.js
114213
- async function pollDeployment(opts) {
114214
- const terminal = opts.terminal ?? ["live", "failed"];
114215
- const intervalMs = opts.intervalMs ?? 2000;
114216
- const timeoutMs = opts.timeoutMs ?? TIMEOUTS.clientPublishPoll;
114217
- const maxRetries = opts.maxRetries ?? 5;
114218
- let deployment = opts.initial;
114219
- const startedAt = Date.now();
114220
- const seenEventIds = new Set;
114221
- if (opts.onEvent) {
114222
- await drainNewEvents(opts, deployment.id, seenEventIds);
114223
- }
114224
- while (!terminal.includes(deployment.status)) {
114225
- if (Date.now() - startedAt > timeoutMs) {
114226
- opts.onTimeout(deployment);
114227
- }
114228
- opts.onTick?.(deployment);
114229
- await new Promise((r) => setTimeout(r, intervalMs));
114230
- let attempt = 0;
114231
- let polled = false;
114232
- while (!polled) {
114233
- try {
114234
- deployment = await opts.ctx.client.apps.getDeployment(opts.appId, deployment.id);
114235
- polled = true;
114236
- } catch (err) {
114237
- attempt++;
114238
- const isNetwork = isTransientNetworkError(err);
114239
- if (!isNetwork || attempt >= maxRetries) {
114240
- if (isNetwork) {
114241
- const msg = err.message ?? String(err);
114242
- throw new Error(`${msg}
114243
-
114244
- The deploy may already be live — your network connection dropped mid-poll. Check with: percher deploys list ${opts.appName}`, { cause: err });
114245
- }
114246
- throw err;
114247
- }
114248
- const backoff = (opts.backoffMs ?? ((n) => 1000 * 2 ** (n - 1)))(attempt);
114249
- await new Promise((r) => setTimeout(r, backoff));
114250
- }
114251
- }
114252
- if (opts.onEvent) {
114253
- await drainNewEvents(opts, deployment.id, seenEventIds);
114254
- }
114255
- }
114256
- return deployment;
114257
- }
114258
- async function drainNewEvents(opts, deployId, seenEventIds) {
114259
- if (!opts.onEvent)
114260
- return;
114261
- try {
114262
- const { events } = await opts.ctx.client.apps.getDeployEvents(opts.appId, deployId);
114263
- for (const event of events) {
114264
- if (seenEventIds.has(event.id))
114265
- continue;
114266
- seenEventIds.add(event.id);
114267
- opts.onEvent(event);
114268
- }
114269
- } catch {}
114270
- }
114271
-
114272
114375
  // ../core/dist/publish-retry.js
114273
114376
  var NON_RETRYABLE_CODES = new Set([
114274
114377
  "WORKER_UNAVAILABLE_PERSISTENT",
@@ -115282,11 +115385,29 @@ async function executeDeploy(opts) {
115282
115385
  throw err;
115283
115386
  }
115284
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;
115285
115407
  const replacedPreview = deployment.replacedPreview === true;
115286
115408
  if (replacedPreview) {
115287
115409
  ctx.status("Replaced previous preview...");
115288
115410
  }
115289
- const uploadMs = Date.now() - uploadStart;
115290
115411
  if (input.waitForLive === false) {
115291
115412
  ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
115292
115413
  return {
@@ -115426,7 +115547,7 @@ async function executeDeploy(opts) {
115426
115547
  firstDeploy: firstDeploy || undefined,
115427
115548
  cacheHit,
115428
115549
  cacheStatus,
115429
- cacheReused: cacheReusedDeployment !== null || undefined,
115550
+ cacheReused: cacheReused || undefined,
115430
115551
  missingBuildEnvKeys: missingBuildEnvKeys.length > 0 ? missingBuildEnvKeys : undefined,
115431
115552
  traceId: deployment.id,
115432
115553
  recovery: RECOVERY_NONE,
@@ -115665,6 +115786,18 @@ function emitPreflight(ctx, config2, tarball) {
115665
115786
  ctx.status(line);
115666
115787
  }
115667
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
+ });
115668
115801
  // ../core/dist/commands/push.js
115669
115802
  import { existsSync as existsSync16 } from "node:fs";
115670
115803
  import { join as join25 } from "node:path";
@@ -117314,13 +117447,23 @@ function registerAccountCommands(cli, ctx) {
117314
117447
  return;
117315
117448
  }
117316
117449
  await writeLine(ctx, import_picocolors2.default.bold("Percher MCP server"));
117317
- await writeLine(ctx, import_picocolors2.default.dim("Add this entry to your MCP-aware client:"));
117318
117450
  await writeLine(ctx, "");
117451
+ await writeLine(ctx, import_picocolors2.default.bold("Local (npx) — full tool set, reads your project files:"));
117319
117452
  await writeLine(ctx, JSON.stringify(result.config, null, 2));
117320
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, "");
117321
117457
  await writeLine(ctx, import_picocolors2.default.bold("Where to paste:"));
117322
117458
  for (const loc of result.installLocations) {
117323
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));
117324
117467
  }
117325
117468
  await writeLine(ctx, "");
117326
117469
  await writeLine(ctx, import_picocolors2.default.dim("Once installed, your assistant gets percher_publish + 28 other tools."));
@@ -119048,7 +119191,7 @@ async function renderError(stderr, error48) {
119048
119191
  // package.json
119049
119192
  var package_default = {
119050
119193
  name: "percher",
119051
- version: "0.5.10",
119194
+ version: "0.5.11",
119052
119195
  description: "CLI for the Percher hosting platform — publish apps with one command",
119053
119196
  type: "module",
119054
119197
  bin: {