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.
- package/dist/bin/percher.js +39 -10
- package/dist/package.json +2 -2
- package/dist/templates/capsule/src/lib/ai.ts +59 -0
- package/package.json +2 -2
package/dist/bin/percher.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|