percher 0.5.10 → 0.5.12

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,43 @@ 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
+ }
106094
+ publishFromGit(input) {
106095
+ const params = new URLSearchParams;
106096
+ params.set("url", input.url);
106097
+ if (input.name)
106098
+ params.set("name", input.name);
106099
+ if (input.branch)
106100
+ params.set("branch", input.branch);
106101
+ if (input.note)
106102
+ params.set("note", input.note);
106103
+ return this.http.post(`/apps/publish-from-git?${params.toString()}`, {});
106104
+ }
106067
106105
  setCrashAnalysisEnabled(appRef, enabled) {
106068
106106
  return this.http.put(`/apps/${encodeURIComponent(appRef)}/crash-analysis`, {
106069
106107
  crashAnalysisEnabled: enabled
@@ -106368,6 +106406,15 @@ class DataResource {
106368
106406
  getAdminLink(appRef) {
106369
106407
  return this.http.get(`/apps/${encodeURIComponent(appRef)}/data/admin-link`);
106370
106408
  }
106409
+ setOAuthProvider(appRef, body) {
106410
+ return this.http.post(`/apps/${encodeURIComponent(appRef)}/data/oauth`, body);
106411
+ }
106412
+ getOAuthStatus(appRef) {
106413
+ return this.http.get(`/apps/${encodeURIComponent(appRef)}/data/oauth`);
106414
+ }
106415
+ removeOAuthProvider(appRef, provider) {
106416
+ return this.http.delete(`/apps/${encodeURIComponent(appRef)}/data/oauth/${encodeURIComponent(provider)}`);
106417
+ }
106371
106418
  }
106372
106419
 
106373
106420
  // ../client/dist/resources/env.js
@@ -106702,9 +106749,10 @@ class PercherClient {
106702
106749
  invites;
106703
106750
  transfers;
106704
106751
  constructor(options = {}) {
106705
- const defaults = loadConfig();
106706
- this.apiUrl = options.apiUrl ?? defaults.apiUrl;
106707
- this.token = options.token !== undefined ? options.token : defaults.token;
106752
+ const bothExplicit = options.apiUrl !== undefined && options.token !== undefined;
106753
+ const defaults = bothExplicit ? undefined : loadConfig();
106754
+ this.apiUrl = options.apiUrl ?? defaults?.apiUrl ?? "";
106755
+ this.token = options.token !== undefined ? options.token : defaults?.token ?? null;
106708
106756
  this.sessionId = options.sessionId;
106709
106757
  const http = new Http({
106710
106758
  apiUrl: this.apiUrl,
@@ -106967,6 +107015,49 @@ async function dataExport(ctx, input) {
106967
107015
  exportedAt: new Date().toISOString()
106968
107016
  };
106969
107017
  }
107018
+ // ../core/dist/commands/data-oauth.js
107019
+ var dataOauthInputSchema = exports_external.object({
107020
+ app: exports_external.string().optional(),
107021
+ provider: exports_external.string().default("google"),
107022
+ clientId: exports_external.string().optional(),
107023
+ clientSecret: exports_external.string().optional(),
107024
+ remove: exports_external.boolean().optional(),
107025
+ json: exports_external.boolean().optional()
107026
+ });
107027
+ async function dataOauth(ctx, input = { provider: "google" }) {
107028
+ const name = input.app ?? readPercherTomlAppName(ctx.cwd);
107029
+ if (!name) {
107030
+ throw new PercherCoreError("No app specified and no percher.toml found", {
107031
+ code: "data.no-app",
107032
+ hint: "Pass an app name or run from a directory with percher.toml."
107033
+ });
107034
+ }
107035
+ const provider = input.provider || "google";
107036
+ if (input.remove) {
107037
+ await ctx.client.data.removeOAuthProvider(name, provider);
107038
+ return { action: "removed", provider };
107039
+ }
107040
+ if (input.clientId && input.clientSecret) {
107041
+ const res = await ctx.client.data.setOAuthProvider(name, {
107042
+ provider,
107043
+ clientId: input.clientId,
107044
+ clientSecret: input.clientSecret
107045
+ });
107046
+ return {
107047
+ action: "set",
107048
+ provider: res.provider,
107049
+ redirectUri: res.redirectUri,
107050
+ note: res.note
107051
+ };
107052
+ }
107053
+ const status = await ctx.client.data.getOAuthStatus(name);
107054
+ return {
107055
+ action: "status",
107056
+ provider,
107057
+ providers: status.providers,
107058
+ redirectUri: status.redirectUri
107059
+ };
107060
+ }
106970
107061
  // ../core/dist/commands/delete.js
106971
107062
  var deleteInputSchema = exports_external.object({
106972
107063
  app: exports_external.string().optional().describe("App name to delete (defaults to percher.toml app name)"),
@@ -106988,427 +107079,278 @@ async function deleteApp(ctx, input) {
106988
107079
  }
106989
107080
  await ctx.client.apps.delete(name);
106990
107081
  }
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
- });
107082
+ // ../core/dist/poll-deployment.js
107083
+ async function pollDeployment(opts) {
107084
+ const terminal = opts.terminal ?? ["live", "failed"];
107085
+ const intervalMs = opts.intervalMs ?? 2000;
107086
+ const timeoutMs = opts.timeoutMs ?? TIMEOUTS.clientPublishPoll;
107087
+ const maxRetries = opts.maxRetries ?? 5;
107088
+ let deployment = opts.initial;
107089
+ const startedAt = Date.now();
107090
+ const seenEventIds = new Set;
107091
+ if (opts.onEvent) {
107092
+ await drainNewEvents(opts, deployment.id, seenEventIds);
107015
107093
  }
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
- });
107094
+ while (!terminal.includes(deployment.status)) {
107095
+ if (Date.now() - startedAt > timeoutMs) {
107096
+ opts.onTimeout(deployment);
107097
+ }
107098
+ opts.onTick?.(deployment);
107099
+ await new Promise((r) => setTimeout(r, intervalMs));
107100
+ let attempt = 0;
107101
+ let polled = false;
107102
+ while (!polled) {
107103
+ try {
107104
+ deployment = await opts.ctx.client.apps.getDeployment(opts.appId, deployment.id);
107105
+ polled = true;
107106
+ } catch (err) {
107107
+ attempt++;
107108
+ const isNetwork = isTransientNetworkError(err);
107109
+ if (!isNetwork || attempt >= maxRetries) {
107110
+ if (isNetwork) {
107111
+ const msg = err.message ?? String(err);
107112
+ throw new Error(`${msg}
107113
+
107114
+ The deploy may already be live — your network connection dropped mid-poll. Check with: percher deploys list ${opts.appName}`, { cause: err });
107115
+ }
107116
+ throw err;
107117
+ }
107118
+ const backoff = (opts.backoffMs ?? ((n) => 1000 * 2 ** (n - 1)))(attempt);
107119
+ await new Promise((r) => setTimeout(r, backoff));
107120
+ }
107121
+ }
107122
+ if (opts.onEvent) {
107123
+ await drainNewEvents(opts, deployment.id, seenEventIds);
107033
107124
  }
107034
- deployId = failed.id;
107035
- }
107036
- if (!deployId) {
107037
- throw new PercherCoreError("Deploy ID is required", {
107038
- code: "deploys.no-id"
107039
- });
107040
107125
  }
107041
- const deploy = await ctx.client.apps.getDeployment(name, deployId);
107042
- let buildLog = null;
107126
+ return deployment;
107127
+ }
107128
+ async function drainNewEvents(opts, deployId, seenEventIds) {
107129
+ if (!opts.onEvent)
107130
+ return;
107043
107131
  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;
107132
+ const { events } = await opts.ctx.client.apps.getDeployEvents(opts.appId, deployId);
107133
+ for (const event of events) {
107134
+ if (seenEventIds.has(event.id))
107135
+ continue;
107136
+ seenEventIds.add(event.id);
107137
+ opts.onEvent(event);
107051
107138
  }
107052
- }
107053
- return { deploy, buildLog };
107139
+ } catch {}
107054
107140
  }
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
107141
 
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}/`))
107096
- continue;
107097
- yield* walk(full, baseDir, ig);
107098
- } else if (stats.isFile()) {
107099
- yield rel;
107100
- }
107101
- }
107102
- }
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 }));
107142
+ // ../core/dist/recovery.js
107143
+ function recoveryNone(opts = {}) {
107143
107144
  return {
107144
- stream: webStream,
107145
- bytes: totalBytes,
107146
- fileCount: fileBuffers.length,
107147
- topFiles,
107148
- contentHash
107145
+ retryable: false,
107146
+ nextAction: "none",
107147
+ reasonCode: opts.reasonCode ?? "none",
107148
+ ...opts.url !== undefined ? { url: opts.url } : {}
107149
107149
  };
107150
107150
  }
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
- });
107151
+ function recoveryLogin(opts = {}) {
107201
107152
  return {
107202
- close() {
107203
- if (timer)
107204
- clearTimeout(timer);
107205
- fsWatcher.close();
107206
- }
107153
+ retryable: false,
107154
+ nextAction: "open_login",
107155
+ suggestedTool: "percher_login",
107156
+ args: {},
107157
+ reasonCode: opts.reasonCode ?? "auth_required"
107207
107158
  };
107208
107159
  }
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;
107160
+ function recoveryWait(opts) {
107161
+ return {
107162
+ retryable: false,
107163
+ nextAction: "wait_deploy",
107164
+ suggestedTool: "percher_wait_for_deploy",
107165
+ args: {
107166
+ app: opts.app,
107167
+ deployId: opts.deployId,
107168
+ timeoutSeconds: opts.timeoutSeconds ?? TIMEOUTS.mcpWaitDeployDefault / 1000
107169
+ },
107170
+ reasonCode: opts.reasonCode
107171
+ };
107226
107172
  }
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
- }
107173
+ function recoveryDoctor(opts) {
107174
+ const args = {};
107175
+ if (opts.app !== undefined)
107176
+ args.app = opts.app;
107177
+ if (opts.deployId !== undefined)
107178
+ args.deployId = opts.deployId;
107179
+ if (opts.mode !== undefined)
107180
+ args.mode = opts.mode;
107181
+ return {
107182
+ retryable: false,
107183
+ nextAction: "run_doctor",
107184
+ suggestedTool: "percher_doctor",
107185
+ args,
107186
+ reasonCode: opts.reasonCode,
107187
+ ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107188
+ };
107240
107189
  }
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 };
107190
+ function recoveryEnv(opts) {
107191
+ const args = {};
107192
+ if (opts.app !== undefined)
107193
+ args.app = opts.app;
107194
+ if (opts.keys.length > 0)
107195
+ args.keys = opts.keys;
107196
+ return {
107197
+ retryable: false,
107198
+ nextAction: "set_env_vars",
107199
+ suggestedTool: "percher_env_set",
107200
+ args,
107201
+ reasonCode: opts.reasonCode ?? "missing_env"
107202
+ };
107289
107203
  }
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}`;
107204
+ function recoveryFixConfig(opts) {
107205
+ return {
107206
+ retryable: false,
107207
+ nextAction: "fix_config",
107208
+ problems: opts.problems,
107209
+ reasonCode: opts.reasonCode ?? "config_invalid"
107210
+ };
107294
107211
  }
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
- }
107212
+ function recoveryFixProblems(opts) {
107213
+ return {
107214
+ retryable: false,
107215
+ nextAction: "fix_problems",
107216
+ problems: opts.problems,
107217
+ reasonCode: opts.reasonCode ?? "build_failed",
107218
+ ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107219
+ };
107307
107220
  }
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);
107221
+ function recoveryRetry(opts) {
107222
+ return {
107223
+ retryable: true,
107224
+ nextAction: "retry",
107225
+ suggestedTool: opts.suggestedTool,
107226
+ args: opts.args,
107227
+ reasonCode: opts.reasonCode
107228
+ };
107229
+ }
107230
+ function recoveryAsk(opts) {
107231
+ if (!opts.prompt || opts.prompt.trim().length === 0) {
107232
+ throw new Error("recoveryAsk requires a non-empty prompt");
107320
107233
  }
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);
107234
+ return {
107235
+ retryable: opts.retryable ?? false,
107236
+ nextAction: "ask_user",
107237
+ prompt: opts.prompt,
107238
+ ...opts.options !== undefined ? { options: opts.options } : {},
107239
+ reasonCode: opts.reasonCode
107240
+ };
107241
+ }
107242
+ function recoveryInspectBuildLog(opts) {
107243
+ const args = opts.deployId ? { id: opts.deployId, app: opts.app } : { latestFailed: true, app: opts.app };
107244
+ return {
107245
+ retryable: false,
107246
+ nextAction: "inspect_build_log",
107247
+ suggestedTool: "percher_deploys_inspect",
107248
+ args,
107249
+ reasonCode: opts.reasonCode ?? "build_failed",
107250
+ ...opts.alternativeActions !== undefined ? { alternativeActions: opts.alternativeActions } : {}
107251
+ };
107252
+ }
107253
+ var RECOVERY_NONE = recoveryNone();
107254
+ var RECOVERY_NEEDS_LOGIN = recoveryLogin();
107255
+ function buildProblemToRecoveryProblem(p) {
107256
+ const file2 = p.file ?? "";
107257
+ const message = p.hint && p.hint.length > 0 ? `${p.message}
107258
+ Hint: ${p.hint}` : p.message;
107259
+ return {
107260
+ file: file2,
107261
+ ...typeof p.line === "number" ? { line: p.line } : {},
107262
+ ...typeof p.column === "number" ? { column: p.column } : {},
107263
+ message
107264
+ };
107265
+ }
107266
+ function recoveryFromErrorClass(errorClass, failure = {}) {
107267
+ switch (errorClass) {
107268
+ case "missing_env":
107269
+ return recoveryEnv({
107270
+ keys: failure.missingEnvVars ?? [],
107271
+ reasonCode: "missing_env"
107272
+ });
107273
+ case "infra_unavailable":
107274
+ return recoveryRetry({
107275
+ suggestedTool: "percher_publish",
107276
+ args: failure.publishInput ?? {},
107277
+ reasonCode: "infra_unavailable"
107278
+ });
107279
+ case "platform_at_capacity":
107280
+ return recoveryRetry({
107281
+ suggestedTool: "percher_publish",
107282
+ args: failure.publishInput ?? {},
107283
+ reasonCode: "infra_transient"
107284
+ });
107285
+ case "config_invalid":
107286
+ return recoveryFixConfig({
107287
+ problems: [
107288
+ {
107289
+ file: "percher.toml",
107290
+ message: "percher.toml failed validation — fix the file and re-run."
107343
107291
  }
107344
- } catch (err) {
107345
- await callbacks.writeLine(`Push error: ${err.message}`);
107346
- startLogStream(app.id);
107347
- }
107348
- } else {
107349
- startLogStream(app.id);
107292
+ ],
107293
+ reasonCode: "config_invalid"
107294
+ });
107295
+ case "build_failed":
107296
+ case "missing_dependency":
107297
+ case "health_check_timeout":
107298
+ case "port_mismatch":
107299
+ case "permission_denied":
107300
+ case "runtime_crash":
107301
+ case "oom_killed": {
107302
+ const fileProblems = (failure.problems ?? []).filter((p) => p.file);
107303
+ const reproduceAlt = failure.deployId ? {
107304
+ action: "reproduce_locally",
107305
+ suggestedTool: "percher_reproduce",
107306
+ args: { deployId: failure.deployId, app: failure.appName },
107307
+ when: "after the primary path didn't surface a clear fix and the user has nixpacks installed locally"
107308
+ } : undefined;
107309
+ if (fileProblems.length > 0) {
107310
+ return recoveryFixProblems({
107311
+ problems: fileProblems.map(buildProblemToRecoveryProblem),
107312
+ reasonCode: "build_failed",
107313
+ ...reproduceAlt ? {
107314
+ alternativeActions: [
107315
+ {
107316
+ ...reproduceAlt,
107317
+ when: "after fixing the file-located problems didn't make the build pass and the user has nixpacks installed locally"
107318
+ }
107319
+ ]
107320
+ } : {}
107321
+ });
107350
107322
  }
107351
- } finally {
107352
- busy = false;
107353
- if (queued) {
107354
- const next = queued;
107355
- queued = null;
107356
- await handleChange(next);
107323
+ const inspectAlt = {
107324
+ action: "inspect_build_log",
107325
+ suggestedTool: "percher_deploys_inspect",
107326
+ args: failure.deployId ? { id: failure.deployId, app: failure.appName } : { latestFailed: true, app: failure.appName },
107327
+ when: "after percher_doctor's recovery didn't yield a fix and you want to read the raw build log directly"
107328
+ };
107329
+ const alternativeActions = [inspectAlt];
107330
+ if (reproduceAlt) {
107331
+ alternativeActions.push({
107332
+ ...reproduceAlt,
107333
+ when: "after percher_doctor + percher_deploys_inspect didn't surface a clear fix and the user has nixpacks installed locally"
107334
+ });
107357
107335
  }
107336
+ return recoveryDoctor({
107337
+ app: failure.appName,
107338
+ deployId: failure.deployId,
107339
+ mode: "deploy",
107340
+ reasonCode: "build_failed",
107341
+ alternativeActions
107342
+ });
107358
107343
  }
107344
+ default:
107345
+ return recoveryAsk({
107346
+ prompt: "Unexpected failure — Percher couldn't classify this error. Try `percher doctor` for diagnostics, or surface the error to the user.",
107347
+ reasonCode: "unknown"
107348
+ });
107359
107349
  }
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
107350
  }
