create-ailk 0.19.1 → 0.21.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 +84 -7
- package/component-catalog.json +184 -6
- package/component-catalog.md +20 -9
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/cli.js +35 -0
- package/dist/copy.d.ts +41 -0
- package/dist/copy.js +87 -21
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/fetch-template-snapshot.d.ts +21 -0
- package/dist/lib/fetch-template-snapshot.js +205 -0
- package/dist/lib/merge-three-way.d.ts +31 -0
- package/dist/lib/merge-three-way.js +65 -0
- package/dist/module-architecture.js +55 -2
- package/dist/programmatic.d.ts +7 -1
- package/dist/programmatic.js +76 -22
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +7 -0
- package/dist/spoke-checkout.js +8 -1
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +78 -2
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +123 -1
- package/dist/template-record.d.ts +59 -0
- package/dist/template-record.js +183 -0
- package/dist/update.d.ts +53 -0
- package/dist/update.js +606 -0
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/.claude/cloudparity-repo-hooks.json +9 -0
- package/templates/.claude/hooks/architecture-check.sh +8 -0
- package/templates/.claude/rules/loop-contract.md +129 -0
- package/templates/.claude/settings.json +3 -2
- package/templates/.env.example +13 -0
- package/templates/CLAUDE.md +1 -1
- package/templates/CONVENTIONS.md +3 -2
- package/templates/README.md +26 -25
- package/templates/_gitignore +162 -0
- package/templates/apps/api/CLAUDE.md +18 -10
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
- package/templates/apps/api/src/__tests__/setup.ts +7 -0
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
- package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
- package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
- package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
- package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
- package/templates/apps/api/src/middleware/auth.ts +8 -4
- package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/README.md +33 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
- package/templates/apps/api/src/server.ts +14 -2
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
- package/templates/apps/api/src/services/audit-log.ts +72 -10
- package/templates/apps/api/src/services/default-tenant.ts +12 -3
- package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
- package/templates/apps/api/vercel.json +1 -1
- package/templates/apps/mcp/CLAUDE.md +19 -7
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +139 -44
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
- package/templates/apps/mcp/src/tools/proposals.ts +27 -0
- package/templates/apps/mcp/src/workspace-server.ts +16 -0
- package/templates/apps/web/CLAUDE.md +26 -1
- package/templates/apps/web/_gitignore +6 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
- package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +48 -4
- package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +46 -4
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/robots.txt/route.ts +13 -10
- package/templates/apps/web/jest.config.cjs +4 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +100 -9
- package/templates/apps/web/lib/account-config.ts +31 -16
- package/templates/apps/web/lib/consent-visitor.ts +103 -0
- package/templates/apps/web/lib/deck-page.ts +186 -0
- package/templates/apps/web/lib/relay.ts +20 -0
- package/templates/apps/web/messages/en.json +83 -9
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/database/CHANGELOG.md +48 -0
- package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
- package/templates/database/__tests__/person-notes.db.test.ts +249 -0
- package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
- package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
- package/templates/database/__tests__/seed.test.ts +39 -2
- package/templates/database/_gitignore +8 -0
- package/templates/database/inbox/schema.prisma +38 -12
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
- package/templates/database/ops/schema.prisma +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +29 -0
- package/templates/project.yaml +6 -3
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +35 -35
- package/templates/.claude/rules/preserve-existing-style.md +0 -51
package/dist/sync-routes.js
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
* route root is copied once into a scratch dir and compared from there.
|
|
22
22
|
* - Never overwrite, never delete. A route dir the site has changed is
|
|
23
23
|
* reported as modified and left alone; `--apply` only creates directories
|
|
24
|
-
* that do not exist, so a second run is a no-op.
|
|
24
|
+
* that do not exist, so a second run is a no-op. When the site has edited
|
|
25
|
+
* its structured-data test, a route `--apply` adds that the template
|
|
26
|
+
* exempts there is reported with the exact `NO_STRUCTURED_DATA_ROUTES`
|
|
27
|
+
* entry to add — the test itself is never written (#8704).
|
|
25
28
|
*
|
|
26
29
|
* Pure module: no `import.meta.url`, so jest can import it directly. The
|
|
27
30
|
* package's own `templates/` dir is supplied by programmatic.ts.
|
|
@@ -35,9 +38,12 @@ import { appendEnvRows } from "./lib/declared-env.js";
|
|
|
35
38
|
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
36
39
|
import { substituteMarkers } from "./substitute.js";
|
|
37
40
|
import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
|
|
41
|
+
import { TEMPLATE_RECORD_PATH, parseTemplateRecord, sha256Hex } from "./template-record.js";
|
|
38
42
|
import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, } from "./topology.js";
|
|
39
43
|
/** Template-relative prefix every `apps/web` route directory sits under. */
|
|
40
44
|
const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
45
|
+
/** The site test that fails a route neither rendered through `renderPage` nor in its `NO_STRUCTURED_DATA_ROUTES` (#8704). */
|
|
46
|
+
export const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data.test.tsx";
|
|
41
47
|
/**
|
|
42
48
|
* `<surface id>/<entry name>` pairs that live directly under a GATEABLE
|
|
43
49
|
* surface's top-level directory but belong to a DIFFERENT module (#5366).
|
|
@@ -50,6 +56,114 @@ const ROUTE_ROOT = "apps/web/app/[locale]";
|
|
|
50
56
|
const NESTED_FOREIGN_MODULE_ENTRIES = new Set([
|
|
51
57
|
"projects/apply",
|
|
52
58
|
]);
|
|
59
|
+
/**
|
|
60
|
+
* The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
|
|
61
|
+
* brace scan that skips string literals and comments. Empty when the
|
|
62
|
+
* file declares no such array. It assumes what the template's array holds:
|
|
63
|
+
* object literals of string, boolean and identifier-array values — no regex
|
|
64
|
+
* literal or `${…}` interpolation.
|
|
65
|
+
*/
|
|
66
|
+
export function parseStructuredDataExemptions(source) {
|
|
67
|
+
const decl = source.indexOf("const NO_STRUCTURED_DATA_ROUTES");
|
|
68
|
+
if (decl === -1)
|
|
69
|
+
return [];
|
|
70
|
+
const open = /=\s*\[/.exec(source.slice(decl));
|
|
71
|
+
if (open === null)
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
let depth = 0;
|
|
75
|
+
let start = -1;
|
|
76
|
+
for (let i = decl + open.index + open[0].length; i < source.length; i++) {
|
|
77
|
+
const ch = source[i];
|
|
78
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
79
|
+
for (i++; i < source.length && source[i] !== ch; i++) {
|
|
80
|
+
if (source[i] === "\\")
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
86
|
+
const eol = source.indexOf("\n", i);
|
|
87
|
+
i = eol === -1 ? source.length : eol;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
91
|
+
const end = source.indexOf("*/", i + 2);
|
|
92
|
+
i = end === -1 ? source.length : end + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === "{") {
|
|
96
|
+
if (depth === 0)
|
|
97
|
+
start = i;
|
|
98
|
+
depth++;
|
|
99
|
+
}
|
|
100
|
+
else if (ch === "}") {
|
|
101
|
+
depth--;
|
|
102
|
+
if (depth === 0 && start !== -1) {
|
|
103
|
+
const raw = source.slice(start, i + 1) + (source[i + 1] === "," ? "," : "");
|
|
104
|
+
const route = /\broute:\s*["']([^"']+)["']/.exec(raw)?.[1];
|
|
105
|
+
if (route !== undefined) {
|
|
106
|
+
const indent = /[ \t]*$/.exec(source.slice(source.lastIndexOf("\n", start) + 1, start))?.[0] ?? "";
|
|
107
|
+
const text = raw
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map((line) => (line.startsWith(indent) ? line.slice(indent.length) : line))
|
|
110
|
+
.join("\n");
|
|
111
|
+
out.push({ route, subtree: /\bsubtree:\s*true\b/.test(raw), text });
|
|
112
|
+
}
|
|
113
|
+
start = -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (ch === "]" && depth === 0) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The first exemption covering `route` by the structured-data test's route
|
|
124
|
+
* rule (`route` itself, or under a `subtree` entry). An entry's `requires`
|
|
125
|
+
* strings are not checked: they key only the generated spoke checkout pages,
|
|
126
|
+
* which no template route dir — so no `--apply` — ever adds.
|
|
127
|
+
*/
|
|
128
|
+
function exemptionFor(exemptions, route) {
|
|
129
|
+
return exemptions.find((e) => route === e.route || (e.subtree && route.startsWith(`${e.route}/`)));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The template `NO_STRUCTURED_DATA_ROUTES` entries the site's structured-data
|
|
133
|
+
* test must gain for `added` routes (#8704). Empty unless that test is
|
|
134
|
+
* recorded and site-edited — an untouched copy is `create-ailk update`'s to
|
|
135
|
+
* bring forward, and an unrecorded one cannot be told apart from an edit.
|
|
136
|
+
*/
|
|
137
|
+
function structuredDataEntriesFor(targetAbs, templatesDir, added) {
|
|
138
|
+
const sitePath = join(targetAbs, STRUCTURED_DATA_TEST_PATH);
|
|
139
|
+
const templatePath = join(templatesDir, STRUCTURED_DATA_TEST_PATH);
|
|
140
|
+
const recordPath = join(targetAbs, TEMPLATE_RECORD_PATH);
|
|
141
|
+
if (added.length === 0 || !existsSync(sitePath) || !existsSync(templatePath) || !existsSync(recordPath)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
let recordedHash;
|
|
145
|
+
try {
|
|
146
|
+
recordedHash = parseTemplateRecord(readFileSync(recordPath, "utf8")).files[STRUCTURED_DATA_TEST_PATH]?.sha256;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const siteBytes = readFileSync(sitePath);
|
|
152
|
+
if (recordedHash === undefined || sha256Hex(siteBytes) === recordedHash)
|
|
153
|
+
return [];
|
|
154
|
+
const templateExemptions = parseStructuredDataExemptions(readFileSync(templatePath, "utf8"));
|
|
155
|
+
const siteExemptions = parseStructuredDataExemptions(siteBytes.toString("utf8"));
|
|
156
|
+
const byEntry = new Map();
|
|
157
|
+
for (const route of [...added].sort()) {
|
|
158
|
+
const needed = exemptionFor(templateExemptions, route);
|
|
159
|
+
if (needed === undefined || exemptionFor(siteExemptions, route) !== undefined)
|
|
160
|
+
continue;
|
|
161
|
+
byEntry.set(needed.text, [...(byEntry.get(needed.text) ?? []), route]);
|
|
162
|
+
}
|
|
163
|
+
return [...byEntry.entries()]
|
|
164
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
165
|
+
.map(([entry, routes]) => ({ routes, entry }));
|
|
166
|
+
}
|
|
53
167
|
/**
|
|
54
168
|
* The template's route directories, ROUTE_ROOT-relative and sorted.
|
|
55
169
|
*
|
|
@@ -276,6 +390,7 @@ export function syncRoutes(opts) {
|
|
|
276
390
|
appliedDependencies,
|
|
277
391
|
missingMessageNamespaces,
|
|
278
392
|
skippedSurfaces,
|
|
393
|
+
structuredDataEntries: structuredDataEntriesFor(targetAbs, opts.templatesDir, applied),
|
|
279
394
|
};
|
|
280
395
|
}
|
|
281
396
|
/**
|
|
@@ -355,6 +470,13 @@ export function formatSyncRoutesReport(result, apply) {
|
|
|
355
470
|
lines.push(`[create-ailk] Also required — message namespace(s) not found in apps/web/messages/en.json (add manually, not auto-merged):`);
|
|
356
471
|
lines.push(...result.missingMessageNamespaces.map((ns) => ` ${ns}`));
|
|
357
472
|
}
|
|
473
|
+
if (result.structuredDataEntries.length > 0) {
|
|
474
|
+
lines.push(`[create-ailk] Also required — ${STRUCTURED_DATA_TEST_PATH} is edited here, so add these NO_STRUCTURED_DATA_ROUTES entries to it by hand (not written):`);
|
|
475
|
+
for (const { routes, entry } of result.structuredDataEntries) {
|
|
476
|
+
lines.push(` for ${routes.join(", ")}:`);
|
|
477
|
+
lines.push(...entry.split("\n").map((line) => ` ${line}`));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
358
480
|
if (result.missing.length > 0 && !apply) {
|
|
359
481
|
lines.push("Run `create-ailk sync-routes <target> --apply` to copy the missing routes.");
|
|
360
482
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Where the per-file record lives, beside `.ailk-site-manifest.json`. */
|
|
2
|
+
export declare const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
|
|
3
|
+
/**
|
|
4
|
+
* The app paths a generator rewrites after `copyTemplate` lands its copy
|
|
5
|
+
* (§1): the nav, the site chrome, the site fonts, the paid-sections
|
|
6
|
+
* registry, and the four spoke seams. Composed from each writer's own
|
|
7
|
+
* exported path constant, so a renamed constant carries its path along
|
|
8
|
+
* here too (#8522 D2).
|
|
9
|
+
*/
|
|
10
|
+
export declare const REGENERATED_APP_PATHS: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Whether a destination path (`/`-separated, as `templateDestination`
|
|
13
|
+
* returns it) is one `update` ever records or touches (#8522 D2): under one
|
|
14
|
+
* of the three app trees, not a `package.json`, not an `.env*` file, no
|
|
15
|
+
* `.claude` segment, and not a path a generator rewrites after the copy.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isRecordable(destRel: string): boolean;
|
|
18
|
+
/** The hex sha256 digest of `bytes` — the one hash every record row uses. */
|
|
19
|
+
export declare function sha256Hex(bytes: Uint8Array): string;
|
|
20
|
+
export interface TemplateFileRow {
|
|
21
|
+
sha256: string;
|
|
22
|
+
version: string;
|
|
23
|
+
}
|
|
24
|
+
export interface TemplateRecord {
|
|
25
|
+
schema: 1;
|
|
26
|
+
projectName: string;
|
|
27
|
+
files: Record<string, TemplateFileRow>;
|
|
28
|
+
}
|
|
29
|
+
/** Thrown by `parseTemplateRecord` on a malformed or unsafe record. */
|
|
30
|
+
export declare class TemplateRecordError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse `.ailk-template-files.json` text, refusing — naming the field — on:
|
|
35
|
+
* JSON that does not parse; `schema !== 1`; an unknown top-level key; an
|
|
36
|
+
* empty `projectName`; a `files` key that is not a recordable path; a
|
|
37
|
+
* `sha256` that is not 64 lowercase hex; a `version` that is not a semver
|
|
38
|
+
* string (#8522 D1).
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseTemplateRecord(text: string): TemplateRecord;
|
|
41
|
+
/** `record` as `scaffold()` and `update` write it: sorted keys, trailing newline. */
|
|
42
|
+
export declare function serializeTemplateRecord(record: TemplateRecord): string;
|
|
43
|
+
export interface PlanTemplateRecordOptions {
|
|
44
|
+
/** The freshly scaffolded site. */
|
|
45
|
+
targetDir: string;
|
|
46
|
+
/** Destination path -> sha256 of the bytes `copyTemplate`'s `onWrite` wrote, for every recordable path. */
|
|
47
|
+
copied: ReadonlyMap<string, string>;
|
|
48
|
+
/** The create-ailk version that wrote this scaffold. */
|
|
49
|
+
version: string;
|
|
50
|
+
projectName: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The record a fresh scaffold writes (#8522 D2): one row per copied path
|
|
54
|
+
* whose file still exists and whose current bytes still hash to the copy —
|
|
55
|
+
* a later layer (a module bundle's patch, a generator) that rewrote the
|
|
56
|
+
* file drops it from the record, exactly as it is absent from the record a
|
|
57
|
+
* `create-ailk update` keeps current for it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function planTemplateRecord(opts: PlanTemplateRecordOptions): TemplateRecord;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.ailk-template-files.json` — the per-file record of what a scaffold's
|
|
3
|
+
* copied app code is (#8522 D1/D2).
|
|
4
|
+
*
|
|
5
|
+
* Written beside `.ailk-site-manifest.json` at the site root, it lists every
|
|
6
|
+
* `apps/web/`, `apps/api/` and `apps/mcp/` file the base `templates/` copy
|
|
7
|
+
* wrote and no later layer rewrote, with the sha256 of the template's copy
|
|
8
|
+
* of that file and the create-ailk version it came from. `create-ailk
|
|
9
|
+
* update` (`update.ts`) reads it to tell an untouched copy from an owner's
|
|
10
|
+
* edit; `scaffold()` (`programmatic.ts`) writes it, as its last write, from
|
|
11
|
+
* the `onWrite` hook `copyTemplate` (`copy.ts`) calls once per file.
|
|
12
|
+
*
|
|
13
|
+
* Pure module: no `import.meta.url`, no filesystem write — `planTemplateRecord`
|
|
14
|
+
* only READS the scaffold's own disk to confirm a copied path's final bytes
|
|
15
|
+
* (a later layer may have rewritten it, D2); every write goes through the
|
|
16
|
+
* caller.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { PAID_SECTIONS_PATH } from "./paid-sections.js";
|
|
22
|
+
import { SITE_CHROME_PATH } from "./site-chrome.js";
|
|
23
|
+
import { SITE_FONTS_PATH } from "./site-fonts.js";
|
|
24
|
+
import { NAV_CONFIG_PATH } from "./surfaces.js";
|
|
25
|
+
import { SPOKE_SEAM_FILES } from "./topology.js";
|
|
26
|
+
/** Where the per-file record lives, beside `.ailk-site-manifest.json`. */
|
|
27
|
+
export const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
|
|
28
|
+
/**
|
|
29
|
+
* The app paths a generator rewrites after `copyTemplate` lands its copy
|
|
30
|
+
* (§1): the nav, the site chrome, the site fonts, the paid-sections
|
|
31
|
+
* registry, and the four spoke seams. Composed from each writer's own
|
|
32
|
+
* exported path constant, so a renamed constant carries its path along
|
|
33
|
+
* here too (#8522 D2).
|
|
34
|
+
*/
|
|
35
|
+
export const REGENERATED_APP_PATHS = [
|
|
36
|
+
NAV_CONFIG_PATH,
|
|
37
|
+
SITE_CHROME_PATH,
|
|
38
|
+
SITE_FONTS_PATH,
|
|
39
|
+
PAID_SECTIONS_PATH,
|
|
40
|
+
...SPOKE_SEAM_FILES,
|
|
41
|
+
];
|
|
42
|
+
const REGENERATED_SET = new Set(REGENERATED_APP_PATHS);
|
|
43
|
+
const RECORDABLE_APP_ROOT = /^apps\/(web|api|mcp)\//;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a destination path (`/`-separated, as `templateDestination`
|
|
46
|
+
* returns it) is one `update` ever records or touches (#8522 D2): under one
|
|
47
|
+
* of the three app trees, not a `package.json`, not an `.env*` file, no
|
|
48
|
+
* `.claude` segment, and not a path a generator rewrites after the copy.
|
|
49
|
+
*/
|
|
50
|
+
export function isRecordable(destRel) {
|
|
51
|
+
if (!RECORDABLE_APP_ROOT.test(destRel))
|
|
52
|
+
return false;
|
|
53
|
+
const segments = destRel.split("/");
|
|
54
|
+
const base = segments[segments.length - 1] ?? "";
|
|
55
|
+
if (base === "package.json")
|
|
56
|
+
return false;
|
|
57
|
+
if (base.startsWith(".env"))
|
|
58
|
+
return false;
|
|
59
|
+
if (segments.includes(".claude"))
|
|
60
|
+
return false;
|
|
61
|
+
if (REGENERATED_SET.has(destRel))
|
|
62
|
+
return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/** The hex sha256 digest of `bytes` — the one hash every record row uses. */
|
|
66
|
+
export function sha256Hex(bytes) {
|
|
67
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
68
|
+
}
|
|
69
|
+
/** Thrown by `parseTemplateRecord` on a malformed or unsafe record. */
|
|
70
|
+
export class TemplateRecordError extends Error {
|
|
71
|
+
constructor(message) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "TemplateRecordError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
77
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
78
|
+
/**
|
|
79
|
+
* Whether `key` is traversal-safe: no `..`, no empty segment, no backslash,
|
|
80
|
+
* no leading `/` (D1). This alone does not make `key` a path `update` may
|
|
81
|
+
* touch — `parseTemplateRecord` also requires `isRecordable(key)`, so a
|
|
82
|
+
* tampered record can never direct a write or delete outside the three app
|
|
83
|
+
* trees nor at a non-recordable path (`package.json`, `.env*`, a
|
|
84
|
+
* `REGENERATED_APP_PATHS` entry, …) inside them.
|
|
85
|
+
*/
|
|
86
|
+
function isSafeRecordKey(key) {
|
|
87
|
+
if (key.length === 0 || key.startsWith("/") || key.includes("\\")) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const segments = key.split("/");
|
|
91
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parse `.ailk-template-files.json` text, refusing — naming the field — on:
|
|
95
|
+
* JSON that does not parse; `schema !== 1`; an unknown top-level key; an
|
|
96
|
+
* empty `projectName`; a `files` key that is not a recordable path; a
|
|
97
|
+
* `sha256` that is not 64 lowercase hex; a `version` that is not a semver
|
|
98
|
+
* string (#8522 D1).
|
|
99
|
+
*/
|
|
100
|
+
export function parseTemplateRecord(text) {
|
|
101
|
+
let doc;
|
|
102
|
+
try {
|
|
103
|
+
doc = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} is not valid JSON: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
109
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} is not a JSON object`);
|
|
110
|
+
}
|
|
111
|
+
const record = doc;
|
|
112
|
+
const allowed = new Set(["schema", "projectName", "files"]);
|
|
113
|
+
for (const key of Object.keys(record)) {
|
|
114
|
+
if (!allowed.has(key)) {
|
|
115
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} has an unknown top-level key "${key}"`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (record["schema"] !== 1) {
|
|
119
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "schema" must be 1, got ${JSON.stringify(record["schema"])}`);
|
|
120
|
+
}
|
|
121
|
+
const projectName = record["projectName"];
|
|
122
|
+
if (typeof projectName !== "string" || projectName.length === 0) {
|
|
123
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "projectName" must be a non-empty string`);
|
|
124
|
+
}
|
|
125
|
+
const filesRaw = record["files"];
|
|
126
|
+
if (typeof filesRaw !== "object" || filesRaw === null || Array.isArray(filesRaw)) {
|
|
127
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files" must be an object`);
|
|
128
|
+
}
|
|
129
|
+
const files = {};
|
|
130
|
+
for (const [key, value] of Object.entries(filesRaw)) {
|
|
131
|
+
if (!isSafeRecordKey(key)) {
|
|
132
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files" has an unsafe key "${key}"`);
|
|
133
|
+
}
|
|
134
|
+
if (!isRecordable(key)) {
|
|
135
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files" has a key "${key}" that is not a recordable path`);
|
|
136
|
+
}
|
|
137
|
+
if (typeof value !== "object" || value === null) {
|
|
138
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files"["${key}"] must be an object`);
|
|
139
|
+
}
|
|
140
|
+
const row = value;
|
|
141
|
+
const sha = row["sha256"];
|
|
142
|
+
if (typeof sha !== "string" || !SHA256_PATTERN.test(sha)) {
|
|
143
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files"["${key}"].sha256 must be 64 lowercase hex characters`);
|
|
144
|
+
}
|
|
145
|
+
const version = row["version"];
|
|
146
|
+
if (typeof version !== "string" || !VERSION_PATTERN.test(version)) {
|
|
147
|
+
throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files"["${key}"].version must be a semver string`);
|
|
148
|
+
}
|
|
149
|
+
files[key] = { sha256: sha, version };
|
|
150
|
+
}
|
|
151
|
+
return { schema: 1, projectName, files };
|
|
152
|
+
}
|
|
153
|
+
/** `record` as `scaffold()` and `update` write it: sorted keys, trailing newline. */
|
|
154
|
+
export function serializeTemplateRecord(record) {
|
|
155
|
+
const files = {};
|
|
156
|
+
for (const key of Object.keys(record.files).sort()) {
|
|
157
|
+
files[key] = record.files[key];
|
|
158
|
+
}
|
|
159
|
+
return (JSON.stringify({ schema: record.schema, projectName: record.projectName, files }, null, 2) +
|
|
160
|
+
"\n");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The record a fresh scaffold writes (#8522 D2): one row per copied path
|
|
164
|
+
* whose file still exists and whose current bytes still hash to the copy —
|
|
165
|
+
* a later layer (a module bundle's patch, a generator) that rewrote the
|
|
166
|
+
* file drops it from the record, exactly as it is absent from the record a
|
|
167
|
+
* `create-ailk update` keeps current for it.
|
|
168
|
+
*/
|
|
169
|
+
export function planTemplateRecord(opts) {
|
|
170
|
+
const files = {};
|
|
171
|
+
for (const path of [...opts.copied.keys()].sort()) {
|
|
172
|
+
const hash = opts.copied.get(path);
|
|
173
|
+
if (hash === undefined)
|
|
174
|
+
continue;
|
|
175
|
+
const abs = join(opts.targetDir, ...path.split("/"));
|
|
176
|
+
if (!existsSync(abs))
|
|
177
|
+
continue;
|
|
178
|
+
if (sha256Hex(readFileSync(abs)) !== hash)
|
|
179
|
+
continue;
|
|
180
|
+
files[path] = { sha256: hash, version: opts.version };
|
|
181
|
+
}
|
|
182
|
+
return { schema: 1, projectName: opts.projectName, files };
|
|
183
|
+
}
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
2
|
+
export declare class UpdateRefusedError extends Error {
|
|
3
|
+
readonly exitCode = 2;
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export interface UpdateSiteOptions {
|
|
7
|
+
/** The existing site. */
|
|
8
|
+
targetDir: string;
|
|
9
|
+
/** The running package's own `templates/` directory. */
|
|
10
|
+
templatesDir: string;
|
|
11
|
+
/** The running create-ailk version. */
|
|
12
|
+
version: string;
|
|
13
|
+
/** Overrides `AILK_NPM_REGISTRY_URL` / the public npm registry default. */
|
|
14
|
+
registryUrl?: string;
|
|
15
|
+
/** Injected fetch for tests. Never a live network call in CI. */
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
}
|
|
18
|
+
export interface UpdateResult {
|
|
19
|
+
targetDir: string;
|
|
20
|
+
version: string;
|
|
21
|
+
updated: string[];
|
|
22
|
+
merged: string[];
|
|
23
|
+
added: string[];
|
|
24
|
+
removed: string[];
|
|
25
|
+
current: string[];
|
|
26
|
+
review: Array<{
|
|
27
|
+
path: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}>;
|
|
30
|
+
skipped: Array<{
|
|
31
|
+
path: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Per file left under review or skipped, the template tests importing it that stay at the site's version (#8704). */
|
|
35
|
+
heldBack: Array<{
|
|
36
|
+
file: string;
|
|
37
|
+
tests: string[];
|
|
38
|
+
}>;
|
|
39
|
+
backfilled?: {
|
|
40
|
+
version: string;
|
|
41
|
+
files: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function updateSite(opts: UpdateSiteOptions): Promise<UpdateResult>;
|
|
45
|
+
/** The printed report (D9): one section per non-empty list, in order. */
|
|
46
|
+
export declare function formatUpdateReport(result: UpdateResult): string;
|
|
47
|
+
export interface UpdateArgs {
|
|
48
|
+
target: string | undefined;
|
|
49
|
+
help: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file whose template copy changed is\noverwritten. An edited file whose template copy also changed is three-way\nmerged with `git` (git must be on PATH); a conflict leaves the file alone\nand is listed under \"Needs review\". Every template test that imports a file\nleft under \"Needs review\" or \"Skipped\" (by a relative import) is held at the\nsite's version with it, its record entry is not advanced, and it is listed\nunder \"Held back with <file>\" \u2014 reconcile the file, then run update again to\nbring its tests forward. Every other file in the run still updates.\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. Commit before running, and review\nthe diff after.\n";
|
|
52
|
+
/** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
|
|
53
|
+
export declare function parseUpdateArgs(argv: string[]): UpdateArgs;
|