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.
@@ -1,28 +1,32 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { resolveSlug } from "../site.js";
3
- import { EXIT_OK, EXIT_USAGE, exitCodeForError } from "../http/client.js";
3
+ import { EXIT_OK, EXIT_USAGE } from "../http/client.js";
4
+ import { emitErr, emitOk, emitUsage, isJson, isQuiet } from "../runtime.js";
4
5
  export async function runOpen(args) {
5
6
  try {
6
7
  const { slug, rest } = await resolveSlug(args);
7
8
  const prod = rest.includes("--prod") || args.includes("--prod");
8
- const url = prod
9
- ? `https://${slug}.frontpage.host/` // staging subdomain always; custom domain is gateway-resolved
10
- : `https://${slug}.frontpage.host/`;
11
- // Both staging editing and public subdomain use *.frontpage.host; --prod is reserved
12
- // for a future production custom-domain lookup. For Phase 1 open the site subdomain.
13
- void prod;
9
+ const url = `https://${slug}.frontpage.host/`;
10
+ // --prod is reserved for a future production custom-domain lookup; both
11
+ // modes currently open the site subdomain on *.frontpage.host.
12
+ if (prod && !isJson() && !isQuiet()) {
13
+ console.error("Note: --prod is a no-op stub; opening https://<slug>.frontpage.host/");
14
+ }
14
15
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
15
16
  const openerArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
16
17
  spawn(opener, openerArgs, { detached: true, stdio: "ignore" }).unref();
17
- console.log(url);
18
+ if (isJson()) {
19
+ emitOk("open", { slug, url, prodRequested: prod, prodNoOp: true });
20
+ }
21
+ else {
22
+ console.log(url);
23
+ }
18
24
  return EXIT_OK;
19
25
  }
20
26
  catch (err) {
21
27
  if (err instanceof Error && err.message.includes("Usage")) {
22
- console.error("Usage: frontpage open [--site <slug>] [--prod]");
23
- return EXIT_USAGE;
28
+ return emitUsage("open", "Usage: frontpage open [--site <slug>] [--prod]", EXIT_USAGE);
24
29
  }
25
- console.error(err instanceof Error ? err.message : String(err));
26
- return exitCodeForError(err);
30
+ return emitErr("open", err);
27
31
  }
28
32
  }
@@ -1,16 +1,21 @@
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 runPages(args) {
4
5
  try {
5
6
  const { slug } = await resolveSlug(args);
6
7
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/pages`);
7
- for (const p of data.pages) {
8
- console.log(`${p.path.padEnd(40)} ${p.title}`);
8
+ if (isJson()) {
9
+ emitOk("pages", { slug, pages: data.pages });
10
+ }
11
+ else {
12
+ for (const p of data.pages) {
13
+ console.log(`${p.path.padEnd(40)} ${p.title}`);
14
+ }
9
15
  }
10
16
  return EXIT_OK;
11
17
  }
12
18
  catch (err) {
13
- console.error(err instanceof Error ? err.message : String(err));
14
- return exitCodeForError(err);
19
+ return emitErr("pages", err);
15
20
  }
16
21
  }
@@ -1,5 +1,6 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Post } from "../http/client.js";
2
+ import { EXIT_OK, EXIT_USAGE, v1Post } from "../http/client.js";
3
+ import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
3
4
  export async function runPrompt(args) {
4
5
  try {
5
6
  const { slug, rest } = await resolveSlug(args);
@@ -19,21 +20,23 @@ export async function runPrompt(args) {
19
20
  }
20
21
  const prompt = promptParts.join(" ").trim();
21
22
  if (!prompt) {
22
- console.error('Usage: frontpage prompt "..." [--site <slug>] [--path /page]');
23
- return EXIT_USAGE;
23
+ return emitUsage("prompt", 'Usage: frontpage prompt "..." [--site <slug>] [--path /page]', EXIT_USAGE);
24
24
  }
25
25
  if (!currentPath.startsWith("/")) {
26
- console.error("currentPath must start with /");
27
- return EXIT_USAGE;
26
+ return emitUsage("prompt", "currentPath must start with /", EXIT_USAGE);
28
27
  }
29
28
  const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`, { prompt, currentPath });
30
- console.log(data.runId);
31
- if (data.coalesced)
32
- console.error("(coalesced with an identical queued run)");
29
+ if (isJson()) {
30
+ emitOk("prompt", { slug, ...data });
31
+ }
32
+ else {
33
+ console.log(data.runId);
34
+ if (data.coalesced)
35
+ console.error("(coalesced with an identical queued run)");
36
+ }
33
37
  return EXIT_OK;
34
38
  }
