@avocadostudio-ai/orchestrator-core 0.1.0
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/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- package/package.json +78 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { pageDocSchema } from "@avocadostudio-ai/shared";
|
|
8
|
+
import { draftPages, versions, ensureHeroImageProps, persistStateNow, getSessionPages, getSiteConfig, isLegacySiteId } from "../state/session-state.js";
|
|
9
|
+
import { toErrorDetail } from "../ops/ops-engine.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Logger interface for functions that need structured logging
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Deployment helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
export function deploymentIdFromAny(input) {
|
|
18
|
+
const match = input.match(/\b(dpl_[a-zA-Z0-9]+)\b/);
|
|
19
|
+
return match?.[1];
|
|
20
|
+
}
|
|
21
|
+
export async function refreshPublishStatusFromVercel(current) {
|
|
22
|
+
const token = process.env.VERCEL_TOKEN?.trim();
|
|
23
|
+
if (!token || !current.deploymentId) {
|
|
24
|
+
// Can't poll Vercel API — auto-resolve after a grace period so the UI
|
|
25
|
+
// doesn't spin forever. Default 120 s (typical Vercel build time).
|
|
26
|
+
const graceSec = Number(process.env.PUBLISH_GRACE_SECONDS) || 120;
|
|
27
|
+
const elapsed = current.startedAt
|
|
28
|
+
? (Date.now() - new Date(current.startedAt).getTime()) / 1000
|
|
29
|
+
: Infinity;
|
|
30
|
+
if (elapsed >= graceSec && current.status === "triggered") {
|
|
31
|
+
current.vercelState = "READY";
|
|
32
|
+
current.status = "triggered";
|
|
33
|
+
current.updatedAt = new Date().toISOString();
|
|
34
|
+
}
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
const teamId = process.env.VERCEL_TEAM_ID?.trim();
|
|
38
|
+
const query = teamId ? `?teamId=${encodeURIComponent(teamId)}` : "";
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`https://api.vercel.com/v13/deployments/${current.deploymentId}${query}`, {
|
|
41
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
current.lastCheckError = `vercel_api_${res.status}`;
|
|
45
|
+
current.updatedAt = new Date().toISOString();
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
const data = (await res.json());
|
|
49
|
+
const state = typeof data.readyState === "string" ? data.readyState : "UNKNOWN";
|
|
50
|
+
current.vercelState = state;
|
|
51
|
+
current.inspectUrl = typeof data.inspectorUrl === "string" ? data.inspectorUrl : current.inspectUrl;
|
|
52
|
+
current.lastCheckError = undefined;
|
|
53
|
+
current.updatedAt = new Date().toISOString();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
current.lastCheckError = "fetch_error";
|
|
57
|
+
current.updatedAt = new Date().toISOString();
|
|
58
|
+
}
|
|
59
|
+
return current;
|
|
60
|
+
}
|
|
61
|
+
export function requirePublishToken(request) {
|
|
62
|
+
const configured = process.env.PUBLISH_TOKEN?.trim();
|
|
63
|
+
if (!configured)
|
|
64
|
+
return true;
|
|
65
|
+
const provided = String(request.headers["x-publish-token"] ?? "").trim();
|
|
66
|
+
if (provided.length === 0 || provided.length !== configured.length)
|
|
67
|
+
return false;
|
|
68
|
+
return timingSafeEqual(Buffer.from(provided), Buffer.from(configured));
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Git operations
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
async function runGit(args, cwd) {
|
|
74
|
+
try {
|
|
75
|
+
const result = await execFileAsync("git", args, { cwd });
|
|
76
|
+
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const maybe = error;
|
|
80
|
+
const stderr = (maybe.stderr ?? "").trim();
|
|
81
|
+
const stdout = (maybe.stdout ?? "").trim();
|
|
82
|
+
const message = (maybe.message ?? "").trim();
|
|
83
|
+
const detail = stderr || stdout || message || toErrorDetail(error);
|
|
84
|
+
throw new Error(detail);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Per-invocation `git -c` prefix that authenticates pushes with a token when
|
|
89
|
+
* the host's checkout has no push credentials (e.g. Render, which clones with
|
|
90
|
+
* a read-only GitHub App). Reads `PUBLISH_GIT_TOKEN` (falls back to
|
|
91
|
+
* `GITHUB_TOKEN`). The token is passed as an `http.extraheader` Authorization
|
|
92
|
+
* header — NOT embedded in the remote URL — so it never lands in `origin` or
|
|
93
|
+
* in git's error output (which echoes URLs but not config headers). Returns
|
|
94
|
+
* `[]` when no token is set, preserving the local-dev path that relies on the
|
|
95
|
+
* developer's ambient git credentials.
|
|
96
|
+
*/
|
|
97
|
+
function gitAuthPrefix() {
|
|
98
|
+
const token = (process.env.PUBLISH_GIT_TOKEN ?? process.env.GITHUB_TOKEN)?.trim();
|
|
99
|
+
if (!token)
|
|
100
|
+
return [];
|
|
101
|
+
const basic = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
102
|
+
return ["-c", `http.extraheader=Authorization: Basic ${basic}`];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Per-invocation `git -c` prefix supplying a committer identity. Render's
|
|
106
|
+
* runtime checkout has no `user.name`/`user.email`, so `git commit` would fail
|
|
107
|
+
* with "Author identity unknown". Overridable via `PUBLISH_GIT_AUTHOR_NAME` /
|
|
108
|
+
* `PUBLISH_GIT_AUTHOR_EMAIL`.
|
|
109
|
+
*/
|
|
110
|
+
function gitIdentityPrefix() {
|
|
111
|
+
const name = process.env.PUBLISH_GIT_AUTHOR_NAME?.trim() || "Avocado Studio";
|
|
112
|
+
const email = process.env.PUBLISH_GIT_AUTHOR_EMAIL?.trim() || "publish@avocadostudio.ai";
|
|
113
|
+
return ["-c", `user.name=${name}`, "-c", `user.email=${email}`];
|
|
114
|
+
}
|
|
115
|
+
const RESTORE_SESSION_PRESETS = [
|
|
116
|
+
{ sessionKey: "adventure-arena::dev", commit: "70584e0" },
|
|
117
|
+
{ sessionKey: "avocado-magic::dev", commit: "269c0be" },
|
|
118
|
+
{ sessionKey: "avocado-odyssey::dev", commit: "69eab10" }
|
|
119
|
+
];
|
|
120
|
+
export async function loadPublishedSnapshotFromCommit(commit) {
|
|
121
|
+
const repoRoot = resolve(process.cwd(), "../..");
|
|
122
|
+
const targetPath = "apps/site/lib/published-content.json";
|
|
123
|
+
const raw = (await runGit(["show", `${commit}:${targetPath}`], repoRoot)).stdout;
|
|
124
|
+
const payload = JSON.parse(raw);
|
|
125
|
+
if (!Array.isArray(payload))
|
|
126
|
+
throw new Error("snapshot payload is not an array");
|
|
127
|
+
const pages = [];
|
|
128
|
+
for (const candidate of payload) {
|
|
129
|
+
const parsed = pageDocSchema.safeParse(candidate);
|
|
130
|
+
if (!parsed.success)
|
|
131
|
+
throw new Error("snapshot payload contains invalid page schema");
|
|
132
|
+
pages.push(parsed.data);
|
|
133
|
+
}
|
|
134
|
+
if (pages.length === 0)
|
|
135
|
+
throw new Error("snapshot has no pages");
|
|
136
|
+
return pages;
|
|
137
|
+
}
|
|
138
|
+
// In-memory cache: full commit hash → descriptor (snapshots are immutable)
|
|
139
|
+
const snapshotDescriptorCache = new Map();
|
|
140
|
+
function parseCommitLine(line) {
|
|
141
|
+
const first = line.indexOf("|");
|
|
142
|
+
const second = line.indexOf("|", first + 1);
|
|
143
|
+
if (first <= 0 || second <= first)
|
|
144
|
+
return null;
|
|
145
|
+
const commit = line.slice(0, first).trim();
|
|
146
|
+
const committedAt = line.slice(first + 1, second).trim();
|
|
147
|
+
const message = line.slice(second + 1).trim();
|
|
148
|
+
if (!commit)
|
|
149
|
+
return null;
|
|
150
|
+
return { commit, committedAt, message };
|
|
151
|
+
}
|
|
152
|
+
async function resolveCommitDescriptor(parsed) {
|
|
153
|
+
const cached = snapshotDescriptorCache.get(parsed.commit);
|
|
154
|
+
if (cached !== undefined)
|
|
155
|
+
return cached;
|
|
156
|
+
try {
|
|
157
|
+
const pages = await loadPublishedSnapshotFromCommit(parsed.commit);
|
|
158
|
+
const home = pages.find((page) => page.slug === "/");
|
|
159
|
+
const homeHeadingRaw = home?.blocks.find((block) => block.type === "Hero")?.props?.heading;
|
|
160
|
+
const homeHeading = typeof homeHeadingRaw === "string" && homeHeadingRaw.trim().length > 0 ? homeHeadingRaw.trim() : "(no hero heading)";
|
|
161
|
+
const signature = `${pages.length}::${homeHeading}::${pages.map((page) => page.slug).join("|")}`;
|
|
162
|
+
const result = {
|
|
163
|
+
descriptor: {
|
|
164
|
+
commit: parsed.commit.slice(0, 7),
|
|
165
|
+
committedAt: parsed.committedAt,
|
|
166
|
+
message: parsed.message,
|
|
167
|
+
pageCount: pages.length,
|
|
168
|
+
homeHeading
|
|
169
|
+
},
|
|
170
|
+
signature
|
|
171
|
+
};
|
|
172
|
+
snapshotDescriptorCache.set(parsed.commit, result);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
snapshotDescriptorCache.set(parsed.commit, null);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export async function listRestoreSnapshots(limit = 30, siteId) {
|
|
181
|
+
const repoRoot = resolve(process.cwd(), "../..");
|
|
182
|
+
const targetPath = "apps/site/lib/published-content.json";
|
|
183
|
+
const cappedLimit = Math.max(1, Math.min(80, Math.floor(limit)));
|
|
184
|
+
const gitArgs = ["log", "--max-count", String(cappedLimit * 4), "--date=iso-strict", "--pretty=format:%H|%ad|%s"];
|
|
185
|
+
if (siteId) {
|
|
186
|
+
const normalised = siteId.trim().toLowerCase();
|
|
187
|
+
if (isLegacySiteId(normalised)) {
|
|
188
|
+
// Legacy commits use bare session key: "publish: session dev ..."
|
|
189
|
+
// The trailing space ensures we don't match scoped keys like "siteId::dev"
|
|
190
|
+
gitArgs.push("--grep=publish: session dev ");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
gitArgs.push(`--grep=publish: session ${normalised}::`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
gitArgs.push("--", targetPath);
|
|
197
|
+
const rawLog = (await runGit(gitArgs, repoRoot)).stdout;
|
|
198
|
+
const parsedLines = rawLog
|
|
199
|
+
.split("\n")
|
|
200
|
+
.map((line) => line.trim())
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.map(parseCommitLine)
|
|
203
|
+
.filter((p) => p !== null);
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
const snapshots = [];
|
|
206
|
+
const CONCURRENCY = 5;
|
|
207
|
+
for (let i = 0; i < parsedLines.length && snapshots.length < cappedLimit; i += CONCURRENCY) {
|
|
208
|
+
const batch = parsedLines.slice(i, i + CONCURRENCY);
|
|
209
|
+
const results = await Promise.allSettled(batch.map(resolveCommitDescriptor));
|
|
210
|
+
for (const result of results) {
|
|
211
|
+
if (result.status !== "fulfilled" || !result.value)
|
|
212
|
+
continue;
|
|
213
|
+
const { descriptor, signature } = result.value;
|
|
214
|
+
if (seen.has(signature))
|
|
215
|
+
continue;
|
|
216
|
+
seen.add(signature);
|
|
217
|
+
snapshots.push(descriptor);
|
|
218
|
+
if (snapshots.length >= cappedLimit)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return snapshots;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Delete a publish snapshot by reverting its git commit.
|
|
226
|
+
* Uses `git revert --no-commit` + `git commit` to keep history linear.
|
|
227
|
+
* Returns true if the revert succeeded, false otherwise.
|
|
228
|
+
*/
|
|
229
|
+
export async function deletePublishSnapshot(commit) {
|
|
230
|
+
const repoRoot = resolve(process.cwd(), "../..");
|
|
231
|
+
if (!/^[0-9a-f]{7,40}$/i.test(commit))
|
|
232
|
+
return false;
|
|
233
|
+
try {
|
|
234
|
+
// Resolve short hash to full hash
|
|
235
|
+
const fullHash = (await runGit(["rev-parse", commit], repoRoot)).stdout.trim();
|
|
236
|
+
// Revert without auto-commit, then commit with a descriptive message
|
|
237
|
+
await runGit(["revert", "--no-commit", fullHash], repoRoot);
|
|
238
|
+
await runGit(["commit", "-m", `revert publish snapshot ${commit}`], repoRoot);
|
|
239
|
+
// Invalidate cache for this commit
|
|
240
|
+
snapshotDescriptorCache.delete(fullHash);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Reset any partial revert state
|
|
245
|
+
try {
|
|
246
|
+
await runGit(["revert", "--abort"], repoRoot);
|
|
247
|
+
}
|
|
248
|
+
catch { /* ignore */ }
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export function isTemplateFallbackSession(sessionDraft) {
|
|
253
|
+
const slugs = Array.from(sessionDraft.keys());
|
|
254
|
+
if (!(slugs.length === 1 || slugs.length === 2))
|
|
255
|
+
return false;
|
|
256
|
+
const home = sessionDraft.get("/");
|
|
257
|
+
if (!home)
|
|
258
|
+
return false;
|
|
259
|
+
const heroHeading = home.blocks.find((block) => block.type === "Hero")?.props?.heading;
|
|
260
|
+
return heroHeading === "Build websites with plain language";
|
|
261
|
+
}
|
|
262
|
+
export async function ensurePresetRestoreSessions(log) {
|
|
263
|
+
let changed = false;
|
|
264
|
+
for (const preset of RESTORE_SESSION_PRESETS) {
|
|
265
|
+
const existing = draftPages.get(preset.sessionKey);
|
|
266
|
+
if (existing && !isTemplateFallbackSession(existing))
|
|
267
|
+
continue;
|
|
268
|
+
try {
|
|
269
|
+
const pages = await loadPublishedSnapshotFromCommit(preset.commit);
|
|
270
|
+
const seeded = new Map();
|
|
271
|
+
for (const page of pages) {
|
|
272
|
+
const clone = structuredClone(page);
|
|
273
|
+
ensureHeroImageProps(clone);
|
|
274
|
+
seeded.set(clone.slug, clone);
|
|
275
|
+
}
|
|
276
|
+
draftPages.set(preset.sessionKey, seeded);
|
|
277
|
+
versions.set(preset.sessionKey, Math.max(1, versions.get(preset.sessionKey) ?? 0));
|
|
278
|
+
changed = true;
|
|
279
|
+
log.info({ session: preset.sessionKey, commit: preset.commit }, "Seeded preset restore session");
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
log.warn({ session: preset.sessionKey, commit: preset.commit, err: toErrorDetail(error) }, "Failed to seed preset restore session");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (changed)
|
|
286
|
+
await persistStateNow(log);
|
|
287
|
+
}
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Image URL rewriting for publish
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
const LOCALHOST_GENERATED_RE = /https?:\/\/localhost:\d+\/generated-images\/([a-zA-Z0-9_-]+\.(?:png|jpg|jpeg|webp|gif))/gi;
|
|
292
|
+
const LOCALHOST_GDRIVE_RE = /https?:\/\/localhost:\d+\/gdrive\/images\/([a-zA-Z0-9_-]+)/gi;
|
|
293
|
+
export function findLocalhostImageUrls(pages) {
|
|
294
|
+
const json = JSON.stringify(pages);
|
|
295
|
+
const urlMap = new Map();
|
|
296
|
+
for (const match of json.matchAll(LOCALHOST_GENERATED_RE)) {
|
|
297
|
+
urlMap.set(match[0], match[1]);
|
|
298
|
+
}
|
|
299
|
+
for (const match of json.matchAll(LOCALHOST_GDRIVE_RE)) {
|
|
300
|
+
urlMap.set(match[0], `gdrive_${match[1]}.webp`);
|
|
301
|
+
}
|
|
302
|
+
return urlMap;
|
|
303
|
+
}
|
|
304
|
+
function rewriteImageUrlsInPages(pages, urlMap) {
|
|
305
|
+
let json = JSON.stringify(pages);
|
|
306
|
+
for (const [originalUrl, fileName] of urlMap) {
|
|
307
|
+
json = json.replaceAll(originalUrl, `/generated-images/${fileName}`);
|
|
308
|
+
}
|
|
309
|
+
return JSON.parse(json);
|
|
310
|
+
}
|
|
311
|
+
const MIME_BY_EXT = {
|
|
312
|
+
png: "image/png",
|
|
313
|
+
jpg: "image/jpeg",
|
|
314
|
+
jpeg: "image/jpeg",
|
|
315
|
+
webp: "image/webp",
|
|
316
|
+
gif: "image/gif",
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* Collect generated/gdrive images referenced by localhost URLs in pages,
|
|
320
|
+
* read them from disk, and return a map of originalUrl → base64 InlineAsset.
|
|
321
|
+
* Missing files are silently skipped.
|
|
322
|
+
*/
|
|
323
|
+
export async function collectInlineAssets(pages, generatedImageDir) {
|
|
324
|
+
const urlMap = findLocalhostImageUrls(pages);
|
|
325
|
+
if (urlMap.size === 0)
|
|
326
|
+
return {};
|
|
327
|
+
const entries = Array.from(urlMap.entries());
|
|
328
|
+
const results = await Promise.allSettled(entries.map(async ([originalUrl, fileName]) => {
|
|
329
|
+
const bytes = await readFile(resolve(generatedImageDir, fileName));
|
|
330
|
+
const ext = fileName.split(".").pop()?.toLowerCase() ?? "png";
|
|
331
|
+
return [originalUrl, { data: bytes.toString("base64"), mimeType: MIME_BY_EXT[ext] ?? "image/png", fileName }];
|
|
332
|
+
}));
|
|
333
|
+
const assets = {};
|
|
334
|
+
for (const r of results) {
|
|
335
|
+
if (r.status === "fulfilled")
|
|
336
|
+
assets[r.value[0]] = r.value[1];
|
|
337
|
+
}
|
|
338
|
+
return assets;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Record a publish snapshot in git history (commit only, no push).
|
|
342
|
+
* Used after site-contract publishes so version history can find them.
|
|
343
|
+
* Best-effort: failures are silently ignored.
|
|
344
|
+
*/
|
|
345
|
+
export async function recordPublishSnapshot(session, pages, log, siteConfig) {
|
|
346
|
+
const repoRoot = resolve(process.cwd(), "../..");
|
|
347
|
+
const targetPath = "apps/site/lib/published-content.json";
|
|
348
|
+
const absoluteTargetPath = resolve(repoRoot, targetPath);
|
|
349
|
+
try {
|
|
350
|
+
// The site's publish handler is responsible for rewriting image URLs
|
|
351
|
+
// and saving assets. We just commit whatever state is on disk.
|
|
352
|
+
const cfg = siteConfig ?? getSiteConfig(session);
|
|
353
|
+
const payload = `${JSON.stringify({ pages, siteConfig: cfg }, null, 2)}\n`;
|
|
354
|
+
await writeFile(absoluteTargetPath, payload, "utf8");
|
|
355
|
+
await runGit(["add", targetPath], repoRoot);
|
|
356
|
+
// Also stage generated images if the site handler wrote them
|
|
357
|
+
const imageDir = resolve(repoRoot, "apps/site/public/generated-images");
|
|
358
|
+
if (existsSync(imageDir)) {
|
|
359
|
+
await runGit(["add", "apps/site/public/generated-images"], repoRoot);
|
|
360
|
+
}
|
|
361
|
+
// Check if there are staged changes
|
|
362
|
+
try {
|
|
363
|
+
await runGit(["diff", "--cached", "--quiet"], repoRoot);
|
|
364
|
+
log?.info({ session }, "recordPublishSnapshot: no content changes to commit");
|
|
365
|
+
return undefined; // no changes
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Has staged changes — commit them
|
|
369
|
+
}
|
|
370
|
+
const commitMessage = `publish: session ${session} ${new Date().toISOString()}`;
|
|
371
|
+
await runGit([...gitIdentityPrefix(), "commit", "-m", commitMessage], repoRoot);
|
|
372
|
+
const rev = await runGit(["rev-parse", "HEAD"], repoRoot);
|
|
373
|
+
const commitSha = rev.stdout.trim();
|
|
374
|
+
log?.info({ session, commitSha: commitSha.slice(0, 12) }, "recordPublishSnapshot: committed");
|
|
375
|
+
// Push to remote so the commit is durable and visible
|
|
376
|
+
const branch = sanitizeBranch(process.env.PUBLISH_GIT_BRANCH ?? "main");
|
|
377
|
+
try {
|
|
378
|
+
await runGit([...gitAuthPrefix(), "push", "origin", `HEAD:${branch}`], repoRoot);
|
|
379
|
+
log?.info({ session, branch }, "recordPublishSnapshot: pushed to remote");
|
|
380
|
+
}
|
|
381
|
+
catch (pushErr) {
|
|
382
|
+
log?.warn({ err: toErrorDetail(pushErr), branch }, "recordPublishSnapshot: push failed (commit exists locally)");
|
|
383
|
+
}
|
|
384
|
+
return commitSha;
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
log?.warn({ err: toErrorDetail(err), session }, "recordPublishSnapshot: failed to record snapshot");
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function sanitizeBranch(input) {
|
|
392
|
+
const trimmed = input.trim();
|
|
393
|
+
return trimmed.length > 0 ? trimmed : "main";
|
|
394
|
+
}
|
|
395
|
+
export async function publishViaGit(session) {
|
|
396
|
+
const repoRoot = resolve(process.cwd(), "../..");
|
|
397
|
+
const targetPath = "apps/site/lib/published-content.json";
|
|
398
|
+
const absoluteTargetPath = resolve(repoRoot, targetPath);
|
|
399
|
+
const branch = sanitizeBranch(process.env.PUBLISH_GIT_BRANCH ?? "main");
|
|
400
|
+
const strict = process.env.PUBLISH_GIT_STRICT === "1";
|
|
401
|
+
let pages = getSessionPages(session);
|
|
402
|
+
const slugs = pages.map((page) => page.slug);
|
|
403
|
+
// Rewrite localhost image URLs → relative paths and copy files into public/
|
|
404
|
+
const imageUrlMap = findLocalhostImageUrls(pages);
|
|
405
|
+
const imageDestDir = resolve(repoRoot, "apps/site/public/generated-images");
|
|
406
|
+
let copiedImages = false;
|
|
407
|
+
if (imageUrlMap.size > 0) {
|
|
408
|
+
const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ??
|
|
409
|
+
resolve(process.cwd(), "../../.data/generated-images");
|
|
410
|
+
await mkdir(imageDestDir, { recursive: true });
|
|
411
|
+
for (const [, fileName] of imageUrlMap) {
|
|
412
|
+
const src = resolve(generatedImageDir, fileName);
|
|
413
|
+
const dest = resolve(imageDestDir, fileName);
|
|
414
|
+
try {
|
|
415
|
+
await copyFile(src, dest);
|
|
416
|
+
copiedImages = true;
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Source file missing — still rewrite URL (relative 404 > unreachable localhost)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
pages = rewriteImageUrlsInPages(pages, imageUrlMap);
|
|
423
|
+
}
|
|
424
|
+
const siteConfig = getSiteConfig(session);
|
|
425
|
+
const payload = `${JSON.stringify({ pages, siteConfig }, null, 2)}\n`;
|
|
426
|
+
await writeFile(absoluteTargetPath, payload, "utf8");
|
|
427
|
+
const statusRaw = await runGit(["status", "--porcelain"], repoRoot);
|
|
428
|
+
const statusLines = statusRaw.stdout
|
|
429
|
+
.split("\n")
|
|
430
|
+
.map((line) => line.trimEnd())
|
|
431
|
+
.filter(Boolean);
|
|
432
|
+
if (strict) {
|
|
433
|
+
const allowedPrefixes = [targetPath, "apps/site/public/generated-images/"];
|
|
434
|
+
const blocking = statusLines.filter((line) => {
|
|
435
|
+
const filePart = line.slice(3); // strip status prefix (e.g. " M " or "?? ")
|
|
436
|
+
return !allowedPrefixes.some((prefix) => filePart.startsWith(prefix));
|
|
437
|
+
});
|
|
438
|
+
if (blocking.length > 0) {
|
|
439
|
+
return {
|
|
440
|
+
status: "failed",
|
|
441
|
+
session,
|
|
442
|
+
slugs,
|
|
443
|
+
reason: "working_tree_not_clean",
|
|
444
|
+
details: blocking.slice(0, 12)
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const addPaths = [targetPath];
|
|
449
|
+
if (copiedImages)
|
|
450
|
+
addPaths.push("apps/site/public/generated-images");
|
|
451
|
+
await runGit(["add", ...addPaths], repoRoot);
|
|
452
|
+
try {
|
|
453
|
+
await runGit(["diff", "--cached", "--quiet"], repoRoot);
|
|
454
|
+
return {
|
|
455
|
+
status: "ready",
|
|
456
|
+
session,
|
|
457
|
+
slugs,
|
|
458
|
+
branch,
|
|
459
|
+
message: "No content changes to publish."
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Expected when there are staged changes for targetPath.
|
|
464
|
+
}
|
|
465
|
+
const commitMessage = `publish: session ${session} ${new Date().toISOString()}`;
|
|
466
|
+
try {
|
|
467
|
+
await runGit([...gitIdentityPrefix(), "commit", "-m", commitMessage], repoRoot);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
const detail = toErrorDetail(error);
|
|
471
|
+
if (detail.includes("nothing to commit")) {
|
|
472
|
+
return {
|
|
473
|
+
status: "ready",
|
|
474
|
+
session,
|
|
475
|
+
slugs,
|
|
476
|
+
branch,
|
|
477
|
+
message: "No content changes to publish."
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
status: "failed",
|
|
482
|
+
session,
|
|
483
|
+
slugs,
|
|
484
|
+
reason: "commit_failed",
|
|
485
|
+
details: [detail]
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
let headSha = "";
|
|
489
|
+
try {
|
|
490
|
+
const rev = await runGit(["rev-parse", "HEAD"], repoRoot);
|
|
491
|
+
headSha = rev.stdout.trim();
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// Ignore; commit succeeded.
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
// Push current HEAD to the configured remote branch so publish works
|
|
498
|
+
// even when orchestrator runs from a non-main local branch. The auth
|
|
499
|
+
// prefix supplies a push token on hosts whose checkout has none (Render).
|
|
500
|
+
await runGit([...gitAuthPrefix(), "push", "origin", `HEAD:${branch}`], repoRoot);
|
|
501
|
+
return {
|
|
502
|
+
status: "triggered",
|
|
503
|
+
session,
|
|
504
|
+
slugs,
|
|
505
|
+
branch,
|
|
506
|
+
commitSha: headSha || undefined,
|
|
507
|
+
vercelState: "TRIGGERED"
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
return {
|
|
512
|
+
status: "failed",
|
|
513
|
+
session,
|
|
514
|
+
slugs,
|
|
515
|
+
branch,
|
|
516
|
+
commitSha: headSha || undefined,
|
|
517
|
+
reason: "push_failed",
|
|
518
|
+
details: [toErrorDetail(error)]
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PublishContext, PublishTarget } from "./publish-target.ts";
|
|
2
|
+
export declare function registerPublishTarget(target: PublishTarget): void;
|
|
3
|
+
export declare function getPublishTarget(name: string): PublishTarget | undefined;
|
|
4
|
+
export declare function listPublishTargets(): PublishTarget[];
|
|
5
|
+
/** Primarily for tests; leaves the registry empty. */
|
|
6
|
+
export declare function _resetPublishTargetsForTest(): void;
|
|
7
|
+
export declare function selectPublishTarget(ctx: PublishContext): PublishTarget | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { SiteContractPublishTarget } from "./targets/site-contract.js";
|
|
2
|
+
import { GitPublishTarget } from "./targets/git.js";
|
|
3
|
+
import { DeployHookPublishTarget } from "./targets/deploy-hook.js";
|
|
4
|
+
/**
|
|
5
|
+
* Registry for {@link PublishTarget} plugins.
|
|
6
|
+
*
|
|
7
|
+
* Built-in targets (`site-contract`, `git`, `deploy-hook`) are registered at
|
|
8
|
+
* module load. Downstream code — whether a consumer of this project or a
|
|
9
|
+
* future plugin — can register additional targets before the server starts
|
|
10
|
+
* handling requests:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { registerPublishTarget } from "./publish/publish-target-registry.js"
|
|
14
|
+
* registerPublishTarget(new S3PublishTarget())
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Target selection order on every `POST /publish` call:
|
|
18
|
+
* 1. If `PUBLISH_TARGET=<name>` env var is set AND that target is registered,
|
|
19
|
+
* use it verbatim. (Escape hatch for operators who want to force one.)
|
|
20
|
+
* 2. Otherwise, iterate registered targets and pick the first whose
|
|
21
|
+
* `canHandle(ctx)` returns true.
|
|
22
|
+
* 3. Fall back to `PUBLISH_MODE` (legacy env): `git` (default) or
|
|
23
|
+
* `deploy_hook` → `deploy-hook`.
|
|
24
|
+
*/
|
|
25
|
+
const targets = new Map();
|
|
26
|
+
export function registerPublishTarget(target) {
|
|
27
|
+
targets.set(target.name, target);
|
|
28
|
+
}
|
|
29
|
+
export function getPublishTarget(name) {
|
|
30
|
+
return targets.get(name);
|
|
31
|
+
}
|
|
32
|
+
export function listPublishTargets() {
|
|
33
|
+
return Array.from(targets.values());
|
|
34
|
+
}
|
|
35
|
+
/** Primarily for tests; leaves the registry empty. */
|
|
36
|
+
export function _resetPublishTargetsForTest() {
|
|
37
|
+
targets.clear();
|
|
38
|
+
}
|
|
39
|
+
export function selectPublishTarget(ctx) {
|
|
40
|
+
const override = process.env.PUBLISH_TARGET?.trim();
|
|
41
|
+
if (override) {
|
|
42
|
+
const forced = targets.get(override);
|
|
43
|
+
if (forced)
|
|
44
|
+
return forced;
|
|
45
|
+
}
|
|
46
|
+
for (const target of targets.values()) {
|
|
47
|
+
if (target.canHandle?.(ctx))
|
|
48
|
+
return target;
|
|
49
|
+
}
|
|
50
|
+
const legacyMode = (process.env.PUBLISH_MODE?.trim().toLowerCase() || "git");
|
|
51
|
+
if (legacyMode === "deploy_hook")
|
|
52
|
+
return targets.get("deploy-hook");
|
|
53
|
+
return targets.get("git");
|
|
54
|
+
}
|
|
55
|
+
// Register built-ins on module load. Order matters: `canHandle` is checked
|
|
56
|
+
// in insertion order, so site-contract (which has a real `canHandle`) must
|
|
57
|
+
// come first — the others have no `canHandle` and are only reachable via the
|
|
58
|
+
// explicit/legacy fallback path.
|
|
59
|
+
registerPublishTarget(new SiteContractPublishTarget());
|
|
60
|
+
registerPublishTarget(new GitPublishTarget());
|
|
61
|
+
registerPublishTarget(new DeployHookPublishTarget());
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Logger } from "../logger.ts";
|
|
2
|
+
import type { PageDoc, SiteConfig } from "@avocadostudio-ai/shared";
|
|
3
|
+
import type { PublishTracker } from "../state/session-state.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Publish targets are the plugin point for where a `POST /publish` call ends
|
|
6
|
+
* up writing content. Built-ins cover git + Vercel, a site-contract POST, and
|
|
7
|
+
* a raw Vercel deploy hook. Third parties (S3, GitLab Pages, custom CI, etc.)
|
|
8
|
+
* implement {@link PublishTarget} and register via
|
|
9
|
+
* {@link registerPublishTarget} from `./publish-target-registry.js`.
|
|
10
|
+
*
|
|
11
|
+
* The route handler's job shrinks to: build {@link PublishContext}, call
|
|
12
|
+
* {@link selectPublishTarget}, invoke `target.publish(ctx)`, save the returned
|
|
13
|
+
* tracker, and reply with `outcome.response` / `outcome.httpStatus`.
|
|
14
|
+
*/
|
|
15
|
+
/** Request context handed to every publish target. */
|
|
16
|
+
export type PublishContext = {
|
|
17
|
+
/** Normalized unscoped session id (what the client sent). */
|
|
18
|
+
session: string;
|
|
19
|
+
/** `session::siteId` key used for state lookups. */
|
|
20
|
+
scopedSession: string;
|
|
21
|
+
/** Raw siteId from the request body (unnormalized). */
|
|
22
|
+
siteId?: string;
|
|
23
|
+
/** Validated remote site origin, if the client supplied one. */
|
|
24
|
+
siteOrigin?: string;
|
|
25
|
+
/** Draft pages for this session, in publish order. */
|
|
26
|
+
pages: PageDoc[];
|
|
27
|
+
/** Slugs of `pages`, precomputed for target convenience. */
|
|
28
|
+
slugs: string[];
|
|
29
|
+
/** Site config for this scoped session. */
|
|
30
|
+
siteConfig: SiteConfig;
|
|
31
|
+
/** Path on disk where the orchestrator caches generated images. */
|
|
32
|
+
generatedImageDir: string;
|
|
33
|
+
/** Fastify request logger. */
|
|
34
|
+
logger: Logger;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* What a target returns to the route handler. The route does not interpret the
|
|
38
|
+
* `response` body — it's passed straight through to the HTTP client.
|
|
39
|
+
*/
|
|
40
|
+
export type PublishOutcome = {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
/** HTTP status for the reply. 200 on success; 400/502 on failure. */
|
|
43
|
+
httpStatus: number;
|
|
44
|
+
/** Tracker to persist in `publishStatusBySession`. */
|
|
45
|
+
tracker: PublishTracker;
|
|
46
|
+
/** Body to send back as JSON. */
|
|
47
|
+
response: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
/** Status row returned by GET /publish/status. */
|
|
50
|
+
export type PublishStatus = {
|
|
51
|
+
status: "triggered" | "failed" | "ready";
|
|
52
|
+
deploymentId?: string;
|
|
53
|
+
vercelState?: string;
|
|
54
|
+
inspectUrl?: string;
|
|
55
|
+
lastCheckError?: string;
|
|
56
|
+
};
|
|
57
|
+
export interface PublishTarget {
|
|
58
|
+
/** Stable identifier. Used by `PUBLISH_TARGET` env var and the registry. */
|
|
59
|
+
readonly name: string;
|
|
60
|
+
/**
|
|
61
|
+
* Optional selection hint. The registry calls this when deciding which
|
|
62
|
+
* target handles a given request. Return `true` to claim the request.
|
|
63
|
+
* If omitted, the target is only dispatched when explicitly selected by
|
|
64
|
+
* name (e.g. via `PUBLISH_TARGET=<name>`).
|
|
65
|
+
*/
|
|
66
|
+
canHandle?(ctx: PublishContext): boolean;
|
|
67
|
+
publish(ctx: PublishContext): Promise<PublishOutcome>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Kept for one release so downstream code importing
|
|
71
|
+
* `PublishResult` from this module continues to compile. New code should use
|
|
72
|
+
* {@link PublishOutcome}. Will be removed once all consumers migrate.
|
|
73
|
+
*/
|
|
74
|
+
export type PublishResult = {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
slugs: string[];
|
|
77
|
+
deploymentId?: string;
|
|
78
|
+
deploymentUrl?: string;
|
|
79
|
+
inspectUrl?: string;
|
|
80
|
+
error?: string;
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|