create-ailk 0.23.0 → 0.24.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 +27 -14
- package/dist/add-module.js +6 -1
- package/dist/add-route.js +7 -1
- package/dist/derive-architecture.js +17 -0
- package/dist/lib/scaffold-render-sweep.d.ts +33 -0
- package/dist/lib/scaffold-render-sweep.js +45 -0
- package/dist/module-architecture.js +17 -0
- package/dist/sync-routes.d.ts +30 -4
- package/dist/sync-routes.js +271 -18
- package/package.json +3 -3
- package/templates/.env.example +4 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/apps/api/.env.example +7 -0
- package/templates/apps/api/CLAUDE.md +25 -11
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +4 -1
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +3 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +333 -0
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +18 -11
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +126 -0
- package/templates/apps/api/src/routes/content/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- package/templates/apps/api/src/routes/content/search.ts +16 -45
- package/templates/apps/api/src/routes/leads/index.ts +364 -298
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +1 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +2 -0
- package/templates/apps/api/src/server.ts +25 -0
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +10 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +28 -0
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +6 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +2 -7
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +53 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +107 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +4 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +4 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +10 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +23 -5
- package/templates/apps/api/src/services/offerings/connection.ts +83 -3
- package/templates/apps/api/src/services/offerings/index.ts +6 -6
- package/templates/apps/api/src/services/person.ts +23 -0
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/mcp/CLAUDE.md +11 -5
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +91 -10
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- package/templates/apps/mcp/src/tools/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/web/CLAUDE.md +12 -3
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +21 -4
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
- package/templates/apps/web/lib/magnet-routes.ts +3 -1
- package/templates/apps/web/lib/newsletter.actions.ts +9 -6
- package/templates/database/CHANGELOG.md +20 -0
- package/templates/database/__tests__/seed.test.ts +33 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +27 -0
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
package/dist/sync-routes.js
CHANGED
|
@@ -36,14 +36,13 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
38
38
|
import { tmpdir } from "node:os";
|
|
39
|
-
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
39
|
+
import { basename, dirname, join, posix, resolve, sep } from "node:path";
|
|
40
40
|
import { parseArgs } from "node:util";
|
|
41
|
-
import { copyTemplate } from "./copy.js";
|
|
41
|
+
import { copyTemplate, renderTemplateFile } from "./copy.js";
|
|
42
42
|
import { SPOKE_BILLING_ACTION_PATH } from "./derive-architecture.js";
|
|
43
43
|
import { appendEnvRows } from "./lib/declared-env.js";
|
|
44
44
|
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
45
45
|
import { BILLING_ACTION, BILLING_ACTION_MARKER, RETIRED_CHECKOUT_E2E_MARKER, RETIRED_CHECKOUT_E2E_WORKFLOW_PATH, RETIRED_SPOKE_CHECKOUT_PATHS, } from "./spoke-checkout.js";
|
|
46
|
-
import { substituteMarkers } from "./substitute.js";
|
|
47
46
|
import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
|
|
48
47
|
import { TEMPLATE_RECORD_PATH, parseTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
|
|
49
48
|
import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, } from "./topology.js";
|
|
@@ -70,6 +69,171 @@ const NESTED_FOREIGN_MODULE_ENTRIES = new Set([
|
|
|
70
69
|
"careers/apply",
|
|
71
70
|
"careers/[slug]",
|
|
72
71
|
]);
|
|
72
|
+
/** Extensions whose files are scanned for import specifiers (#9406 DD1). */
|
|
73
|
+
const SOURCE_FILE = /\.(?:ts|tsx|js|jsx|mjs|mts)$/;
|
|
74
|
+
/**
|
|
75
|
+
* Static import and re-export (`import … from`, `export … from`), side-effect
|
|
76
|
+
* (`import "…"`) and dynamic (`import("…")`) specifiers (#9406 DD1). Matched
|
|
77
|
+
* over the whole text: `[^'"]*?` spans newlines, so a multi-line
|
|
78
|
+
* `import {\n a,\n} from "…"` is caught from its `import` line.
|
|
79
|
+
*/
|
|
80
|
+
const IMPORT_PATTERNS = [
|
|
81
|
+
/^\s*(?:import|export)\b[^'"]*?\bfrom\s*['"]([^'"]+)['"]/gm,
|
|
82
|
+
/^\s*import\s*['"]([^'"]+)['"]/gm,
|
|
83
|
+
/\bimport\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
84
|
+
];
|
|
85
|
+
/** The site-local (`./`, `../`) import specifiers of a source text, deduplicated. */
|
|
86
|
+
function relativeImportSpecifiers(text) {
|
|
87
|
+
const out = new Set();
|
|
88
|
+
for (const pattern of IMPORT_PATTERNS) {
|
|
89
|
+
for (const match of text.matchAll(pattern)) {
|
|
90
|
+
const specifier = match[1];
|
|
91
|
+
if (specifier !== undefined && (specifier.startsWith("./") || specifier.startsWith("../"))) {
|
|
92
|
+
out.add(specifier);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...out];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The scaffold-relative files `specifier`, imported from `importer`, may name —
|
|
100
|
+
* in #9406 DD2's probe order: a `.js`/`.jsx` specifier's `.ts`/`.tsx` spelling
|
|
101
|
+
* first, then the exact path, the extension probes and the directory index.
|
|
102
|
+
* Empty when the path climbs out of the scaffold root.
|
|
103
|
+
*/
|
|
104
|
+
function importCandidates(importer, specifier) {
|
|
105
|
+
const base = posix.normalize(posix.join(posix.dirname(importer), specifier));
|
|
106
|
+
if (base === ".." || base.startsWith("../"))
|
|
107
|
+
return [];
|
|
108
|
+
const out = [];
|
|
109
|
+
const js = /\.(jsx?)$/.exec(base);
|
|
110
|
+
if (js !== null) {
|
|
111
|
+
const stem = base.slice(0, -js[0].length);
|
|
112
|
+
if (js[1] === "js")
|
|
113
|
+
out.push(`${stem}.ts`);
|
|
114
|
+
out.push(`${stem}.tsx`);
|
|
115
|
+
}
|
|
116
|
+
out.push(base);
|
|
117
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".mjs"])
|
|
118
|
+
out.push(base + ext);
|
|
119
|
+
out.push(`${base}/index.ts`, `${base}/index.tsx`);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** Whether `root`/`rel` (`/`-separated) is an existing regular file. */
|
|
123
|
+
function isRegularFile(root, rel) {
|
|
124
|
+
try {
|
|
125
|
+
return statSync(join(root, ...rel.split("/"))).isFile();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The deepest route in `missingRoutes` a scaffold-relative path sits under —
|
|
133
|
+
* the route whose own copy writes it — or undefined.
|
|
134
|
+
*/
|
|
135
|
+
function ownerRoute(path, missingRoutes) {
|
|
136
|
+
if (!path.startsWith(`${ROUTE_ROOT}/`))
|
|
137
|
+
return undefined;
|
|
138
|
+
const rel = path.slice(ROUTE_ROOT.length + 1);
|
|
139
|
+
let owner;
|
|
140
|
+
for (const m of missingRoutes) {
|
|
141
|
+
if (rel.startsWith(`${m}/`) && (owner === undefined || m.length > owner.length))
|
|
142
|
+
owner = m;
|
|
143
|
+
}
|
|
144
|
+
return owner;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the site-local imports of every file `route`'s own copy writes, and
|
|
148
|
+
* transitively of every helper that brings (#9406 DD1–DD3). Each import is
|
|
149
|
+
* resolved against the site first, then the template. A file the site has
|
|
150
|
+
* ends the walk there (the site owns its closure); a file another missing
|
|
151
|
+
* route's copy brings is recorded for DD4's propagation; a file only the
|
|
152
|
+
* template has is a copy — unless the spoke drops it or its surface is not
|
|
153
|
+
* carried, which refuses the route.
|
|
154
|
+
*/
|
|
155
|
+
function walkRouteImports(route, ctx) {
|
|
156
|
+
const inScratch = (path) => path.startsWith(`${ROUTE_ROOT}/`) ? path.slice(ROUTE_ROOT.length + 1) : undefined;
|
|
157
|
+
const templateHas = (path) => {
|
|
158
|
+
const rel = inScratch(path);
|
|
159
|
+
return rel !== undefined
|
|
160
|
+
? isRegularFile(ctx.scratch, rel)
|
|
161
|
+
: isRegularFile(ctx.templatesDir, path);
|
|
162
|
+
};
|
|
163
|
+
// The bytes `copyTemplate` (a route file) or `copyDependencyFile` (any
|
|
164
|
+
// other) would write for `path`.
|
|
165
|
+
const templateBytes = (path) => {
|
|
166
|
+
const rel = inScratch(path);
|
|
167
|
+
if (rel !== undefined)
|
|
168
|
+
return readFileSync(join(ctx.scratch, ...rel.split("/")));
|
|
169
|
+
return Buffer.from(renderTemplateFile(readFileSync(join(ctx.templatesDir, ...path.split("/"))), path, ctx.versions, ctx.markers));
|
|
170
|
+
};
|
|
171
|
+
const copies = new Set();
|
|
172
|
+
const differs = new Set();
|
|
173
|
+
const unresolved = [];
|
|
174
|
+
const reached = [];
|
|
175
|
+
const queue = regularFilesUnder(ctx.scratch, route)
|
|
176
|
+
.map((rel) => `${ROUTE_ROOT}/${rel}`)
|
|
177
|
+
.filter((path) => ownerRoute(path, ctx.missingRoutes) === route);
|
|
178
|
+
const visited = new Set(queue);
|
|
179
|
+
for (let from = queue.shift(); from !== undefined; from = queue.shift()) {
|
|
180
|
+
if (!SOURCE_FILE.test(from))
|
|
181
|
+
continue;
|
|
182
|
+
const importer = from;
|
|
183
|
+
for (const specifier of relativeImportSpecifiers(templateBytes(importer).toString("utf8"))) {
|
|
184
|
+
const candidates = importCandidates(importer, specifier);
|
|
185
|
+
const inSite = candidates.find((c) => isRegularFile(ctx.targetAbs, c));
|
|
186
|
+
const path = inSite ?? candidates.find(templateHas);
|
|
187
|
+
if (path === undefined) {
|
|
188
|
+
unresolved.push({ specifier, from: importer, reason: "not in the template" });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const owner = ownerRoute(path, ctx.missingRoutes);
|
|
192
|
+
if (owner !== undefined) {
|
|
193
|
+
if (owner !== route)
|
|
194
|
+
reached.push({ path, owner, specifier, from: importer });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (inSite !== undefined) {
|
|
198
|
+
const siteBytes = readFileSync(join(ctx.targetAbs, ...path.split("/")));
|
|
199
|
+
if (templateHas(path) && !templateBytes(path).equals(siteBytes))
|
|
200
|
+
differs.add(path);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (ctx.spoke !== undefined && isSpokeExcluded(path, ctx.spoke)) {
|
|
204
|
+
unresolved.push({ specifier, from: importer, reason: "the spoke does not carry it" });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const routeRel = inScratch(path);
|
|
208
|
+
const surface = routeRel === undefined ? "core" : surfaceForRouteDir(routeRel);
|
|
209
|
+
if (surface === undefined || !ctx.carried.has(surface)) {
|
|
210
|
+
unresolved.push({
|
|
211
|
+
specifier,
|
|
212
|
+
from: importer,
|
|
213
|
+
reason: `its surface (${surface ?? "none"}) is not carried by this site`,
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
copies.add(path);
|
|
218
|
+
if (!visited.has(path)) {
|
|
219
|
+
visited.add(path);
|
|
220
|
+
queue.push(path);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
route,
|
|
226
|
+
copies: [...copies].sort(),
|
|
227
|
+
differs: [...differs].sort(),
|
|
228
|
+
unresolved: sortUnresolved(unresolved),
|
|
229
|
+
reached,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** `unresolved` rows sorted by `from`, then `specifier`. */
|
|
233
|
+
function sortUnresolved(rows) {
|
|
234
|
+
const key = (row) => `${row.from}\u0000${row.specifier}`;
|
|
235
|
+
return [...rows].sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
|
|
236
|
+
}
|
|
73
237
|
/**
|
|
74
238
|
* The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
|
|
75
239
|
* brace scan that skips string literals and comments. Empty when the
|
|
@@ -462,6 +626,7 @@ export function syncRoutes(opts) {
|
|
|
462
626
|
const targetRouteRoot = join(targetAbs, ROUTE_ROOT);
|
|
463
627
|
const missing = [];
|
|
464
628
|
const modified = [];
|
|
629
|
+
let walks = [];
|
|
465
630
|
const scratch = mkdtempSync(join(tmpdir(), "ailk-sync-routes-"));
|
|
466
631
|
try {
|
|
467
632
|
copyTemplate({
|
|
@@ -479,18 +644,73 @@ export function syncRoutes(opts) {
|
|
|
479
644
|
modified.push(report);
|
|
480
645
|
}
|
|
481
646
|
}
|
|
647
|
+
// #9406 — resolve each missing route's site-local imports while the
|
|
648
|
+
// scratch render is still on disk.
|
|
649
|
+
walks = missing.map((r) => walkRouteImports(r.route, {
|
|
650
|
+
scratch,
|
|
651
|
+
templatesDir: opts.templatesDir,
|
|
652
|
+
targetAbs,
|
|
653
|
+
missingRoutes: missing.map((m) => m.route),
|
|
654
|
+
versions: opts.versions,
|
|
655
|
+
markers,
|
|
656
|
+
spoke,
|
|
657
|
+
carried: new Set(["core", "authed", ...carried]),
|
|
658
|
+
}));
|
|
482
659
|
}
|
|
483
660
|
finally {
|
|
484
661
|
rmSync(scratch, { recursive: true, force: true });
|
|
485
662
|
}
|
|
663
|
+
// #9406 DD4 — a route refuses when an import resolves nowhere; refusal
|
|
664
|
+
// inherits to its sub-routes and to any route importing a file only a
|
|
665
|
+
// refused route's copy would write, iterated to a fixpoint. Never upward:
|
|
666
|
+
// an ancestor is still copied without the refused sub-tree.
|
|
667
|
+
const missingRoutes = missing.map((r) => r.route);
|
|
668
|
+
const refusedSet = new Set(walks.filter((w) => w.unresolved.length > 0).map((w) => w.route));
|
|
669
|
+
for (let changed = true; changed;) {
|
|
670
|
+
changed = false;
|
|
671
|
+
for (const walk of walks) {
|
|
672
|
+
if (refusedSet.has(walk.route))
|
|
673
|
+
continue;
|
|
674
|
+
if (missingRoutes.some((a) => refusedSet.has(a) && walk.route.startsWith(`${a}/`))) {
|
|
675
|
+
refusedSet.add(walk.route);
|
|
676
|
+
changed = true;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const dangling = walk.reached.filter((r) => refusedSet.has(r.owner));
|
|
680
|
+
if (dangling.length > 0) {
|
|
681
|
+
walk.unresolved = sortUnresolved([
|
|
682
|
+
...walk.unresolved,
|
|
683
|
+
...dangling.map(({ path, owner, specifier, from }) => ({
|
|
684
|
+
specifier,
|
|
685
|
+
from,
|
|
686
|
+
reason: `${path} is brought by ${owner}, which is refused`,
|
|
687
|
+
})),
|
|
688
|
+
]);
|
|
689
|
+
refusedSet.add(walk.route);
|
|
690
|
+
changed = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const refused = missingRoutes.filter((r) => refusedSet.has(r)).sort();
|
|
695
|
+
const routeImports = walks
|
|
696
|
+
.filter((w) => w.copies.length + w.differs.length + w.unresolved.length > 0)
|
|
697
|
+
.map(({ route, copies, differs, unresolved }) => ({ route, copies, differs, unresolved }))
|
|
698
|
+
.sort((a, b) => (a.route < b.route ? -1 : a.route > b.route ? 1 : 0));
|
|
486
699
|
// #5014 third clause: a route's ROUTE_DEPENDENCIES files must exist in the
|
|
487
700
|
// target too, or `--apply` leaves a route that cannot compile. Computed
|
|
488
701
|
// from `missing` only — a route already present is assumed to already have
|
|
489
702
|
// brought its own dependencies along (or the site owner has otherwise
|
|
490
703
|
// satisfied them); re-checking every IN-SCOPE route's dependencies on every
|
|
491
704
|
// run would report a pre-existing, unrelated gap as if this sync caused it.
|
|
705
|
+
// #9406 — unioned with the copies each route's imports resolve to, and
|
|
706
|
+
// never for a refused route: its helpers are neither required nor copied.
|
|
492
707
|
const missingDependencies = [
|
|
493
|
-
...new Set(missing
|
|
708
|
+
...new Set(missing
|
|
709
|
+
.filter((r) => !refusedSet.has(r.route))
|
|
710
|
+
.flatMap((r) => [
|
|
711
|
+
...r.dependencies.filter((dep) => !existsSync(join(targetAbs, dep))),
|
|
712
|
+
...(walks.find((w) => w.route === r.route)?.copies ?? []),
|
|
713
|
+
])),
|
|
494
714
|
].sort();
|
|
495
715
|
const missingMessageNamespaces = [
|
|
496
716
|
...new Set(missing.flatMap((r) => r.messageNamespaces.filter((ns) => !hasMessageNamespace(targetAbs, ns)))),
|
|
@@ -498,11 +718,11 @@ export function syncRoutes(opts) {
|
|
|
498
718
|
const applied = [];
|
|
499
719
|
const appliedDependencies = [];
|
|
500
720
|
if (opts.apply === true) {
|
|
501
|
-
const
|
|
502
|
-
for (const route of
|
|
721
|
+
const writable = missingRoutes.filter((r) => !refusedSet.has(r));
|
|
722
|
+
for (const route of writable) {
|
|
503
723
|
applied.push(route);
|
|
504
724
|
// An ancestor route dir's subtree copy already brings this one.
|
|
505
|
-
if (
|
|
725
|
+
if (writable.some((m) => route.startsWith(m + "/")))
|
|
506
726
|
continue;
|
|
507
727
|
const dst = resolve(join(targetRouteRoot, route));
|
|
508
728
|
if (!dst.startsWith(targetAbs + sep)) {
|
|
@@ -513,6 +733,10 @@ export function syncRoutes(opts) {
|
|
|
513
733
|
targetDir: dst,
|
|
514
734
|
versions: opts.versions,
|
|
515
735
|
markers,
|
|
736
|
+
// #9406 DD4 — hold back each refused sub-route from the ancestor's copy.
|
|
737
|
+
excludePaths: refused
|
|
738
|
+
.filter((r) => r.startsWith(route + "/"))
|
|
739
|
+
.map((r) => r.slice(route.length + 1)),
|
|
516
740
|
});
|
|
517
741
|
}
|
|
518
742
|
for (const dep of missingDependencies) {
|
|
@@ -520,7 +744,7 @@ export function syncRoutes(opts) {
|
|
|
520
744
|
if (!dst.startsWith(targetAbs + sep)) {
|
|
521
745
|
throw new Error(`refusing to write outside target dir: ${dst}`);
|
|
522
746
|
}
|
|
523
|
-
copyDependencyFile(
|
|
747
|
+
copyDependencyFile(opts.templatesDir, dep, dst, opts.versions, markers);
|
|
524
748
|
appliedDependencies.push(dep);
|
|
525
749
|
}
|
|
526
750
|
// #7874 D9 — a spoke declares what the copied routes read, as the last
|
|
@@ -536,6 +760,8 @@ export function syncRoutes(opts) {
|
|
|
536
760
|
missing,
|
|
537
761
|
modified,
|
|
538
762
|
applied,
|
|
763
|
+
refused,
|
|
764
|
+
routeImports,
|
|
539
765
|
missingDependencies,
|
|
540
766
|
appliedDependencies,
|
|
541
767
|
missingMessageNamespaces,
|
|
@@ -572,19 +798,20 @@ function hasMessageNamespace(targetAbs, namespace) {
|
|
|
572
798
|
}
|
|
573
799
|
}
|
|
574
800
|
/**
|
|
575
|
-
* Copy one dependency file from the template into
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
* safe to call on
|
|
801
|
+
* Copy one dependency file (`rel`, template-relative) from the template into
|
|
802
|
+
* the target through `renderTemplateFile` — the renderer `copyTemplate` uses,
|
|
803
|
+
* so a text file gets its markers and a non-text asset an import reached
|
|
804
|
+
* (`./logo.png`, #9406) is copied byte-for-byte. Never overwrites: the caller
|
|
805
|
+
* (`syncRoutes`) only calls this for a path already confirmed absent from the
|
|
806
|
+
* target, but the check is repeated here so this helper is safe to call on
|
|
807
|
+
* its own.
|
|
581
808
|
*/
|
|
582
|
-
function copyDependencyFile(
|
|
809
|
+
function copyDependencyFile(templatesDir, rel, dst, versions, markers) {
|
|
583
810
|
if (existsSync(dst))
|
|
584
811
|
return;
|
|
585
812
|
mkdirSync(dirname(dst), { recursive: true });
|
|
586
|
-
const
|
|
587
|
-
writeFileSync(dst,
|
|
813
|
+
const bytes = readFileSync(join(templatesDir, ...rel.split("/")));
|
|
814
|
+
writeFileSync(dst, renderTemplateFile(bytes, rel, versions, markers));
|
|
588
815
|
}
|
|
589
816
|
/** Render the report the CLI prints after a run. Ends with a newline. */
|
|
590
817
|
export function formatSyncRoutesReport(result, apply) {
|
|
@@ -598,7 +825,7 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
598
825
|
}
|
|
599
826
|
else if (apply) {
|
|
600
827
|
lines.push(`[create-ailk] Copied ${result.applied.length} route(s):`);
|
|
601
|
-
lines.push(...result.missing.map(entry));
|
|
828
|
+
lines.push(...result.missing.filter((r) => result.applied.includes(r.route)).map(entry));
|
|
602
829
|
}
|
|
603
830
|
else {
|
|
604
831
|
lines.push(`[create-ailk] ${result.missing.length} template route(s) missing:`);
|
|
@@ -611,6 +838,32 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
611
838
|
if (result.skippedSurfaces.length > 0) {
|
|
612
839
|
lines.push(`[create-ailk] Surfaces this site does not carry (skipped): ${result.skippedSurfaces.join(", ")}`);
|
|
613
840
|
}
|
|
841
|
+
// #9406 DD5 — per missing route, what its imports copy, leave, or cannot
|
|
842
|
+
// resolve; then the routes a dangling import holds back.
|
|
843
|
+
if (result.routeImports.length > 0) {
|
|
844
|
+
lines.push("[create-ailk] Local modules the missing route(s) import:");
|
|
845
|
+
for (const { route, copies, differs, unresolved } of result.routeImports) {
|
|
846
|
+
lines.push(` ${route}`);
|
|
847
|
+
if (copies.length > 0) {
|
|
848
|
+
lines.push(" copies:");
|
|
849
|
+
lines.push(...copies.map((path) => ` ${path}`));
|
|
850
|
+
}
|
|
851
|
+
if (differs.length > 0) {
|
|
852
|
+
lines.push(" left as-is (differs from the template):");
|
|
853
|
+
lines.push(...differs.map((path) => ` ${path}`));
|
|
854
|
+
}
|
|
855
|
+
if (unresolved.length > 0) {
|
|
856
|
+
lines.push(" cannot resolve:");
|
|
857
|
+
lines.push(...unresolved.map(({ specifier, from, reason }) => ` ${specifier} (from ${from}): ${reason}`));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (result.refused.length > 0) {
|
|
862
|
+
lines.push(apply
|
|
863
|
+
? `[create-ailk] ${result.refused.length} route(s) not copied — an import does not resolve:`
|
|
864
|
+
: `[create-ailk] ${result.refused.length} route(s) would not be copied — an import does not resolve:`);
|
|
865
|
+
lines.push(...result.refused.map((route) => ` ${route}`));
|
|
866
|
+
}
|
|
614
867
|
// #5014 third clause: the routes/messages/lib a synced route needs to
|
|
615
868
|
// compile, reported (and, for lib files, applied) alongside the route.
|
|
616
869
|
if (result.missingDependencies.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tar": "^7.5.21"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@working-theory/schema": "0.
|
|
32
|
-
"@working-theory/templates": "0.
|
|
31
|
+
"@working-theory/schema": "0.45.0",
|
|
32
|
+
"@working-theory/templates": "0.45.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
package/templates/.env.example
CHANGED
|
@@ -446,6 +446,10 @@ CONTENT_ADAPTER=mdx
|
|
|
446
446
|
# that database. Rotation and the rekey bin: apps/api/.env.example.
|
|
447
447
|
# STRIPE_CONNECTION_KEY=
|
|
448
448
|
# STRIPE_CONNECTION_KEY_PREVIOUS=
|
|
449
|
+
# The Organization that owns STRIPE_SECRET_KEY (#9285): with the multi-tenant
|
|
450
|
+
# module only the Organization whose id is named here sells through it; unset
|
|
451
|
+
# means nobody. Without the module the single Organization owns it.
|
|
452
|
+
# STRIPE_ENV_ORGANIZATION_ID=
|
|
449
453
|
# HMAC-SHA256 key POST /v1/webhooks/calcom verifies the x-cal-signature-256
|
|
450
454
|
# header against (#6085). Unset makes the receiver answer 503 and accept
|
|
451
455
|
# nothing — the same fail-closed posture an unset STRIPE_WEBHOOK_SECRET has.
|
package/templates/CLAUDE.md
CHANGED
|
@@ -25,8 +25,10 @@ content/ Site settings (_site.mdx) and MDX pages (en/<TypeName>/)
|
|
|
25
25
|
tools/ This repo's own checks (architecture, CLAUDE.md drift)
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
You consume `@working-theory/*` packages from npm
|
|
29
|
-
|
|
28
|
+
You consume `@working-theory/*` packages from npm: read a README or types,
|
|
29
|
+
not the source. Build on one by composing, wrapping or configuring it via
|
|
30
|
+
its documented extension points. Never copy it out of `node_modules`, fork,
|
|
31
|
+
patch or pin it: a copy stops taking fixes. `pnpm architecture` refuses each.
|
|
30
32
|
<!-- /ailk:template id="repo-shape" -->
|
|
31
33
|
|
|
32
34
|
<!-- ailk:template id="architecture" -->
|
|
@@ -125,6 +125,13 @@ STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
|
125
125
|
# STRIPE_CONNECTION_KEY=
|
|
126
126
|
# STRIPE_CONNECTION_KEY_PREVIOUS=
|
|
127
127
|
#
|
|
128
|
+
# The Organization that owns STRIPE_SECRET_KEY (#9285). On a deployment with
|
|
129
|
+
# the multi-tenant module, only the Organization whose id is named here sees
|
|
130
|
+
# that key as its Stripe connection and sells its Offerings through it; unset,
|
|
131
|
+
# blank or naming no Organization, nobody owns it. Without the multi-tenant
|
|
132
|
+
# module the deployment's single Organization owns it and this is not read.
|
|
133
|
+
# STRIPE_ENV_ORGANIZATION_ID=
|
|
134
|
+
#
|
|
128
135
|
# The booking webhook receiver (#6085) — a SEPARATE credential from the
|
|
129
136
|
# three above: it verifies POST /v1/webhooks/calcom's x-cal-signature-256
|
|
130
137
|
# header, it does not call the provider. Unset makes the receiver answer 503
|