frontpage-host 0.1.0 → 0.1.1

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 ADDED
@@ -0,0 +1,159 @@
1
+ # Frontpage CLI — agent operating manual
2
+
3
+ This file ships inside the `frontpage-host` npm package. Prefer it over scraping the product website.
4
+
5
+ ## Install and invoke
6
+
7
+ | Item | Value |
8
+ |------|--------|
9
+ | npm package | `frontpage-host` |
10
+ | Binary | `frontpage` |
11
+ | Wrong package | Do **not** install npm `frontpage` (unrelated) |
12
+
13
+ ```bash
14
+ npm i -g frontpage-host
15
+ frontpage help --json # introspect commands + topics
16
+ ```
17
+
18
+ Node.js ≥ 20.
19
+
20
+ ## Non-interactive auth
21
+
22
+ ```bash
23
+ frontpage login "$FRONTPAGE_API_KEY"
24
+ # or export FRONTPAGE_API_KEY=fp_live_… (wins over keychain)
25
+ frontpage whoami --json
26
+ frontpage doctor --json
27
+ ```
28
+
29
+ - Keys are minted only in the Frontpage UI (profile → Developers). Prefix `fp_live_`.
30
+ - Keys act as the minting user on **every** site they can access. Treat like passwords.
31
+ - `PUBLIC_CLI_ENABLED` is server-side; the CLI cannot turn it on. Bare “not found” on `/api/v1/me` usually means the flag is off (exit 5).
32
+
33
+ ## Site IDs
34
+
35
+ - Always use the **subdomain** (`arbiter`, `asimov-press`).
36
+ - Never pass custom domains, FQDNs, or URLs to `use` / `--site`.
37
+ - Folder state: `.frontpage/state.json` → `{ slug, baseCommitSha }`.
38
+
39
+ ```bash
40
+ frontpage sites --json
41
+ frontpage use <subdomain>
42
+ # or: frontpage <cmd> --site <subdomain>
43
+ ```
44
+
45
+ ## File sync recipe
46
+
47
+ Allowlisted roots only: `src/`, `public/`, `content/`. Other paths are never pulled/pushed.
48
+
49
+ ```bash
50
+ frontpage pull
51
+ # edit local allowlisted files
52
+ frontpage diff --json # data.dirty === true when local ≠ remote
53
+ frontpage push --message "…"
54
+ # on exit 3 (CAS / active run): frontpage status --json → pull or wait → retry push
55
+ frontpage deploy wait
56
+ # optional production promote:
57
+ frontpage publish --site <subdomain>
58
+ ```
59
+
60
+ Rules:
61
+
62
+ 1. **Pull before push** so `baseCommitSha` is set (CAS).
63
+ 2. **Do not push** while an agent run is active (`status` / `runs`).
64
+ 3. Push commits to the site branch and triggers staging deploy. **`publish` promotes staging → production.**
65
+ 4. Pushing module markup may auto-activate modules (same as the editor agent).
66
+ 5. Binaries use base64; do not UTF-8 round-trip them.
67
+
68
+ ## Agent prompt loop
69
+
70
+ ```bash
71
+ RUN=$(frontpage prompt "make the hero shorter" --json | …) # or bare runId on stdout without --json
72
+ frontpage runs wait "$RUN"
73
+ # Mid-task continuation is automatic — there is NO `runs continue` in v1.
74
+ frontpage deploy wait
75
+ ```
76
+
77
+ ## Content / JSON-native commands
78
+
79
+ These already return JSON bodies (wrap further with global `--json` where supported):
80
+
81
+ - `usage`, `analytics`, `seo`, `articles`, `settings`, `inbox` (default JSON)
82
+ - Mutate: `modules enable|disable|archive|config`, `extensions …`
83
+
84
+ Click-edit (exact single occurrence + deploy), **not** file push:
85
+
86
+ ```bash
87
+ frontpage edit --path / --old "exact old" --new "exact new"
88
+ ```
89
+
90
+ ## History / revert
91
+
92
+ ```bash
93
+ frontpage history --json
94
+ frontpage revert <40-char-sha> --yes # required --yes when non-TTY
95
+ ```
96
+
97
+ - `S` = staging tip, `P` = production tip.
98
+ - Revert creates a **new** commit whose tree matches the target (linear restore).
99
+ - Full 40-char SHA only. Refused while an agent run is active.
100
+
101
+ ## Exit codes — what to do next
102
+
103
+ | Code | Meaning | Next step |
104
+ |------|---------|-----------|
105
+ | 0 | Success | Continue |
106
+ | 2 | Usage / budget / rate-limit / unavailable / failed run wait | Fix args, wait, or check `usage` |
107
+ | 3 | Conflict (CAS tip moved, active run, cancel-while-deploying) | `status` / `pull` / `runs wait`; retry |
108
+ | 4 | Authn/authz **or** site not found (404 often hides existence) | Check key, membership, subdomain |
109
+ | 5 | Unexpected server/network / API disabled | Retry; verify `PUBLIC_CLI_ENABLED` / `FRONTPAGE_API_BASE` |
110
+
111
+ Global machine envelope (`--json`):
112
+
113
+ ```json
114
+ { "ok": true, "command": "status", "data": { } }
115
+ { "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3 } }
116
+ ```
117
+
118
+ Failures with `--json` print the envelope on **stdout** (still use the process exit code).
119
+
120
+ ## Global flags
121
+
122
+ | Flag | Meaning |
123
+ |------|---------|
124
+ | `--json` | Stable JSON envelope for supported commands |
125
+ | `--quiet` | Suppress progress spinners / chatter |
126
+ | `--site <slug>` | Subdomain override (most site commands) |
127
+ | `--yes` | Confirm destructive ops (`unpublish`, `revert`) when non-TTY |
128
+ | `--help` / `-h` | Per-command help (alias of `frontpage help <cmd>`) |
129
+
130
+ ## Destructive ops — defaults for agents
131
+
132
+ **Never** run unless the user explicitly asked:
133
+
134
+ - `frontpage unpublish` (takes production offline) — requires `--yes` when non-TTY
135
+ - `frontpage revert <sha>` — requires `--yes` when non-TTY
136
+ - `frontpage inbox … --mark-read` (side-effecting)
137
+ - Broad `frontpage edit` / settings mutations without a clear request
138
+
139
+ `publish` promotes staging to production — confirm intent before running.
140
+
141
+ ## Out of v1 (do not invent)
142
+
143
+ `domains*`, `billing*`, `members*`, `suggestions*`, `sites delete|restore`, `runs continue`, MCP, git remotes / clone URLs.
144
+
145
+ ## Isolation (hard fence)
146
+
147
+ - API never returns another site’s branch, GitHub tokens, or clone URLs.
148
+ - Site targeting is subdomain → `site/<slug>` only.
149
+ - Non-member site calls → 404 (exit 4) — do not treat as “wrong password” only.
150
+ - See `frontpage help isolation`.
151
+
152
+ ## Canonical introspection
153
+
154
+ ```bash
155
+ frontpage help
156
+ frontpage help --json
157
+ frontpage help sync
158
+ frontpage doctor --json
159
+ ```
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # frontpage-host
2
+
3
+ Frontpage CLI — manage your site from the terminal.
4
+
5
+ **npm package:** `frontpage-host`
6
+ **Binary:** `frontpage`
7
+ **Do not install** the unrelated npm package named `frontpage`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i -g frontpage-host
13
+ # or one-shot:
14
+ npx frontpage-host help
15
+ ```
16
+
17
+ Requires Node.js ≥ 20.
18
+
19
+ ## 60-second path
20
+
21
+ 1. In the Frontpage editor: profile menu → **Developers** → create an API key (`fp_live_…`).
22
+ 2. `frontpage login` (or `frontpage login "$FRONTPAGE_API_KEY"`).
23
+ 3. `frontpage sites` — note the **subdomain** (not a custom domain).
24
+ 4. `mkdir my-site && cd my-site && frontpage use <subdomain>`
25
+ 5. `frontpage pull` → edit files under `src/`, `public/`, or `content/`
26
+ 6. `frontpage push --message "describe the change"`
27
+ 7. `frontpage deploy wait` (staging). Optional: `frontpage publish` (production).
28
+
29
+ ## Environment
30
+
31
+ | Variable | Purpose |
32
+ |----------|---------|
33
+ | `FRONTPAGE_API_KEY` | Bearer key (preferred in CI; overrides keychain) |
34
+ | `FRONTPAGE_API_BASE` | API origin (default `https://app.frontpage.host`) |
35
+
36
+ ## Help (source of truth in the binary)
37
+
38
+ ```bash
39
+ frontpage help # catalog + quickstart
40
+ frontpage help quickstart
41
+ frontpage help auth
42
+ frontpage help sync
43
+ frontpage help agent
44
+ frontpage help content
45
+ frontpage help history
46
+ frontpage help isolation
47
+ frontpage help exit-codes
48
+ frontpage help push # per-command details
49
+ frontpage push --help # same
50
+ ```
51
+
52
+ Machine-readable: `frontpage help --json` and global `--json` / `--quiet` on commands.
53
+
54
+ ## Agents
55
+
56
+ See [AGENTS.md](./AGENTS.md) and [llms.txt](./llms.txt) in this package.
package/dist/bin.js CHANGED
@@ -33,13 +33,21 @@ import { runHistory } from "./commands/history.js";
33
33
  import { runRevert } from "./commands/revert.js";