35
39
  catch (err) {
36
- console.error(err instanceof Error ? err.message : String(err));
37
- return exitCodeForError(err);
40
+ return emitErr("prompt", err);
38
41
  }
39
42
  }
@@ -1,28 +1,40 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, exitCodeForError, v1Delete, v1Post } from "../http/client.js";
2
+ import { EXIT_OK, EXIT_USAGE, v1Delete, v1Post } from "../http/client.js";
3
+ import { emitErr, emitOk, emitUsage, isJson, requireYesOrTty } from "../runtime.js";
3
4
  export async function runPublish(args) {
4
5
  try {
5
6
  const { slug } = await resolveSlug(args);
6
7
  const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/publish`);
7
- console.log(`Published ${slug}`);
8
- if (data.productionUrl)
9
- console.log(data.productionUrl);
8
+ if (isJson()) {
9
+ emitOk("publish", { slug, productionUrl: data.productionUrl ?? null });
10
+ }
11
+ else {
12
+ console.log(`Published ${slug}`);
13
+ if (data.productionUrl)
14
+ console.log(data.productionUrl);
15
+ }
10
16
  return EXIT_OK;
11
17
  }
12
18
  catch (err) {
13
- console.error(err instanceof Error ? err.message : String(err));
14
- return exitCodeForError(err);
19
+ return emitErr("publish", err);
15
20
  }
16
21
  }
17
22
  export async function runUnpublish(args) {
18
23
  try {
24
+ const refuse = requireYesOrTty("unpublish");
25
+ if (refuse)
26
+ return emitUsage("unpublish", refuse, EXIT_USAGE);
19
27
  const { slug } = await resolveSlug(args);
20
28
  await v1Delete(`/api/v1/sites/${encodeURIComponent(slug)}/publish`);
21
- console.log(`Unpublished ${slug}`);
29
+ if (isJson()) {
30
+ emitOk("unpublish", { slug });
31
+ }
32
+ else {
33
+ console.log(`Unpublished ${slug}`);
34
+ }
22
35
  return EXIT_OK;
23
36
  }
24
37
  catch (err) {
25
- console.error(err instanceof Error ? err.message : String(err));
26
- return exitCodeForError(err);
38
+ return emitErr("unpublish", err);
27
39
  }
28
40
  }
@@ -1,8 +1,9 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve, sep } from "node:path";
3
3
  import { resolveSlug } from "../site.js";
4
- import { EXIT_OK, EXIT_UNEXPECTED, exitCodeForError, v1Get } from "../http/client.js";
4
+ import { EXIT_OK, EXIT_UNEXPECTED, v1Get } from "../http/client.js";
5
5
  import { readState, writeState } from "../state/frontpage-state.js";
6
+ import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
6
7
  /** Reject absolute / traversal paths before writing into cwd (defense in depth). */
7
8
  function assertSafePullPath(repoPath, cwd) {
8
9
  if (!repoPath ||
@@ -31,8 +32,7 @@ export async function runPull(args) {
31
32
  abs = assertSafePullPath(file.path, cwd);
32
33
  }
33
34
  catch (err) {
34
- console.error(err instanceof Error ? err.message : String(err));
35
- return EXIT_UNEXPECTED;
35
+ return emitUsage("pull", err instanceof Error ? err.message : String(err), EXIT_UNEXPECTED);
36
36
  }
37
37
  await mkdir(dirname(abs), { recursive: true });
38
38
  if (file.encoding === "base64") {
@@ -45,16 +45,26 @@ export async function runPull(args) {
45
45
  }
46
46
  const prev = await readState(cwd);
47
47
  await writeState({ ...prev, slug: data.slug, baseCommitSha: data.baseCommitSha ?? undefined }, cwd);
48
- console.log(`Pulled ${written} file(s) for ${data.slug}`);
49
- if (data.baseCommitSha)
50
- console.log(`baseCommitSha: ${data.baseCommitSha}`);
51
- if (data.truncated)
52
- console.log("Warning: pull truncated (file or size cap). Re-run or narrow the tree.");
53
- console.log("Wrote .frontpage/state.json");
48
+ if (isJson()) {
49
+ emitOk("pull", {
50
+ slug: data.slug,
51
+ baseCommitSha: data.baseCommitSha,
52
+ written,
53
+ fileCount: data.fileCount,
54
+ truncated: data.truncated,
55
+ });
56
+ }
57
+ else {
58
+ console.log(`Pulled ${written} file(s) for ${data.slug}`);
59
+ if (data.baseCommitSha)
60
+ console.log(`baseCommitSha: ${data.baseCommitSha}`);
61
+ if (data.truncated)
62
+ console.log("Warning: pull truncated (file or size cap). Re-run or narrow the tree.");
63
+ console.log("Wrote .frontpage/state.json");
64
+ }
54
65
  return EXIT_OK;
55
66
  }
56
67
  catch (err) {
57
- console.error(err instanceof Error ? err.message : String(err));
58
- return exitCodeForError(err);
68
+ return emitErr("pull", err);
59
69
  }
60
70
  }
@@ -1,8 +1,9 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join, relative, sep } from "node:path";
3
3
  import { resolveSlug } from "../site.js";
4
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Put } from "../http/client.js";
4
+ import { EXIT_OK, EXIT_USAGE, v1Put } from "../http/client.js";
5
5
  import { readState, writeState } from "../state/frontpage-state.js";
6
+ import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
6
7
  const TEXT_EXTS = new Set([
7
8
  ".astro",
8
9
  ".ts",
@@ -68,13 +69,11 @@ export async function runPush(args) {
68
69
  const cwd = process.cwd();
69
70
  const state = await readState(cwd);
70
71
  if (!state.baseCommitSha) {
71
- console.error("No baseCommitSha in .frontpage/state.json. Run: frontpage pull");
72
- return EXIT_USAGE;
72
+ return emitUsage("push", "No baseCommitSha in .frontpage/state.json. Run: frontpage pull", EXIT_USAGE);
73
73
  }
74
74
  const paths = await walkAllowlisted(cwd);
75
75
  if (paths.length === 0) {
76
- console.error("No allowlisted files (src/, public/, content/) found in this folder.");
77
- return EXIT_USAGE;
76
+ return emitUsage("push", "No allowlisted files (src/, public/, content/) found in this folder.", EXIT_USAGE);
78
77
  }
79
78
  const files = [];
80
79
  for (const path of paths) {
@@ -100,6 +99,16 @@ export async function runPush(args) {
100
99
  if (data.commitSha) {
101
100
  await writeState({ ...state, slug, baseCommitSha: data.commitSha }, cwd);
102
101
  }
102
+ if (isJson()) {
103
+ emitOk("push", {
104
+ slug,
105
+ changed: data.changed,
106
+ commitSha: data.commitSha,
107
+ baseCommitSha: data.baseCommitSha,
108
+ filesChanged: data.filesChanged,
109
+ });
110
+ return EXIT_OK;
111
+ }
103
112
  if (!data.changed) {
104
113
  console.log("Nothing to commit (remote already matches).");
105
114
  return EXIT_OK;
@@ -111,7 +120,6 @@ export async function runPush(args) {
111
120
  return EXIT_OK;
112
121
  }
113
122
  catch (err) {
114
- console.error(err instanceof Error ? err.message : String(err));
115
- return exitCodeForError(err);
123
+ return emitErr("push", err);
116
124
  }
117
125
  }
@@ -1,27 +1,32 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Post } from "../http/client.js";
2
+ import { EXIT_OK, EXIT_USAGE, v1Post } from "../http/client.js";
3
+ import { emitErr, emitOk, emitUsage, isJson, requireYesOrTty } from "../runtime.js";
3
4
  export async function runRevert(args) {
4
5
  try {
6
+ const refuse = requireYesOrTty("revert");
7
+ if (refuse)
8
+ return emitUsage("revert", refuse, EXIT_USAGE);
5
9
  const { slug, rest } = await resolveSlug(args);
6
10
  const target = rest.find((a) => !a.startsWith("-"));
7
11
  if (!target || !/^[0-9a-f]{7,40}$/i.test(target)) {
8
- console.error("Usage: frontpage revert <commitSha> [--site <slug>]");
9
- console.error("Pass a full 40-char SHA from `frontpage history`.");
10
- return EXIT_USAGE;
12
+ return emitUsage("revert", "Usage: frontpage revert <commitSha> [--site <slug>] [--yes]\nPass a full 40-char SHA from `frontpage history`.", EXIT_USAGE);
11
13
  }
12
14
  if (target.length !== 40) {
13
- console.error("targetCommitSha must be a full 40-char hex SHA (not a short SHA).");
14
- return EXIT_USAGE;
15
+ return emitUsage("revert", "targetCommitSha must be a full 40-char hex SHA (not a short SHA).", EXIT_USAGE);
15
16
  }
16
17
  const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/revert`, {
17
18
  targetCommitSha: target.toLowerCase(),
18
19
  });
19
- console.log(`${data.commitHash.slice(0, 7)} ${data.message}`);
20
- console.log(`restored ${data.targetCommitSha.slice(0, 7)} (was ${data.previousTipSha.slice(0, 7)})`);
20
+ if (isJson()) {
21
+ emitOk("revert", { slug, ...data });
22
+ }
23
+ else {
24
+ console.log(`${data.commitHash.slice(0, 7)} ${data.message}`);
25
+ console.log(`restored ${data.targetCommitSha.slice(0, 7)} (was ${data.previousTipSha.slice(0, 7)})`);
26
+ }
21
27
  return EXIT_OK;
22
28
  }
