frontpage-host 0.1.7 → 0.1.9
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 +10 -6
- package/dist/commands/deploy.js +9 -27
- package/dist/commands/media.js +24 -14
- 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/push.js +55 -19
- package/dist/commands/runs.js +33 -19
- package/dist/commands/wait-agent-idle.d.ts +49 -0
- package/dist/commands/wait-agent-idle.js +181 -0
- package/dist/http/client.d.ts +13 -7
- package/dist/http/client.js +46 -51
- 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 +140 -0
- package/dist/registry.js +41 -16
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +32 -12
- package/llms.txt +5 -3
- 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). Use returned `sitePath` (`/__media/uploads/<slug>/…`) in markup — never raw blob hosts. 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,14 +122,18 @@ 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
|
|
125
|
+
frontpage media upload ./clip.mp4
|
|
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.
|
|
134
|
+
- `runs wait` with `--json`: failures use `ok: false` and include `data.run` (with `run.error` when present).
|
|
135
|
+
- `deploy wait` is fail-closed: exit 2 if the waited run (or latest, when already idle) is `failed`/`canceled`.
|
|
136
|
+
- `frontpage push --wait` waits for agent-run idle after push (same busy statuses; not a separate Vercel oracle).
|
|
133
137
|
|
|
134
138
|
Mid-task continuation is automatic. There is **no** `runs continue` in v1.
|
|
135
139
|
|
|
@@ -173,10 +177,10 @@ Global machine envelope (`--json`):
|
|
|
173
177
|
|
|
174
178
|
```json
|
|
175
179
|
{ "ok": true, "command": "status", "data": { } }
|
|
176
|
-
{ "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3 } }
|
|
180
|
+
{ "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3, "requestId": "…" } }
|
|
177
181
|
```
|
|
178
182
|
|
|
179
|
-
Failures with `--json` print the envelope on **stdout** (still use the process exit code).
|
|
183
|
+
Failures with `--json` print the envelope on **stdout** (still use the process exit code). Include `requestId` when filing support issues.
|
|
180
184
|
|
|
181
185
|
## Global flags
|
|
182
186
|
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import {
|
|
3
|
-
import { emitErr,
|
|
4
|
-
import {
|
|
2
|
+
import { EXIT_USAGE } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitUsage } from "../runtime.js";
|
|
4
|
+
import { emitWaitOutcome, waitForAgentIdle } from "./wait-agent-idle.js";
|
|
5
5
|
/** `frontpage deploy wait` — wait until no run is in deploying/queued/running. */
|
|
6
6
|
export async function runDeploy(args) {
|
|
7
7
|
try {
|
|
@@ -10,30 +10,12 @@ export async function runDeploy(args) {
|
|
|
10
10
|
return emitUsage("deploy", "Usage: frontpage deploy wait [--site <slug>]", EXIT_USAGE);
|
|
11
11
|
}
|
|
12
12
|
const { slug } = await resolveSlug(args.slice(1));
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (isJson()) {
|
|
20
|
-
emitOk("deploy", {
|
|
21
|
-
slug,
|
|
22
|
-
waited: true,
|
|
23
|
-
run: data.run,
|
|
24
|
-
status: data.run?.status ?? "idle",
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
console.log(data.run ? `${data.run.id} ${data.run.status}` : "No active deploy.");
|
|
29
|
-
}
|
|
30
|
-
return EXIT_OK;
|
|
31
|
-
}
|
|
32
|
-
const head = busy.find((r) => r.status === "deploying") ?? busy[0];
|
|
33
|
-
progress(`\r${head.status} ${head.statusMessage ?? ""} `);
|
|
34
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
35
|
-
}
|
|
36
|
-
return emitUsage("deploy", "Timed out waiting for deploy.", EXIT_USAGE);
|
|
13
|
+
const result = await waitForAgentIdle({
|
|
14
|
+
slug,
|
|
15
|
+
// Fail-closed: idle with latest failed/canceled is not success.
|
|
16
|
+
failClosedOnIdleTerminalFailure: true,
|
|
17
|
+
});
|
|
18
|
+
return emitWaitOutcome({ command: "deploy", slug, result });
|
|
37
19
|
}
|
|
38
20
|
catch (err) {
|
|
39
21
|
return emitErr("deploy", err);
|
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,32 @@ 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
|
-
console.log(data.url
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
if (data.url)
|
|
38
|
+
console.log(data.url);
|
|
39
|
+
if (data.sitePath)
|
|
40
|
+
console.log(`sitePath: ${data.sitePath}`);
|
|
41
|
+
else if (data.key)
|
|
42
|
+
console.log(`sitePath: /__media/${data.key}`);
|
|
35
43
|
return EXIT_OK;
|
|
36
44
|
}
|
|
37
|
-
console.error("Usage: frontpage media list|upload <file> [--site <slug>]");
|
|
45
|
+
console.error("Usage: frontpage media list|upload <file> [--site <slug>] [--json]");
|
|
38
46
|
return EXIT_USAGE;
|
|
39
47
|
}
|
|
40
48
|
catch (err) {
|
|
49
|
+
if (isJson())
|
|
50
|
+
return emitErr("media", err);
|
|
41
51
|
console.error(err instanceof Error ? err.message : String(err));
|
|
42
52
|
return exitCodeForError(err);
|
|
43
53
|
}
|
|
@@ -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/push.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveSlug } from "../site.js";
|
|
|
4
4
|
import { EXIT_OK, EXIT_USAGE, v1Put } from "../http/client.js";
|
|
5
5
|
import { readState, writeState } from "../state/frontpage-state.js";
|
|
6
6
|
import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
|
|
7
|
+
import { emitWaitOutcome, waitForAgentIdle } from "./wait-agent-idle.js";
|
|
7
8
|
const TEXT_EXTS = new Set([
|
|
8
9
|
".astro",
|
|
9
10
|
".ts",
|
|
@@ -59,13 +60,22 @@ function isTextPath(repoPath) {
|
|
|
59
60
|
return true;
|
|
60
61
|
return TEXT_EXTS.has(repoPath.slice(i).toLowerCase());
|
|
61
62
|
}
|
|
63
|
+
function takeWaitFlag(rest) {
|
|
64
|
+
const i = rest.indexOf("--wait");
|
|
65
|
+
if (i === -1)
|
|
66
|
+
return false;
|
|
67
|
+
rest.splice(i, 1);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
62
70
|
export async function runPush(args) {
|
|
63
71
|
try {
|
|
64
72
|
const { slug, rest } = await resolveSlug(args);
|
|
65
|
-
const
|
|
73
|
+
const work = [...rest];
|
|
74
|
+
const wait = takeWaitFlag(work);
|
|
75
|
+
const messageIdx = work.indexOf("--message");
|
|
66
76
|
const message = messageIdx >= 0
|
|
67
|
-
?
|
|
68
|
-
:
|
|
77
|
+
? work[messageIdx + 1]
|
|
78
|
+
: work.find((a) => a.startsWith("--message="))?.slice("--message=".length);
|
|
69
79
|
const cwd = process.cwd();
|
|
70
80
|
const state = await readState(cwd);
|
|
71
81
|
if (!state.baseCommitSha) {
|
|
@@ -99,25 +109,51 @@ export async function runPush(args) {
|
|
|
99
109
|
if (data.commitSha) {
|
|
100
110
|
await writeState({ ...state, slug, baseCommitSha: data.commitSha }, cwd);
|
|
101
111
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
const pushPayload = {
|
|
113
|
+
slug,
|
|
114
|
+
changed: data.changed,
|
|
115
|
+
commitSha: data.commitSha,
|
|
116
|
+
baseCommitSha: data.baseCommitSha,
|
|
117
|
+
filesChanged: data.filesChanged,
|
|
118
|
+
};
|
|
119
|
+
if (!wait) {
|
|
120
|
+
if (isJson()) {
|
|
121
|
+
emitOk("push", pushPayload);
|
|
122
|
+
return EXIT_OK;
|
|
123
|
+
}
|
|
124
|
+
if (!data.changed) {
|
|
125
|
+
console.log("Nothing to commit (remote already matches).");
|
|
126
|
+
return EXIT_OK;
|
|
127
|
+
}
|
|
128
|
+
console.log(`Pushed ${data.filesChanged.length} file(s) → ${data.commitSha}`);
|
|
129
|
+
console.log("Updated .frontpage/state.json baseCommitSha");
|
|
130
|
+
console.log("Note: module markup may auto-activate modules (same as the editor agent).");
|
|
131
|
+
console.log("tokens.ts may be auto-repaired at commit if it would break the build.");
|
|
110
132
|
return EXIT_OK;
|
|
111
133
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
// --wait: after push, wait for agent-run busy to clear (same statuses as deploy wait).
|
|
135
|
+
// Does not invent a Vercel deploy oracle; file-only pushes with no active run return promptly.
|
|
136
|
+
if (!isJson()) {
|
|
137
|
+
if (!data.changed) {
|
|
138
|
+
console.log("Nothing to commit (remote already matches).");
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.log(`Pushed ${data.filesChanged.length} file(s) → ${data.commitSha}`);
|
|
142
|
+
console.log("Updated .frontpage/state.json baseCommitSha");
|
|
143
|
+
}
|
|
144
|
+
console.error("Waiting for agent run / deploy idle…");
|
|
115
145
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
const result = await waitForAgentIdle({
|
|
147
|
+
slug,
|
|
148
|
+
// Do not fail on an ancient canceled run when this push never saw busy work.
|
|
149
|
+
failClosedOnIdleTerminalFailure: false,
|
|
150
|
+
});
|
|
151
|
+
return emitWaitOutcome({
|
|
152
|
+
command: "push",
|
|
153
|
+
slug,
|
|
154
|
+
result,
|
|
155
|
+
extra: { ...pushPayload, waited: true },
|
|
156
|
+
});
|
|
121
157
|
}
|
|
122
158
|
catch (err) {
|
|
123
159
|
return emitErr("push", err);
|
package/dist/commands/runs.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
2
|
import { EXIT_OK, EXIT_USAGE, v1Get, v1Post } from "../http/client.js";
|
|
3
|
-
import { emitErr, emitOk, emitUsage, isJson, progress } from "../runtime.js";
|
|
3
|
+
import { emitErr, emitFail, emitOk, emitUsage, isJson, progress } from "../runtime.js";
|
|
4
4
|
const TERMINAL = new Set(["succeeded", "failed", "canceled"]);
|
|
5
|
+
const FAIL_TERMINAL = new Set(["failed", "canceled"]);
|
|
5
6
|
function printRun(run) {
|
|
6
|
-
const msg = run.statusMessage || run.summary ||
|
|
7
|
+
const msg = run.statusMessage || run.summary || "";
|
|
7
8
|
console.log(`${run.id} ${run.status}${msg ? ` ${msg}` : ""}`);
|
|
9
|
+
if (run.error)
|
|
10
|
+
console.log(` error: ${run.error}`);
|
|
8
11
|
if (run.commitSha)
|
|
9
12
|
console.log(` commit: ${run.commitSha}`);
|
|
10
13
|
}
|
|
14
|
+
/** Finish runs wait — honest JSON (ok:false on failure) + always surface run.error. */
|
|
15
|
+
function finishWait(slug, run) {
|
|
16
|
+
const failed = FAIL_TERMINAL.has(run.status);
|
|
17
|
+
if (isJson()) {
|
|
18
|
+
if (failed) {
|
|
19
|
+
return emitFail("runs", {
|
|
20
|
+
message: run.error || run.statusMessage || `Run ${run.status}`,
|
|
21
|
+
exitCode: EXIT_USAGE,
|
|
22
|
+
data: { slug, action: "wait", run },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
emitOk("runs", { slug, action: "wait", run });
|
|
26
|
+
return EXIT_OK;
|
|
27
|
+
}
|
|
28
|
+
printRun(run);
|
|
29
|
+
return failed ? EXIT_USAGE : EXIT_OK;
|
|
30
|
+
}
|
|
11
31
|
export async function runRuns(args) {
|
|
12
32
|
try {
|
|
13
33
|
const sub = args[0];
|
|
@@ -84,17 +104,7 @@ async function waitRun(args) {
|
|
|
84
104
|
progress(`\r${data.run.status} ${data.run.statusMessage ?? ""} `);
|
|
85
105
|
if (TERMINAL.has(data.run.status)) {
|
|
86
106
|
progress("\n");
|
|
87
|
-
|
|
88
|
-
emitOk("runs", { slug, action: "wait", run: data.run });
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
printRun(data.run);
|
|
92
|
-
}
|
|
93
|
-
if (data.run.status === "failed")
|
|
94
|
-
return EXIT_USAGE;
|
|
95
|
-
if (data.run.status === "canceled")
|
|
96
|
-
return EXIT_USAGE;
|
|
97
|
-
return EXIT_OK;
|
|
107
|
+
return finishWait(slug, data.run);
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
else {
|
|
@@ -107,13 +117,17 @@ async function waitRun(args) {
|
|
|
107
117
|
progress(`\r${current.status} ${current.statusMessage ?? ""} `);
|
|
108
118
|
if (TERMINAL.has(current.status) && !data.poll?.active) {
|
|
109
119
|
progress("\n");
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
// Refresh full run for error field when list view is thin.
|
|
121
|
+
if (!current.error) {
|
|
122
|
+
try {
|
|
123
|
+
const full = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs/${encodeURIComponent(current.id)}`);
|
|
124
|
+
return finishWait(slug, full.run);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return finishWait(slug, current);
|
|
128
|
+
}
|
|
115
129
|
}
|
|
116
|
-
return current
|
|
130
|
+
return finishWait(slug, current);
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
else {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type WaitRunSnapshot = {
|
|
2
|
+
id: string;
|
|
3
|
+
status: string;
|
|
4
|
+
statusMessage?: string | null;
|
|
5
|
+
summary?: string | null;
|
|
6
|
+
error?: string | null;
|
|
7
|
+
commitSha?: string | null;
|
|
8
|
+
};
|
|
9
|
+
export type WaitIdleResult = {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
/** Run that determined the outcome (watched busy run, or latest when idle). */
|
|
12
|
+
run: WaitRunSnapshot | null;
|
|
13
|
+
/** True when we observed queued/running/deploying during this wait. */
|
|
14
|
+
observedBusy: boolean;
|
|
15
|
+
status: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function isBusyStatus(status: string): boolean;
|
|
18
|
+
export declare function isFailTerminal(status: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Decide exit after idle. Pure — unit-tested.
|
|
21
|
+
* - If we watched a busy run end: succeeded → 0; failed/canceled → 2.
|
|
22
|
+
* - If never busy: optional fail-closed on latest terminal failure (deploy wait);
|
|
23
|
+
* otherwise idle/succeeded → 0 (push --wait after a file-only push).
|
|
24
|
+
*/
|
|
25
|
+
export declare function decideIdleExit(args: {
|
|
26
|
+
watched: WaitRunSnapshot | null;
|
|
27
|
+
latest: WaitRunSnapshot | null;
|
|
28
|
+
observedBusy: boolean;
|
|
29
|
+
failClosedOnIdleTerminalFailure: boolean;
|
|
30
|
+
}): {
|
|
31
|
+
exitCode: number;
|
|
32
|
+
run: WaitRunSnapshot | null;
|
|
33
|
+
status: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function waitForAgentIdle(args: {
|
|
36
|
+
slug: string;
|
|
37
|
+
/** When true, idle + latest failed/canceled → non-zero (deploy wait). */
|
|
38
|
+
failClosedOnIdleTerminalFailure: boolean;
|
|
39
|
+
maxMs?: number;
|
|
40
|
+
pollMs?: number;
|
|
41
|
+
}): Promise<WaitIdleResult>;
|
|
42
|
+
/** Emit deploy/push-wait JSON or text from a WaitIdleResult. */
|
|
43
|
+
export declare function emitWaitOutcome(args: {
|
|
44
|
+
command: "deploy" | "push";
|
|
45
|
+
slug: string;
|
|
46
|
+
result: WaitIdleResult;
|
|
47
|
+
/** Extra fields merged into JSON data (e.g. push commitSha). */
|
|
48
|
+
extra?: Record<string, unknown>;
|
|
49
|
+
}): number;
|