frontpage-host 0.1.0

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.
Files changed (81) hide show
  1. package/dist/auth/keychain.d.ts +5 -0
  2. package/dist/auth/keychain.js +85 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +124 -0
  5. package/dist/commands/analytics.d.ts +1 -0
  6. package/dist/commands/analytics.js +18 -0
  7. package/dist/commands/articles.d.ts +1 -0
  8. package/dist/commands/articles.js +111 -0
  9. package/dist/commands/cat.d.ts +1 -0
  10. package/dist/commands/cat.js +27 -0
  11. package/dist/commands/completion.d.ts +1 -0
  12. package/dist/commands/completion.js +19 -0
  13. package/dist/commands/deploy.d.ts +2 -0
  14. package/dist/commands/deploy.js +32 -0
  15. package/dist/commands/diff.d.ts +1 -0
  16. package/dist/commands/diff.js +93 -0
  17. package/dist/commands/doctor.d.ts +1 -0
  18. package/dist/commands/doctor.js +26 -0
  19. package/dist/commands/edit.d.ts +1 -0
  20. package/dist/commands/edit.js +31 -0
  21. package/dist/commands/extensions.d.ts +1 -0
  22. package/dist/commands/extensions.js +60 -0
  23. package/dist/commands/help.d.ts +1 -0
  24. package/dist/commands/help.js +27 -0
  25. package/dist/commands/history.d.ts +1 -0
  26. package/dist/commands/history.js +49 -0
  27. package/dist/commands/inbox.d.ts +1 -0
  28. package/dist/commands/inbox.js +46 -0
  29. package/dist/commands/login.d.ts +1 -0
  30. package/dist/commands/login.js +38 -0
  31. package/dist/commands/logout.d.ts +1 -0
  32. package/dist/commands/logout.js +7 -0
  33. package/dist/commands/ls.d.ts +1 -0
  34. package/dist/commands/ls.js +19 -0
  35. package/dist/commands/media.d.ts +1 -0
  36. package/dist/commands/media.js +44 -0
  37. package/dist/commands/modules.d.ts +1 -0
  38. package/dist/commands/modules.js +56 -0
  39. package/dist/commands/open.d.ts +1 -0
  40. package/dist/commands/open.js +28 -0
  41. package/dist/commands/pages.d.ts +1 -0
  42. package/dist/commands/pages.js +16 -0
  43. package/dist/commands/prompt.d.ts +1 -0
  44. package/dist/commands/prompt.js +39 -0
  45. package/dist/commands/publish.d.ts +2 -0
  46. package/dist/commands/publish.js +28 -0
  47. package/dist/commands/pull.d.ts +1 -0
  48. package/dist/commands/pull.js +60 -0
  49. package/dist/commands/push.d.ts +1 -0
  50. package/dist/commands/push.js +117 -0
  51. package/dist/commands/revert.d.ts +1 -0
  52. package/dist/commands/revert.js +27 -0
  53. package/dist/commands/runs.d.ts +1 -0
  54. package/dist/commands/runs.js +134 -0
  55. package/dist/commands/search.d.ts +1 -0
  56. package/dist/commands/search.js +30 -0
  57. package/dist/commands/seo.d.ts +1 -0
  58. package/dist/commands/seo.js +57 -0
  59. package/dist/commands/settings.d.ts +1 -0
  60. package/dist/commands/settings.js +65 -0
  61. package/dist/commands/sites.d.ts +1 -0
  62. package/dist/commands/sites.js +20 -0
  63. package/dist/commands/status.d.ts +1 -0
  64. package/dist/commands/status.js +35 -0
  65. package/dist/commands/usage.d.ts +1 -0
  66. package/dist/commands/usage.js +14 -0
  67. package/dist/commands/use.d.ts +1 -0
  68. package/dist/commands/use.js +25 -0
  69. package/dist/commands/whoami.d.ts +1 -0
  70. package/dist/commands/whoami.js +14 -0
  71. package/dist/completions/generate.d.ts +4 -0
  72. package/dist/completions/generate.js +82 -0
  73. package/dist/http/client.d.ts +27 -0
  74. package/dist/http/client.js +163 -0
  75. package/dist/registry.d.ts +14 -0
  76. package/dist/registry.js +116 -0
  77. package/dist/site.d.ts +5 -0
  78. package/dist/site.js +32 -0
  79. package/dist/state/frontpage-state.d.ts +8 -0
  80. package/dist/state/frontpage-state.js +28 -0
  81. package/package.json +40 -0
