create-ailk 0.14.0 → 0.15.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/component-catalog.json +3 -3
- package/dist/add-module.d.ts +7 -0
- package/dist/add-module.js +34 -2
- package/dist/add-route.js +18 -2
- package/dist/apply-design.d.ts +12 -1
- package/dist/apply-design.js +36 -4
- package/dist/cli.js +23 -3
- package/dist/derive-architecture.d.ts +15 -2
- package/dist/derive-architecture.js +34 -6
- package/dist/design-package.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/chrome-readers.d.ts +21 -0
- package/dist/lib/chrome-readers.js +100 -0
- package/dist/lib/extract-module-bundle.d.ts +11 -0
- package/dist/lib/extract-module-bundle.js +36 -6
- package/dist/lib/license-key-files.d.ts +6 -1
- package/dist/lib/license-key-files.js +9 -4
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +6 -0
- package/dist/parse-args.d.ts +22 -0
- package/dist/parse-args.js +73 -0
- package/dist/programmatic.d.ts +20 -0
- package/dist/programmatic.js +110 -9
- package/dist/site-chrome.d.ts +4 -0
- package/dist/site-chrome.js +34 -3
- package/dist/site-manifest.d.ts +12 -5
- package/dist/site-manifest.js +25 -11
- package/dist/sync-routes.js +11 -2
- package/dist/topology.d.ts +131 -0
- package/dist/topology.js +533 -0
- package/package.json +3 -3
- package/site-templates/designs/minimalism/DESIGN.md +3 -3
- package/templates/apps/api/CLAUDE.md +4 -2
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +10 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
- package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
- package/templates/apps/api/src/lib/agent-store.ts +95 -3
- package/templates/apps/api/src/lib/site.ts +37 -0
- package/templates/apps/api/src/lib/spoke-store.ts +18 -2
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
- package/templates/apps/api/src/middleware/auth.ts +3 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
- package/templates/apps/api/src/openapi/spec.ts +38 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
- package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +275 -0
- package/templates/apps/api/src/routes/leads/index.ts +69 -6
- package/templates/apps/api/src/routes/webhooks/README.md +39 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +61 -1
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +199 -0
- package/templates/apps/api/src/services/default-tenant.ts +41 -7
- package/templates/apps/api/src/services/lead-routing.ts +148 -29
- package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
- package/templates/apps/mcp/__tests__/index.test.ts +5 -5
- package/templates/apps/mcp/src/index.ts +6 -4
- package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
- package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
- package/templates/apps/web/CLAUDE.md +1 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +105 -11
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +12 -8
- package/templates/apps/web/app/[locale]/layout.tsx +71 -19
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +89 -1
- package/templates/apps/web/app/openapi.json/route.ts +69 -1
- package/templates/apps/web/instrumentation.ts +7 -4
- package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +290 -0
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
- package/templates/apps/web/lib/closing-footer-path.ts +50 -0
- package/templates/apps/web/lib/relay.ts +206 -2
- package/templates/apps/web/lib/site-brand.tsx +22 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -0
- package/templates/apps/web/lib/topology.generated.ts +23 -0
- package/templates/apps/web/package.json +1 -1
- package/templates/database/CHANGELOG.md +25 -0
- package/templates/database/__tests__/agents.db.test.ts +65 -0
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
- package/templates/database/auth/schema.prisma +23 -0
- package/templates/database/inbox/schema.prisma +13 -0
- package/templates/database/jest.config.cjs +7 -11
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
- package/templates/database/package.json +1 -1
- package/versions.json +34 -33
package/dist/site-chrome.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export interface TemplateLayout {
|
|
|
76
76
|
colorMode?: SiteChromeColorMode;
|
|
77
77
|
/** #7742 D1 — `minimal`: the minimal closing slide, consent bar and themed 404. */
|
|
78
78
|
chromeStyle?: SiteChromeChromeStyle;
|
|
79
|
+
/** #7797 D1 — route paths whose page ends on the closing footer (wordmark, contact lines, legal line). */
|
|
80
|
+
closingFooterPaths?: readonly string[];
|
|
79
81
|
}
|
|
80
82
|
/** `TemplateLayout` with every field resolved — what `site-chrome.ts` exports. */
|
|
81
83
|
export interface ResolvedSiteChrome {
|
|
@@ -93,6 +95,8 @@ export interface ResolvedSiteChrome {
|
|
|
93
95
|
colorMode?: "light";
|
|
94
96
|
/** Present only when it resolves to "minimal" — never "default" (#7742 D1). */
|
|
95
97
|
chromeStyle?: "minimal";
|
|
98
|
+
/** Present only when a non-empty list resolves — never `[]` (#7797 D1). */
|
|
99
|
+
closingFooterPaths?: readonly string[];
|
|
96
100
|
}
|
|
97
101
|
/** Defaults (#5612 D1; banner off since #6469). */
|
|
98
102
|
export declare const DEFAULT_SITE_CHROME: ResolvedSiteChrome;
|
package/dist/site-chrome.js
CHANGED
|
@@ -66,6 +66,14 @@ const HOME_VALUES = ["default", "deck"];
|
|
|
66
66
|
const INNER_PAGES_VALUES = ["default", "bare"];
|
|
67
67
|
const COLOR_MODE_VALUES = ["user", "light"];
|
|
68
68
|
const CHROME_STYLE_VALUES = ["default", "minimal"];
|
|
69
|
+
/**
|
|
70
|
+
* A `closingFooterPaths` member (#7797): a route path as the site writes it —
|
|
71
|
+
* a leading "/" and no query, fragment, whitespace or control character. The
|
|
72
|
+
* layout normalizes the visitor's pathname (locale prefix, trailing slash)
|
|
73
|
+
* before comparing, so "/about" and "/about/" name the same page.
|
|
74
|
+
*/
|
|
75
|
+
// eslint-disable-next-line no-control-regex -- the class deliberately refuses control characters
|
|
76
|
+
const CLOSING_FOOTER_PATH = /^\/[^\s?#\u0000-\u001f]*$/;
|
|
69
77
|
/**
|
|
70
78
|
* Validate an UNTRUSTED `layout` value (a bundle's `ailk-template.json`
|
|
71
79
|
* `layout` field — arbitrary JSON, never compile-time checked the way the
|
|
@@ -144,6 +152,16 @@ export function validateTemplateLayout(value, subject) {
|
|
|
144
152
|
}
|
|
145
153
|
result.chromeStyle = raw.chromeStyle;
|
|
146
154
|
}
|
|
155
|
+
if ("closingFooterPaths" in raw && raw.closingFooterPaths !== undefined) {
|
|
156
|
+
const paths = raw.closingFooterPaths;
|
|
157
|
+
const badIndex = Array.isArray(paths)
|
|
158
|
+
? paths.findIndex((p) => typeof p !== "string" || !CLOSING_FOOTER_PATH.test(p))
|
|
159
|
+
: -1;
|
|
160
|
+
if (!Array.isArray(paths) || paths.length === 0 || badIndex !== -1) {
|
|
161
|
+
throw new TemplateLayoutError(`${subject}: layout.closingFooterPaths must be a non-empty list of route paths ("/about") with no query or fragment — got ${JSON.stringify(badIndex !== -1 ? paths[badIndex] : paths)}`);
|
|
162
|
+
}
|
|
163
|
+
result.closingFooterPaths = paths;
|
|
164
|
+
}
|
|
147
165
|
return result;
|
|
148
166
|
}
|
|
149
167
|
/**
|
|
@@ -195,6 +213,12 @@ export function resolveSiteChrome(layout, designLayout) {
|
|
|
195
213
|
if ((layout?.chromeStyle ?? fromDesign.chromeStyle) === "minimal") {
|
|
196
214
|
resolved.chromeStyle = "minimal";
|
|
197
215
|
}
|
|
216
|
+
// #7797 D1 — the list resolves as a whole (an explicit layout wins over the
|
|
217
|
+
// design's), and only a non-empty list is ever a key.
|
|
218
|
+
const closingFooterPaths = layout?.closingFooterPaths ?? fromDesign.closingFooterPaths;
|
|
219
|
+
if (closingFooterPaths !== undefined && closingFooterPaths.length > 0) {
|
|
220
|
+
resolved.closingFooterPaths = closingFooterPaths;
|
|
221
|
+
}
|
|
198
222
|
return resolved;
|
|
199
223
|
}
|
|
200
224
|
/**
|
|
@@ -225,6 +249,10 @@ export function renderSiteChrome(resolved) {
|
|
|
225
249
|
// only for the opt-in values.
|
|
226
250
|
const colorModeValue = resolved.colorMode === "light" ? '\n colorMode: "light",' : "";
|
|
227
251
|
const chromeStyleValue = resolved.chromeStyle === "minimal" ? '\n chromeStyle: "minimal",' : "";
|
|
252
|
+
// #7797 D1 — the same form: member and bullet always, the value line only for a non-empty list.
|
|
253
|
+
const closingFooterPathsValue = resolved.closingFooterPaths === undefined
|
|
254
|
+
? ""
|
|
255
|
+
: `\n closingFooterPaths: [${resolved.closingFooterPaths.map((p) => JSON.stringify(p)).join(", ")}],`;
|
|
228
256
|
return `// site-chrome.ts — resolved template layout knobs.
|
|
229
257
|
//
|
|
230
258
|
// GENERATED by create-ailk from the declared template's \`layout\` block
|
|
@@ -247,7 +275,9 @@ export function renderSiteChrome(resolved) {
|
|
|
247
275
|
// switch; dark appears only as a deck ground.
|
|
248
276
|
// - chromeStyle: "minimal" → the deck's closing slide is the wordmark,
|
|
249
277
|
// contact lines and one legal line; the consent bar is a hairline bar on
|
|
250
|
-
// the page ground; the 404 renders in the site's theme
|
|
278
|
+
// the page ground; the 404 renders in the site's theme.
|
|
279
|
+
// - closingFooterPaths → each listed inner page ends on the closing footer
|
|
280
|
+
// (the wordmark, contact lines and legal line) instead of its usual footer.${groundsComment}
|
|
251
281
|
|
|
252
282
|
export interface SiteChrome {
|
|
253
283
|
nav: "full" | "mark-only";
|
|
@@ -257,14 +287,15 @@ export interface SiteChrome {
|
|
|
257
287
|
innerPages?: "bare";
|
|
258
288
|
deckPresentation?: true;
|
|
259
289
|
colorMode?: "light";
|
|
260
|
-
chromeStyle?: "minimal"
|
|
290
|
+
chromeStyle?: "minimal";
|
|
291
|
+
closingFooterPaths?: readonly string[];${groundsMember}
|
|
261
292
|
}
|
|
262
293
|
|
|
263
294
|
export const siteChrome: SiteChrome = {
|
|
264
295
|
nav: ${JSON.stringify(resolved.nav)},
|
|
265
296
|
footer: ${JSON.stringify(resolved.footer)},
|
|
266
297
|
announcementBanner: ${JSON.stringify(resolved.announcementBanner)},
|
|
267
|
-
home: ${JSON.stringify(resolved.home)},${groundsValue}${innerPagesValue}${presentationValue}${colorModeValue}${chromeStyleValue}
|
|
298
|
+
home: ${JSON.stringify(resolved.home)},${groundsValue}${innerPagesValue}${presentationValue}${colorModeValue}${chromeStyleValue}${closingFooterPathsValue}
|
|
268
299
|
};
|
|
269
300
|
`;
|
|
270
301
|
}
|
package/dist/site-manifest.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Level } from "./lib/module-tier.js";
|
|
|
2
2
|
import type { ModuleName } from "./parse-args.js";
|
|
3
3
|
import { type SiteRoster } from "./site-roster.js";
|
|
4
4
|
import type { MarkerMap } from "./substitute.js";
|
|
5
|
+
import type { Topology } from "./topology.js";
|
|
5
6
|
export interface PlanSiteManifestOptions {
|
|
6
7
|
targetDir: string;
|
|
7
8
|
/** The installed manifest, read after every layer landed. */
|
|
@@ -10,16 +11,22 @@ export interface PlanSiteManifestOptions {
|
|
|
10
11
|
modules: readonly ModuleName[];
|
|
11
12
|
/** The level the key path derived, or undefined when it derived none. */
|
|
12
13
|
level: Level | undefined;
|
|
14
|
+
/** The scaffold's topology (#7426 D10). */
|
|
15
|
+
topology: Topology;
|
|
16
|
+
/** The create-ailk version that wrote the scaffold (`CREATE_AILK_VERSION`). */
|
|
17
|
+
patternVersion: string;
|
|
13
18
|
siteTemplatesDir: string;
|
|
14
19
|
templatesDir: string;
|
|
15
20
|
markers: MarkerMap;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
|
-
* The manifest's next text: `modules` (and `level`, when derived)
|
|
19
|
-
* `requires`, and a row for every
|
|
23
|
+
* The manifest's next text: `modules` (and `level`, when derived), then
|
|
24
|
+
* `topology` and `patternVersion`, after `requires`, and a row for every
|
|
25
|
+
* route present on disk.
|
|
20
26
|
*
|
|
21
|
-
* @throws AddModuleRefusedError when the file already records `modules
|
|
22
|
-
* `level`, or its layout is not the
|
|
23
|
-
* at the top level) — guessing would
|
|
27
|
+
* @throws AddModuleRefusedError when the file already records `modules`,
|
|
28
|
+
* `level`, `topology` or `patternVersion`, or its layout is not the
|
|
29
|
+
* generator's (`rows` last, `requires` at the top level) — guessing would
|
|
30
|
+
* silently mis-record the site.
|
|
24
31
|
*/
|
|
25
32
|
export declare function planSiteManifest(opts: PlanSiteManifestOptions): string;
|
package/dist/site-manifest.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* (`resolveModuleSet` / `deriveLevel`). Never inferred from `modules`:
|
|
16
16
|
* the level comes only from the key. An alias or `--modules` keep run
|
|
17
17
|
* derives none and records none;
|
|
18
|
+
* - `topology` and `patternVersion` (#7426 D10) — `"hub"` or `"spoke"`,
|
|
19
|
+
* and the create-ailk version that wrote the files, on every scaffold.
|
|
20
|
+
* A site with no `topology` predates the option and reads as a hub
|
|
21
|
+
* (`topology.ts` `readSiteTopology`);
|
|
18
22
|
* - a row for every route present on disk — each module's contributed rows
|
|
19
23
|
* through `planRosterAppend` (add-module's own append), the sample pages
|
|
20
24
|
* a module's bundle installs (`sampleRows`), and a code-route row for any
|
|
@@ -51,16 +55,18 @@ const CODE_ROUTE_ROW = {
|
|
|
51
55
|
sections: [],
|
|
52
56
|
};
|
|
53
57
|
/**
|
|
54
|
-
* The manifest's next text: `modules` (and `level`, when derived)
|
|
55
|
-
* `requires`, and a row for every
|
|
58
|
+
* The manifest's next text: `modules` (and `level`, when derived), then
|
|
59
|
+
* `topology` and `patternVersion`, after `requires`, and a row for every
|
|
60
|
+
* route present on disk.
|
|
56
61
|
*
|
|
57
|
-
* @throws AddModuleRefusedError when the file already records `modules
|
|
58
|
-
* `level`, or its layout is not the
|
|
59
|
-
* at the top level) — guessing would
|
|
62
|
+
* @throws AddModuleRefusedError when the file already records `modules`,
|
|
63
|
+
* `level`, `topology` or `patternVersion`, or its layout is not the
|
|
64
|
+
* generator's (`rows` last, `requires` at the top level) — guessing would
|
|
65
|
+
* silently mis-record the site.
|
|
60
66
|
*/
|
|
61
67
|
export function planSiteManifest(opts) {
|
|
62
68
|
const { roster } = opts;
|
|
63
|
-
for (const key of ["modules", "level"]) {
|
|
69
|
+
for (const key of ["modules", "level", "topology", "patternVersion"]) {
|
|
64
70
|
if (key in roster.doc) {
|
|
65
71
|
throw new AddModuleRefusedError(`${roster.path} already records "${key}"`);
|
|
66
72
|
}
|
|
@@ -81,7 +87,7 @@ export function planSiteManifest(opts) {
|
|
|
81
87
|
...codeRouteRows(opts.targetDir, declared),
|
|
82
88
|
];
|
|
83
89
|
const withRows = appendRows(appended, extra, roster);
|
|
84
|
-
return insertRecord(withRows, opts
|
|
90
|
+
return insertRecord(withRows, opts, roster.path);
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
93
|
* The rows for the sample pages a module's bundle installs through its
|
|
@@ -233,8 +239,12 @@ function appendRows(text, rows, roster) {
|
|
|
233
239
|
? text.slice(0, end) + rendered + roster.trail + text.slice(close)
|
|
234
240
|
: text.slice(0, end) + "," + rendered + text.slice(end);
|
|
235
241
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
242
|
+
/**
|
|
243
|
+
* `text` with `modules` (and `level`, when derived), `topology` and
|
|
244
|
+
* `patternVersion` inserted after the top-level `requires`.
|
|
245
|
+
*/
|
|
246
|
+
function insertRecord(text, opts, label) {
|
|
247
|
+
const { modules, level, topology, patternVersion } = opts;
|
|
238
248
|
const anchor = '\n "requires": ';
|
|
239
249
|
const at = text.indexOf(anchor);
|
|
240
250
|
if (at === -1 || text.indexOf(anchor, at + 1) !== -1) {
|
|
@@ -243,11 +253,15 @@ function insertRecord(text, modules, level, label) {
|
|
|
243
253
|
const end = text.indexOf("]", at) + 1;
|
|
244
254
|
const name = '"modules": ';
|
|
245
255
|
const record = `,\n ${name}${renderJson([...modules], 2, name.length, 1)}` +
|
|
246
|
-
(level !== undefined ? `,\n "level": ${JSON.stringify(level)}` : "")
|
|
256
|
+
(level !== undefined ? `,\n "level": ${JSON.stringify(level)}` : "") +
|
|
257
|
+
`,\n "topology": ${JSON.stringify(topology)}` +
|
|
258
|
+
`,\n "patternVersion": ${JSON.stringify(patternVersion)}`;
|
|
247
259
|
const next = text.slice(0, end) + record + text.slice(end);
|
|
248
260
|
const doc = JSON.parse(next);
|
|
249
261
|
if (!isDeepStrictEqual(doc["modules"], [...modules]) ||
|
|
250
|
-
doc["level"] !== level
|
|
262
|
+
doc["level"] !== level ||
|
|
263
|
+
doc["topology"] !== topology ||
|
|
264
|
+
doc["patternVersion"] !== patternVersion) {
|
|
251
265
|
throw new AddModuleRefusedError(`${label}: the module set could not be recorded beside "requires"`);
|
|
252
266
|
}
|
|
253
267
|
return next;
|
package/dist/sync-routes.js
CHANGED
|
@@ -33,6 +33,7 @@ import { parseArgs } from "node:util";
|
|
|
33
33
|
import { copyTemplate } from "./copy.js";
|
|
34
34
|
import { substituteMarkers } from "./substitute.js";
|
|
35
35
|
import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
|
|
36
|
+
import { isSpokeExcluded, readSiteTopology } from "./topology.js";
|
|
36
37
|
/** Template-relative prefix every `apps/web` route directory sits under. */
|
|
37
38
|
const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
38
39
|
/**
|
|
@@ -163,12 +164,20 @@ export function syncRoutes(opts) {
|
|
|
163
164
|
skippedSurfaces.push(surface.id);
|
|
164
165
|
}
|
|
165
166
|
skippedSurfaces.sort();
|
|
167
|
+
// #7426 D13 — on a spoke, a route dir the spoke does not carry (the
|
|
168
|
+
// `(auth)` and `(authed)` groups) is out of scope: neither reported nor
|
|
169
|
+
// copied. This is the one filter point for add-module, add-route and the
|
|
170
|
+
// sync-routes subcommand alike; a hub's scope is unchanged.
|
|
171
|
+
const spoke = readSiteTopology(targetAbs) === "spoke"
|
|
172
|
+
? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
|
|
173
|
+
: undefined;
|
|
166
174
|
const inScope = listTemplateRouteDirs(opts.templatesDir)
|
|
167
175
|
.map(reportFor)
|
|
168
176
|
// "authed" (#5014): the `(authed)` route group ships unconditionally in
|
|
169
|
-
// every scaffold
|
|
177
|
+
// every hub scaffold, the same as "core" — it is checked regardless of
|
|
170
178
|
// `carried`, which only tracks GATEABLE_SURFACES opt-in.
|
|
171
|
-
.filter((r) => r.surface === "core" || r.surface === "authed" || carried.has(r.surface))
|
|
179
|
+
.filter((r) => r.surface === "core" || r.surface === "authed" || carried.has(r.surface))
|
|
180
|
+
.filter((r) => spoke === undefined || !isSpokeExcluded(`${ROUTE_ROOT}/${r.route}`, spoke));
|
|
172
181
|
// Identical to scaffoldSync's defaults, so a synced route matches a fresh
|
|
173
182
|
// scaffold of the same target byte-for-byte.
|
|
174
183
|
const markers = {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { type ModuleName } from "./parse-args.js";
|
|
2
|
+
/** The two topologies. `hub` is the default and the only one before #7426. */
|
|
3
|
+
export declare const TOPOLOGIES: readonly ["hub", "spoke"];
|
|
4
|
+
export type Topology = (typeof TOPOLOGIES)[number];
|
|
5
|
+
/** A refusal or a malformed input this module names. */
|
|
6
|
+
export declare class TopologyError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/** Roots no spoke carries. `apps/mcp` joins them with `--without-mcp`. */
|
|
10
|
+
export declare const SPOKE_REMOVED_ROOTS: readonly string[];
|
|
11
|
+
/** The root `--without-mcp` removes. */
|
|
12
|
+
export declare const SPOKE_MCP_ROOT = "apps/mcp";
|
|
13
|
+
/**
|
|
14
|
+
* The sign-in and database files a spoke does not carry — files and
|
|
15
|
+
* directories, scaffold-relative. Every other file that imports one of them
|
|
16
|
+
* goes with it (`spokeExclusions`).
|
|
17
|
+
*/
|
|
18
|
+
export declare const SPOKE_REMOVAL_SEEDS: readonly string[];
|
|
19
|
+
/** The two generated seam files (D5). A seam is rewritten, never removed. */
|
|
20
|
+
export declare const WEB_SEAM_PATH = "apps/web/lib/topology.generated.ts";
|
|
21
|
+
export declare const MCP_SEAM_PATH = "apps/mcp/src/lib/topology.generated.ts";
|
|
22
|
+
export declare const SPOKE_SEAM_FILES: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* The closure's allow-list: a file `spokeExclusions` removes must sit under
|
|
25
|
+
* one of these prefixes, or under a `__tests__/` directory. A member outside
|
|
26
|
+
* both is a defect — a shared file started importing a sign-in or database
|
|
27
|
+
* file — and `spokeExclusions` throws naming it rather than cut it silently.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SPOKE_REMOVABLE_PREFIXES: readonly string[];
|
|
30
|
+
export interface SpokeShapeOptions {
|
|
31
|
+
/** Whether the spoke keeps `apps/mcp` (the default; `--without-mcp` drops it). */
|
|
32
|
+
mcp: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** The removed roots for these options. */
|
|
35
|
+
export declare function spokeRemovedRoots(opts: SpokeShapeOptions): string[];
|
|
36
|
+
/** Whether a closure member is one the registry allows a spoke to drop. */
|
|
37
|
+
export declare function isRemovableClosureMember(path: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Every path a spoke does not carry, computed over the built `templates/`
|
|
40
|
+
* tree: the removed roots, the seeds that apply, and every source file under
|
|
41
|
+
* the kept apps that reaches a seed through relative imports, to a fixed
|
|
42
|
+
* point. Sorted; directories and files alike, matched at a directory
|
|
43
|
+
* boundary by `isSpokeExcluded` and `copyTemplate`'s `excludePaths`.
|
|
44
|
+
*
|
|
45
|
+
* The walk is the relative-import resolution `importClosure` uses
|
|
46
|
+
* (`lib/import-graph.ts`), with ONE difference: a seam file is never a
|
|
47
|
+
* candidate, so the walk never crosses it. The hub render of a seam imports
|
|
48
|
+
* a seed by design (D5); its importers keep their place on a spoke because
|
|
49
|
+
* the spoke render of the same file imports none.
|
|
50
|
+
*
|
|
51
|
+
* @throws TopologyError naming every closure member outside
|
|
52
|
+
* `SPOKE_REMOVABLE_PREFIXES` and not a test — a new shared file importing
|
|
53
|
+
* a sign-in or database file, which removing would silently break the
|
|
54
|
+
* spoke (spec § 5.3: escalate, never widen).
|
|
55
|
+
*/
|
|
56
|
+
export declare function spokeExclusions(templatesDir: string, opts: SpokeShapeOptions): string[];
|
|
57
|
+
export interface IsSpokeExcludedOptions extends SpokeShapeOptions {
|
|
58
|
+
/**
|
|
59
|
+
* The full `spokeExclusions(...)` list, when the caller computed it. Absent,
|
|
60
|
+
* only the removed roots and the seeds are checked — exact for any path
|
|
61
|
+
* that is not a file of the built `templates/` tree (a module bundle's
|
|
62
|
+
* file, a route directory), since only a template file can be a closure
|
|
63
|
+
* member.
|
|
64
|
+
*/
|
|
65
|
+
exclusions?: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The one predicate for "a spoke does not carry this path" (the Phase 2
|
|
69
|
+
* generator contract). Scaffold-relative, `/`- or `\`-separated.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isSpokeExcluded(path: string, opts: IsSpokeExcludedOptions): boolean;
|
|
72
|
+
/** `apps/web/lib/topology.generated.ts` for `topology` (D5). */
|
|
73
|
+
export declare function renderWebSeam(topology: Topology): string;
|
|
74
|
+
/** `apps/mcp/src/lib/topology.generated.ts` for `topology` (D5). */
|
|
75
|
+
export declare function renderMcpSeam(topology: Topology): string;
|
|
76
|
+
/** `pnpm-workspace.yaml` without its `database` package entry. */
|
|
77
|
+
export declare function transformWorkspaceYaml(text: string): string;
|
|
78
|
+
/** The root `package.json` without its `db:*` scripts. */
|
|
79
|
+
export declare function transformRootPackageJson(text: string): string;
|
|
80
|
+
/** The root `tsconfig.json` without a reference under a removed root. */
|
|
81
|
+
export declare function transformRootTsconfig(text: string, opts: SpokeShapeOptions): string;
|
|
82
|
+
/**
|
|
83
|
+
* An app's `package.json` without its `@working-theory/database` dependency.
|
|
84
|
+
* @throws TopologyError when `required` and the dependency is absent.
|
|
85
|
+
*/
|
|
86
|
+
export declare function transformAppPackageJson(text: string, label: string, required: boolean): string;
|
|
87
|
+
/**
|
|
88
|
+
* Apply every D6 root-file transform to the spoke at `targetDir` (after the
|
|
89
|
+
* base copy, before any module or template layer), and write the spoke
|
|
90
|
+
* render of each seam it carries (D5).
|
|
91
|
+
* @throws TopologyError naming the file whose anchor is missing.
|
|
92
|
+
*/
|
|
93
|
+
export declare function applySpokeTransforms(targetDir: string, opts: SpokeShapeOptions): void;
|
|
94
|
+
/** The env var a spoke reaches its hub through. */
|
|
95
|
+
export declare const HUB_ORIGIN_ENV_VAR = "AILK_API_URL";
|
|
96
|
+
/**
|
|
97
|
+
* Write `AILK_API_URL=<origin>` to the spoke's `apps/web/.env.local` (D2) —
|
|
98
|
+
* the `auth-secret-file.ts` never-clobber rule: an assignment already in any
|
|
99
|
+
* env file Next loads for `apps/web` is kept, and `.env.local` stays
|
|
100
|
+
* gitignored. Never writes `AILK_SPOKE_KEY` or `AILK_VISITOR_FORWARD_KEY`:
|
|
101
|
+
* the hub issues those.
|
|
102
|
+
*/
|
|
103
|
+
export declare function writeHubOriginEnv(targetDir: string, origin: string): void;
|
|
104
|
+
/** The `hard_floors:` block a spoke's `project.yaml` carries (D9). */
|
|
105
|
+
export declare function renderSpokeHardFloors(opts: SpokeShapeOptions): string;
|
|
106
|
+
/** `project.yaml` with its `hard_floors:` block replaced by the spoke's (D9). */
|
|
107
|
+
export declare function transformProjectYaml(text: string, opts: SpokeShapeOptions): string;
|
|
108
|
+
/** The modules a spoke may carry — every slice declaring `spoke: true`. */
|
|
109
|
+
export declare function spokeModules(): ModuleName[];
|
|
110
|
+
/** The refusal for a module a spoke cannot carry, or undefined when admitted. */
|
|
111
|
+
export declare function spokeModuleRefusal(name: ModuleName): string | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* The topology `<targetDir>` records. A missing manifest, or one without a
|
|
114
|
+
* `topology` key, reads as `"hub"` — every site scaffolded before #7426.
|
|
115
|
+
* @throws TopologyError on an unreadable manifest or an unknown value.
|
|
116
|
+
*/
|
|
117
|
+
export declare function readSiteTopology(targetDir: string): Topology;
|
|
118
|
+
/**
|
|
119
|
+
* Hosts a spoke may reach over plain `http:` — the set
|
|
120
|
+
* `packages/api-client/src/http.ts` sends the spoke key under. Named here
|
|
121
|
+
* rather than imported: create-ailk carries no runtime dependency.
|
|
122
|
+
*/
|
|
123
|
+
export declare const LOOPBACK_HOSTS: readonly string[];
|
|
124
|
+
/**
|
|
125
|
+
* The hub origin for a spoke: `--hub-url`, else `AILK_API_URL`. An `https:`
|
|
126
|
+
* URL, or `http:` on a loopback host, with no path but `/`, no query and no
|
|
127
|
+
* fragment — normalized to its origin with no trailing slash.
|
|
128
|
+
* @throws TopologyError naming both inputs when neither is set or the value
|
|
129
|
+
* is not such an origin. Never echoes a query or fragment.
|
|
130
|
+
*/
|
|
131
|
+
export declare function normalizeHubUrl(raw: string | undefined): string;
|