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.
- package/dist/auth/keychain.d.ts +5 -0
- package/dist/auth/keychain.js +85 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +124 -0
- package/dist/commands/analytics.d.ts +1 -0
- package/dist/commands/analytics.js +18 -0
- package/dist/commands/articles.d.ts +1 -0
- package/dist/commands/articles.js +111 -0
- package/dist/commands/cat.d.ts +1 -0
- package/dist/commands/cat.js +27 -0
- package/dist/commands/completion.d.ts +1 -0
- package/dist/commands/completion.js +19 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +32 -0
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +93 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +26 -0
- package/dist/commands/edit.d.ts +1 -0
- package/dist/commands/edit.js +31 -0
- package/dist/commands/extensions.d.ts +1 -0
- package/dist/commands/extensions.js +60 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +27 -0
- package/dist/commands/history.d.ts +1 -0
- package/dist/commands/history.js +49 -0
- package/dist/commands/inbox.d.ts +1 -0
- package/dist/commands/inbox.js +46 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +38 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +7 -0
- package/dist/commands/ls.d.ts +1 -0
- package/dist/commands/ls.js +19 -0
- package/dist/commands/media.d.ts +1 -0
- package/dist/commands/media.js +44 -0
- package/dist/commands/modules.d.ts +1 -0
- package/dist/commands/modules.js +56 -0
- package/dist/commands/open.d.ts +1 -0
- package/dist/commands/open.js +28 -0
- package/dist/commands/pages.d.ts +1 -0
- package/dist/commands/pages.js +16 -0
- package/dist/commands/prompt.d.ts +1 -0
- package/dist/commands/prompt.js +39 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +28 -0
- package/dist/commands/pull.d.ts +1 -0
- package/dist/commands/pull.js +60 -0
- package/dist/commands/push.d.ts +1 -0
- package/dist/commands/push.js +117 -0
- package/dist/commands/revert.d.ts +1 -0
- package/dist/commands/revert.js +27 -0
- package/dist/commands/runs.d.ts +1 -0
- package/dist/commands/runs.js +134 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +30 -0
- package/dist/commands/seo.d.ts +1 -0
- package/dist/commands/seo.js +57 -0
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +65 -0
- package/dist/commands/sites.d.ts +1 -0
- package/dist/commands/sites.js +20 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +35 -0
- package/dist/commands/usage.d.ts +1 -0
- package/dist/commands/usage.js +14 -0
- package/dist/commands/use.d.ts +1 -0
- package/dist/commands/use.js +25 -0
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +14 -0
- package/dist/completions/generate.d.ts +4 -0
- package/dist/completions/generate.js +82 -0
- package/dist/http/client.d.ts +27 -0
- package/dist/http/client.js +163 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +116 -0
- package/dist/site.d.ts +5 -0
- package/dist/site.js +32 -0
- package/dist/state/frontpage-state.d.ts +8 -0
- package/dist/state/frontpage-state.js +28 -0
- package/package.json +40 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const EXIT_OK = 0;
|
|
2
|
+
export declare const EXIT_USAGE = 2;
|
|
3
|
+
export declare const EXIT_CONFLICT = 3;
|
|
4
|
+
export declare const EXIT_AUTH = 4;
|
|
5
|
+
export declare const EXIT_UNEXPECTED = 5;
|
|
6
|
+
export declare class CliHttpError extends Error {
|
|
7
|
+
readonly exitCode: number;
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly code?: string;
|
|
10
|
+
constructor(message: string, exitCode: number, status: number, code?: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function apiBase(): string;
|
|
13
|
+
export type V1Envelope = {
|
|
14
|
+
error?: {
|
|
15
|
+
code?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
requestId?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function v1Get<T>(path: string): Promise<T>;
|
|
21
|
+
export declare function v1Post<T>(path: string, body?: unknown): Promise<T>;
|
|
22
|
+
export declare function v1Put<T>(path: string, body?: unknown): Promise<T>;
|
|
23
|
+
export declare function v1Delete<T>(path: string): Promise<T>;
|
|
24
|
+
export declare function v1Patch<T>(path: string, body?: unknown): Promise<T>;
|
|
25
|
+
/** Multipart POST (e.g. media upload). Does not set Content-Type — fetch sets the boundary. */
|
|
26
|
+
export declare function v1PostMultipart<T>(path: string, form: FormData): Promise<T>;
|
|
27
|
+
export declare function exitCodeForError(err: unknown): number;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP client for `/api/v1`.
|
|
3
|
+
* Exit: 0 ok · 2 rate-limit/budget/usage · 3 conflict · 4 auth · 5 other.
|
|
4
|
+
*/
|
|
5
|
+
import { getApiKey } from "../auth/keychain.js";
|
|
6
|
+
export const EXIT_OK = 0;
|
|
7
|
+
export const EXIT_USAGE = 2;
|
|
8
|
+
export const EXIT_CONFLICT = 3;
|
|
9
|
+
export const EXIT_AUTH = 4;
|
|
10
|
+
export const EXIT_UNEXPECTED = 5;
|
|
11
|
+
export class CliHttpError extends Error {
|
|
12
|
+
exitCode;
|
|
13
|
+
status;
|
|
14
|
+
code;
|
|
15
|
+
constructor(message, exitCode, status, code) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "CliHttpError";
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function apiBase() {
|
|
24
|
+
const fromEnv = process.env.FRONTPAGE_API_BASE?.replace(/\/$/, "");
|
|
25
|
+
if (fromEnv)
|
|
26
|
+
return fromEnv;
|
|
27
|
+
return "https://app.frontpage.host";
|
|
28
|
+
}
|
|
29
|
+
async function v1Request(method, path, body) {
|
|
30
|
+
const key = await getApiKey();
|
|
31
|
+
if (!key) {
|
|
32
|
+
throw new CliHttpError("Not logged in. Run: frontpage login", EXIT_AUTH, 401);
|
|
33
|
+
}
|
|
34
|
+
const url = `${apiBase()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
35
|
+
const headers = {
|
|
36
|
+
Authorization: `Bearer ${key}`,
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
};
|
|
39
|
+
let payload;
|
|
40
|
+
if (body !== undefined) {
|
|
41
|
+
headers["Content-Type"] = "application/json";
|
|
42
|
+
payload = JSON.stringify(body);
|
|
43
|
+
}
|
|
44
|
+
let res;
|
|
45
|
+
try {
|
|
46
|
+
res = await fetch(url, { method, headers, body: payload });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new CliHttpError(`Network error: ${err instanceof Error ? err.message : String(err)}`, EXIT_UNEXPECTED, 0);
|
|
50
|
+
}
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
let parsed = {};
|
|
53
|
+
try {
|
|
54
|
+
parsed = text ? JSON.parse(text) : {};
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
parsed = {};
|
|
58
|
+
}
|
|
59
|
+
if (res.ok)
|
|
60
|
+
return parsed;
|
|
61
|
+
const message = parsed.error?.message ?? `Request failed (${res.status})`;
|
|
62
|
+
const code = parsed.error?.code;
|
|
63
|
+
if (res.status === 401 || res.status === 403) {
|
|
64
|
+
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
65
|
+
}
|
|
66
|
+
if (res.status === 402 || res.status === 429 || code === "rate_limited" || code === "payment_required") {
|
|
67
|
+
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
68
|
+
}
|
|
69
|
+
if (res.status === 409 || code === "conflict") {
|
|
70
|
+
throw new CliHttpError(message, EXIT_CONFLICT, res.status, code);
|
|
71
|
+
}
|
|
72
|
+
if (res.status === 413 || code === "payload_too_large") {
|
|
73
|
+
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
74
|
+
}
|
|
75
|
+
if (res.status === 503 || code === "unavailable") {
|
|
76
|
+
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
77
|
+
}
|
|
78
|
+
if (res.status === 404) {
|
|
79
|
+
const lower = message.toLowerCase();
|
|
80
|
+
if (lower === "not found." || lower === "not found") {
|
|
81
|
+
throw new CliHttpError("API not available (is PUBLIC_CLI_ENABLED on for this environment?).", EXIT_UNEXPECTED, res.status, code);
|
|
82
|
+
}
|
|
83
|
+
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
84
|
+
}
|
|
85
|
+
throw new CliHttpError(message, EXIT_UNEXPECTED, res.status, code);
|
|
86
|
+
}
|
|
87
|
+
export async function v1Get(path) {
|
|
88
|
+
return v1Request("GET", path);
|
|
89
|
+
}
|
|
90
|
+
export async function v1Post(path, body) {
|
|
91
|
+
return v1Request("POST", path, body ?? {});
|
|
92
|
+
}
|
|
93
|
+
export async function v1Put(path, body) {
|
|
94
|
+
return v1Request("PUT", path, body ?? {});
|
|
95
|
+
}
|
|
96
|
+
export async function v1Delete(path) {
|
|
97
|
+
return v1Request("DELETE", path);
|
|
98
|
+
}
|
|
99
|
+
export async function v1Patch(path, body) {
|
|
100
|
+
return v1Request("PATCH", path, body ?? {});
|
|
101
|
+
}
|
|
102
|
+
/** Multipart POST (e.g. media upload). Does not set Content-Type — fetch sets the boundary. */
|
|
103
|
+
export async function v1PostMultipart(path, form) {
|
|
104
|
+
const key = await getApiKey();
|
|
105
|
+
if (!key) {
|
|
106
|
+
throw new CliHttpError("Not logged in. Run: frontpage login", EXIT_AUTH, 401);
|
|
107
|
+
}
|
|
108
|
+
const url = `${apiBase()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
109
|
+
let res;
|
|
110
|
+
try {
|
|
111
|
+
res = await fetch(url, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${key}`,
|
|
115
|
+
Accept: "application/json",
|
|
116
|
+
},
|
|
117
|
+
body: form,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw new CliHttpError(`Network error: ${err instanceof Error ? err.message : String(err)}`, EXIT_UNEXPECTED, 0);
|
|
122
|
+
}
|
|
123
|
+
const text = await res.text();
|
|
124
|
+
let parsed = {};
|
|
125
|
+
try {
|
|
126
|
+
parsed = text ? JSON.parse(text) : {};
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
parsed = {};
|
|
130
|
+
}
|
|
131
|
+
if (res.ok)
|
|
132
|
+
return parsed;
|
|
133
|
+
const message = parsed.error?.message ?? `Request failed (${res.status})`;
|
|
134
|
+
const code = parsed.error?.code;
|
|
135
|
+
if (res.status === 401 || res.status === 403) {
|
|
136
|
+
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
137
|
+
}
|
|
138
|
+
if (res.status === 402 || res.status === 429 || code === "rate_limited" || code === "payment_required") {
|
|
139
|
+
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
140
|
+
}
|
|
141
|
+
if (res.status === 409 || code === "conflict") {
|
|
142
|
+
throw new CliHttpError(message, EXIT_CONFLICT, res.status, code);
|
|
143
|
+
}
|
|
144
|
+
if (res.status === 413 || code === "payload_too_large") {
|
|
145
|
+
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
146
|
+
}
|
|
147
|
+
if (res.status === 503 || code === "unavailable") {
|
|
148
|
+
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
149
|
+
}
|
|
150
|
+
if (res.status === 404) {
|
|
151
|
+
const lower = message.toLowerCase();
|
|
152
|
+
if (lower === "not found." || lower === "not found") {
|
|
153
|
+
throw new CliHttpError("API not available (is PUBLIC_CLI_ENABLED on for this environment?).", EXIT_UNEXPECTED, res.status, code);
|
|
154
|
+
}
|
|
155
|
+
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
156
|
+
}
|
|
157
|
+
throw new CliHttpError(message, EXIT_UNEXPECTED, res.status, code);
|
|
158
|
+
}
|
|
159
|
+
export function exitCodeForError(err) {
|
|
160
|
+
if (err instanceof CliHttpError)
|
|
161
|
+
return err.exitCode;
|
|
162
|
+
return EXIT_UNEXPECTED;
|
|
163
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command registry — single source of truth for help + dispatch.
|
|
3
|
+
* Only shipped Phase 0–5 commands appear here.
|
|
4
|
+
*/
|
|
5
|
+
export type CommandDef = {
|
|
6
|
+
name: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
usage?: string;
|
|
9
|
+
/** Topic tags for `help <topic>`. */
|
|
10
|
+
topics?: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare const COMMANDS: readonly CommandDef[];
|
|
13
|
+
export declare const TOPICS: Record<string, string>;
|
|
14
|
+
export declare function findCommand(name: string): CommandDef | undefined;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command registry — single source of truth for help + dispatch.
|
|
3
|
+
* Only shipped Phase 0–5 commands appear here.
|
|
4
|
+
*/
|
|
5
|
+
export const COMMANDS = [
|
|
6
|
+
{ name: "help", summary: "Show available commands", usage: "frontpage help [command|topic]", topics: ["auth", "sync", "agent", "content", "history", "exit-codes"] },
|
|
7
|
+
{ name: "login", summary: "Save an API key to the OS keychain", usage: "frontpage login [api-key]", topics: ["auth"] },
|
|
8
|
+
{ name: "logout", summary: "Remove the saved API key", usage: "frontpage logout", topics: ["auth"] },
|
|
9
|
+
{ name: "whoami", summary: "Show the identity for the current API key", usage: "frontpage whoami", topics: ["auth"] },
|
|
10
|
+
{ name: "sites", summary: "List sites your key can access", usage: "frontpage sites", topics: ["auth"] },
|
|
11
|
+
{ name: "use", summary: "Set the default site subdomain for this folder", usage: "frontpage use <subdomain>", topics: ["auth", "sync"] },
|
|
12
|
+
{ name: "link", summary: "Alias for use", usage: "frontpage link <subdomain>", topics: ["auth", "sync"] },
|
|
13
|
+
{ name: "doctor", summary: "Check CLI auth and API connectivity", usage: "frontpage doctor", topics: ["auth"] },
|
|
14
|
+
{ name: "pull", summary: "Download allowlisted site files into this folder", usage: "frontpage pull [--site <slug>]", topics: ["sync"] },
|
|
15
|
+
{ name: "push", summary: "Upload local allowlisted files (CAS + active-run guard)", usage: "frontpage push [--site <slug>] [--message …]", topics: ["sync"] },
|
|
16
|
+
{ name: "diff", summary: "Compare local allowlisted files to the remote tip", usage: "frontpage diff [--site <slug>]", topics: ["sync"] },
|
|
17
|
+
{ name: "ls", summary: "List remote allowlisted files", usage: "frontpage ls [prefix] [--site <slug>]", topics: ["sync"] },
|
|
18
|
+
{ name: "cat", summary: "Print a remote file", usage: "frontpage cat <path> [--site <slug>]", topics: ["sync"] },
|
|
19
|
+
{ name: "search", summary: "Search remote text files", usage: "frontpage search <pattern> [path] [--site <slug>]", topics: ["sync"] },
|
|
20
|
+
{ name: "status", summary: "Show linked site and tip vs last pull", usage: "frontpage status [--site <slug>]", topics: ["sync"] },
|
|
21
|
+
{ name: "open", summary: "Open the site in a browser", usage: "frontpage open [--site <slug>]", topics: ["sync"] },
|
|
22
|
+
{ name: "prompt", summary: "Enqueue an agent build", usage: 'frontpage prompt "..." [--site <slug>] [--path /]', topics: ["agent"] },
|
|
23
|
+
{ name: "runs", summary: "List, wait, cancel, or retry agent runs", usage: "frontpage runs [current|wait|cancel|retry|get] …", topics: ["agent"] },
|
|
24
|
+
{ name: "publish", summary: "Promote staging to production", usage: "frontpage publish [--site <slug>]", topics: ["agent"] },
|
|
25
|
+
{ name: "unpublish", summary: "Take the live site offline (admin)", usage: "frontpage unpublish [--site <slug>]", topics: ["agent"] },
|
|
26
|
+
{ name: "deploy", summary: "Wait for active agent deploy to finish", usage: "frontpage deploy wait [--site <slug>]", topics: ["agent"] },
|
|
27
|
+
{ name: "usage", summary: "Show plan and AI/bandwidth usage", usage: "frontpage usage [--site <slug>]", topics: ["agent"] },
|
|
28
|
+
{ name: "analytics", summary: "Show analytics dashboard JSON", usage: "frontpage analytics [--days 7] [--site <slug>]", topics: ["agent"] },
|
|
29
|
+
{ name: "pages", summary: "List site pages", usage: "frontpage pages [--site <slug>]", topics: ["agent"] },
|
|
30
|
+
{ name: "modules", summary: "List or mutate modules (secret masks only)", usage: "frontpage modules [list|enable|disable|config] …", topics: ["agent", "content"] },
|
|
31
|
+
{ name: "extensions", summary: "List or mutate extensions (no secret values)", usage: "frontpage extensions [list|enable|disable|config] …", topics: ["agent", "content"] },
|
|
32
|
+
{ name: "media", summary: "List or upload site media assets", usage: "frontpage media list|upload <file>", topics: ["content"] },
|
|
33
|
+
{ name: "inbox", summary: "List leads / subscribers / orders", usage: "frontpage inbox [leads|…] [--mark-read] [--csv]", topics: ["content"] },
|
|
34
|
+
{ name: "seo", summary: "Get or set page SEO", usage: "frontpage seo get|set --path / …", topics: ["content"] },
|
|
35
|
+
{ name: "articles", summary: "Articles hub, content, and section ops", usage: "frontpage articles list|content|create-article|…", topics: ["content"] },
|
|
36
|
+
{ name: "settings", summary: "Get or set site settings (admin)", usage: "frontpage settings get|set …", topics: ["content"] },
|
|
37
|
+
{ name: "edit", summary: "Click-edit style exact text replace + deploy", usage: 'frontpage edit --path / --old "…" --new "…"', topics: ["content"] },
|
|
38
|
+
{ name: "history", summary: "List recent commits on the site branch", usage: "frontpage history [--limit N] [--page N]", topics: ["history", "agent"] },
|
|
39
|
+
{ name: "revert", summary: "Restore site to a prior commit (tree restore + deploy)", usage: "frontpage revert <40-char-sha>", topics: ["history"] },
|
|
40
|
+
{ name: "completion", summary: "Print shell completion script", usage: "frontpage completion bash|zsh|fish", topics: ["auth"] },
|
|
41
|
+
];
|
|
42
|
+
export const TOPICS = {
|
|
43
|
+
auth: `Authentication
|
|
44
|
+
1. Create a key in the Frontpage editor: profile menu → Developers
|
|
45
|
+
2. frontpage login
|
|
46
|
+
3. frontpage sites
|
|
47
|
+
4. frontpage use <subdomain> (e.g. asimov-press, not a custom domain)
|
|
48
|
+
|
|
49
|
+
Keys act as you on every site you can access. Treat them like passwords.
|
|
50
|
+
Override with FRONTPAGE_API_KEY in CI.`,
|
|
51
|
+
sync: `File sync
|
|
52
|
+
frontpage use <subdomain>
|
|
53
|
+
frontpage pull # downloads src/, public/, content/ into cwd
|
|
54
|
+
frontpage diff # local vs remote tip
|
|
55
|
+
frontpage push [--message …] # uploads allowlisted files; CAS + active-run guard
|
|
56
|
+
frontpage ls [prefix]
|
|
57
|
+
frontpage cat <path>
|
|
58
|
+
frontpage search <pattern>
|
|
59
|
+
frontpage status
|
|
60
|
+
frontpage open
|
|
61
|
+
|
|
62
|
+
Local state: .frontpage/state.json { slug, baseCommitSha }
|
|
63
|
+
Pull first so push can compare-and-swap on baseCommitSha.
|
|
64
|
+
Do not push while the editor agent is building (status / runs show active work).
|
|
65
|
+
Pushing module markup may auto-activate modules (same as the agent).
|
|
66
|
+
tokens.ts may be auto-repaired at commit if it would break the build.
|
|
67
|
+
Binaries use base64 (not utf8-corrupted).`,
|
|
68
|
+
agent: `Agent + publish
|
|
69
|
+
frontpage prompt "make the hero shorter"
|
|
70
|
+
frontpage runs # latest + active
|
|
71
|
+
frontpage runs wait [runId]
|
|
72
|
+
frontpage runs cancel <runId>
|
|
73
|
+
frontpage runs retry <runId>
|
|
74
|
+
frontpage deploy wait
|
|
75
|
+
frontpage publish
|
|
76
|
+
frontpage unpublish # admin only
|
|
77
|
+
frontpage usage
|
|
78
|
+
frontpage analytics [--days 7]
|
|
79
|
+
frontpage pages
|
|
80
|
+
frontpage modules
|
|
81
|
+
frontpage extensions
|
|
82
|
+
|
|
83
|
+
Mid-task continuation is automatic — there is no runs continue.
|
|
84
|
+
Do not push while an agent run is active.`,
|
|
85
|
+
content: `Content
|
|
86
|
+
frontpage media list|upload <file>
|
|
87
|
+
frontpage inbox [leads|subscribers|…] [--mark-read] [--csv]
|
|
88
|
+
frontpage seo get|set --path /
|
|
89
|
+
frontpage articles list|content|create-section|create-article|set-draft|…
|
|
90
|
+
frontpage modules enable|disable|config <key>
|
|
91
|
+
frontpage extensions enable|disable|config <key>
|
|
92
|
+
frontpage settings get|set
|
|
93
|
+
frontpage edit --path / --old "…" --new "…"
|
|
94
|
+
|
|
95
|
+
edit uses click-edit (exact single occurrence), not push / gated-write.
|
|
96
|
+
Module OAuth and secret values are not exposed via the CLI.
|
|
97
|
+
Domains, billing, members, and site delete are out of v1.`,
|
|
98
|
+
history: `History
|
|
99
|
+
frontpage history [--limit 20] [--page 1]
|
|
100
|
+
frontpage revert <40-char-sha>
|
|
101
|
+
|
|
102
|
+
history lists commits on site/<slug> (plan Time Machine window).
|
|
103
|
+
S = staging tip, P = production tip.
|
|
104
|
+
revert creates a NEW commit whose tree matches the target (linear restore).
|
|
105
|
+
Target SHA must be on this site's history (not another site in the monorepo).
|
|
106
|
+
Refused while an agent run is active. Goes through commitSiteBranch (build gate).`,
|
|
107
|
+
"exit-codes": `Exit codes
|
|
108
|
+
0 success
|
|
109
|
+
2 usage / budget / rate-limit / unavailable
|
|
110
|
+
3 conflict (CAS / active run / unreachable domain / cancel-while-deploying)
|
|
111
|
+
4 authentication or authorization failure
|
|
112
|
+
5 unexpected server or network error`,
|
|
113
|
+
};
|
|
114
|
+
export function findCommand(name) {
|
|
115
|
+
return COMMANDS.find((c) => c.name === name);
|
|
116
|
+
}
|
package/dist/site.d.ts
ADDED
package/dist/site.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { assertSubdomainSlug, readState } from "./state/frontpage-state.js";
|
|
2
|
+
import { CliHttpError, EXIT_USAGE } from "./http/client.js";
|
|
3
|
+
/** Resolve --site flag or .frontpage/state.json slug. */
|
|
4
|
+
export async function resolveSlug(args) {
|
|
5
|
+
const rest = [];
|
|
6
|
+
let siteFlag;
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const a = args[i];
|
|
9
|
+
if (a === "--site" || a === "-s") {
|
|
10
|
+
siteFlag = args[++i];
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (a.startsWith("--site=")) {
|
|
14
|
+
siteFlag = a.slice("--site=".length);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
rest.push(a);
|
|
18
|
+
}
|
|
19
|
+
if (siteFlag) {
|
|
20
|
+
try {
|
|
21
|
+
return { slug: assertSubdomainSlug(siteFlag), rest };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
throw new CliHttpError(err instanceof Error ? err.message : String(err), EXIT_USAGE, 400);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const state = await readState();
|
|
28
|
+
if (!state.slug) {
|
|
29
|
+
throw new CliHttpError("No site selected. Run: frontpage use <subdomain> or pass --site <subdomain>", EXIT_USAGE, 400);
|
|
30
|
+
}
|
|
31
|
+
return { slug: state.slug, rest };
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type FrontpageState = {
|
|
2
|
+
slug?: string;
|
|
3
|
+
/** Tip SHA from the last successful pull (Phase 3 CAS will use this). */
|
|
4
|
+
baseCommitSha?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function assertSubdomainSlug(raw: string): string;
|
|
7
|
+
export declare function readState(cwd?: string): Promise<FrontpageState>;
|
|
8
|
+
export declare function writeState(state: FrontpageState, cwd?: string): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
4
|
+
export function assertSubdomainSlug(raw) {
|
|
5
|
+
const slug = raw.trim().toLowerCase();
|
|
6
|
+
if (!slug || slug.includes(".") || slug.includes("/") || !SLUG_PATTERN.test(slug)) {
|
|
7
|
+
throw new Error("Site must be a subdomain slug (e.g. asimov-press), not a domain or URL.");
|
|
8
|
+
}
|
|
9
|
+
return slug;
|
|
10
|
+
}
|
|
11
|
+
function statePath(cwd = process.cwd()) {
|
|
12
|
+
return join(cwd, ".frontpage", "state.json");
|
|
13
|
+
}
|
|
14
|
+
export async function readState(cwd = process.cwd()) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readFile(statePath(cwd), "utf8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function writeState(state, cwd = process.cwd()) {
|
|
25
|
+
const dir = join(cwd, ".frontpage");
|
|
26
|
+
await mkdir(dir, { recursive: true });
|
|
27
|
+
await writeFile(statePath(cwd), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontpage-host",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Frontpage CLI — manage your site from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"frontpage": "dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"dev": "tsx src/bin.ts",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"frontpage": "tsx src/bin.ts",
|
|
17
|
+
"prepublishOnly": "npm test && npm run build"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/mkmccarty3/frontpage-app.git",
|
|
26
|
+
"directory": "cli"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://frontpage.host",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"frontpage",
|
|
31
|
+
"cli",
|
|
32
|
+
"landing-page"
|
|
33
|
+
],
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.13.10",
|
|
36
|
+
"tsx": "^4.19.3",
|
|
37
|
+
"typescript": "^5.8.2",
|
|
38
|
+
"vitest": "^3.0.9"
|
|
39
|
+
}
|
|
40
|
+
}
|