34
34
  import { runCompletion } from "./commands/completion.js";
35
35
  import { EXIT_USAGE, EXIT_UNEXPECTED } from "./http/client.js";
36
+ import { parseGlobalFlags } from "./flags.js";
37
+ import { setRuntimeFlags, isJson } from "./runtime.js";
36
38
  async function main() {
37
- const argv = process.argv.slice(2);
39
+ const parsed = parseGlobalFlags(process.argv.slice(2));
40
+ setRuntimeFlags({ json: parsed.json, quiet: parsed.quiet, yes: parsed.yes });
41
+ const argv = parsed.args;
38
42
  const cmd = argv[0];
39
43
  const args = argv.slice(1);
40
- if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
44
+ // Global or per-command help aliases (before site resolution).
45
+ if (!cmd || cmd === "help") {
41
46
  return runHelp(cmd === "help" ? args : []);
42
47
  }
48
+ if (parsed.wantsHelp) {
49
+ return runHelp([cmd, ...args]);
50
+ }
43
51
  switch (cmd) {
44
52
  case "login":
45
53
  return runLogin(args);
@@ -109,8 +117,17 @@ async function main() {
109
117
  case "completion":
110
118
  return runCompletion(args);
111
119
  default:
112
- console.error(`Unknown command: ${cmd}`);
113
- console.error("Run: frontpage help");
120
+ if (isJson()) {
121
+ console.log(JSON.stringify({
122
+ ok: false,
123
+ command: cmd,
124
+ error: { message: `Unknown command: ${cmd}`, exitCode: EXIT_USAGE },
125
+ }));
126
+ }
127
+ else {
128
+ console.error(`Unknown command: ${cmd}`);
129
+ console.error("Run: frontpage help");
130
+ }
114
131
  return EXIT_USAGE;
115
132
  }
116
133
  }
@@ -119,6 +136,16 @@ main()
119
136
  process.exitCode = code;
120
137
  })
