frontpage-host 0.1.9 → 0.1.11
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 +6 -4
- package/dist/argv.d.ts +12 -0
- package/dist/argv.js +48 -0
- package/dist/bin.js +5 -0
- package/dist/commands/analytics.d.ts +7 -0
- package/dist/commands/analytics.js +68 -16
- package/dist/commands/help.js +1 -1
- package/dist/commands/media.d.ts +3 -0
- package/dist/commands/media.js +48 -7
- package/dist/commands/modules.js +6 -8
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +40 -6
- package/dist/commands/search.js +14 -18
- package/dist/commands/seo.js +28 -13
- package/dist/commands/version.d.ts +1 -0
- package/dist/commands/version.js +13 -0
- package/dist/flags.d.ts +3 -1
- package/dist/flags.js +7 -2
- package/dist/media/upload.js +2 -1
- package/dist/registry.js +50 -14
- package/dist/version.d.ts +7 -0
- package/dist/version.js +15 -0
- package/llms.txt +7 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -29,15 +29,16 @@ Frontpage has two surfaces:
|
|
|
29
29
|
**How changes become visible on staging**
|
|
30
30
|
|
|
31
31
|
- `frontpage push`, `frontpage prompt`, and `frontpage edit` update the site branch and the **staging** surface.
|
|
32
|
-
- After a push or agent run finishes building, changes are
|
|
32
|
+
- After a push or agent run finishes building, changes are on the site branch behind `https://<subdomain>.frontpage.host`.
|
|
33
33
|
- There is **no separate “deploy to staging” step** agents must run. Do not treat `frontpage deploy wait` as “going live.” At most it waits for an in-flight build to finish before you open the subdomain to review.
|
|
34
34
|
- `frontpage open` opens the subdomain staging URL.
|
|
35
|
+
- **Custom-domain caveat:** `<subdomain>.frontpage.host` is publicly viewable (anonymous) only when the site has **no** custom domain and subdomain sharing is public. If a custom domain is connected (e.g. `arbiter.wiki`), the subdomain is often **owner-private** — an unauthenticated browser/curl may see the app login page. That login wall is **not** a failed push. Anonymous public review is then on the **published custom domain**; editing the subdomain still needs an owner browser session (CLI API key ≠ browser cookie).
|
|
35
36
|
|
|
36
37
|
**Production publish is a deliberate promote**
|
|
37
38
|
|
|
38
39
|
- `frontpage publish` copies the current **staging tip** to **production** (custom domain / published site).
|
|
39
40
|
- **Never run `publish` or `unpublish` unless the user explicitly asked**, or the user’s own automation/workflow clearly requires autonomous publishing.
|
|
40
|
-
- Default: finish work on staging, tell the user to review
|
|
41
|
+
- Default: finish work on staging, tell the user to review the staging subdomain when it is publicly shareable (see custom-domain caveat above), and let **them** publish (or ask you to publish after they approve).
|
|
41
42
|
|
|
42
43
|
## Non-interactive auth
|
|
43
44
|
|
|
@@ -112,7 +113,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
112
113
|
- Articles: `frontpage articles …` for lifecycle; bodies via sync under `content/` or `articles content get|set`. Section landing is an owned `src/pages/<section>/index.astro`.
|
|
113
114
|
- Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
|
|
114
115
|
- Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
|
|
115
|
-
- Media: `frontpage media upload <file>` — images/PDF ≤25 MB, video (MP4/WebM/MOV) ≤50 MB, SVG/text ≤4 MB (sanitizing proxy).
|
|
116
|
+
- Media: `frontpage media upload <file>` — images/PDF ≤25 MB, video (MP4/WebM/MOV) ≤50 MB, SVG/text ≤4 MB (sanitizing proxy). Human output prints `sitePath:` first (use that in markup — never raw blob hosts), then optional `url:`. Optional `?w=` on rasters only. **Text/CSV** uploads are inline only — **not** added to the media library and **cannot** be linked via `/__media` (`media upload --json` omits full `content`; use `inbox … --csv` for submission exports).
|
|
116
117
|
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
117
118
|
|
|
118
119
|
## Agent prompt loop
|
|
@@ -180,7 +181,7 @@ Global machine envelope (`--json`):
|
|
|
180
181
|
{ "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3, "requestId": "…" } }
|
|
181
182
|
```
|
|
182
183
|
|
|
183
|
-
Failures with `--json` print the envelope on **stdout** (still use the process exit code). Include `requestId` when
|
|
184
|
+
Failures with `--json` print the envelope on **stdout** (still use the process exit code). Include `requestId` when the API returned one (local validation errors omit `requestId` and `httpStatus`).
|
|
184
185
|
|
|
185
186
|
## Global flags
|
|
186
187
|
|
|
@@ -191,6 +192,7 @@ Failures with `--json` print the envelope on **stdout** (still use the process e
|
|
|
191
192
|
| `--site <slug>` | Subdomain override (most site commands) |
|
|
192
193
|
| `--yes` | Confirm destructive ops (`unpublish`, `revert`) when non-TTY |
|
|
193
194
|
| `--help` / `-h` | Per-command help (alias of `frontpage help <cmd>`) |
|
|
195
|
+
| `--version` / `-V` | Print `frontpage-host <semver>` (also `frontpage version`) |
|
|
194
196
|
|
|
195
197
|
## Destructive ops — defaults for agents
|
|
196
198
|
|
package/dist/argv.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Remove `--name value` (or `--name=value`) from rest; return the value. */
|
|
2
|
+
export declare function takeFlag(rest: string[], name: string): string | undefined;
|
|
3
|
+
/** Remove a boolean `--name` flag from rest; return whether it was present. */
|
|
4
|
+
export declare function hasFlag(rest: string[], name: string): boolean;
|
|
5
|
+
/** True when a token looks like a flag (not a negative number). */
|
|
6
|
+
export declare function looksLikeFlag(token: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* After a command has consumed its known options, fail if any flag-like
|
|
9
|
+
* tokens remain. Positionals (non-flags) are allowed leftovers unless the
|
|
10
|
+
* caller also checks them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertNoUnknownFlags(rest: string[], command: string): void;
|
package/dist/argv.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared argv helpers for command parsers (CLI Agent UX R1–R3).
|
|
3
|
+
*/
|
|
4
|
+
import { CliHttpError, EXIT_USAGE } from "./http/client.js";
|
|
5
|
+
/** Remove `--name value` (or `--name=value`) from rest; return the value. */
|
|
6
|
+
export function takeFlag(rest, name) {
|
|
7
|
+
const eq = `${name}=`;
|
|
8
|
+
for (let i = 0; i < rest.length; i++) {
|
|
9
|
+
const a = rest[i];
|
|
10
|
+
if (a === name) {
|
|
11
|
+
const v = rest[i + 1];
|
|
12
|
+
rest.splice(i, 2);
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
if (a.startsWith(eq)) {
|
|
16
|
+
rest.splice(i, 1);
|
|
17
|
+
return a.slice(eq.length);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Remove a boolean `--name` flag from rest; return whether it was present. */
|
|
23
|
+
export function hasFlag(rest, name) {
|
|
24
|
+
const i = rest.indexOf(name);
|
|
25
|
+
if (i === -1)
|
|
26
|
+
return false;
|
|
27
|
+
rest.splice(i, 1);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
/** True when a token looks like a flag (not a negative number). */
|
|
31
|
+
export function looksLikeFlag(token) {
|
|
32
|
+
if (token === "-" || token === "--")
|
|
33
|
+
return true;
|
|
34
|
+
if (/^-\d/.test(token))
|
|
35
|
+
return false; // allow negative numbers as values
|
|
36
|
+
return token.startsWith("-");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* After a command has consumed its known options, fail if any flag-like
|
|
40
|
+
* tokens remain. Positionals (non-flags) are allowed leftovers unless the
|
|
41
|
+
* caller also checks them.
|
|
42
|
+
*/
|
|
43
|
+
export function assertNoUnknownFlags(rest, command) {
|
|
44
|
+
const unknown = rest.find(looksLikeFlag);
|
|
45
|
+
if (!unknown)
|
|
46
|
+
return;
|
|
47
|
+
throw new CliHttpError(`Unknown flag: ${unknown}. Run: frontpage help ${command}`, EXIT_USAGE, 0, "bad_request");
|
|
48
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -34,6 +34,7 @@ import { runTokens } from "./commands/tokens.js";
|
|
|
34
34
|
import { runHistory } from "./commands/history.js";
|
|
35
35
|
import { runRevert } from "./commands/revert.js";
|
|
36
36
|
import { runCompletion } from "./commands/completion.js";
|
|
37
|
+
import { runVersion } from "./commands/version.js";
|
|
37
38
|
import { EXIT_USAGE, EXIT_UNEXPECTED } from "./http/client.js";
|
|
38
39
|
import { parseGlobalFlags } from "./flags.js";
|
|
39
40
|
import { setRuntimeFlags, isJson } from "./runtime.js";
|
|
@@ -43,6 +44,10 @@ async function main() {
|
|
|
43
44
|
const argv = parsed.args;
|
|
44
45
|
const cmd = argv[0];
|
|
45
46
|
const args = argv.slice(1);
|
|
47
|
+
// Global version before help/command dispatch (R6).
|
|
48
|
+
if (parsed.wantsVersion || cmd === "version") {
|
|
49
|
+
return runVersion();
|
|
50
|
+
}
|
|
46
51
|
// Global or per-command help aliases (before site resolution).
|
|
47
52
|
if (!cmd || cmd === "help") {
|
|
48
53
|
return runHelp(cmd === "help" ? args : []);
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funnel --steps: JSON array (canonical) or comma-separated site paths (R9).
|
|
3
|
+
* Example shorthand: `--steps /, /problems, /updates`
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseFunnelSteps(raw: string): Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
}>;
|
|
1
8
|
export declare function runAnalytics(args: string[]): Promise<number>;
|
|
@@ -1,24 +1,48 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
|
|
2
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, takeFlag } from "../argv.js";
|
|
3
4
|
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
4
|
-
function takeFlag(rest, name) {
|
|
5
|
-
const i = rest.indexOf(name);
|
|
6
|
-
if (i === -1)
|
|
7
|
-
return undefined;
|
|
8
|
-
const v = rest[i + 1];
|
|
9
|
-
rest.splice(i, 2);
|
|
10
|
-
return v;
|
|
11
|
-
}
|
|
12
5
|
function parseJsonFlag(raw, label) {
|
|
13
6
|
if (raw === undefined)
|
|
14
7
|
return undefined;
|
|
15
8
|
try {
|
|
16
9
|
return JSON.parse(raw);
|
|
17
10
|
}
|
|
18
|
-
catch {
|
|
19
|
-
|
|
11
|
+
catch (err) {
|
|
12
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
13
|
+
throw new CliHttpError(`Invalid JSON for ${label}: ${detail}`, EXIT_USAGE, 0, "bad_request");
|
|
20
14
|
}
|
|
21
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Funnel --steps: JSON array (canonical) or comma-separated site paths (R9).
|
|
18
|
+
* Example shorthand: `--steps /, /problems, /updates`
|
|
19
|
+
*/
|
|
20
|
+
export function parseFunnelSteps(raw) {
|
|
21
|
+
const trimmed = raw.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
throw new CliHttpError(`Usage: frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]' or --steps /, /pricing`, EXIT_USAGE, 0, "bad_request");
|
|
24
|
+
}
|
|
25
|
+
if (trimmed.startsWith("[")) {
|
|
26
|
+
const parsed = parseJsonFlag(trimmed, "--steps");
|
|
27
|
+
if (!Array.isArray(parsed)) {
|
|
28
|
+
throw new CliHttpError("Invalid JSON for --steps: expected an array of { path } objects", EXIT_USAGE, 0, "bad_request");
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
const paths = trimmed
|
|
33
|
+
.split(",")
|
|
34
|
+
.map((s) => s.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
if (paths.length === 0) {
|
|
37
|
+
throw new CliHttpError("Invalid --steps: provide a JSON array or comma-separated paths like /, /pricing", EXIT_USAGE, 0, "bad_request");
|
|
38
|
+
}
|
|
39
|
+
for (const path of paths) {
|
|
40
|
+
if (!path.startsWith("/") || path.startsWith("//")) {
|
|
41
|
+
throw new CliHttpError(`Invalid funnel step path "${path}" — each step must be a site-relative path starting with /`, EXIT_USAGE, 0, "bad_request");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return paths.map((path) => ({ path }));
|
|
45
|
+
}
|
|
22
46
|
function rangeFields(work) {
|
|
23
47
|
const days = takeFlag(work, "--days");
|
|
24
48
|
const from = takeFlag(work, "--from");
|
|
@@ -35,6 +59,12 @@ function rangeFields(work) {
|
|
|
35
59
|
function printJson(data) {
|
|
36
60
|
console.log(JSON.stringify(data, null, 2));
|
|
37
61
|
}
|
|
62
|
+
function finishWork(work) {
|
|
63
|
+
assertNoUnknownFlags(work, "analytics");
|
|
64
|
+
if (work.length > 0) {
|
|
65
|
+
throw new CliHttpError(`Unexpected argument: ${work[0]}. Run: frontpage help analytics`, EXIT_USAGE, 0, "bad_request");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
38
68
|
export async function runAnalytics(args) {
|
|
39
69
|
try {
|
|
40
70
|
const { slug, rest } = await resolveSlug(args);
|
|
@@ -42,10 +72,11 @@ export async function runAnalytics(args) {
|
|
|
42
72
|
const sub = work[0];
|
|
43
73
|
const ops = new Set(["summary", "query", "funnel", "timeseries", "compare", "sql", "dashboard"]);
|
|
44
74
|
// Bare `analytics` or `analytics dashboard` → legacy GET dashboard (compat).
|
|
45
|
-
if (!sub || sub === "dashboard" || !ops.has(sub) && sub.startsWith("--")) {
|
|
75
|
+
if (!sub || sub === "dashboard" || (!ops.has(sub) && sub.startsWith("--"))) {
|
|
46
76
|
if (sub === "dashboard")
|
|
47
77
|
work.shift();
|
|
48
78
|
const days = takeFlag(work, "--days") ?? "7";
|
|
79
|
+
finishWork(work);
|
|
49
80
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/analytics?days=${encodeURIComponent(days)}`);
|
|
50
81
|
if (isJson()) {
|
|
51
82
|
emitOk("analytics", data);
|
|
@@ -63,6 +94,7 @@ export async function runAnalytics(args) {
|
|
|
63
94
|
if (sub === "summary") {
|
|
64
95
|
const filters = parseJsonFlag(takeFlag(work, "--filters"), "--filters");
|
|
65
96
|
const body = { op: "summary", ...rangeFields(work), ...(filters ? { filters } : {}) };
|
|
97
|
+
finishWork(work);
|
|
66
98
|
const data = await v1Post(base, body);
|
|
67
99
|
if (isJson())
|
|
68
100
|
emitOk("analytics", data);
|
|
@@ -87,6 +119,7 @@ export async function runAnalytics(args) {
|
|
|
87
119
|
...(limit ? { limit: Number(limit) } : {}),
|
|
88
120
|
...(filters ? { filters } : {}),
|
|
89
121
|
};
|
|
122
|
+
finishWork(work);
|
|
90
123
|
const data = await v1Post(base, body);
|
|
91
124
|
if (isJson())
|
|
92
125
|
emitOk("analytics", data);
|
|
@@ -95,13 +128,21 @@ export async function runAnalytics(args) {
|
|
|
95
128
|
return EXIT_OK;
|
|
96
129
|
}
|
|
97
130
|
if (sub === "funnel") {
|
|
98
|
-
|
|
131
|
+
let stepsRaw = takeFlag(work, "--steps");
|
|
99
132
|
if (!stepsRaw) {
|
|
100
|
-
console.error(`Usage: frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]' [--days N]`
|
|
133
|
+
console.error(`Usage: frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]' [--days N]\n` +
|
|
134
|
+
` or: frontpage analytics funnel --steps /, /pricing, /updates [--days N]`);
|
|
101
135
|
return EXIT_USAGE;
|
|
102
136
|
}
|
|
103
|
-
|
|
137
|
+
// Shell splits `--steps /, /pricing` into multiple argv tokens; reassemble path list.
|
|
138
|
+
if (!stepsRaw.trim().startsWith("[")) {
|
|
139
|
+
while (work[0] && work[0].startsWith("/") && !work[0].startsWith("//")) {
|
|
140
|
+
stepsRaw = `${stepsRaw},${work.shift()}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const steps = parseFunnelSteps(stepsRaw);
|
|
104
144
|
const body = { op: "funnel", steps, ...rangeFields(work) };
|
|
145
|
+
finishWork(work);
|
|
105
146
|
const data = await v1Post(base, body);
|
|
106
147
|
if (isJson())
|
|
107
148
|
emitOk("analytics", data);
|
|
@@ -120,6 +161,7 @@ export async function runAnalytics(args) {
|
|
|
120
161
|
...(interval ? { interval } : {}),
|
|
121
162
|
...(filters ? { filters } : {}),
|
|
122
163
|
};
|
|
164
|
+
finishWork(work);
|
|
123
165
|
const data = await v1Post(base, body);
|
|
124
166
|
if (isJson())
|
|
125
167
|
emitOk("analytics", data);
|
|
@@ -144,6 +186,7 @@ export async function runAnalytics(args) {
|
|
|
144
186
|
...rangeFields(work),
|
|
145
187
|
...(metric ? { metric } : {}),
|
|
146
188
|
};
|
|
189
|
+
finishWork(work);
|
|
147
190
|
const data = await v1Post(base, body);
|
|
148
191
|
if (isJson())
|
|
149
192
|
emitOk("analytics", data);
|
|
@@ -152,7 +195,16 @@ export async function runAnalytics(args) {
|
|
|
152
195
|
return EXIT_OK;
|
|
153
196
|
}
|
|
154
197
|
if (sub === "sql") {
|
|
155
|
-
const
|
|
198
|
+
const sqlFlag = takeFlag(work, "--sql");
|
|
199
|
+
const sql = sqlFlag ?? work.join(" ").trim();
|
|
200
|
+
if (sqlFlag) {
|
|
201
|
+
// Consumed via flag; reject leftover flags/args.
|
|
202
|
+
finishWork(work);
|
|
203
|
+
}
|
|
204
|
+
else if (sql) {
|
|
205
|
+
// Positional SQL consumed all remaining tokens.
|
|
206
|
+
work.length = 0;
|
|
207
|
+
}
|
|
156
208
|
if (!sql) {
|
|
157
209
|
console.error('Usage: frontpage analytics sql --sql "SELECT path, count(*) FROM analytics_events GROUP BY 1"');
|
|
158
210
|
return EXIT_USAGE;
|
package/dist/commands/help.js
CHANGED
|
@@ -70,7 +70,7 @@ export function runHelp(args) {
|
|
|
70
70
|
console.log(` ${c.name.padEnd(12)} ${c.summary}`);
|
|
71
71
|
}
|
|
72
72
|
console.log(`\nTopics: frontpage help ${TOPIC_NAMES.join(" | frontpage help ")}`);
|
|
73
|
-
console.log("Global: --json --quiet --yes --site <slug> --help");
|
|
73
|
+
console.log("Global: --json --quiet --yes --site <slug> --help --version");
|
|
74
74
|
console.log("Agents: see AGENTS.md in the npm package");
|
|
75
75
|
return 0;
|
|
76
76
|
}
|
package/dist/commands/media.d.ts
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
+
import { type UploadedMedia } from "../media/upload.js";
|
|
2
|
+
/** Sanitize media --json payload: omit full text body (R5); keep content for prompt --attach path. */
|
|
3
|
+
export declare function mediaJsonData(slug: string, data: UploadedMedia): Record<string, unknown>;
|
|
1
4
|
export declare function runMedia(args: string[]): Promise<number>;
|
package/dist/commands/media.js
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, EXIT_USAGE, exitCodeForError } from "../http/client.js";
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags } from "../argv.js";
|
|
3
4
|
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
4
5
|
import { uploadLocalMedia } from "../media/upload.js";
|
|
5
|
-
|
|
6
|
+
const TEXT_NOT_LIBRARY = "not added to the media library and cannot be linked via /__media. " +
|
|
7
|
+
"For form submission exports use inbox … --csv; large data files via the editor.";
|
|
8
|
+
/** Sanitize media --json payload: omit full text body (R5); keep content for prompt --attach path. */
|
|
9
|
+
export function mediaJsonData(slug, data) {
|
|
10
|
+
if (data.kind === "text") {
|
|
11
|
+
const content = data.content;
|
|
12
|
+
const contentBytes = data.sizeBytes ??
|
|
13
|
+
(typeof content === "string" ? Buffer.byteLength(content, "utf8") : 0);
|
|
14
|
+
const contentPreview = typeof content === "string"
|
|
15
|
+
? content.length > 256
|
|
16
|
+
? content.slice(0, 256)
|
|
17
|
+
: content
|
|
18
|
+
: undefined;
|
|
19
|
+
return {
|
|
20
|
+
slug,
|
|
21
|
+
kind: data.kind,
|
|
22
|
+
filename: data.filename,
|
|
23
|
+
sizeBytes: data.sizeBytes,
|
|
24
|
+
contentType: data.contentType,
|
|
25
|
+
transport: data.transport,
|
|
26
|
+
contentBytes,
|
|
27
|
+
contentPreview,
|
|
28
|
+
contentOmitted: true,
|
|
29
|
+
libraryAsset: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { slug, ...data };
|
|
33
|
+
}
|
|
6
34
|
export async function runMedia(args) {
|
|
7
35
|
try {
|
|
8
36
|
const { slug, rest } = await resolveSlug(args);
|
|
9
37
|
const sub = rest[0] ?? "list";
|
|
10
38
|
if (sub === "list" || sub === "ls") {
|
|
39
|
+
const work = rest.slice(1);
|
|
40
|
+
assertNoUnknownFlags(work, "media");
|
|
41
|
+
if (work.length > 0) {
|
|
42
|
+
console.error(`Unexpected argument: ${work[0]}\nUsage: frontpage media list [--site <slug>] [--json]`);
|
|
43
|
+
return EXIT_USAGE;
|
|
44
|
+
}
|
|
11
45
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/media`);
|
|
12
46
|
if (isJson()) {
|
|
13
47
|
emitOk("media", { slug, ...data });
|
|
@@ -20,26 +54,33 @@ export async function runMedia(args) {
|
|
|
20
54
|
return EXIT_OK;
|
|
21
55
|
}
|
|
22
56
|
if (sub === "upload") {
|
|
23
|
-
const
|
|
57
|
+
const work = rest.slice(1);
|
|
58
|
+
const filePath = work.shift();
|
|
24
59
|
if (!filePath) {
|
|
25
60
|
console.error("Usage: frontpage media upload <file> [--site <slug>] [--json]");
|
|
26
61
|
return EXIT_USAGE;
|
|
27
62
|
}
|
|
63
|
+
assertNoUnknownFlags(work, "media");
|
|
64
|
+
if (work.length > 0) {
|
|
65
|
+
console.error(`Unexpected argument: ${work[0]}\nUsage: frontpage media upload <file> [--site <slug>] [--json]`);
|
|
66
|
+
return EXIT_USAGE;
|
|
67
|
+
}
|
|
28
68
|
const data = await uploadLocalMedia({ slug, filePath });
|
|
29
69
|
if (isJson()) {
|
|
30
|
-
emitOk("media",
|
|
70
|
+
emitOk("media", mediaJsonData(slug, data));
|
|
31
71
|
return EXIT_OK;
|
|
32
72
|
}
|
|
33
73
|
if (data.kind === "text") {
|
|
34
|
-
console.log(`Inline text (${data.filename}): ${data.sizeBytes ?? 0} bytes —
|
|
74
|
+
console.log(`Inline text (${data.filename}): ${data.sizeBytes ?? 0} bytes — ${TEXT_NOT_LIBRARY}`);
|
|
35
75
|
return EXIT_OK;
|
|
36
76
|
}
|
|
37
|
-
|
|
38
|
-
console.log(data.url);
|
|
77
|
+
// Human mode: sitePath first so first-line copy/paste is markup-safe (R4).
|
|
39
78
|
if (data.sitePath)
|
|
40
79
|
console.log(`sitePath: ${data.sitePath}`);
|
|
41
80
|
else if (data.key)
|
|
42
81
|
console.log(`sitePath: /__media/${data.key}`);
|
|
82
|
+
if (data.url)
|
|
83
|
+
console.log(`url: ${data.url}`);
|
|
43
84
|
return EXIT_OK;
|
|
44
85
|
}
|
|
45
86
|
console.error("Usage: frontpage media list|upload <file> [--site <slug>] [--json]");
|
package/dist/commands/modules.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
2
|
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
|
|
3
|
-
|
|
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
|
-
}
|
|
3
|
+
import { takeFlag } from "../argv.js";
|
|
11
4
|
export async function runModules(args) {
|
|
12
5
|
try {
|
|
13
6
|
const { slug, rest } = await resolveSlug(args);
|
|
@@ -22,7 +15,12 @@ export async function runModules(args) {
|
|
|
22
15
|
if (sub === "docs" || sub === "doc") {
|
|
23
16
|
const key = rest[1];
|
|
24
17
|
if (!key) {
|
|
18
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/modules`);
|
|
25
19
|
console.error("Usage: frontpage modules docs <key> [--instance <id>] [--site <slug>]");
|
|
20
|
+
console.error("Known modules (run: frontpage modules docs <key>):");
|
|
21
|
+
for (const m of data.modules) {
|
|
22
|
+
console.error(` ${m.key.padEnd(16)} ${m.name}`);
|
|
23
|
+
}
|
|
26
24
|
return EXIT_USAGE;
|
|
27
25
|
}
|
|
28
26
|
const work = rest.slice(2);
|
package/dist/commands/open.d.ts
CHANGED
package/dist/commands/open.js
CHANGED
|
@@ -1,22 +1,56 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { resolveSlug } from "../site.js";
|
|
3
|
-
import { EXIT_OK, EXIT_USAGE } from "../http/client.js";
|
|
3
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE } from "../http/client.js";
|
|
4
|
+
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
4
5
|
import { emitErr, emitOk, emitUsage, isJson, isQuiet } from "../runtime.js";
|
|
6
|
+
/** Site-relative path for open; rejects schemes and traversal. */
|
|
7
|
+
export function normalizeOpenPath(raw) {
|
|
8
|
+
const trimmed = raw.trim();
|
|
9
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//")) {
|
|
10
|
+
throw new CliHttpError("Open path must be site-relative and start with / (e.g. /updates).", EXIT_USAGE, 0, "bad_request");
|
|
11
|
+
}
|
|
12
|
+
if (/^https?:/i.test(trimmed) || trimmed.includes("..") || trimmed.includes("\\")) {
|
|
13
|
+
throw new CliHttpError("Open path must be a site-relative path (no http(s) URLs or ..).", EXIT_USAGE, 0, "bad_request");
|
|
14
|
+
}
|
|
15
|
+
const collapsed = trimmed.replace(/\/+/g, "/");
|
|
16
|
+
if (collapsed.length > 1 && collapsed.endsWith("/"))
|
|
17
|
+
return collapsed.slice(0, -1);
|
|
18
|
+
return collapsed;
|
|
19
|
+
}
|
|
5
20
|
export async function runOpen(args) {
|
|
6
21
|
try {
|
|
7
22
|
const { slug, rest } = await resolveSlug(args);
|
|
8
|
-
const
|
|
9
|
-
const
|
|
23
|
+
const work = [...rest];
|
|
24
|
+
const prod = hasFlag(work, "--prod");
|
|
25
|
+
const pathFlag = takeFlag(work, "--path");
|
|
26
|
+
let path = "/";
|
|
27
|
+
if (pathFlag !== undefined) {
|
|
28
|
+
path = normalizeOpenPath(pathFlag);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const positional = work.find((a) => a.startsWith("/") && !a.startsWith("//"));
|
|
32
|
+
if (positional) {
|
|
33
|
+
work.splice(work.indexOf(positional), 1);
|
|
34
|
+
path = normalizeOpenPath(positional);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
assertNoUnknownFlags(work, "open");
|
|
38
|
+
if (work.length > 0) {
|
|
39
|
+
return emitUsage("open", "Usage: frontpage open [/path|--path /path] [--site <slug>] [--prod]", EXIT_USAGE);
|
|
40
|
+
}
|
|
41
|
+
const url = path === "/"
|
|
42
|
+
? `https://${slug}.frontpage.host/`
|
|
43
|
+
: `https://${slug}.frontpage.host${path}`;
|
|
10
44
|
// --prod is reserved for a future production custom-domain lookup; both
|
|
11
45
|
// modes currently open the site subdomain on *.frontpage.host.
|
|
12
46
|
if (prod && !isJson() && !isQuiet()) {
|
|
13
|
-
console.error("Note: --prod is a no-op stub; opening https://<slug>.frontpage.host
|
|
47
|
+
console.error("Note: --prod is a no-op stub; opening https://<slug>.frontpage.host/…");
|
|
14
48
|
}
|
|
15
49
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
16
50
|
const openerArgs = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
17
51
|
spawn(opener, openerArgs, { detached: true, stdio: "ignore" }).unref();
|
|
18
52
|
if (isJson()) {
|
|
19
|
-
emitOk("open", { slug, url, prodRequested: prod, prodNoOp: true });
|
|
53
|
+
emitOk("open", { slug, url, path, prodRequested: prod, prodNoOp: true });
|
|
20
54
|
}
|
|
21
55
|
else {
|
|
22
56
|
console.log(url);
|
|
@@ -25,7 +59,7 @@ export async function runOpen(args) {
|
|
|
25
59
|
}
|
|
26
60
|
catch (err) {
|
|
27
61
|
if (err instanceof Error && err.message.includes("Usage")) {
|
|
28
|
-
return emitUsage("open", "Usage: frontpage open [--site <slug>] [--prod]", EXIT_USAGE);
|
|
62
|
+
return emitUsage("open", "Usage: frontpage open [/path|--path /path] [--site <slug>] [--prod]", EXIT_USAGE);
|
|
29
63
|
}
|
|
30
64
|
return emitErr("open", err);
|
|
31
65
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
2
|
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
3
4
|
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
4
|
-
|
|
5
|
-
const i = rest.indexOf(name);
|
|
6
|
-
if (i === -1)
|
|
7
|
-
return undefined;
|
|
8
|
-
const v = rest[i + 1];
|
|
9
|
-
rest.splice(i, 2);
|
|
10
|
-
return v;
|
|
11
|
-
}
|
|
12
|
-
function hasFlag(rest, name) {
|
|
13
|
-
const i = rest.indexOf(name);
|
|
14
|
-
if (i === -1)
|
|
15
|
-
return false;
|
|
16
|
-
rest.splice(i, 1);
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
5
|
+
const SEARCH_USAGE = "Usage: frontpage search <pattern> [path|--path <path>] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>]";
|
|
19
6
|
export async function runSearch(args) {
|
|
20
7
|
try {
|
|
21
8
|
const { slug, rest } = await resolveSlug(args);
|
|
@@ -26,12 +13,21 @@ export async function runSearch(args) {
|
|
|
26
13
|
const filesOnly = hasFlag(work, "--files-only");
|
|
27
14
|
const contextLines = takeFlag(work, "--context-lines");
|
|
28
15
|
const maxResults = takeFlag(work, "--max-results");
|
|
29
|
-
const
|
|
16
|
+
const pathFlag = takeFlag(work, "--path");
|
|
17
|
+
const pattern = work.shift();
|
|
30
18
|
if (!pattern) {
|
|
31
|
-
console.error(
|
|
19
|
+
console.error(SEARCH_USAGE);
|
|
20
|
+
return EXIT_USAGE;
|
|
21
|
+
}
|
|
22
|
+
let pathFilter = pathFlag;
|
|
23
|
+
if (pathFilter === undefined && work[0] && !work[0].startsWith("-")) {
|
|
24
|
+
pathFilter = work.shift();
|
|
25
|
+
}
|
|
26
|
+
assertNoUnknownFlags(work, "search");
|
|
27
|
+
if (work.length > 0) {
|
|
28
|
+
console.error(`Unexpected argument: ${work[0]}\n${SEARCH_USAGE}`);
|
|
32
29
|
return EXIT_USAGE;
|
|
33
30
|
}
|
|
34
|
-
const pathFilter = work[1] && !work[1].startsWith("-") ? work[1] : undefined;
|
|
35
31
|
const q = new URLSearchParams({ pattern });
|
|
36
32
|
if (pathFilter)
|
|
37
33
|
q.set("path", pathFilter);
|
package/dist/commands/seo.js
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, takeFlag } from "../argv.js";
|
|
4
|
+
import { emitErr, isJson } from "../runtime.js";
|
|
5
|
+
function resolveSeoPath(work) {
|
|
6
|
+
const fromFlag = takeFlag(work, "--path");
|
|
7
|
+
if (fromFlag !== undefined) {
|
|
8
|
+
if (!fromFlag.startsWith("/") || fromFlag.startsWith("//")) {
|
|
9
|
+
throw new CliHttpError("SEO path must start with / (e.g. / or /about).", EXIT_USAGE, 0, "bad_request");
|
|
10
|
+
}
|
|
11
|
+
return fromFlag;
|
|
12
|
+
}
|
|
13
|
+
const positional = work.find((a) => a.startsWith("/") && !a.startsWith("//"));
|
|
14
|
+
if (positional) {
|
|
15
|
+
const i = work.indexOf(positional);
|
|
16
|
+
work.splice(i, 1);
|
|
17
|
+
return positional;
|
|
18
|
+
}
|
|
19
|
+
return "/";
|
|
10
20
|
}
|
|
11
21
|
export async function runSeo(args) {
|
|
12
22
|
try {
|
|
13
23
|
const { slug, rest } = await resolveSlug(args);
|
|
14
24
|
const sub = rest[0];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
25
|
+
// `seo`, `seo get`, `seo get /path`, `seo get --path /path`, `seo --path /path`
|
|
26
|
+
if (!sub || sub === "get" || sub === "--path" || (sub.startsWith("/") && !sub.startsWith("//"))) {
|
|
27
|
+
const work = sub === "get" ? rest.slice(1) : [...rest];
|
|
28
|
+
const path = resolveSeoPath(work);
|
|
29
|
+
assertNoUnknownFlags(work, "seo");
|
|
18
30
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/seo?path=${encodeURIComponent(path)}`);
|
|
19
31
|
console.log(JSON.stringify(data, null, 2));
|
|
20
32
|
return EXIT_OK;
|
|
21
33
|
}
|
|
22
34
|
if (sub === "set") {
|
|
23
35
|
const work = rest.slice(1);
|
|
24
|
-
const path =
|
|
36
|
+
const path = resolveSeoPath(work);
|
|
25
37
|
const body = { path };
|
|
26
38
|
for (const field of [
|
|
27
39
|
"title",
|
|
@@ -39,6 +51,7 @@ export async function runSeo(args) {
|
|
|
39
51
|
if (noindex !== undefined) {
|
|
40
52
|
body.noindex = noindex === "true" || noindex === "1";
|
|
41
53
|
}
|
|
54
|
+
assertNoUnknownFlags(work, "seo");
|
|
42
55
|
if (Object.keys(body).length <= 1) {
|
|
43
56
|
console.error("Usage: frontpage seo set --path / [--title …] [--description …] [--ogImage …] [--noindex true|false]");
|
|
44
57
|
return EXIT_USAGE;
|
|
@@ -47,10 +60,12 @@ export async function runSeo(args) {
|
|
|
47
60
|
console.log(JSON.stringify(data, null, 2));
|
|
48
61
|
return EXIT_OK;
|
|
49
62
|
}
|
|
50
|
-
console.error("Usage: frontpage seo get|set … [--site <slug>]");
|
|
63
|
+
console.error("Usage: frontpage seo get [/path|--path /path] | seo set --path / [--title …] [--site <slug>]");
|
|
51
64
|
return EXIT_USAGE;
|
|
52
65
|
}
|
|
53
66
|
catch (err) {
|
|
67
|
+
if (isJson())
|
|
68
|
+
return emitErr("seo", err);
|
|
54
69
|
console.error(err instanceof Error ? err.message : String(err));
|
|
55
70
|
return exitCodeForError(err);
|
|
56
71
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runVersion(): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EXIT_OK } from "../http/client.js";
|
|
2
|
+
import { emitOk, isJson } from "../runtime.js";
|
|
3
|
+
import { packageIdentity, versionLine } from "../version.js";
|
|
4
|
+
export function runVersion() {
|
|
5
|
+
const identity = packageIdentity();
|
|
6
|
+
if (isJson()) {
|
|
7
|
+
emitOk("version", identity);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
console.log(versionLine());
|
|
11
|
+
}
|
|
12
|
+
return EXIT_OK;
|
|
13
|
+
}
|
package/dist/flags.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Strip global flags from argv. Safe to call before command dispatch.
|
|
3
|
-
* Recognizes --json, --quiet, --yes, and help aliases anywhere in the list.
|
|
3
|
+
* Recognizes --json, --quiet, --yes, --version/-V, and help aliases anywhere in the list.
|
|
4
4
|
*/
|
|
5
5
|
export type GlobalFlags = {
|
|
6
6
|
json: boolean;
|
|
@@ -8,6 +8,8 @@ export type GlobalFlags = {
|
|
|
8
8
|
yes: boolean;
|
|
9
9
|
/** True when argv asked for help (--help / -h), not the `help` command itself. */
|
|
10
10
|
wantsHelp: boolean;
|
|
11
|
+
/** True when argv asked for version (--version / -V). */
|
|
12
|
+
wantsVersion: boolean;
|
|
11
13
|
args: string[];
|
|
12
14
|
};
|
|
13
15
|
export declare function parseGlobalFlags(argv: string[]): GlobalFlags;
|
package/dist/flags.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Strip global flags from argv. Safe to call before command dispatch.
|
|
3
|
-
* Recognizes --json, --quiet, --yes, and help aliases anywhere in the list.
|
|
3
|
+
* Recognizes --json, --quiet, --yes, --version/-V, and help aliases anywhere in the list.
|
|
4
4
|
*/
|
|
5
5
|
export function parseGlobalFlags(argv) {
|
|
6
6
|
const args = [];
|
|
@@ -8,6 +8,7 @@ export function parseGlobalFlags(argv) {
|
|
|
8
8
|
let quiet = false;
|
|
9
9
|
let yes = false;
|
|
10
10
|
let wantsHelp = false;
|
|
11
|
+
let wantsVersion = false;
|
|
11
12
|
for (const a of argv) {
|
|
12
13
|
if (a === "--json") {
|
|
13
14
|
json = true;
|
|
@@ -25,7 +26,11 @@ export function parseGlobalFlags(argv) {
|
|
|
25
26
|
wantsHelp = true;
|
|
26
27
|
continue;
|
|
27
28
|
}
|
|
29
|
+
if (a === "--version" || a === "-V") {
|
|
30
|
+
wantsVersion = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
28
33
|
args.push(a);
|
|
29
34
|
}
|
|
30
|
-
return { json, quiet, yes, wantsHelp, args };
|
|
35
|
+
return { json, quiet, yes, wantsHelp, wantsVersion, args };
|
|
31
36
|
}
|
package/dist/media/upload.js
CHANGED
|
@@ -124,7 +124,8 @@ export async function uploadLocalMedia(args) {
|
|
|
124
124
|
headBytes: bytes.subarray(0, 4096),
|
|
125
125
|
});
|
|
126
126
|
if (decision.transport === "reject") {
|
|
127
|
-
|
|
127
|
+
// Local validation only — omit httpStatus (status 0) and never invent requestId (R10).
|
|
128
|
+
throw new CliHttpError(decision.message, EXIT_USAGE, 0, "bad_request");
|
|
128
129
|
}
|
|
129
130
|
if (decision.transport === "client") {
|
|
130
131
|
return uploadViaClient({
|
package/dist/registry.js
CHANGED
|
@@ -26,6 +26,13 @@ export const COMMANDS = [
|
|
|
26
26
|
"frontpage help --json",
|
|
27
27
|
],
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
name: "version",
|
|
31
|
+
summary: "Print frontpage-host package version",
|
|
32
|
+
usage: "frontpage version | frontpage --version | frontpage -V",
|
|
33
|
+
examples: ["frontpage --version", "frontpage version --json"],
|
|
34
|
+
flags: ["Also available as global --version / -V"],
|
|
35
|
+
},
|
|
29
36
|
{
|
|
30
37
|
name: "login",
|
|
31
38
|
summary: "Save an API key to the OS keychain",
|
|
@@ -133,15 +140,21 @@ export const COMMANDS = [
|
|
|
133
140
|
{
|
|
134
141
|
name: "search",
|
|
135
142
|
summary: "Search remote text files",
|
|
136
|
-
usage: "frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>] [--json]",
|
|
143
|
+
usage: "frontpage search <pattern> [path|--path <path>] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>] [--json]",
|
|
137
144
|
topics: ["sync"],
|
|
145
|
+
examples: [
|
|
146
|
+
"frontpage search Subscribe src/pages",
|
|
147
|
+
"frontpage search Subscribe --path src/pages",
|
|
148
|
+
],
|
|
138
149
|
flags: [
|
|
150
|
+
"--path <prefix> (alias for positional path filter; flag wins if both present)",
|
|
139
151
|
"--regex",
|
|
140
152
|
"--case-sensitive",
|
|
141
153
|
"--whole-word",
|
|
142
154
|
"--files-only",
|
|
143
155
|
"--context-lines <n>",
|
|
144
156
|
"--max-results <n>",
|
|
157
|
+
"Unknown leftover flags fail closed (exit 2)",
|
|
145
158
|
],
|
|
146
159
|
},
|
|
147
160
|
{
|
|
@@ -184,9 +197,13 @@ export const COMMANDS = [
|
|
|
184
197
|
{
|
|
185
198
|
name: "open",
|
|
186
199
|
summary: "Open the site subdomain in a browser",
|
|
187
|
-
usage: "frontpage open [--site <slug>] [--prod]",
|
|
200
|
+
usage: "frontpage open [/path|--path /path] [--site <slug>] [--prod]",
|
|
188
201
|
topics: ["sync"],
|
|
189
|
-
|
|
202
|
+
examples: ["frontpage open", "frontpage open /updates", "frontpage open --path /pricing"],
|
|
203
|
+
flags: [
|
|
204
|
+
"Optional site-relative path appended to https://<slug>.frontpage.host",
|
|
205
|
+
"--prod is a no-op stub today — still opens the *.frontpage.host subdomain",
|
|
206
|
+
],
|
|
190
207
|
},
|
|
191
208
|
{
|
|
192
209
|
name: "prompt",
|
|
@@ -277,12 +294,16 @@ export const COMMANDS = [
|
|
|
277
294
|
"frontpage analytics summary --days 30",
|
|
278
295
|
"frontpage analytics query --dimension path --measure count --limit 10",
|
|
279
296
|
`frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]'`,
|
|
297
|
+
"frontpage analytics funnel --steps /, /pricing, /updates",
|
|
280
298
|
"frontpage analytics timeseries --metric pageviews --interval day --days 14",
|
|
281
299
|
`frontpage analytics compare --a '{"device":"mobile"}' --b '{"device":"desktop"}'`,
|
|
282
300
|
'frontpage analytics sql --sql "SELECT path, count(*)::int AS n FROM analytics_events GROUP BY 1 ORDER BY n DESC LIMIT 10"',
|
|
283
301
|
],
|
|
284
302
|
flags: [
|
|
285
303
|
"Bare analytics / dashboard = legacy GET dashboard (compat)",
|
|
304
|
+
"funnel --steps accepts JSON array or comma-separated paths (/, /pricing)",
|
|
305
|
+
"Invalid JSON for --steps / --a / --b prints an explicit parse error (not bare Usage)",
|
|
306
|
+
"Unknown leftover flags fail closed (exit 2)",
|
|
286
307
|
"Ops use POST …/analytics with allowlisted dimensions/measures; SQL is guard-validated + RLS",
|
|
287
308
|
],
|
|
288
309
|
},
|
|
@@ -299,6 +320,7 @@ export const COMMANDS = [
|
|
|
299
320
|
topics: ["agent", "content"],
|
|
300
321
|
examples: [
|
|
301
322
|
"frontpage modules docs forms",
|
|
323
|
+
"frontpage modules docs signups",
|
|
302
324
|
"frontpage modules list",
|
|
303
325
|
'frontpage modules request-setup payments --reason "You asked for a $50 deposit button — Stripe setup makes it live"',
|
|
304
326
|
],
|
|
@@ -326,8 +348,11 @@ export const COMMANDS = [
|
|
|
326
348
|
],
|
|
327
349
|
flags: [
|
|
328
350
|
"Auto-selects proxy (≤4 MB) vs direct-to-Blob for large rasters/PDFs and all video",
|
|
329
|
-
"
|
|
351
|
+
"Human success: sitePath first (markup-safe), then url: (blob URL for debugging)",
|
|
352
|
+
"Use sitePath (/__media/uploads/<slug>/…) in markup — never raw blob hosts in pages",
|
|
353
|
+
"Text/CSV upload is inline only — not a media-library asset (no /__media link); --json omits full content",
|
|
330
354
|
"SVG stays on the sanitizing proxy path (≤4 MB)",
|
|
355
|
+
"Unknown leftover flags fail closed (exit 2)",
|
|
331
356
|
],
|
|
332
357
|
seeAlso: ["prompt", "help content", "help site-building"],
|
|
333
358
|
},
|
|
@@ -341,8 +366,14 @@ export const COMMANDS = [
|
|
|
341
366
|
{
|
|
342
367
|
name: "seo",
|
|
343
368
|
summary: "Get or set page SEO",
|
|
344
|
-
usage: "frontpage seo get|set --path / [--title …] [--description …] [--site <slug>]",
|
|
369
|
+
usage: "frontpage seo get [/path|--path /path] | seo set --path / [--title …] [--description …] [--site <slug>]",
|
|
345
370
|
topics: ["content"],
|
|
371
|
+
examples: [
|
|
372
|
+
"frontpage seo get /",
|
|
373
|
+
"frontpage seo get --path /updates",
|
|
374
|
+
"frontpage seo set --path / --title \"Home\"",
|
|
375
|
+
],
|
|
376
|
+
flags: ["get accepts positional path or --path (flag wins); unknown leftover flags fail closed"],
|
|
346
377
|
},
|
|
347
378
|
{
|
|
348
379
|
name: "articles",
|
|
@@ -422,7 +453,9 @@ export const TOPICS = {
|
|
|
422
453
|
5. frontpage pull
|
|
423
454
|
6. Edit files under src/, public/, or content/
|
|
424
455
|
7. frontpage push --message "…"
|
|
425
|
-
8. Review https://<subdomain>.frontpage.host
|
|
456
|
+
8. Review staging: https://<subdomain>.frontpage.host when that subdomain is publicly shareable.
|
|
457
|
+
With a custom domain connected, the subdomain is often owner-private (login wall ≠ failed push);
|
|
458
|
+
anonymous review is on the published custom domain after publish.
|
|
426
459
|
9. frontpage publish # ONLY if the user asked — promotes to production
|
|
427
460
|
|
|
428
461
|
Staging is the subdomain. There is no separate staging-deploy step for agents.
|
|
@@ -447,8 +480,8 @@ export const TOPICS = {
|
|
|
447
480
|
frontpage push [--message …] [--wait] # uploads allowlisted files; CAS + active-run guard
|
|
448
481
|
frontpage ls [prefix]
|
|
449
482
|
frontpage cat <path>
|
|
450
|
-
frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only]
|
|
451
|
-
frontpage open
|
|
483
|
+
frontpage search <pattern> [path|--path <path>] [--regex] [--case-sensitive] [--whole-word] [--files-only]
|
|
484
|
+
frontpage open [/path] # opens https://<slug>.frontpage.host[/path] (--prod is a no-op stub)
|
|
452
485
|
|
|
453
486
|
Local state: .frontpage/state.json { slug, baseCommitSha }
|
|
454
487
|
Pull BEFORE EDITING when tip may have moved (UI agent / another checkout) — check tipDrift.
|
|
@@ -520,18 +553,20 @@ export const TOPICS = {
|
|
|
520
553
|
frontpage extensions
|
|
521
554
|
|
|
522
555
|
--attach uploads via media (images/PDF/video/text) or accepts /__media/… / https URLs.
|
|
523
|
-
API errors include requestId in --json
|
|
556
|
+
API errors include requestId in --json envelopes when the server returned one; local validation errors omit requestId and httpStatus.
|
|
524
557
|
Mid-task continuation is automatic — there is no runs continue.
|
|
525
558
|
Do not file-sync while an agent run is active (see help sync for push --wait).
|
|
526
|
-
Default after work: review https://<subdomain>.frontpage.host
|
|
559
|
+
Default after work: review staging on https://<subdomain>.frontpage.host when public;
|
|
560
|
+
with a custom domain, that subdomain may require an owner browser session (login wall ≠ failed deploy).
|
|
561
|
+
Do not publish unless asked.`,
|
|
527
562
|
content: `Content
|
|
528
|
-
frontpage media list|upload <file> # images/PDF
|
|
563
|
+
frontpage media list|upload <file> # images/PDF/video; text/CSV = inline not library; sitePath first when stored
|
|
529
564
|
frontpage inbox [leads|subscribers|orders|…] [--mark-read] [--csv]
|
|
530
|
-
frontpage seo get|set --path /
|
|
565
|
+
frontpage seo get [/path|--path /path] | seo set --path /
|
|
531
566
|
frontpage articles list|content|create-section|create-article|set-draft|…
|
|
532
567
|
frontpage fonts [list|search] [--slot …] [--query …]
|
|
533
568
|
frontpage tokens set --group … --key … --value …
|
|
534
|
-
frontpage modules list|docs|request-setup|enable|disable|archive|config <key>
|
|
569
|
+
frontpage modules list|docs|request-setup|enable|disable|archive|config <key> # mailing list key: signups
|
|
535
570
|
frontpage extensions list|docs|enable|disable|config <key>
|
|
536
571
|
frontpage settings get|set
|
|
537
572
|
frontpage edit --path / --old "…" --new "…"
|
|
@@ -573,7 +608,8 @@ export const TOPICS = {
|
|
|
573
608
|
{ "ok": true, "command": "…", "data": {…} }
|
|
574
609
|
{ "ok": false, "command": "…", "error": { "message", "code?", "httpStatus?", "exitCode", "requestId?" }, "data"? }
|
|
575
610
|
Failures with --json still use the process exit code; envelope is on stdout.
|
|
576
|
-
|
|
611
|
+
requestId is present only when the API returned one — local CLI validation (e.g. media size/type reject) omits requestId and httpStatus.
|
|
612
|
+
Prefer requestId from API error envelopes when filing support issues.`,
|
|
577
613
|
};
|
|
578
614
|
export function findCommand(name) {
|
|
579
615
|
return COMMANDS.find((c) => c.name === name);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Read package identity from frontpage-host package.json (works from dist/). */
|
|
2
|
+
export declare function packageIdentity(): {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
};
|
|
6
|
+
/** Human one-liner: `frontpage-host 0.1.10`. */
|
|
7
|
+
export declare function versionLine(): string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
/** Read package identity from frontpage-host package.json (works from dist/). */
|
|
4
|
+
export function packageIdentity() {
|
|
5
|
+
const pkg = require("../package.json");
|
|
6
|
+
return {
|
|
7
|
+
name: pkg.name ?? "frontpage-host",
|
|
8
|
+
version: pkg.version ?? "0.0.0",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Human one-liner: `frontpage-host 0.1.10`. */
|
|
12
|
+
export function versionLine() {
|
|
13
|
+
const { name, version } = packageIdentity();
|
|
14
|
+
return `${name} ${version}`;
|
|
15
|
+
}
|
package/llms.txt
CHANGED
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
- Search flags: --regex --case-sensitive --whole-word --files-only --context-lines --max-results
|
|
11
11
|
- Modules setup: frontpage modules request-setup <key> --reason "…" (owner status bar; CLI cannot show it)
|
|
12
12
|
- Analytics ops: frontpage analytics summary|query|funnel|timeseries|compare|sql (bare analytics = dashboard)
|
|
13
|
-
- Media: frontpage media upload <file> (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB);
|
|
13
|
+
- Media: frontpage media upload <file> (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB); human prints sitePath first — use /__media/uploads/<slug>/… in markup. Text/CSV is inline only (not media library; --json omits full content).
|
|
14
14
|
- Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (same media caps as media upload)
|
|
15
|
-
-
|
|
15
|
+
- Version: frontpage --version | -V | version
|
|
16
|
+
- SEO get: frontpage seo get [/path|--path /path]; search path filter: [path|--path <path>]; open [/path|--path /path]
|
|
17
|
+
- Analytics funnel: --steps JSON array or comma paths (/, /pricing); bad JSON → Invalid JSON for --steps
|
|
18
|
+
- Modules: unknown key errors list Known: …; mailing list module key is signups (not subscribe)
|
|
19
|
+
- Errors: --json API envelopes include requestId when the server returned one; local validation omits requestId/httpStatus; runs wait failed/canceled → ok:false + run.error (exit 2); deploy wait fail-closed; push --wait
|
|
16
20
|
- Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
|
|
17
21
|
- Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
|
|
18
|
-
- Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step
|
|
22
|
+
- Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step. With a custom domain, subdomain may be owner-private (login wall ≠ failed push); public review is published custom domain.
|
|
19
23
|
- Production: custom domain after frontpage publish — NEVER publish/unpublish unless the user explicitly asks
|
|
20
24
|
- Sync: allowlist src/ public/ content/; platform overlay + secret-like paths rejected; pull before edit if tip may have moved (status/diff tipDrift); pull → edit → diff (blobSha) → push [--wait] (CAS); no push during active agent run; UI agent does not use CLI pull; review on subdomain
|
|
21
25
|
- Machine: global --json / --quiet; exit 0/2/3/4/5 (see help exit-codes)
|