23
29
  catch (err) {
24
- console.error(err instanceof Error ? err.message : String(err));
25
- return exitCodeForError(err);
30
+ return emitErr("revert", err);
26
31
  }
27
32
  }
@@ -1,5 +1,6 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
2
+ import { EXIT_OK, EXIT_USAGE, v1Get, v1Post } from "../http/client.js";
3
+ import { emitErr, emitOk, emitUsage, isJson, progress } from "../runtime.js";
3
4
  const TERMINAL = new Set(["succeeded", "failed", "canceled"]);
4
5
  function printRun(run) {
5
6
  const msg = run.statusMessage || run.summary || run.error || "";
@@ -23,20 +24,21 @@ export async function runRuns(args) {
23
24
  return await retryRunCmd(args.slice(1));
24
25
  if (sub === "get")
25
26
  return await getRun(args.slice(1));
26
- // `frontpage runs <runId>` → get
27
27
  if (/^[0-9a-f-]{36}$/i.test(sub))
28
28
  return await getRun(args);
29
- console.error("Usage: frontpage runs [current|wait|cancel|retry|get] …");
30
- return EXIT_USAGE;
29
+ return emitUsage("runs", "Usage: frontpage runs [list|current|wait|cancel|retry|get] [runId] [--site <slug>]", EXIT_USAGE);
31
30
  }
32
31
  catch (err) {
33
- console.error(err instanceof Error ? err.message : String(err));
34
- return exitCodeForError(err);
32
+ return emitErr("runs", err);
35
33
  }
36
34
  }