121
138
  .catch((err) => {
122
- console.error(err instanceof Error ? err.message : String(err));
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ if (isJson()) {
141
+ console.log(JSON.stringify({
142
+ ok: false,
143
+ command: "frontpage",
144
+ error: { message, exitCode: EXIT_UNEXPECTED },
145
+ }));
146
+ }
147
+ else {
148
+ console.error(message);
149
+ }
123
150
  process.exitCode = EXIT_UNEXPECTED;
124
151
  });
@@ -1,12 +1,13 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
2
+ import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
3
+ import { emitErr, emitOk, emitUsage, progress } from "../runtime.js";
4
+ import { isJson } from "../runtime.js";
3
5
  /** `frontpage deploy wait` — wait until no run is in deploying/queued/running. */
4
6
  export async function runDeploy(args) {
5
7
  try {
6
8
  const sub = args[0];
7
9
  if (sub !== "wait") {
8
- console.error("Usage: frontpage deploy wait [--site <slug>]");
9
- return EXIT_USAGE;
10
+ return emitUsage("deploy", "Usage: frontpage deploy wait [--site <slug>]", EXIT_USAGE);
10
11
  }
11
12
  const { slug } = await resolveSlug(args.slice(1));
12
13
  const started = Date.now();
@@ -15,18 +16,26 @@ export async function runDeploy(args) {
15
16
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`);
16
17
  const busy = (data.active ?? []).filter((r) => ["queued", "running", "deploying"].includes(r.status));
17
18
  if (busy.length === 0) {
18
- console.log(data.run ? `${data.run.id} ${data.run.status}` : "No active deploy.");
19
+ if (isJson()) {
20
+ emitOk("deploy", {
21
+ slug,
22
+ waited: true,
23
+ run: data.run,
24
+ status: data.run?.status ?? "idle",
25
+ });
26
+ }
27
+ else {
28
+ console.log(data.run ? `${data.run.id} ${data.run.status}` : "No active deploy.");
29
+ }
19
30
  return EXIT_OK;
20
31
  }
21
32
  const head = busy.find((r) => r.status === "deploying") ?? busy[0];
22
- process.stderr.write(`\r${head.status} ${head.statusMessage ?? ""} `);
33
+ progress(`\r${head.status} ${head.statusMessage ?? ""} `);
23
34
  await new Promise((r) => setTimeout(r, 2000));
24
35
  }
25
- console.error("Timed out waiting for deploy.");
26
- return EXIT_USAGE;
36
+ return emitUsage("deploy", "Timed out waiting for deploy.", EXIT_USAGE);
27
37
  }
28
38
  catch (err) {
29
- console.error(err instanceof Error ? err.message : String(err));
30
- return exitCodeForError(err);
39
+ return emitErr("deploy", err);
31
40
  }
32
41
  }
@@ -2,7 +2,8 @@ import { readFile, readdir, stat } from "node:fs/promises";
2
2
  import { createHash } from "node:crypto";
3
3
  import { join, relative, sep } from "node:path";
4
4
  import { resolveSlug } from "../site.js";
5
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
5
+ import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
6
+ import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
6
7
  const ROOTS = ["src", "public", "content"];
7
8
  async function walkLocal(cwd) {
8
9
  const map = new Map();
@@ -47,47 +48,56 @@ export async function runDiff(args) {
47
48
  const local = await walkLocal(cwd);
48
49
  const listing = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files`);
49
50
  const remotePaths = new Set(listing.files.map((f) => f.path));
50
- let changed = 0;
51
- let onlyLocal = 0;
52
- let onlyRemote = 0;
51
+ const added = [];
52
+ const modified = [];
53
+ const deleted = [];
53
54
  for (const path of [...local.keys()].sort()) {
54
55
  if (!remotePaths.has(path)) {
55
- console.log(`A ${path}`);
56
- onlyLocal++;
56
+ added.push(path);
57
57
  continue;
58
58
  }
59
59
  const encPath = path.split("/").map(encodeURIComponent).join("/");
60
60
  const remote = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files/${encPath}`);
61
61
  const rh = remoteHash(remote.file);
62
62
  const lh = local.get(path);
63
- if (rh !== lh) {
64
- console.log(`M ${path}`);
65
- changed++;
66
- }
63
+ if (rh !== lh)
64
+ modified.push(path);
67
65
  }
68
66
  for (const f of listing.files) {
69
- if (!local.has(f.path)) {
70
- console.log(`D ${f.path}`);
71
- onlyRemote++;
72
- }
67
+ if (!local.has(f.path))
68
+ deleted.push(f.path);
73
69
  }
74
- const total = changed + onlyLocal + onlyRemote;
75
- if (total === 0) {
70
+ const dirty = added.length + modified.length + deleted.length > 0;
71
+ void (await stat(cwd).catch(() => null));
72
+ if (isJson()) {
73
+ emitOk("diff", {
74
+ slug,
75
+ baseCommitSha: listing.baseCommitSha,
76
+ dirty,
77
+ added,
78
+ modified,
79
+ deleted,
80
+ });
81
+ return EXIT_OK;
82
+ }
83
+ for (const path of added)
84
+ console.log(`A ${path}`);
85
+ for (const path of modified)
86
+ console.log(`M ${path}`);
87
+ for (const path of deleted)
88
+ console.log(`D ${path}`);
89
+ if (!dirty) {
76
90
  console.log("No differences.");
77
91
  }
78
92
  else {
79
- console.error(`# ${total} difference(s) · tip ${(listing.baseCommitSha ?? "").slice(0, 7) || "?"} · A=local-only M=modified D=remote-only`);
93
+ console.error(`# ${added.length + modified.length + deleted.length} difference(s) · tip ${(listing.baseCommitSha ?? "").slice(0, 7) || "?"} · A=local-only M=modified D=remote-only`);
80
94
  }
81
- // Ensure cwd has the tree (optional UX check)
82
- void (await stat(cwd).catch(() => null));
83
95
  return EXIT_OK;
84
96
  }
85
97
  catch (err) {
86
98
  if (err instanceof Error && /No site selected/.test(err.message)) {
87
- console.error(err.message);
88
- return EXIT_USAGE;
99
+ return emitUsage("diff", err.message, EXIT_USAGE);
89
100
  }
90
- console.error(err instanceof Error ? err.message : String(err));
91
- return exitCodeForError(err);
101
+ return emitErr("diff", err);
92
102
  }
93
103
  }
