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.
Files changed (96) hide show
  1. package/README.md +27 -14
  2. package/dist/add-module.js +6 -1
  3. package/dist/add-route.js +7 -1
  4. package/dist/derive-architecture.js +17 -0
  5. package/dist/lib/scaffold-render-sweep.d.ts +33 -0
  6. package/dist/lib/scaffold-render-sweep.js +45 -0
  7. package/dist/module-architecture.js +17 -0
  8. package/dist/sync-routes.d.ts +30 -4
  9. package/dist/sync-routes.js +271 -18
  10. package/package.json +3 -3
  11. package/templates/.env.example +4 -0
  12. package/templates/CLAUDE.md +4 -2
  13. package/templates/apps/api/.env.example +7 -0
  14. package/templates/apps/api/CLAUDE.md +25 -11
  15. package/templates/apps/api/src/lib/__mocks__/prisma.ts +4 -1
  16. package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
  17. package/templates/apps/api/src/lib/site.ts +4 -1
  18. package/templates/apps/api/src/lib/stripe.ts +3 -0
  19. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
  20. package/templates/apps/api/src/openapi/spec.ts +333 -0
  21. package/templates/apps/api/src/routes/billing/plan-checkout.ts +18 -11
  22. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +126 -0
  23. package/templates/apps/api/src/routes/content/get-page.ts +34 -119
  24. package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
  25. package/templates/apps/api/src/routes/content/search.ts +16 -45
  26. package/templates/apps/api/src/routes/leads/index.ts +364 -298
  27. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
  28. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
  29. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
  30. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
  31. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +1 -0
  32. package/templates/apps/api/src/routes/webhooks/stripe.ts +2 -0
  33. package/templates/apps/api/src/server.ts +25 -0
  34. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
  35. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +10 -0
  36. package/templates/apps/api/src/services/__tests__/person.test.ts +28 -0
  37. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +6 -0
  38. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
  39. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
  40. package/templates/apps/api/src/services/abandon-email.ts +28 -5
  41. package/templates/apps/api/src/services/deliverable-fulfillment.ts +2 -7
  42. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +53 -0
  43. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +107 -0
  44. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +4 -0
  45. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +4 -0
  46. package/templates/apps/api/src/services/offerings/catalog.ts +10 -0
  47. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +23 -5
  48. package/templates/apps/api/src/services/offerings/connection.ts +83 -3
  49. package/templates/apps/api/src/services/offerings/index.ts +6 -6
  50. package/templates/apps/api/src/services/person.ts +23 -0
  51. package/templates/apps/api/src/services/project-listings.ts +43 -31
  52. package/templates/apps/mcp/CLAUDE.md +11 -5
  53. package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
  54. package/templates/apps/mcp/__tests__/http-transport.test.ts +91 -10
  55. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
  56. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
  57. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
  58. package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
  59. package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
  60. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
  61. package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
  62. package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
  63. package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
  64. package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
  65. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
  66. package/templates/apps/mcp/package.json +1 -0
  67. package/templates/apps/mcp/src/abuse/index.ts +18 -11
  68. package/templates/apps/mcp/src/http-transport.ts +47 -9
  69. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
  70. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
  71. package/templates/apps/mcp/src/lib/site-config.ts +53 -0
  72. package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
  73. package/templates/apps/mcp/src/tools/index.ts +22 -21
  74. package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
  75. package/templates/apps/mcp/src/tools/read_content.ts +46 -40
  76. package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
  77. package/templates/apps/mcp/src/tools/search_content.ts +20 -24
  78. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
  79. package/templates/apps/web/CLAUDE.md +12 -3
  80. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  81. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
  82. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  83. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +21 -4
  84. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
  85. package/templates/apps/web/lib/magnet-routes.ts +3 -1
  86. package/templates/apps/web/lib/newsletter.actions.ts +9 -6
  87. package/templates/database/CHANGELOG.md +20 -0
  88. package/templates/database/__tests__/seed.test.ts +33 -1
  89. package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
  90. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
  91. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
  92. package/templates/database/package.json +1 -1
  93. package/templates/database/seed.ts +27 -0
  94. package/templates/tools/lint-architecture.ts +205 -1
  95. package/versions.json +34 -34
  96. package/templates/apps/api/src/routes/content/slug.ts +0 -9
@@ -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.flatMap((r) => r.dependencies.filter((dep) => !existsSync(join(targetAbs, dep))))),
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 missingRoutes = missing.map((r) => r.route);
502
- for (const route of missingRoutes) {
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 (missingRoutes.some((m) => route.startsWith(m + "/")))
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(join(opts.templatesDir, dep), dst, markers);
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 the target, applying the
576
- * same marker substitution `copyTemplate` applies to text files — every
577
- * `ROUTE_DEPENDENCIES` entry is `.ts`/`.tsx`, always text. Never overwrites:
578
- * the caller (`syncRoutes`) only calls this for a path already confirmed
579
- * absent from the target, but the check is repeated here so this helper is
580
- * safe to call on its own.
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(src, dst, markers) {
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 text = readFileSync(src, "utf8");
587
- writeFileSync(dst, substituteMarkers(text, markers));
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.23.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.44.0",
32
- "@working-theory/templates": "0.44.0"
31
+ "@working-theory/schema": "0.45.0",
32
+ "@working-theory/templates": "0.45.0"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",
@@ -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.
@@ -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. Treat each one as a
29
- third-party dependency: read its README or its types, not its source.
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