frontpage-host 0.1.7 → 0.1.9

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.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Shared local-file upload used by `frontpage media upload` and `prompt --attach`.
3
+ */
4
+ import { randomUUID } from "node:crypto";
5
+ import { readFile } from "node:fs/promises";
6
+ import { basename } from "node:path";
7
+ import { put } from "@vercel/blob/client";
8
+ import { apiBase, CliHttpError, EXIT_AUTH, EXIT_UNEXPECTED, EXIT_USAGE, getBearerAuthHeader, httpErrorFromV1, v1PostMultipart, } from "../http/client.js";
9
+ import { contentTypeForExt, sitePathFromKey } from "./limits.js";
10
+ import { selectMediaTransport } from "./select-transport.js";
11
+ async function requestClientToken(args) {
12
+ const auth = await getBearerAuthHeader();
13
+ const url = `${apiBase()}/api/v1/sites/${encodeURIComponent(args.slug)}/media/client`;
14
+ let res;
15
+ try {
16
+ res = await fetch(url, {
17
+ method: "POST",
18
+ headers: {
19
+ ...auth,
20
+ Accept: "application/json",
21
+ "Content-Type": "application/json",
22
+ },
23
+ body: JSON.stringify({
24
+ type: "blob.generate-client-token",
25
+ payload: {
26
+ pathname: args.pathname,
27
+ clientPayload: JSON.stringify({ declaredSize: args.declaredSize }),
28
+ multipart: args.multipart,
29
+ },
30
+ }),
31
+ });
32
+ }
33
+ catch (err) {
34
+ throw new CliHttpError(`Network error: ${err instanceof Error ? err.message : String(err)}`, EXIT_UNEXPECTED, 0);
35
+ }
36
+ const text = await res.text();
37
+ let parsed = {};
38
+ try {
39
+ parsed = text ? JSON.parse(text) : {};
40
+ }
41
+ catch {
42
+ parsed = {};
43
+ }
44
+ if (!res.ok) {
45
+ const msg = typeof parsed.error === "string"
46
+ ? parsed.error
47
+ : typeof parsed.error === "object" && parsed.error?.message
48
+ ? parsed.error.message
49
+ : `Upload authorization failed (${res.status})`;
50
+ // Auth / rate-limit use v1 envelopes; policy denials use { error: string }.
51
+ if (typeof parsed.error === "object" && parsed.error) {
52
+ throw httpErrorFromV1(res.status, parsed, msg);
53
+ }
54
+ if (res.status === 401 || res.status === 403) {
55
+ throw new CliHttpError(msg, EXIT_AUTH, res.status);
56
+ }
57
+ if (res.status === 402 || res.status === 413 || res.status === 429 || res.status === 400) {
58
+ throw new CliHttpError(msg, EXIT_USAGE, res.status);
59
+ }
60
+ throw new CliHttpError(msg, EXIT_UNEXPECTED, res.status);
61
+ }
62
+ if (typeof parsed.clientToken !== "string" || !parsed.clientToken) {
63
+ throw new CliHttpError("Upload authorization failed (no client token).", EXIT_UNEXPECTED, res.status);
64
+ }
65
+ return parsed.clientToken;
66
+ }
67
+ async function uploadViaClient(args) {
68
+ const pathname = `uploads/${args.slug}/${randomUUID()}.${args.ext}`;
69
+ const multipart = args.bytes.length > 8 * 1024 * 1024;
70
+ const clientToken = await requestClientToken({
71
+ slug: args.slug,
72
+ pathname,
73
+ declaredSize: args.bytes.length,
74
+ multipart,
75
+ });
76
+ try {
77
+ const blob = await put(pathname, args.bytes, {
78
+ access: "public",
79
+ token: clientToken,
80
+ contentType: args.contentType,
81
+ multipart,
82
+ });
83
+ const key = blob.pathname.replace(/^\//, "");
84
+ return {
85
+ kind: args.kind,
86
+ url: blob.url,
87
+ key,
88
+ sitePath: sitePathFromKey(key),
89
+ filename: args.filename,
90
+ sizeBytes: args.bytes.length,
91
+ contentType: args.contentType || contentTypeForExt(args.ext),
92
+ transport: "client",
93
+ };
94
+ }
95
+ catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ throw new CliHttpError(`Upload failed: ${msg}`, EXIT_UNEXPECTED, 500);
98
+ }
99
+ }
100
+ async function uploadViaProxy(args) {
101
+ const form = new FormData();
102
+ form.append("file", new Blob([Uint8Array.from(args.bytes)]), args.filename);
103
+ const data = await v1PostMultipart(`/api/v1/sites/${encodeURIComponent(args.slug)}/media`, form);
104
+ const key = data.key?.replace(/^\//, "");
105
+ return {
106
+ kind: data.kind,
107
+ url: data.url,
108
+ key,
109
+ sitePath: data.sitePath ?? (key ? sitePathFromKey(key) : undefined),
110
+ filename: data.filename ?? args.filename,
111
+ sizeBytes: data.sizeBytes ?? args.bytes.length,
112
+ contentType: data.contentType,
113
+ content: data.content,
114
+ transport: "proxy",
115
+ };
116
+ }
117
+ /** Upload a local file using the Phase E transport matrix. */
118
+ export async function uploadLocalMedia(args) {
119
+ const bytes = await readFile(args.filePath);
120
+ const filename = basename(args.filePath);
121
+ const decision = selectMediaTransport({
122
+ filename,
123
+ sizeBytes: bytes.length,
124
+ headBytes: bytes.subarray(0, 4096),
125
+ });
126
+ if (decision.transport === "reject") {
127
+ throw new CliHttpError(decision.message, EXIT_USAGE, 400, "bad_request");
128
+ }
129
+ if (decision.transport === "client") {
130
+ return uploadViaClient({
131
+ slug: args.slug,
132
+ bytes,
133
+ filename,
134
+ ext: decision.ext,
135
+ contentType: decision.contentType,
136
+ kind: decision.kind,
137
+ });
138
+ }
139
+ return uploadViaProxy({ slug: args.slug, bytes, filename });
140
+ }
package/dist/registry.js CHANGED
@@ -88,10 +88,17 @@ export const COMMANDS = [
88
88
  {
89
89
  name: "push",
90
90
  summary: "Upload local allowlisted files (CAS + active-run guard)",
91
- usage: "frontpage push [--site <slug>] [--message …] [--json]",
91
+ usage: "frontpage push [--site <slug>] [--message …] [--wait] [--json]",
92
92
  topics: ["sync"],
93
- examples: ['frontpage push --message "Update hero copy"'],
94
- flags: ["--site <slug>", "--message <text>"],
93
+ examples: [
94
+ 'frontpage push --message "Update hero copy"',
95
+ 'frontpage push --message "…" --wait',
96
+ ],
97
+ flags: [
98
+ "--site <slug>",
99
+ "--message <text>",
100
+ "--wait: after push, wait until no agent run is queued/running/deploying (same as deploy wait; not a Vercel oracle)",
101
+ ],
95
102
  dangers: [
96
103
  "Requires prior pull (baseCommitSha)",
97
104
  "Refused while an agent run is active (exit 3)",
@@ -197,7 +204,7 @@ export const COMMANDS = [
197
204
  "--path must start with /",
198
205
  "--attach: local file (uploads via media), /__media/… site path, or https Blob URL; repeatable",
199
206
  "--asset-url: https image URL (legacy); repeatable",
200
- "Local attach uses the same caps as frontpage media upload (images/PDF/text; no CLI video yet)",
207
+ "Local attach uses frontpage media upload (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB proxy)",
201
208
  "stdout is bare runId (or JSON with --json)",
202
209
  ],
203
210
  seeAlso: ["runs", "deploy", "media"],
@@ -215,7 +222,10 @@ export const COMMANDS = [
215
222
  "frontpage runs retry <runId>",
216
223
  "frontpage runs get <runId>",
217
224
  ],
218
- flags: ["There is no runs continue — mid-task continuation is automatic"],
225
+ flags: [
226
+ "There is no runs continue — mid-task continuation is automatic",
227
+ "runs wait: failed/canceled → exit 2; --json uses ok:false and includes run.error",
228
+ ],
219
229
  seeAlso: ["prompt", "deploy"],
220
230
  },
221
231
  {
@@ -248,6 +258,7 @@ export const COMMANDS = [
248
258
  flags: [
249
259
  "Optional build wait only — staging is https://<subdomain>.frontpage.host",
250
260
  "Does NOT promote to production; use publish only when the user asks",
261
+ "Fail-closed: exit 2 when the waited run (or latest, if already idle) is failed/canceled",
251
262
  ],
252
263
  },
253
264
  {
@@ -305,9 +316,20 @@ export const COMMANDS = [
305
316
  },
306
317
  {
307
318
  name: "media",
308
- summary: "List or upload site media assets",
309
- usage: "frontpage media list|upload <file> [--site <slug>]",
319
+ summary: "List or upload site media (images/PDF ≤25 MB, video ≤50 MB)",
320
+ usage: "frontpage media list|upload <file> [--site <slug>] [--json]",
310
321
  topics: ["content"],
322
+ examples: [
323
+ "frontpage media list",
324
+ "frontpage media upload ./hero.webp",
325
+ "frontpage media upload ./clip.mp4",
326
+ ],
327
+ flags: [
328
+ "Auto-selects proxy (≤4 MB) vs direct-to-Blob for large rasters/PDFs and all video",
329
+ "Prints sitePath (/__media/uploads/<slug>/…) for markup — never raw blob hosts in pages",
330
+ "SVG stays on the sanitizing proxy path (≤4 MB)",
331
+ ],
332
+ seeAlso: ["prompt", "help content", "help site-building"],
311
333
  },
312
334
  {
313
335
  name: "inbox",
@@ -422,7 +444,7 @@ export const TOPICS = {
422
444
  frontpage status # tipDrift when remote moved since last pull
423
445
  frontpage pull # downloads src/, public/, content/ into cwd
424
446
  frontpage diff # local vs remote via git blobSha (--json → dirty + tipDrift)
425
- frontpage push [--message …] # uploads allowlisted files; CAS + active-run guard
447
+ frontpage push [--message …] [--wait] # uploads allowlisted files; CAS + active-run guard
426
448
  frontpage ls [prefix]
427
449
  frontpage cat <path>
428
450
  frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only]
@@ -431,6 +453,7 @@ export const TOPICS = {
431
453
  Local state: .frontpage/state.json { slug, baseCommitSha }
432
454
  Pull BEFORE EDITING when tip may have moved (UI agent / another checkout) — check tipDrift.
433
455
  Pull before push so CAS has baseCommitSha. Do not push while the editor agent is building.
456
+ --wait after push blocks until no agent run is queued/running/deploying (same as deploy wait).
434
457
  The UI/canvas agent does not use this CLI; only CLI checkouts need pull.
435
458
  Pushing module markup may auto-activate modules (same as the agent).
436
459
  tokens.ts may be auto-repaired at commit if it would break the build.
@@ -473,7 +496,7 @@ export const TOPICS = {
473
496
  Modules & articles (short):
474
497
  - Forms: <form data-fp-form="slug"> with named inputs; no action/onsubmit/honeypot.
475
498
  - Articles: frontpage articles …; migrate folders via frontpage prompt (don't invent mass delete+write).
476
- - Media in markup: /__media/uploads/<slug>/… from frontpage media (not raw blob URLs).
499
+ - Media: frontpage media upload (images/PDF ≤25 MB, video ≤50 MB). Use sitePath /__media/uploads/<slug>/… in markup (not raw blob URLs). Optional ?w= on rasters only.
477
500
 
478
501
  Full guide: https://frontpage.host/developers/site-building
479
502
  See also: frontpage help sync | content | AGENTS.md`,
@@ -482,10 +505,10 @@ export const TOPICS = {
482
505
  frontpage prompt "Use this logo" --attach ./logo.png
483
506
  frontpage prompt "…" --attach /__media/uploads/<slug>/… --asset-url https://…
484
507
  frontpage runs # latest + active
485
- frontpage runs wait [runId]
508
+ frontpage runs wait [runId] # failed/canceled → exit 2; JSON ok:false + run.error
486
509
  frontpage runs cancel <runId>
487
510
  frontpage runs retry <runId>
488
- frontpage deploy wait # optional: wait for build; NOT publish
511
+ frontpage deploy wait # optional build wait; fail-closed on failed/canceled; NOT publish
489
512
  frontpage publish # ONLY if the user asked — production promote
490
513
  frontpage unpublish --yes # admin only; never unless asked; --yes when non-TTY
491
514
  frontpage usage
@@ -496,12 +519,13 @@ export const TOPICS = {
496
519
  frontpage modules request-setup <key> --reason "…"
497
520
  frontpage extensions
498
521
 
499
- --attach uploads local files via media (images/PDF/text) or accepts /__media/… / https URLs.
522
+ --attach uploads via media (images/PDF/video/text) or accepts /__media/… / https URLs.
523
+ API errors include requestId in --json error envelopes (and stderr when not --json).
500
524
  Mid-task continuation is automatic — there is no runs continue.
501
- Do not push while an agent run is active.
525
+ Do not file-sync while an agent run is active (see help sync for push --wait).
502
526
  Default after work: review https://<subdomain>.frontpage.host; do not publish.`,
503
527
  content: `Content
504
- frontpage media list|upload <file>
528
+ frontpage media list|upload <file> # images/PDF ≤25 MB, video ≤50 MB; sitePath for markup
505
529
  frontpage inbox [leads|subscribers|orders|…] [--mark-read] [--csv]
506
530
  frontpage seo get|set --path /
507
531
  frontpage articles list|content|create-section|create-article|set-draft|…
@@ -547,8 +571,9 @@ export const TOPICS = {
547
571
 
548
572
  Global --json envelope:
549
573
  { "ok": true, "command": "…", "data": {…} }
550
- { "ok": false, "command": "…", "error": { "message", "code?", "httpStatus?", "exitCode" } }
551
- Failures with --json still use the process exit code; envelope is on stdout.`,
574
+ { "ok": false, "command": "…", "error": { "message", "code?", "httpStatus?", "exitCode", "requestId?" }, "data"? }
575
+ Failures with --json still use the process exit code; envelope is on stdout.
576
+ Prefer requestId from error envelopes when filing support issues.`,
552
577
  };
553
578
  export function findCommand(name) {
554
579
  return COMMANDS.find((c) => c.name === name);
package/dist/runtime.d.ts CHANGED
@@ -13,8 +13,18 @@ export type JsonErrorBody = {
13
13
  code?: string;
14
14
  httpStatus?: number;
15
15
  exitCode: number;
16
+ requestId?: string;
16
17
  };
17
18
  export declare function emitOk(command: string, data: unknown): void;
19
+ /** Structured failure with optional data payload (e.g. failed run wait). */
20
+ export declare function emitFail(command: string, args: {
21
+ message: string;
22
+ exitCode: number;
23
+ code?: string;
24
+ requestId?: string;
25
+ httpStatus?: number;
26
+ data?: unknown;
27
+ }): number;
18
28
  export declare function emitErr(command: string, err: unknown): number;
19
29
  export declare function emitUsage(command: string, message: string, exitCode?: number): number;
20
30
  /** Progress / spinner lines — suppressed when --quiet or --json. */
package/dist/runtime.js CHANGED
@@ -31,23 +31,43 @@ export function requireYesOrTty(action) {
31
31
  export function emitOk(command, data) {
32
32
  console.log(JSON.stringify({ ok: true, command, data }));
33
33
  }
34
+ /** Structured failure with optional data payload (e.g. failed run wait). */
35
+ export function emitFail(command, args) {
36
+ const body = {
37
+ message: args.message,
38
+ exitCode: args.exitCode,
39
+ };
40
+ if (args.code)
41
+ body.code = args.code;
42
+ if (args.httpStatus)
43
+ body.httpStatus = args.httpStatus;
44
+ if (args.requestId)
45
+ body.requestId = args.requestId;
46
+ if (jsonMode) {
47
+ const out = { ok: false, command, error: body };
48
+ if (args.data !== undefined)
49
+ out.data = args.data;
50
+ console.log(JSON.stringify(out));
51
+ }
52
+ else {
53
+ const suffix = args.requestId ? ` (requestId: ${args.requestId})` : "";
54
+ console.error(`${args.message}${suffix}`);
55
+ }
56
+ return args.exitCode;
57
+ }
34
58
  export function emitErr(command, err) {
35
59
  const exitCode = exitCodeForError(err);
36
60
  const message = err instanceof Error ? err.message : String(err);
37
- const body = { message, exitCode };
38
61
  if (err instanceof CliHttpError) {
39
- if (err.code)
40
- body.code = err.code;
41
- if (err.status)
42
- body.httpStatus = err.status;
62
+ return emitFail(command, {
63
+ message,
64
+ exitCode,
65
+ code: err.code,
66
+ httpStatus: err.status || undefined,
67
+ requestId: err.requestId,
68
+ });
43
69
  }
44
- if (jsonMode) {
45
- console.log(JSON.stringify({ ok: false, command, error: body }));
46
- }
47
- else {
48
- console.error(message);
49
- }
50
- return exitCode;
70
+ return emitFail(command, { message, exitCode });
51
71
  }
52
72
  export function emitUsage(command, message, exitCode = 2) {
53
73
  if (jsonMode) {
package/llms.txt CHANGED
@@ -10,12 +10,14 @@
10
10
  - Search flags: --regex --case-sensitive --whole-word --files-only --context-lines --max-results
11
11
  - Modules setup: frontpage modules request-setup <key> --reason "…" (owner status bar; CLI cannot show it)
12
12
  - Analytics ops: frontpage analytics summary|query|funnel|timeseries|compare|sql (bare analytics = dashboard)
13
- - Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (media upload for locals; images/PDF/text)
13
+ - Media: frontpage media upload <file> (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB); use sitePath /__media/uploads/<slug>/… in markup
14
+ - Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (same media caps as media upload)
15
+ - Errors: --json error envelopes include requestId; runs wait failed/canceled → ok:false + run.error (exit 2); deploy wait fail-closed; push --wait
14
16
  - Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
15
17
  - Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
16
18
  - Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step for agents
17
19
  - Production: custom domain after frontpage publish — NEVER publish/unpublish unless the user explicitly asks
18
- - Sync: allowlist src/ public/ content/; platform overlay + secret-like paths rejected; pull before edit if tip may have moved (status/diff tipDrift); pull → edit → diff (blobSha) → push (CAS); no push during active agent run; UI agent does not use CLI pull; review on subdomain
20
+ - Sync: allowlist src/ public/ content/; platform overlay + secret-like paths rejected; pull before edit if tip may have moved (status/diff tipDrift); pull → edit → diff (blobSha) → push [--wait] (CAS); no push during active agent run; UI agent does not use CLI pull; review on subdomain
19
21
  - Machine: global --json / --quiet; exit 0/2/3/4/5 (see help exit-codes)
20
- - deploy wait = optional build wait only; it is NOT go-live / publish
22
+ - deploy wait = optional build wait only (fail-closed on failed/canceled); it is NOT go-live / publish
21
23
  - No runs continue, domains, billing, members, MCP, or git remotes in v1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,9 @@
34
34
  "cli",
35
35
  "landing-page"
36
36
  ],
37
+ "dependencies": {
38
+ "@vercel/blob": "^2.4.0"
39
+ },
37
40
  "devDependencies": {
38
41
  "@types/node": "^22.13.10",
39
42
  "tsx": "^4.19.3",