@@ -1,26 +1,51 @@
1
1
  import { getApiKey, maskKey } from "../auth/keychain.js";
2
- import { apiBase, EXIT_OK, EXIT_AUTH, EXIT_UNEXPECTED, v1Get } from "../http/client.js";
2
+ import { apiBase, EXIT_OK, EXIT_AUTH, exitCodeForError, v1Get } from "../http/client.js";
3
3
  import { readState } from "../state/frontpage-state.js";
4
+ import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
4
5
  export async function runDoctor() {
5
- console.log(`API base: ${apiBase()}`);
6
6
  const key = await getApiKey();
7
+ const state = await readState();
8
+ const base = apiBase();
7
9
  if (!key) {
10
+ if (isJson()) {
11
+ return emitUsage("doctor", "No API key. Run: frontpage login", EXIT_AUTH);
12
+ }
8
13
  console.error("No API key. Run: frontpage login");
9
14
  return EXIT_AUTH;
10
15
  }
11
- console.log(`API key: ${maskKey(key)} (never prints the full secret)`);
12
- const state = await readState();
13
- console.log(`Folder site: ${state.slug ?? "(not set — run frontpage use <subdomain>)"}`);
14
16
  try {
15
17
  const me = await v1Get("/api/v1/me");
16
- console.log(`whoami: ok (${me.email ?? me.userId})`);
17
18
  const sites = await v1Get("/api/v1/sites");
18
- console.log(`sites: ok (${sites.sites.length} site(s))`);
19
- console.log("doctor: all checks passed");
19
+ const data = {
20
+ apiBase: base,
21
+ apiKeyMasked: maskKey(key),
22
+ folderSite: state.slug ?? null,
23
+ baseCommitSha: state.baseCommitSha ?? null,
24
+ whoami: { email: me.email ?? null, userId: me.userId },
25
+ siteCount: sites.sites.length,
26
+ ok: true,
27
+ };
28
+ if (isJson()) {
29
+ emitOk("doctor", data);
30
+ }
31
+ else {
32
+ console.log(`API base: ${base}`);
33
+ console.log(`API key: ${maskKey(key)} (never prints the full secret)`);
34
+ console.log(`Folder site: ${state.slug ?? "(not set — run frontpage use <subdomain>)"}`);
35
+ console.log(`whoami: ok (${me.email ?? me.userId})`);
36
+ console.log(`sites: ok (${sites.sites.length} site(s))`);
37
+ console.log("doctor: all checks passed");
38
+ }
20
39
  return EXIT_OK;
21
40
  }
22
41
  catch (err) {
42
+ if (isJson()) {
43
+ return emitErr("doctor", err);
44
+ }
45
+ console.log(`API base: ${base}`);
46
+ console.log(`API key: ${maskKey(key)} (never prints the full secret)`);
47
+ console.log(`Folder site: ${state.slug ?? "(not set — run frontpage use <subdomain>)"}`);
23
48
  console.error(err instanceof Error ? err.message : String(err));
24
- return EXIT_UNEXPECTED;
49
+ return exitCodeForError(err);
25
50
  }
26
51
  }