@@ -0,0 +1,117 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
+ import { resolveSlug } from "../site.js";
4
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Put } from "../http/client.js";
5
+ import { readState, writeState } from "../state/frontpage-state.js";
6
+ const TEXT_EXTS = new Set([
7
+ ".astro",
8
+ ".ts",
9
+ ".tsx",
10
+ ".js",
11
+ ".jsx",
12
+ ".mjs",
13
+ ".cjs",
14
+ ".css",
15
+ ".scss",
16
+ ".md",
17
+ ".mdx",
18
+ ".json",
19
+ ".svg",
20
+ ".html",
21
+ ".txt",
22
+ ".yml",
23
+ ".yaml",
24
+ ".toml",
25
+ ]);
26
+ const ROOTS = ["src", "public", "content"];
27
+ async function walkAllowlisted(cwd) {
28
+ const out = [];
29
+ async function walk(abs) {
30
+ let entries;
31
+ try {
32
+ entries = await readdir(abs, { withFileTypes: true });
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ for (const ent of entries) {
38
+ const child = join(abs, ent.name);
39
+ if (ent.isDirectory()) {
40
+ if (ent.name === "node_modules" || ent.name === ".git" || ent.name === ".frontpage")
41
+ continue;
42
+ await walk(child);
43
+ }
44
+ else if (ent.isFile()) {
45
+ const rel = relative(cwd, child).split(sep).join("/");
46
+ if (ROOTS.some((r) => rel === r || rel.startsWith(`${r}/`)))
47
+ out.push(rel);
48
+ }
49
+ }
50
+ }
51
+ for (const root of ROOTS)
52
+ await walk(join(cwd, root));
53
+ return out.sort();
54
+ }
55
+ function isTextPath(repoPath) {
56
+ const i = repoPath.lastIndexOf(".");
57
+ if (i < 0)
58
+ return true;
59
+ return TEXT_EXTS.has(repoPath.slice(i).toLowerCase());
60
+ }
61
+ export async function runPush(args) {
62
+ try {
63
+ const { slug, rest } = await resolveSlug(args);
64
+ const messageIdx = rest.indexOf("--message");
65
+ const message = messageIdx >= 0
66
+ ? rest[messageIdx + 1]
67
+ : rest.find((a) => a.startsWith("--message="))?.slice("--message=".length);
68
+ const cwd = process.cwd();
69
+ const state = await readState(cwd);
70
+ if (!state.baseCommitSha) {
71
+ console.error("No baseCommitSha in .frontpage/state.json. Run: frontpage pull");
72
+ return EXIT_USAGE;
73
+ }
74
+ const paths = await walkAllowlisted(cwd);
75
+ if (paths.length === 0) {
76
+ console.error("No allowlisted files (src/, public/, content/) found in this folder.");
77
+ return EXIT_USAGE;
78
+ }
79
+ const files = [];
80
+ for (const path of paths) {
81
+ const abs = join(cwd, path);
82
+ const st = await stat(abs);
83
+ if (!st.isFile())
84
+ continue;
85
+ if (isTextPath(path)) {
86
+ const content = await readFile(abs, "utf8");
87
+ files.push({ path, encoding: "utf-8", content });
88
+ }
89
+ else {
90
+ const buf = await readFile(abs);
91
+ files.push({ path, encoding: "base64", content: buf.toString("base64") });
92
+ }
93
+ }
94
+ const data = await v1Put(`/api/v1/sites/${encodeURIComponent(slug)}/files`, {
95
+ baseCommitSha: state.baseCommitSha,
96
+ expectedHeadSha: state.baseCommitSha,
97
+ message: message || undefined,
98
+ files,
99
+ });
100
+ if (data.commitSha) {
101
+ await writeState({ ...state, slug, baseCommitSha: data.commitSha }, cwd);
102
+ }
103
+ if (!data.changed) {
104
+ console.log("Nothing to commit (remote already matches).");
105
+ return EXIT_OK;
106
+ }
107
+ console.log(`Pushed ${data.filesChanged.length} file(s) → ${data.commitSha}`);
108
+ console.log("Updated .frontpage/state.json baseCommitSha");
109
+ console.log("Note: module markup may auto-activate modules (same as the editor agent).");
110
+ console.log("tokens.ts may be auto-repaired at commit if it would break the build.");
111
+ return EXIT_OK;
112
+ }
113
+ catch (err) {
114
+ console.error(err instanceof Error ? err.message : String(err));
115
+ return exitCodeForError(err);
116
+ }
117
+ }
@@ -0,0 +1 @@
1
+ export declare function runRevert(args: string[]): Promise<number>;
@@ -0,0 +1,27 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Post } from "../http/client.js";
3
+ export async function runRevert(args) {
4
+ try {
5
+ const { slug, rest } = await resolveSlug(args);
6
+ const target = rest.find((a) => !a.startsWith("-"));
7
+ 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;
11
+ }
12
+ 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
+ }
16
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/revert`, {
17
+ targetCommitSha: target.toLowerCase(),
18
+ });
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)})`);
21
+ return EXIT_OK;
22
+ }
23
+ catch (err) {
24
+ console.error(err instanceof Error ? err.message : String(err));
25
+ return exitCodeForError(err);
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ export declare function runRuns(args: string[]): Promise<number>;
@@ -0,0 +1,134 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
3
+ const TERMINAL = new Set(["succeeded", "failed", "canceled"]);
4
+ function printRun(run) {
5
+ const msg = run.statusMessage || run.summary || run.error || "";
6
+ console.log(`${run.id} ${run.status}${msg ? ` ${msg}` : ""}`);
7
+ if (run.commitSha)
8
+ console.log(` commit: ${run.commitSha}`);
9
+ }
10
+ export async function runRuns(args) {
11
+ try {
12
+ const sub = args[0];
13
+ if (!sub || sub === "list" || sub.startsWith("--")) {
14
+ return await listRuns(args[0] === "list" ? args.slice(1) : args);
15
+ }
16
+ if (sub === "current")
17
+ return await listRuns(args.slice(1));
18
+ if (sub === "wait")
19
+ return await waitRun(args.slice(1));
20
+ if (sub === "cancel")
21
+ return await cancelRun(args.slice(1));
22
+ if (sub === "retry")
23
+ return await retryRunCmd(args.slice(1));
24
+ if (sub === "get")
25
+ return await getRun(args.slice(1));
26
+ // `frontpage runs <runId>` → get
27
+ if (/^[0-9a-f-]{36}$/i.test(sub))
28
+ return await getRun(args);
29
+ console.error("Usage: frontpage runs [current|wait|cancel|retry|get] …");
30
+ return EXIT_USAGE;
31
+ }
32
+ catch (err) {
33
+ console.error(err instanceof Error ? err.message : String(err));
34
+ return exitCodeForError(err);
35
+ }
36
+ }
37
+ async function listRuns(args) {
38
+ const { slug } = await resolveSlug(args);
39
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`);
40
+ if (data.run) {
41
+ console.log("latest:");
42
+ printRun(data.run);
43
+ }
44
+ else {
45
+ console.log("latest: (none)");
46
+ }
47
+ if (data.active?.length) {
48
+ console.log(`active (${data.active.length}):`);
49
+ for (const r of data.active)
50
+ printRun(r);
51
+ }
52
+ if (data.poll?.reply) {
53
+ const r = data.poll.reply;
54
+ const detail = r.summary || r.message || r.kind;
55
+ console.log(`poll: ${r.kind}${detail && detail !== r.kind ? ` — ${detail}` : ""}`);
56
+ }
57
+ return EXIT_OK;
58
+ }
59
+ async function getRun(args) {
60
+ const { slug, rest } = await resolveSlug(args);
61
+ const runId = rest[0];
62
+ if (!runId) {
63
+ console.error("Usage: frontpage runs get <runId> [--site <slug>]");
64
+ return EXIT_USAGE;
65
+ }
66
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}`);
67
+ printRun(data.run);
68
+ return EXIT_OK;
69
+ }
70
+ async function waitRun(args) {
71
+ const { slug, rest } = await resolveSlug(args);
72
+ let runId = rest[0];
73
+ const started = Date.now();
74
+ const maxMs = 30 * 60 * 1000;
75
+ while (Date.now() - started < maxMs) {
76
+ if (runId) {
77
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}`);
78
+ process.stderr.write(`\r${data.run.status} ${data.run.statusMessage ?? ""} `);
79
+ if (TERMINAL.has(data.run.status)) {
80
+ process.stderr.write("\n");
81
+ printRun(data.run);
82
+ if (data.run.status === "failed")
83
+ return EXIT_USAGE;
84
+ if (data.run.status === "canceled")
85
+ return EXIT_USAGE;
86
+ return EXIT_OK;
87
+ }
88
+ }
89
+ else {
90
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`);
91
+ const current = data.poll?.activeRunId
92
+ ? data.active.find((r) => r.id === data.poll?.activeRunId) ?? data.run
93
+ : data.run;
94
+ if (current) {
95
+ runId = current.id;
96
+ process.stderr.write(`\r${current.status} ${current.statusMessage ?? ""} `);
97
+ if (TERMINAL.has(current.status) && !data.poll?.active) {
98
+ process.stderr.write("\n");
99
+ printRun(current);
100
+ return current.status === "succeeded" ? EXIT_OK : EXIT_USAGE;
101
+ }
102
+ }
103
+ else {
104
+ console.log("No active run.");
105
+ return EXIT_OK;
106
+ }
107
+ }
108
+ await new Promise((r) => setTimeout(r, 2000));
109
+ }
110
+ console.error("Timed out waiting for run.");
111
+ return EXIT_USAGE;
112
+ }
113
+ async function cancelRun(args) {
114
+ const { slug, rest } = await resolveSlug(args);
115
+ const runId = rest[0];
116
+ if (!runId) {
117
+ console.error("Usage: frontpage runs cancel <runId> [--site <slug>]");
118
+ return EXIT_USAGE;
119
+ }
120
+ await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}/cancel`);
121
+ console.log(`Canceled ${runId}`);
122
+ return EXIT_OK;
123
+ }
124
+ async function retryRunCmd(args) {
125
+ const { slug, rest } = await resolveSlug(args);
126
+ const runId = rest[0];
127
+ if (!runId) {
128
+ console.error("Usage: frontpage runs retry <runId> [--site <slug>]");
129
+ return EXIT_USAGE;
130
+ }
131
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(runId)}/retry`);
132
+ console.log(data.runId);
133
+ return EXIT_OK;
134
+ }
@@ -0,0 +1 @@
1
+ export declare function runSearch(args: string[]): Promise<number>;
@@ -0,0 +1,30 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
3
+ export async function runSearch(args) {
4
+ try {
5
+ const { slug, rest } = await resolveSlug(args);
6
+ const pattern = rest[0];
7
+ if (!pattern) {
8
+ console.error("Usage: frontpage search <pattern> [--site <slug>]");
9
+ return EXIT_USAGE;
10
+ }
11
+ const q = new URLSearchParams({ pattern });
12
+ const pathFilter = rest[1];
13
+ if (pathFilter)
14
+ q.set("path", pathFilter);
15
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/search?${q.toString()}`);
16
+ const matches = data.matches ?? [];
17
+ for (const m of matches) {
18
+ console.log(`${m.file}:${m.lineNumber}: ${m.line}`);
19
+ }
20
+ if (matches.length === 0)
21
+ console.error("(no matches)");
22
+ if (data.truncated || data.scanTruncated)
23
+ console.error("# results truncated");
24
+ return EXIT_OK;
25
+ }
26
+ catch (err) {
27
+ console.error(err instanceof Error ? err.message : String(err));
28
+ return exitCodeForError(err);
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ export declare function runSeo(args: string[]): Promise<number>;
@@ -0,0 +1,57 @@
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;
10
+ }
11
+ export async function runSeo(args) {
12
+ try {
13
+ const { slug, rest } = await resolveSlug(args);
14
+ const sub = rest[0];
15
+ if (!sub || sub === "get") {
16
+ const pathArgs = rest[0] === "get" ? rest.slice(1) : rest;
17
+ const path = pathArgs[0] ?? "/";
18
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/seo?path=${encodeURIComponent(path)}`);
19
+ console.log(JSON.stringify(data, null, 2));
20
+ return EXIT_OK;
21
+ }
22
+ if (sub === "set") {
23
+ const work = rest.slice(1);
24
+ const path = takeFlag(work, "--path") ?? work.find((a) => a.startsWith("/")) ?? "/";
25
+ const body = { path };
26
+ for (const field of [
27
+ "title",
28
+ "description",
29
+ "ogImage",
30
+ "ogTitle",
31
+ "ogDescription",
32
+ "canonical",
33
+ ]) {
34
+ const v = takeFlag(work, `--${field}`);
35
+ if (v !== undefined)
36
+ body[field] = v;
37
+ }
38
+ const noindex = takeFlag(work, "--noindex");
39
+ if (noindex !== undefined) {
40
+ body.noindex = noindex === "true" || noindex === "1";
41
+ }
42
+ if (Object.keys(body).length <= 1) {
43
+ console.error("Usage: frontpage seo set --path / [--title …] [--description …] [--ogImage …] [--noindex true|false]");
44
+ return EXIT_USAGE;
45
+ }
46
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/seo`, body);
47
+ console.log(JSON.stringify(data, null, 2));
48
+ return EXIT_OK;
49
+ }
50
+ console.error("Usage: frontpage seo get|set … [--site <slug>]");
51
+ return EXIT_USAGE;
52
+ }
53
+ catch (err) {
54
+ console.error(err instanceof Error ? err.message : String(err));
55
+ return exitCodeForError(err);
56
+ }
57
+ }
@@ -0,0 +1 @@
1
+ export declare function runSettings(args: string[]): Promise<number>;
@@ -0,0 +1,65 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Patch } 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;
10
+ }
11
+ export async function runSettings(args) {
12
+ try {
13
+ const { slug, rest } = await resolveSlug(args);
14
+ const sub = rest[0] ?? "get";
15
+ if (sub === "get") {
16
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/settings`);
17
+ console.log(JSON.stringify(data, null, 2));
18
+ return EXIT_OK;
19
+ }
20
+ if (sub === "set") {
21
+ const work = rest.slice(1);
22
+ const body = {};
23
+ const subdomainPublic = takeFlag(work, "--subdomain-public");
24
+ if (subdomainPublic !== undefined) {
25
+ body.subdomainPublic = subdomainPublic === "true" || subdomainPublic === "1";
26
+ }
27
+ const usageAlerts = takeFlag(work, "--usage-alerts");
28
+ if (usageAlerts !== undefined) {
29
+ body.notificationPrefs = {
30
+ usageAlerts: usageAlerts === "true" || usageAlerts === "1",
31
+ };
32
+ }
33
+ const buildModel = takeFlag(work, "--build-model");
34
+ if (buildModel !== undefined)
35
+ body.buildModel = buildModel;
36
+ const brandName = takeFlag(work, "--brand-name");
37
+ const brandLogo = takeFlag(work, "--brand-logo");
38
+ if (work.includes("--brand-reset")) {
39
+ body.brand = null;
40
+ const i = work.indexOf("--brand-reset");
41
+ if (i !== -1)
42
+ work.splice(i, 1);
43
+ }
44
+ else if (brandName !== undefined || brandLogo !== undefined) {
45
+ body.brand = {
46
+ ...(brandName !== undefined ? { name: brandName } : {}),
47
+ ...(brandLogo !== undefined ? { logo: brandLogo } : {}),
48
+ };
49
+ }
50
+ if (Object.keys(body).length === 0) {
51
+ console.error("Usage: frontpage settings set [--subdomain-public true|false] [--usage-alerts true|false] [--build-model …] [--brand-name …] [--brand-logo …] [--brand-reset]");
52
+ return EXIT_USAGE;
53
+ }
54
+ const data = await v1Patch(`/api/v1/sites/${encodeURIComponent(slug)}/settings`, body);
55
+ console.log(JSON.stringify(data, null, 2));
56
+ return EXIT_OK;
57
+ }
58
+ console.error("Usage: frontpage settings get|set … [--site <slug>]");
59
+ return EXIT_USAGE;
60
+ }
61
+ catch (err) {
62
+ console.error(err instanceof Error ? err.message : String(err));
63
+ return exitCodeForError(err);
64
+ }
65
+ }
@@ -0,0 +1 @@
1
+ export declare function runSites(): Promise<number>;
@@ -0,0 +1,20 @@
1
+ import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
2
+ export async function runSites() {
3
+ try {
4
+ const data = await v1Get("/api/v1/sites");
5
+ if (data.sites.length === 0) {
6
+ console.log("No sites. Create a site in Frontpage first.");
7
+ return EXIT_OK;
8
+ }
9
+ console.log("Use the subdomain with `frontpage use <subdomain>` (not a custom domain).\n");
10
+ for (const s of data.sites) {
11
+ const label = s.customDomain ? `${s.name} · ${s.customDomain}` : s.name;
12
+ console.log(`${s.subdomain.padEnd(28)} ${s.role.padEnd(8)} ${label}`);
13
+ }
14
+ return EXIT_OK;
15
+ }
16
+ catch (err) {
17
+ console.error(err instanceof Error ? err.message : String(err));
18
+ return exitCodeForError(err);
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export declare function runStatus(args: string[]): Promise<number>;
@@ -0,0 +1,35 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, exitCodeForError, v1Get, apiBase } from "../http/client.js";
3
+ import { readState } from "../state/frontpage-state.js";
4
+ export async function runStatus(args) {
5
+ try {
6
+ const { slug } = await resolveSlug(args);
7
+ const state = await readState();
8
+ const [data, runs] = await Promise.all([
9
+ v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files`),
10
+ v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`).catch(() => ({
11
+ active: [],
12
+ })),
13
+ ]);
14
+ console.log(`API base: ${apiBase()}`);
15
+ console.log(`Site: ${slug}`);
16
+ console.log(`Local state: ${state.slug ?? "(none)"}${state.baseCommitSha ? ` @ ${state.baseCommitSha.slice(0, 7)}` : ""}`);
17
+ console.log(`Remote tip: ${data.baseCommitSha ?? "(unknown)"}`);
18
+ console.log(`Remote files: ${data.files.length}`);
19
+ if (runs.active?.length) {
20
+ console.log(`Active runs: ${runs.active.map((r) => `${r.status}:${r.id.slice(0, 8)}`).join(", ")}`);
21
+ console.log("Note: do not push while an agent run is active");
22
+ }
23
+ else {
24
+ console.log("Active runs: none");
25
+ }
26
+ if (state.baseCommitSha && data.baseCommitSha && state.baseCommitSha !== data.baseCommitSha) {
27
+ console.log("Note: remote tip differs from last pull — run frontpage pull");
28
+ }
29
+ return EXIT_OK;
30
+ }
31
+ catch (err) {
32
+ console.error(err instanceof Error ? err.message : String(err));
33
+ return exitCodeForError(err);
34
+ }
35
+ }
@@ -0,0 +1 @@
1
+ export declare function runUsage(args: string[]): Promise<number>;
@@ -0,0 +1,14 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
3
+ export async function runUsage(args) {
4
+ try {
5
+ const { slug } = await resolveSlug(args);
6
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/usage`);
7
+ console.log(JSON.stringify(data.usage, null, 2));
8
+ return EXIT_OK;
9
+ }
10
+ catch (err) {
11
+ console.error(err instanceof Error ? err.message : String(err));
12
+ return exitCodeForError(err);
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ export declare function runUse(args: string[]): Promise<number>;
@@ -0,0 +1,25 @@
1
+ import { assertSubdomainSlug, readState, writeState } from "../state/frontpage-state.js";
2
+ import { EXIT_OK, EXIT_USAGE } from "../http/client.js";
3
+ export async function runUse(args) {
4
+ const raw = args[0];
5
+ if (!raw) {
6
+ console.error("Usage: frontpage use <subdomain> (alias: frontpage link <subdomain>)");
7
+ return EXIT_USAGE;
8
+ }
9
+ try {
10
+ const slug = assertSubdomainSlug(raw);
11
+ const prev = await readState();
12
+ // Switching sites must drop the previous tip — a stale baseCommitSha would poison status/CAS.
13
+ const next = prev.slug === slug
14
+ ? { ...prev, slug }
15
+ : { slug };
16
+ await writeState(next);
17
+ console.log(`Using site subdomain: ${slug}`);
18
+ console.log("Wrote .frontpage/state.json");
19
+ return EXIT_OK;
20
+ }
21
+ catch (err) {
22
+ console.error(err instanceof Error ? err.message : String(err));
23
+ return EXIT_USAGE;
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export declare function runWhoami(): Promise<number>;
@@ -0,0 +1,14 @@
1
+ import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
2
+ export async function runWhoami() {
3
+ try {
4
+ 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}`);
8
+ return EXIT_OK;
9
+ }
10
+ catch (err) {
11
+ console.error(err instanceof Error ? err.message : String(err));
12
+ return exitCodeForError(err);
13
+ }
14
+ }
@@ -0,0 +1,4 @@
1
+ export declare function generateBashCompletion(): string;
2
+ export declare function generateZshCompletion(): string;
3
+ export declare function generateFishCompletion(): string;
4
+ export declare function generateCompletion(shell: string): string | null;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shell completion scripts generated from the command registry.
3
+ */
4
+ import { COMMANDS } from "../registry.js";
5
+ const NAMES = COMMANDS.map((c) => c.name).join(" ");
6
+ export function generateBashCompletion() {
7
+ return `# frontpage bash completion — eval "$(frontpage completion bash)"
8
+ _frontpage_completions() {
9
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
10
+ local cmds="${NAMES}"
11
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
12
+ COMPREPLY=( $(compgen -W "\${cmds}" -- "\${cur}") )
13
+ return
14
+ fi
15
+ local cmd="\${COMP_WORDS[1]}"
16
+ case "\${cmd}" in
17
+ help) COMPREPLY=( $(compgen -W "\${cmds} auth sync agent content history exit-codes" -- "\${cur}") ) ;;
18
+ completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") ) ;;
19
+ runs) COMPREPLY=( $(compgen -W "current wait cancel retry get" -- "\${cur}") ) ;;
20
+ deploy) COMPREPLY=( $(compgen -W "wait" -- "\${cur}") ) ;;
21
+ media) COMPREPLY=( $(compgen -W "list upload" -- "\${cur}") ) ;;
22
+ seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
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}") ) ;;
26
+ esac
27
+ }
28
+ complete -F _frontpage_completions frontpage
29
+ `;
30
+ }
31
+ export function generateZshCompletion() {
32
+ const describe = COMMANDS.map((c) => `'${c.name}:${c.summary.replace(/'/g, "'\\''")}'`).join(" ");
33
+ return `# frontpage zsh completion — eval "$(frontpage completion zsh)"
34
+ #compdef frontpage
35
+ _frontpage() {
36
+ local -a cmds
37
+ cmds=(${describe})
38
+ _arguments '1:command:->cmds' '*::arg:->args'
39
+ case $state in
40
+ cmds) _describe 'command' cmds ;;
41
+ args)
42
+ case $words[1] in
43
+ completion) _values 'shell' bash zsh fish ;;
44
+ runs) _values 'subcommand' current wait cancel retry get ;;
45
+ deploy) _values 'subcommand' wait ;;
46
+ media) _values 'subcommand' list upload ;;
47
+ seo|settings) _values 'subcommand' get set ;;
48
+ modules|extensions) _values 'subcommand' list enable disable config ;;
49
+ esac
50
+ ;;
51
+ esac
52
+ }
53
+ compdef _frontpage frontpage
54
+ `;
55
+ }
56
+ export function generateFishCompletion() {
57
+ const lines = [
58
+ `# frontpage fish completion — frontpage completion fish | source`,
59
+ `complete -c frontpage -f`,
60
+ ...COMMANDS.map((c) => `complete -c frontpage -n "__fish_use_subcommand" -a "${c.name}" -d "${c.summary.replace(/"/g, '\\"')}"`),
61
+ `complete -c frontpage -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"`,
62
+ `complete -c frontpage -n "__fish_seen_subcommand_from runs" -a "current wait cancel retry get"`,
63
+ `complete -c frontpage -n "__fish_seen_subcommand_from deploy" -a "wait"`,
64
+ `complete -c frontpage -n "__fish_seen_subcommand_from media" -a "list upload"`,
65
+ `complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
66
+ `complete -c frontpage -n "__fish_seen_subcommand_from modules extensions" -a "list enable disable config"`,
67
+ `complete -c frontpage -l site -d "Site subdomain slug"`,
68
+ ];
69
+ return `${lines.join("\n")}\n`;
70
+ }
71
+ export function generateCompletion(shell) {
72
+ switch (shell) {
73
+ case "bash":
74
+ return generateBashCompletion();
75
+ case "zsh":
76
+ return generateZshCompletion();
77
+ case "fish":
78
+ return generateFishCompletion();
79
+ default:
80
+ return null;
81
+ }
82
+ }