107409
- // ../core/dist/commands/doctor.js
107410
- import { existsSync as existsSync9 } from "node:fs";
107411
- import { join as join13 } from "node:path";
107351
+
107352
+ // ../core/dist/commands/publish-failure.js
107353
+ import { join as join9 } from "node:path";
107412
107354
 
107413
107355
  // ../core/dist/error-classifier.js
107414
107356
  function extractEnvVarsFromLog(combined) {
@@ -107742,219 +107684,6 @@ ${buildLog ?? ""}`;
107742
107684
  return null;
107743
107685
  }
107744
107686
 
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) {
107808
- return {
107809
- retryable: false,
107810
- nextAction: "fix_config",
107811
- problems: opts.problems,
107812
- reasonCode: opts.reasonCode ?? "config_invalid"
107813
- };
107814
- }
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
- };
107823
- }
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
- };
107832
- }
107833
- function recoveryAsk(opts) {
107834
- if (!opts.prompt || opts.prompt.trim().length === 0) {
107835
- throw new Error("recoveryAsk requires a non-empty prompt");
107836
- }
107837
- 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
107843
- };
107844
- }
107845
- function recoveryInspectBuildLog(opts) {
107846
- const args = opts.deployId ? { id: opts.deployId, app: opts.app } : { latestFailed: true, app: opts.app };
107847
- 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 } : {}
107854
- };
107855
- }
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;
107862
- return {
107863
- file: file2,
107864
- ...typeof p.line === "number" ? { line: p.line } : {},
107865
- ...typeof p.column === "number" ? { column: p.column } : {},
107866
- message
107867
- };
107868
- }
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"
107897
- });
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
107945
- });
107946
- }
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
- });
107952
- }
107953
- }
107954
-
107955
- // ../core/dist/commands/publish-failure.js
107956
- import { join as join12 } from "node:path";
107957
-
107958
107687
  // ../core/dist/structured-error-codes.js
107959
107688
  var MAPPINGS = {
107960
107689
  WORKER_UNAVAILABLE: {
@@ -108005,128 +107734,600 @@ var MAPPINGS = {
108005
107734
  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
107735
  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."
108007
107736
  }
108008
- };
108009
- function classifyByErrorCode(code, rawMessage) {
108010
- if (!code)
108011
- return null;
108012
- const mapping = MAPPINGS[code];
108013
- if (!mapping)
108014
- return null;
107737
+ };
107738
+ function classifyByErrorCode(code, rawMessage) {
107739
+ if (!code)
107740
+ return null;
107741
+ const mapping = MAPPINGS[code];
107742
+ if (!mapping)
107743
+ return null;
107744
+ return {
107745
+ title: mapping.title,
107746
+ explanation: mapping.explanation(rawMessage),
107747
+ suggestion: mapping.suggestion,
107748
+ errorClass: mapping.errorClass,
107749
+ phase: mapping.phase,
107750
+ cause: mapping.cause,
107751
+ relevantFiles: [],
107752
+ missingEnvVars: []
107753
+ };
107754
+ }
107755
+ function trimMessage(raw) {
107756
+ const firstLine = raw.split(`
107757
+ `)[0] ?? "";
107758
+ return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine;
107759
+ }
107760
+
107761
+ // ../core/dist/commands/publish-failure.js
107762
+ function tomlPathFor(cwd) {
107763
+ return join9(cwd, "percher.toml");
107764
+ }
107765
+ async function buildFailureError(ctx, app, deployment, signal) {
107766
+ let buildLogTail;
107767
+ let buildLogFetchFailed = false;
107768
+ let fullLog = "";
107769
+ try {
107770
+ fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
107771
+ if (!fullLog && !signal?.aborted) {
107772
+ await abortableSleep(1500, signal);
107773
+ if (!signal?.aborted) {
107774
+ fullLog = await ctx.client.apps.getBuildLog(app.id, deployment.id, { signal });
107775
+ }
107776
+ }
107777
+ if (fullLog) {
107778
+ const lines = fullLog.split(`
107779
+ `);
107780
+ buildLogTail = lines.slice(-50).join(`
107781
+ `);
107782
+ }
107783
+ } catch {
107784
+ buildLogFetchFailed = true;
107785
+ }
107786
+ const errorMessage = deployment.errorMessage ?? "";
107787
+ const classified = classifyByErrorCode(deployment.errorCode, errorMessage) ?? classifyError(errorMessage || "Deployment failed", fullLog);
107788
+ const rawErrorMessage = errorMessage && !buildLogTail && !classified?.explanation.includes(errorMessage) ? errorMessage : undefined;
107789
+ const problems = deployment.problems && deployment.problems.length > 0 ? deployment.problems : undefined;
107790
+ if (classified) {
107791
+ return {
107792
+ title: classified.title,
107793
+ explanation: classified.explanation,
107794
+ suggestion: classified.suggestion,
107795
+ buildLogTail,
107796
+ rawErrorMessage,
107797
+ errorClass: classified.errorClass,
107798
+ phase: classified.phase,
107799
+ cause: classified.cause,
107800
+ relevantFiles: classified.relevantFiles,
107801
+ missingEnvVars: classified.missingEnvVars,
107802
+ problems
107803
+ };
107804
+ }
107805
+ return {
107806
+ title: "Deployment failed",
107807
+ explanation: errorMessage ? `The deployment failed: ${errorMessage}` : "The deployment failed. See the build log for details.",
107808
+ 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(`
107809
+ `)[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}`,
107810
+ buildLogTail,
107811
+ errorClass: "build_failed",
107812
+ phase: "build",
107813
+ cause: "unknown",
107814
+ relevantFiles: [],
107815
+ missingEnvVars: [],
107816
+ problems
107817
+ };
107818
+ }
107819
+ async function classifyDeploymentFailure(opts) {
107820
+ const { ctx, app, deployment, publishInput } = opts;
107821
+ const error48 = await buildFailureError(ctx, app, deployment, opts.signal);
107822
+ const recovery = error48.errorClass ? recoveryFromErrorClass(error48.errorClass, {
107823
+ missingEnvVars: error48.missingEnvVars,
107824
+ deployId: deployment.id,
107825
+ appName: app.name,
107826
+ publishInput,
107827
+ problems: error48.problems
107828
+ }) : recoveryAsk({
107829
+ prompt: `${app.name} deploy ${deployment.id} failed: ${error48.title}. ${error48.suggestion}`.trim(),
107830
+ reasonCode: "build_failed"
107831
+ });
107832
+ return {
107833
+ error: error48,
107834
+ recovery,
107835
+ summary: `${error48.title} (${app.name}, deploy ${deployment.id}).`
107836
+ };
107837
+ }
107838
+ async function buildFailureResult(opts) {
107839
+ const { ctx, app, deployment, tarball, input } = opts;
107840
+ const { error: error48, recovery, summary } = await classifyDeploymentFailure({
107841
+ ctx,
107842
+ app,
107843
+ deployment,
107844
+ publishInput: input
107845
+ });
107846
+ return {
107847
+ status: "failed",
107848
+ app,
107849
+ deployment,
107850
+ fileCount: tarball.fileCount,
107851
+ bytes: tarball.bytes,
107852
+ error: error48,
107853
+ recovery,
107854
+ summary,
107855
+ configPath: tomlPathFor(ctx.cwd),
107856
+ bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes }
107857
+ };
107858
+ }
107859
+
107860
+ // ../core/dist/commands/deploy-repo.js
107861
+ var deployRepoInputSchema = exports_external.object({
107862
+ app: exports_external.string().describe("App name or id (required — hosted has no cwd)."),
107863
+ branch: exports_external.string().optional().describe("Branch to deploy. Defaults to the app's tracked branch."),
107864
+ note: exports_external.string().optional().describe("Optional note recorded against the deploy.")
107865
+ });
107866
+ // ../core/dist/commands/deploys.js
107867
+ var deploysListInputSchema = exports_external.object({
107868
+ app: exports_external.string().optional(),
107869
+ limit: exports_external.number().int().positive().max(100).optional()
107870
+ });
107871
+ var deploysInspectInputSchema = exports_external.object({
107872
+ app: exports_external.string().optional(),
107873
+ id: exports_external.string().optional(),
107874
+ latestFailed: exports_external.boolean().optional()
107875
+ }).superRefine((val, ctx) => {
107876
+ if (!val.id && !val.latestFailed) {
107877
+ ctx.addIssue({
107878
+ code: exports_external.ZodIssueCode.custom,
107879
+ message: "either `id` or `latestFailed: true` must be provided"
107880
+ });
107881
+ }
107882
+ });
107883
+ function resolveAppName(ctx, app) {
107884
+ const name = app ?? readPercherTomlAppName(ctx.cwd);
107885
+ if (!name) {
107886
+ throw new PercherCoreError("No app specified and no percher.toml found", {
107887
+ code: "deploys.no-app",
107888
+ hint: "Pass --app <name> or run from a directory with percher.toml."
107889
+ });
107890
+ }
107891
+ return name;
107892
+ }
107893
+ async function deploysList(ctx, input = {}) {
107894
+ const name = resolveAppName(ctx, input.app);
107895
+ return ctx.client.apps.listDeploys(name, { limit: input.limit ?? 10 });
107896
+ }
107897
+ async function deploysInspect(ctx, input) {
107898
+ const name = resolveAppName(ctx, input.app);
107899
+ let deployId = input.id;
107900
+ if (!deployId && input.latestFailed) {
107901
+ const recent = await ctx.client.apps.listDeploys(name, { limit: 25 });
107902
+ const failed = recent.find((d) => d.status === "failed");
107903
+ if (!failed) {
107904
+ throw new PercherCoreError(`No failed deploys found for "${name}"`, {
107905
+ code: "deploys.no-failed",
107906
+ hint: "Try `percher deploys` to list recent deploys, or omit --latest-failed."
107907
+ });
107908
+ }
107909
+ deployId = failed.id;
107910
+ }
107911
+ if (!deployId) {
107912
+ throw new PercherCoreError("Deploy ID is required", {
107913
+ code: "deploys.no-id"
107914
+ });
107915
+ }
107916
+ const deploy = await ctx.client.apps.getDeployment(name, deployId);
107917
+ let buildLog = null;
107918
+ try {
107919
+ const log = await ctx.client.apps.getBuildLog(name, deployId);
107920
+ buildLog = log.length > 0 ? log : null;
107921
+ } catch (err) {
107922
+ if (err instanceof PercherApiError && err.status === 404) {
107923
+ buildLog = null;
107924
+ } else {
107925
+ throw err;
107926
+ }
107927
+ }
107928
+ return { deploy, buildLog };
107929
+ }
107930
+ // ../core/dist/commands/dev.js
107931
+ import { existsSync as existsSync8 } from "node:fs";
107932
+ import { join as join12 } from "node:path";
107933
+
107934
+ // ../core/dist/tarball.js
107935
+ var import_ignore = __toESM(require_ignore(), 1);
107936
+ import { createHash as createHash2 } from "node:crypto";
107937
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
107938
+ import { join as join10, relative as relative3, sep as sep2 } from "node:path";
107939
+ import { Readable } from "node:stream";
107940
+ import { createGzip } from "node:zlib";
107941
+
107942
+ // ../../node_modules/.bun/tar-stream@3.1.8/node_modules/tar-stream/index.js
107943
+ var $extract = require_extract();
107944
+ var $pack = require_pack();
107945
+
107946
+ // ../core/dist/tarball.js
107947
+ function isFilesOptions(options) {
107948
+ return "files" in options;
107949
+ }
107950
+ var DEFAULT_IGNORE = [
107951
+ "node_modules",
107952
+ ".git",
107953
+ "dist",
107954
+ ".next",
107955
+ ".svelte-kit",
107956
+ "build",
107957
+ "coverage",
107958
+ ".env",
107959
+ ".env.*",
107960
+ ".bun",
107961
+ "*.log",
107962
+ ".DS_Store"
107963
+ ];
107964
+ function* walk(dir, baseDir, ig) {
107965
+ const entries = readdirSync2(dir);
107966
+ for (const name of entries) {
107967
+ const full = join10(dir, name);
107968
+ const rel = relative3(baseDir, full).split(sep2).join("/");
107969
+ if (ig.ignores(rel))
107970
+ continue;
107971
+ const stats = statSync2(full);
107972
+ if (stats.isDirectory()) {
107973
+ if (ig.ignores(`${rel}/`))
107974
+ continue;
107975
+ yield* walk(full, baseDir, ig);
107976
+ } else if (stats.isFile()) {
107977
+ yield rel;
107978
+ }
107979
+ }
107980
+ }
107981
+ function normalizeInlinePath(raw) {
107982
+ const path = raw.replace(/\\/g, "/");
107983
+ if (path.length === 0) {
107984
+ throw new Error("file path must not be empty");
107985
+ }
107986
+ if (path.startsWith("/")) {
107987
+ throw new Error(`file path must be relative, got absolute path: ${raw}`);
107988
+ }
107989
+ if (/^[a-zA-Z]:/.test(path)) {
107990
+ throw new Error(`file path must be relative, got drive path: ${raw}`);
107991
+ }
107992
+ const segments = path.split("/");
107993
+ if (segments.some((s) => s === "..")) {
107994
+ throw new Error(`file path must not contain "..": ${raw}`);
107995
+ }
107996
+ const cleaned = segments.filter((s) => s !== "" && s !== ".").join("/");
107997
+ if (cleaned.length === 0) {
107998
+ throw new Error(`file path resolves to nothing: ${raw}`);
107999
+ }
108000
+ return cleaned;
108001
+ }
108002
+ function buildTarball(fileBuffers) {
108003
+ fileBuffers.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
108004
+ const totalBytes = fileBuffers.reduce((sum, f) => sum + f.size, 0);
108005
+ const contentHasher = createHash2("sha256");
108006
+ for (const f of fileBuffers) {
108007
+ contentHasher.update(`PATH:${f.path.length}:${f.path}
108008
+ `);
108009
+ contentHasher.update(`SIZE:${f.size}
108010
+ `);
108011
+ contentHasher.update(f.data);
108012
+ contentHasher.update(`
108013
+ `);
108014
+ }
108015
+ const contentHash = contentHasher.digest("hex");
108016
+ const pack = $pack();
108017
+ (async () => {
108018
+ for (const file2 of fileBuffers) {
108019
+ await new Promise((resolve7, reject) => {
108020
+ pack.entry({ name: file2.path, size: file2.data.length }, file2.data, (err) => err ? reject(err) : resolve7());
108021
+ });
108022
+ }
108023
+ pack.finalize();
108024
+ })();
108025
+ const gzip = createGzip();
108026
+ const nodeStream = pack.pipe(gzip);
108027
+ const webStream = Readable.toWeb(nodeStream);
108028
+ const topFiles = [...fileBuffers].sort((a, b) => b.size - a.size).slice(0, 5).map((f) => ({ path: f.path, size: f.size }));
108029
+ return {
108030
+ stream: webStream,
108031
+ bytes: totalBytes,
108032
+ fileCount: fileBuffers.length,
108033
+ topFiles,
108034
+ contentHash
108035
+ };
108036
+ }
108037
+ async function createTarball(options) {
108038
+ if (isFilesOptions(options)) {
108039
+ return createTarballFromFiles(options.files, options.config);
108040
+ }
108041
+ const ig = import_ignore.default();
108042
+ ig.add(DEFAULT_IGNORE);
108043
+ const gitignorePath = join10(options.cwd, ".gitignore");
108044
+ if (existsSync6(gitignorePath)) {
108045
+ ig.add(readFileSync6(gitignorePath, "utf8"));
108046
+ }
108047
+ if (options.extraIgnore)
108048
+ ig.add(options.extraIgnore);
108049
+ const fileBuffers = [];
108050
+ for (const rel of walk(options.cwd, options.cwd, ig)) {
108051
+ const data2 = readFileSync6(join10(options.cwd, rel));
108052
+ fileBuffers.push({ path: rel, size: data2.length, data: data2 });
108053
+ }
108054
+ return buildTarball(fileBuffers);
108055
+ }
108056
+ function createTarballFromFiles(files, _config) {
108057
+ const seen = new Set;
108058
+ const fileBuffers = [];
108059
+ for (const file2 of files) {
108060
+ const path = normalizeInlinePath(file2.path);
108061
+ if (seen.has(path)) {
108062
+ throw new Error(`duplicate file path in inline source: ${path}`);
108063
+ }
108064
+ seen.add(path);
108065
+ const data2 = Buffer.from(file2.data);
108066
+ fileBuffers.push({ path, size: data2.length, data: data2 });
108067
+ }
108068
+ return buildTarball(fileBuffers);
108069
+ }
108070
+
108071
+ // ../core/dist/watcher.js
108072
+ var import_ignore2 = __toESM(require_ignore(), 1);
108073
+ import { existsSync as existsSync7, readFileSync as readFileSync7, watch } from "node:fs";
108074
+ import { join as join11, sep as sep3 } from "node:path";
108075
+ var DEFAULT_IGNORE2 = [
108076
+ "node_modules",
108077
+ ".git",
108078
+ "dist",
108079
+ ".next",
108080
+ ".svelte-kit",
108081
+ "build",
108082
+ "coverage",
108083
+ ".env",
108084
+ ".env.*",
108085
+ ".bun",
108086
+ "*.log",
108087
+ ".DS_Store",
108088
+ "__pycache__"
108089
+ ];
108090
+ function createWatcher(options) {
108091
+ const debounceMs = options.debounceMs ?? 500;
108092
+ const ig = import_ignore2.default();
108093
+ ig.add(DEFAULT_IGNORE2);
108094
+ const gitignorePath = join11(options.cwd, ".gitignore");
108095
+ if (existsSync7(gitignorePath)) {
108096
+ ig.add(readFileSync7(gitignorePath, "utf8"));
108097
+ }
108098
+ if (options.extraIgnore)
108099
+ ig.add(options.extraIgnore);
108100
+ let timer = null;
108101
+ let pending = new Set;
108102
+ const fsWatcher = watch(options.cwd, { recursive: true }, (_event, filename) => {
108103
+ if (!filename)
108104
+ return;
108105
+ const rel = filename.split(sep3).join("/");
108106
+ if (ig.ignores(rel))
108107
+ return;
108108
+ pending.add(rel);
108109
+ if (timer)
108110
+ clearTimeout(timer);
108111
+ timer = setTimeout(() => {
108112
+ const files = [...pending];
108113
+ pending = new Set;
108114
+ timer = null;
108115
+ if (files.length > 0) {
108116
+ options.onChange(files);
108117
+ }
108118
+ }, debounceMs);
108119
+ });
108015
108120
  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: []
108121
+ close() {
108122
+ if (timer)
108123
+ clearTimeout(timer);
108124
+ fsWatcher.close();
108125
+ }
108024
108126
  };
