create-ailk 0.8.0 → 0.9.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 +68 -0
- package/component-catalog.json +279 -45
- package/dist/add-module.d.ts +20 -0
- package/dist/add-module.js +43 -7
- package/dist/add-route.d.ts +56 -0
- package/dist/add-route.js +286 -0
- package/dist/apply-design-md.d.ts +7 -0
- package/dist/apply-design-md.js +50 -0
- package/dist/apply-design.d.ts +64 -0
- package/dist/apply-design.js +273 -0
- package/dist/claude-md.d.ts +111 -0
- package/dist/claude-md.js +334 -0
- package/dist/cli.js +92 -4
- package/dist/derive-architecture.js +41 -36
- package/dist/design-compile.d.ts +71 -0
- package/dist/design-compile.js +98 -0
- package/dist/design-package.d.ts +83 -0
- package/dist/design-package.js +468 -0
- package/dist/design-presets.d.ts +86 -0
- package/dist/design-presets.js +642 -0
- package/dist/design-scaffold.d.ts +76 -0
- package/dist/design-scaffold.js +220 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/legacy-nav-renderers.d.ts +45 -0
- package/dist/legacy-nav-renderers.js +545 -0
- package/dist/lib/auth-secret-file.d.ts +19 -0
- package/dist/lib/auth-secret-file.js +112 -0
- package/dist/lib/design-checks.d.ts +78 -0
- package/dist/lib/design-checks.js +174 -0
- package/dist/lib/license-key-files.d.ts +9 -0
- package/dist/lib/license-key-files.js +2 -2
- package/dist/lib/module-license-gate.d.ts +19 -9
- package/dist/lib/module-license-gate.js +19 -8
- package/dist/lib/render-verdict.d.ts +52 -0
- package/dist/lib/render-verdict.js +77 -0
- package/dist/lib/scaffold-render-sweep.d.ts +105 -0
- package/dist/lib/scaffold-render-sweep.js +236 -0
- package/dist/module-architecture.d.ts +19 -16
- package/dist/module-architecture.js +64 -21
- package/dist/parse-args.d.ts +12 -2
- package/dist/parse-args.js +74 -13
- package/dist/programmatic.d.ts +30 -6
- package/dist/programmatic.js +206 -17
- package/dist/site-chrome.d.ts +19 -9
- package/dist/site-chrome.js +23 -13
- package/dist/site-manifest.d.ts +25 -0
- package/dist/site-manifest.js +254 -0
- package/dist/site-roster.d.ts +41 -0
- package/dist/site-roster.js +141 -8
- package/dist/starter-grid.d.ts +32 -16
- package/dist/starter-grid.js +129 -33
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +200 -83
- package/dist/template-manifest.d.ts +30 -13
- package/dist/template-manifest.js +49 -24
- package/package.json +5 -4
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
- package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/designs/standard/DESIGN.md +98 -6
- package/site-templates/marketing-site/DESIGN.md +98 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
- package/site-templates/marketing-site-free/DESIGN.md +98 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/saas-product/DESIGN.md +98 -6
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
- package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
- package/site-templates/shapes/organization-service/ailk-template.json +1 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
- package/site-templates/shapes/person-service/ailk-template.json +1 -1
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
- package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
- package/templates/.claude/rules/architecture.md +7 -0
- package/templates/.claude/skills/README.md +3 -2
- package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +85 -76
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
- package/templates/.claude/skills/check-site/SKILL.md +23 -0
- package/templates/.claude/skills/fill-page/SKILL.md +26 -7
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +28 -11
- package/templates/.claude/skills/update-page/SKILL.md +117 -0
- package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
- package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +98 -6
- package/templates/README.md +50 -38
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
- package/templates/apps/api/src/__tests__/server.test.ts +116 -0
- package/templates/apps/api/src/lib/stripe.ts +9 -0
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
- package/templates/apps/api/src/middleware/auth.ts +54 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
- package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
- package/templates/apps/api/src/server.ts +28 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
- package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
- package/templates/apps/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/purchase.ts +4 -2
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/CLAUDE.md +22 -28
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
- package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +35 -19
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
- package/templates/apps/web/app/api/revalidate/route.ts +78 -36
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
- package/templates/apps/web/lib/locale-guard.ts +18 -0
- package/templates/apps/web/lib/meet-placeholder.ts +5 -0
- package/templates/apps/web/lib/nav-config.ts +14 -13
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-logo.tsx +36 -0
- package/templates/apps/web/lib/site-theme.ts +46 -26
- package/templates/apps/web/messages/en.json +23 -9
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/en/BookCall/book-a-call.mdx +80 -0
- package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
- package/templates/content/en/BookCall/onsite-service.mdx +56 -0
- package/templates/content/en/BookCall/start-project.mdx +56 -0
- package/templates/content/en/ContactPage/contact.mdx +40 -1
- package/templates/content/en/HomePage/home.mdx +2 -6
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +2 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
- package/site-templates/minimalism-site/ailk-template.json +0 -23
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
- package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffold's root `CLAUDE.md` as marked, self-hashing regions (#6904).
|
|
3
|
+
*
|
|
4
|
+
* A rendered `CLAUDE.md` is a run of template-owned regions, each opened by
|
|
5
|
+
* `<!-- ailk:template id="<id>" sha256="<hex>" -->` and closed by
|
|
6
|
+
* `<!-- /ailk:template id="<id>" -->`, followed by one `<!-- ailk:site -->`
|
|
7
|
+
* line that opens the site-owned section running to end of file (spec D1).
|
|
8
|
+
* Each begin marker records the SHA-256 of its own body (D2), so the drift
|
|
9
|
+
* check needs only the file.
|
|
10
|
+
*
|
|
11
|
+
* This is the core `--sync-claude-md` (`syncClaudeMdSync`, programmatic.ts)
|
|
12
|
+
* and the tests use. The scaffold's own drift check,
|
|
13
|
+
* `template-overlay/tools/check-claude-md.ts`, reimplements `findDrift` +
|
|
14
|
+
* `formatDrift` because a scaffold does not carry this source; the
|
|
15
|
+
* `claude-md-sync` suite holds the two to identical verdicts.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { basename, join, resolve } from "node:path";
|
|
20
|
+
import { buildScaffoldMarkers } from "./copy.js";
|
|
21
|
+
import { substituteMarkers } from "./substitute.js";
|
|
22
|
+
/** The drift check a scaffold ships (`template-overlay/tools/`). */
|
|
23
|
+
export const CLAUDE_MD_CHECK_PATH = "tools/check-claude-md.ts";
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 of every `templates/CLAUDE.md` a published create-ailk shipped
|
|
26
|
+
* before markers existed, with `{{PROJECT_NAME}}` unsubstituted (spec § 1.7,
|
|
27
|
+
* D4). An unmarked scaffold `CLAUDE.md` that hashes to one of these after
|
|
28
|
+
* undoing the name substitution was never edited by its owner.
|
|
29
|
+
*/
|
|
30
|
+
export const LEGACY_TEMPLATE_SHA256 = [
|
|
31
|
+
// 0.1.1 – 0.6.0
|
|
32
|
+
"a47dd9f391f6773f44aa7d7a42aa936bf22dd5fd5e8420f5001340239aea5aae",
|
|
33
|
+
// 0.6.1
|
|
34
|
+
"32c6e3278b5a4663562a2af307e56c3d9b6e74b4e34af02b033975eefe50b5db",
|
|
35
|
+
// 0.7.0, 0.8.0
|
|
36
|
+
"9ce9295be23c7b5e2f87357c78753e6f609a551bf74c8b5f5e463897f523ee89",
|
|
37
|
+
];
|
|
38
|
+
const BEGIN_RE = /^<!-- ailk:template id="([^"]*)"(?: sha256="([^"]*)")? -->$/;
|
|
39
|
+
const END_RE = /^<!-- \/ailk:template id="([^"]*)" -->$/;
|
|
40
|
+
const SITE_LINE = "<!-- ailk:site -->";
|
|
41
|
+
const ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
42
|
+
const SHA_RE = /^[0-9a-f]{64}$/;
|
|
43
|
+
/** SHA-256, lowercase hex, of a region body with every CRLF folded to LF (D2). */
|
|
44
|
+
export function hashBody(body) {
|
|
45
|
+
return createHash("sha256").update(body.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
46
|
+
}
|
|
47
|
+
/** Parse a `CLAUDE.md` against the D1 grammar. Never throws. */
|
|
48
|
+
export function parseClaudeMd(text) {
|
|
49
|
+
const regions = [];
|
|
50
|
+
const faults = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
let open;
|
|
53
|
+
let siteStart;
|
|
54
|
+
let marked = false;
|
|
55
|
+
let pos = 0;
|
|
56
|
+
while (pos < text.length) {
|
|
57
|
+
const nl = text.indexOf("\n", pos);
|
|
58
|
+
const next = nl === -1 ? text.length : nl + 1;
|
|
59
|
+
const line = text.slice(pos, nl === -1 ? text.length : nl).replace(/\r$/, "");
|
|
60
|
+
if (line === SITE_LINE) {
|
|
61
|
+
marked = true;
|
|
62
|
+
siteStart = pos;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
const begin = BEGIN_RE.exec(line);
|
|
66
|
+
const end = END_RE.exec(line);
|
|
67
|
+
if (begin) {
|
|
68
|
+
marked = true;
|
|
69
|
+
const id = begin[1] ?? "";
|
|
70
|
+
const sha = begin[2];
|
|
71
|
+
if (!ID_RE.test(id))
|
|
72
|
+
faults.push(`invalid section id "${id}"`);
|
|
73
|
+
if (sha !== undefined && !SHA_RE.test(sha))
|
|
74
|
+
faults.push(`section "${id}" has a malformed sha256`);
|
|
75
|
+
if (open) {
|
|
76
|
+
faults.push(`nested region: section "${id}" opens inside section "${open.id}"`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
if (seen.has(id))
|
|
80
|
+
faults.push(`duplicate section id "${id}"`);
|
|
81
|
+
seen.add(id);
|
|
82
|
+
open = { id, sha256: sha, start: pos, bodyStart: next };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (end) {
|
|
86
|
+
marked = true;
|
|
87
|
+
const id = end[1] ?? "";
|
|
88
|
+
if (!open) {
|
|
89
|
+
faults.push(`unbalanced marker: end of section "${id}" has no begin marker`);
|
|
90
|
+
}
|
|
91
|
+
else if (open.id !== id) {
|
|
92
|
+
faults.push(`unbalanced marker: section "${open.id}" is closed by an end marker for "${id}"`);
|
|
93
|
+
open = undefined;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
regions.push({
|
|
97
|
+
id,
|
|
98
|
+
sha256: open.sha256,
|
|
99
|
+
start: open.start,
|
|
100
|
+
end: next,
|
|
101
|
+
body: text.slice(open.bodyStart, pos),
|
|
102
|
+
});
|
|
103
|
+
open = undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (/^<!-- \/?ailk:template\b/.test(line)) {
|
|
107
|
+
marked = true;
|
|
108
|
+
faults.push(`malformed marker line: ${line}`);
|
|
109
|
+
}
|
|
110
|
+
pos = next;
|
|
111
|
+
}
|
|
112
|
+
if (open)
|
|
113
|
+
faults.push(`unbalanced marker: section "${open.id}" has no end marker`);
|
|
114
|
+
if (marked && siteStart === undefined)
|
|
115
|
+
faults.push("no ailk:site marker");
|
|
116
|
+
return { regions, siteStart, faults, marked };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Render the committed template for a project (D2): substitute the scaffold
|
|
120
|
+
* markers, then stamp each begin marker with its body's hash. LF output.
|
|
121
|
+
*
|
|
122
|
+
* @throws Error when the template is malformed or a begin marker already
|
|
123
|
+
* carries a hash — a hash is never committed by hand.
|
|
124
|
+
*/
|
|
125
|
+
export function renderClaudeMd(templateText, projectName) {
|
|
126
|
+
const text = substituteMarkers(templateText.replace(/\r\n/g, "\n"), buildScaffoldMarkers(projectName));
|
|
127
|
+
const parsed = parseClaudeMd(text);
|
|
128
|
+
if (parsed.faults.length > 0) {
|
|
129
|
+
throw new Error(`templates/CLAUDE.md is malformed: ${parsed.faults.join("; ")}`);
|
|
130
|
+
}
|
|
131
|
+
let out = text;
|
|
132
|
+
for (const region of [...parsed.regions].reverse()) {
|
|
133
|
+
if (region.sha256 !== undefined) {
|
|
134
|
+
throw new Error(`templates/CLAUDE.md: section "${region.id}" carries a sha256 — the render stamps it`);
|
|
135
|
+
}
|
|
136
|
+
const lineEnd = out.indexOf("\n", region.start) + 1;
|
|
137
|
+
const stamped = `<!-- ailk:template id="${region.id}" sha256="${hashBody(region.body)}" -->\n`;
|
|
138
|
+
out = out.slice(0, region.start) + stamped + out.slice(lineEnd);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The drift verdict for a rendered file (D6). `undefined` = no file. The
|
|
144
|
+
* reference implementation `tools/check-claude-md.ts` is held to.
|
|
145
|
+
*/
|
|
146
|
+
export function findDrift(text) {
|
|
147
|
+
if (text === undefined)
|
|
148
|
+
return { state: "missing", edited: [], faults: [] };
|
|
149
|
+
const parsed = parseClaudeMd(text);
|
|
150
|
+
if (!parsed.marked)
|
|
151
|
+
return { state: "unmarked", edited: [], faults: [] };
|
|
152
|
+
const faults = [...parsed.faults];
|
|
153
|
+
const edited = [];
|
|
154
|
+
for (const region of parsed.regions) {
|
|
155
|
+
if (region.sha256 === undefined) {
|
|
156
|
+
faults.push(`begin marker of section "${region.id}" has no sha256`);
|
|
157
|
+
}
|
|
158
|
+
else if (SHA_RE.test(region.sha256) && region.sha256 !== hashBody(region.body)) {
|
|
159
|
+
// A malformed hash is already a parse fault, not an edit.
|
|
160
|
+
edited.push(region.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { state: "marked", edited, faults };
|
|
164
|
+
}
|
|
165
|
+
/** The drift check's output lines; empty means the file passes. */
|
|
166
|
+
export function formatDrift(drift) {
|
|
167
|
+
if (drift.state === "missing")
|
|
168
|
+
return ["CLAUDE.md: missing"];
|
|
169
|
+
if (drift.state === "unmarked") {
|
|
170
|
+
return ["CLAUDE.md: no ailk:template markers — run pnpm create ailk@latest --sync-claude-md"];
|
|
171
|
+
}
|
|
172
|
+
return [
|
|
173
|
+
...drift.faults.map((f) => `CLAUDE.md: ${f}`),
|
|
174
|
+
...drift.edited.map((id) => `CLAUDE.md: template section "${id}" was edited by hand — move your change to Site notes, then run pnpm create ailk@latest --sync-claude-md`),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Rewrite a marked file's template-owned regions to a rendered template (D3).
|
|
179
|
+
* Every character outside a region — the gaps and the whole site section —
|
|
180
|
+
* is copied through unchanged, except the one blank line a retired region
|
|
181
|
+
* takes with it. Regions and inserted separators are written in the file's
|
|
182
|
+
* own line ending, so a CRLF checkout stays CRLF and syncs idempotently.
|
|
183
|
+
*
|
|
184
|
+
* @throws Error naming the fault when the file is structurally malformed.
|
|
185
|
+
*/
|
|
186
|
+
export function mergeClaudeMd(fileText, renderedText) {
|
|
187
|
+
const file = parseClaudeMd(fileText);
|
|
188
|
+
if (file.faults.length > 0 || file.siteStart === undefined) {
|
|
189
|
+
const faults = file.faults.length > 0 ? file.faults : ["no ailk:site marker"];
|
|
190
|
+
throw new Error(`CLAUDE.md: ${faults.join("; ")} — fix the markers by hand, then re-run --sync-claude-md`);
|
|
191
|
+
}
|
|
192
|
+
const first = file.regions[0];
|
|
193
|
+
const eol = first !== undefined && fileText[fileText.indexOf("\n", first.start) - 1] === "\r" ? "\r\n" : "\n";
|
|
194
|
+
const blank = (text) => text === "\n" || text === "\r\n";
|
|
195
|
+
const rendered = parseClaudeMd(renderedText);
|
|
196
|
+
const renderedById = new Map(rendered.regions.map((r) => [r.id, renderedText.slice(r.start, r.end).replace(/\n/g, eol)]));
|
|
197
|
+
const result = { text: "", updated: [], added: [], removed: [], overwritten: [] };
|
|
198
|
+
// The pre-site text as gap / region pieces, with each region already
|
|
199
|
+
// replaced (or dropped, when the template retired it).
|
|
200
|
+
const pieces = [];
|
|
201
|
+
let pos = 0;
|
|
202
|
+
let dropBlank = false;
|
|
203
|
+
for (const region of file.regions) {
|
|
204
|
+
const gap = fileText.slice(pos, region.start);
|
|
205
|
+
pieces.push({ text: dropBlank && blank(gap) ? "" : gap });
|
|
206
|
+
dropBlank = false;
|
|
207
|
+
pos = region.end;
|
|
208
|
+
const next = renderedById.get(region.id);
|
|
209
|
+
if (next === undefined) {
|
|
210
|
+
// A retired region takes the one blank line that separated it from its
|
|
211
|
+
// neighbour with it (the inverse of an insert), so the result matches
|
|
212
|
+
// a fresh render. Any other gap text is kept.
|
|
213
|
+
result.removed.push(region.id);
|
|
214
|
+
const kept = pieces.some((p) => p.region !== undefined);
|
|
215
|
+
const prev = pieces[pieces.length - 1];
|
|
216
|
+
if (kept && prev !== undefined && blank(prev.text))
|
|
217
|
+
prev.text = "";
|
|
218
|
+
else
|
|
219
|
+
dropBlank = !kept;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const current = fileText.slice(region.start, region.end);
|
|
223
|
+
if (region.sha256 !== hashBody(region.body))
|
|
224
|
+
result.overwritten.push(region.id);
|
|
225
|
+
if (next !== current)
|
|
226
|
+
result.updated.push(region.id);
|
|
227
|
+
pieces.push({ region: region.id, text: next });
|
|
228
|
+
}
|
|
229
|
+
const tail = fileText.slice(pos, file.siteStart);
|
|
230
|
+
pieces.push({ text: dropBlank && blank(tail) ? "" : tail });
|
|
231
|
+
// Insert each region the file lacks after its nearest preceding region
|
|
232
|
+
// in template order, one blank line apart (D3 step 5).
|
|
233
|
+
const order = rendered.regions.map((r) => r.id);
|
|
234
|
+
for (const [i, id] of order.entries()) {
|
|
235
|
+
if (pieces.some((p) => p.region === id))
|
|
236
|
+
continue;
|
|
237
|
+
const text = renderedById.get(id) ?? "";
|
|
238
|
+
const before = order
|
|
239
|
+
.slice(0, i)
|
|
240
|
+
.reverse()
|
|
241
|
+
.map((prev) => pieces.findIndex((p) => p.region === prev))
|
|
242
|
+
.find((idx) => idx !== -1);
|
|
243
|
+
if (before !== undefined) {
|
|
244
|
+
pieces.splice(before + 1, 0, { text: eol }, { region: id, text });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const first = pieces.findIndex((p) => p.region !== undefined);
|
|
248
|
+
pieces.splice(first === -1 ? pieces.length : first, 0, { region: id, text }, { text: eol });
|
|
249
|
+
}
|
|
250
|
+
result.added.push(id);
|
|
251
|
+
}
|
|
252
|
+
result.text = pieces.map((p) => p.text).join("") + fileText.slice(file.siteStart);
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* True when an unmarked `CLAUDE.md` is a published template the owner never
|
|
257
|
+
* changed (D4): with some candidate project name un-substituted, it hashes to
|
|
258
|
+
* a `LEGACY_TEMPLATE_SHA256` entry. A miss is always safe — it refuses.
|
|
259
|
+
*/
|
|
260
|
+
export function isUneditedLegacy(text, names) {
|
|
261
|
+
const folded = text.replace(/\r\n/g, "\n");
|
|
262
|
+
return names
|
|
263
|
+
.filter((name) => name.length > 0)
|
|
264
|
+
.some((name) => {
|
|
265
|
+
const hash = createHash("sha256")
|
|
266
|
+
.update(folded.split(name).join("{{PROJECT_NAME}}"), "utf8")
|
|
267
|
+
.digest("hex");
|
|
268
|
+
return LEGACY_TEMPLATE_SHA256.includes(hash);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `--sync-claude-md` (D3): the `syncCatalogSync` shape — validate the target
|
|
273
|
+
* is an existing scaffold, write at most its `CLAUDE.md`, spawn nothing. An
|
|
274
|
+
* unmarked file is replaced only when it is an unedited published template
|
|
275
|
+
* (D4); anything else unmarked, or a structural fault, is refused with
|
|
276
|
+
* nothing written.
|
|
277
|
+
*/
|
|
278
|
+
export function syncClaudeMd(opts) {
|
|
279
|
+
const targetAbs = resolve(opts.targetDir);
|
|
280
|
+
if (!existsSync(targetAbs)) {
|
|
281
|
+
throw new Error(`sync target does not exist: ${targetAbs}. Point --sync-claude-md at an already-scaffolded repo.`);
|
|
282
|
+
}
|
|
283
|
+
if (!statSync(targetAbs).isDirectory()) {
|
|
284
|
+
throw new Error(`sync target is not a directory: ${targetAbs}`);
|
|
285
|
+
}
|
|
286
|
+
if (!existsSync(join(targetAbs, "architecture.yaml"))) {
|
|
287
|
+
throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
|
|
288
|
+
}
|
|
289
|
+
const pkg = readJsonObject(join(targetAbs, "package.json"));
|
|
290
|
+
const pkgName = typeof pkg?.["name"] === "string" ? pkg["name"] : "";
|
|
291
|
+
const scripts = pkg?.["scripts"];
|
|
292
|
+
const hasScript = typeof scripts === "object" && scripts !== null && "claude-md:check" in scripts;
|
|
293
|
+
const rendered = renderClaudeMd(opts.templateText, pkgName || basename(targetAbs));
|
|
294
|
+
const claudeMdPath = join(targetAbs, "CLAUDE.md");
|
|
295
|
+
const base = {
|
|
296
|
+
targetDir: targetAbs,
|
|
297
|
+
claudeMdPath,
|
|
298
|
+
updated: [],
|
|
299
|
+
added: [],
|
|
300
|
+
removed: [],
|
|
301
|
+
overwritten: [],
|
|
302
|
+
missingDriftCheck: !hasScript || !existsSync(join(targetAbs, CLAUDE_MD_CHECK_PATH)),
|
|
303
|
+
};
|
|
304
|
+
if (!existsSync(claudeMdPath)) {
|
|
305
|
+
writeFileSync(claudeMdPath, rendered);
|
|
306
|
+
return { ...base, action: "created" };
|
|
307
|
+
}
|
|
308
|
+
const current = readFileSync(claudeMdPath, "utf8");
|
|
309
|
+
if (!parseClaudeMd(current).marked) {
|
|
310
|
+
if (!isUneditedLegacy(current, [pkgName, basename(targetAbs)])) {
|
|
311
|
+
throw new Error(`${claudeMdPath} has no ailk:template markers and is not an unedited AI Launch Kit template, so --sync-claude-md will not overwrite it. ` +
|
|
312
|
+
"Copy anything you added out of CLAUDE.md, delete it, run --sync-claude-md again (it then writes a fresh file), and paste your notes under Site notes.");
|
|
313
|
+
}
|
|
314
|
+
writeFileSync(claudeMdPath, rendered);
|
|
315
|
+
return { ...base, action: "replaced-legacy" };
|
|
316
|
+
}
|
|
317
|
+
const { text, updated, added, removed, overwritten } = mergeClaudeMd(current, rendered);
|
|
318
|
+
const changed = text !== current;
|
|
319
|
+
if (changed)
|
|
320
|
+
writeFileSync(claudeMdPath, text);
|
|
321
|
+
return { ...base, action: changed ? "updated" : "unchanged", updated, added, removed, overwritten };
|
|
322
|
+
}
|
|
323
|
+
/** A JSON file's top-level object, or undefined when absent or unreadable. */
|
|
324
|
+
function readJsonObject(path) {
|
|
325
|
+
try {
|
|
326
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
327
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
328
|
+
? value
|
|
329
|
+
: undefined;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* Orchestration:
|
|
6
6
|
* 0. on an `add-module` / `upgrade` subcommand: parse its own argv, run
|
|
7
7
|
* addModule(), print the report, exit (2 on a refusal it names)
|
|
8
|
+
* on an `add-route` subcommand: add one route surface to an existing
|
|
9
|
+
* site, print the report, exit (2 on a refusal it names) (#6947)
|
|
8
10
|
* on a `sync-routes` subcommand: parse its own argv, run syncRoutesSync(),
|
|
9
11
|
* print the report, exit (1 iff routes are missing in check mode)
|
|
10
12
|
* on a `license set` subcommand: write the key into the scaffold, push it
|
|
11
13
|
* to a linked Vercel project and redeploy, exit (#6683 D9)
|
|
14
|
+
* on an `apply-design` subcommand: rewrite an existing site's look,
|
|
15
|
+
* print the changed-file report, exit (#6819)
|
|
12
16
|
* 1. parse argv → CliOptions
|
|
13
17
|
* 2. on --help: print helpText, exit 0
|
|
14
18
|
* 3. on --version: print version, exit 0
|
|
@@ -31,14 +35,18 @@
|
|
|
31
35
|
* synchronous as before.
|
|
32
36
|
*/
|
|
33
37
|
import { spawnSync } from "node:child_process";
|
|
34
|
-
import { readFileSync } from "node:fs";
|
|
35
|
-
import { dirname, resolve } from "node:path";
|
|
38
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
39
|
+
import { basename, dirname, resolve } from "node:path";
|
|
36
40
|
import { fileURLToPath } from "node:url";
|
|
37
41
|
import { ADD_MODULE_HELP, AddModuleRefusedError, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
|
|
42
|
+
import { ADD_ROUTE_HELP, AddRouteRefusedError, addRouteToScaffold, formatAddRouteReport, parseAddRouteArgs, } from "./add-route.js";
|
|
43
|
+
import { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
|
|
44
|
+
import { CLAUDE_MD_CHECK_PATH } from "./claude-md.js";
|
|
45
|
+
import { buildScaffoldMarkers } from "./copy.js";
|
|
38
46
|
import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
|
|
39
47
|
import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
|
|
40
48
|
import { helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
|
|
41
|
-
import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncRoutesSync, } from "./programmatic.js";
|
|
49
|
+
import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
|
|
42
50
|
import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
|
|
43
51
|
import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
|
|
44
52
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -47,6 +55,13 @@ function readPackageVersion() {
|
|
|
47
55
|
const raw = readFileSync(PKG_JSON, "utf8");
|
|
48
56
|
return JSON.parse(raw).version;
|
|
49
57
|
}
|
|
58
|
+
/** The workspace version map `build-templates` wrote; empty before the first build. */
|
|
59
|
+
function readVersionMap() {
|
|
60
|
+
const path = resolve(PKG_ROOT, "versions.json");
|
|
61
|
+
if (!existsSync(path))
|
|
62
|
+
return {};
|
|
63
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
64
|
+
}
|
|
50
65
|
/** Raw value of a `--flag <value>` or `--flag=<value>` argv entry, if present. */
|
|
51
66
|
function readRawFlagValue(argv, flag) {
|
|
52
67
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -115,6 +130,36 @@ async function main() {
|
|
|
115
130
|
}
|
|
116
131
|
process.exit(0);
|
|
117
132
|
}
|
|
133
|
+
// `add-route <route> [dir]` (#6947): add a route surface the site was
|
|
134
|
+
// scaffolded without — add-module's route, architecture and nav steps with
|
|
135
|
+
// no licence and no bundle. A refusal it names (an unknown route, one a
|
|
136
|
+
// module delivers, an unreadable architecture.yaml) exits 2.
|
|
137
|
+
if (argv[0] === "add-route") {
|
|
138
|
+
const args = parseAddRouteArgs(argv.slice(1));
|
|
139
|
+
if (args.help || args.route === undefined) {
|
|
140
|
+
process.stdout.write(ADD_ROUTE_HELP);
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
const targetDir = resolve(args.target ?? process.cwd());
|
|
144
|
+
try {
|
|
145
|
+
const result = addRouteToScaffold({
|
|
146
|
+
targetDir,
|
|
147
|
+
route: args.route,
|
|
148
|
+
versions: readVersionMap(),
|
|
149
|
+
markers: buildScaffoldMarkers(basename(targetDir)),
|
|
150
|
+
templatesDir: resolve(PKG_ROOT, "templates"),
|
|
151
|
+
siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
|
|
152
|
+
});
|
|
153
|
+
process.stdout.write(formatAddRouteReport(result));
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (!(err instanceof AddRouteRefusedError))
|
|
157
|
+
throw err;
|
|
158
|
+
process.stderr.write(`[create-ailk] ${err.message}\n`);
|
|
159
|
+
process.exit(err.exitCode);
|
|
160
|
+
}
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
118
163
|
// Subcommand dispatch, ahead of the flag parser: `sync-routes` has its own
|
|
119
164
|
// argv shape (#4986). Check mode exits 1 iff a template route is missing;
|
|
120
165
|
// locally modified routes are reported but do not fail; --apply exits 0.
|
|
@@ -141,6 +186,25 @@ async function main() {
|
|
|
141
186
|
readStdin: () => readFileSync(0, "utf8"),
|
|
142
187
|
}));
|
|
143
188
|
}
|
|
189
|
+
// `apply-design <name|path> [--target <dir>]` (#6819): rewrite an existing
|
|
190
|
+
// site's look only. Every refusal — the licence gate included — reaches
|
|
191
|
+
// the top-level catch, exit 1, as a scaffold-time design refusal does.
|
|
192
|
+
if (argv[0] === "apply-design") {
|
|
193
|
+
const args = parseApplyDesignArgs(argv.slice(1));
|
|
194
|
+
if (args.help || args.design === undefined) {
|
|
195
|
+
process.stdout.write(APPLY_DESIGN_HELP);
|
|
196
|
+
process.exit(0);
|
|
197
|
+
}
|
|
198
|
+
const result = await applyDesign({
|
|
199
|
+
design: args.design,
|
|
200
|
+
targetDir: args.target ?? process.cwd(),
|
|
201
|
+
designsDir: resolve(PKG_ROOT, "site-templates", "designs"),
|
|
202
|
+
siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
|
|
203
|
+
catalogPath: resolve(PKG_ROOT, "component-catalog.json"),
|
|
204
|
+
});
|
|
205
|
+
process.stdout.write(formatApplyDesignReport(result));
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
144
208
|
// No silent default (#6754 D2/D3): a scaffold run naming neither --shape
|
|
145
209
|
// nor --template asks for a shape on a terminal and is refused without one.
|
|
146
210
|
const opts = await resolveCliShape(parseCliArgs(argv, { interactive: process.stdin.isTTY === true }), { input: process.stdin, output: process.stdout });
|
|
@@ -168,6 +232,10 @@ async function main() {
|
|
|
168
232
|
"\n" +
|
|
169
233
|
ADD_MODULE_HELP +
|
|
170
234
|
"\n" +
|
|
235
|
+
ADD_ROUTE_HELP +
|
|
236
|
+
"\n" +
|
|
237
|
+
APPLY_DESIGN_HELP +
|
|
238
|
+
"\n" +
|
|
171
239
|
LICENSE_SET_HELP);
|
|
172
240
|
process.exit(0);
|
|
173
241
|
}
|
|
@@ -186,6 +254,26 @@ async function main() {
|
|
|
186
254
|
`[create-ailk] Search it by CAPABILITY before building a component — it indexes every @working-theory/ui section, block and chrome component by what it does.\n`);
|
|
187
255
|
process.exit(0);
|
|
188
256
|
}
|
|
257
|
+
// CLAUDE.md sync mode (#6904 D3): rewrite the template-owned sections of an
|
|
258
|
+
// already-scaffolded repo's CLAUDE.md, then exit. One file, nothing else.
|
|
259
|
+
if (opts.syncClaudeMd) {
|
|
260
|
+
const r = syncClaudeMdSync({ targetDir: opts.target ?? process.cwd() });
|
|
261
|
+
const lines = [`[create-ailk] ${r.claudeMdPath}: ${r.action}`];
|
|
262
|
+
if (r.updated.length > 0)
|
|
263
|
+
lines.push(`[create-ailk] Updated sections: ${r.updated.join(", ")}`);
|
|
264
|
+
if (r.added.length > 0)
|
|
265
|
+
lines.push(`[create-ailk] Added sections: ${r.added.join(", ")}`);
|
|
266
|
+
if (r.removed.length > 0)
|
|
267
|
+
lines.push(`[create-ailk] Removed sections: ${r.removed.join(", ")}`);
|
|
268
|
+
if (r.overwritten.length > 0) {
|
|
269
|
+
lines.push(`[create-ailk] Replaced hand-edits in: ${r.overwritten.join(", ")} — the previous text is in git; move anything you need into Site notes.`);
|
|
270
|
+
}
|
|
271
|
+
if (r.missingDriftCheck) {
|
|
272
|
+
lines.push(`[create-ailk] This site has no CLAUDE.md drift check yet — copy ${CLAUDE_MD_CHECK_PATH} from a fresh scaffold and add "claude-md:check": "tsx ${CLAUDE_MD_CHECK_PATH}" to package.json scripts and preflight.`);
|
|
273
|
+
}
|
|
274
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
189
277
|
// Sync mode: regenerate architecture.yaml in an already-scaffolded repo from
|
|
190
278
|
// the module set, then exit — bypassing the scaffold / --install / next-steps
|
|
191
279
|
// path entirely (spec #3561 D2). Data-only and non-destructive: only
|
|
@@ -284,7 +372,7 @@ async function main() {
|
|
|
284
372
|
lines.push("");
|
|
285
373
|
lines.push(`Route surfaces: ${CORE_SURFACE_PATHS.join(", ")}` +
|
|
286
374
|
(surfaces.length > 0 ? ` + ${surfaces.join(", ")}` : ""));
|
|
287
|
-
lines.push("Add another with
|
|
375
|
+
lines.push("Add another with `create-ailk add-route <id>`, or re-run with --surfaces <id>.");
|
|
288
376
|
// Only when asked for. The default scaffold's output is unchanged.
|
|
289
377
|
if (opts.withCatalog) {
|
|
290
378
|
lines.push("Component catalog: COMPONENT-CATALOG.md — search it by capability before building a component.");
|
|
@@ -130,12 +130,6 @@ function appToYaml(app) {
|
|
|
130
130
|
}
|
|
131
131
|
return m;
|
|
132
132
|
}
|
|
133
|
-
function packageToYaml(pkg) {
|
|
134
|
-
return {
|
|
135
|
-
description: pkg.description,
|
|
136
|
-
may_import_from: [...pkg.may_import_from].sort(),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
133
|
function namespacesToYaml(ns) {
|
|
140
134
|
const members = {};
|
|
141
135
|
const sortedMembers = Object.entries(ns.members).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -170,37 +164,50 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
|
170
164
|
const enabledSurfaces = surfaces === undefined
|
|
171
165
|
? resolveSurfaces(selected)
|
|
172
166
|
: [...new Set(surfaces)].sort();
|
|
173
|
-
// apps
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
// The apps a scaffold ships on disk: web, api and mcp always (BASE), plus
|
|
168
|
+
// any a module brings (admin, with multi-tenant). A module's own `api`/`mcp`
|
|
169
|
+
// block replaces the base one. Canonical order web → api → mcp → admin.
|
|
170
|
+
// `admin` was `workspace` until #5637 renamed the owner surface; the slice's
|
|
171
|
+
// key was renamed there and this list was not, which dropped the Admin app
|
|
172
|
+
// from every derived architecture without failing anything but its own test.
|
|
173
|
+
const declared = {
|
|
176
174
|
web: {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
may_import_from: webImports,
|
|
180
|
-
may_not_import_from: [...ARCHITECTURE_BASE.web.may_not_import_from].sort(),
|
|
181
|
-
forbidden_patterns: ARCHITECTURE_BASE.web.forbidden_patterns.map(forbiddenToYaml),
|
|
175
|
+
...ARCHITECTURE_BASE.web,
|
|
176
|
+
may_not_import_from: [...ARCHITECTURE_BASE.web.may_not_import_from],
|
|
177
|
+
may_import_from: sortedUnion(...slices.map((s) => s.webImports)),
|
|
182
178
|
},
|
|
179
|
+
...ARCHITECTURE_BASE.apps,
|
|
183
180
|
};
|
|
184
|
-
// Additional apps (api, mcp, admin) in canonical order. `admin` was
|
|
185
|
-
// `workspace` until #5637 renamed the owner surface; the slice's key was
|
|
186
|
-
// renamed there and this list was not, which dropped the Admin app from
|
|
187
|
-
// every derived architecture without failing anything but its own test.
|
|
188
|
-
const extraApps = {};
|
|
189
181
|
for (const s of slices)
|
|
190
|
-
Object.assign(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
182
|
+
Object.assign(declared, s.apps ?? {});
|
|
183
|
+
// A scaffold carries no `packages/` tree — `@working-theory/*` install from
|
|
184
|
+
// npm — so an entry naming a monorepo folder is dropped rather than shipped
|
|
185
|
+
// as a rule the linter can never apply (#6940). What survives names a path
|
|
186
|
+
// the scaffold has: a declared app, or `database/`.
|
|
187
|
+
const roots = [
|
|
188
|
+
"database",
|
|
189
|
+
...Object.values(declared).map((a) => a.path),
|
|
190
|
+
];
|
|
191
|
+
const inScaffold = (p) => {
|
|
192
|
+
const path = p.endsWith("/**") ? p.slice(0, -3) : p;
|
|
193
|
+
return roots.some((r) => path === r || path.startsWith(`${r}/`));
|
|
194
|
+
};
|
|
195
|
+
const scoped = (p) => {
|
|
196
|
+
const { paths_excluded, ...rest } = p;
|
|
197
|
+
const kept = (paths_excluded ?? []).filter(inScaffold);
|
|
198
|
+
return kept.length > 0 ? { ...rest, paths_excluded: kept } : rest;
|
|
199
|
+
};
|
|
200
|
+
const apps = {};
|
|
201
|
+
for (const key of ["web", "api", "mcp", "admin"]) {
|
|
202
|
+
const a = declared[key];
|
|
203
|
+
if (!a)
|
|
204
|
+
continue;
|
|
205
|
+
apps[key] = appToYaml({
|
|
206
|
+
...a,
|
|
207
|
+
may_import_from: a.may_import_from.filter(inScaffold),
|
|
208
|
+
may_not_import_from: a.may_not_import_from.filter(inScaffold),
|
|
209
|
+
forbidden_patterns: mergeForbidden([...ARCHITECTURE_BASE.appForbiddenPatterns], a.forbidden_patterns ?? []).map(scoped),
|
|
210
|
+
});
|
|
204
211
|
}
|
|
205
212
|
// namespaces — present only if a data-tier module contributes one.
|
|
206
213
|
const nsSlice = slices.find((s) => s.namespaces)?.namespaces;
|
|
@@ -208,11 +215,10 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
|
208
215
|
const extraForbidden = [];
|
|
209
216
|
for (const s of slices)
|
|
210
217
|
extraForbidden.push(...(s.forbiddenPatterns ?? []));
|
|
211
|
-
const forbidden = mergeForbidden([...ARCHITECTURE_BASE.forbiddenPatterns], extraForbidden);
|
|
218
|
+
const forbidden = mergeForbidden([...ARCHITECTURE_BASE.forbiddenPatterns], extraForbidden).map(scoped);
|
|
212
219
|
const doc = {
|
|
213
220
|
version: ARCHITECTURE_BASE.version,
|
|
214
221
|
composition_model: {
|
|
215
|
-
reference: ARCHITECTURE_BASE.composition_model.reference,
|
|
216
222
|
tiers: { ...ARCHITECTURE_BASE.composition_model.tiers },
|
|
217
223
|
},
|
|
218
224
|
surfaces: {
|
|
@@ -221,7 +227,6 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
|
|
|
221
227
|
available: [...RECOGNIZED_SURFACES],
|
|
222
228
|
},
|
|
223
229
|
apps,
|
|
224
|
-
packages,
|
|
225
230
|
...(nsSlice ? { namespaces: namespacesToYaml(nsSlice) } : {}),
|
|
226
231
|
forbidden_patterns: forbidden.map(forbiddenToYaml),
|
|
227
232
|
};
|