37
35
  async function listRuns(args) {
38
36
  const { slug } = await resolveSlug(args);
39
37
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`);
38
+ if (isJson()) {
39
+ emitOk("runs", { slug, ...data });
40
+ return EXIT_OK;
41
+ }
40
42
  if (data.run) {
41
43
  console.log("latest:");
42
44
  printRun(data.run);
@@ -60,11 +62,15 @@ async function getRun(args) {
60
62
  const { slug, rest } = await resolveSlug(args);
61
63
  const runId = rest[0];
62
64
  if (!runId) {
63
- console.error("Usage: frontpage runs get <runId> [--site <slug>]");
64
- return EXIT_USAGE;
65
+ return emitUsage("runs", "Usage: frontpage runs get <runId> [--site <slug>]", EXIT_USAGE);
65
66
  }
66
67
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}`);
67
- printRun(data.run);
68
+ if (isJson()) {
69
+ emitOk("runs", { slug, run: data.run });
70
+ }
71
+ else {
72
+ printRun(data.run);
73
+ }
68
74
  return EXIT_OK;
69
75
  }
70
76
  async function waitRun(args) {
@@ -75,10 +81,15 @@ async function waitRun(args) {
75
81
  while (Date.now() - started < maxMs) {
76
82
  if (runId) {
77
83
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}`);
78
- process.stderr.write(`\r${data.run.status} ${data.run.statusMessage ?? ""} `);
84
+ progress(`\r${data.run.status} ${data.run.statusMessage ?? ""} `);
79
85
  if (TERMINAL.has(data.run.status)) {
80
- process.stderr.write("\n");
81
- printRun(data.run);
86
+ progress("\n");
87
+ if (isJson()) {
88
+ emitOk("runs", { slug, action: "wait", run: data.run });
89
+ }
90
+ else {
91
+ printRun(data.run);
92
+ }
82
93
  if (data.run.status === "failed")
83
94
  return EXIT_USAGE;
84
95
  if (data.run.status === "canceled")
@@ -93,42 +104,59 @@ async function waitRun(args) {
93
104
  : data.run;
94
105
  if (current) {
95
106
  runId = current.id;
96
- process.stderr.write(`\r${current.status} ${current.statusMessage ?? ""} `);
107
+ progress(`\r${current.status} ${current.statusMessage ?? ""} `);
97
108
  if (TERMINAL.has(current.status) && !data.poll?.active) {
98
- process.stderr.write("\n");
99
- printRun(current);
109
+ progress("\n");
110
+ if (isJson()) {
111
+ emitOk("runs", { slug, action: "wait", run: current });
112
+ }
113
+ else {
114
+ printRun(current);
115
+ }
100
116
  return current.status === "succeeded" ? EXIT_OK : EXIT_USAGE;
101
117
  }
102
118
  }
103
119
  else {
104
- console.log("No active run.");
120
+ if (isJson()) {
121
+ emitOk("runs", { slug, action: "wait", run: null });
122
+ }
123
+ else {
124
+ console.log("No active run.");
125
+ }
105
126
  return EXIT_OK;
106
127
  }
107
128
  }
108
129
  await new Promise((r) => setTimeout(r, 2000));
109
130
  }
110
- console.error("Timed out waiting for run.");
111
- return EXIT_USAGE;
131
+ return emitUsage("runs", "Timed out waiting for run.", EXIT_USAGE);
112
132
  }
113
133
  async function cancelRun(args) {
114
134
  const { slug, rest } = await resolveSlug(args);
115
135
  const runId = rest[0];
116
136
  if (!runId) {
117
- console.error("Usage: frontpage runs cancel <runId> [--site <slug>]");
118
- return EXIT_USAGE;
137
+ return emitUsage("runs", "Usage: frontpage runs cancel <runId> [--site <slug>]", EXIT_USAGE);
119
138
  }
120
139
  await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}/cancel`);
121
- console.log(`Canceled ${runId}`);
140
+ if (isJson()) {
141
+ emitOk("runs", { slug, action: "cancel", runId });
142
+ }
143
+ else {
144
+ console.log(`Canceled ${runId}`);
145
+ }
122
146
  return EXIT_OK;
123
147
  }
124
148
  async function retryRunCmd(args) {
125
149
  const { slug, rest } = await resolveSlug(args);
126
150
  const runId = rest[0];
127
151
  if (!runId) {
128
- console.error("Usage: frontpage runs retry <runId> [--site <slug>]");
129
- return EXIT_USAGE;
152
+ return emitUsage("runs", "Usage: frontpage runs retry <runId> [--site <slug>]", EXIT_USAGE);
130
153
  }
131
154
  const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}/retry`);
132
- console.log(data.runId);
155
+ if (isJson()) {
156
+ emitOk("runs", { slug, action: "retry", runId: data.runId, previousRunId: runId });
157
+ }
158
+ else {
159
+ console.log(data.runId);
160
+ }
133
161
  return EXIT_OK;
134
162
  }
@@ -1,7 +1,13 @@
1
- import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
1
+ import { EXIT_OK } from "../http/client.js";
2
+ import { v1Get } from "../http/client.js";
3
+ import { emitErr, emitOk, isJson } from "../runtime.js";
2
4
  export async function runSites() {
3
5
  try {
4
6
  const data = await v1Get("/api/v1/sites");
7
+ if (isJson()) {
8
+ emitOk("sites", data);
9
+ return EXIT_OK;
10
+ }
5
11
  if (data.sites.length === 0) {
6
12
  console.log("No sites. Create a site in Frontpage first.");
7
13
  return EXIT_OK;
@@ -14,7 +20,6 @@ export async function runSites() {
14
20
  return EXIT_OK;
15
21
  }
16
22
  catch (err) {
17
- console.error(err instanceof Error ? err.message : String(err));
18
- return exitCodeForError(err);
23
+ return emitErr("sites", err);
19
24
  }
20
25
  }
@@ -1,6 +1,7 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, exitCodeForError, v1Get, apiBase } from "../http/client.js";
2
+ import { EXIT_OK, v1Get, apiBase } from "../http/client.js";
3
3
  import { readState } from "../state/frontpage-state.js";
4
+ import { emitErr, emitOk, isJson } from "../runtime.js";
4
5
  export async function runStatus(args) {
5
6
  try {
6
7
  const { slug } = await resolveSlug(args);
@@ -11,7 +12,22 @@ export async function runStatus(args) {
11
12
  active: [],
12
13
  })),
13
14
  ]);
14
- console.log(`API base: ${apiBase()}`);
15
+ const tipDrift = Boolean(state.baseCommitSha && data.baseCommitSha && state.baseCommitSha !== data.baseCommitSha);
16
+ const payload = {
17
+ apiBase: apiBase(),
18
+ site: slug,
19
+ localSlug: state.slug ?? null,
20
+ localBaseCommitSha: state.baseCommitSha ?? null,
21
+ remoteTip: data.baseCommitSha,
22
+ remoteFileCount: data.files.length,
23
+ activeRuns: runs.active ?? [],
24
+ tipDrift,
25
+ };
26
+ if (isJson()) {
27
+ emitOk("status", payload);
28
+ return EXIT_OK;
29
+ }
30
+ console.log(`API base: ${payload.apiBase}`);
15
31
  console.log(`Site: ${slug}`);
16
32
  console.log(`Local state: ${state.slug ?? "(none)"}${state.baseCommitSha ? ` @ ${state.baseCommitSha.slice(0, 7)}` : ""}`);
17
33
  console.log(`Remote tip: ${data.baseCommitSha ?? "(unknown)"}`);
@@ -23,13 +39,12 @@ export async function runStatus(args) {
23
39
  else {
24
40
  console.log("Active runs: none");
25
41
  }
26
- if (state.baseCommitSha && data.baseCommitSha && state.baseCommitSha !== data.baseCommitSha) {
42
+ if (tipDrift) {
27
43
  console.log("Note: remote tip differs from last pull — run frontpage pull");
28
44
  }
29
45
  return EXIT_OK;
30
46
  }
31
47
  catch (err) {
32
- console.error(err instanceof Error ? err.message : String(err));
33
- return exitCodeForError(err);
48
+ return emitErr("status", err);
34
49
  }
35
50
  }
@@ -1,14 +1,20 @@
1
- import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
1
+ import { EXIT_OK } from "../http/client.js";
2
+ import { v1Get } from "../http/client.js";
3
+ import { emitErr, emitOk, isJson } from "../runtime.js";
2
4
  export async function runWhoami() {
3
5
  try {
4
6
  const me = await v1Get("/api/v1/me");
5
- console.log(`email: ${me.email ?? "(none)"}`);
6
- console.log(`userId: ${me.userId}`);
7
- console.log(`keyId: ${me.keyId}`);
7
+ if (isJson()) {
8
+ emitOk("whoami", me);
9
+ }
10
+ else {
11
+ console.log(`email: ${me.email ?? "(none)"}`);
12
+ console.log(`userId: ${me.userId}`);
13
+ console.log(`keyId: ${me.keyId}`);
14
+ }
8
15
  return EXIT_OK;
9
16
  }
10
17
  catch (err) {
11
- console.error(err instanceof Error ? err.message : String(err));
12
- return exitCodeForError(err);
18
+ return emitErr("whoami", err);
13
19
  }
14
20
  }
@@ -14,15 +14,16 @@ _frontpage_completions() {
14
14
  fi
15
15
  local cmd="\${COMP_WORDS[1]}"
16
16
  case "\${cmd}" in
17
- help) COMPREPLY=( $(compgen -W "\${cmds} auth sync agent content history exit-codes" -- "\${cur}") ) ;;
17
+ help) COMPREPLY=( $(compgen -W "\${cmds} quickstart auth sync agent content history isolation exit-codes" -- "\${cur}") ) ;;
18
18
  completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") ) ;;
19
- runs) COMPREPLY=( $(compgen -W "current wait cancel retry get" -- "\${cur}") ) ;;
19
+ runs) COMPREPLY=( $(compgen -W "list current wait cancel retry get" -- "\${cur}") ) ;;
20
20
  deploy) COMPREPLY=( $(compgen -W "wait" -- "\${cur}") ) ;;
21
21
  media) COMPREPLY=( $(compgen -W "list upload" -- "\${cur}") ) ;;
22
22
  seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
23
23
  articles) COMPREPLY=( $(compgen -W "list content create-section create-article set-draft" -- "\${cur}") ) ;;
24
- modules|extensions) COMPREPLY=( $(compgen -W "list enable disable config" -- "\${cur}") ) ;;
25
- *) COMPREPLY=( $(compgen -W "--site --help" -- "\${cur}") ) ;;
24
+ modules) COMPREPLY=( $(compgen -W "list enable disable archive config" -- "\${cur}") ) ;;
25
+ extensions) COMPREPLY=( $(compgen -W "list enable disable config" -- "\${cur}") ) ;;
26
+ *) COMPREPLY=( $(compgen -W "--site --help --json --quiet --yes" -- "\${cur}") ) ;;
26
27
  esac
27
28
  }
28
29
  complete -F _frontpage_completions frontpage
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Strip global flags from argv. Safe to call before command dispatch.
3
+ * Recognizes --json, --quiet, --yes, and help aliases anywhere in the list.
4
+ */
5
+ export type GlobalFlags = {
6
+ json: boolean;
7
+ quiet: boolean;
8
+ yes: boolean;
9
+ /** True when argv asked for help (--help / -h), not the `help` command itself. */
10
+ wantsHelp: boolean;
11
+ args: string[];
12
+ };
13
+ export declare function parseGlobalFlags(argv: string[]): GlobalFlags;
package/dist/flags.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Strip global flags from argv. Safe to call before command dispatch.
3
+ * Recognizes --json, --quiet, --yes, and help aliases anywhere in the list.
4
+ */
5
+ export function parseGlobalFlags(argv) {
6
+ const args = [];
7
+ let json = false;
8
+ let quiet = false;
9
+ let yes = false;
10
+ let wantsHelp = false;
11
+ for (const a of argv) {
12
+ if (a === "--json") {
13
+ json = true;
14
+ continue;
15
+ }
16
+ if (a === "--quiet" || a === "-q") {
17
+ quiet = true;
18
+ continue;
19
+ }
20
+ if (a === "--yes" || a === "-y") {
21
+ yes = true;
22
+ continue;
23
+ }
24
+ if (a === "--help" || a === "-h") {
25
+ wantsHelp = true;
26
+ continue;
27
+ }
28
+ args.push(a);
29
+ }
30
+ return { json, quiet, yes, wantsHelp, args };
31
+ }
@@ -8,7 +8,12 @@ export type CommandDef = {
8
8
  usage?: string;
9
9
  /** Topic tags for `help <topic>`. */
10
10
  topics?: string[];
11
+ examples?: string[];
12
+ flags?: string[];
13
+ dangers?: string[];
14
+ seeAlso?: string[];
11
15
  };
12
16
  export declare const COMMANDS: readonly CommandDef[];
17
+ export declare const TOPIC_NAMES: readonly ["quickstart", "auth", "sync", "agent", "content", "history", "isolation", "exit-codes"];
13
18
  export declare const TOPICS: Record<string, string>;
14
19
  export declare function findCommand(name: string): CommandDef | undefined;