percher 0.5.13 → 0.5.16

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.
@@ -104733,7 +104733,15 @@ var percherConfigSchema = exports_external.object({
104733
104733
  crons: cronsSchema.optional(),
104734
104734
  addons: addonsSchema.optional(),
104735
104735
  checks: exports_external.record(exports_external.string(), checkSchema).optional()
104736
- }).strict();
104736
+ }).strict().superRefine((cfg, ctx) => {
104737
+ if (cfg.app?.runtime === "static" && cfg.build?.command) {
104738
+ ctx.addIssue({
104739
+ code: exports_external.ZodIssueCode.custom,
104740
+ path: ["build", "command"],
104741
+ message: 'runtime = "static" serves pre-built files and does not run a build command. Remove [build].command (commit the built output), or switch to runtime = "docker" for a build step.'
104742
+ });
104743
+ }
104744
+ });
104737
104745
 
104738
104746
  // ../toml/dist/parse.js
104739
104747
  function parse5(source) {
@@ -107267,6 +107275,7 @@ function recoveryFromErrorClass(errorClass, failure = {}) {
107267
107275
  switch (errorClass) {
107268
107276
  case "missing_env":
107269
107277
  return recoveryEnv({
107278
+ app: failure.appName,
107270
107279
  keys: failure.missingEnvVars ?? [],
107271
107280
  reasonCode: "missing_env"
107272
107281
  });
@@ -107647,7 +107656,7 @@ var patterns = [
107647
107656
  result: {
107648
107657
  title: "Health check failed",
107649
107658
  explanation: "Your app started but did not respond to the health check endpoint in time.",
107650
- suggestion: "Make sure your app listens on process.env.PORT and returns HTTP 200 on the health path. Then publish again.",
107659
+ suggestion: "Make sure your app listens on the injected port (process.env.PORT) and that your configured health path ([web].health in percher.toml, default '/') returns HTTP 200. Run `percher doctor --app <name>` for deeper analysis, then publish again.",
107651
107660
  errorClass: "health_check_timeout",
107652
107661
  phase: "health_check",
107653
107662
  cause: "timeout",
@@ -107961,6 +107970,7 @@ var DEFAULT_IGNORE = [
107961
107970
  "*.log",
107962
107971
  ".DS_Store"
107963
107972
  ];
107973
+ var ALWAYS_IGNORE_SECRETS = [".env", ".env.*"];
107964
107974
  function* walk(dir, baseDir, ig) {
107965
107975
  const entries = readdirSync2(dir);
107966
107976
  for (const name of entries) {
@@ -108026,14 +108036,25 @@ function buildTarball(fileBuffers) {
108026
108036
  const nodeStream = pack.pipe(gzip);
108027
108037
  const webStream = Readable.toWeb(nodeStream);
108028
108038
  const topFiles = [...fileBuffers].sort((a, b) => b.size - a.size).slice(0, 5).map((f) => ({ path: f.path, size: f.size }));
108039
+ const entrySizes = new Map;
108040
+ for (const f of fileBuffers) {
108041
+ const slash = f.path.indexOf("/");
108042
+ const key = slash === -1 ? f.path : `${f.path.slice(0, slash)}/`;
108043
+ entrySizes.set(key, (entrySizes.get(key) ?? 0) + f.size);
108044
+ }
108045
+ const topEntries = [...entrySizes.entries()].map(([path, size]) => ({ path, size, isDir: path.endsWith("/") })).sort((a, b) => b.size - a.size).slice(0, 5);
108029
108046
  return {
108030
108047
  stream: webStream,
108031
108048
  bytes: totalBytes,
108032
108049
  fileCount: fileBuffers.length,
108033
108050
  topFiles,
108051
+ topEntries,
108034
108052
  contentHash
108035
108053
  };
108036
108054
  }
108055
+ function formatBiggestEntries(entries) {
108056
+ return entries.map((e) => `${e.path} ${(e.size / 1024 / 1024).toFixed(1)} MB`).join(", ");
108057
+ }
108037
108058
  async function createTarball(options) {
108038
108059
  if (isFilesOptions(options)) {
108039
108060
  return createTarballFromFiles(options.files, options.config);
@@ -108044,8 +108065,13 @@ async function createTarball(options) {
108044
108065
  if (existsSync6(gitignorePath)) {
108045
108066
  ig.add(readFileSync6(gitignorePath, "utf8"));
108046
108067
  }
108068
+ const percherignorePath = join10(options.cwd, ".percherignore");
108069
+ if (existsSync6(percherignorePath)) {
108070
+ ig.add(readFileSync6(percherignorePath, "utf8"));
108071
+ }
108047
108072
  if (options.extraIgnore)
108048
108073
  ig.add(options.extraIgnore);
108074
+ ig.add(ALWAYS_IGNORE_SECRETS);
108049
108075
  const fileBuffers = [];
108050
108076
  for (const rel of walk(options.cwd, options.cwd, ig)) {
108051
108077
  const data2 = readFileSync6(join10(options.cwd, rel));
@@ -115318,10 +115344,10 @@ ${bullets}`,
115318
115344
  error: {
115319
115345
  title: "Repository too large",
115320
115346
  explanation: `The project is ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB, which exceeds the 500 MB limit.`,
115321
- suggestion: `Add large files/folders to .gitignore. Biggest: ${tarball.topFiles.map((f) => f.path).join(", ")}`
115347
+ suggestion: `Add large files/folders to .gitignore or .percherignore. Biggest: ${formatBiggestEntries(tarball.topEntries)}`
115322
115348
  },
115323
115349
  recovery: recoveryAsk({
115324
- prompt: `Repository is ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB, which exceeds the 500 MB upload limit. Add large files/folders to .gitignore (biggest: ${tarball.topFiles.map((f) => f.path).join(", ")}) and try again.`,
115350
+ prompt: `Repository is ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB, which exceeds the 500 MB upload limit. Add large files/folders to .gitignore or .percherignore (biggest: ${formatBiggestEntries(tarball.topEntries)}) and try again.`,
115325
115351
  reasonCode: "unknown"
115326
115352
  }),
115327
115353
  summary: `Repository too large (${(tarball.bytes / 1024 / 1024).toFixed(1)} MB exceeds 500 MB limit).`,
@@ -115330,7 +115356,7 @@ ${bullets}`,
115330
115356
  };
115331
115357
  }
115332
115358
  if (!input.force && (tarball.fileCount > SUSPICIOUS_FILES || tarball.bytes > SUSPICIOUS_BYTES)) {
115333
- ctx.status(`Warning: ${tarball.fileCount} files, ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB — auto-continuing (MCP mode).`);
115359
+ ctx.status(`Warning: ${tarball.fileCount} files, ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB — auto-continuing (MCP mode). Biggest: ${formatBiggestEntries(tarball.topEntries)}. Trim the upload by adding large items to .gitignore or .percherignore.`);
115334
115360
  }
115335
115361
  emitPreflight(ctx, config2, {
115336
115362
  fileCount: tarball.fileCount,
@@ -115991,17 +116017,17 @@ async function push(ctx, input = {}, internals = {}) {
115991
116017
  if (tarball.bytes > MAX_TARBALL_BYTES) {
115992
116018
  throw new PercherCoreError(`Repository is larger than 500 MB (current: ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB)`, {
115993
116019
  code: "push.too-large",
115994
- hint: `Biggest files: ${tarball.topFiles.map((f) => f.path).join(", ")}`,
116020
+ hint: `Biggest: ${formatBiggestEntries(tarball.topEntries)}`,
115995
116021
  recovery: {
115996
116022
  ...recoveryAsk({
115997
- prompt: `Repository is ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB, exceeding the 500 MB limit. Add large files/folders to .gitignore (biggest: ${tarball.topFiles.map((f) => f.path).join(", ")}) and re-run.`,
116023
+ prompt: `Repository is ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB, exceeding the 500 MB limit. Add large files/folders to .gitignore or .percherignore (biggest: ${formatBiggestEntries(tarball.topEntries)}) and re-run.`,
115998
116024
  reasonCode: "unknown"
115999
116025
  })
116000
116026
  }
116001
116027
  });
116002
116028
  }
116003
116029
  if (!input.force && (tarball.fileCount > SUSPICIOUS_FILES2 || tarball.bytes > SUSPICIOUS_BYTES2)) {
116004
- ctx.status(`This looks suspicious: ${tarball.fileCount} files, ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB. Are node_modules / dist excluded?`);
116030
+ ctx.status(`This looks suspicious: ${tarball.fileCount} files, ${(tarball.bytes / 1024 / 1024).toFixed(1)} MB. Biggest: ${formatBiggestEntries(tarball.topEntries)}. Are node_modules / dist excluded?`);
116005
116031
  const answer = await ctx.prompt("Continue anyway? [y/N]", { default: "n" });
116006
116032
  if (!/^y(es)?$/i.test(answer.trim())) {
116007
116033
  throw new PercherCoreError("Push aborted — looked suspicious", {
@@ -117303,6 +117329,9 @@ var BLOCKED_APP_NAMES = new Set([
117303
117329
  "caddy",
117304
117330
  "git",
117305
117331
  "percher",
117332
+ "mcp",
117333
+ "preview",
117334
+ "metrics",
117306
117335
  "login",
117307
117336
  "signin",
117308
117337
  "signup",
@@ -119331,7 +119360,7 @@ async function renderError(stderr, error48) {
119331
119360
  // package.json
119332
119361
  var package_default = {
119333
119362
  name: "percher",
119334
- version: "0.5.13",
119363
+ version: "0.5.16",
119335
119364
  description: "CLI for the Percher hosting platform — publish apps with one command",
119336
119365
  type: "module",
119337
119366
  bin: {
@@ -119356,7 +119385,7 @@ var package_default = {
119356
119385
  },
119357
119386
  dependencies: {
119358
119387
  "@percher/client": "^0.2.16",
119359
- "@percher/core": "^0.4.17",
119388
+ "@percher/core": "^0.4.18",
119360
119389
  "@percher/toml": "^0.1.5",
119361
119390
  cac: "^7.0.0",
119362
119391
  picocolors: "^1.1.1"
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "percher",
3
- "version": "0.5.13",
3
+ "version": "0.5.16",
4
4
  "description": "CLI for the Percher hosting platform \u2014 publish apps with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@percher/client": "^0.2.16",
28
- "@percher/core": "^0.4.17",
28
+ "@percher/core": "^0.4.18",
29
29
  "@percher/toml": "^0.1.5",
30
30
  "cac": "^7.0.0",
31
31
  "picocolors": "^1.1.1"
@@ -30,3 +30,62 @@ export async function complete(
30
30
  const data = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> };
31
31
  return data.choices?.[0]?.message?.content ?? "";
32
32
  }
33
+
34
+ /**
35
+ * Streaming variant of {@link complete} — calls `onToken` with each text delta
36
+ * as it arrives and resolves with the full text. Same auth + billing as
37
+ * `complete` (keyless, billed to the app owner). Use it for a typewriter UI:
38
+ *
39
+ * const full = await streamComplete("Write a haiku", (t) => { output.value += t; });
40
+ */
41
+ export async function streamComplete(
42
+ prompt: string,
43
+ onToken: (delta: string) => void,
44
+ opts?: { model?: string; maxTokens?: number },
45
+ ): Promise<string> {
46
+ if (!AI_URL) throw new Error("Managed inference isn't configured for this app.");
47
+ if (!pb.authStore.isValid) throw new Error("Sign in to use AI.");
48
+ const res = await fetch(AI_URL, {
49
+ method: "POST",
50
+ headers: { "content-type": "application/json", authorization: pb.authStore.token },
51
+ body: JSON.stringify({
52
+ model: opts?.model,
53
+ max_tokens: opts?.maxTokens ?? 512,
54
+ messages: [{ role: "user", content: prompt }],
55
+ stream: true,
56
+ }),
57
+ });
58
+ if (!res.ok || !res.body) throw new Error(`AI request failed (${res.status})`);
59
+
60
+ const reader = res.body.getReader();
61
+ const decoder = new TextDecoder();
62
+ let buffer = "";
63
+ let full = "";
64
+ for (;;) {
65
+ const { done, value } = await reader.read();
66
+ if (done) break;
67
+ buffer += decoder.decode(value, { stream: true });
68
+ // SSE events are separated by a blank line; parse each complete one.
69
+ let sep = buffer.indexOf("\n\n");
70
+ while (sep !== -1) {
71
+ const event = buffer.slice(0, sep);
72
+ buffer = buffer.slice(sep + 2);
73
+ sep = buffer.indexOf("\n\n");
74
+ const line = event.startsWith("data:") ? event.slice(5).trim() : "";
75
+ if (!line || line === "[DONE]") continue;
76
+ try {
77
+ const chunk = JSON.parse(line) as {
78
+ choices?: Array<{ delta?: { content?: string } }>;
79
+ };
80
+ const delta = chunk.choices?.[0]?.delta?.content ?? "";
81
+ if (delta) {
82
+ full += delta;
83
+ onToken(delta);
84
+ }
85
+ } catch {
86
+ /* ignore keep-alive / non-JSON lines */
87
+ }
88
+ }
89
+ }
90
+ return full;
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "percher",
3
- "version": "0.5.13",
3
+ "version": "0.5.16",
4
4
  "description": "CLI for the Percher hosting platform \u2014 publish apps with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@percher/client": "^0.2.16",
28
- "@percher/core": "^0.4.17",
28
+ "@percher/core": "^0.4.18",
29
29
  "@percher/toml": "^0.1.5",
30
30
  "cac": "^7.0.0",
31
31
  "picocolors": "^1.1.1"