frontpage-host 0.1.8 → 0.1.10
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/help.js +1 -1
- package/dist/commands/media.js +25 -14
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +40 -6
- package/dist/commands/prompt-attach.d.ts +12 -10
- package/dist/commands/prompt-attach.js +55 -22
- package/dist/commands/prompt.js +3 -1
- 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/http/client.d.ts +2 -0
- package/dist/http/client.js +8 -0
- package/dist/media/limits.d.ts +18 -0
- package/dist/media/limits.js +101 -0
- package/dist/media/select-transport.d.ts +27 -0
- package/dist/media/select-transport.js +134 -0
- package/dist/media/upload.d.ts +16 -0
- package/dist/media/upload.js +141 -0
- package/dist/registry.js +51 -15
- package/dist/version.d.ts +7 -0
- package/dist/version.js +15 -0
- package/llms.txt +5 -2
- package/package.json +4 -1
package/AGENTS.md
CHANGED
|
@@ -112,7 +112,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
112
112
|
- 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
113
|
- Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
|
|
114
114
|
- Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
|
|
115
|
-
- Media
|
|
115
|
+
- 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.
|
|
116
116
|
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
117
117
|
|
|
118
118
|
## Agent prompt loop
|
|
@@ -122,13 +122,14 @@ frontpage prompt "make the hero shorter"
|
|
|
122
122
|
frontpage prompt "Use this logo in the header" --attach ./logo.png
|
|
123
123
|
frontpage prompt "Match this brand PDF" --attach ./guide.pdf --path /
|
|
124
124
|
frontpage prompt "Reuse uploaded art" --attach /__media/uploads/<slug>/<file>
|
|
125
|
+
frontpage media upload ./clip.mp4
|
|
125
126
|
frontpage runs wait # wait for the agent run to finish (failed/canceled → exit 2)
|
|
126
127
|
# Review https://<subdomain>.frontpage.host — do not publish unless asked.
|
|
127
128
|
```
|
|
128
129
|
|
|
129
|
-
- `--attach` accepts a local file (
|
|
130
|
+
- `--attach` accepts a local file (same transport as `frontpage media upload`), an existing `/__media/…` site path, or an `https://` Blob URL. Repeatable.
|
|
130
131
|
- `--asset-url <https://…>` adds a legacy image URL. Repeatable.
|
|
131
|
-
- Caps
|
|
132
|
+
- Caps: images/PDF ≤25 MB, video ≤50 MB, SVG/text via proxy ≤4 MB (large text/data Blob upload is editor-only).
|
|
132
133
|
- Prompt text is still required (API). Attachments ride on the same enqueue as the editor.
|
|
133
134
|
- `runs wait` with `--json`: failures use `ok: false` and include `data.run` (with `run.error` when present).
|
|
134
135
|
- `deploy wait` is fail-closed: exit 2 if the waited run (or latest, when already idle) is `failed`/`canceled`.
|
|
@@ -179,7 +180,7 @@ Global machine envelope (`--json`):
|
|
|
179
180
|
{ "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3, "requestId": "…" } }
|
|
180
181
|
```
|
|
181
182
|
|
|
182
|
-
Failures with `--json` print the envelope on **stdout** (still use the process exit code). Include `requestId` when
|
|
183
|
+
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`).
|
|
183
184
|
|
|
184
185
|
## Global flags
|
|
185
186
|
|
|
@@ -190,6 +191,7 @@ Failures with `--json` print the envelope on **stdout** (still use the process e
|
|
|
190
191
|
| `--site <slug>` | Subdomain override (most site commands) |
|
|
191
192
|
| `--yes` | Confirm destructive ops (`unpublish`, `revert`) when non-TTY |
|
|
192
193
|
| `--help` / `-h` | Per-command help (alias of `frontpage help <cmd>`) |
|
|
194
|
+
| `--version` / `-V` | Print `frontpage-host <semver>` (also `frontpage version`) |
|
|
193
195
|
|
|
194
196
|
## Destructive ops — defaults for agents
|
|
195
197
|
|
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 : []);
|
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.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { basename } from "node:path";
|
|
3
1
|
import { resolveSlug } from "../site.js";
|
|
4
|
-
import { EXIT_OK, EXIT_USAGE, exitCodeForError
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, exitCodeForError } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
4
|
+
import { uploadLocalMedia } from "../media/upload.js";
|
|
5
|
+
import { v1Get } from "../http/client.js";
|
|
5
6
|
export async function runMedia(args) {
|
|
6
7
|
try {
|
|
7
8
|
const { slug, rest } = await resolveSlug(args);
|
|
8
9
|
const sub = rest[0] ?? "list";
|
|
9
10
|
if (sub === "list" || sub === "ls") {
|
|
10
11
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/media`);
|
|
12
|
+
if (isJson()) {
|
|
13
|
+
emitOk("media", { slug, ...data });
|
|
14
|
+
return EXIT_OK;
|
|
15
|
+
}
|
|
11
16
|
for (const a of data.assets) {
|
|
12
17
|
console.log(`${a.sitePath.padEnd(48)} ${String(a.sizeBytes).padStart(8)} ${a.name}`);
|
|
13
18
|
}
|
|
@@ -17,27 +22,33 @@ export async function runMedia(args) {
|
|
|
17
22
|
if (sub === "upload") {
|
|
18
23
|
const filePath = rest[1];
|
|
19
24
|
if (!filePath) {
|
|
20
|
-
console.error("Usage: frontpage media upload <file> [--site <slug>]");
|
|
25
|
+
console.error("Usage: frontpage media upload <file> [--site <slug>] [--json]");
|
|
21
26
|
return EXIT_USAGE;
|
|
22
27
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
const data = await uploadLocalMedia({ slug, filePath });
|
|
29
|
+
if (isJson()) {
|
|
30
|
+
emitOk("media", { slug, ...data });
|
|
31
|
+
return EXIT_OK;
|
|
32
|
+
}
|
|
27
33
|
if (data.kind === "text") {
|
|
28
34
|
console.log(`Inline text (${data.filename}): ${data.sizeBytes ?? 0} bytes — not stored.`);
|
|
35
|
+
return EXIT_OK;
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
// Human mode: sitePath first so first-line copy/paste is markup-safe (R4).
|
|
38
|
+
if (data.sitePath)
|
|
39
|
+
console.log(`sitePath: ${data.sitePath}`);
|
|
40
|
+
else if (data.key)
|
|
41
|
+
console.log(`sitePath: /__media/${data.key}`);
|
|
42
|
+
if (data.url)
|
|
43
|
+
console.log(`url: ${data.url}`);
|
|
35
44
|
return EXIT_OK;
|
|
36
45
|
}
|
|
37
|
-
console.error("Usage: frontpage media list|upload <file> [--site <slug>]");
|
|
46
|
+
console.error("Usage: frontpage media list|upload <file> [--site <slug>] [--json]");
|
|
38
47
|
return EXIT_USAGE;
|
|
39
48
|
}
|
|
40
49
|
catch (err) {
|
|
50
|
+
if (isJson())
|
|
51
|
+
return emitErr("media", err);
|
|
41
52
|
console.error(err instanceof Error ? err.message : String(err));
|
|
42
53
|
return exitCodeForError(err);
|
|
43
54
|
}
|
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
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { type UploadedMedia } from "../media/upload.js";
|
|
2
|
+
/** Mirror ATTACH_LIMITS in src/lib/agent/attachments.ts. */
|
|
2
3
|
export declare const CLI_ATTACH_LIMITS: {
|
|
3
4
|
readonly maxImages: 6;
|
|
4
5
|
readonly maxDocuments: 4;
|
|
5
6
|
readonly maxTextFiles: 6;
|
|
7
|
+
readonly maxVideos: 3;
|
|
6
8
|
};
|
|
7
9
|
export type PromptAttachmentsPayload = {
|
|
8
10
|
images: Array<{
|
|
@@ -17,19 +19,19 @@ export type PromptAttachmentsPayload = {
|
|
|
17
19
|
name: string;
|
|
18
20
|
content: string;
|
|
19
21
|
}>;
|
|
22
|
+
videos: Array<{
|
|
23
|
+
url: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
sizeBytes?: number;
|
|
26
|
+
}>;
|
|
20
27
|
};
|
|
21
|
-
export type MediaUploadResult =
|
|
22
|
-
kind: string;
|
|
23
|
-
url?: string;
|
|
24
|
-
key?: string;
|
|
25
|
-
filename?: string;
|
|
26
|
-
sizeBytes?: number;
|
|
27
|
-
content?: string;
|
|
28
|
-
};
|
|
28
|
+
export type MediaUploadResult = UploadedMedia;
|
|
29
29
|
export declare function emptyAttachments(): PromptAttachmentsPayload;
|
|
30
30
|
export declare function isHttpsUrl(s: string): boolean;
|
|
31
31
|
export declare function isMediaSitePath(s: string): boolean;
|
|
32
|
-
export declare function classifyRemoteByName(nameOrUrl: string): "image" | "document";
|
|
32
|
+
export declare function classifyRemoteByName(nameOrUrl: string): "image" | "document" | "video";
|
|
33
|
+
/** Agent normalizeAttachments only keeps videos on our Blob host under uploads/. */
|
|
34
|
+
export declare function isFrontpageBlobUploadUrl(url: string): boolean;
|
|
33
35
|
export declare function addHttpsRef(out: PromptAttachmentsPayload, url: string, opts?: {
|
|
34
36
|
name?: string;
|
|
35
37
|
asImageOnly?: boolean;
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* §CLI Phase C — resolve --attach / --asset-url into the agent attachments payload.
|
|
3
|
-
* Local files
|
|
2
|
+
* §CLI Phase C (+ E) — resolve --attach / --asset-url into the agent attachments payload.
|
|
3
|
+
* Local files use the same transport matrix as `frontpage media upload`.
|
|
4
4
|
*/
|
|
5
|
-
import { access
|
|
6
|
-
import { basename } from "node:path";
|
|
5
|
+
import { access } from "node:fs/promises";
|
|
7
6
|
import { constants as fsConstants } from "node:fs";
|
|
8
|
-
import { v1Get,
|
|
9
|
-
|
|
7
|
+
import { v1Get, CliHttpError, EXIT_USAGE, EXIT_UNEXPECTED } from "../http/client.js";
|
|
8
|
+
import { uploadLocalMedia } from "../media/upload.js";
|
|
9
|
+
import { VIDEO_UPLOAD_EXT, extOf } from "../media/limits.js";
|
|
10
|
+
/** Mirror ATTACH_LIMITS in src/lib/agent/attachments.ts. */
|
|
10
11
|
export const CLI_ATTACH_LIMITS = {
|
|
11
12
|
maxImages: 6,
|
|
12
13
|
maxDocuments: 4,
|
|
13
14
|
maxTextFiles: 6,
|
|
15
|
+
maxVideos: 3,
|
|
14
16
|
};
|
|
15
17
|
export function emptyAttachments() {
|
|
16
|
-
return { images: [], documents: [], textFiles: [] };
|
|
18
|
+
return { images: [], documents: [], textFiles: [], videos: [] };
|
|
17
19
|
}
|
|
18
20
|
export function isHttpsUrl(s) {
|
|
19
21
|
return /^https:\/\/[^\s]+$/i.test(s) && s.length <= 2048;
|
|
@@ -23,7 +25,9 @@ export function isMediaSitePath(s) {
|
|
|
23
25
|
}
|
|
24
26
|
export function classifyRemoteByName(nameOrUrl) {
|
|
25
27
|
const base = nameOrUrl.split(/[?#]/)[0] ?? nameOrUrl;
|
|
26
|
-
const ext =
|
|
28
|
+
const ext = extOf(base);
|
|
29
|
+
if (VIDEO_UPLOAD_EXT.has(ext))
|
|
30
|
+
return "video";
|
|
27
31
|
if (ext === "pdf")
|
|
28
32
|
return "document";
|
|
29
33
|
return "image";
|
|
@@ -42,6 +46,19 @@ function pushText(list, name, content) {
|
|
|
42
46
|
}
|
|
43
47
|
list.push({ name, content });
|
|
44
48
|
}
|
|
49
|
+
/** Agent normalizeAttachments only keeps videos on our Blob host under uploads/. */
|
|
50
|
+
export function isFrontpageBlobUploadUrl(url) {
|
|
51
|
+
try {
|
|
52
|
+
const u = new URL(url);
|
|
53
|
+
if (!u.hostname.endsWith(".blob.vercel-storage.com"))
|
|
54
|
+
return false;
|
|
55
|
+
const pathname = u.pathname.replace(/^\/+/, "");
|
|
56
|
+
return pathname.startsWith("uploads/");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
45
62
|
export function addHttpsRef(out, url, opts) {
|
|
46
63
|
if (!isHttpsUrl(url)) {
|
|
47
64
|
throw new CliHttpError(`Not an https URL: ${url}`, EXIT_USAGE, 400, "bad_request");
|
|
@@ -49,10 +66,16 @@ export function addHttpsRef(out, url, opts) {
|
|
|
49
66
|
const kind = opts?.asImageOnly ? "image" : classifyRemoteByName(opts?.name ?? url);
|
|
50
67
|
if (kind === "document") {
|
|
51
68
|
pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, url, opts?.name, "document");
|
|
69
|
+
return;
|
|
52
70
|
}
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
if (kind === "video") {
|
|
72
|
+
if (!isFrontpageBlobUploadUrl(url)) {
|
|
73
|
+
throw new CliHttpError("Video --attach URLs must be Frontpage Blob uploads (…blob.vercel-storage.com/uploads/…). Upload with frontpage media upload first, or pass a local .mp4/.webm/.mov file.", EXIT_USAGE, 400, "bad_request");
|
|
74
|
+
}
|
|
75
|
+
pushUniqueUrl(out.videos, CLI_ATTACH_LIMITS.maxVideos, url, opts?.name, "video");
|
|
76
|
+
return;
|
|
55
77
|
}
|
|
78
|
+
pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, url, opts?.name, "image");
|
|
56
79
|
}
|
|
57
80
|
export function addUploadResult(out, data) {
|
|
58
81
|
const name = data.filename?.trim() || undefined;
|
|
@@ -63,19 +86,31 @@ export function addUploadResult(out, data) {
|
|
|
63
86
|
pushText(out.textFiles, name ?? "snippet.txt", data.content);
|
|
64
87
|
return;
|
|
65
88
|
}
|
|
66
|
-
if (data.kind === "image" || data.kind === "document") {
|
|
89
|
+
if (data.kind === "image" || data.kind === "document" || data.kind === "video") {
|
|
67
90
|
if (!data.url || !isHttpsUrl(data.url)) {
|
|
68
91
|
throw new CliHttpError(`Upload of ${name ?? "file"} did not return a Blob URL.`, EXIT_UNEXPECTED, 500, "internal");
|
|
69
92
|
}
|
|
70
93
|
if (data.kind === "document") {
|
|
71
94
|
pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, data.url, name, "document");
|
|
95
|
+
return;
|
|
72
96
|
}
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
if (data.kind === "video") {
|
|
98
|
+
if (out.videos.length >= CLI_ATTACH_LIMITS.maxVideos) {
|
|
99
|
+
throw new CliHttpError(`Too many video attachments (max ${CLI_ATTACH_LIMITS.maxVideos}).`, EXIT_USAGE, 400, "bad_request");
|
|
100
|
+
}
|
|
101
|
+
if (!out.videos.some((x) => x.url === data.url)) {
|
|
102
|
+
out.videos.push({
|
|
103
|
+
url: data.url,
|
|
104
|
+
name,
|
|
105
|
+
sizeBytes: data.sizeBytes,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
75
109
|
}
|
|
110
|
+
pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, data.url, name, "image");
|
|
76
111
|
return;
|
|
77
112
|
}
|
|
78
|
-
throw new CliHttpError(`Unsupported upload kind "${data.kind}" for prompt attach. Use images, PDFs, or text/code files
|
|
113
|
+
throw new CliHttpError(`Unsupported upload kind "${data.kind}" for prompt attach. Use images, PDFs, videos, or text/code files.`, EXIT_USAGE, 400, "bad_request");
|
|
79
114
|
}
|
|
80
115
|
export async function uploadLocalForAttach(slug, filePath) {
|
|
81
116
|
try {
|
|
@@ -84,13 +119,7 @@ export async function uploadLocalForAttach(slug, filePath) {
|
|
|
84
119
|
catch {
|
|
85
120
|
throw new CliHttpError(`Cannot read file for --attach: ${filePath}`, EXIT_USAGE, 400, "bad_request");
|
|
86
121
|
}
|
|
87
|
-
|
|
88
|
-
if (bytes.length === 0) {
|
|
89
|
-
throw new CliHttpError(`File is empty: ${filePath}`, EXIT_USAGE, 400, "bad_request");
|
|
90
|
-
}
|
|
91
|
-
const form = new FormData();
|
|
92
|
-
form.append("file", new Blob([Uint8Array.from(bytes)]), basename(filePath));
|
|
93
|
-
return v1PostMultipart(`/api/v1/sites/${encodeURIComponent(slug)}/media`, form);
|
|
122
|
+
return uploadLocalMedia({ slug, filePath });
|
|
94
123
|
}
|
|
95
124
|
export async function resolveSitePathUrl(slug, sitePath) {
|
|
96
125
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/media`);
|
|
@@ -123,6 +152,9 @@ export async function resolveAttachRef(slug, ref, out) {
|
|
|
123
152
|
if (kind === "document") {
|
|
124
153
|
pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, asset.url, asset.name, "document");
|
|
125
154
|
}
|
|
155
|
+
else if (kind === "video") {
|
|
156
|
+
pushUniqueUrl(out.videos, CLI_ATTACH_LIMITS.maxVideos, asset.url, asset.name, "video");
|
|
157
|
+
}
|
|
126
158
|
else {
|
|
127
159
|
pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, asset.url, asset.name, "image");
|
|
128
160
|
}
|
|
@@ -143,7 +175,8 @@ export async function buildPromptAttachments(args) {
|
|
|
143
175
|
}
|
|
144
176
|
if (out.images.length === 0 &&
|
|
145
177
|
out.documents.length === 0 &&
|
|
146
|
-
out.textFiles.length === 0
|
|
178
|
+
out.textFiles.length === 0 &&
|
|
179
|
+
out.videos.length === 0) {
|
|
147
180
|
throw new CliHttpError("No attachments could be resolved from --attach / --asset-url.", EXIT_USAGE, 400, "bad_request");
|
|
148
181
|
}
|
|
149
182
|
return out;
|
package/dist/commands/prompt.js
CHANGED
|
@@ -29,6 +29,7 @@ export async function runPrompt(args) {
|
|
|
29
29
|
images: attachments.images.length,
|
|
30
30
|
documents: attachments.documents.length,
|
|
31
31
|
textFiles: attachments.textFiles.length,
|
|
32
|
+
videos: attachments.videos.length,
|
|
32
33
|
}
|
|
33
34
|
: undefined,
|
|
34
35
|
});
|
|
@@ -40,7 +41,8 @@ export async function runPrompt(args) {
|
|
|
40
41
|
if (attachments) {
|
|
41
42
|
const n = attachments.images.length +
|
|
42
43
|
attachments.documents.length +
|
|
43
|
-
attachments.textFiles.length
|
|
44
|
+
attachments.textFiles.length +
|
|
45
|
+
attachments.videos.length;
|
|
44
46
|
console.error(`(attached ${n} file${n === 1 ? "" : "s"})`);
|
|
45
47
|
}
|
|
46
48
|
}
|
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;
|