frontpage-host 0.1.6 → 0.1.8
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 +14 -3
- package/dist/commands/deploy.js +9 -27
- package/dist/commands/prompt-attach.d.ts +59 -0
- package/dist/commands/prompt-attach.js +207 -0
- package/dist/commands/prompt.js +28 -18
- 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/completions/generate.js +1 -0
- package/dist/http/client.d.ts +11 -7
- package/dist/http/client.js +38 -51
- package/dist/registry.js +43 -14
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +32 -12
- package/llms.txt +4 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -119,10 +119,21 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
119
119
|
|
|
120
120
|
```bash
|
|
121
121
|
frontpage prompt "make the hero shorter"
|
|
122
|
-
frontpage
|
|
122
|
+
frontpage prompt "Use this logo in the header" --attach ./logo.png
|
|
123
|
+
frontpage prompt "Match this brand PDF" --attach ./guide.pdf --path /
|
|
124
|
+
frontpage prompt "Reuse uploaded art" --attach /__media/uploads/<slug>/<file>
|
|
125
|
+
frontpage runs wait # wait for the agent run to finish (failed/canceled → exit 2)
|
|
123
126
|
# Review https://<subdomain>.frontpage.host — do not publish unless asked.
|
|
124
127
|
```
|
|
125
128
|
|
|
129
|
+
- `--attach` accepts a local file (uploads via the same path as `frontpage media upload`), an existing `/__media/…` site path, or an `https://` Blob URL. Repeatable.
|
|
130
|
+
- `--asset-url <https://…>` adds a legacy image URL. Repeatable.
|
|
131
|
+
- Caps match the media proxy: images / PDF / text-code. CLI video and >4 MB binaries are not supported yet (editor attach / Phase E).
|
|
132
|
+
- Prompt text is still required (API). Attachments ride on the same enqueue as the editor.
|
|
133
|
+
- `runs wait` with `--json`: failures use `ok: false` and include `data.run` (with `run.error` when present).
|
|
134
|
+
- `deploy wait` is fail-closed: exit 2 if the waited run (or latest, when already idle) is `failed`/`canceled`.
|
|
135
|
+
- `frontpage push --wait` waits for agent-run idle after push (same busy statuses; not a separate Vercel oracle).
|
|
136
|
+
|
|
126
137
|
Mid-task continuation is automatic. There is **no** `runs continue` in v1.
|
|
127
138
|
|
|
128
139
|
## Content / JSON-native commands
|
|
@@ -165,10 +176,10 @@ Global machine envelope (`--json`):
|
|
|
165
176
|
|
|
166
177
|
```json
|
|
167
178
|
{ "ok": true, "command": "status", "data": { } }
|
|
168
|
-
{ "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3 } }
|
|
179
|
+
{ "ok": false, "command": "push", "error": { "message": "…", "code": "conflict", "httpStatus": 409, "exitCode": 3, "requestId": "…" } }
|
|
169
180
|
```
|
|
170
181
|
|
|
171
|
-
Failures with `--json` print the envelope on **stdout** (still use the process exit code).
|
|
182
|
+
Failures with `--json` print the envelope on **stdout** (still use the process exit code). Include `requestId` when filing support issues.
|
|
172
183
|
|
|
173
184
|
## Global flags
|
|
174
185
|
|
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);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Mirror ATTACH_LIMITS in src/lib/agent/attachments.ts (images/docs/text only via proxy media). */
|
|
2
|
+
export declare const CLI_ATTACH_LIMITS: {
|
|
3
|
+
readonly maxImages: 6;
|
|
4
|
+
readonly maxDocuments: 4;
|
|
5
|
+
readonly maxTextFiles: 6;
|
|
6
|
+
};
|
|
7
|
+
export type PromptAttachmentsPayload = {
|
|
8
|
+
images: Array<{
|
|
9
|
+
url: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
}>;
|
|
12
|
+
documents: Array<{
|
|
13
|
+
url: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
}>;
|
|
16
|
+
textFiles: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
content: string;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
export type MediaUploadResult = {
|
|
22
|
+
kind: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
key?: string;
|
|
25
|
+
filename?: string;
|
|
26
|
+
sizeBytes?: number;
|
|
27
|
+
content?: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function emptyAttachments(): PromptAttachmentsPayload;
|
|
30
|
+
export declare function isHttpsUrl(s: string): boolean;
|
|
31
|
+
export declare function isMediaSitePath(s: string): boolean;
|
|
32
|
+
export declare function classifyRemoteByName(nameOrUrl: string): "image" | "document";
|
|
33
|
+
export declare function addHttpsRef(out: PromptAttachmentsPayload, url: string, opts?: {
|
|
34
|
+
name?: string;
|
|
35
|
+
asImageOnly?: boolean;
|
|
36
|
+
}): void;
|
|
37
|
+
export declare function addUploadResult(out: PromptAttachmentsPayload, data: MediaUploadResult): void;
|
|
38
|
+
export declare function uploadLocalForAttach(slug: string, filePath: string): Promise<MediaUploadResult>;
|
|
39
|
+
export declare function resolveSitePathUrl(slug: string, sitePath: string): Promise<{
|
|
40
|
+
url: string;
|
|
41
|
+
name: string;
|
|
42
|
+
type: string;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve one --attach value (local path, /__media/…, or https URL) into the payload.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveAttachRef(slug: string, ref: string, out: PromptAttachmentsPayload): Promise<void>;
|
|
48
|
+
export declare function buildPromptAttachments(args: {
|
|
49
|
+
slug: string;
|
|
50
|
+
attachRefs: string[];
|
|
51
|
+
assetUrls: string[];
|
|
52
|
+
}): Promise<PromptAttachmentsPayload | undefined>;
|
|
53
|
+
/** Parse prompt argv: prompt words + --path / --attach / --asset-url flags. */
|
|
54
|
+
export declare function parsePromptArgs(rest: string[]): {
|
|
55
|
+
prompt: string;
|
|
56
|
+
currentPath: string;
|
|
57
|
+
attachRefs: string[];
|
|
58
|
+
assetUrls: string[];
|
|
59
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §CLI Phase C — resolve --attach / --asset-url into the agent attachments payload.
|
|
3
|
+
* Local files upload via the same multipart media endpoint as `frontpage media upload`.
|
|
4
|
+
*/
|
|
5
|
+
import { access, readFile } from "node:fs/promises";
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
import { constants as fsConstants } from "node:fs";
|
|
8
|
+
import { v1Get, v1PostMultipart, CliHttpError, EXIT_USAGE, EXIT_UNEXPECTED } from "../http/client.js";
|
|
9
|
+
/** Mirror ATTACH_LIMITS in src/lib/agent/attachments.ts (images/docs/text only via proxy media). */
|
|
10
|
+
export const CLI_ATTACH_LIMITS = {
|
|
11
|
+
maxImages: 6,
|
|
12
|
+
maxDocuments: 4,
|
|
13
|
+
maxTextFiles: 6,
|
|
14
|
+
};
|
|
15
|
+
export function emptyAttachments() {
|
|
16
|
+
return { images: [], documents: [], textFiles: [] };
|
|
17
|
+
}
|
|
18
|
+
export function isHttpsUrl(s) {
|
|
19
|
+
return /^https:\/\/[^\s]+$/i.test(s) && s.length <= 2048;
|
|
20
|
+
}
|
|
21
|
+
export function isMediaSitePath(s) {
|
|
22
|
+
return s.startsWith("/__media/");
|
|
23
|
+
}
|
|
24
|
+
export function classifyRemoteByName(nameOrUrl) {
|
|
25
|
+
const base = nameOrUrl.split(/[?#]/)[0] ?? nameOrUrl;
|
|
26
|
+
const ext = base.includes(".") ? (base.split(".").pop() ?? "").toLowerCase() : "";
|
|
27
|
+
if (ext === "pdf")
|
|
28
|
+
return "document";
|
|
29
|
+
return "image";
|
|
30
|
+
}
|
|
31
|
+
function pushUniqueUrl(list, max, url, name, label) {
|
|
32
|
+
if (list.some((x) => x.url === url))
|
|
33
|
+
return;
|
|
34
|
+
if (list.length >= max) {
|
|
35
|
+
throw new CliHttpError(`Too many ${label} attachments (max ${max}).`, EXIT_USAGE, 400, "bad_request");
|
|
36
|
+
}
|
|
37
|
+
list.push(name ? { url, name } : { url });
|
|
38
|
+
}
|
|
39
|
+
function pushText(list, name, content) {
|
|
40
|
+
if (list.length >= CLI_ATTACH_LIMITS.maxTextFiles) {
|
|
41
|
+
throw new CliHttpError(`Too many text attachments (max ${CLI_ATTACH_LIMITS.maxTextFiles}).`, EXIT_USAGE, 400, "bad_request");
|
|
42
|
+
}
|
|
43
|
+
list.push({ name, content });
|
|
44
|
+
}
|
|
45
|
+
export function addHttpsRef(out, url, opts) {
|
|
46
|
+
if (!isHttpsUrl(url)) {
|
|
47
|
+
throw new CliHttpError(`Not an https URL: ${url}`, EXIT_USAGE, 400, "bad_request");
|
|
48
|
+
}
|
|
49
|
+
const kind = opts?.asImageOnly ? "image" : classifyRemoteByName(opts?.name ?? url);
|
|
50
|
+
if (kind === "document") {
|
|
51
|
+
pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, url, opts?.name, "document");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, url, opts?.name, "image");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function addUploadResult(out, data) {
|
|
58
|
+
const name = data.filename?.trim() || undefined;
|
|
59
|
+
if (data.kind === "text") {
|
|
60
|
+
if (typeof data.content !== "string" || !data.content) {
|
|
61
|
+
throw new CliHttpError("Text upload returned no content.", EXIT_UNEXPECTED, 500, "internal");
|
|
62
|
+
}
|
|
63
|
+
pushText(out.textFiles, name ?? "snippet.txt", data.content);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (data.kind === "image" || data.kind === "document") {
|
|
67
|
+
if (!data.url || !isHttpsUrl(data.url)) {
|
|
68
|
+
throw new CliHttpError(`Upload of ${name ?? "file"} did not return a Blob URL.`, EXIT_UNEXPECTED, 500, "internal");
|
|
69
|
+
}
|
|
70
|
+
if (data.kind === "document") {
|
|
71
|
+
pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, data.url, name, "document");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, data.url, name, "image");
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw new CliHttpError(`Unsupported upload kind "${data.kind}" for prompt attach. Use images, PDFs, or text/code files (video/large files need the editor path until Phase E).`, EXIT_USAGE, 400, "bad_request");
|
|
79
|
+
}
|
|
80
|
+
export async function uploadLocalForAttach(slug, filePath) {
|
|
81
|
+
try {
|
|
82
|
+
await access(filePath, fsConstants.R_OK);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new CliHttpError(`Cannot read file for --attach: ${filePath}`, EXIT_USAGE, 400, "bad_request");
|
|
86
|
+
}
|
|
87
|
+
const bytes = await readFile(filePath);
|
|
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);
|
|
94
|
+
}
|
|
95
|
+
export async function resolveSitePathUrl(slug, sitePath) {
|
|
96
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/media`);
|
|
97
|
+
const norm = (p) => p.replace(/\/+/g, "/").replace(/\/$/, "") || p;
|
|
98
|
+
const want = norm(sitePath);
|
|
99
|
+
const hit = data.assets.find((a) => a.sitePath === sitePath || norm(a.sitePath) === want);
|
|
100
|
+
if (!hit?.url) {
|
|
101
|
+
throw new CliHttpError(`No media asset found for ${sitePath}. Upload with frontpage media upload, or pass a Blob https URL.`, EXIT_USAGE, 404, "not_found");
|
|
102
|
+
}
|
|
103
|
+
return { url: hit.url, name: hit.name, type: hit.type };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve one --attach value (local path, /__media/…, or https URL) into the payload.
|
|
107
|
+
*/
|
|
108
|
+
export async function resolveAttachRef(slug, ref, out) {
|
|
109
|
+
const raw = ref.trim();
|
|
110
|
+
if (!raw) {
|
|
111
|
+
throw new CliHttpError("--attach requires a path or URL", EXIT_USAGE, 400, "bad_request");
|
|
112
|
+
}
|
|
113
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
114
|
+
if (!isHttpsUrl(raw)) {
|
|
115
|
+
throw new CliHttpError(`--attach URLs must be https:// (got ${raw.split(":")[0]}://)`, EXIT_USAGE, 400, "bad_request");
|
|
116
|
+
}
|
|
117
|
+
addHttpsRef(out, raw);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (isMediaSitePath(raw)) {
|
|
121
|
+
const asset = await resolveSitePathUrl(slug, raw);
|
|
122
|
+
const kind = classifyRemoteByName(asset.name || asset.type || raw);
|
|
123
|
+
if (kind === "document") {
|
|
124
|
+
pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, asset.url, asset.name, "document");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, asset.url, asset.name, "image");
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const uploaded = await uploadLocalForAttach(slug, raw);
|
|
132
|
+
addUploadResult(out, uploaded);
|
|
133
|
+
}
|
|
134
|
+
export async function buildPromptAttachments(args) {
|
|
135
|
+
if (args.attachRefs.length === 0 && args.assetUrls.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
const out = emptyAttachments();
|
|
138
|
+
for (const url of args.assetUrls) {
|
|
139
|
+
addHttpsRef(out, url.trim(), { asImageOnly: true });
|
|
140
|
+
}
|
|
141
|
+
for (const ref of args.attachRefs) {
|
|
142
|
+
await resolveAttachRef(args.slug, ref, out);
|
|
143
|
+
}
|
|
144
|
+
if (out.images.length === 0 &&
|
|
145
|
+
out.documents.length === 0 &&
|
|
146
|
+
out.textFiles.length === 0) {
|
|
147
|
+
throw new CliHttpError("No attachments could be resolved from --attach / --asset-url.", EXIT_USAGE, 400, "bad_request");
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/** Parse prompt argv: prompt words + --path / --attach / --asset-url flags. */
|
|
152
|
+
export function parsePromptArgs(rest) {
|
|
153
|
+
let currentPath = "/";
|
|
154
|
+
const promptParts = [];
|
|
155
|
+
const attachRefs = [];
|
|
156
|
+
const assetUrls = [];
|
|
157
|
+
for (let i = 0; i < rest.length; i++) {
|
|
158
|
+
const a = rest[i];
|
|
159
|
+
if (a === "--path") {
|
|
160
|
+
currentPath = rest[++i] ?? "/";
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (a.startsWith("--path=")) {
|
|
164
|
+
currentPath = a.slice("--path=".length) || "/";
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (a === "--attach") {
|
|
168
|
+
const v = rest[++i];
|
|
169
|
+
if (!v || v.startsWith("--")) {
|
|
170
|
+
throw new CliHttpError('Usage: frontpage prompt "…" --attach <file|/__media/…|https://…>', EXIT_USAGE, 400, "bad_request");
|
|
171
|
+
}
|
|
172
|
+
attachRefs.push(v);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (a.startsWith("--attach=")) {
|
|
176
|
+
const v = a.slice("--attach=".length);
|
|
177
|
+
if (!v) {
|
|
178
|
+
throw new CliHttpError("--attach= requires a value", EXIT_USAGE, 400, "bad_request");
|
|
179
|
+
}
|
|
180
|
+
attachRefs.push(v);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (a === "--asset-url") {
|
|
184
|
+
const v = rest[++i];
|
|
185
|
+
if (!v || v.startsWith("--")) {
|
|
186
|
+
throw new CliHttpError("Usage: frontpage prompt \"…\" --asset-url <https://…>", EXIT_USAGE, 400, "bad_request");
|
|
187
|
+
}
|
|
188
|
+
assetUrls.push(v);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (a.startsWith("--asset-url=")) {
|
|
192
|
+
const v = a.slice("--asset-url=".length);
|
|
193
|
+
if (!v) {
|
|
194
|
+
throw new CliHttpError("--asset-url= requires a value", EXIT_USAGE, 400, "bad_request");
|
|
195
|
+
}
|
|
196
|
+
assetUrls.push(v);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
promptParts.push(a);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
prompt: promptParts.join(" ").trim(),
|
|
203
|
+
currentPath,
|
|
204
|
+
attachRefs,
|
|
205
|
+
assetUrls,
|
|
206
|
+
};
|
|
207
|
+
}
|
package/dist/commands/prompt.js
CHANGED
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
2
|
import { EXIT_OK, EXIT_USAGE, v1Post } from "../http/client.js";
|
|
3
3
|
import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
|
|
4
|
+
import { buildPromptAttachments, parsePromptArgs } from "./prompt-attach.js";
|
|
4
5
|
export async function runPrompt(args) {
|
|
5
6
|
try {
|
|
6
7
|
const { slug, rest } = await resolveSlug(args);
|
|
7
|
-
|
|
8
|
-
const promptParts = [];
|
|
9
|
-
for (let i = 0; i < rest.length; i++) {
|
|
10
|
-
const a = rest[i];
|
|
11
|
-
if (a === "--path") {
|
|
12
|
-
currentPath = rest[++i] ?? "/";
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (a.startsWith("--path=")) {
|
|
16
|
-
currentPath = a.slice("--path=".length) || "/";
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
promptParts.push(a);
|
|
20
|
-
}
|
|
21
|
-
const prompt = promptParts.join(" ").trim();
|
|
8
|
+
const { prompt, currentPath, attachRefs, assetUrls } = parsePromptArgs(rest);
|
|
22
9
|
if (!prompt) {
|
|
23
|
-
return emitUsage("prompt", 'Usage: frontpage prompt "..." [--site <slug>] [--path /page]', EXIT_USAGE);
|
|
10
|
+
return emitUsage("prompt", 'Usage: frontpage prompt "..." [--site <slug>] [--path /page] [--attach <file|/__media/…|https://…>]… [--asset-url <https://…>]…', EXIT_USAGE);
|
|
24
11
|
}
|
|
25
12
|
if (!currentPath.startsWith("/")) {
|
|
26
13
|
return emitUsage("prompt", "currentPath must start with /", EXIT_USAGE);
|
|
27
14
|
}
|
|
28
|
-
const
|
|
15
|
+
const attachments = await buildPromptAttachments({ slug, attachRefs, assetUrls });
|
|
16
|
+
const body = { prompt, currentPath };
|
|
17
|
+
if (attachments) {
|
|
18
|
+
body.attachments = attachments;
|
|
19
|
+
// Legacy image list for run display / back-compat readers.
|
|
20
|
+
body.assetUrls = attachments.images.map((i) => i.url);
|
|
21
|
+
}
|
|
22
|
+
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`, body);
|
|
29
23
|
if (isJson()) {
|
|
30
|
-
emitOk("prompt", {
|
|
24
|
+
emitOk("prompt", {
|
|
25
|
+
slug,
|
|
26
|
+
...data,
|
|
27
|
+
attached: attachments
|
|
28
|
+
? {
|
|
29
|
+
images: attachments.images.length,
|
|
30
|
+
documents: attachments.documents.length,
|
|
31
|
+
textFiles: attachments.textFiles.length,
|
|
32
|
+
}
|
|
33
|
+
: undefined,
|
|
34
|
+
});
|
|
31
35
|
}
|
|
32
36
|
else {
|
|
33
37
|
console.log(data.runId);
|
|
34
38
|
if (data.coalesced)
|
|
35
39
|
console.error("(coalesced with an identical queued run)");
|
|
40
|
+
if (attachments) {
|
|
41
|
+
const n = attachments.images.length +
|
|
42
|
+
attachments.documents.length +
|
|
43
|
+
attachments.textFiles.length;
|
|
44
|
+
console.error(`(attached ${n} file${n === 1 ? "" : "s"})`);
|
|
45
|
+
}
|
|
36
46
|
}
|
|
37
47
|
return EXIT_OK;
|
|
38
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;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §CLI Phase D — shared wait for agent-run busy states (queued/running/deploying).
|
|
3
|
+
* Used by `deploy wait` and `push --wait`. Not a Vercel deploy oracle.
|
|
4
|
+
*/
|
|
5
|
+
import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
|
|
6
|
+
import { emitFail, emitOk, isJson, progress } from "../runtime.js";
|
|
7
|
+
const BUSY = new Set(["queued", "running", "deploying"]);
|
|
8
|
+
const FAIL_TERMINAL = new Set(["failed", "canceled"]);
|
|
9
|
+
export function isBusyStatus(status) {
|
|
10
|
+
return BUSY.has(status);
|
|
11
|
+
}
|
|
12
|
+
export function isFailTerminal(status) {
|
|
13
|
+
return FAIL_TERMINAL.has(status);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Decide exit after idle. Pure — unit-tested.
|
|
17
|
+
* - If we watched a busy run end: succeeded → 0; failed/canceled → 2.
|
|
18
|
+
* - If never busy: optional fail-closed on latest terminal failure (deploy wait);
|
|
19
|
+
* otherwise idle/succeeded → 0 (push --wait after a file-only push).
|
|
20
|
+
*/
|
|
21
|
+
export function decideIdleExit(args) {
|
|
22
|
+
if (args.observedBusy && args.watched) {
|
|
23
|
+
const status = args.watched.status;
|
|
24
|
+
// Stale snapshot still busy → not success (caller should keep polling).
|
|
25
|
+
if (isBusyStatus(status)) {
|
|
26
|
+
return { exitCode: EXIT_USAGE, run: args.watched, status: "still_busy" };
|
|
27
|
+
}
|
|
28
|
+
if (isFailTerminal(status)) {
|
|
29
|
+
return { exitCode: EXIT_USAGE, run: args.watched, status };
|
|
30
|
+
}
|
|
31
|
+
return { exitCode: EXIT_OK, run: args.watched, status: status || "succeeded" };
|
|
32
|
+
}
|
|
33
|
+
const latest = args.latest;
|
|
34
|
+
if (args.failClosedOnIdleTerminalFailure &&
|
|
35
|
+
latest &&
|
|
36
|
+
isFailTerminal(latest.status)) {
|
|
37
|
+
return { exitCode: EXIT_USAGE, run: latest, status: latest.status };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
exitCode: EXIT_OK,
|
|
41
|
+
run: latest,
|
|
42
|
+
status: latest?.status ?? "idle",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function waitForAgentIdle(args) {
|
|
46
|
+
const maxMs = args.maxMs ?? 30 * 60 * 1000;
|
|
47
|
+
const pollMs = args.pollMs ?? 2000;
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
let observedBusy = false;
|
|
50
|
+
let watched = null;
|
|
51
|
+
while (Date.now() - started < maxMs) {
|
|
52
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(args.slug)}/agent-runs`);
|
|
53
|
+
const busy = (data.active ?? []).filter((r) => isBusyStatus(r.status));
|
|
54
|
+
if (busy.length > 0) {
|
|
55
|
+
observedBusy = true;
|
|
56
|
+
// If we were watching a run that left the active set, resolve it before
|
|
57
|
+
// switching — otherwise a failed run can be skipped when the next starts.
|
|
58
|
+
if (watched && !busy.some((r) => r.id === watched.id)) {
|
|
59
|
+
try {
|
|
60
|
+
const detail = await v1Get(`/api/v1/sites/${encodeURIComponent(args.slug)}/agent-runs/${encodeURIComponent(watched.id)}`);
|
|
61
|
+
if (detail.run && isFailTerminal(detail.run.status)) {
|
|
62
|
+
progress("\n");
|
|
63
|
+
return {
|
|
64
|
+
exitCode: EXIT_USAGE,
|
|
65
|
+
run: detail.run,
|
|
66
|
+
observedBusy: true,
|
|
67
|
+
status: detail.run.status,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Fall through and follow the new busy head.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const prevId = watched?.id;
|
|
76
|
+
const still = prevId
|
|
77
|
+
? busy.find((r) => r.id === prevId)
|
|
78
|
+
: undefined;
|
|
79
|
+
const next = still ?? busy.find((r) => r.status === "deploying") ?? busy[0];
|
|
80
|
+
watched = next;
|
|
81
|
+
progress(`\r${next.status} ${next.statusMessage ?? ""} `);
|
|
82
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Idle — if we were watching a busy run, refresh it for terminal fields (error).
|
|
86
|
+
if (observedBusy && watched) {
|
|
87
|
+
const watching = watched;
|
|
88
|
+
let refreshed = watching;
|
|
89
|
+
try {
|
|
90
|
+
const detail = await v1Get(`/api/v1/sites/${encodeURIComponent(args.slug)}/agent-runs/${encodeURIComponent(watching.id)}`);
|
|
91
|
+
if (detail.run)
|
|
92
|
+
refreshed = detail.run;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Fall back to list `run` only when it is the same id.
|
|
96
|
+
if (data.run?.id === watching.id)
|
|
97
|
+
refreshed = data.run;
|
|
98
|
+
}
|
|
99
|
+
watched = refreshed;
|
|
100
|
+
// Active list cleared but our snapshot is still busy → keep polling.
|
|
101
|
+
if (isBusyStatus(refreshed.status)) {
|
|
102
|
+
progress(`\r${refreshed.status} ${refreshed.statusMessage ?? ""} `);
|
|
103
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
progress("\n");
|
|
108
|
+
const decided = decideIdleExit({
|
|
109
|
+
watched,
|
|
110
|
+
latest: data.run,
|
|
111
|
+
observedBusy,
|
|
112
|
+
failClosedOnIdleTerminalFailure: args.failClosedOnIdleTerminalFailure,
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
exitCode: decided.exitCode,
|
|
116
|
+
run: decided.run,
|
|
117
|
+
observedBusy,
|
|
118
|
+
status: decided.status,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
progress("\n");
|
|
122
|
+
return {
|
|
123
|
+
exitCode: EXIT_USAGE,
|
|
124
|
+
run: watched,
|
|
125
|
+
observedBusy,
|
|
126
|
+
status: "timed_out",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Emit deploy/push-wait JSON or text from a WaitIdleResult. */
|
|
130
|
+
export function emitWaitOutcome(args) {
|
|
131
|
+
const { result } = args;
|
|
132
|
+
const message = result.status === "timed_out"
|
|
133
|
+
? "Timed out waiting for deploy."
|
|
134
|
+
: result.status === "still_busy"
|
|
135
|
+
? "Timed out waiting for deploy (run still busy)."
|
|
136
|
+
: result.run?.error ||
|
|
137
|
+
result.run?.statusMessage ||
|
|
138
|
+
(isFailTerminal(result.status)
|
|
139
|
+
? `Latest run ${result.status}`
|
|
140
|
+
: result.status);
|
|
141
|
+
if (result.exitCode !== EXIT_OK) {
|
|
142
|
+
if (isJson()) {
|
|
143
|
+
return emitFail(args.command, {
|
|
144
|
+
message,
|
|
145
|
+
exitCode: result.exitCode,
|
|
146
|
+
data: {
|
|
147
|
+
slug: args.slug,
|
|
148
|
+
waited: true,
|
|
149
|
+
observedBusy: result.observedBusy,
|
|
150
|
+
status: result.status,
|
|
151
|
+
run: result.run,
|
|
152
|
+
...args.extra,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (result.run) {
|
|
157
|
+
console.error(`${result.run.id} ${result.run.status}${result.run.error ? ` ${result.run.error}` : result.run.statusMessage ? ` ${result.run.statusMessage}` : ""}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.error(message);
|
|
161
|
+
}
|
|
162
|
+
return result.exitCode;
|
|
163
|
+
}
|
|
164
|
+
if (isJson()) {
|
|
165
|
+
emitOk(args.command, {
|
|
166
|
+
slug: args.slug,
|
|
167
|
+
waited: true,
|
|
168
|
+
observedBusy: result.observedBusy,
|
|
169
|
+
status: result.status,
|
|
170
|
+
run: result.run,
|
|
171
|
+
...args.extra,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else if (result.run) {
|
|
175
|
+
console.log(`${result.run.id} ${result.run.status}${result.run.statusMessage ? ` ${result.run.statusMessage}` : ""}`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log("No active deploy.");
|
|
179
|
+
}
|
|
180
|
+
return EXIT_OK;
|
|
181
|
+
}
|
|
@@ -26,6 +26,7 @@ _frontpage_completions() {
|
|
|
26
26
|
analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
|
|
27
27
|
modules) COMPREPLY=( $(compgen -W "list docs request-setup enable disable archive config" -- "\${cur}") ) ;;
|
|
28
28
|
extensions) COMPREPLY=( $(compgen -W "list docs enable disable config" -- "\${cur}") ) ;;
|
|
29
|
+
prompt) COMPREPLY=( $(compgen -W "--path --attach --asset-url --site --json --help" -- "\${cur}") ) ;;
|
|
29
30
|
*) COMPREPLY=( $(compgen -W "--site --help --json --quiet --yes" -- "\${cur}") ) ;;
|
|
30
31
|
esac
|
|
31
32
|
}
|
package/dist/http/client.d.ts
CHANGED
|
@@ -3,13 +3,6 @@ export declare const EXIT_USAGE = 2;
|
|
|
3
3
|
export declare const EXIT_CONFLICT = 3;
|
|
4
4
|
export declare const EXIT_AUTH = 4;
|
|
5
5
|
export declare const EXIT_UNEXPECTED = 5;
|
|
6
|
-
export declare class CliHttpError extends Error {
|
|
7
|
-
readonly exitCode: number;
|
|
8
|
-
readonly status: number;
|
|
9
|
-
readonly code?: string;
|
|
10
|
-
constructor(message: string, exitCode: number, status: number, code?: string);
|
|
11
|
-
}
|
|
12
|
-
export declare function apiBase(): string;
|
|
13
6
|
export type V1Envelope = {
|
|
14
7
|
error?: {
|
|
15
8
|
code?: string;
|
|
@@ -17,6 +10,17 @@ export type V1Envelope = {
|
|
|
17
10
|
requestId?: string;
|
|
18
11
|
};
|
|
19
12
|
};
|
|
13
|
+
export declare class CliHttpError extends Error {
|
|
14
|
+
readonly exitCode: number;
|
|
15
|
+
readonly status: number;
|
|
16
|
+
readonly code?: string;
|
|
17
|
+
/** v1 envelope requestId when the API returned one (§CLI Phase D). */
|
|
18
|
+
readonly requestId?: string;
|
|
19
|
+
constructor(message: string, exitCode: number, status: number, code?: string, requestId?: string);
|
|
20
|
+
}
|
|
21
|
+
/** Map a non-OK v1 response into CliHttpError (shared by JSON + multipart clients). */
|
|
22
|
+
export declare function httpErrorFromV1(status: number, parsed: V1Envelope, fallbackMessage?: string): CliHttpError;
|
|
23
|
+
export declare function apiBase(): string;
|
|
20
24
|
export declare function v1Get<T>(path: string): Promise<T>;
|
|
21
25
|
export declare function v1Post<T>(path: string, body?: unknown): Promise<T>;
|
|
22
26
|
export declare function v1Put<T>(path: string, body?: unknown): Promise<T>;
|
package/dist/http/client.js
CHANGED
|
@@ -12,14 +12,49 @@ export class CliHttpError extends Error {
|
|
|
12
12
|
exitCode;
|
|
13
13
|
status;
|
|
14
14
|
code;
|
|
15
|
-
|
|
15
|
+
/** v1 envelope requestId when the API returned one (§CLI Phase D). */
|
|
16
|
+
requestId;
|
|
17
|
+
constructor(message, exitCode, status, code, requestId) {
|
|
16
18
|
super(message);
|
|
17
19
|
this.name = "CliHttpError";
|
|
18
20
|
this.exitCode = exitCode;
|
|
19
21
|
this.status = status;
|
|
20
22
|
this.code = code;
|
|
23
|
+
this.requestId = requestId;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
/** Map a non-OK v1 response into CliHttpError (shared by JSON + multipart clients). */
|
|
27
|
+
export function httpErrorFromV1(status, parsed, fallbackMessage) {
|
|
28
|
+
const message = parsed.error?.message ?? fallbackMessage ?? `Request failed (${status})`;
|
|
29
|
+
const code = parsed.error?.code;
|
|
30
|
+
const requestId = parsed.error?.requestId;
|
|
31
|
+
if (status === 401 || status === 403) {
|
|
32
|
+
return new CliHttpError(message, EXIT_AUTH, status, code, requestId);
|
|
33
|
+
}
|
|
34
|
+
if (status === 400 || code === "bad_request") {
|
|
35
|
+
return new CliHttpError(message, EXIT_USAGE, status, code, requestId);
|
|
36
|
+
}
|
|
37
|
+
if (status === 402 || status === 429 || code === "rate_limited" || code === "payment_required") {
|
|
38
|
+
return new CliHttpError(message, EXIT_USAGE, status, code, requestId);
|
|
39
|
+
}
|
|
40
|
+
if (status === 409 || code === "conflict") {
|
|
41
|
+
return new CliHttpError(message, EXIT_CONFLICT, status, code, requestId);
|
|
42
|
+
}
|
|
43
|
+
if (status === 413 || code === "payload_too_large") {
|
|
44
|
+
return new CliHttpError(message, EXIT_USAGE, status, code, requestId);
|
|
45
|
+
}
|
|
46
|
+
if (status === 503 || code === "unavailable") {
|
|
47
|
+
return new CliHttpError(message, EXIT_USAGE, status, code, requestId);
|
|
48
|
+
}
|
|
49
|
+
if (status === 404) {
|
|
50
|
+
const lower = message.toLowerCase();
|
|
51
|
+
if (lower === "not found." || lower === "not found") {
|
|
52
|
+
return new CliHttpError("API not available (is PUBLIC_CLI_ENABLED on for this environment?).", EXIT_UNEXPECTED, status, code, requestId);
|
|
53
|
+
}
|
|
54
|
+
return new CliHttpError(message, EXIT_AUTH, status, code, requestId);
|
|
55
|
+
}
|
|
56
|
+
return new CliHttpError(message, EXIT_UNEXPECTED, status, code, requestId);
|
|
57
|
+
}
|
|
23
58
|
export function apiBase() {
|
|
24
59
|
const fromEnv = process.env.FRONTPAGE_API_BASE?.replace(/\/$/, "");
|
|
25
60
|
if (fromEnv)
|
|
@@ -58,31 +93,7 @@ async function v1Request(method, path, body) {
|
|
|
58
93
|
}
|
|
59
94
|
if (res.ok)
|
|
60
95
|
return parsed;
|
|
61
|
-
|
|
62
|
-
const code = parsed.error?.code;
|
|
63
|
-
if (res.status === 401 || res.status === 403) {
|
|
64
|
-
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
65
|
-
}
|
|
66
|
-
if (res.status === 402 || res.status === 429 || code === "rate_limited" || code === "payment_required") {
|
|
67
|
-
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
68
|
-
}
|
|
69
|
-
if (res.status === 409 || code === "conflict") {
|
|
70
|
-
throw new CliHttpError(message, EXIT_CONFLICT, res.status, code);
|
|
71
|
-
}
|
|
72
|
-
if (res.status === 413 || code === "payload_too_large") {
|
|
73
|
-
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
74
|
-
}
|
|
75
|
-
if (res.status === 503 || code === "unavailable") {
|
|
76
|
-
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
77
|
-
}
|
|
78
|
-
if (res.status === 404) {
|
|
79
|
-
const lower = message.toLowerCase();
|
|
80
|
-
if (lower === "not found." || lower === "not found") {
|
|
81
|
-
throw new CliHttpError("API not available (is PUBLIC_CLI_ENABLED on for this environment?).", EXIT_UNEXPECTED, res.status, code);
|
|
82
|
-
}
|
|
83
|
-
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
84
|
-
}
|
|
85
|
-
throw new CliHttpError(message, EXIT_UNEXPECTED, res.status, code);
|
|
96
|
+
throw httpErrorFromV1(res.status, parsed);
|
|
86
97
|
}
|
|
87
98
|
export async function v1Get(path) {
|
|
88
99
|
return v1Request("GET", path);
|
|
@@ -130,31 +141,7 @@ export async function v1PostMultipart(path, form) {
|
|
|
130
141
|
}
|
|
131
142
|
if (res.ok)
|
|
132
143
|
return parsed;
|
|
133
|
-
|
|
134
|
-
const code = parsed.error?.code;
|
|
135
|
-
if (res.status === 401 || res.status === 403) {
|
|
136
|
-
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
137
|
-
}
|
|
138
|
-
if (res.status === 402 || res.status === 429 || code === "rate_limited" || code === "payment_required") {
|
|
139
|
-
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
140
|
-
}
|
|
141
|
-
if (res.status === 409 || code === "conflict") {
|
|
142
|
-
throw new CliHttpError(message, EXIT_CONFLICT, res.status, code);
|
|
143
|
-
}
|
|
144
|
-
if (res.status === 413 || code === "payload_too_large") {
|
|
145
|
-
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
146
|
-
}
|
|
147
|
-
if (res.status === 503 || code === "unavailable") {
|
|
148
|
-
throw new CliHttpError(message, EXIT_USAGE, res.status, code);
|
|
149
|
-
}
|
|
150
|
-
if (res.status === 404) {
|
|
151
|
-
const lower = message.toLowerCase();
|
|
152
|
-
if (lower === "not found." || lower === "not found") {
|
|
153
|
-
throw new CliHttpError("API not available (is PUBLIC_CLI_ENABLED on for this environment?).", EXIT_UNEXPECTED, res.status, code);
|
|
154
|
-
}
|
|
155
|
-
throw new CliHttpError(message, EXIT_AUTH, res.status, code);
|
|
156
|
-
}
|
|
157
|
-
throw new CliHttpError(message, EXIT_UNEXPECTED, res.status, code);
|
|
144
|
+
throw httpErrorFromV1(res.status, parsed);
|
|
158
145
|
}
|
|
159
146
|
export function exitCodeForError(err) {
|
|
160
147
|
if (err instanceof CliHttpError)
|
package/dist/registry.js
CHANGED
|
@@ -88,10 +88,17 @@ export const COMMANDS = [
|
|
|
88
88
|
{
|
|
89
89
|
name: "push",
|
|
90
90
|
summary: "Upload local allowlisted files (CAS + active-run guard)",
|
|
91
|
-
usage: "frontpage push [--site <slug>] [--message …] [--json]",
|
|
91
|
+
usage: "frontpage push [--site <slug>] [--message …] [--wait] [--json]",
|
|
92
92
|
topics: ["sync"],
|
|
93
|
-
examples: [
|
|
94
|
-
|
|
93
|
+
examples: [
|
|
94
|
+
'frontpage push --message "Update hero copy"',
|
|
95
|
+
'frontpage push --message "…" --wait',
|
|
96
|
+
],
|
|
97
|
+
flags: [
|
|
98
|
+
"--site <slug>",
|
|
99
|
+
"--message <text>",
|
|
100
|
+
"--wait: after push, wait until no agent run is queued/running/deploying (same as deploy wait; not a Vercel oracle)",
|
|
101
|
+
],
|
|
95
102
|
dangers: [
|
|
96
103
|
"Requires prior pull (baseCommitSha)",
|
|
97
104
|
"Refused while an agent run is active (exit 3)",
|
|
@@ -184,11 +191,23 @@ export const COMMANDS = [
|
|
|
184
191
|
{
|
|
185
192
|
name: "prompt",
|
|
186
193
|
summary: "Enqueue an agent build",
|
|
187
|
-
usage: 'frontpage prompt "..." [--site <slug>] [--path /] [--json]',
|
|
194
|
+
usage: 'frontpage prompt "..." [--site <slug>] [--path /] [--attach <file|/__media/…|https://…>]… [--asset-url <https://…>]… [--json]',
|
|
188
195
|
topics: ["agent"],
|
|
189
|
-
examples: [
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
examples: [
|
|
197
|
+
'frontpage prompt "make the hero shorter"',
|
|
198
|
+
'frontpage prompt "Use this logo in the header" --attach ./logo.png',
|
|
199
|
+
'frontpage prompt "Match this PDF brand guide" --attach ./guide.pdf --path /',
|
|
200
|
+
'frontpage prompt "Reuse uploaded art" --attach /__media/uploads/acme/uuid.png',
|
|
201
|
+
'frontpage prompt "Use this image" --asset-url "https://….blob.vercel-storage.com/uploads/…"',
|
|
202
|
+
],
|
|
203
|
+
flags: [
|
|
204
|
+
"--path must start with /",
|
|
205
|
+
"--attach: local file (uploads via media), /__media/… site path, or https Blob URL; repeatable",
|
|
206
|
+
"--asset-url: https image URL (legacy); repeatable",
|
|
207
|
+
"Local attach uses the same caps as frontpage media upload (images/PDF/text; no CLI video yet)",
|
|
208
|
+
"stdout is bare runId (or JSON with --json)",
|
|
209
|
+
],
|
|
210
|
+
seeAlso: ["runs", "deploy", "media"],
|
|
192
211
|
},
|
|
193
212
|
{
|
|
194
213
|
name: "runs",
|
|
@@ -203,7 +222,10 @@ export const COMMANDS = [
|
|
|
203
222
|
"frontpage runs retry <runId>",
|
|
204
223
|
"frontpage runs get <runId>",
|
|
205
224
|
],
|
|
206
|
-
flags: [
|
|
225
|
+
flags: [
|
|
226
|
+
"There is no runs continue — mid-task continuation is automatic",
|
|
227
|
+
"runs wait: failed/canceled → exit 2; --json uses ok:false and includes run.error",
|
|
228
|
+
],
|
|
207
229
|
seeAlso: ["prompt", "deploy"],
|
|
208
230
|
},
|
|
209
231
|
{
|
|
@@ -236,6 +258,7 @@ export const COMMANDS = [
|
|
|
236
258
|
flags: [
|
|
237
259
|
"Optional build wait only — staging is https://<subdomain>.frontpage.host",
|
|
238
260
|
"Does NOT promote to production; use publish only when the user asks",
|
|
261
|
+
"Fail-closed: exit 2 when the waited run (or latest, if already idle) is failed/canceled",
|
|
239
262
|
],
|
|
240
263
|
},
|
|
241
264
|
{
|
|
@@ -410,7 +433,7 @@ export const TOPICS = {
|
|
|
410
433
|
frontpage status # tipDrift when remote moved since last pull
|
|
411
434
|
frontpage pull # downloads src/, public/, content/ into cwd
|
|
412
435
|
frontpage diff # local vs remote via git blobSha (--json → dirty + tipDrift)
|
|
413
|
-
frontpage push [--message …] # uploads allowlisted files; CAS + active-run guard
|
|
436
|
+
frontpage push [--message …] [--wait] # uploads allowlisted files; CAS + active-run guard
|
|
414
437
|
frontpage ls [prefix]
|
|
415
438
|
frontpage cat <path>
|
|
416
439
|
frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only]
|
|
@@ -419,6 +442,7 @@ export const TOPICS = {
|
|
|
419
442
|
Local state: .frontpage/state.json { slug, baseCommitSha }
|
|
420
443
|
Pull BEFORE EDITING when tip may have moved (UI agent / another checkout) — check tipDrift.
|
|
421
444
|
Pull before push so CAS has baseCommitSha. Do not push while the editor agent is building.
|
|
445
|
+
--wait after push blocks until no agent run is queued/running/deploying (same as deploy wait).
|
|
422
446
|
The UI/canvas agent does not use this CLI; only CLI checkouts need pull.
|
|
423
447
|
Pushing module markup may auto-activate modules (same as the agent).
|
|
424
448
|
tokens.ts may be auto-repaired at commit if it would break the build.
|
|
@@ -467,11 +491,13 @@ export const TOPICS = {
|
|
|
467
491
|
See also: frontpage help sync | content | AGENTS.md`,
|
|
468
492
|
agent: `Agent + publish
|
|
469
493
|
frontpage prompt "make the hero shorter"
|
|
494
|
+
frontpage prompt "Use this logo" --attach ./logo.png
|
|
495
|
+
frontpage prompt "…" --attach /__media/uploads/<slug>/… --asset-url https://…
|
|
470
496
|
frontpage runs # latest + active
|
|
471
|
-
frontpage runs wait [runId]
|
|
497
|
+
frontpage runs wait [runId] # failed/canceled → exit 2; JSON ok:false + run.error
|
|
472
498
|
frontpage runs cancel <runId>
|
|
473
499
|
frontpage runs retry <runId>
|
|
474
|
-
frontpage deploy wait # optional
|
|
500
|
+
frontpage deploy wait # optional build wait; fail-closed on failed/canceled; NOT publish
|
|
475
501
|
frontpage publish # ONLY if the user asked — production promote
|
|
476
502
|
frontpage unpublish --yes # admin only; never unless asked; --yes when non-TTY
|
|
477
503
|
frontpage usage
|
|
@@ -482,8 +508,10 @@ export const TOPICS = {
|
|
|
482
508
|
frontpage modules request-setup <key> --reason "…"
|
|
483
509
|
frontpage extensions
|
|
484
510
|
|
|
511
|
+
--attach uploads local files via media (images/PDF/text) or accepts /__media/… / https URLs.
|
|
512
|
+
API errors include requestId in --json error envelopes (and stderr when not --json).
|
|
485
513
|
Mid-task continuation is automatic — there is no runs continue.
|
|
486
|
-
Do not
|
|
514
|
+
Do not file-sync while an agent run is active (see help sync for push --wait).
|
|
487
515
|
Default after work: review https://<subdomain>.frontpage.host; do not publish.`,
|
|
488
516
|
content: `Content
|
|
489
517
|
frontpage media list|upload <file>
|
|
@@ -532,8 +560,9 @@ export const TOPICS = {
|
|
|
532
560
|
|
|
533
561
|
Global --json envelope:
|
|
534
562
|
{ "ok": true, "command": "…", "data": {…} }
|
|
535
|
-
{ "ok": false, "command": "…", "error": { "message", "code?", "httpStatus?", "exitCode" } }
|
|
536
|
-
Failures with --json still use the process exit code; envelope is on stdout
|
|
563
|
+
{ "ok": false, "command": "…", "error": { "message", "code?", "httpStatus?", "exitCode", "requestId?" }, "data"? }
|
|
564
|
+
Failures with --json still use the process exit code; envelope is on stdout.
|
|
565
|
+
Prefer requestId from error envelopes when filing support issues.`,
|
|
537
566
|
};
|
|
538
567
|
export function findCommand(name) {
|
|
539
568
|
return COMMANDS.find((c) => c.name === name);
|
package/dist/runtime.d.ts
CHANGED
|
@@ -13,8 +13,18 @@ export type JsonErrorBody = {
|
|
|
13
13
|
code?: string;
|
|
14
14
|
httpStatus?: number;
|
|
15
15
|
exitCode: number;
|
|
16
|
+
requestId?: string;
|
|
16
17
|
};
|
|
17
18
|
export declare function emitOk(command: string, data: unknown): void;
|
|
19
|
+
/** Structured failure with optional data payload (e.g. failed run wait). */
|
|
20
|
+
export declare function emitFail(command: string, args: {
|
|
21
|
+
message: string;
|
|
22
|
+
exitCode: number;
|
|
23
|
+
code?: string;
|
|
24
|
+
requestId?: string;
|
|
25
|
+
httpStatus?: number;
|
|
26
|
+
data?: unknown;
|
|
27
|
+
}): number;
|
|
18
28
|
export declare function emitErr(command: string, err: unknown): number;
|
|
19
29
|
export declare function emitUsage(command: string, message: string, exitCode?: number): number;
|
|
20
30
|
/** Progress / spinner lines — suppressed when --quiet or --json. */
|
package/dist/runtime.js
CHANGED
|
@@ -31,23 +31,43 @@ export function requireYesOrTty(action) {
|
|
|
31
31
|
export function emitOk(command, data) {
|
|
32
32
|
console.log(JSON.stringify({ ok: true, command, data }));
|
|
33
33
|
}
|
|
34
|
+
/** Structured failure with optional data payload (e.g. failed run wait). */
|
|
35
|
+
export function emitFail(command, args) {
|
|
36
|
+
const body = {
|
|
37
|
+
message: args.message,
|
|
38
|
+
exitCode: args.exitCode,
|
|
39
|
+
};
|
|
40
|
+
if (args.code)
|
|
41
|
+
body.code = args.code;
|
|
42
|
+
if (args.httpStatus)
|
|
43
|
+
body.httpStatus = args.httpStatus;
|
|
44
|
+
if (args.requestId)
|
|
45
|
+
body.requestId = args.requestId;
|
|
46
|
+
if (jsonMode) {
|
|
47
|
+
const out = { ok: false, command, error: body };
|
|
48
|
+
if (args.data !== undefined)
|
|
49
|
+
out.data = args.data;
|
|
50
|
+
console.log(JSON.stringify(out));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const suffix = args.requestId ? ` (requestId: ${args.requestId})` : "";
|
|
54
|
+
console.error(`${args.message}${suffix}`);
|
|
55
|
+
}
|
|
56
|
+
return args.exitCode;
|
|
57
|
+
}
|
|
34
58
|
export function emitErr(command, err) {
|
|
35
59
|
const exitCode = exitCodeForError(err);
|
|
36
60
|
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
-
const body = { message, exitCode };
|
|
38
61
|
if (err instanceof CliHttpError) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
return emitFail(command, {
|
|
63
|
+
message,
|
|
64
|
+
exitCode,
|
|
65
|
+
code: err.code,
|
|
66
|
+
httpStatus: err.status || undefined,
|
|
67
|
+
requestId: err.requestId,
|
|
68
|
+
});
|
|
43
69
|
}
|
|
44
|
-
|
|
45
|
-
console.log(JSON.stringify({ ok: false, command, error: body }));
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
console.error(message);
|
|
49
|
-
}
|
|
50
|
-
return exitCode;
|
|
70
|
+
return emitFail(command, { message, exitCode });
|
|
51
71
|
}
|
|
52
72
|
export function emitUsage(command, message, exitCode = 2) {
|
|
53
73
|
if (jsonMode) {
|
package/llms.txt
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
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
|
+
- Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (media upload for locals; images/PDF/text)
|
|
14
|
+
- Errors: --json error envelopes include requestId; runs wait failed/canceled → ok:false + run.error (exit 2); deploy wait fail-closed; push --wait
|
|
13
15
|
- Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
|
|
14
16
|
- Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
|
|
15
17
|
- Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step for agents
|
|
16
18
|
- Production: custom domain after frontpage publish — NEVER publish/unpublish unless the user explicitly asks
|
|
17
|
-
- 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 (CAS); no push during active agent run; UI agent does not use CLI pull; review on subdomain
|
|
19
|
+
- 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
|
|
18
20
|
- Machine: global --json / --quiet; exit 0/2/3/4/5 (see help exit-codes)
|
|
19
|
-
- deploy wait = optional build wait only; it is NOT go-live / publish
|
|
21
|
+
- deploy wait = optional build wait only (fail-closed on failed/canceled); it is NOT go-live / publish
|
|
20
22
|
- No runs continue, domains, billing, members, MCP, or git remotes in v1
|