create-ailk 0.30.0 → 0.32.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/README.md +108 -4
- package/component-catalog.json +524 -8
- package/component-catalog.md +8 -5
- package/dist/body-heading.d.ts +13 -0
- package/dist/body-heading.js +19 -5
- package/dist/cli.js +99 -0
- package/dist/cutover.d.ts +114 -0
- package/dist/cutover.js +511 -0
- package/dist/design-presets.js +20 -4
- package/dist/export-content.d.ts +55 -0
- package/dist/export-content.js +303 -0
- package/dist/inventory.d.ts +216 -0
- package/dist/inventory.js +833 -0
- package/dist/language-import.d.ts +127 -0
- package/dist/language-import.js +451 -0
- package/dist/lib/env-catalog.js +1 -1
- package/dist/lib/import-mdx.d.ts +38 -0
- package/dist/lib/import-mdx.js +445 -65
- package/dist/lib/render-comparison.d.ts +251 -0
- package/dist/lib/render-comparison.js +573 -0
- package/dist/mdx-braces.d.ts +73 -0
- package/dist/mdx-braces.js +285 -0
- package/dist/migrate-content.d.ts +6 -2
- package/dist/migrate-content.js +62 -14
- package/dist/programmatic.js +2 -8
- package/dist/seed-workspace.d.ts +3 -1
- package/dist/seed-workspace.js +45 -16
- package/dist/site-manifest.js +6 -38
- package/dist/site-roster.d.ts +46 -3
- package/dist/site-roster.js +124 -4
- package/dist/surfaces.d.ts +8 -0
- package/dist/surfaces.js +16 -0
- package/dist/topology.d.ts +12 -0
- package/dist/topology.js +24 -10
- package/dist/update.d.ts +21 -1
- package/dist/update.js +142 -11
- package/package.json +3 -3
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/content-source-guard.sh +75 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/CLAUDE.md +5 -0
- package/templates/apps/mcp/.env.example +7 -1
- package/templates/apps/mcp/CLAUDE.md +8 -4
- package/templates/apps/web/.env.example +4 -2
- package/templates/apps/web/CLAUDE.md +16 -5
- package/templates/apps/web/__tests__/setup.ts +9 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +156 -4
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +45 -2
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +6 -4
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/__tests__/program-event.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +11 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +44 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.thanks-by-tier.test.tsx +306 -0
- package/templates/apps/web/app/[locale]/layout.tsx +122 -27
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +21 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/llms.txt/route.ts +16 -9
- package/templates/apps/web/app/sitemap.xml/route.ts +3 -0
- package/templates/apps/web/jest.config.cjs +46 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +247 -4
- package/templates/apps/web/lib/__tests__/in-page-magnet-self-request.test.ts +92 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +33 -8
- package/templates/apps/web/lib/__tests__/site-identity-feature-list.test.ts +196 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +145 -11
- package/templates/apps/web/lib/bot-check.ts +113 -13
- package/templates/apps/web/lib/in-page-magnet.ts +4 -1
- package/templates/apps/web/lib/page-chrome.ts +65 -5
- package/templates/apps/web/lib/relay.ts +22 -3
- package/templates/apps/web/lib/site-identity.ts +75 -24
- package/templates/apps/web/messages/en.json +5 -1
- package/templates/apps/web/next.config.mjs +15 -1
- package/templates/apps/web/package.json +1 -1
- package/templates/apps/web/scripts/assert-content-bundled.ts +36 -8
- package/templates/apps/web/scripts/content-snapshot.ts +33 -0
- package/templates/apps/web/src/lib/routes.ts +5 -3
- package/templates/package.json +3 -1
- package/templates/tools/body-heading.ts +440 -0
- package/templates/tools/check-content-source.ts +108 -0
- package/templates/tools/check-mdx-body.ts +123 -0
- package/templates/tools/mdx-braces.ts +303 -0
- package/versions.json +48 -48
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bare `{…}` braces on mdx-body pages (#10773, spec
|
|
3
|
+
* docs/specs/10773-update-never-refreshes-0-19-spec.md D3, D4).
|
|
4
|
+
*
|
|
5
|
+
* MDX reads every `{` in body text as the start of a JavaScript expression.
|
|
6
|
+
* Under the renderer's `next-mdx-remote` defaults (`blockJS`), an expression
|
|
7
|
+
* that does not parse fails the page, and one that parses is deleted — so on
|
|
8
|
+
* an mdx-body page every `{` in text other than a `{/* … *\/}` comment is a
|
|
9
|
+
* defect. This module is the one lexical rule for that brace:
|
|
10
|
+
*
|
|
11
|
+
* - `findBareBraces` names every bare brace in a body, by offset, line and
|
|
12
|
+
* column, in one left-to-right pass;
|
|
13
|
+
* - `escapeBareBraces` inserts one `\` before each, and keeps the result only
|
|
14
|
+
* when the site's own MDX parser reads it with no expression left but
|
|
15
|
+
* comments — the lexer proposes, the parser disposes.
|
|
16
|
+
*
|
|
17
|
+
* Used by `create-ailk update` and, through a byte-identical copy in the
|
|
18
|
+
* scaffold's `tools/`, by `pnpm mdx-body:check`. Pure: no filesystem and no
|
|
19
|
+
* imports. It must never contain a double open brace, which the scaffold copy
|
|
20
|
+
* substitutes as a marker.
|
|
21
|
+
*/
|
|
22
|
+
const ESM_RE = /^(?:import|export)[ {*]/;
|
|
23
|
+
// MDX has no indented code, so a fence opens at any indentation — and inside
|
|
24
|
+
// a list item or a blockquote, after its markers.
|
|
25
|
+
const FENCE_RE = /^[ \t]*(?:(?:>|[-*+]|\d{1,9}[.)])[ \t]*)*(`{3,}|~{3,})/;
|
|
26
|
+
const COMMENT_CLOSE_RE = /\*\/\s*\}/g;
|
|
27
|
+
const WHITESPACE_RE = /\s/;
|
|
28
|
+
const TAG_START_RE = /[A-Za-z/>]/;
|
|
29
|
+
const isBlank = (line) => line.trim() === "";
|
|
30
|
+
/**
|
|
31
|
+
* Every bare `{` in an MDX body. Skips, in this order of precedence: fenced
|
|
32
|
+
* code, ESM blocks, backslash escapes, inline code, JSX tags (attribute
|
|
33
|
+
* expressions included) and `{/* … *\/}` comments. Linear in the body.
|
|
34
|
+
*/
|
|
35
|
+
export function findBareBraces(body) {
|
|
36
|
+
const found = [];
|
|
37
|
+
const lineEnd = (from) => {
|
|
38
|
+
const end = body.indexOf("\n", from);
|
|
39
|
+
return end === -1 ? body.length : end;
|
|
40
|
+
};
|
|
41
|
+
// A code span or a tag never crosses a blank line: true at a `\n` that ends
|
|
42
|
+
// the body or starts a blank line.
|
|
43
|
+
const paragraphBreak = (at) => {
|
|
44
|
+
let j = at + 1;
|
|
45
|
+
while (body[j] === " " || body[j] === "\t" || body[j] === "\r")
|
|
46
|
+
j += 1;
|
|
47
|
+
return j >= body.length || body[j] === "\n";
|
|
48
|
+
};
|
|
49
|
+
// Memo so a failed search is never repeated over the same text.
|
|
50
|
+
const spanFailedUntil = new Map();
|
|
51
|
+
let tagFailedUntil = -1;
|
|
52
|
+
let commentClose;
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < body.length) {
|
|
55
|
+
if (i === 0 || body[i - 1] === "\n") {
|
|
56
|
+
const end = lineEnd(i);
|
|
57
|
+
const line = body.slice(i, end);
|
|
58
|
+
const fence = FENCE_RE.exec(line)?.[1];
|
|
59
|
+
if (fence !== undefined) {
|
|
60
|
+
// Skip to after the closing fence line, or to the end of the body.
|
|
61
|
+
let at = end;
|
|
62
|
+
while (at < body.length) {
|
|
63
|
+
const next = lineEnd(at + 1);
|
|
64
|
+
const candidate = body.slice(at + 1, next);
|
|
65
|
+
const run = FENCE_RE.exec(candidate);
|
|
66
|
+
at = next;
|
|
67
|
+
if (run !== null &&
|
|
68
|
+
run[1][0] === fence[0] &&
|
|
69
|
+
run[1].length >= fence.length &&
|
|
70
|
+
candidate.slice(run[0].length).trim() === "") {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
i = at + 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ESM_RE.test(line)) {
|
|
78
|
+
let at = end;
|
|
79
|
+
while (at < body.length) {
|
|
80
|
+
const next = lineEnd(at + 1);
|
|
81
|
+
if (isBlank(body.slice(at + 1, next)))
|
|
82
|
+
break;
|
|
83
|
+
at = next;
|
|
84
|
+
}
|
|
85
|
+
i = at + 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const char = body[i];
|
|
90
|
+
if (char === "\\") {
|
|
91
|
+
// A backslash before a line break is a hard break; the next line is read as a line.
|
|
92
|
+
i += body[i + 1] === "\n" ? 1 : 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === "`") {
|
|
96
|
+
let run = 1;
|
|
97
|
+
while (body[i + run] === "`")
|
|
98
|
+
run += 1;
|
|
99
|
+
const close = spanFailedUntil.get(run) ?? -1;
|
|
100
|
+
let closed = -1;
|
|
101
|
+
if (close < i) {
|
|
102
|
+
let at = i + run;
|
|
103
|
+
for (; at < body.length; at += 1) {
|
|
104
|
+
if (body[at] === "\n" && (paragraphBreak(at) || FENCE_RE.test(body.slice(at + 1, lineEnd(at + 1)))))
|
|
105
|
+
break;
|
|
106
|
+
if (body[at] !== "`")
|
|
107
|
+
continue;
|
|
108
|
+
let length = 1;
|
|
109
|
+
while (body[at + length] === "`")
|
|
110
|
+
length += 1;
|
|
111
|
+
if (length === run) {
|
|
112
|
+
closed = at + length;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
at += length - 1;
|
|
116
|
+
}
|
|
117
|
+
if (closed === -1)
|
|
118
|
+
spanFailedUntil.set(run, at);
|
|
119
|
+
}
|
|
120
|
+
i = closed === -1 ? i + run : closed;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (char === "<" && TAG_START_RE.test(body[i + 1] ?? "") && i >= tagFailedUntil) {
|
|
124
|
+
let depth = 0;
|
|
125
|
+
let quote = "";
|
|
126
|
+
let closed = -1;
|
|
127
|
+
let at = i + 1;
|
|
128
|
+
for (; at < body.length; at += 1) {
|
|
129
|
+
const c = body[at];
|
|
130
|
+
if (c === "\n" && paragraphBreak(at))
|
|
131
|
+
break;
|
|
132
|
+
if (quote !== "") {
|
|
133
|
+
if (c === quote)
|
|
134
|
+
quote = "";
|
|
135
|
+
}
|
|
136
|
+
else if (depth > 0) {
|
|
137
|
+
if (c === "{")
|
|
138
|
+
depth += 1;
|
|
139
|
+
else if (c === "}")
|
|
140
|
+
depth -= 1;
|
|
141
|
+
}
|
|
142
|
+
else if (c === '"' || c === "'") {
|
|
143
|
+
quote = c;
|
|
144
|
+
}
|
|
145
|
+
else if (c === "{") {
|
|
146
|
+
depth = 1;
|
|
147
|
+
}
|
|
148
|
+
else if (c === ">") {
|
|
149
|
+
closed = at + 1;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (closed !== -1) {
|
|
154
|
+
i = closed;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// An unclosed tag already breaks the page; no later `<` before `at` is
|
|
158
|
+
// read as a tag, which keeps the scan linear.
|
|
159
|
+
tagFailedUntil = at;
|
|
160
|
+
i += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === "{") {
|
|
164
|
+
let next = i + 1;
|
|
165
|
+
while (next < body.length && WHITESPACE_RE.test(body[next]))
|
|
166
|
+
next += 1;
|
|
167
|
+
if (body[next] === "/" && body[next + 1] === "*") {
|
|
168
|
+
if (commentClose === undefined || commentClose.from > i || (commentClose.at !== -1 && commentClose.at < i)) {
|
|
169
|
+
COMMENT_CLOSE_RE.lastIndex = next + 2;
|
|
170
|
+
const match = COMMENT_CLOSE_RE.exec(body);
|
|
171
|
+
commentClose = { from: i, at: match === null ? -1 : match.index + match[0].length };
|
|
172
|
+
}
|
|
173
|
+
if (commentClose.at !== -1) {
|
|
174
|
+
i = commentClose.at;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
found.push({ offset: i, kind: body[next] === "/" ? "comment-like" : "placeholder" });
|
|
179
|
+
}
|
|
180
|
+
i += 1;
|
|
181
|
+
}
|
|
182
|
+
// Offsets to 1-based line and column, in one pass.
|
|
183
|
+
const braces = [];
|
|
184
|
+
let line = 1;
|
|
185
|
+
let lineStart = 0;
|
|
186
|
+
let scanned = 0;
|
|
187
|
+
for (const brace of found) {
|
|
188
|
+
for (; scanned < brace.offset; scanned += 1) {
|
|
189
|
+
if (body[scanned] === "\n") {
|
|
190
|
+
line += 1;
|
|
191
|
+
lineStart = scanned + 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
braces.push({ ...brace, line, column: brace.offset - lineStart + 1 });
|
|
195
|
+
}
|
|
196
|
+
return braces;
|
|
197
|
+
}
|
|
198
|
+
const COMMENT_VALUE_RE = /^\s*\/\*[\s\S]*\*\/\s*$/;
|
|
199
|
+
/** The first expression node left in the tree that is not a comment. */
|
|
200
|
+
export function firstExpression(node) {
|
|
201
|
+
if ((node.type === "mdxFlowExpression" || node.type === "mdxTextExpression") &&
|
|
202
|
+
!(typeof node.value === "string" && COMMENT_VALUE_RE.test(node.value))) {
|
|
203
|
+
return node;
|
|
204
|
+
}
|
|
205
|
+
for (const child of node.children ?? []) {
|
|
206
|
+
const hit = firstExpression(child);
|
|
207
|
+
if (hit !== undefined)
|
|
208
|
+
return hit;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
/** The first `code` or `inlineCode` node whose span holds one of `offsets` (sorted). */
|
|
213
|
+
function codeNodeHolding(node, offsets) {
|
|
214
|
+
if (node.type === "code" || node.type === "inlineCode") {
|
|
215
|
+
const start = node.position?.start?.offset;
|
|
216
|
+
const end = node.position?.end?.offset;
|
|
217
|
+
if (typeof start === "number" && typeof end === "number") {
|
|
218
|
+
let lo = 0;
|
|
219
|
+
let hi = offsets.length;
|
|
220
|
+
while (lo < hi) {
|
|
221
|
+
const mid = (lo + hi) >> 1;
|
|
222
|
+
if (offsets[mid] < start)
|
|
223
|
+
lo = mid + 1;
|
|
224
|
+
else
|
|
225
|
+
hi = mid;
|
|
226
|
+
}
|
|
227
|
+
if (lo < offsets.length && offsets[lo] < end)
|
|
228
|
+
return node;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const child of node.children ?? []) {
|
|
232
|
+
const hit = codeNodeHolding(child, offsets);
|
|
233
|
+
if (hit !== undefined)
|
|
234
|
+
return hit;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Escape every bare brace in `body`, all or nothing: `clean` when there is
|
|
240
|
+
* none (`parse` is not called), `refused` when a brace is a mangled comment,
|
|
241
|
+
* the escaped text does not parse, or an expression the escape did not reach
|
|
242
|
+
* remains; otherwise `escaped`.
|
|
243
|
+
*/
|
|
244
|
+
export function escapeBareBraces(body, parse) {
|
|
245
|
+
const braces = findBareBraces(body);
|
|
246
|
+
if (braces.length === 0)
|
|
247
|
+
return { kind: "clean" };
|
|
248
|
+
const mangled = braces.find((brace) => brace.kind === "comment-like");
|
|
249
|
+
if (mangled !== undefined) {
|
|
250
|
+
return { kind: "refused", reason: `{/ at line ${mangled.line} is not a {/* … */} comment` };
|
|
251
|
+
}
|
|
252
|
+
const parts = [];
|
|
253
|
+
let from = 0;
|
|
254
|
+
for (const { offset } of braces) {
|
|
255
|
+
parts.push(body.slice(from, offset), "\\");
|
|
256
|
+
from = offset;
|
|
257
|
+
}
|
|
258
|
+
parts.push(body.slice(from));
|
|
259
|
+
const text = parts.join("");
|
|
260
|
+
let tree;
|
|
261
|
+
try {
|
|
262
|
+
tree = parse(text);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const { line, reason } = (err ?? {});
|
|
266
|
+
const why = typeof reason === "string" ? reason : err instanceof Error ? err.message : String(err);
|
|
267
|
+
return { kind: "refused", reason: `line ${typeof line === "number" ? line : "?"}: ${why}` };
|
|
268
|
+
}
|
|
269
|
+
const escapes = braces.map((brace, k) => brace.offset + k);
|
|
270
|
+
const inCode = codeNodeHolding(tree, escapes);
|
|
271
|
+
if (inCode !== undefined) {
|
|
272
|
+
return {
|
|
273
|
+
kind: "refused",
|
|
274
|
+
reason: `a brace at line ${inCode.position?.start?.line ?? "?"} is inside code the lexer did not see`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const left = firstExpression(tree);
|
|
278
|
+
if (left !== undefined) {
|
|
279
|
+
return {
|
|
280
|
+
kind: "refused",
|
|
281
|
+
reason: `an expression at line ${left.position?.start?.line ?? "?"} the escape did not reach`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return { kind: "escaped", text, count: braces.length };
|
|
285
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LanguageImportResult } from "./language-import.js";
|
|
1
2
|
import { type ImportMdxResult, type MdxAdapterModule } from "./lib/import-mdx.js";
|
|
2
3
|
/** A refusal the CLI exits 2 on, thrown before any request is sent. */
|
|
3
4
|
export declare class MigrateContentRefusedError extends Error {
|
|
@@ -27,12 +28,15 @@ export interface MigrateContentOptions {
|
|
|
27
28
|
/**
|
|
28
29
|
* A fetch that sends `GET`s through `inner` and never sends a write: a `POST`
|
|
29
30
|
* is answered `201` and a `PATCH` `200` locally, with the one member of a
|
|
30
|
-
* write's answer `importMdx` reads (`data.locale`)
|
|
31
|
+
* write's answer `importMdx` reads (`data.locale`), and the structure write's
|
|
32
|
+
* `PUT` (#10534) `200` as a layout unchanged (`data.written: false`). Any
|
|
33
|
+
* other method throws.
|
|
31
34
|
*/
|
|
32
35
|
export declare function readOnlyFetch(inner: typeof globalThis.fetch, locale: string): typeof globalThis.fetch;
|
|
33
36
|
export declare function migrateContent(opts: MigrateContentOptions): Promise<{
|
|
34
37
|
result: ImportMdxResult;
|
|
35
38
|
exitCode: 0 | 1;
|
|
39
|
+
language?: LanguageImportResult;
|
|
36
40
|
}>;
|
|
37
41
|
export interface MigrateContentArgs {
|
|
38
42
|
target: string | undefined;
|
|
@@ -41,5 +45,5 @@ export interface MigrateContentArgs {
|
|
|
41
45
|
workspaceId: string | undefined;
|
|
42
46
|
help: boolean;
|
|
43
47
|
}
|
|
44
|
-
export declare const MIGRATE_CONTENT_HELP = "create-ailk migrate-content \u2014 import an existing site's pages into its workspace.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk migrate-content [<dir>] [--apply] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --apply Write the pages. Without it the command reads the\n workspace, prints what it would do, and writes\n nothing.\n --api-url <origin> The API origin that holds the workspace \u2014 on a\n spoke, its hub's. Else AILK_API_URL. https: only\n (http: on localhost or 127.0.0.1).\n --workspace <wksp_id> The workspace the pages are written to. Else\n AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nEach content/en/<Type>/<slug>.mdx page (and each legacy\ncontent/<Type>/<slug>.mdx) is written to the workspace as a draft through\nthe API's content routes, with the agent key, which is read from\nAILK_AGENT_KEY only and needs the content-edit grant. Nothing is published,\nso the live site does not change. The site needs no .ailk-site-manifest.json\nand may be a spoke; its dependencies must be installed.\n\nA dry run prints create, update, unchanged, skipped or failed per page. It\ncannot see a write the API would refuse at --apply (a conflict or a server\nerror),
|
|
48
|
+
export declare const MIGRATE_CONTENT_HELP = "create-ailk migrate-content \u2014 import an existing site's pages into its workspace.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk migrate-content [<dir>] [--apply] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --apply Write the pages. Without it the command reads the\n workspace, prints what it would do, and writes\n nothing.\n --api-url <origin> The API origin that holds the workspace \u2014 on a\n spoke, its hub's. Else AILK_API_URL. https: only\n (http: on localhost or 127.0.0.1).\n --workspace <wksp_id> The workspace the pages are written to. Else\n AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nEach content/en/<Type>/<slug>.mdx page (and each legacy\ncontent/<Type>/<slug>.mdx) is written to the workspace as a draft through\nthe API's content routes, with the agent key, which is read from\nAILK_AGENT_KEY only and needs the content-edit grant. Nothing is published,\nso the live site does not change. The site needs no .ailk-site-manifest.json\nand may be a spoke; its dependencies must be installed. A page whose\nfrontmatter carries sections is converted: its words are written to the\npage's Markdown and its sections to its layout.\n\nEach FAQ item and glossary-index term becomes one Language item of its kind,\nset Live when the run creates it, and its section pins the items in the\norder the file lists them. A question on several pages is one item, with\nthe FAQ page's words (the glossary page's, for a term). Lines prefixed\nlanguage: report each item, and each page the database read will not show\nas its file does. An API with no Language collection leaves them inline.\n\nWhen the site has apps/web/content, each blog post and docs page in it is\nwritten too: apps/web/content/en/blog/<slug>.mdx (or <slug>/index.mdx) as a\nBlogPosting, or one series down, blog/<series>/<post>.mdx, as a BlogPosting\nwhose parent is the series; the same under docs as a DocsDetail, and each\ndirectory's own index.mdx as its BlogList or DocsList index. A file the walk\nskips is listed with the rule it failed. Their lines are prefixed\neditorial: \u2014 each outcome's root is content or editorial.\n\nA dry run prints create, update, unchanged, skipped or failed per page. It\ncannot see a write the API would refuse at --apply (a conflict or a server\nerror), nor a layout change on a page whose words did not change, and a\npage whose content fails its page type is planned and saved\nwith errors the editor shows, not failed. --apply prints created, updated,\nunchanged, skipped or failed per page, so a re-run is safe. Exit 0 when no\npage failed, 1 when any did, 2 on a refusal (nothing is sent).\n";
|
|
45
49
|
export declare function parseMigrateContentArgs(argv: string[]): MigrateContentArgs;
|
package/dist/migrate-content.js
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
* [--workspace <id>]` — import an existing site's MDX pages into its
|
|
4
4
|
* workspace (#10207).
|
|
5
5
|
*
|
|
6
|
-
* The same import as `seed-workspace` — `importMdx` over `<dir>/content
|
|
6
|
+
* The same import as `seed-workspace` — `importMdx` over `<dir>/content`, and
|
|
7
|
+
* the docs-blog tree `<dir>/apps/web/content` when it exists (#10701), with
|
|
7
8
|
* the site's own installed adapter — for the site `seed-workspace` refuses: one
|
|
8
9
|
* with no `.ailk-site-manifest.json`, or a spoke, whose workspace lives on its
|
|
9
10
|
* hub (D1). No manifest and no topology is read; the operator names the API
|
|
10
11
|
* origin and the workspace, which is all the topology would decide.
|
|
11
12
|
*
|
|
12
13
|
* A dry run is the default (D3): the importer runs over `readOnlyFetch`, which
|
|
13
|
-
* passes each `GET` through and answers a `POST` or `
|
|
14
|
-
* sending it, so the plan — create · update · unchanged · skipped · failed — is
|
|
14
|
+
* passes each `GET` through and answers a `POST`, `PATCH` or `PUT` locally
|
|
15
|
+
* without sending it, so the plan — create · update · unchanged · skipped · failed — is
|
|
15
16
|
* decided by the importer itself against the live workspace, and no write can
|
|
16
17
|
* leave the process. `--apply` writes; a second `--apply` reports every page
|
|
17
18
|
* `unchanged`.
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
*/
|
|
28
29
|
import { join, resolve } from "node:path";
|
|
29
30
|
import { parseArgs } from "node:util";
|
|
30
|
-
import {
|
|
31
|
+
import { languageLines, languageOffLine, withLanguage, } from "./language-import.js";
|
|
32
|
+
import { formatImportMdxReport, importMdx, isDirectory, loadSiteMdxAdapter, outcomePath, } from "./lib/import-mdx.js";
|
|
31
33
|
import { ParseError } from "./parse-args.js";
|
|
32
34
|
import { AGENT_KEY_ENV_VAR, WORKSPACE_ID_ENV_VAR } from "./seed-workspace.js";
|
|
33
35
|
import { HUB_ORIGIN_ENV_VAR, TopologyError, normalizeHubUrl, } from "./topology.js";
|
|
@@ -53,17 +55,20 @@ function given(value) {
|
|
|
53
55
|
/**
|
|
54
56
|
* A fetch that sends `GET`s through `inner` and never sends a write: a `POST`
|
|
55
57
|
* is answered `201` and a `PATCH` `200` locally, with the one member of a
|
|
56
|
-
* write's answer `importMdx` reads (`data.locale`)
|
|
58
|
+
* write's answer `importMdx` reads (`data.locale`), and the structure write's
|
|
59
|
+
* `PUT` (#10534) `200` as a layout unchanged (`data.written: false`). Any
|
|
60
|
+
* other method throws.
|
|
57
61
|
*/
|
|
58
62
|
export function readOnlyFetch(inner, locale) {
|
|
59
63
|
return (async (input, init) => {
|
|
60
64
|
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
61
65
|
if (method === "GET")
|
|
62
66
|
return inner(input, init);
|
|
63
|
-
if (method !== "POST" && method !== "PATCH") {
|
|
67
|
+
if (method !== "POST" && method !== "PATCH" && method !== "PUT") {
|
|
64
68
|
throw new Error(`a dry run sends no ${method} request`);
|
|
65
69
|
}
|
|
66
|
-
|
|
70
|
+
const data = method === "PUT" ? { locale, written: false } : { locale };
|
|
71
|
+
return new Response(JSON.stringify({ ok: true, data }), {
|
|
67
72
|
status: method === "POST" ? 201 : 200,
|
|
68
73
|
headers: { "content-type": "application/json" },
|
|
69
74
|
});
|
|
@@ -81,8 +86,8 @@ const PLAN_VERB = {
|
|
|
81
86
|
function planLine(outcome) {
|
|
82
87
|
const verb = PLAN_VERB[outcome.result];
|
|
83
88
|
return "reason" in outcome
|
|
84
|
-
? `${outcome
|
|
85
|
-
: `${outcome
|
|
89
|
+
? `${outcomePath(outcome)}: ${verb} — ${outcome.reason}`
|
|
90
|
+
: `${outcomePath(outcome)}: ${verb}`;
|
|
86
91
|
}
|
|
87
92
|
// ── The command (D1–D4) ─────────────────────────────────────────────────────
|
|
88
93
|
export async function migrateContent(opts) {
|
|
@@ -119,14 +124,29 @@ export async function migrateContent(opts) {
|
|
|
119
124
|
: `Planning ${opts.targetDir} against ${at} Nothing is written without --apply.`);
|
|
120
125
|
const adapters = opts.adapters ?? (await loadSiteMdxAdapter(targetDir));
|
|
121
126
|
const fetch = opts.fetch ?? globalThis.fetch;
|
|
127
|
+
const transport = apply ? fetch : readOnlyFetch(fetch, LOCALE);
|
|
128
|
+
// #10735 D1 — the Language pass wraps the importer's read.
|
|
129
|
+
const pass = withLanguage(adapters, {
|
|
130
|
+
apiOrigin: origin,
|
|
131
|
+
workspaceId,
|
|
132
|
+
apiKey,
|
|
133
|
+
locale: LOCALE,
|
|
134
|
+
fetch: transport,
|
|
135
|
+
apply,
|
|
136
|
+
...(opts.sleep !== undefined && { sleep: opts.sleep }),
|
|
137
|
+
onLine,
|
|
138
|
+
});
|
|
139
|
+
// #10701 D7 — the docs-blog tree, when the site has one.
|
|
140
|
+
const editorialRoot = join(targetDir, "apps", "web", "content");
|
|
122
141
|
const result = await importMdx({
|
|
123
142
|
contentRoot: join(targetDir, "content"),
|
|
124
|
-
|
|
143
|
+
...(isDirectory(editorialRoot) && { editorialRoot }),
|
|
144
|
+
adapters: pass.adapters,
|
|
125
145
|
apiOrigin: origin,
|
|
126
146
|
workspaceId,
|
|
127
147
|
apiKey,
|
|
128
148
|
defaultLocale: LOCALE,
|
|
129
|
-
fetch:
|
|
149
|
+
fetch: transport,
|
|
130
150
|
...(opts.sleep !== undefined && { sleep: opts.sleep }),
|
|
131
151
|
// A dry run's importer lines say `created`/`updated`, which nothing was.
|
|
132
152
|
...(apply && { onLine }),
|
|
@@ -148,7 +168,17 @@ export async function migrateContent(opts) {
|
|
|
148
168
|
onLine(`${failed} page(s) would fail. Fix them before --apply.`);
|
|
149
169
|
}
|
|
150
170
|
}
|
|
151
|
-
|
|
171
|
+
// #10735 D7 — the Language lines come last.
|
|
172
|
+
const { result: language, off } = pass.report();
|
|
173
|
+
if (off !== undefined)
|
|
174
|
+
onLine(languageOffLine(off));
|
|
175
|
+
if (language !== undefined)
|
|
176
|
+
languageLines(language, apply).forEach(onLine);
|
|
177
|
+
return {
|
|
178
|
+
result,
|
|
179
|
+
exitCode: failed > 0 ? 1 : 0,
|
|
180
|
+
...(language !== undefined && { language }),
|
|
181
|
+
};
|
|
152
182
|
}
|
|
153
183
|
export const MIGRATE_CONTENT_HELP = `create-ailk migrate-content — import an existing site's pages into its workspace.
|
|
154
184
|
|
|
@@ -174,11 +204,29 @@ content/<Type>/<slug>.mdx) is written to the workspace as a draft through
|
|
|
174
204
|
the API's content routes, with the agent key, which is read from
|
|
175
205
|
AILK_AGENT_KEY only and needs the content-edit grant. Nothing is published,
|
|
176
206
|
so the live site does not change. The site needs no .ailk-site-manifest.json
|
|
177
|
-
and may be a spoke; its dependencies must be installed.
|
|
207
|
+
and may be a spoke; its dependencies must be installed. A page whose
|
|
208
|
+
frontmatter carries sections is converted: its words are written to the
|
|
209
|
+
page's Markdown and its sections to its layout.
|
|
210
|
+
|
|
211
|
+
Each FAQ item and glossary-index term becomes one Language item of its kind,
|
|
212
|
+
set Live when the run creates it, and its section pins the items in the
|
|
213
|
+
order the file lists them. A question on several pages is one item, with
|
|
214
|
+
the FAQ page's words (the glossary page's, for a term). Lines prefixed
|
|
215
|
+
language: report each item, and each page the database read will not show
|
|
216
|
+
as its file does. An API with no Language collection leaves them inline.
|
|
217
|
+
|
|
218
|
+
When the site has apps/web/content, each blog post and docs page in it is
|
|
219
|
+
written too: apps/web/content/en/blog/<slug>.mdx (or <slug>/index.mdx) as a
|
|
220
|
+
BlogPosting, or one series down, blog/<series>/<post>.mdx, as a BlogPosting
|
|
221
|
+
whose parent is the series; the same under docs as a DocsDetail, and each
|
|
222
|
+
directory's own index.mdx as its BlogList or DocsList index. A file the walk
|
|
223
|
+
skips is listed with the rule it failed. Their lines are prefixed
|
|
224
|
+
editorial: — each outcome's root is content or editorial.
|
|
178
225
|
|
|
179
226
|
A dry run prints create, update, unchanged, skipped or failed per page. It
|
|
180
227
|
cannot see a write the API would refuse at --apply (a conflict or a server
|
|
181
|
-
error),
|
|
228
|
+
error), nor a layout change on a page whose words did not change, and a
|
|
229
|
+
page whose content fails its page type is planned and saved
|
|
182
230
|
with errors the editor shows, not failed. --apply prints created, updated,
|
|
183
231
|
unchanged, skipped or failed per page, so a re-run is safe. Exit 0 when no
|
|
184
232
|
page failed, 1 when any did, 2 on a refusal (nothing is sent).
|
package/dist/programmatic.js
CHANGED
|
@@ -46,7 +46,7 @@ import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
|
|
|
46
46
|
import { planSiteManifest } from "./site-manifest.js";
|
|
47
47
|
import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, withMeetPlaceholderSeeds, } from "./site-roster.js";
|
|
48
48
|
import { SPOKE_CHECKOUT_TEMPLATE_PATHS, dropSpokeCheckoutDependency, spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
|
|
49
|
-
import { GATEABLE_SURFACES,
|
|
49
|
+
import { GATEABLE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, isUnservedSurfaceContent, renderNavConfig, resolveSurfaces, surfacesNamedByRoutes, } from "./surfaces.js";
|
|
50
50
|
import { syncRoutes } from "./sync-routes.js";
|
|
51
51
|
import { BASE_LAYER_DIR, SHAPE_STARTERS, isOwnTree, shippedManifestPath, TEMPLATE_MANIFESTS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, templateTreeDir, } from "./template-manifest.js";
|
|
52
52
|
import { TEMPLATE_RECORD_PATH, isRecordable, planTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
|
|
@@ -1075,13 +1075,7 @@ function pruneUnrosteredSeeds(targetDir, roster, surfaces, ownership) {
|
|
|
1075
1075
|
* path segment is the surface's top-level route directory.
|
|
1076
1076
|
*/
|
|
1077
1077
|
function rosterSurfaces(template) {
|
|
1078
|
-
|
|
1079
|
-
const moduleOwned = new Set(Object.values(MODULE_SURFACES).flat());
|
|
1080
|
-
const topSegments = new Set(mergeRoster(template, template.requires)
|
|
1081
|
-
.map((row) => row.route.split("/").filter(Boolean)[0])
|
|
1082
|
-
.filter((segment) => segment !== undefined));
|
|
1083
|
-
return GATEABLE_SURFACES.filter((surface) => !moduleOwned.has(surface.id) &&
|
|
1084
|
-
surface.routes.some((dir) => topSegments.has(dir.slice(routeRoot.length + 1)))).map((surface) => surface.id);
|
|
1078
|
+
return surfacesNamedByRoutes(mergeRoster(template, template.requires).map((row) => row.route));
|
|
1085
1079
|
}
|
|
1086
1080
|
// Re-exported so a consumer of the async `scaffold()` above can type its own
|
|
1087
1081
|
// wrapper (e.g. the MCP scaffold-site tool) without reaching into `lib/`.
|
package/dist/seed-workspace.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { type Topology } from "./topology.js";
|
|
|
4
4
|
export declare const AGENT_KEY_ENV_VAR = "AILK_AGENT_KEY";
|
|
5
5
|
/** The environment variable the workspace id is read from when no flag names one. */
|
|
6
6
|
export declare const WORKSPACE_ID_ENV_VAR = "AILK_WORKSPACE_ID";
|
|
7
|
+
/** The environment variable each app chooses its content source by (#10659). */
|
|
8
|
+
export declare const CONTENT_ADAPTER_ENV_VAR = "CONTENT_ADAPTER";
|
|
7
9
|
/** A refusal the CLI exits 2 on, thrown before any request is sent. */
|
|
8
10
|
export declare class SeedWorkspaceRefusedError extends Error {
|
|
9
11
|
readonly exitCode = 2;
|
|
@@ -67,5 +69,5 @@ export interface SeedWorkspaceArgs {
|
|
|
67
69
|
workspaceId: string | undefined;
|
|
68
70
|
help: boolean;
|
|
69
71
|
}
|
|
70
|
-
export declare const SEED_WORKSPACE_HELP = "create-ailk seed-workspace \u2014 import a starter's pages into its workspace.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk seed-workspace [<dir>] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --api-url <origin> The site's API origin. Else AILK_API_URL. https:\n only (http: on localhost or 127.0.0.1).\n --workspace <wksp_id> The workspace the pages are written to. Else\n AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nEach content/en/<Type>/<slug>.mdx page is written to the workspace
|
|
72
|
+
export declare const SEED_WORKSPACE_HELP = "create-ailk seed-workspace \u2014 import a starter's pages into its workspace.\n\nUsage:\n AILK_AGENT_KEY=<key> create-ailk seed-workspace [<dir>] [--api-url <origin>] [--workspace <wksp_id>]\n\nArguments:\n <dir> The site. Defaults to the current dir.\n\nOptions:\n --api-url <origin> The site's API origin. Else AILK_API_URL. https:\n only (http: on localhost or 127.0.0.1).\n --workspace <wksp_id> The workspace the pages are written to. Else\n AILK_WORKSPACE_ID.\n -h, --help Show this help.\n\nEach content/en/<Type>/<slug>.mdx page is written to the workspace, and set\nLive when it has no Live version, through the API's content routes, with the\nagent key, which is read from AILK_AGENT_KEY only and needs the content-edit\nand content-publish grants. Each page prints created, updated, unchanged,\nskipped or failed, so a re-run is safe. When every page is Live, the site\nswitches apps/web and apps/api to CONTENT_ADAPTER=database, keeping a value\neither already sets. The site's dependencies must be installed. Exit 0 when\nevery page was seeded, 1 when any failed, 2 on a refusal (nothing is sent).\n";
|
|
71
73
|
export declare function parseSeedWorkspaceArgs(argv: string[]): SeedWorkspaceArgs;
|
package/dist/seed-workspace.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* A fresh scaffold's `content/` tree is exactly its roster (spec §1), so the
|
|
7
7
|
* seed is `importMdx` over `<dir>/content` with the site's own installed
|
|
8
8
|
* adapter (D1): one `text/page` Artifact per page, and a re-run writes
|
|
9
|
-
* nothing and reports every page `unchanged`.
|
|
9
|
+
* nothing and reports every page `unchanged`. Each page with no Live version
|
|
10
|
+
* is set Live, and when every page is Live both apps that choose a content
|
|
11
|
+
* source are switched to `CONTENT_ADAPTER=database`, each keeping a value it
|
|
12
|
+
* already sets (#10659 D3); anything less leaves the site on `content/`.
|
|
10
13
|
*
|
|
11
14
|
* The API origin comes from `--api-url`, else `AILK_API_URL`; the workspace
|
|
12
15
|
* id from `--workspace`, else `AILK_WORKSPACE_ID`; the agent key from
|
|
@@ -18,12 +21,14 @@
|
|
|
18
21
|
*
|
|
19
22
|
* The scaffold-time step (D3, D6) runs only on a hub with an Admin
|
|
20
23
|
* (`workspace` or `multi-tenant`), after `--install`, with all three
|
|
21
|
-
* variables set; otherwise it prints why and the command that seeds
|
|
22
|
-
* or nothing on a site with no Admin. It never fails the
|
|
24
|
+
* variables set; otherwise it prints why and the command that seeds and
|
|
25
|
+
* switches later, or nothing on a site with no Admin. It never fails the
|
|
26
|
+
* scaffold.
|
|
23
27
|
*
|
|
24
28
|
* Pure module: no `import.meta.url`, so jest can import it directly. `fetch`,
|
|
25
29
|
* `sleep`, `env` and the adapter are injectable. Spec:
|
|
26
|
-
* docs/specs/10204-seed-the-starter-s-roster-spec.md
|
|
30
|
+
* docs/specs/10204-seed-the-starter-s-roster-spec.md; the publish and the
|
|
31
|
+
* switch docs/specs/10659-a-licensed-hub-scaffold-starts-spec.md.
|
|
27
32
|
*/
|
|
28
33
|
import { join, resolve } from "node:path";
|
|
29
34
|
import { parseArgs } from "node:util";
|
|
@@ -31,11 +36,13 @@ import { AddModuleRefusedError } from "./add-module-core.js";
|
|
|
31
36
|
import { ImportMdxRefusedError, formatImportMdxReport, importMdx, loadSiteMdxAdapter, } from "./lib/import-mdx.js";
|
|
32
37
|
import { ParseError } from "./parse-args.js";
|
|
33
38
|
import { readSiteRoster } from "./site-roster.js";
|
|
34
|
-
import { HUB_ORIGIN_ENV_VAR, TopologyError, normalizeHubUrl, readSiteTopology, } from "./topology.js";
|
|
39
|
+
import { HUB_ORIGIN_ENV_VAR, TopologyError, normalizeHubUrl, readSiteTopology, writeApiEnv, writeWebEnv, } from "./topology.js";
|
|
35
40
|
/** The environment variable the agent key is read from — and nowhere else. */
|
|
36
41
|
export const AGENT_KEY_ENV_VAR = "AILK_AGENT_KEY";
|
|
37
42
|
/** The environment variable the workspace id is read from when no flag names one. */
|
|
38
43
|
export const WORKSPACE_ID_ENV_VAR = "AILK_WORKSPACE_ID";
|
|
44
|
+
/** The environment variable each app chooses its content source by (#10659). */
|
|
45
|
+
export const CONTENT_ADAPTER_ENV_VAR = "CONTENT_ADAPTER";
|
|
39
46
|
/** A refusal the CLI exits 2 on, thrown before any request is sent. */
|
|
40
47
|
export class SeedWorkspaceRefusedError extends Error {
|
|
41
48
|
exitCode = 2;
|
|
@@ -83,7 +90,7 @@ export async function seedWorkspace(opts) {
|
|
|
83
90
|
}
|
|
84
91
|
const apiKey = given(env[AGENT_KEY_ENV_VAR]);
|
|
85
92
|
if (apiKey === undefined) {
|
|
86
|
-
refuse(`set ${AGENT_KEY_ENV_VAR} to an agent key with the content-edit
|
|
93
|
+
refuse(`set ${AGENT_KEY_ENV_VAR} to an agent key with the content-edit and content-publish grants. It is read from the environment only — never a flag or a file.`);
|
|
87
94
|
}
|
|
88
95
|
const workspaceId = given(opts.workspaceId) ?? given(env[WORKSPACE_ID_ENV_VAR]);
|
|
89
96
|
if (workspaceId === undefined) {
|
|
@@ -115,12 +122,30 @@ export async function seedWorkspace(opts) {
|
|
|
115
122
|
...(opts.fetch !== undefined && { fetch: opts.fetch }),
|
|
116
123
|
...(opts.sleep !== undefined && { sleep: opts.sleep }),
|
|
117
124
|
onLine,
|
|
125
|
+
publish: true,
|
|
118
126
|
});
|
|
119
127
|
// The report's count line alone: no page rows.
|
|
120
128
|
onLine(formatImportMdxReport({ pages: [], counts: result.counts }).trimEnd());
|
|
121
|
-
const { failed } = result.counts;
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
const { created, updated, unchanged, failed } = result.counts;
|
|
130
|
+
const live = result.live ?? 0;
|
|
131
|
+
onLine(`Live: ${live} page(s), ${result.published ?? 0} published now.`);
|
|
132
|
+
// #10659 D3 — both apps switch only when every page is in and Live.
|
|
133
|
+
if (failed === 0 && live >= 1 && live === created + updated + unchanged) {
|
|
134
|
+
for (const [app, file, write] of [
|
|
135
|
+
["apps/web", "apps/web/.env.local", writeWebEnv],
|
|
136
|
+
["apps/api", "apps/api/.env", writeApiEnv],
|
|
137
|
+
]) {
|
|
138
|
+
onLine(write(targetDir, CONTENT_ADAPTER_ENV_VAR, () => "database")
|
|
139
|
+
? `Content source: the database for ${app} (${CONTENT_ADAPTER_ENV_VAR}=database in ${file}).`
|
|
140
|
+
: `Content source: ${app} already sets ${CONTENT_ADAPTER_ENV_VAR}, which was kept.`);
|
|
141
|
+
}
|
|
142
|
+
onLine(" The API serves these pages from the database it was seeded through (DATABASE_URL), for the workspace its site resolves.");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (failed > 0) {
|
|
146
|
+
onLine(`${failed} page(s) were not seeded. Fix them and re-run: create-ailk seed-workspace ${opts.targetDir}`);
|
|
147
|
+
}
|
|
148
|
+
onLine("Content source: content/ (unchanged) until every page is seeded and Live.");
|
|
124
149
|
}
|
|
125
150
|
return { result, exitCode: failed > 0 ? 1 : 0 };
|
|
126
151
|
}
|
|
@@ -166,9 +191,10 @@ export async function runScaffoldSeed(opts) {
|
|
|
166
191
|
const plan = scaffoldSeedPlan(opts);
|
|
167
192
|
if (plan.kind === "not-applicable")
|
|
168
193
|
return;
|
|
194
|
+
const later = ` To seed it and switch to the database later: ${AGENT_KEY_ENV_VAR}=<key> create-ailk seed-workspace ${opts.targetDir} --api-url <origin> --workspace <wksp_id>`;
|
|
169
195
|
if (plan.kind === "skip") {
|
|
170
196
|
opts.onLine(plan.reason);
|
|
171
|
-
opts.onLine(
|
|
197
|
+
opts.onLine(later);
|
|
172
198
|
return;
|
|
173
199
|
}
|
|
174
200
|
opts.onLine("Seeding the workspace:");
|
|
@@ -188,6 +214,7 @@ export async function runScaffoldSeed(opts) {
|
|
|
188
214
|
throw err;
|
|
189
215
|
}
|
|
190
216
|
opts.onLine(err.message);
|
|
217
|
+
opts.onLine(later);
|
|
191
218
|
}
|
|
192
219
|
}
|
|
193
220
|
export const SEED_WORKSPACE_HELP = `create-ailk seed-workspace — import a starter's pages into its workspace.
|
|
@@ -205,12 +232,14 @@ Options:
|
|
|
205
232
|
AILK_WORKSPACE_ID.
|
|
206
233
|
-h, --help Show this help.
|
|
207
234
|
|
|
208
|
-
Each content/en/<Type>/<slug>.mdx page is written to the workspace
|
|
209
|
-
|
|
210
|
-
from AILK_AGENT_KEY only and needs the content-edit
|
|
211
|
-
created, updated, unchanged,
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
Each content/en/<Type>/<slug>.mdx page is written to the workspace, and set
|
|
236
|
+
Live when it has no Live version, through the API's content routes, with the
|
|
237
|
+
agent key, which is read from AILK_AGENT_KEY only and needs the content-edit
|
|
238
|
+
and content-publish grants. Each page prints created, updated, unchanged,
|
|
239
|
+
skipped or failed, so a re-run is safe. When every page is Live, the site
|
|
240
|
+
switches apps/web and apps/api to CONTENT_ADAPTER=database, keeping a value
|
|
241
|
+
either already sets. The site's dependencies must be installed. Exit 0 when
|
|
242
|
+
every page was seeded, 1 when any failed, 2 on a refusal (nothing is sent).
|
|
214
243
|
`;
|
|
215
244
|
export function parseSeedWorkspaceArgs(argv) {
|
|
216
245
|
let parsed;
|