frontpage-host 0.1.16 → 0.1.18
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 +30 -19
- package/README.md +3 -3
- package/dist/bin-path.d.ts +21 -0
- package/dist/bin-path.js +65 -0
- package/dist/bin.js +9 -0
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +4 -5
- package/dist/commands/images.d.ts +1 -0
- package/dist/commands/images.js +57 -0
- package/dist/commands/modules.js +4 -0
- package/dist/commands/people.d.ts +1 -0
- package/dist/commands/people.js +239 -0
- package/dist/commands/publish.js +73 -11
- package/dist/commands/push.d.ts +5 -0
- package/dist/commands/push.js +18 -13
- package/dist/commands/redirects.d.ts +1 -0
- package/dist/commands/redirects.js +83 -0
- package/dist/commands/wait-agent-idle.d.ts +12 -0
- package/dist/commands/wait-agent-idle.js +23 -5
- package/dist/commands/wait-site-compile.d.ts +22 -0
- package/dist/commands/wait-site-compile.js +98 -0
- package/dist/compile.d.ts +46 -0
- package/dist/compile.js +99 -0
- package/dist/completions/generate.js +14 -3
- package/dist/modules/commands.js +24 -1
- package/dist/modules/forms_destinations.d.ts +3 -0
- package/dist/modules/forms_destinations.js +147 -0
- package/dist/modules/typed_config.js +21 -9
- package/dist/registry.js +103 -22
- package/llms.txt +8 -5
- package/package.json +1 -1
package/dist/commands/publish.js
CHANGED
|
@@ -1,19 +1,81 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, EXIT_USAGE,
|
|
3
|
-
import { emitErr, emitOk, emitUsage, isJson, requireYesOrTty } from "../runtime.js";
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, v1Post, v1Delete } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitFail, emitOk, emitUsage, isJson, progress, requireYesOrTty } from "../runtime.js";
|
|
4
|
+
import { describePublishResult, waitForCompileReady } from "../compile.js";
|
|
5
|
+
function takeWaitFlag(rest) {
|
|
6
|
+
const i = rest.indexOf("--wait");
|
|
7
|
+
if (i === -1)
|
|
8
|
+
return false;
|
|
9
|
+
rest.splice(i, 1);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
function emitPublished(slug, data) {
|
|
13
|
+
const described = describePublishResult({
|
|
14
|
+
productionCommit: data.productionCommit ?? null,
|
|
15
|
+
productionUrl: data.productionUrl ?? null,
|
|
16
|
+
});
|
|
17
|
+
if (isJson()) {
|
|
18
|
+
emitOk("publish", {
|
|
19
|
+
slug,
|
|
20
|
+
productionUrl: data.productionUrl ?? null,
|
|
21
|
+
productionCommit: data.productionCommit ?? null,
|
|
22
|
+
servingCommit: described.servingCommit,
|
|
23
|
+
matchesTip: described.matchesTip,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(`Published ${slug}`);
|
|
28
|
+
for (const line of described.lines)
|
|
29
|
+
console.log(line);
|
|
30
|
+
if (data.productionUrl)
|
|
31
|
+
console.log(data.productionUrl);
|
|
32
|
+
if (!described.matchesTip) {
|
|
33
|
+
console.error("Publish promoted a build that is not the current tip. Wait for the site compile, then publish again.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return EXIT_OK;
|
|
37
|
+
}
|
|
4
38
|
export async function runPublish(args) {
|
|
5
39
|
try {
|
|
6
|
-
const { slug } = await resolveSlug(args);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
40
|
+
const { slug, rest } = await resolveSlug(args);
|
|
41
|
+
const work = [...rest];
|
|
42
|
+
const wait = takeWaitFlag(work);
|
|
43
|
+
if (work.length > 0) {
|
|
44
|
+
return emitUsage("publish", "Usage: frontpage publish [--site <slug>] [--wait] [--json]", EXIT_USAGE);
|
|
10
45
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
46
|
+
if (wait) {
|
|
47
|
+
if (!isJson())
|
|
48
|
+
progress("Waiting for the site compile…\n");
|
|
49
|
+
const compile = await waitForCompileReady({ slug });
|
|
50
|
+
if (compile.exitCode !== EXIT_OK) {
|
|
51
|
+
const message = compile.apiMissing
|
|
52
|
+
? "Compile status API is missing. Refusing to publish."
|
|
53
|
+
: compile.failed
|
|
54
|
+
? "Site compile failed. Refusing to publish a previous build."
|
|
55
|
+
: compile.timedOut
|
|
56
|
+
? "Timed out waiting for the site compile. Refusing to publish a previous build."
|
|
57
|
+
: "Site compile is not ready. Refusing to publish a previous build.";
|
|
58
|
+
return emitFail("publish", {
|
|
59
|
+
message,
|
|
60
|
+
exitCode: compile.exitCode,
|
|
61
|
+
data: {
|
|
62
|
+
slug,
|
|
63
|
+
compile: compile.status,
|
|
64
|
+
tip: compile.status?.stagingCommit ?? null,
|
|
65
|
+
live: compile.status?.liveCommit ?? null,
|
|
66
|
+
status: compile.apiMissing
|
|
67
|
+
? "compile_api_missing"
|
|
68
|
+
: compile.failed
|
|
69
|
+
? "compile_failed"
|
|
70
|
+
: compile.timedOut
|
|
71
|
+
? "compile_timeout"
|
|
72
|
+
: "compile_not_ready",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
15
76
|
}
|
|
16
|
-
|
|
77
|
+
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/publish`);
|
|
78
|
+
return emitPublished(slug, data);
|
|
17
79
|
}
|
|
18
80
|
catch (err) {
|
|
19
81
|
return emitErr("publish", err);
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
+
/** Remote allowlisted paths that are gone locally become explicit deletes. */
|
|
2
|
+
export declare function pushDeletesForMissingLocal(localPaths: Iterable<string>, remotePaths: Iterable<string>): Array<{
|
|
3
|
+
path: string;
|
|
4
|
+
delete: true;
|
|
5
|
+
}>;
|
|
1
6
|
export declare function runPush(args: string[]): Promise<number>;
|
package/dist/commands/push.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { resolveSlug } from "../site.js";
|
|
4
|
-
import { EXIT_OK, EXIT_USAGE, v1Put } from "../http/client.js";
|
|
4
|
+
import { EXIT_OK, EXIT_USAGE, v1Get, 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 {
|
|
7
|
+
import { waitForAgentThenCompile } from "./wait-site-compile.js";
|
|
8
8
|
const TEXT_EXTS = new Set([
|
|
9
9
|
".astro",
|
|
10
10
|
".ts",
|
|
@@ -26,6 +26,14 @@ const TEXT_EXTS = new Set([
|
|
|
26
26
|
".toml",
|
|
27
27
|
]);
|
|
28
28
|
const ROOTS = ["src", "public", "content"];
|
|
29
|
+
/** Remote allowlisted paths that are gone locally become explicit deletes. */
|
|
30
|
+
export function pushDeletesForMissingLocal(localPaths, remotePaths) {
|
|
31
|
+
const local = new Set(localPaths);
|
|
32
|
+
return [...remotePaths]
|
|
33
|
+
.filter((path) => path && !local.has(path))
|
|
34
|
+
.sort()
|
|
35
|
+
.map((path) => ({ path, delete: true }));
|
|
36
|
+
}
|
|
29
37
|
async function walkAllowlisted(cwd) {
|
|
30
38
|
const out = [];
|
|
31
39
|
async function walk(abs) {
|
|
@@ -85,7 +93,10 @@ export async function runPush(args) {
|
|
|
85
93
|
if (paths.length === 0) {
|
|
86
94
|
return emitUsage("push", "No allowlisted files (src/, public/, content/) found in this folder.", EXIT_USAGE);
|
|
87
95
|
}
|
|
88
|
-
const
|
|
96
|
+
const listing = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files`);
|
|
97
|
+
const files = [
|
|
98
|
+
...pushDeletesForMissingLocal(paths, listing.files.map((f) => f.path)),
|
|
99
|
+
];
|
|
89
100
|
for (const path of paths) {
|
|
90
101
|
const abs = join(cwd, path);
|
|
91
102
|
const st = await stat(abs);
|
|
@@ -136,8 +147,7 @@ export async function runPush(args) {
|
|
|
136
147
|
console.error(`Warning: ${w}`);
|
|
137
148
|
return EXIT_OK;
|
|
138
149
|
}
|
|
139
|
-
// --wait:
|
|
140
|
-
// Does not invent a Vercel deploy oracle; file-only pushes with no active run return promptly.
|
|
150
|
+
// --wait: agent-run idle (if any), then the new commit's site compile.
|
|
141
151
|
if (!isJson()) {
|
|
142
152
|
if (!data.changed) {
|
|
143
153
|
console.log("Nothing to commit (remote already matches).");
|
|
@@ -148,17 +158,12 @@ export async function runPush(args) {
|
|
|
148
158
|
}
|
|
149
159
|
for (const w of data.warnings ?? [])
|
|
150
160
|
console.error(`Warning: ${w}`);
|
|
151
|
-
console.error("Waiting for agent run / deploy idle…");
|
|
152
161
|
}
|
|
153
|
-
|
|
154
|
-
slug,
|
|
155
|
-
// Do not fail on an ancient canceled run when this push never saw busy work.
|
|
156
|
-
failClosedOnIdleTerminalFailure: false,
|
|
157
|
-
});
|
|
158
|
-
return emitWaitOutcome({
|
|
162
|
+
return waitForAgentThenCompile({
|
|
159
163
|
command: "push",
|
|
160
164
|
slug,
|
|
161
|
-
|
|
165
|
+
expectCommit: data.commitSha ?? null,
|
|
166
|
+
failClosedOnIdleTerminalFailure: false,
|
|
162
167
|
extra: { ...pushPayload, waited: true },
|
|
163
168
|
});
|
|
164
169
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runRedirects(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolveSlug } from "../site.js";
|
|
2
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Put } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, takeFlag } from "../argv.js";
|
|
4
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
5
|
+
const USAGE = "Usage: frontpage redirects list | set --from /old --to /new [--status 301|308] | rm --from /old [--site <slug>] [--json]";
|
|
6
|
+
function requirePathFlag(work, flag) {
|
|
7
|
+
const value = takeFlag(work, flag);
|
|
8
|
+
if (!value) {
|
|
9
|
+
throw new CliHttpError(`${flag} is required (e.g. ${flag} /old).`, EXIT_USAGE, 0, "bad_request");
|
|
10
|
+
}
|
|
11
|
+
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
12
|
+
throw new CliHttpError(`${flag} must be a pathname starting with /.`, EXIT_USAGE, 0, "bad_request");
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export async function runRedirects(args) {
|
|
17
|
+
try {
|
|
18
|
+
const { slug, rest } = await resolveSlug(args);
|
|
19
|
+
const sub = rest[0];
|
|
20
|
+
if (!sub || sub === "list") {
|
|
21
|
+
const work = sub === "list" ? rest.slice(1) : [...rest];
|
|
22
|
+
assertNoUnknownFlags(work, "redirects");
|
|
23
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/redirects`);
|
|
24
|
+
if (isJson()) {
|
|
25
|
+
emitOk("redirects", data);
|
|
26
|
+
return EXIT_OK;
|
|
27
|
+
}
|
|
28
|
+
const rows = data.redirects ?? [];
|
|
29
|
+
if (rows.length === 0) {
|
|
30
|
+
console.log("No redirects.");
|
|
31
|
+
return EXIT_OK;
|
|
32
|
+
}
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
console.log(`${row.from} → ${row.to} (${row.status})`);
|
|
35
|
+
}
|
|
36
|
+
return EXIT_OK;
|
|
37
|
+
}
|
|
38
|
+
if (sub === "set") {
|
|
39
|
+
const work = rest.slice(1);
|
|
40
|
+
const from = requirePathFlag(work, "--from");
|
|
41
|
+
const to = requirePathFlag(work, "--to");
|
|
42
|
+
const statusRaw = takeFlag(work, "--status");
|
|
43
|
+
assertNoUnknownFlags(work, "redirects");
|
|
44
|
+
const body = { from, to };
|
|
45
|
+
if (statusRaw !== undefined) {
|
|
46
|
+
const status = Number(statusRaw);
|
|
47
|
+
if (status !== 301 && status !== 308) {
|
|
48
|
+
console.error("Status must be 301 or 308.");
|
|
49
|
+
return EXIT_USAGE;
|
|
50
|
+
}
|
|
51
|
+
body.status = status;
|
|
52
|
+
}
|
|
53
|
+
const data = await v1Put(`/api/v1/sites/${encodeURIComponent(slug)}/redirects`, body);
|
|
54
|
+
if (isJson()) {
|
|
55
|
+
emitOk("redirects", data);
|
|
56
|
+
return EXIT_OK;
|
|
57
|
+
}
|
|
58
|
+
const row = data.redirect;
|
|
59
|
+
console.log(`${row.from} → ${row.to} (${row.status})`);
|
|
60
|
+
return EXIT_OK;
|
|
61
|
+
}
|
|
62
|
+
if (sub === "rm" || sub === "remove") {
|
|
63
|
+
const work = rest.slice(1);
|
|
64
|
+
const from = requirePathFlag(work, "--from");
|
|
65
|
+
assertNoUnknownFlags(work, "redirects");
|
|
66
|
+
const data = await v1Delete(`/api/v1/sites/${encodeURIComponent(slug)}/redirects?from=${encodeURIComponent(from)}`);
|
|
67
|
+
if (isJson()) {
|
|
68
|
+
emitOk("redirects", data);
|
|
69
|
+
return EXIT_OK;
|
|
70
|
+
}
|
|
71
|
+
console.log(`Removed ${data.removed.from}`);
|
|
72
|
+
return EXIT_OK;
|
|
73
|
+
}
|
|
74
|
+
console.error(USAGE);
|
|
75
|
+
return EXIT_USAGE;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (isJson())
|
|
79
|
+
return emitErr("redirects", err);
|
|
80
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
81
|
+
return exitCodeForError(err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -16,6 +16,18 @@ export type WaitIdleResult = {
|
|
|
16
16
|
};
|
|
17
17
|
export declare function isBusyStatus(status: string): boolean;
|
|
18
18
|
export declare function isFailTerminal(status: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* A historical succeeded run is unrelated when this wait never saw busy work.
|
|
21
|
+
* Keep failed/canceled latest so deploy wait stays fail-closed (Phase D).
|
|
22
|
+
*/
|
|
23
|
+
export declare function sanitizeUnrelatedIdleRun(args: {
|
|
24
|
+
observedBusy: boolean;
|
|
25
|
+
run: WaitRunSnapshot | null;
|
|
26
|
+
status: string;
|
|
27
|
+
}): {
|
|
28
|
+
run: WaitRunSnapshot | null;
|
|
29
|
+
status: string;
|
|
30
|
+
};
|
|
19
31
|
/**
|
|
20
32
|
* Decide exit after idle. Pure — unit-tested.
|
|
21
33
|
* - If we watched a busy run end: succeeded → 0; failed/canceled → 2.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Shared wait for editor agent-run busy states (queued/running/deploying).
|
|
3
|
+
* This is the agent-run clock, not the site compile. File-only pushes have
|
|
4
|
+
* no agent run — success here does not mean the latest commit is built.
|
|
4
5
|
*/
|
|
5
6
|
import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
|
|
6
7
|
import { emitFail, emitOk, isJson, progress } from "../runtime.js";
|
|
@@ -12,6 +13,18 @@ export function isBusyStatus(status) {
|
|
|
12
13
|
export function isFailTerminal(status) {
|
|
13
14
|
return FAIL_TERMINAL.has(status);
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* A historical succeeded run is unrelated when this wait never saw busy work.
|
|
18
|
+
* Keep failed/canceled latest so deploy wait stays fail-closed (Phase D).
|
|
19
|
+
*/
|
|
20
|
+
export function sanitizeUnrelatedIdleRun(args) {
|
|
21
|
+
if (args.observedBusy)
|
|
22
|
+
return { run: args.run, status: args.status };
|
|
23
|
+
if (args.run && isFailTerminal(args.run.status)) {
|
|
24
|
+
return { run: args.run, status: args.status };
|
|
25
|
+
}
|
|
26
|
+
return { run: null, status: "idle" };
|
|
27
|
+
}
|
|
15
28
|
/**
|
|
16
29
|
* Decide exit after idle. Pure — unit-tested.
|
|
17
30
|
* - If we watched a busy run end: succeeded → 0; failed/canceled → 2.
|
|
@@ -111,11 +124,16 @@ export async function waitForAgentIdle(args) {
|
|
|
111
124
|
observedBusy,
|
|
112
125
|
failClosedOnIdleTerminalFailure: args.failClosedOnIdleTerminalFailure,
|
|
113
126
|
});
|
|
127
|
+
const sanitized = sanitizeUnrelatedIdleRun({
|
|
128
|
+
observedBusy,
|
|
129
|
+
run: decided.run,
|
|
130
|
+
status: decided.status,
|
|
131
|
+
});
|
|
114
132
|
return {
|
|
115
133
|
exitCode: decided.exitCode,
|
|
116
|
-
run:
|
|
134
|
+
run: sanitized.run,
|
|
117
135
|
observedBusy,
|
|
118
|
-
status:
|
|
136
|
+
status: sanitized.status,
|
|
119
137
|
};
|
|
120
138
|
}
|
|
121
139
|
progress("\n");
|
|
@@ -175,7 +193,7 @@ export function emitWaitOutcome(args) {
|
|
|
175
193
|
console.log(`${result.run.id} ${result.run.status}${result.run.statusMessage ? ` ${result.run.statusMessage}` : ""}`);
|
|
176
194
|
}
|
|
177
195
|
else {
|
|
178
|
-
console.log("No
|
|
196
|
+
console.log("No agent run was in flight. This does not mean the latest commit is compiled.");
|
|
179
197
|
}
|
|
180
198
|
return EXIT_OK;
|
|
181
199
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DeployStatus } from "../compile.js";
|
|
2
|
+
import { type WaitIdleResult } from "./wait-agent-idle.js";
|
|
3
|
+
export declare function waitForAgentThenCompile(args: {
|
|
4
|
+
command: "deploy" | "push";
|
|
5
|
+
slug: string;
|
|
6
|
+
expectCommit?: string | null;
|
|
7
|
+
failClosedOnIdleTerminalFailure: boolean;
|
|
8
|
+
extra?: Record<string, unknown>;
|
|
9
|
+
}): Promise<number>;
|
|
10
|
+
export declare function emitCompileWaitOutcome(args: {
|
|
11
|
+
command: "deploy" | "push";
|
|
12
|
+
slug: string;
|
|
13
|
+
agent: WaitIdleResult;
|
|
14
|
+
compile: {
|
|
15
|
+
exitCode: number;
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
failed?: boolean;
|
|
18
|
+
apiMissing?: boolean;
|
|
19
|
+
status: DeployStatus | null;
|
|
20
|
+
};
|
|
21
|
+
extra?: Record<string, unknown>;
|
|
22
|
+
}): number;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* After the agent-run clock is idle, wait for the site compile of the tip.
|
|
3
|
+
*/
|
|
4
|
+
import { EXIT_OK } from "../http/client.js";
|
|
5
|
+
import { emitFail, emitOk, isJson, progress } from "../runtime.js";
|
|
6
|
+
import { waitForCompileReady } from "../compile.js";
|
|
7
|
+
import { emitWaitOutcome, waitForAgentIdle, } from "./wait-agent-idle.js";
|
|
8
|
+
export async function waitForAgentThenCompile(args) {
|
|
9
|
+
const agent = await waitForAgentIdle({
|
|
10
|
+
slug: args.slug,
|
|
11
|
+
failClosedOnIdleTerminalFailure: args.failClosedOnIdleTerminalFailure,
|
|
12
|
+
});
|
|
13
|
+
if (agent.exitCode !== EXIT_OK) {
|
|
14
|
+
return emitWaitOutcome({
|
|
15
|
+
command: args.command,
|
|
16
|
+
slug: args.slug,
|
|
17
|
+
result: agent,
|
|
18
|
+
extra: args.extra,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (!isJson()) {
|
|
22
|
+
if (agent.observedBusy && agent.run) {
|
|
23
|
+
console.log(`${agent.run.id} ${agent.run.status}${agent.run.statusMessage ? ` ${agent.run.statusMessage}` : ""}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log("No agent run was in flight.");
|
|
27
|
+
}
|
|
28
|
+
progress("Waiting for the site compile…\n");
|
|
29
|
+
}
|
|
30
|
+
const compile = await waitForCompileReady({
|
|
31
|
+
slug: args.slug,
|
|
32
|
+
expectCommit: args.expectCommit,
|
|
33
|
+
});
|
|
34
|
+
return emitCompileWaitOutcome({
|
|
35
|
+
command: args.command,
|
|
36
|
+
slug: args.slug,
|
|
37
|
+
agent,
|
|
38
|
+
compile,
|
|
39
|
+
extra: args.extra,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function emitCompileWaitOutcome(args) {
|
|
43
|
+
const compileData = {
|
|
44
|
+
compile: args.compile.status,
|
|
45
|
+
compileReady: Boolean(args.compile.status?.ready) && !args.compile.timedOut,
|
|
46
|
+
};
|
|
47
|
+
if (args.compile.exitCode !== EXIT_OK) {
|
|
48
|
+
const message = args.compile.apiMissing
|
|
49
|
+
? "Compile status API is missing. Cannot confirm the tip is live."
|
|
50
|
+
: args.compile.failed
|
|
51
|
+
? "Site compile failed. The git tip is not live."
|
|
52
|
+
: args.compile.timedOut
|
|
53
|
+
? "Timed out waiting for the site compile. The git tip is not live yet."
|
|
54
|
+
: "Site compile is not ready.";
|
|
55
|
+
if (isJson()) {
|
|
56
|
+
return emitFail(args.command, {
|
|
57
|
+
message,
|
|
58
|
+
exitCode: args.compile.exitCode,
|
|
59
|
+
data: {
|
|
60
|
+
slug: args.slug,
|
|
61
|
+
waited: true,
|
|
62
|
+
observedBusy: args.agent.observedBusy,
|
|
63
|
+
status: args.compile.apiMissing
|
|
64
|
+
? "compile_api_missing"
|
|
65
|
+
: args.compile.failed
|
|
66
|
+
? "compile_failed"
|
|
67
|
+
: args.compile.timedOut
|
|
68
|
+
? "compile_timeout"
|
|
69
|
+
: "compile_not_ready",
|
|
70
|
+
run: args.agent.run,
|
|
71
|
+
...compileData,
|
|
72
|
+
...args.extra,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
console.error(message);
|
|
77
|
+
if (args.compile.status) {
|
|
78
|
+
console.error(`tip ${args.compile.status.stagingCommit ?? "(none)"} live ${args.compile.status.liveCommit ?? "(none)"}`);
|
|
79
|
+
}
|
|
80
|
+
return args.compile.exitCode;
|
|
81
|
+
}
|
|
82
|
+
if (isJson()) {
|
|
83
|
+
emitOk(args.command, {
|
|
84
|
+
slug: args.slug,
|
|
85
|
+
waited: true,
|
|
86
|
+
observedBusy: args.agent.observedBusy,
|
|
87
|
+
status: "ready",
|
|
88
|
+
run: args.agent.run,
|
|
89
|
+
...compileData,
|
|
90
|
+
...args.extra,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const live = args.compile.status?.liveCommit ?? args.compile.status?.stagingCommit;
|
|
95
|
+
console.log(`Site compile ready ${live ?? ""}`.trim());
|
|
96
|
+
}
|
|
97
|
+
return EXIT_OK;
|
|
98
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type DeployStatus = {
|
|
2
|
+
slug?: string;
|
|
3
|
+
stagingCommit: string | null;
|
|
4
|
+
servingCommit: string | null;
|
|
5
|
+
liveCommit: string | null;
|
|
6
|
+
ready: boolean;
|
|
7
|
+
splitBrain: boolean;
|
|
8
|
+
compileState?: "ready" | "building" | "failed" | "canceled" | "unknown";
|
|
9
|
+
stagingUrl: string | null;
|
|
10
|
+
productionCommit: string | null;
|
|
11
|
+
productionServingCommit: string | null;
|
|
12
|
+
productionUrl: string | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function normalizeSha(raw: string | null | undefined): string | null;
|
|
15
|
+
export declare function shortSha(raw: string | null | undefined): string;
|
|
16
|
+
/** SHA encoded in a Blob artifact prefix URL. */
|
|
17
|
+
export declare function commitFromArtifactUrl(url: string | null | undefined): string | null;
|
|
18
|
+
export declare function compileIsReady(status: Pick<DeployStatus, "ready" | "liveCommit">, expectCommit?: string | null): boolean;
|
|
19
|
+
export type CompileWaitResult = {
|
|
20
|
+
exitCode: number;
|
|
21
|
+
timedOut: boolean;
|
|
22
|
+
failed: boolean;
|
|
23
|
+
apiMissing?: boolean;
|
|
24
|
+
status: DeployStatus | null;
|
|
25
|
+
};
|
|
26
|
+
/** Compile clock is gone (flag off / old API). A site-not-found 404 is not this. */
|
|
27
|
+
export declare function isMissingDeployApi(err: unknown): boolean;
|
|
28
|
+
/** A deploy payload we can poll. Missing `ready` means the compile clock is not there. */
|
|
29
|
+
export declare function deployStatusLooksComplete(status: DeployStatus | null | undefined): boolean;
|
|
30
|
+
export declare function fetchDeployStatus(slug: string): Promise<DeployStatus>;
|
|
31
|
+
export declare function waitForCompileReady(args: {
|
|
32
|
+
slug: string;
|
|
33
|
+
expectCommit?: string | null;
|
|
34
|
+
maxMs?: number;
|
|
35
|
+
pollMs?: number;
|
|
36
|
+
fetchStatus?: (slug: string) => Promise<DeployStatus>;
|
|
37
|
+
}): Promise<CompileWaitResult>;
|
|
38
|
+
export declare function describePublishResult(args: {
|
|
39
|
+
productionCommit: string | null;
|
|
40
|
+
productionUrl: string | null;
|
|
41
|
+
}): {
|
|
42
|
+
tipCommit: string | null;
|
|
43
|
+
servingCommit: string | null;
|
|
44
|
+
matchesTip: boolean;
|
|
45
|
+
lines: string[];
|
|
46
|
+
};
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site compile clock (not the editor agent run).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/sites/{slug}/deploy → ready when the live compile SHA equals the tip.
|
|
5
|
+
*/
|
|
6
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, v1Get } from "./http/client.js";
|
|
7
|
+
import { progress } from "./runtime.js";
|
|
8
|
+
const SHA_RE = /^[0-9a-f]{40}$/i;
|
|
9
|
+
export function normalizeSha(raw) {
|
|
10
|
+
const sha = (raw ?? "").trim().toLowerCase();
|
|
11
|
+
return SHA_RE.test(sha) ? sha : null;
|
|
12
|
+
}
|
|
13
|
+
export function shortSha(raw) {
|
|
14
|
+
const sha = normalizeSha(raw);
|
|
15
|
+
return sha ? sha.slice(0, 7) : "(none)";
|
|
16
|
+
}
|
|
17
|
+
/** SHA encoded in a Blob artifact prefix URL. */
|
|
18
|
+
export function commitFromArtifactUrl(url) {
|
|
19
|
+
if (!url)
|
|
20
|
+
return null;
|
|
21
|
+
const m = String(url).match(/\/artifacts\/[^/]+\/([0-9a-f]{40})(?:\/|$)/i);
|
|
22
|
+
return m ? m[1].toLowerCase() : null;
|
|
23
|
+
}
|
|
24
|
+
export function compileIsReady(status, expectCommit) {
|
|
25
|
+
if (!status.ready)
|
|
26
|
+
return false;
|
|
27
|
+
const live = normalizeSha(status.liveCommit);
|
|
28
|
+
if (!live)
|
|
29
|
+
return false;
|
|
30
|
+
const expect = normalizeSha(expectCommit ?? null);
|
|
31
|
+
if (!expect)
|
|
32
|
+
return true;
|
|
33
|
+
return live === expect;
|
|
34
|
+
}
|
|
35
|
+
/** Compile clock is gone (flag off / old API). A site-not-found 404 is not this. */
|
|
36
|
+
export function isMissingDeployApi(err) {
|
|
37
|
+
if (!(err instanceof CliHttpError))
|
|
38
|
+
return false;
|
|
39
|
+
const message = err.message.toLowerCase();
|
|
40
|
+
if (message.includes("api not available"))
|
|
41
|
+
return true;
|
|
42
|
+
if (err.status === 404 && (message === "not found." || message === "not found"))
|
|
43
|
+
return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/** A deploy payload we can poll. Missing `ready` means the compile clock is not there. */
|
|
47
|
+
export function deployStatusLooksComplete(status) {
|
|
48
|
+
return Boolean(status && typeof status.ready === "boolean");
|
|
49
|
+
}
|
|
50
|
+
export async function fetchDeployStatus(slug) {
|
|
51
|
+
return v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/deploy`);
|
|
52
|
+
}
|
|
53
|
+
export async function waitForCompileReady(args) {
|
|
54
|
+
const maxMs = args.maxMs ?? 30 * 60 * 1000;
|
|
55
|
+
const pollMs = args.pollMs ?? 2000;
|
|
56
|
+
const fetchStatus = args.fetchStatus ?? fetchDeployStatus;
|
|
57
|
+
const started = Date.now();
|
|
58
|
+
let last = null;
|
|
59
|
+
while (Date.now() - started < maxMs) {
|
|
60
|
+
try {
|
|
61
|
+
last = await fetchStatus(args.slug);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (isMissingDeployApi(err)) {
|
|
65
|
+
progress("\n");
|
|
66
|
+
return { exitCode: EXIT_USAGE, timedOut: false, failed: true, apiMissing: true, status: null };
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
if (!deployStatusLooksComplete(last)) {
|
|
71
|
+
progress("\n");
|
|
72
|
+
return { exitCode: EXIT_USAGE, timedOut: false, failed: true, apiMissing: true, status: last };
|
|
73
|
+
}
|
|
74
|
+
if (last.compileState === "failed" || last.compileState === "canceled") {
|
|
75
|
+
progress("\n");
|
|
76
|
+
return { exitCode: EXIT_USAGE, timedOut: false, failed: true, status: last };
|
|
77
|
+
}
|
|
78
|
+
if (compileIsReady(last, args.expectCommit)) {
|
|
79
|
+
progress("\n");
|
|
80
|
+
return { exitCode: EXIT_OK, timedOut: false, failed: false, status: last };
|
|
81
|
+
}
|
|
82
|
+
const tip = shortSha(args.expectCommit ?? last.stagingCommit);
|
|
83
|
+
const live = shortSha(last.liveCommit ?? last.servingCommit);
|
|
84
|
+
progress(`\rwaiting for site compile tip ${tip} live ${live} `);
|
|
85
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
86
|
+
}
|
|
87
|
+
progress("\n");
|
|
88
|
+
return { exitCode: EXIT_USAGE, timedOut: true, failed: false, status: last };
|
|
89
|
+
}
|
|
90
|
+
export function describePublishResult(args) {
|
|
91
|
+
const tipCommit = normalizeSha(args.productionCommit);
|
|
92
|
+
const servingCommit = commitFromArtifactUrl(args.productionUrl) ?? tipCommit;
|
|
93
|
+
const matchesTip = Boolean(tipCommit && servingCommit && tipCommit === servingCommit);
|
|
94
|
+
const lines = [
|
|
95
|
+
`tip ${tipCommit ?? "(none)"}`,
|
|
96
|
+
`serving ${servingCommit ?? "(none)"}${matchesTip ? "" : " ← not the tip; this is the previous build"}`,
|
|
97
|
+
];
|
|
98
|
+
return { tipCommit, servingCommit, matchesTip, lines };
|
|
99
|
+
}
|
|
@@ -19,15 +19,19 @@ _frontpage_completions() {
|
|
|
19
19
|
runs) COMPREPLY=( $(compgen -W "list current wait cancel retry get" -- "\${cur}") ) ;;
|
|
20
20
|
deploy) COMPREPLY=( $(compgen -W "wait" -- "\${cur}") ) ;;
|
|
21
21
|
media) COMPREPLY=( $(compgen -W "list upload" -- "\${cur}") ) ;;
|
|
22
|
+
images) COMPREPLY=( $(compgen -W "generate" -- "\${cur}") ) ;;
|
|
22
23
|
fonts) COMPREPLY=( $(compgen -W "list search" -- "\${cur}") ) ;;
|
|
23
24
|
tokens) COMPREPLY=( $(compgen -W "set" -- "\${cur}") ) ;;
|
|
24
25
|
seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
|
|
25
26
|
articles) COMPREPLY=( $(compgen -W "list content create-section create-article set-draft" -- "\${cur}") ) ;;
|
|
26
27
|
analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
|
|
28
|
+
people) COMPREPLY=( $(compgen -W "list get add set note rm-note follow-up hide unhide" -- "\${cur}") ) ;;
|
|
27
29
|
modules)
|
|
28
30
|
if [[ \${COMP_CWORD} -ge 3 && \${COMP_WORDS[2]} == google_reviews ]]; then
|
|
29
31
|
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect" -- "\${cur}") )
|
|
30
|
-
elif [[ \${COMP_CWORD} -ge 3 &&
|
|
32
|
+
elif [[ \${COMP_CWORD} -ge 3 && \${COMP_WORDS[2]} == forms ]]; then
|
|
33
|
+
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config get set destinations connect-destination remove-destination disconnect-destination attach-destination detach-destination request-destination-setup" -- "\${cur}") )
|
|
34
|
+
elif [[ \${COMP_CWORD} -ge 3 && (\${COMP_WORDS[2]} == callbar || \${COMP_WORDS[2]} == booking || \${COMP_WORDS[2]} == signups) ]]; then
|
|
31
35
|
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config get set" -- "\${cur}") )
|
|
32
36
|
elif [[ \${COMP_CWORD} -ge 3 && (\${COMP_WORDS[2]} == payments || \${COMP_WORDS[2]} == shop) ]]; then
|
|
33
37
|
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config status connect disconnect" -- "\${cur}") )
|
|
@@ -61,14 +65,18 @@ _frontpage() {
|
|
|
61
65
|
runs) _values 'subcommand' current wait cancel retry get ;;
|
|
62
66
|
deploy) _values 'subcommand' wait ;;
|
|
63
67
|
media) _values 'subcommand' list upload ;;
|
|
68
|
+
images) _values 'subcommand' generate ;;
|
|
64
69
|
fonts) _values 'subcommand' list search ;;
|
|
65
70
|
tokens) _values 'subcommand' set ;;
|
|
66
71
|
seo|settings) _values 'subcommand' get set ;;
|
|
67
72
|
analytics) _values 'subcommand' dashboard summary query funnel timeseries compare sql ;;
|
|
73
|
+
people) _values 'subcommand' list get add set note rm-note follow-up hide unhide ;;
|
|
68
74
|
modules)
|
|
69
75
|
if [[ $words[2] == google_reviews ]]; then
|
|
70
76
|
_values 'subcommand' docs request-setup enable disable archive config connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect
|
|
71
|
-
elif [[ $words[2] ==
|
|
77
|
+
elif [[ $words[2] == forms ]]; then
|
|
78
|
+
_values 'subcommand' docs request-setup enable disable archive config get set destinations connect-destination remove-destination disconnect-destination attach-destination detach-destination request-destination-setup
|
|
79
|
+
elif [[ $words[2] == callbar || $words[2] == booking || $words[2] == signups ]]; then
|
|
72
80
|
_values 'subcommand' docs request-setup enable disable archive config get set
|
|
73
81
|
elif [[ $words[2] == payments || $words[2] == shop ]]; then
|
|
74
82
|
_values 'subcommand' docs request-setup enable disable archive config status connect disconnect
|
|
@@ -95,13 +103,16 @@ export function generateFishCompletion() {
|
|
|
95
103
|
`complete -c frontpage -n "__fish_seen_subcommand_from runs" -a "current wait cancel retry get"`,
|
|
96
104
|
`complete -c frontpage -n "__fish_seen_subcommand_from deploy" -a "wait"`,
|
|
97
105
|
`complete -c frontpage -n "__fish_seen_subcommand_from media" -a "list upload"`,
|
|
106
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from images" -a "generate"`,
|
|
98
107
|
`complete -c frontpage -n "__fish_seen_subcommand_from fonts" -a "list search"`,
|
|
99
108
|
`complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
|
|
100
109
|
`complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
|
|
101
110
|
`complete -c frontpage -n "__fish_seen_subcommand_from analytics" -a "dashboard summary query funnel timeseries compare sql"`,
|
|
111
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from people" -a "list get add set note rm-note follow-up hide unhide"`,
|
|
102
112
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules" -a "list docs request-setup enable disable archive config callbar forms booking signups payments shop ordering articles google_reviews"`,
|
|
103
113
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from google_reviews" -a "connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect"`,
|
|
104
|
-
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from
|
|
114
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from forms" -a "get set destinations connect-destination remove-destination disconnect-destination attach-destination detach-destination request-destination-setup"`,
|
|
115
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from callbar booking signups" -a "get set"`,
|
|
105
116
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from payments shop" -a "status connect disconnect"`,
|
|
106
117
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from ordering" -a "status connect disconnect sync"`,
|
|
107
118
|
`complete -c frontpage -n "__fish_seen_subcommand_from extensions" -a "list docs enable disable config"`,
|