108025
108127
  }
108026
- function trimMessage(raw) {
108027
- const firstLine = raw.split(`
108028
- `)[0] ?? "";
108029
- return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine;
108030
- }
108031
108128
 
108032
- // ../core/dist/commands/publish-failure.js
108033
- function tomlPathFor(cwd) {
108034
- return join12(cwd, "percher.toml");
108129
+ // ../core/dist/commands/dev.js
108130
+ var devInputSchema = exports_external.object({
108131
+ app: exports_external.string().optional()
108132
+ });
108133
+ async function loadConfig2(ctx, appOverride) {
108134
+ const tomlPath = join12(ctx.cwd, "percher.toml");
108135
+ if (!existsSync8(tomlPath)) {
108136
+ throw new PercherCoreError("No percher.toml found. Run: percher init", {
108137
+ code: "dev.no-app",
108138
+ hint: "Create a percher.toml first with `percher init`."
108139
+ });
108140
+ }
108141
+ const config2 = await parseFile(tomlPath);
108142
+ if (appOverride)
108143
+ return { ...config2, app: { ...config2.app, name: appOverride } };
108144
+ return config2;
108035
108145
  }
108036
- async function buildFailureError(ctx, app, deployment, signal) {
108037
- let buildLogTail;
108038
- let buildLogFetchFailed = false;
108039
- let fullLog = "";
108146
+ async function ensureApp(ctx, config2) {
108040
108147
  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 });
108148
+ return await ctx.client.apps.get(config2.app.name);
108149
+ } catch (err) {
108150
+ if (err.code === "APP_NOT_FOUND") {
108151
+ return ctx.client.apps.create({
108152
+ name: config2.app.name,
108153
+ runtime: config2.app.runtime,
108154
+ framework: config2.app.framework
108155
+ });
108156
+ }
108157
+ throw err;
108158
+ }
108159
+ }
108160
+ async function doPush(ctx, config2, callbacks) {
108161
+ const tarball = await createTarball({ cwd: ctx.cwd, config: config2 });
108162
+ const app = await ensureApp(ctx, config2);
108163
+ await callbacks.writeLine(`Pushing ${tarball.fileCount} files (${(tarball.bytes / 1024).toFixed(0)} KB)...`);
108164
+ const deployResponse = await ctx.client.apps.deploy(app.id, { tarball: tarball.stream });
108165
+ if (deployResponse.status === "already_in_progress") {
108166
+ const active = deployResponse;
108167
+ await callbacks.writeLine(` Skipped — deploy already in progress (${active.deployId}, ${active.deployStatus}).`);
108168
+ return { success: false, appId: app.id };
108169
+ }
108170
+ let deployment = deployResponse;
108171
+ const startedAt = Date.now();
108172
+ const timeoutMs = TIMEOUTS.clientPublishPoll;
108173
+ const pollInterval = 2000;
108174
+ let lastStatus = "";
108175
+ while (deployment.status !== "live" && deployment.status !== "failed") {
108176
+ if (Date.now() - startedAt > timeoutMs) {
108177
+ await callbacks.writeLine("Deploy timed out");
108178
+ return { success: false, appId: app.id };
108179
+ }
108180
+ if (callbacks.signal.aborted)
108181
+ return { success: false, appId: app.id };
108182
+ if (deployment.status !== lastStatus) {
108183
+ lastStatus = deployment.status;
108184
+ await callbacks.writeLine(` ${deployment.status}...`);
108185
+ }
108186
+ await new Promise((r) => setTimeout(r, pollInterval));
108187
+ deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
108188
+ }
108189
+ if (deployment.status === "failed") {
108190
+ try {
108191
+ const insp = await ctx.client.apps.getDeployment(app.id, deployment.id);
108192
+ if (insp.errorMessage) {
108193
+ await callbacks.writeLine("Deploy failed:");
108194
+ for (const line of insp.errorMessage.split(`
108195
+ `).slice(0, 30)) {
108196
+ await callbacks.writeLine(` ${line}`);
108197
+ }
108198
+ } else {
108199
+ await callbacks.writeLine("Deploy failed.");
108046
108200
  }
108201
+ } catch {
108202
+ await callbacks.writeLine("Deploy failed.");
108047
108203
  }
108048
- if (fullLog) {
108049
- const lines = fullLog.split(`
108050
- `);
108051
- buildLogTail = lines.slice(-50).join(`
108052
- `);
108204
+ return { success: false, appId: app.id };
108205
+ }
108206
+ await callbacks.writeLine(`Live at ${deployment.url ?? ""}`);
108207
+ return { success: true, appId: app.id };
108208
+ }
108209
+ function formatLogLine(l) {
108210
+ const level = l.level.toUpperCase().padEnd(5);
108211
+ const src = l.source === "runtime" ? "" : `[${l.source}] `;
108212
+ return ` ${src}${level} ${l.message}`;
108213
+ }
108214
+ async function streamLogs(ctx, appId, callbacks, signal) {
108215
+ try {
108216
+ for await (const line of ctx.client.logs.stream(appId, { signal })) {
108217
+ if (signal.aborted)
108218
+ return;
108219
+ await callbacks.writeLine(formatLogLine(line));
108053
108220
  }
108054
- } catch {
108055
- buildLogFetchFailed = true;
108221
+ } catch (err) {
108222
+ if (signal.aborted)
108223
+ return;
108224
+ await callbacks.writeLine(`Log stream error: ${err.message}`);
108056
108225
  }
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
- };
108226
+ }
108227
+ async function dev(ctx, input, callbacks) {
108228
+ const config2 = await loadConfig2(ctx, input.app);
108229
+ const app = await ensureApp(ctx, config2);
108230
+ await callbacks.writeLine(`App: ${app.url}`);
108231
+ await callbacks.writeLine("Watching for changes (logs streaming below)...");
108232
+ await callbacks.writeLine("");
108233
+ const logStream = { ctrl: null };
108234
+ function startLogStream(appId) {
108235
+ logStream.ctrl?.abort();
108236
+ logStream.ctrl = new AbortController;
108237
+ const linkedSignal = logStream.ctrl.signal;
108238
+ streamLogs(ctx, appId, callbacks, linkedSignal);
108239
+ }
108240
+ startLogStream(app.id);
108241
+ let busy = false;
108242
+ let queued = null;
108243
+ async function handleChange(files) {
108244
+ if (busy) {
108245
+ queued = files;
108246
+ return;
108247
+ }
108248
+ busy = true;
108249
+ try {
108250
+ const shortList = files.length <= 3 ? files.join(", ") : `${files.slice(0, 3).join(", ")} +${files.length - 3} more`;
108251
+ await callbacks.writeLine(`Change detected: ${shortList}`);
108252
+ logStream.ctrl?.abort();
108253
+ const shouldPush = await callbacks.onFilesChanged(files);
108254
+ if (shouldPush) {
108255
+ try {
108256
+ const freshConfig = await loadConfig2(ctx, input.app);
108257
+ const result = await doPush(ctx, freshConfig, callbacks);
108258
+ if (result.success) {
108259
+ startLogStream(result.appId);
108260
+ } else {
108261
+ startLogStream(app.id);
108262
+ }
108263
+ } catch (err) {
108264
+ await callbacks.writeLine(`Push error: ${err.message}`);
108265
+ startLogStream(app.id);
108266
+ }
108267
+ } else {
108268
+ startLogStream(app.id);
108269
+ }
108270
+ } finally {
108271
+ busy = false;
108272
+ if (queued) {
108273
+ const next = queued;
108274
+ queued = null;
108275
+ await handleChange(next);
108276
+ }
108277
+ }
108075
108278
  }
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"
108279
+ const devConfig = config2.dev;
108280
+ const watcher = createWatcher({
108281
+ cwd: ctx.cwd,
108282
+ extraIgnore: devConfig?.ignore,
108283
+ debounceMs: devConfig?.debounce,
108284
+ onChange(files) {
108285
+ handleChange(files);
108286
+ }
108102
108287
  });
108103
- return {
108104
- error: error48,
108105
- recovery,
108106
- summary: `${error48.title} (${app.name}, deploy ${deployment.id}).`
108107
- };
108288
+ try {
108289
+ await new Promise((resolve7) => {
108290
+ if (callbacks.signal.aborted) {
108291
+ resolve7();
108292
+ return;
108293
+ }
108294
+ callbacks.signal.addEventListener("abort", () => resolve7(), { once: true });
108295
+ });
108296
+ } finally {
108297
+ logStream.ctrl?.abort();
108298
+ watcher.close();
108299
+ }
108108
108300
  }
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
108301
+ // ../core/dist/commands/diagnose.js
108302
+ var diagnoseInputSchema = exports_external.object({
108303
+ app: exports_external.string().optional(),
108304
+ logLines: exports_external.number().int().min(0).max(200).optional(),
108305
+ recencyHours: exports_external.number().positive().max(24 * 30).optional()
108306
+ });
108307
+ // ../core/dist/commands/diff.js
108308
+ var diffInputSchema = exports_external.object({
108309
+ app: exports_external.string().optional(),
108310
+ base: exports_external.string().optional(),
108311
+ head: exports_external.string().optional(),
108312
+ patch: exports_external.boolean().optional()
108313
+ });
108314
+ async function diff(ctx, input = {}) {
108315
+ const name = input.app ?? readPercherTomlAppName(ctx.cwd);
108316
+ if (!name) {
108317
+ throw new PercherCoreError("No app specified and no percher.toml found", {
108318
+ code: "diff.no-app",
108319
+ hint: "Pass --app <name> or run from a directory with percher.toml."
108320
+ });
108321
+ }
108322
+ return ctx.client.apps.getChanges(name, {
108323
+ base: input.base,
108324
+ head: input.head,
108325
+ patch: input.patch
108116
108326
  });
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
108327
  }