@@ -1,14 +1,77 @@
1
- import { COMMANDS, TOPICS, findCommand } from "../registry.js";
1
+ import { COMMANDS, TOPIC_NAMES, TOPICS, findCommand } from "../registry.js";
2
+ import { isJson, emitOk } from "../runtime.js";
3
+ function printCommandHelp(cmd) {
4
+ console.log(`${cmd.name} — ${cmd.summary}`);
5
+ if (cmd.usage)
6
+ console.log(`Usage: ${cmd.usage}`);
7
+ if (cmd.flags?.length) {
8
+ console.log("\nFlags / notes:");
9
+ for (const f of cmd.flags)
10
+ console.log(` - ${f}`);
11
+ }
12
+ if (cmd.examples?.length) {
13
+ console.log("\nExamples:");
14
+ for (const e of cmd.examples)
15
+ console.log(` ${e}`);
16
+ }
17
+ if (cmd.dangers?.length) {
18
+ console.log("\nDanger:");
19
+ for (const d of cmd.dangers)
20
+ console.log(` ! ${d}`);
21
+ }
22
+ if (cmd.seeAlso?.length) {
23
+ console.log(`\nSee also: ${cmd.seeAlso.join(", ")}`);
24
+ }
25
+ }
2
26
  export function runHelp(args) {
3
- const topic = args[0]?.trim();
27
+ const filtered = args.filter((a) => a !== "--json" && a !== "--quiet" && a !== "-q");
28
+ const topic = filtered[0]?.trim();
29
+ if (isJson()) {
30
+ if (!topic) {
31
+ emitOk("help", {
32
+ commands: COMMANDS.map((c) => ({
33
+ name: c.name,
34
+ summary: c.summary,
35
+ usage: c.usage,
36
+ topics: c.topics,
37
+ examples: c.examples,
38
+ flags: c.flags,
39
+ dangers: c.dangers,
40
+ seeAlso: c.seeAlso,
41
+ })),
42
+ topics: TOPIC_NAMES,
43
+ topicBodies: TOPICS,
44
+ });
45
+ return 0;
46
+ }
47
+ if (TOPICS[topic]) {
48
+ emitOk("help", { topic, body: TOPICS[topic] });
49
+ return 0;
50
+ }
51
+ const cmd = findCommand(topic);
52
+ if (cmd) {
53
+ emitOk("help", { command: cmd });
54
+ return 0;
55
+ }
56
+ console.log(JSON.stringify({
57
+ ok: false,
58
+ command: "help",
59
+ error: { message: `Unknown help topic: ${topic}`, exitCode: 2 },
60
+ }));
61
+ return 2;
62
+ }
4
63
  if (!topic) {
5
64
  console.log("Frontpage CLI\n");
6
- console.log("Usage: frontpage <command> [options]\n");
7
- console.log("Commands:");
65
+ console.log("Package: frontpage-host · Binary: frontpage");
66
+ console.log("Do not install the unrelated npm package named \"frontpage\".\n");
67
+ console.log(TOPICS.quickstart.split("\n").slice(0, 12).join("\n"));
68
+ console.log("\nCommands:");
8
69
  for (const c of COMMANDS) {
9
- console.log(` ${c.name.padEnd(10)} ${c.summary}`);
70
+ console.log(` ${c.name.padEnd(12)} ${c.summary}`);
10
71
  }
11
- console.log("\nTopics: frontpage help auth | frontpage help exit-codes");
72
+ console.log(`\nTopics: frontpage help ${TOPIC_NAMES.join(" | frontpage help ")}`);
73
+ console.log("Global: --json --quiet --yes --site <slug> --help");
74
+ console.log("Agents: see AGENTS.md in the npm package");
12
75
  return 0;
13
76
  }
14
77
  if (TOPICS[topic]) {
@@ -17,9 +80,7 @@ export function runHelp(args) {
17
80
  }
18
81
  const cmd = findCommand(topic);
19
82
  if (cmd) {
20
- console.log(`${cmd.name} — ${cmd.summary}`);
21
- if (cmd.usage)
22
- console.log(`Usage: ${cmd.usage}`);
83
+ printCommandHelp(cmd);
23
84
  return 0;
24
85
  }
25
86
  console.error(`Unknown help topic: ${topic}`);
@@ -1,5 +1,6 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
2
+ import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
3
+ import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
3
4
  export async function runHistory(args) {
4
5
  try {
5
6
  const { slug, rest } = await resolveSlug(args);
@@ -23,13 +24,13 @@ export async function runHistory(args) {
23
24
  page = a.slice("--page=".length);
24
25
  continue;
25
26
  }
26
- if (a === "--help" || a === "-h") {
27
- console.error("Usage: frontpage history [--limit N] [--page N] [--site <slug>]");
28
- return EXIT_USAGE;
29
- }
30
27
  }
31
28
  const qs = new URLSearchParams({ limit, page });
32
29
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/history?${qs}`);
30
+ if (isJson()) {
31
+ emitOk("history", { slug, ...data });
32
+ return EXIT_OK;
33
+ }
33
34
  for (const c of data.commits) {
34
35
  const mark = data.stagingCommit === c.sha ? "S" : data.productionCommit === c.sha ? "P" : " ";
35
36
  const who = c.actorEmail ?? c.actorKind ?? "";
@@ -43,7 +44,9 @@ export async function runHistory(args) {
43
44
  return EXIT_OK;
44
45
  }
45
46
  catch (err) {
46
- console.error(err instanceof Error ? err.message : String(err));
47
- return exitCodeForError(err);
47
+ if (err instanceof Error && /Usage:/.test(err.message)) {
48
+ return emitUsage("history", err.message, EXIT_USAGE);
49
+ }
50
+ return emitErr("history", err);
48
51
  }
49
52
  }
@@ -1,11 +1,16 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
2
+ import { EXIT_OK, v1Get } from "../http/client.js";
3
+ import { emitErr, emitOk, isJson } from "../runtime.js";
3
4
  export async function runLs(args) {
4
5
  try {
5
6
  const { slug, rest } = await resolveSlug(args);
6
7
  const prefix = rest[0];
7
8
  const q = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
8
9
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files${q}`);
10
+ if (isJson()) {
11
+ emitOk("ls", { ...data, prefix: prefix ?? null });
12
+ return EXIT_OK;
13
+ }
9
14
  for (const f of data.files) {
10
15
  console.log(`${String(f.bytes).padStart(10)} ${f.path}`);
11
16
  }
@@ -13,7 +18,6 @@ export async function runLs(args) {
13
18
  return EXIT_OK;
14
19
  }
15
20
  catch (err) {
16
- console.error(err instanceof Error ? err.message : String(err));
17
- return exitCodeForError(err);
21
+ return emitErr("ls", err);
18
22
  }
19
23
  }