create-ailk 0.27.0 → 0.28.1
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/dist/add-module-core.js +2 -1
- package/dist/add-module.d.ts +2 -1
- package/dist/add-module.js +9 -4
- package/dist/add-route.js +46 -19
- package/dist/body-heading.d.ts +56 -0
- package/dist/body-heading.js +394 -0
- package/dist/cli.js +28 -1
- package/dist/content-payloads.js +2 -2
- package/dist/derive-architecture.js +8 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/lib/auth-secret-file.d.ts +6 -2
- package/dist/lib/auth-secret-file.js +46 -20
- package/dist/lib/chrome-readers.d.ts +21 -5
- package/dist/lib/chrome-readers.js +128 -20
- package/dist/lib/declared-env.d.ts +6 -0
- package/dist/lib/declared-env.js +2 -0
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/import-graph.d.ts +16 -0
- package/dist/lib/import-graph.js +53 -0
- package/dist/lib/site-modules.d.ts +8 -0
- package/dist/lib/site-modules.js +34 -0
- package/dist/module-architecture.js +23 -14
- package/dist/paid-sections.d.ts +17 -0
- package/dist/paid-sections.js +39 -0
- package/dist/programmatic.d.ts +16 -0
- package/dist/programmatic.js +73 -4
- package/dist/remove-module.d.ts +98 -0
- package/dist/remove-module.js +620 -0
- package/dist/seed-flows.d.ts +2 -2
- package/dist/seed-flows.js +16 -9
- package/dist/site-roster.js +4 -1
- package/dist/starter-grid.d.ts +26 -0
- package/dist/starter-grid.js +34 -0
- package/dist/surfaces.d.ts +26 -0
- package/dist/surfaces.js +105 -9
- package/dist/sync-routes.d.ts +7 -0
- package/dist/sync-routes.js +53 -6
- package/dist/template-pages.d.ts +36 -0
- package/dist/template-pages.js +129 -0
- package/dist/template-record.d.ts +13 -4
- package/dist/template-record.js +8 -4
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +34 -2
- package/dist/update.js +395 -48
- package/package.json +3 -3
- package/release-scope.json +2 -1
- package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
- package/templates/.env.example +5 -2
- package/templates/apps/api/.env.example +9 -5
- package/templates/apps/api/CLAUDE.md +14 -9
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
- package/templates/apps/api/src/vercel-handler.ts +8 -1
- package/templates/apps/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +25 -11
- package/templates/apps/web/CLAUDE.md +45 -4
- package/templates/apps/web/__tests__/setup.ts +7 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
- package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
- package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/layout.tsx +157 -139
- package/templates/apps/web/app/[locale]/page.tsx +51 -3
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
- package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
- package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
- package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
- package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
- package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +224 -0
- package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
- package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
- package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +237 -0
- package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +204 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +151 -33
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
- package/templates/apps/web/app/llms.txt/route.ts +41 -3
- package/templates/apps/web/app/robots.txt/route.ts +5 -1
- package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
- package/templates/apps/web/auth.ts +102 -50
- package/templates/apps/web/components/signed-in-nav.tsx +8 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
- package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
- package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +279 -0
- package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
- package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +202 -10
- package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
- package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
- package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
- package/templates/apps/web/lib/auth-secret.ts +57 -0
- package/templates/apps/web/lib/bot-check.ts +138 -0
- package/templates/apps/web/lib/consent-visitor.ts +81 -7
- package/templates/apps/web/lib/deck-page.ts +6 -3
- package/templates/apps/web/lib/docs-index.ts +100 -0
- package/templates/apps/web/lib/docs-path.ts +94 -0
- package/templates/apps/web/lib/hub-children.tsx +318 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/nav-label-check.ts +61 -0
- package/templates/apps/web/lib/nav-label-keys.ts +78 -0
- package/templates/apps/web/lib/newsletter.actions.ts +31 -6
- package/templates/apps/web/lib/offerings-pricing.ts +135 -6
- package/templates/apps/web/lib/page-chrome.ts +367 -0
- package/templates/apps/web/lib/posting-title.ts +26 -4
- package/templates/apps/web/lib/pricing-page.ts +430 -0
- package/templates/apps/web/lib/relay.ts +31 -106
- package/templates/apps/web/lib/route-family.ts +69 -0
- package/templates/apps/web/lib/service-listing.ts +89 -0
- package/templates/apps/web/lib/services-path.ts +97 -0
- package/templates/apps/web/lib/site-header.ts +94 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
- package/templates/apps/web/lib/template-listing.ts +169 -0
- package/templates/apps/web/messages/en.json +86 -30
- package/templates/apps/web/middleware.ts +171 -11
- package/templates/apps/web/next.config.mjs +211 -2
- package/templates/apps/web/package.json +1 -1
- package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
- package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
- package/templates/apps/web/sentry.server.config.ts +30 -2
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
- package/templates/apps/web/src/lib/rate-limit.ts +68 -27
- package/templates/apps/web/src/lib/routes.ts +15 -1
- package/templates/tools/declared-env.ts +7 -0
- package/versions.json +48 -48
package/dist/update.js
CHANGED
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
* version's copy — read from this package's own `templates/` when that is
|
|
11
11
|
* the running version, else re-materialised from the published npm tarball
|
|
12
12
|
* (`lib/fetch-template-snapshot.ts`) — and the running version's copy. A
|
|
13
|
-
* conflict leaves the file untouched and is listed under "Needs review"
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* conflict leaves the file untouched and is listed under "Needs review" — but
|
|
14
|
+
* a `apps/web/messages/` catalog takes the template's new keys and keeps the
|
|
15
|
+
* site's values, reviewed only on a key-level conflict (#10173). Every
|
|
16
|
+
* template file this run would write that imports, transitively, a file left
|
|
17
|
+
* under review or skipped — or a file the site will not have — is held at the
|
|
18
|
+
* site's version with it, so a run never writes code newer than what it
|
|
19
|
+
* imports (#8704 for tests, #10173 for the rest). The env vars the written
|
|
20
|
+
* files read gain `project.yaml` `env_contract` rows (#10172), and a record
|
|
21
|
+
* row at a path no longer recorded is dropped, not refused (#10173). A site
|
|
22
|
+
* with no record gets one backfilled from
|
|
17
23
|
* `.ailk-site-manifest.json`'s `patternVersion` on its first run — one row
|
|
18
24
|
* per in-scope path of that version's snapshot, on disk or not (#9527) —
|
|
19
25
|
* then updates normally. A site scaffolded before `@working-theory/mcp-server`
|
|
@@ -27,24 +33,32 @@
|
|
|
27
33
|
* (#9566): the record retires the copied server files and lands the boot
|
|
28
34
|
* files; the app's rename, its dependencies and delegating scripts, and the
|
|
29
35
|
* bundle-delivered module files are planned here. Every refusal throws
|
|
30
|
-
* before the first write.
|
|
36
|
+
* before the first write. Last, every mdx-body page in the site's `content/`
|
|
37
|
+
* with no `# ` heading gets its title and description written into its body
|
|
38
|
+
* (#7495, `body-heading.ts`); a page with one or more headings is never
|
|
39
|
+
* touched, and `content/` is never recorded.
|
|
31
40
|
*
|
|
32
41
|
* Pure module: no `import.meta.url` — the CLI supplies `templatesDir`, the
|
|
33
42
|
* `adopt.ts` / `add-route.ts` split.
|
|
34
43
|
*/
|
|
35
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync, } from "node:fs";
|
|
36
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
44
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync, writeFileSync, } from "node:fs";
|
|
45
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
37
46
|
import { parseArgs } from "node:util";
|
|
38
47
|
import { AddModuleRefusedError, readArchitectureHeader } from "./add-module-core.js";
|
|
48
|
+
import { headingViolations, mdxBodyRule, migrateBodyHeadings } from "./body-heading.js";
|
|
39
49
|
import { buildScaffoldMarkers, isExcluded, isTextFile, renderTemplateFile, templateDestination } from "./copy.js";
|
|
50
|
+
import { EnvContractError, planEnvRows, readEnvContract, readManifestTopology, } from "./lib/declared-env.js";
|
|
51
|
+
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
40
52
|
import { fetchTemplateSnapshot } from "./lib/fetch-template-snapshot.js";
|
|
53
|
+
import { exportsName, listFilesUnder, namedImportsOf } from "./lib/import-graph.js";
|
|
41
54
|
import { mergeThreeWay } from "./lib/merge-three-way.js";
|
|
55
|
+
import { publicTreeRemoved } from "./lib/site-modules.js";
|
|
42
56
|
import { MODULE_SLICES } from "./module-architecture.js";
|
|
43
57
|
import { MODULE_FLAGS_PATH, PAID_MCP_TOOLS_PATH, writeModuleFlags, writePaidMcpTools, } from "./paid-sections.js";
|
|
44
58
|
import { ParseError } from "./parse-args.js";
|
|
45
59
|
import { SITE_ROSTER_PATH } from "./site-roster.js";
|
|
46
60
|
import { SPOKE_CHECKOUT_TEMPLATE_PATHS } from "./spoke-checkout.js";
|
|
47
|
-
import { excludedRoutePaths } from "./surfaces.js";
|
|
61
|
+
import { excludedRoutePaths, isRemovedPublicRoutePath } from "./surfaces.js";
|
|
48
62
|
import { DATABASE_BOOT_FILES, ROOT_TOOL_FILES, TEMPLATE_RECORD_PATH, isRecordable, parseTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
|
|
49
63
|
import { MCP_SEAM_PATH, MCP_SERVER_HUB, TopologyError, isSpokeExcluded, readSpokeShape, spokeExclusions, } from "./topology.js";
|
|
50
64
|
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
@@ -165,6 +179,7 @@ function containsKnownPath(prefix, knownPaths) {
|
|
|
165
179
|
const TEST_FILE = /(^|\/)__tests__\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
166
180
|
/** Module specifiers `import`/`export … from`, `import()`, `require()` and `jest.mock()` name. */
|
|
167
181
|
const IMPORT_SPECIFIER = /\bfrom\s*["']([^"']+)["']|\bimport\s*["']([^"']+)["']|\b(?:import|require|jest\.mock|jest\.requireActual)\s*\(\s*["']([^"']+)["']/g;
|
|
182
|
+
const WEB_ROOT = "apps/web/";
|
|
168
183
|
const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
169
184
|
/**
|
|
170
185
|
* The record-style paths a relative specifier in `fromPath` can resolve to:
|
|
@@ -172,8 +187,10 @@ const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs"
|
|
|
172
187
|
* swapped for its TypeScript source, and as a directory's `index.*`.
|
|
173
188
|
*/
|
|
174
189
|
function relativeImportCandidates(fromPath, specifier) {
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
// `@/x` is apps/web's `x` — its tsconfig `paths`; no other app declares one.
|
|
191
|
+
const alias = specifier.startsWith("@/") && fromPath.startsWith(WEB_ROOT);
|
|
192
|
+
const parts = alias ? ["apps", "web"] : fromPath.split("/").slice(0, -1);
|
|
193
|
+
for (const segment of (alias ? specifier.slice(2) : specifier).split("/")) {
|
|
177
194
|
if (segment === "" || segment === ".")
|
|
178
195
|
continue;
|
|
179
196
|
if (segment === "..")
|
|
@@ -191,13 +208,24 @@ function relativeImportCandidates(fromPath, specifier) {
|
|
|
191
208
|
out.push(...RESOLVE_EXTENSIONS.map((ext) => `${target}/index${ext}`));
|
|
192
209
|
return out;
|
|
193
210
|
}
|
|
194
|
-
/**
|
|
195
|
-
function
|
|
211
|
+
/** Whether `specifier` names a site file: relative, or `@/` from an apps/web file. */
|
|
212
|
+
function isLocal(fromPath, specifier) {
|
|
213
|
+
return specifier.startsWith(".") || (specifier.startsWith("@/") && fromPath.startsWith(WEB_ROOT));
|
|
214
|
+
}
|
|
215
|
+
/** The local module specifiers `text` names. */
|
|
216
|
+
function localSpecifiers(fromPath, text) {
|
|
196
217
|
const out = new Set();
|
|
197
218
|
for (const match of text.matchAll(IMPORT_SPECIFIER)) {
|
|
198
219
|
const specifier = match[1] ?? match[2] ?? match[3];
|
|
199
|
-
if (specifier
|
|
200
|
-
|
|
220
|
+
if (specifier !== undefined && isLocal(fromPath, specifier))
|
|
221
|
+
out.add(specifier);
|
|
222
|
+
}
|
|
223
|
+
return [...out];
|
|
224
|
+
}
|
|
225
|
+
/** The files of `held` that `text`, the source of `fromPath`, imports by local specifier. */
|
|
226
|
+
function importedHeldFiles(fromPath, text, held) {
|
|
227
|
+
const out = new Set();
|
|
228
|
+
for (const specifier of localSpecifiers(fromPath, text)) {
|
|
201
229
|
for (const candidate of relativeImportCandidates(fromPath, specifier)) {
|
|
202
230
|
if (held.has(candidate))
|
|
203
231
|
out.add(candidate);
|
|
@@ -205,6 +233,57 @@ function importedHeldFiles(fromPath, text, held) {
|
|
|
205
233
|
}
|
|
206
234
|
return [...out];
|
|
207
235
|
}
|
|
236
|
+
/** The `apps/web/messages/` catalogs next-intl loads by locale, never by import. */
|
|
237
|
+
const MESSAGES_JSON = /^apps\/web\/messages\/(?:[^/]+\/)*[^/]+\.json$/;
|
|
238
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
239
|
+
function isPlainObject(value) {
|
|
240
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The site's messages catalog with every key the template's copy has and it
|
|
244
|
+
* lacks (#10173): the site's values always win, nothing is removed, and the
|
|
245
|
+
* keys it gains follow its own. A key that is an object on one side and a
|
|
246
|
+
* value on the other is a conflict, and nothing is merged.
|
|
247
|
+
*/
|
|
248
|
+
export function mergeMessagesAdditively(ours, theirs) {
|
|
249
|
+
let oursDoc;
|
|
250
|
+
let theirsDoc;
|
|
251
|
+
try {
|
|
252
|
+
oursDoc = JSON.parse(ours);
|
|
253
|
+
theirsDoc = JSON.parse(theirs);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
return { status: "invalid", reason: `not JSON: ${err.message}` };
|
|
257
|
+
}
|
|
258
|
+
if (!isPlainObject(oursDoc) || !isPlainObject(theirsDoc)) {
|
|
259
|
+
return { status: "invalid", reason: "the root is not an object" };
|
|
260
|
+
}
|
|
261
|
+
const conflicts = [];
|
|
262
|
+
let added = 0;
|
|
263
|
+
const walk = (o, t, at) => {
|
|
264
|
+
for (const [key, value] of Object.entries(t)) {
|
|
265
|
+
if (UNSAFE_KEYS.has(key))
|
|
266
|
+
continue; // never written through, so never a prototype change
|
|
267
|
+
const path = at === "" ? key : `${at}.${key}`;
|
|
268
|
+
if (!Object.prototype.hasOwnProperty.call(o, key)) {
|
|
269
|
+
o[key] = value;
|
|
270
|
+
added++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const mine = o[key];
|
|
274
|
+
if (isPlainObject(mine) && isPlainObject(value))
|
|
275
|
+
walk(mine, value, path);
|
|
276
|
+
else if (isPlainObject(mine) || isPlainObject(value))
|
|
277
|
+
conflicts.push(path);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
walk(oursDoc, theirsDoc, "");
|
|
281
|
+
if (conflicts.length > 0)
|
|
282
|
+
return { status: "conflict", keys: conflicts };
|
|
283
|
+
const indent = /\n(\t| +)"/.exec(ours)?.[1] ?? 2;
|
|
284
|
+
const text = JSON.stringify(oursDoc, null, indent) + (ours.endsWith("\n") ? "\n" : "");
|
|
285
|
+
return { status: "merged", text: added === 0 ? ours : text, added };
|
|
286
|
+
}
|
|
208
287
|
// ── The MCP move (#9567): a copied apps/mcp becomes the boot set ──────────
|
|
209
288
|
const MCP_SERVER_PACKAGE = "@working-theory/mcp-server";
|
|
210
289
|
const MCP_PACKAGE_JSON = "apps/mcp/package.json";
|
|
@@ -784,9 +863,14 @@ export async function updateSite(opts) {
|
|
|
784
863
|
const recordPath = join(targetDir, TEMPLATE_RECORD_PATH);
|
|
785
864
|
let record;
|
|
786
865
|
let backfillPatternVersion;
|
|
866
|
+
// #10173 — a row at a path a later create-ailk no longer records (a
|
|
867
|
+
// generated file) is dropped and reported, never a refusal of the run.
|
|
868
|
+
const droppedRows = [];
|
|
787
869
|
if (existsSync(recordPath)) {
|
|
788
870
|
try {
|
|
789
|
-
record = parseTemplateRecord(readFileSync(recordPath, "utf8")
|
|
871
|
+
record = parseTemplateRecord(readFileSync(recordPath, "utf8"), {
|
|
872
|
+
onUnrecordable: (key) => droppedRows.push(key),
|
|
873
|
+
});
|
|
790
874
|
}
|
|
791
875
|
catch (err) {
|
|
792
876
|
throw new UpdateRefusedError(err.message);
|
|
@@ -1076,6 +1160,26 @@ export async function updateSite(opts) {
|
|
|
1076
1160
|
nextFiles[path] = { sha256: newHash, version };
|
|
1077
1161
|
merged.push(path);
|
|
1078
1162
|
}
|
|
1163
|
+
else if (mergeResult.status === "conflict" && MESSAGES_JSON.test(path)) {
|
|
1164
|
+
// #10173 — a messages catalog takes the template's new keys and keeps
|
|
1165
|
+
// every value the site has; only a key-level conflict is reviewed.
|
|
1166
|
+
const additive = mergeMessagesAdditively(diskBytes.toString("utf8"), Buffer.from(newBytes).toString("utf8"));
|
|
1167
|
+
if (additive.status === "conflict") {
|
|
1168
|
+
review.push({
|
|
1169
|
+
path,
|
|
1170
|
+
reason: `conflicts with the template change ${rec.version} → ${version} at key ${additive.keys.join(", ")}`,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
else if (additive.status === "invalid") {
|
|
1174
|
+
review.push({ path, reason: `the additive pass could not run: ${additive.reason}` });
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
if (additive.added > 0)
|
|
1178
|
+
writes.push({ path, bytes: Buffer.from(additive.text, "utf8") });
|
|
1179
|
+
nextFiles[path] = { sha256: newHash, version };
|
|
1180
|
+
merged.push(path);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1079
1183
|
else if (mergeResult.status === "conflict") {
|
|
1080
1184
|
review.push({
|
|
1081
1185
|
path,
|
|
@@ -1091,10 +1195,17 @@ export async function updateSite(opts) {
|
|
|
1091
1195
|
// module object at a boot-set path goes before the writes below, and the
|
|
1092
1196
|
// template's re-export lands there as an added file.
|
|
1093
1197
|
const retired = new Set(apiMove?.deletes ?? []);
|
|
1198
|
+
// #10040 D10 — a site that dropped `marketing` never gains a public route
|
|
1199
|
+
// file from a newer template, tombstone or not.
|
|
1200
|
+
const marketingRemoved = publicTreeRemoved(targetDir);
|
|
1094
1201
|
const knownPaths = new Set([...Object.keys(record.files), ...(knownSnapshotPaths ?? [])]);
|
|
1095
1202
|
for (const path of [...newMap.keys()].sort()) {
|
|
1096
1203
|
if (Object.prototype.hasOwnProperty.call(record.files, path))
|
|
1097
1204
|
continue;
|
|
1205
|
+
if (marketingRemoved && isRemovedPublicRoutePath(path)) {
|
|
1206
|
+
skipped.push({ path, reason: "marketing removed" });
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1098
1209
|
// The guard runs before the disk check, so a full-copy site's boot path
|
|
1099
1210
|
// reads as guarded, never as "exists, not in the record" (#9569 DD8).
|
|
1100
1211
|
if (bootFileGuarded(path)) {
|
|
@@ -1139,46 +1250,209 @@ export async function updateSite(opts) {
|
|
|
1139
1250
|
// exercising the held file is enough. A held test-support file
|
|
1140
1251
|
// (`__tests__/fixtures.ts`) holds the tests importing it in turn, until no
|
|
1141
1252
|
// further test is held.
|
|
1253
|
+
//
|
|
1254
|
+
// #10173 — the same holds for every other template file this run would
|
|
1255
|
+
// write: one importing a held file stays at the site's version (or, when
|
|
1256
|
+
// new, is not added), so the written tree never imports an export the held
|
|
1257
|
+
// file lacks. A written file whose relative import names a file the site
|
|
1258
|
+
// will not have once the run lands is held too, under that missing path.
|
|
1259
|
+
// Only the bytes being written are read for a non-test file: the site's
|
|
1260
|
+
// own copy already compiles against what it has.
|
|
1142
1261
|
const heldFiles = new Set([...review, ...skipped].map((row) => row.path));
|
|
1262
|
+
// The files left alone outright, before any hold-back joins them.
|
|
1263
|
+
const leftAlone = new Set(heldFiles);
|
|
1264
|
+
const writtenBytes = new Map(writes.map((w) => [w.path, w.bytes]));
|
|
1265
|
+
// The moves' removals (#9567, #9566) land too: never code that stays.
|
|
1266
|
+
const deleting = new Set([...deletes, ...(mcpMove?.deletes ?? []), ...(apiMove?.deletes ?? [])]);
|
|
1267
|
+
const adding = new Set(added);
|
|
1143
1268
|
const heldBackByFile = new Map();
|
|
1144
|
-
const
|
|
1145
|
-
const moving = [...updated, ...merged, ...added, ...removed, ...current]
|
|
1146
|
-
|
|
1269
|
+
const heldPaths = new Set();
|
|
1270
|
+
const moving = [...new Set([...updated, ...merged, ...added, ...removed, ...current])].sort();
|
|
1271
|
+
const isFileCache = new Map();
|
|
1272
|
+
const isFileOnDisk = (path) => {
|
|
1273
|
+
let hit = isFileCache.get(path);
|
|
1274
|
+
if (hit === undefined) {
|
|
1275
|
+
try {
|
|
1276
|
+
hit = statSync(join(targetDir, ...path.split("/"))).isFile();
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
hit = false;
|
|
1280
|
+
}
|
|
1281
|
+
isFileCache.set(path, hit);
|
|
1282
|
+
}
|
|
1283
|
+
return hit;
|
|
1284
|
+
};
|
|
1285
|
+
// What the run rewrites after the record's writes, never from the template.
|
|
1286
|
+
const rewrittenLater = new Set([
|
|
1287
|
+
MODULE_FLAGS_PATH,
|
|
1288
|
+
PAID_MCP_TOOLS_PATH,
|
|
1289
|
+
...(mcpMove?.seam !== undefined ? [MCP_SEAM_PATH] : []),
|
|
1290
|
+
]);
|
|
1291
|
+
/** Whether `path` is a file once this run's writes and deletes, less the held ones, land. */
|
|
1292
|
+
const presentAfter = (path) => {
|
|
1293
|
+
if (heldPaths.has(path))
|
|
1294
|
+
return isFileOnDisk(path);
|
|
1295
|
+
if (adding.has(path) || rewrittenLater.has(path))
|
|
1296
|
+
return true;
|
|
1297
|
+
if (deleting.has(path))
|
|
1298
|
+
return false;
|
|
1299
|
+
return isFileOnDisk(path);
|
|
1300
|
+
};
|
|
1301
|
+
const sourcesOf = new Map();
|
|
1302
|
+
for (const path of moving) {
|
|
1303
|
+
const writing = writtenBytes.get(path);
|
|
1304
|
+
const written = writing !== undefined ? writing.toString("utf8") : undefined;
|
|
1305
|
+
const newBytes = newMap.get(path);
|
|
1306
|
+
const newText = newBytes !== undefined ? Buffer.from(newBytes).toString("utf8") : undefined;
|
|
1307
|
+
const all = [];
|
|
1308
|
+
if (TEST_FILE.test(path)) {
|
|
1309
|
+
const abs = join(targetDir, ...path.split("/"));
|
|
1310
|
+
if (existsSync(abs))
|
|
1311
|
+
all.push(readFileSync(abs, "utf8"));
|
|
1312
|
+
if (newText !== undefined)
|
|
1313
|
+
all.push(newText);
|
|
1314
|
+
if (written !== undefined)
|
|
1315
|
+
all.push(written);
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
const text = written ?? newText;
|
|
1319
|
+
if (text !== undefined)
|
|
1320
|
+
all.push(text);
|
|
1321
|
+
}
|
|
1322
|
+
sourcesOf.set(path, { all, written });
|
|
1323
|
+
}
|
|
1324
|
+
const SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
1325
|
+
const hold = (path, file, missing) => {
|
|
1326
|
+
const entry = heldBackByFile.get(file) ?? { tests: [], importers: [], missing };
|
|
1327
|
+
const list = TEST_FILE.test(path) ? entry.tests : entry.importers;
|
|
1328
|
+
if (!list.includes(path))
|
|
1329
|
+
list.push(path);
|
|
1330
|
+
heldBackByFile.set(file, entry);
|
|
1331
|
+
};
|
|
1332
|
+
const importsCache = new Map();
|
|
1333
|
+
const namedImportsAt = (path, text) => {
|
|
1334
|
+
let hit = importsCache.get(path);
|
|
1335
|
+
if (hit === undefined)
|
|
1336
|
+
importsCache.set(path, (hit = namedImportsOf(text)));
|
|
1337
|
+
return hit;
|
|
1338
|
+
};
|
|
1339
|
+
const textCache = new Map();
|
|
1340
|
+
const diskText = (path) => {
|
|
1341
|
+
if (!textCache.has(path)) {
|
|
1342
|
+
textCache.set(path, isFileOnDisk(path) ? readFileSync(join(targetDir, ...path.split("/")), "utf8") : undefined);
|
|
1343
|
+
}
|
|
1344
|
+
return textCache.get(path);
|
|
1345
|
+
};
|
|
1346
|
+
// Every app source file this run does not write: the code that keeps the
|
|
1347
|
+
// site's version and must still find what it imports.
|
|
1348
|
+
const stayingFiles = [...listFilesUnder(targetDir, ["apps/web", "apps/api", "apps/mcp"])]
|
|
1349
|
+
.filter((path) => !writtenBytes.has(path) && !deleting.has(path))
|
|
1147
1350
|
.sort();
|
|
1148
|
-
|
|
1351
|
+
let grew = true;
|
|
1352
|
+
const holdPath = (path) => {
|
|
1353
|
+
heldPaths.add(path);
|
|
1354
|
+
heldFiles.add(path);
|
|
1355
|
+
grew = true;
|
|
1356
|
+
};
|
|
1357
|
+
while (grew) {
|
|
1149
1358
|
grew = false;
|
|
1150
1359
|
for (const path of moving) {
|
|
1151
|
-
if (
|
|
1360
|
+
if (heldPaths.has(path))
|
|
1152
1361
|
continue;
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1362
|
+
const { all, written } = sourcesOf.get(path);
|
|
1363
|
+
const imported = new Set(all.flatMap((text) => importedHeldFiles(path, text, heldFiles)));
|
|
1364
|
+
const missing = new Set();
|
|
1365
|
+
if (written !== undefined && !TEST_FILE.test(path)) {
|
|
1366
|
+
for (const specifier of localSpecifiers(path, written)) {
|
|
1367
|
+
const candidates = relativeImportCandidates(path, specifier);
|
|
1368
|
+
if (candidates.some((c) => heldFiles.has(c) || presentAfter(c)))
|
|
1369
|
+
continue;
|
|
1370
|
+
// A dot-directory (`.source/`, `.next/`) is build output the site
|
|
1371
|
+
// generates, never a file update could have carried.
|
|
1372
|
+
if (/(^|\/)\.[^./][^/]*\//.test(candidates[0]))
|
|
1373
|
+
continue;
|
|
1374
|
+
missing.add(candidates[0]);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
// A named import the file it resolves to — staying at the site's
|
|
1378
|
+
// version — does not export (a regenerated nav-config, say).
|
|
1379
|
+
const lacking = new Set();
|
|
1380
|
+
if (written !== undefined) {
|
|
1381
|
+
for (const { specifier, names } of namedImportsOf(written)) {
|
|
1382
|
+
if (!isLocal(path, specifier))
|
|
1383
|
+
continue;
|
|
1384
|
+
const target = relativeImportCandidates(path, specifier).find(isFileOnDisk);
|
|
1385
|
+
if (target === undefined || heldFiles.has(target) || rewrittenLater.has(target))
|
|
1386
|
+
continue;
|
|
1387
|
+
if (writtenBytes.has(target) && !heldPaths.has(target))
|
|
1388
|
+
continue;
|
|
1389
|
+
if (deleting.has(target))
|
|
1390
|
+
continue; // the missing check above owns it
|
|
1391
|
+
if (!SOURCE_FILE.test(target))
|
|
1392
|
+
continue;
|
|
1393
|
+
const text = diskText(target);
|
|
1394
|
+
if (text !== undefined && names.some((name) => !exportsName(text, name)))
|
|
1395
|
+
lacking.add(target);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (imported.size === 0 && missing.size === 0 && lacking.size === 0)
|
|
1399
|
+
continue;
|
|
1400
|
+
holdPath(path);
|
|
1401
|
+
for (const file of imported)
|
|
1402
|
+
hold(path, file, false);
|
|
1403
|
+
for (const file of missing)
|
|
1404
|
+
hold(path, file, true);
|
|
1405
|
+
for (const file of lacking)
|
|
1406
|
+
hold(path, file, false);
|
|
1407
|
+
}
|
|
1408
|
+
// The reverse: a file staying at the site's version imports, by name,
|
|
1409
|
+
// something the written copy of its target no longer exports.
|
|
1410
|
+
for (const staying of [...stayingFiles, ...heldPaths]) {
|
|
1411
|
+
const text = diskText(staying);
|
|
1412
|
+
if (text === undefined)
|
|
1413
|
+
continue;
|
|
1414
|
+
// A file left alone keeps the code it was written against: every
|
|
1415
|
+
// template file it imports stays with it, whatever that file exports.
|
|
1416
|
+
if (leftAlone.has(staying)) {
|
|
1417
|
+
for (const specifier of localSpecifiers(staying, text)) {
|
|
1418
|
+
const target = relativeImportCandidates(staying, specifier).find((c) => writtenBytes.has(c) && !heldPaths.has(c));
|
|
1419
|
+
if (target === undefined || !SOURCE_FILE.test(target))
|
|
1420
|
+
continue;
|
|
1421
|
+
holdPath(target);
|
|
1422
|
+
hold(target, staying, false);
|
|
1423
|
+
}
|
|
1162
1424
|
continue;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1425
|
+
}
|
|
1426
|
+
for (const { specifier, names } of namedImportsAt(staying, text)) {
|
|
1427
|
+
if (!isLocal(staying, specifier))
|
|
1428
|
+
continue;
|
|
1429
|
+
const target = relativeImportCandidates(staying, specifier).find((c) => writtenBytes.has(c) && !heldPaths.has(c));
|
|
1430
|
+
if (target === undefined || !SOURCE_FILE.test(target))
|
|
1431
|
+
continue;
|
|
1432
|
+
const next = writtenBytes.get(target).toString("utf8");
|
|
1433
|
+
if (names.every((name) => exportsName(next, name)))
|
|
1434
|
+
continue;
|
|
1435
|
+
holdPath(target);
|
|
1436
|
+
hold(target, staying, false);
|
|
1168
1437
|
}
|
|
1169
1438
|
}
|
|
1170
1439
|
}
|
|
1171
|
-
for (const path of
|
|
1440
|
+
for (const path of heldPaths) {
|
|
1172
1441
|
const recorded = record.files[path];
|
|
1173
1442
|
if (recorded !== undefined)
|
|
1174
1443
|
nextFiles[path] = recorded;
|
|
1175
1444
|
else
|
|
1176
1445
|
delete nextFiles[path];
|
|
1177
1446
|
}
|
|
1178
|
-
const notHeld = (path) => !
|
|
1447
|
+
const notHeld = (path) => !heldPaths.has(path);
|
|
1179
1448
|
const heldBack = [...heldBackByFile.entries()]
|
|
1180
1449
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
1181
|
-
.map(([file, tests]) => ({
|
|
1450
|
+
.map(([file, { tests, importers, missing }]) => ({
|
|
1451
|
+
file,
|
|
1452
|
+
tests: tests.sort(),
|
|
1453
|
+
...(importers.length > 0 ? { importers: importers.sort() } : {}),
|
|
1454
|
+
...(missing ? { missing: true } : {}),
|
|
1455
|
+
}));
|
|
1182
1456
|
// ── Apply: writes and removals, then the record (D4 Order) ───────────
|
|
1183
1457
|
// #9566 — the API move's removals first: a retired file at a boot-set
|
|
1184
1458
|
// path is overwritten by the write that follows, never deleted after it.
|
|
@@ -1204,7 +1478,9 @@ export async function updateSite(opts) {
|
|
|
1204
1478
|
removed: removed.filter(notHeld),
|
|
1205
1479
|
current: current.filter(notHeld),
|
|
1206
1480
|
};
|
|
1207
|
-
const recordChanged = backfilled !== undefined ||
|
|
1481
|
+
const recordChanged = backfilled !== undefined ||
|
|
1482
|
+
droppedRows.length > 0 ||
|
|
1483
|
+
Object.values(moved).some((paths) => paths.length > 0);
|
|
1208
1484
|
if (recordChanged) {
|
|
1209
1485
|
writeFileSync(recordPath, serializeTemplateRecord({ schema: 1, projectName: record.projectName, files: nextFiles }));
|
|
1210
1486
|
}
|
|
@@ -1250,6 +1526,50 @@ export async function updateSite(opts) {
|
|
|
1250
1526
|
writeFileSync(join(targetDir, ...WEB_PACKAGE_JSON.split("/")), webPackageJson);
|
|
1251
1527
|
moved.updated.push(WEB_PACKAGE_JSON);
|
|
1252
1528
|
}
|
|
1529
|
+
// #10172 — the env vars the written files read get an env_contract row
|
|
1530
|
+
// when the site's project.yaml has a block and no row for them, so the
|
|
1531
|
+
// code update brings never fails `env:declared`. Rows are appended, never
|
|
1532
|
+
// edited; a hub with no block is not checked and gains none.
|
|
1533
|
+
const envRowsAdded = [];
|
|
1534
|
+
const projectYamlPath = join(targetDir, "project.yaml");
|
|
1535
|
+
const writtenPaths = new Set(writes.map((w) => w.path).filter(notHeld));
|
|
1536
|
+
if (writtenPaths.size > 0 && existsSync(projectYamlPath)) {
|
|
1537
|
+
const yamlText = readFileSync(projectYamlPath, "utf8");
|
|
1538
|
+
const topology = readManifestTopology(targetDir);
|
|
1539
|
+
if (readEnvContract(yamlText).present && "topology" in topology) {
|
|
1540
|
+
try {
|
|
1541
|
+
const plan = planEnvRows(targetDir, yamlText, {
|
|
1542
|
+
topology: topology.topology,
|
|
1543
|
+
catalog: ENV_CATALOG,
|
|
1544
|
+
onlyFiles: writtenPaths,
|
|
1545
|
+
});
|
|
1546
|
+
if (plan.added.length > 0) {
|
|
1547
|
+
writeFileSync(projectYamlPath, plan.text);
|
|
1548
|
+
envRowsAdded.push(...plan.added);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
catch (err) {
|
|
1552
|
+
if (!(err instanceof EnvContractError))
|
|
1553
|
+
throw err;
|
|
1554
|
+
review.push({ path: "project.yaml", reason: `no env_contract rows added: ${err.message}` });
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
// #7495 D6 — the page body owns its H1: give every heading-less mdx-body
|
|
1559
|
+
// page its title and description, read the rule after the writes so a
|
|
1560
|
+
// route this run added counts. Its rows join `review` after `heldFiles`
|
|
1561
|
+
// was computed, so a content path never holds a template test back.
|
|
1562
|
+
const contentDir = join(targetDir, "content");
|
|
1563
|
+
const rule = mdxBodyRule(targetDir);
|
|
1564
|
+
const sitePath = (abs) => relative(targetDir, abs).split(sep).join("/");
|
|
1565
|
+
const headings = migrateBodyHeadings(contentDir, rule);
|
|
1566
|
+
for (const { path, count } of headingViolations(contentDir, rule)) {
|
|
1567
|
+
if (count > 1)
|
|
1568
|
+
review.push({ path: sitePath(path), reason: `${count} '# ' headings; keep one` });
|
|
1569
|
+
}
|
|
1570
|
+
for (const { path, reason } of headings.refused) {
|
|
1571
|
+
review.push({ path: sitePath(path), reason: `no '# ' heading added: ${reason}` });
|
|
1572
|
+
}
|
|
1253
1573
|
return {
|
|
1254
1574
|
targetDir,
|
|
1255
1575
|
version,
|
|
@@ -1257,7 +1577,10 @@ export async function updateSite(opts) {
|
|
|
1257
1577
|
review,
|
|
1258
1578
|
skipped,
|
|
1259
1579
|
heldBack,
|
|
1580
|
+
headingsAdded: headings.written.map(sitePath),
|
|
1260
1581
|
...(backfilled !== undefined ? { backfilled } : {}),
|
|
1582
|
+
...(envRowsAdded.length > 0 ? { envRowsAdded } : {}),
|
|
1583
|
+
...(droppedRows.length > 0 ? { droppedRows: droppedRows.sort() } : {}),
|
|
1261
1584
|
};
|
|
1262
1585
|
}
|
|
1263
1586
|
/** The printed report (D9): one section per non-empty list, in order. */
|
|
@@ -1272,6 +1595,7 @@ export function formatUpdateReport(result) {
|
|
|
1272
1595
|
["Added", result.added],
|
|
1273
1596
|
["Removed", result.removed],
|
|
1274
1597
|
["Already current", result.current],
|
|
1598
|
+
["Page heading added", result.headingsAdded],
|
|
1275
1599
|
];
|
|
1276
1600
|
for (const [label, paths] of sections) {
|
|
1277
1601
|
if (paths.length === 0)
|
|
@@ -1283,9 +1607,18 @@ export function formatUpdateReport(result) {
|
|
|
1283
1607
|
lines.push(`Needs review (${result.review.length}):`);
|
|
1284
1608
|
lines.push(...result.review.map((row) => ` ${row.path} — ${row.reason}`));
|
|
1285
1609
|
}
|
|
1286
|
-
for (const { file, tests } of result.heldBack) {
|
|
1287
|
-
|
|
1288
|
-
lines.push(
|
|
1610
|
+
for (const { file, tests, importers = [], missing } of result.heldBack) {
|
|
1611
|
+
const held = [...tests, ...importers].sort();
|
|
1612
|
+
lines.push(`Held back with ${file}${missing ? " (not in this site)" : ""} (${held.length}):`);
|
|
1613
|
+
lines.push(...held.map((path) => ` ${path}`));
|
|
1614
|
+
}
|
|
1615
|
+
if (result.envRowsAdded !== undefined) {
|
|
1616
|
+
lines.push(`project.yaml env_contract rows added (${result.envRowsAdded.length}):`);
|
|
1617
|
+
lines.push(...result.envRowsAdded.map((name) => ` ${name}`));
|
|
1618
|
+
}
|
|
1619
|
+
if (result.droppedRows !== undefined) {
|
|
1620
|
+
lines.push(`Dropped from ${TEMPLATE_RECORD_PATH}, no longer recorded (${result.droppedRows.length}):`);
|
|
1621
|
+
lines.push(...result.droppedRows.map((path) => ` ${path}`));
|
|
1289
1622
|
}
|
|
1290
1623
|
if (result.skipped.length > 0) {
|
|
1291
1624
|
lines.push(`Skipped (${result.skipped.length}):`);
|
|
@@ -1297,8 +1630,11 @@ export function formatUpdateReport(result) {
|
|
|
1297
1630
|
result.added.length === 0 &&
|
|
1298
1631
|
result.removed.length === 0 &&
|
|
1299
1632
|
result.current.length === 0 &&
|
|
1633
|
+
result.headingsAdded.length === 0 &&
|
|
1300
1634
|
result.review.length === 0 &&
|
|
1301
|
-
result.skipped.length === 0
|
|
1635
|
+
result.skipped.length === 0 &&
|
|
1636
|
+
result.envRowsAdded === undefined &&
|
|
1637
|
+
result.droppedRows === undefined;
|
|
1302
1638
|
if (nothingHappened) {
|
|
1303
1639
|
return `[create-ailk] Already up to date (create-ailk ${result.version}).\n`;
|
|
1304
1640
|
}
|
|
@@ -1329,11 +1665,18 @@ untouched apps/web, apps/api or apps/mcp file, or a root tools/jest file the
|
|
|
1329
1665
|
app jest configs require, whose template copy changed is overwritten. An
|
|
1330
1666
|
edited file whose template copy also changed is three-way merged with
|
|
1331
1667
|
\`git\` (git must be on PATH); a conflict leaves the file alone and is
|
|
1332
|
-
listed under "Needs review"
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1668
|
+
listed under "Needs review"; an apps/web/messages catalog instead gains the
|
|
1669
|
+
template's new keys and keeps every value it has, and is reviewed only when a
|
|
1670
|
+
key is an object on one side and a value on the other. Every template file,
|
|
1671
|
+
test or not, that imports a file left under "Needs review" or "Skipped" (by a
|
|
1672
|
+
relative or @/ import, transitively) is held at the site's version with it,
|
|
1673
|
+
its record entry is not advanced, and it is listed under "Held back with
|
|
1674
|
+
<file>" — reconcile the file, then run update again to bring it forward. A
|
|
1675
|
+
written file whose import names a file the site will not have is held the
|
|
1676
|
+
same way, under "Held back with <file> (not in this site)". Every other file
|
|
1677
|
+
in the run still updates. When project.yaml has an env_contract block, each
|
|
1678
|
+
env var the written files read that has no row gains one; no row is edited.
|
|
1679
|
+
A record row at a path create-ailk no longer records is dropped and listed.
|
|
1337
1680
|
A site with no record gets one on its first run, from the version its
|
|
1338
1681
|
.ailk-site-manifest.json already records. A site scaffolded before
|
|
1339
1682
|
@working-theory/mcp-server has its copied apps/mcp moved to the boot set:
|
|
@@ -1344,7 +1687,11 @@ copied apps/api moved to the boot file: the app becomes @working-theory/app-api,
|
|
|
1344
1687
|
the package and the six @working-theory/api-<module> packages join
|
|
1345
1688
|
apps/api/package.json, its bins and build:vercel delegate to the package, and
|
|
1346
1689
|
the copied server files go. Run \`pnpm install\` after either move, since the
|
|
1347
|
-
app's package.json gained dependencies.
|
|
1690
|
+
app's package.json gained dependencies. Each mdx-body page in content/ with no
|
|
1691
|
+
'# ' heading gets its title and description written into its body as its
|
|
1692
|
+
heading and intro, listed under "Page heading added"; a page with one heading
|
|
1693
|
+
is left as it is, and one with several, or one whose frontmatter it cannot
|
|
1694
|
+
read, is listed under "Needs review". Commit before running, and review the
|
|
1348
1695
|
diff after.
|
|
1349
1696
|
`;
|
|
1350
1697
|
/** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tar": "^7.5.21"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@working-theory/schema": "0.
|
|
32
|
-
"@working-theory/templates": "0.
|
|
31
|
+
"@working-theory/schema": "0.50.0",
|
|
32
|
+
"@working-theory/templates": "0.50.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|