108328
+ // ../core/dist/commands/doctor.js
108329
+ import { existsSync as existsSync9 } from "node:fs";
108330
+ import { join as join13 } from "node:path";
108130
108331
 
108131
108332
  // ../core/dist/commands/wait-deploy.js
108132
108333
  var waitForDeployInputSchema = exports_external.object({
@@ -109790,12 +109991,12 @@ import { basename as basename3, join as join17 } from "node:path";
109790
109991
  // ../core/dist/detect.js
109791
109992
  import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
109792
109993
  import { join as join16 } from "node:path";
109793
- function exists(dir, ...names) {
109794
- return names.some((n) => existsSync12(join16(dir, n)));
109795
- }
109796
- function readJson(path) {
109994
+ function readPackageJson(src) {
109995
+ const raw = src.text("package.json");
109996
+ if (raw === null)
109997
+ return null;
109797
109998
  try {
109798
- return JSON.parse(readFileSync9(path, "utf8"));
109999
+ return JSON.parse(raw);
109799
110000
  } catch {
109800
110001
  return null;
109801
110002
  }
@@ -109807,8 +110008,8 @@ function hasDependency(pkg, name) {
109807
110008
  const dev2 = pkg.devDependencies ?? {};
109808
110009
  return name in deps || name in dev2;
109809
110010
  }
109810
- function detectDataMode(pkg, cwd) {
109811
- if (exists(cwd, "convex.json", "convex/_generated")) {
110011
+ function detectDataMode(pkg, src) {
110012
+ if (src.has("convex.json", "convex/_generated")) {
109812
110013
  return { mode: "convex" };
109813
110014
  }
109814
110015
  if (hasDependency(pkg, "convex")) {
@@ -109819,36 +110020,35 @@ function detectDataMode(pkg, cwd) {
109819
110020
  }
109820
110021
  return { mode: "none" };
109821
110022
  }
109822
- async function detectFramework(cwd) {
109823
- const pkgPath = join16(cwd, "package.json");
109824
- const pkg = existsSync12(pkgPath) ? readJson(pkgPath) : null;
109825
- if (exists(cwd, "Dockerfile")) {
110023
+ function detectFromSource(src) {
110024
+ const pkg = readPackageJson(src);
110025
+ if (src.has("Dockerfile")) {
109826
110026
  return {
109827
110027
  runtime: "docker",
109828
110028
  framework: "docker",
109829
110029
  web: { port: 8080, health: "/health" },
109830
- data: detectDataMode(pkg, cwd)
110030
+ data: detectDataMode(pkg, src)
109831
110031
  };
109832
110032
  }
109833
- if (exists(cwd, "next.config.js", "next.config.ts", "next.config.mjs")) {
110033
+ if (src.has("next.config.js", "next.config.ts", "next.config.mjs")) {
109834
110034
  return {
109835
110035
  runtime: "node",
109836
110036
  framework: "nextjs",
109837
110037
  build: { command: "bun run build", output: ".next" },
109838
110038
  web: { port: 3000, health: "/api/health" },
109839
- data: detectDataMode(pkg, cwd)
110039
+ data: detectDataMode(pkg, src)
109840
110040
  };
109841
110041
  }
109842
- if (exists(cwd, "svelte.config.js", "svelte.config.ts")) {
110042
+ if (src.has("svelte.config.js", "svelte.config.ts")) {
109843
110043
  return {
109844
110044
  runtime: "node",
109845
110045
  framework: "sveltekit",
109846
110046
  build: { command: "bun run build", output: "build" },
109847
110047
  web: { port: 3000, health: "/health" },
109848
- data: detectDataMode(pkg, cwd)
110048
+ data: detectDataMode(pkg, src)
109849
110049
  };
109850
110050
  }
109851
- if (exists(cwd, "astro.config.mjs", "astro.config.ts", "astro.config.js")) {
110051
+ if (src.has("astro.config.mjs", "astro.config.ts", "astro.config.js")) {
109852
110052
  const ssr = hasDependency(pkg, "@astrojs/node");
109853
110053
  if (ssr) {
109854
110054
  return {
@@ -109856,7 +110056,7 @@ async function detectFramework(cwd) {
109856
110056
  framework: "astro",
109857
110057
  build: { command: "bun run build", output: "dist" },
109858
110058
  web: { port: 4321, health: "/health" },
109859
- data: detectDataMode(pkg, cwd)
110059
+ data: detectDataMode(pkg, src)
109860
110060
  };
109861
110061
  }
109862
110062
  return {
@@ -109864,25 +110064,25 @@ async function detectFramework(cwd) {
109864
110064
  framework: "astro",
109865
110065
  web: { port: STATIC_DOCKER_PORT, health: "/" },
109866
110066
  staticBuild: { outputDir: "dist", spa: false },
109867
- data: detectDataMode(pkg, cwd)
110067
+ data: detectDataMode(pkg, src)
109868
110068
  };
109869
110069
  }
109870
- if (exists(cwd, "remix.config.js", "remix.config.ts") || hasDependency(pkg, "@remix-run/node")) {
110070
+ if (src.has("remix.config.js", "remix.config.ts") || hasDependency(pkg, "@remix-run/node")) {
109871
110071
  return {
109872
110072
  runtime: "node",
109873
110073
  framework: "remix",
109874
110074
  build: { command: "bun run build", output: "build" },
109875
110075
  web: { port: 3000, health: "/health" },
109876
- data: detectDataMode(pkg, cwd)
110076
+ data: detectDataMode(pkg, src)
109877
110077
  };
109878
110078
  }
109879
- if (exists(cwd, "vite.config.js", "vite.config.ts")) {
110079
+ if (src.has("vite.config.js", "vite.config.ts")) {
109880
110080
  return {
109881
110081
  runtime: "docker",
109882
110082
  framework: "vite",
109883
110083
  web: { port: STATIC_DOCKER_PORT, health: "/" },
109884
110084
  staticBuild: { outputDir: "dist", spa: true },
109885
- data: detectDataMode(pkg, cwd)
110085
+ data: detectDataMode(pkg, src)
109886
110086
  };
109887
110087
  }
109888
110088
  if (hasDependency(pkg, "express")) {
@@ -109890,7 +110090,7 @@ async function detectFramework(cwd) {
109890
110090
  runtime: "node",
109891
110091
  framework: "express",
109892
110092
  web: { port: 3000, health: "/health" },
109893
- data: detectDataMode(pkg, cwd)
110093
+ data: detectDataMode(pkg, src)
109894
110094
  };
109895
110095
  }
109896
110096
  if (hasDependency(pkg, "fastify")) {
@@ -109898,7 +110098,7 @@ async function detectFramework(cwd) {
109898
110098
  runtime: "node",
109899
110099
  framework: "fastify",
109900
110100
  web: { port: 3000, health: "/health" },
109901
- data: detectDataMode(pkg, cwd)
110101
+ data: detectDataMode(pkg, src)
109902
110102
  };
109903
110103
  }
109904
110104
  if (hasDependency(pkg, "hono")) {
@@ -109906,14 +110106,14 @@ async function detectFramework(cwd) {
109906
110106
  runtime: "node",
109907
110107
  framework: "hono",
109908
110108
  web: { port: 3000, health: "/health" },
109909
- data: detectDataMode(pkg, cwd)
110109
+ data: detectDataMode(pkg, src)
109910
110110
  };
109911
110111
  }
109912
110112
  if (pkg) {
109913
110113
  const detected = {
109914
110114
  runtime: "node",
109915
110115
  web: { port: 3000, health: "/health" },
109916
- data: detectDataMode(pkg, cwd)
110116
+ data: detectDataMode(pkg, src)
109917
110117
  };
109918
110118
  const scripts = pkg.scripts ?? {};
109919
110119
  if (scripts.build) {
@@ -109921,65 +110121,64 @@ async function detectFramework(cwd) {
109921
110121
  }
109922
110122
  return detected;
109923
110123
  }
109924
- const hasPython = exists(cwd, "requirements.txt", "pyproject.toml");
110124
+ const hasPython = src.has("requirements.txt", "pyproject.toml");
109925
110125
  if (hasPython) {
109926
- if (existsSync12(join16(cwd, "manage.py"))) {
110126
+ if (src.has("manage.py")) {
109927
110127
  return {
109928
110128
  runtime: "python",
109929
110129
  framework: "django",
109930
110130
  web: { port: 8000, health: "/health" },
109931
- data: detectDataMode(pkg, cwd)
110131
+ data: detectDataMode(pkg, src)
109932
110132
  };
109933
110133
  }
109934
- const mainPy = existsSync12(join16(cwd, "main.py")) ? readFileSync9(join16(cwd, "main.py"), "utf8") : "";
110134
+ const mainPy = src.text("main.py") ?? "";
109935
110135
  if (/FastAPI\s*\(/.test(mainPy) || /from\s+fastapi/.test(mainPy)) {
109936
110136
  return {
109937
110137
  runtime: "python",
109938
110138
  framework: "fastapi",
109939
110139
  web: { port: 8000, health: "/health" },
109940
- data: detectDataMode(pkg, cwd)
110140
+ data: detectDataMode(pkg, src)
109941
110141
  };
109942
110142
  }
109943
- const appPy = existsSync12(join16(cwd, "app.py")) ? readFileSync9(join16(cwd, "app.py"), "utf8") : "";
110143
+ const appPy = src.text("app.py") ?? "";
109944
110144
  if (/Flask\s*\(/.test(appPy) || /from\s+flask/.test(appPy)) {
109945
110145
  return {
109946
110146
  runtime: "python",
109947
110147
  framework: "flask",
109948
110148
  web: { port: 5000, health: "/health" },
109949
- data: detectDataMode(pkg, cwd)
110149
+ data: detectDataMode(pkg, src)
109950
110150
  };
109951
110151
  }
109952
110152
  return {
109953
110153
  runtime: "python",
109954
110154
  web: { port: 8000, health: "/health" },
109955
- data: detectDataMode(pkg, cwd)
110155
+ data: detectDataMode(pkg, src)
109956
110156
  };
109957
110157
  }
109958
- if (existsSync12(join16(cwd, "index.html"))) {
110158
+ if (src.has("index.html")) {
109959
110159
  return { runtime: "static" };
109960
110160
  }
109961
110161
  return null;
109962
110162
  }
110163
+ async function detectFramework(cwd) {
110164
+ const src = {
110165
+ has: (...names) => names.some((n) => existsSync12(join16(cwd, n))),
110166
+ text: (name) => {
110167
+ try {
110168
+ return readFileSync9(join16(cwd, name), "utf8");
110169
+ } catch {
110170
+ return null;
110171
+ }
110172
+ }
110173
+ };
110174
+ return detectFromSource(src);
110175
+ }
109963
110176
  function refineBunRuntime(detected, cwd) {
109964
- if (detected.runtime === "node" && exists(cwd, "bun.lockb", "bun.lock")) {
110177
+ if (detected.runtime === "node" && (existsSync12(join16(cwd, "bun.lockb")) || existsSync12(join16(cwd, "bun.lock")))) {
109965
110178
  return { ...detected, runtime: "bun" };
109966
110179
  }
109967
110180
  return detected;
109968
110181
  }
109969
-
109970
- // ../core/dist/commands/init.js
109971
- function isSuspiciousAutoName(name) {
109972
- if (/-\d{8,}$/.test(name))
109973
- return true;
109974
- if (/\d{4}-?\d{2}-?\d{2}/.test(name))
109975
- return true;
109976
- if (name.length >= 30)
109977
- return true;
109978
- return false;
109979
- }
109980
- function detectInteractive() {
109981
- return process.stdin?.isTTY === true && process.stdout?.isTTY === true;
109982
- }
109983
110182
  function buildDataConfig(mode) {
109984
110183
  if (mode === "convex") {
109985
110184
  return {
@@ -109995,6 +110194,32 @@ function buildDataConfig(mode) {
109995
110194
  }
109996
110195
  return { mode };
109997
110196
  }
110197
+ function configFromDetection(name, detected, dataMode) {
110198
+ return {
110199
+ app: {
110200
+ name,
110201
+ runtime: detected?.runtime ?? "node",
110202
+ ...detected?.framework ? { framework: detected.framework } : {}
110203
+ },
110204
+ ...detected?.build ? { build: detected.build } : {},
110205
+ ...detected?.web ? { web: detected.web } : {},
110206
+ ...detected?.data || dataMode ? { data: buildDataConfig(dataMode ?? detected?.data?.mode ?? "none") } : {}
110207
+ };
110208
+ }
110209
+
110210
+ // ../core/dist/commands/init.js
110211
+ function isSuspiciousAutoName(name) {
110212
+ if (/-\d{8,}$/.test(name))
110213
+ return true;
110214
+ if (/\d{4}-?\d{2}-?\d{2}/.test(name))
110215
+ return true;
110216
+ if (name.length >= 30)
110217
+ return true;
110218
+ return false;
110219
+ }
110220
+ function detectInteractive() {
110221
+ return process.stdin?.isTTY === true && process.stdout?.isTTY === true;
110222
+ }
109998
110223
  var initInputSchema = exports_external.object({
109999
110224
  force: exports_external.boolean().optional(),
110000
110225
  name: exports_external.string().optional(),
@@ -110006,8 +110231,8 @@ function scanEnvVars(_cwd) {
110006
110231
  }
110007
110232
  async function init(ctx, input = {}) {
110008
110233
  const path = join17(ctx.cwd, "percher.toml");
110009
- const exists2 = existsSync13(path);
110010
- if (exists2 && !input.force) {
110234
+ const exists = existsSync13(path);
110235
+ if (exists && !input.force) {
110011
110236
  throw new PercherCoreError("percher.toml already exists", {
110012
110237
  code: "init.exists",
110013
110238
  hint: "Run with --force to overwrite."
@@ -110037,18 +110262,7 @@ async function init(ctx, input = {}) {
110037
110262
  name = suggestion;
110038
110263
  }
110039
110264
  }
110040
- const config2 = {
110041
- app: {
110042
- name,
110043
- runtime: detected?.runtime ?? "node",
110044
- ...detected?.framework ? { framework: detected.framework } : {}
110045
- },
110046
- ...detected?.build ? { build: detected.build } : {},
110047
- ...detected?.web ? { web: detected.web } : {},
110048
- ...detected?.data || input.dataMode ? {
110049
- data: buildDataConfig(input.dataMode ?? detected?.data?.mode ?? "none")
110050
- } : {}
110051
- };
110265
+ const config2 = configFromDetection(name, detected, input.dataMode);
110052
110266
  const envVars = scanEnvVars(ctx.cwd);
110053
110267
  if (Object.keys(envVars).length > 0) {
110054
110268
  config2.env = envVars;
@@ -110080,7 +110294,7 @@ async function init(ctx, input = {}) {
110080
110294
  path,
110081
110295
  config: config2,
110082
110296
  detected,
110083
- overwritten: exists2,
110297
+ overwritten: exists,
110084
110298
  scaffoldedFiles
110085
110299
  };
110086
110300
  }
@@ -110225,6 +110439,7 @@ async function logs(ctx, input = {}) {
110225
110439
  }
110226
110440
  // ../core/dist/commands/mcp.js
110227
110441
  var mcpInputSchema = exports_external.object({});
110442
+ var HOSTED_MCP_URL = "https://mcp.percher.app/mcp";
110228
110443
  function mcp(_ctx, _input = {}) {
110229
110444
  return {
110230
110445
  config: {
@@ -110235,19 +110450,43 @@ function mcp(_ctx, _input = {}) {
110235
110450
  }
110236
110451
  }
110237
110452
  },
110453
+ hosted: {
110454
+ config: {
110455
+ mcpServers: {
110456
+ percher: {
110457
+ transport: "http",
110458
+ url: HOSTED_MCP_URL
110459
+ }
110460
+ }
110461
+ },
110462
+ url: HOSTED_MCP_URL,
110463
+ auth: "oauth"
110464
+ },
110238
110465
  installLocations: [
110239
110466
  {
110240
110467
  client: "Claude Code",
110241
- path: "~/.claude.json (under mcpServers, or a per-project .mcp.json)"
110468
+ path: "~/.claude.json (under mcpServers, or a per-project .mcp.json)",
110469
+ note: "Local stdio — recommended for Claude Code (full tool set, incl. publish from local files)."
110470
+ },
110471
+ {
110472
+ client: "Claude.ai (web/desktop)",
110473
+ path: `Settings → Connectors → Add custom connector → ${HOSTED_MCP_URL}`,
110474
+ note: "Hosted — OAuth, no token to paste. Remote-safe subset (operate existing apps + deploy connected repos)."
110242
110475
  },
110243
110476
  {
110244
110477
  client: "Cursor / Windsurf",
110245
- path: "settings → MCP → add server (use the same npx command)"
110478
+ path: "settings → MCP → add server (local: the same npx command; hosted: add the URL as a connector)"
110246
110479
  },
110247
110480
  {
110248
110481
  client: "VS Code (Continue / Cline)",
110249
- path: "extension settings → MCP servers"
110482
+ path: "extension settings → MCP servers (local npx command)"
110250
110483
  }
110484
+ ],
110485
+ notes: [
110486
+ "Two ways to connect:",
110487
+ " • Local (npx): full tool set, reads your project files — required for first publish from local files. Recommended for Claude Code.",
110488
+ ` • 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.`,
110489
+ "Manage or revoke hosted connections from the Percher dashboard."
110251
110490
  ]
110252
110491
  };
110253
110492
  }
@@ -114209,66 +114448,6 @@ function withDuration(label, durationMs) {
114209
114448
  return `${label} (${seconds}s)`;
114210
114449
  }
114211
114450
 
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
114451
  // ../core/dist/publish-retry.js
114273
114452
  var NON_RETRYABLE_CODES = new Set([
114274
114453
  "WORKER_UNAVAILABLE_PERSISTENT",
@@ -115282,11 +115461,29 @@ async function executeDeploy(opts) {
115282
115461
  throw err;
115283
115462
  }
115284
115463
  }
115464
+ const uploadMs = Date.now() - uploadStart;
115465
+ return finalizeDeploy({
115466
+ ctx,
115467
+ config: config2,
115468
+ input,
115469
+ app,
115470
+ firstDeploy,
115471
+ tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
115472
+ deployment,
115473
+ t0,
115474
+ packageMs,
115475
+ uploadMs,
115476
+ missingBuildEnvKeys,
115477
+ cacheReused: cacheReusedDeployment !== null
115478
+ });
115479
+ }
115480
+ async function finalizeDeploy(opts) {
115481
+ const { ctx, config: config2, input, app, firstDeploy, tarball, t0, packageMs, uploadMs, missingBuildEnvKeys, cacheReused } = opts;
115482
+ let deployment = opts.deployment;
115285
115483
  const replacedPreview = deployment.replacedPreview === true;
115286
115484
  if (replacedPreview) {
115287
115485
  ctx.status("Replaced previous preview...");
115288
115486
  }
115289
- const uploadMs = Date.now() - uploadStart;
115290
115487
  if (input.waitForLive === false) {
115291
115488
  ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
115292
115489
  return {
@@ -115426,7 +115623,7 @@ async function executeDeploy(opts) {
115426
115623
  firstDeploy: firstDeploy || undefined,
115427
115624
  cacheHit,
115428
115625
  cacheStatus,
115429
- cacheReused: cacheReusedDeployment !== null || undefined,
115626
+ cacheReused: cacheReused || undefined,
115430
115627
  missingBuildEnvKeys: missingBuildEnvKeys.length > 0 ? missingBuildEnvKeys : undefined,
115431
115628
  traceId: deployment.id,
115432
115629
  recovery: RECOVERY_NONE,
@@ -115665,6 +115862,27 @@ function emitPreflight(ctx, config2, tarball) {
115665
115862
  ctx.status(line);
115666
115863
  }
115667
115864
  }
115865
+ // ../core/dist/commands/publish-from-repo.js
115866
+ var publishFromRepoInputSchema = exports_external.object({
115867
+ url: exports_external.string().min(1).describe("Public https git repo URL to clone and deploy (one-shot — no webhook is created)."),
115868
+ name: exports_external.string().optional().describe("App name. Overrides the repo's percher.toml [app].name; required if the repo omits it."),
115869
+ branch: exports_external.string().optional().describe("Branch to deploy. Defaults to main."),
115870
+ note: exports_external.string().optional().describe("Optional note recorded against the deploy."),
115871
+ waitForLive: exports_external.boolean().optional().describe("If false (default on the hosted connector), returns once the deploy is queued — resume with percher_wait_for_deploy. If true, block until live or failed.")
115872
+ });
115873
+ // ../core/dist/commands/publish-inline.js
115874
+ var publishInlineInputSchema = exports_external.object({
115875
+ name: exports_external.string().optional().describe("App name. Overrides [app].name when percher.toml is present; REQUIRED when no percher.toml is provided (so the auto-detected app can be named)."),
115876
+ files: exports_external.array(exports_external.object({
115877
+ path: exports_external.string().min(1).describe("Repo-relative path (no leading / and no ..)."),
115878
+ content: exports_external.string().describe("File contents — UTF-8 text, or base64-encoded bytes when encoding is 'base64'."),
115879
+ encoding: exports_external.enum(["utf8", "base64"]).optional().describe("Content encoding. Default 'utf8' (text). Use 'base64' for binary assets (images, fonts).")
115880
+ })).min(1).describe("Inline source files. Include a percher.toml ([app].name + runtime), or omit it and Percher auto-detects the runtime from the files (then `name` is required)."),
115881
+ preview: exports_external.boolean().optional().describe("Deploy as a preview (does not replace the live version)."),
115882
+ message: exports_external.string().optional().describe("Deploy note (visible in deploy history)."),
115883
+ noCache: exports_external.boolean().optional().describe("Skip the image cache and force a fresh build."),
115884
+ 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.")
115885
+ });
115668
115886
  // ../core/dist/commands/push.js
115669
115887
  import { existsSync as existsSync16 } from "node:fs";
115670
115888
  import { join as join25 } from "node:path";
@@ -117314,13 +117532,23 @@ function registerAccountCommands(cli, ctx) {
117314
117532
  return;
117315
117533
  }
117316
117534
  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
117535
  await writeLine(ctx, "");
117536
+ await writeLine(ctx, import_picocolors2.default.bold("Local (npx) — full tool set, reads your project files:"));
117319
117537
  await writeLine(ctx, JSON.stringify(result.config, null, 2));
117320
117538
  await writeLine(ctx, "");
117539
+ await writeLine(ctx, import_picocolors2.default.bold("Hosted connector — add this URL, OAuth login in the browser (no token):"));
117540
+ await writeLine(ctx, ` ${result.hosted.url}`);
117541
+ await writeLine(ctx, "");
117321
117542
  await writeLine(ctx, import_picocolors2.default.bold("Where to paste:"));
117322
117543
  for (const loc of result.installLocations) {
117323
117544
  await writeLine(ctx, ` ${loc.client.padEnd(28)} ${import_picocolors2.default.dim(loc.path)}`);
117545
+ if (loc.note) {
117546
+ await writeLine(ctx, ` ${"".padEnd(28)} ${import_picocolors2.default.dim(loc.note)}`);
117547
+ }
117548
+ }
117549
+ await writeLine(ctx, "");
117550
+ for (const note of result.notes) {
117551
+ await writeLine(ctx, import_picocolors2.default.dim(note));
117324
117552
  }
117325
117553
  await writeLine(ctx, "");
117326
117554
  await writeLine(ctx, import_picocolors2.default.dim("Once installed, your assistant gets percher_publish + 28 other tools."));
@@ -118044,7 +118272,7 @@ function registerAuthCommands(cli, ctx) {
118044
118272
  // src/commands/data.ts
118045
118273
  var import_picocolors6 = __toESM(require_picocolors(), 1);
118046
118274
  function registerDataCommands(cli, ctx, argv) {
118047
- cli.command("data [app] [...rest]", "Show app data status and collections").option("--json", "Output as JSON").option("--reveal", "(reset-superuser only) also print the plaintext password once").option("--out <path>", "(export only) Output file (default: ./<app>-pb-<timestamp>.tar.gz)").action(async (app, _rest, options) => {
118275
+ cli.command("data [app] [...rest]", "Show app data status and collections").option("--json", "Output as JSON").option("--reveal", "(reset-superuser only) also print the plaintext password once").option("--out <path>", "(export only) Output file (default: ./<app>-pb-<timestamp>.tar.gz)").option("--provider <name>", "(oauth only) OAuth provider (default: google)").option("--app <name>", "(oauth only) target app (default: read percher.toml)").option("--client-id <id>", "(oauth only) OAuth client id").option("--client-secret <secret>", "(oauth only) OAuth client secret").option("--remove", "(oauth only) remove the provider instead of setting it").action(async (app, _rest, options) => {
118048
118276
  if (app === "reset-superuser") {
118049
118277
  const idx = argv.indexOf("reset-superuser");
118050
118278
  const realApp = idx >= 0 ? firstNonFlagAfter(argv, idx) : undefined;
@@ -118065,6 +118293,37 @@ function registerDataCommands(cli, ctx, argv) {
118065
118293
  await writeLine(ctx, ` Size: ${mb} MB`);
118066
118294
  return;
118067
118295
  }
118296
+ if (app === "oauth") {
118297
+ const idx = argv.indexOf("oauth");
118298
+ const provider = (idx >= 0 ? firstNonFlagAfter(argv, idx) : undefined) ?? options.provider ?? "google";
118299
+ const result2 = await dataOauth(ctx, {
118300
+ app: options.app,
118301
+ provider,
118302
+ clientId: options.clientId,
118303
+ clientSecret: options.clientSecret,
118304
+ remove: options.remove
118305
+ });
118306
+ if (result2.action === "removed") {
118307
+ await writeLine(ctx, `${import_picocolors6.default.green("✓")} Removed ${result2.provider} sign-in.`);
118308
+ } else if (result2.action === "set") {
118309
+ await writeLine(ctx, `${import_picocolors6.default.green("✓")} ${result2.provider} sign-in enabled.`);
118310
+ await writeLine(ctx, "");
118311
+ await writeLine(ctx, import_picocolors6.default.bold("One more step — add this redirect URI in the provider console:"));
118312
+ await writeLine(ctx, ` ${result2.redirectUri}`);
118313
+ } else {
118314
+ if (!result2.providers?.length) {
118315
+ await writeLine(ctx, import_picocolors6.default.dim("No OAuth providers configured."));
118316
+ } else {
118317
+ await writeLine(ctx, import_picocolors6.default.bold("Configured providers"));
118318
+ for (const p of result2.providers) {
118319
+ await writeLine(ctx, ` ${p.provider.padEnd(12)} ${import_picocolors6.default.dim(p.clientId)}`);
118320
+ }
118321
+ }
118322
+ await writeLine(ctx, "");
118323
+ await writeLine(ctx, `Redirect URI: ${result2.redirectUri}`);
118324
+ }
118325
+ return;
118326
+ }
118068
118327
  const result = await data(ctx, { app, json: options.json });
118069
118328
  if (options.json) {
118070
118329
  await writeLine(ctx, JSON.stringify(result, null, 2));
@@ -118094,6 +118353,7 @@ function registerDataCommands(cli, ctx, argv) {
118094
118353
  const result = await resetSuperuser(ctx, { app, reveal: options.reveal });
118095
118354
  await printSuperuserResetResult(ctx, result);
118096
118355
  });
118356
+ cli.command("data oauth [provider]", "Configure capsule OAuth sign-in (e.g. `data oauth google --client-id … --client-secret …`)").action(async () => {});
118097
118357
  cli.command("data export [app]", "Download a snapshot of your app's PocketBase data as a .tar.gz").action(async () => {});
118098
118358
  cli.command("backups [app]", "List available backups for an app").action(async (app) => {
118099
118359
  const { backups, skipped } = await listBackups(ctx, { app });
@@ -119048,7 +119308,7 @@ async function renderError(stderr, error48) {
119048
119308
  // package.json
119049
119309
  var package_default = {
119050
119310
  name: "percher",
119051
- version: "0.5.10",
119311
+ version: "0.5.12",
119052
119312
  description: "CLI for the Percher hosting platform — publish apps with one command",
119053
119313
  type: "module",
119054
119314
  bin: {
@@ -119072,8 +119332,8 @@ var package_default = {
119072
119332
  typecheck: "tsc --noEmit"
119073
119333
  },
119074
119334
  dependencies: {
119075
- "@percher/client": "^0.2.10",
119076
- "@percher/core": "^0.4.5",
119335
+ "@percher/client": "^0.2.16",
119336
+ "@percher/core": "^0.4.16",
119077
119337
  "@percher/toml": "^0.1.5",
119078
119338
  cac: "^7.0.0",
119079
119339
  picocolors: "^1.1.1"