frontpage-host 0.1.9 → 0.1.10

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/AGENTS.md CHANGED
@@ -112,7 +112,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
112
112
  - Articles: `frontpage articles …` for lifecycle; bodies via sync under `content/` or `articles content get|set`. Section landing is an owned `src/pages/<section>/index.astro`.
113
113
  - Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
114
114
  - Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
115
- - Media: `frontpage media upload <file>` — images/PDF ≤25 MB, video (MP4/WebM/MOV) ≤50 MB, SVG/text ≤4 MB (sanitizing proxy). Use returned `sitePath` (`/__media/uploads/<slug>/…`) in markup — never raw blob hosts. Optional `?w=` on rasters only.
115
+ - Media: `frontpage media upload <file>` — images/PDF ≤25 MB, video (MP4/WebM/MOV) ≤50 MB, SVG/text ≤4 MB (sanitizing proxy). Human output prints `sitePath:` first (use that in markup — never raw blob hosts), then optional `url:`. Optional `?w=` on rasters only.
116
116
  - Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
117
117
 
118
118
  ## Agent prompt loop
@@ -180,7 +180,7 @@ Global machine envelope (`--json`):
180
180
  { "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3, "requestId": "…" } }
181
181
  ```
182
182
 
183
- Failures with `--json` print the envelope on **stdout** (still use the process exit code). Include `requestId` when filing support issues.
183
+ Failures with `--json` print the envelope on **stdout** (still use the process exit code). Include `requestId` when the API returned one (local validation errors omit `requestId` and `httpStatus`).
184
184
 
185
185
  ## Global flags
186
186
 
@@ -191,6 +191,7 @@ Failures with `--json` print the envelope on **stdout** (still use the process e
191
191
  | `--site <slug>` | Subdomain override (most site commands) |
192
192
  | `--yes` | Confirm destructive ops (`unpublish`, `revert`) when non-TTY |
193
193
  | `--help` / `-h` | Per-command help (alias of `frontpage help <cmd>`) |
194
+ | `--version` / `-V` | Print `frontpage-host <semver>` (also `frontpage version`) |
194
195
 
195
196
  ## Destructive ops — defaults for agents
196
197
 
package/dist/argv.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /** Remove `--name value` (or `--name=value`) from rest; return the value. */
2
+ export declare function takeFlag(rest: string[], name: string): string | undefined;
3
+ /** Remove a boolean `--name` flag from rest; return whether it was present. */
4
+ export declare function hasFlag(rest: string[], name: string): boolean;
5
+ /** True when a token looks like a flag (not a negative number). */
6
+ export declare function looksLikeFlag(token: string): boolean;
7
+ /**
8
+ * After a command has consumed its known options, fail if any flag-like
9
+ * tokens remain. Positionals (non-flags) are allowed leftovers unless the
10
+ * caller also checks them.
11
+ */
12
+ export declare function assertNoUnknownFlags(rest: string[], command: string): void;
package/dist/argv.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shared argv helpers for command parsers (CLI Agent UX R1–R3).
3
+ */
4
+ import { CliHttpError, EXIT_USAGE } from "./http/client.js";
5
+ /** Remove `--name value` (or `--name=value`) from rest; return the value. */
6
+ export function takeFlag(rest, name) {
7
+ const eq = `${name}=`;
8
+ for (let i = 0; i < rest.length; i++) {
9
+ const a = rest[i];
10
+ if (a === name) {
11
+ const v = rest[i + 1];
12
+ rest.splice(i, 2);
13
+ return v;
14
+ }
15
+ if (a.startsWith(eq)) {
16
+ rest.splice(i, 1);
17
+ return a.slice(eq.length);
18
+ }
19
+ }
20
+ return undefined;
21
+ }
22
+ /** Remove a boolean `--name` flag from rest; return whether it was present. */
23
+ export function hasFlag(rest, name) {
24
+ const i = rest.indexOf(name);
25
+ if (i === -1)
26
+ return false;
27
+ rest.splice(i, 1);
28
+ return true;
29
+ }
30
+ /** True when a token looks like a flag (not a negative number). */
31
+ export function looksLikeFlag(token) {
32
+ if (token === "-" || token === "--")
33
+ return true;
34
+ if (/^-\d/.test(token))
35
+ return false; // allow negative numbers as values
36
+ return token.startsWith("-");
37
+ }
38
+ /**
39
+ * After a command has consumed its known options, fail if any flag-like
40
+ * tokens remain. Positionals (non-flags) are allowed leftovers unless the
41
+ * caller also checks them.
42
+ */
43
+ export function assertNoUnknownFlags(rest, command) {
44
+ const unknown = rest.find(looksLikeFlag);
45
+ if (!unknown)
46
+ return;
47
+ throw new CliHttpError(`Unknown flag: ${unknown}. Run: frontpage help ${command}`, EXIT_USAGE, 0, "bad_request");
48
+ }
package/dist/bin.js CHANGED
@@ -34,6 +34,7 @@ import { runTokens } from "./commands/tokens.js";
34
34
  import { runHistory } from "./commands/history.js";
35
35
  import { runRevert } from "./commands/revert.js";
36
36
  import { runCompletion } from "./commands/completion.js";
37
+ import { runVersion } from "./commands/version.js";
37
38
  import { EXIT_USAGE, EXIT_UNEXPECTED } from "./http/client.js";
38
39
  import { parseGlobalFlags } from "./flags.js";
39
40
  import { setRuntimeFlags, isJson } from "./runtime.js";
@@ -43,6 +44,10 @@ async function main() {
43
44
  const argv = parsed.args;
44
45
  const cmd = argv[0];
45
46
  const args = argv.slice(1);
47
+ // Global version before help/command dispatch (R6).
48
+ if (parsed.wantsVersion || cmd === "version") {
49
+ return runVersion();
50
+ }
46
51
  // Global or per-command help aliases (before site resolution).
47
52
  if (!cmd || cmd === "help") {
48
53
  return runHelp(cmd === "help" ? args : []);
@@ -70,7 +70,7 @@ export function runHelp(args) {
70
70
  console.log(` ${c.name.padEnd(12)} ${c.summary}`);
71
71
  }
72
72
  console.log(`\nTopics: frontpage help ${TOPIC_NAMES.join(" | frontpage help ")}`);
73
- console.log("Global: --json --quiet --yes --site <slug> --help");
73
+ console.log("Global: --json --quiet --yes --site <slug> --help --version");
74
74
  console.log("Agents: see AGENTS.md in the npm package");
75
75
  return 0;
76
76
  }
@@ -34,12 +34,13 @@ export async function runMedia(args) {
34
34
  console.log(`Inline text (${data.filename}): ${data.sizeBytes ?? 0} bytes — not stored.`);
35
35
  return EXIT_OK;
36
36
  }
37
- if (data.url)
38
- console.log(data.url);
37
+ // Human mode: sitePath first so first-line copy/paste is markup-safe (R4).
39
38
  if (data.sitePath)
40
39
  console.log(`sitePath: ${data.sitePath}`);
41
40
  else if (data.key)
42
41
  console.log(`sitePath: /__media/${data.key}`);
42
+ if (data.url)
43
+ console.log(`url: ${data.url}`);
43
44
  return EXIT_OK;
44
45
  }
45
46
  console.error("Usage: frontpage media list|upload <file> [--site <slug>] [--json]");
@@ -1 +1,3 @@
1
+ /** Site-relative path for open; rejects schemes and traversal. */
2
+ export declare function normalizeOpenPath(raw: string): string;
1
3
  export declare function runOpen(args: string[]): Promise<number>;
@@ -1,22 +1,56 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { resolveSlug } from "../site.js";
3
- import { EXIT_OK, EXIT_USAGE } from "../http/client.js";
3
+ import { CliHttpError, EXIT_OK, EXIT_USAGE } from "../http/client.js";
4
+ import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
4
5
  import { emitErr, emitOk, emitUsage, isJson, isQuiet } from "../runtime.js";
6
+ /** Site-relative path for open; rejects schemes and traversal. */
7
+ export function normalizeOpenPath(raw) {
8
+ const trimmed = raw.trim();
9
+ if (!trimmed.startsWith("/") || trimmed.startsWith("//")) {
10
+ throw new CliHttpError("Open path must be site-relative and start with / (e.g. /updates).", EXIT_USAGE, 0, "bad_request");
11
+ }
12
+ if (/^https?:/i.test(trimmed) || trimmed.includes("..") || trimmed.includes("\\")) {
13
+ throw new CliHttpError("Open path must be a site-relative path (no http(s) URLs or ..).", EXIT_USAGE, 0, "bad_request");
14
+ }
15
+ const collapsed = trimmed.replace(/\/+/g, "/");
16
+ if (collapsed.length > 1 && collapsed.endsWith("/"))
17
+ return collapsed.slice(0, -1);
18
+ return collapsed;
19
+ }
5
20
  export async function runOpen(args) {
6
21
  try {
7
22
  const { slug, rest } = await resolveSlug(args);
8
- const prod = rest.includes("--prod") || args.includes("--prod");
9
- const url = `https://${slug}.frontpage.host/`;
23
+ const work = [...rest];
24
+ const prod = hasFlag(work, "--prod");
25
+ const pathFlag = takeFlag(work, "--path");
26
+ let path = "/";
27
+ if (pathFlag !== undefined) {
28
+ path = normalizeOpenPath(pathFlag);
29
+ }
30
+ else {
31
+ const positional = work.find((a) => a.startsWith("/") && !a.startsWith("//"));
32
+ if (positional) {
33
+ work.splice(work.indexOf(positional), 1);
34
+ path = normalizeOpenPath(positional);
35
+ }
36
+ }
37
+ assertNoUnknownFlags(work, "open");
38
+ if (work.length > 0) {
39
+ return emitUsage("open", "Usage: frontpage open [/path|--path /path] [--site <slug>] [--prod]", EXIT_USAGE);
40
+ }
41
+ const url = path === "/"
42
+ ? `https://${slug}.frontpage.host/`
43
+ : `https://${slug}.frontpage.host${path}`;
10
44
  // --prod is reserved for a future production custom-domain lookup; both
11
45
  // modes currently open the site subdomain on *.frontpage.host.
12
46
  if (prod && !isJson() && !isQuiet()) {
13
- console.error("Note: --prod is a no-op stub; opening https://<slug>.frontpage.host/");
47
+ console.error("Note: --prod is a no-op stub; opening https://<slug>.frontpage.host/…");
14
48
  }
15
49
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
16
50
  const openerArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
17
51
  spawn(opener, openerArgs, { detached: true, stdio: "ignore" }).unref();
18
52
  if (isJson()) {
19
- emitOk("open", { slug, url, prodRequested: prod, prodNoOp: true });
53
+ emitOk("open", { slug, url, path, prodRequested: prod, prodNoOp: true });
20
54
  }
21
55
  else {
22
56
  console.log(url);
@@ -25,7 +59,7 @@ export async function runOpen(args) {
25
59
  }
26
60
  catch (err) {
27
61
  if (err instanceof Error && err.message.includes("Usage")) {
28
- return emitUsage("open", "Usage: frontpage open [--site <slug>] [--prod]", EXIT_USAGE);
62
+ return emitUsage("open", "Usage: frontpage open [/path|--path /path] [--site <slug>] [--prod]", EXIT_USAGE);
29
63
  }
30
64
  return emitErr("open", err);
31
65
  }
@@ -1,21 +1,8 @@
1
1
  import { resolveSlug } from "../site.js";
2
2
  import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
3
+ import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
3
4
  import { emitErr, emitOk, isJson } from "../runtime.js";
4
- function takeFlag(rest, name) {
5
- const i = rest.indexOf(name);
6
- if (i === -1)
7
- return undefined;
8
- const v = rest[i + 1];
9
- rest.splice(i, 2);
10
- return v;
11
- }
12
- function hasFlag(rest, name) {
13
- const i = rest.indexOf(name);
14
- if (i === -1)
15
- return false;
16
- rest.splice(i, 1);
17
- return true;
18
- }
5
+ const SEARCH_USAGE = "Usage: frontpage search <pattern> [path|--path <path>] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>]";
19
6
  export async function runSearch(args) {
20
7
  try {
21
8
  const { slug, rest } = await resolveSlug(args);
@@ -26,12 +13,21 @@ export async function runSearch(args) {
26
13
  const filesOnly = hasFlag(work, "--files-only");
27
14
  const contextLines = takeFlag(work, "--context-lines");
28
15
  const maxResults = takeFlag(work, "--max-results");
29
- const pattern = work[0];
16
+ const pathFlag = takeFlag(work, "--path");
17
+ const pattern = work.shift();
30
18
  if (!pattern) {
31
- console.error("Usage: frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>]");
19
+ console.error(SEARCH_USAGE);
20
+ return EXIT_USAGE;
21
+ }
22
+ let pathFilter = pathFlag;
23
+ if (pathFilter === undefined && work[0] && !work[0].startsWith("-")) {
24
+ pathFilter = work.shift();
25
+ }
26
+ assertNoUnknownFlags(work, "search");
27
+ if (work.length > 0) {
28
+ console.error(`Unexpected argument: ${work[0]}\n${SEARCH_USAGE}`);
32
29
  return EXIT_USAGE;
33
30
  }
34
- const pathFilter = work[1] && !work[1].startsWith("-") ? work[1] : undefined;
35
31
  const q = new URLSearchParams({ pattern });
36
32
  if (pathFilter)
37
33
  q.set("path", pathFilter);
@@ -1,27 +1,39 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
3
- function takeFlag(rest, name) {
4
- const i = rest.indexOf(name);
5
- if (i === -1)
6
- return undefined;
7
- const v = rest[i + 1];
8
- rest.splice(i, 2);
9
- return v;
2
+ import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
3
+ import { assertNoUnknownFlags, takeFlag } from "../argv.js";
4
+ import { emitErr, isJson } from "../runtime.js";
5
+ function resolveSeoPath(work) {
6
+ const fromFlag = takeFlag(work, "--path");
7
+ if (fromFlag !== undefined) {
8
+ if (!fromFlag.startsWith("/") || fromFlag.startsWith("//")) {
9
+ throw new CliHttpError("SEO path must start with / (e.g. / or /about).", EXIT_USAGE, 0, "bad_request");
10
+ }
11
+ return fromFlag;
12
+ }
13
+ const positional = work.find((a) => a.startsWith("/") && !a.startsWith("//"));
14
+ if (positional) {
15
+ const i = work.indexOf(positional);
16
+ work.splice(i, 1);
17
+ return positional;
18
+ }
19
+ return "/";
10
20
  }
11
21
  export async function runSeo(args) {
12
22
  try {
13
23
  const { slug, rest } = await resolveSlug(args);
14
24
  const sub = rest[0];
15
- if (!sub || sub === "get") {
16
- const pathArgs = rest[0] === "get" ? rest.slice(1) : rest;
17
- const path = pathArgs[0] ?? "/";
25
+ // `seo`, `seo get`, `seo get /path`, `seo get --path /path`, `seo --path /path`
26
+ if (!sub || sub === "get" || sub === "--path" || (sub.startsWith("/") && !sub.startsWith("//"))) {
27
+ const work = sub === "get" ? rest.slice(1) : [...rest];
28
+ const path = resolveSeoPath(work);
29
+ assertNoUnknownFlags(work, "seo");
18
30
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/seo?path=${encodeURIComponent(path)}`);
19
31
  console.log(JSON.stringify(data, null, 2));
20
32
  return EXIT_OK;
21
33
  }
22
34
  if (sub === "set") {
23
35
  const work = rest.slice(1);
24
- const path = takeFlag(work, "--path") ?? work.find((a) => a.startsWith("/")) ?? "/";
36
+ const path = resolveSeoPath(work);
25
37
  const body = { path };
26
38
  for (const field of [
27
39
  "title",
@@ -39,6 +51,7 @@ export async function runSeo(args) {
39
51
  if (noindex !== undefined) {
40
52
  body.noindex = noindex === "true" || noindex === "1";
41
53
  }
54
+ assertNoUnknownFlags(work, "seo");
42
55
  if (Object.keys(body).length <= 1) {
43
56
  console.error("Usage: frontpage seo set --path / [--title …] [--description …] [--ogImage …] [--noindex true|false]");
44
57
  return EXIT_USAGE;
@@ -47,10 +60,12 @@ export async function runSeo(args) {
47
60
  console.log(JSON.stringify(data, null, 2));
48
61
  return EXIT_OK;
49
62
  }
50
- console.error("Usage: frontpage seo get|set … [--site <slug>]");
63
+ console.error("Usage: frontpage seo get [/path|--path /path] | seo set --path / [--title …] [--site <slug>]");
51
64
  return EXIT_USAGE;
52
65
  }
53
66
  catch (err) {
67
+ if (isJson())
68
+ return emitErr("seo", err);
54
69
  console.error(err instanceof Error ? err.message : String(err));
55
70
  return exitCodeForError(err);
56
71
  }
@@ -0,0 +1 @@
1
+ export declare function runVersion(): number;
@@ -0,0 +1,13 @@
1
+ import { EXIT_OK } from "../http/client.js";
2
+ import { emitOk, isJson } from "../runtime.js";
3
+ import { packageIdentity, versionLine } from "../version.js";
4
+ export function runVersion() {
5
+ const identity = packageIdentity();
6
+ if (isJson()) {
7
+ emitOk("version", identity);
8
+ }
9
+ else {
10
+ console.log(versionLine());
11
+ }
12
+ return EXIT_OK;
13
+ }
package/dist/flags.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Strip global flags from argv. Safe to call before command dispatch.
3
- * Recognizes --json, --quiet, --yes, and help aliases anywhere in the list.
3
+ * Recognizes --json, --quiet, --yes, --version/-V, and help aliases anywhere in the list.
4
4
  */
5
5
  export type GlobalFlags = {
6
6
  json: boolean;
@@ -8,6 +8,8 @@ export type GlobalFlags = {
8
8
  yes: boolean;
9
9
  /** True when argv asked for help (--help / -h), not the `help` command itself. */
10
10
  wantsHelp: boolean;
11
+ /** True when argv asked for version (--version / -V). */
12
+ wantsVersion: boolean;
11
13
  args: string[];
12
14
  };
13
15
  export declare function parseGlobalFlags(argv: string[]): GlobalFlags;
package/dist/flags.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Strip global flags from argv. Safe to call before command dispatch.
3
- * Recognizes --json, --quiet, --yes, and help aliases anywhere in the list.
3
+ * Recognizes --json, --quiet, --yes, --version/-V, and help aliases anywhere in the list.
4
4
  */
5
5
  export function parseGlobalFlags(argv) {
6
6
  const args = [];
@@ -8,6 +8,7 @@ export function parseGlobalFlags(argv) {
8
8
  let quiet = false;
9
9
  let yes = false;
10
10
  let wantsHelp = false;
11
+ let wantsVersion = false;
11
12
  for (const a of argv) {
12
13
  if (a === "--json") {
13
14
  json = true;
@@ -25,7 +26,11 @@ export function parseGlobalFlags(argv) {
25
26
  wantsHelp = true;
26
27
  continue;
27
28
  }
29
+ if (a === "--version" || a === "-V") {
30
+ wantsVersion = true;
31
+ continue;
32
+ }
28
33
  args.push(a);
29
34
  }
30
- return { json, quiet, yes, wantsHelp, args };
35
+ return { json, quiet, yes, wantsHelp, wantsVersion, args };
31
36
  }
@@ -124,7 +124,8 @@ export async function uploadLocalMedia(args) {
124
124
  headBytes: bytes.subarray(0, 4096),
125
125
  });
126
126
  if (decision.transport === "reject") {
127
- throw new CliHttpError(decision.message, EXIT_USAGE, 400, "bad_request");
127
+ // Local validation only — omit httpStatus (status 0) and never invent requestId (R10).
128
+ throw new CliHttpError(decision.message, EXIT_USAGE, 0, "bad_request");
128
129
  }
129
130
  if (decision.transport === "client") {
130
131
  return uploadViaClient({
package/dist/registry.js CHANGED
@@ -26,6 +26,13 @@ export const COMMANDS = [
26
26
  "frontpage help --json",
27
27
  ],
28
28
  },
29
+ {
30
+ name: "version",
31
+ summary: "Print frontpage-host package version",
32
+ usage: "frontpage version | frontpage --version | frontpage -V",
33
+ examples: ["frontpage --version", "frontpage version --json"],
34
+ flags: ["Also available as global --version / -V"],
35
+ },
29
36
  {
30
37
  name: "login",
31
38
  summary: "Save an API key to the OS keychain",
@@ -133,15 +140,21 @@ export const COMMANDS = [
133
140
  {
134
141
  name: "search",
135
142
  summary: "Search remote text files",
136
- usage: "frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>] [--json]",
143
+ usage: "frontpage search <pattern> [path|--path <path>] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>] [--json]",
137
144
  topics: ["sync"],
145
+ examples: [
146
+ "frontpage search Subscribe src/pages",
147
+ "frontpage search Subscribe --path src/pages",
148
+ ],
138
149
  flags: [
150
+ "--path <prefix> (alias for positional path filter; flag wins if both present)",
139
151
  "--regex",
140
152
  "--case-sensitive",
141
153
  "--whole-word",
142
154
  "--files-only",
143
155
  "--context-lines <n>",
144
156
  "--max-results <n>",
157
+ "Unknown leftover flags fail closed (exit 2)",
145
158
  ],
146
159
  },
147
160
  {
@@ -184,9 +197,13 @@ export const COMMANDS = [
184
197
  {
185
198
  name: "open",
186
199
  summary: "Open the site subdomain in a browser",
187
- usage: "frontpage open [--site <slug>] [--prod]",
200
+ usage: "frontpage open [/path|--path /path] [--site <slug>] [--prod]",
188
201
  topics: ["sync"],
189
- flags: ["--prod is a no-op stub today — always opens https://<slug>.frontpage.host/"],
202
+ examples: ["frontpage open", "frontpage open /updates", "frontpage open --path /pricing"],
203
+ flags: [
204
+ "Optional site-relative path appended to https://<slug>.frontpage.host",
205
+ "--prod is a no-op stub today — still opens the *.frontpage.host subdomain",
206
+ ],
190
207
  },
191
208
  {
192
209
  name: "prompt",
@@ -326,7 +343,8 @@ export const COMMANDS = [
326
343
  ],
327
344
  flags: [
328
345
  "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",
346
+ "Human success: sitePath first (markup-safe), then url: (blob URL for debugging)",
347
+ "Use sitePath (/__media/uploads/<slug>/…) in markup — never raw blob hosts in pages",
330
348
  "SVG stays on the sanitizing proxy path (≤4 MB)",
331
349
  ],
332
350
  seeAlso: ["prompt", "help content", "help site-building"],
@@ -341,8 +359,14 @@ export const COMMANDS = [
341
359
  {
342
360
  name: "seo",
343
361
  summary: "Get or set page SEO",
344
- usage: "frontpage seo get|set --path / [--title …] [--description …] [--site <slug>]",
362
+ usage: "frontpage seo get [/path|--path /path] | seo set --path / [--title …] [--description …] [--site <slug>]",
345
363
  topics: ["content"],
364
+ examples: [
365
+ "frontpage seo get /",
366
+ "frontpage seo get --path /updates",
367
+ "frontpage seo set --path / --title \"Home\"",
368
+ ],
369
+ flags: ["get accepts positional path or --path (flag wins); unknown leftover flags fail closed"],
346
370
  },
347
371
  {
348
372
  name: "articles",
@@ -447,8 +471,8 @@ export const TOPICS = {
447
471
  frontpage push [--message …] [--wait] # uploads allowlisted files; CAS + active-run guard
448
472
  frontpage ls [prefix]
449
473
  frontpage cat <path>
450
- frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only]
451
- frontpage open # opens https://<slug>.frontpage.host/ (--prod is a no-op stub)
474
+ frontpage search <pattern> [path|--path <path>] [--regex] [--case-sensitive] [--whole-word] [--files-only]
475
+ frontpage open [/path] # opens https://<slug>.frontpage.host[/path] (--prod is a no-op stub)
452
476
 
453
477
  Local state: .frontpage/state.json { slug, baseCommitSha }
454
478
  Pull BEFORE EDITING when tip may have moved (UI agent / another checkout) — check tipDrift.
@@ -520,14 +544,14 @@ export const TOPICS = {
520
544
  frontpage extensions
521
545
 
522
546
  --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).
547
+ API errors include requestId in --json envelopes when the server returned one; local validation errors omit requestId and httpStatus.
524
548
  Mid-task continuation is automatic — there is no runs continue.
525
549
  Do not file-sync while an agent run is active (see help sync for push --wait).
526
550
  Default after work: review https://<subdomain>.frontpage.host; do not publish.`,
527
551
  content: `Content
528
- frontpage media list|upload <file> # images/PDF ≤25 MB, video ≤50 MB; sitePath for markup
552
+ frontpage media list|upload <file> # images/PDF ≤25 MB, video ≤50 MB; human prints sitePath first
529
553
  frontpage inbox [leads|subscribers|orders|…] [--mark-read] [--csv]
530
- frontpage seo get|set --path /
554
+ frontpage seo get [/path|--path /path] | seo set --path /
531
555
  frontpage articles list|content|create-section|create-article|set-draft|…
532
556
  frontpage fonts [list|search] [--slot …] [--query …]
533
557
  frontpage tokens set --group … --key … --value …
@@ -573,7 +597,8 @@ export const TOPICS = {
573
597
  { "ok": true, "command": "…", "data": {…} }
574
598
  { "ok": false, "command": "…", "error": { "message", "code?", "httpStatus?", "exitCode", "requestId?" }, "data"? }
575
599
  Failures with --json still use the process exit code; envelope is on stdout.
576
- Prefer requestId from error envelopes when filing support issues.`,
600
+ requestId is present only when the API returned one — local CLI validation (e.g. media size/type reject) omits requestId and httpStatus.
601
+ Prefer requestId from API error envelopes when filing support issues.`,
577
602
  };
578
603
  export function findCommand(name) {
579
604
  return COMMANDS.find((c) => c.name === name);
@@ -0,0 +1,7 @@
1
+ /** Read package identity from frontpage-host package.json (works from dist/). */
2
+ export declare function packageIdentity(): {
3
+ name: string;
4
+ version: string;
5
+ };
6
+ /** Human one-liner: `frontpage-host 0.1.10`. */
7
+ export declare function versionLine(): string;
@@ -0,0 +1,15 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ /** Read package identity from frontpage-host package.json (works from dist/). */
4
+ export function packageIdentity() {
5
+ const pkg = require("../package.json");
6
+ return {
7
+ name: pkg.name ?? "frontpage-host",
8
+ version: pkg.version ?? "0.0.0",
9
+ };
10
+ }
11
+ /** Human one-liner: `frontpage-host 0.1.10`. */
12
+ export function versionLine() {
13
+ const { name, version } = packageIdentity();
14
+ return `${name} ${version}`;
15
+ }
package/llms.txt CHANGED
@@ -10,9 +10,11 @@
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
- - Media: frontpage media upload <file> (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB); use sitePath /__media/uploads/<slug>/… in markup
13
+ - Media: frontpage media upload <file> (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB); human prints sitePath first — use /__media/uploads/<slug>/… in markup
14
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
15
+ - Version: frontpage --version | -V | version
16
+ - SEO get: frontpage seo get [/path|--path /path]; search path filter: [path|--path <path>]; open [/path|--path /path]
17
+ - Errors: --json API envelopes include requestId when the server returned one; local validation omits requestId/httpStatus; runs wait failed/canceled → ok:false + run.error (exit 2); deploy wait fail-closed; push --wait
16
18
  - Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
17
19
  - Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
18
20
  - Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step for agents
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {