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/update.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create-ailk update [<target-dir>]` — bring template fixes into a site's
|
|
3
|
+
* already-copied `apps/web`, `apps/api` and `apps/mcp` code (#8522).
|
|
4
|
+
*
|
|
5
|
+
* Reads `.ailk-template-files.json` (`template-record.ts`) to tell an
|
|
6
|
+
* untouched copy from an owner's edit. An untouched file whose template
|
|
7
|
+
* copy changed is overwritten. An edited file whose template copy changed
|
|
8
|
+
* is three-way merged (`lib/merge-three-way.ts`) against its recorded
|
|
9
|
+
* version's copy — read from this package's own `templates/` when that is
|
|
10
|
+
* the running version, else re-materialised from the published npm tarball
|
|
11
|
+
* (`lib/fetch-template-snapshot.ts`) — and the running version's copy. A
|
|
12
|
+
* conflict leaves the file untouched and is listed under "Needs review". Every
|
|
13
|
+
* template test that imports a file left under review or skipped is held at
|
|
14
|
+
* the site's version with it, so a run never leaves a test newer than the
|
|
15
|
+
* code it exercises (#8704). A site with no record gets one backfilled from
|
|
16
|
+
* `.ailk-site-manifest.json`'s `patternVersion` on its first run, then
|
|
17
|
+
* updates normally. Every refusal throws before the first write.
|
|
18
|
+
*
|
|
19
|
+
* Pure module: no `import.meta.url` — the CLI supplies `templatesDir`, the
|
|
20
|
+
* `adopt.ts` / `add-route.ts` split.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
23
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
24
|
+
import { parseArgs } from "node:util";
|
|
25
|
+
import { AddModuleRefusedError, readArchitectureHeader } from "./add-module-core.js";
|
|
26
|
+
import { buildScaffoldMarkers, isExcluded, isTextFile, renderTemplateFile, templateDestination } from "./copy.js";
|
|
27
|
+
import { fetchTemplateSnapshot } from "./lib/fetch-template-snapshot.js";
|
|
28
|
+
import { mergeThreeWay } from "./lib/merge-three-way.js";
|
|
29
|
+
import { ParseError } from "./parse-args.js";
|
|
30
|
+
import { SITE_ROSTER_PATH } from "./site-roster.js";
|
|
31
|
+
import { excludedRoutePaths } from "./surfaces.js";
|
|
32
|
+
import { TEMPLATE_RECORD_PATH, isRecordable, parseTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
|
|
33
|
+
import { TopologyError, isSpokeExcluded, readSpokeShape, spokeExclusions, } from "./topology.js";
|
|
34
|
+
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
35
|
+
export class UpdateRefusedError extends Error {
|
|
36
|
+
exitCode = 2;
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "UpdateRefusedError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** `buildScaffoldMarkers`, with `CREATE_AILK_VERSION` set to the version being rendered (D3). */
|
|
43
|
+
function markersForVersion(projectName, version) {
|
|
44
|
+
return { ...buildScaffoldMarkers(projectName), CREATE_AILK_VERSION: version };
|
|
45
|
+
}
|
|
46
|
+
const VERSION_TUPLE = /^(\d+)\.(\d+)\.(\d+)/;
|
|
47
|
+
/** `true` iff semver `a` is strictly older than semver `b` (major, minor, patch, numerically). */
|
|
48
|
+
function versionOlder(a, b) {
|
|
49
|
+
const ma = VERSION_TUPLE.exec(a);
|
|
50
|
+
const mb = VERSION_TUPLE.exec(b);
|
|
51
|
+
if (ma === null || mb === null)
|
|
52
|
+
return false;
|
|
53
|
+
for (let i = 1; i <= 3; i++) {
|
|
54
|
+
const na = Number(ma[i]);
|
|
55
|
+
const nb = Number(mb[i]);
|
|
56
|
+
if (na !== nb)
|
|
57
|
+
return na < nb;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/** Every file under `templatesDir`'s three app trees, raw bytes keyed by destination path (D3). */
|
|
62
|
+
function walkLocalTemplates(templatesDir) {
|
|
63
|
+
const out = new Map();
|
|
64
|
+
for (const app of ["web", "api", "mcp"]) {
|
|
65
|
+
const root = join(templatesDir, "apps", app);
|
|
66
|
+
if (!existsSync(root))
|
|
67
|
+
continue;
|
|
68
|
+
const stack = [root];
|
|
69
|
+
while (stack.length > 0) {
|
|
70
|
+
const dir = stack.pop();
|
|
71
|
+
if (dir === undefined)
|
|
72
|
+
break;
|
|
73
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
74
|
+
const abs = join(dir, entry.name);
|
|
75
|
+
if (entry.isSymbolicLink())
|
|
76
|
+
continue;
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
stack.push(abs);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const rel = `apps/${app}/${relativeToRoot(root, abs)}`;
|
|
82
|
+
const dest = templateDestination(rel);
|
|
83
|
+
if (dest === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
out.set(dest, readFileSync(abs));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** `abs`'s path relative to `root` (both under the same app directory), `/`-separated. */
|
|
92
|
+
function relativeToRoot(root, abs) {
|
|
93
|
+
return abs.slice(root.length + 1).split(sep).join("/");
|
|
94
|
+
}
|
|
95
|
+
/** The running or an old template version's recordable files, rendered (D3). */
|
|
96
|
+
function renderSnapshot(raw, projectName, version) {
|
|
97
|
+
const markers = markersForVersion(projectName, version);
|
|
98
|
+
const out = new Map();
|
|
99
|
+
for (const [dest, bytes] of raw) {
|
|
100
|
+
if (!isRecordable(dest))
|
|
101
|
+
continue;
|
|
102
|
+
out.set(dest, renderTemplateFile(bytes, dest, {}, markers));
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** The root `package.json` `name`, falling back to the target's basename (D8). */
|
|
107
|
+
function backfillProjectName(targetDir) {
|
|
108
|
+
try {
|
|
109
|
+
const pkg = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf8"));
|
|
110
|
+
if (typeof pkg.name === "string" && pkg.name.length > 0)
|
|
111
|
+
return pkg.name;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// no readable root package.json — fall back below.
|
|
115
|
+
}
|
|
116
|
+
return targetDir.split(sep).filter(Boolean).pop() ?? targetDir;
|
|
117
|
+
}
|
|
118
|
+
/** The highest directory or file missing on disk on `destRel`'s chain below `apps/<app>/` (D4 rule iii). */
|
|
119
|
+
function highestMissingAncestor(destRel, targetDir) {
|
|
120
|
+
const segments = destRel.split("/");
|
|
121
|
+
let current = segments.slice(0, 2).join("/");
|
|
122
|
+
for (let i = 2; i < segments.length; i++) {
|
|
123
|
+
current = `${current}/${segments[i]}`;
|
|
124
|
+
if (!existsSync(join(targetDir, ...current.split("/"))))
|
|
125
|
+
return current;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/** Whether `prefix` (or something under it) is one of `knownPaths` (D4 rule iii). */
|
|
130
|
+
function containsKnownPath(prefix, knownPaths) {
|
|
131
|
+
for (const path of knownPaths) {
|
|
132
|
+
if (path === prefix || path.startsWith(`${prefix}/`))
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
/** A template test file: a `*.test.*` / `*.spec.*` module, or anything under `__tests__/`. */
|
|
138
|
+
const TEST_FILE = /(^|\/)__tests__\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
139
|
+
/** Module specifiers `import`/`export … from`, `import()`, `require()` and `jest.mock()` name. */
|
|
140
|
+
const IMPORT_SPECIFIER = /\bfrom\s*["']([^"']+)["']|\bimport\s*["']([^"']+)["']|\b(?:import|require|jest\.mock|jest\.requireActual)\s*\(\s*["']([^"']+)["']/g;
|
|
141
|
+
const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
142
|
+
/**
|
|
143
|
+
* The record-style paths a relative specifier in `fromPath` can resolve to:
|
|
144
|
+
* the path as written, with each source extension, a `.js`-family suffix
|
|
145
|
+
* swapped for its TypeScript source, and as a directory's `index.*`.
|
|
146
|
+
*/
|
|
147
|
+
function relativeImportCandidates(fromPath, specifier) {
|
|
148
|
+
const parts = fromPath.split("/").slice(0, -1);
|
|
149
|
+
for (const segment of specifier.split("/")) {
|
|
150
|
+
if (segment === "" || segment === ".")
|
|
151
|
+
continue;
|
|
152
|
+
if (segment === "..")
|
|
153
|
+
parts.pop();
|
|
154
|
+
else
|
|
155
|
+
parts.push(segment);
|
|
156
|
+
}
|
|
157
|
+
const target = parts.join("/");
|
|
158
|
+
const out = [target, ...RESOLVE_EXTENSIONS.map((ext) => `${target}${ext}`)];
|
|
159
|
+
const jsSuffix = /\.([cm]?)jsx?$/.exec(target);
|
|
160
|
+
if (jsSuffix !== null) {
|
|
161
|
+
const stem = target.slice(0, -jsSuffix[0].length);
|
|
162
|
+
out.push(`${stem}.${jsSuffix[1]}ts`, `${stem}.tsx`);
|
|
163
|
+
}
|
|
164
|
+
out.push(...RESOLVE_EXTENSIONS.map((ext) => `${target}/index${ext}`));
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
/** The files of `held` that `text`, the source of `fromPath`, imports by relative specifier. */
|
|
168
|
+
function importedHeldFiles(fromPath, text, held) {
|
|
169
|
+
const out = new Set();
|
|
170
|
+
for (const match of text.matchAll(IMPORT_SPECIFIER)) {
|
|
171
|
+
const specifier = match[1] ?? match[2] ?? match[3];
|
|
172
|
+
if (specifier === undefined || !specifier.startsWith("."))
|
|
173
|
+
continue;
|
|
174
|
+
for (const candidate of relativeImportCandidates(fromPath, specifier)) {
|
|
175
|
+
if (held.has(candidate))
|
|
176
|
+
out.add(candidate);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [...out];
|
|
180
|
+
}
|
|
181
|
+
export async function updateSite(opts) {
|
|
182
|
+
const targetDir = resolve(opts.targetDir);
|
|
183
|
+
const version = opts.version;
|
|
184
|
+
// ── Load the record, or determine the backfill (D8) ──────────────────
|
|
185
|
+
const recordPath = join(targetDir, TEMPLATE_RECORD_PATH);
|
|
186
|
+
let record;
|
|
187
|
+
let backfillPatternVersion;
|
|
188
|
+
if (existsSync(recordPath)) {
|
|
189
|
+
try {
|
|
190
|
+
record = parseTemplateRecord(readFileSync(recordPath, "utf8"));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
throw new UpdateRefusedError(err.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const manifestPath = join(targetDir, SITE_ROSTER_PATH);
|
|
198
|
+
let patternVersion;
|
|
199
|
+
if (existsSync(manifestPath)) {
|
|
200
|
+
try {
|
|
201
|
+
const doc = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
202
|
+
const value = doc["patternVersion"];
|
|
203
|
+
if (typeof value === "string" && value.length > 0)
|
|
204
|
+
patternVersion = value;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// an unreadable manifest is the same refusal as a missing one, below.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (patternVersion === undefined) {
|
|
211
|
+
throw new UpdateRefusedError(`no ${TEMPLATE_RECORD_PATH} and no "patternVersion" in ${SITE_ROSTER_PATH} to rebuild it from; run \`create-ailk adopt\` first`);
|
|
212
|
+
}
|
|
213
|
+
backfillPatternVersion = patternVersion;
|
|
214
|
+
record = { schema: 1, projectName: backfillProjectName(targetDir), files: {} };
|
|
215
|
+
}
|
|
216
|
+
// ── Refuse a downgrade before any fetch (D8) ──────────────────────────
|
|
217
|
+
const recordedVersions = backfillPatternVersion !== undefined
|
|
218
|
+
? [backfillPatternVersion]
|
|
219
|
+
: [...new Set(Object.values(record.files).map((row) => row.version))];
|
|
220
|
+
for (const recorded of recordedVersions) {
|
|
221
|
+
if (versionOlder(version, recorded)) {
|
|
222
|
+
throw new UpdateRefusedError(`create-ailk ${version} is older than ${recorded}, already recorded in ${TEMPLATE_RECORD_PATH} — use a newer create-ailk to run update`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ── The site's scope: gated-off surfaces and spoke exclusions (D4 i/ii) ─
|
|
226
|
+
const archPath = join(targetDir, "architecture.yaml");
|
|
227
|
+
if (!existsSync(archPath)) {
|
|
228
|
+
throw new UpdateRefusedError(`${targetDir} has no architecture.yaml; run \`create-ailk adopt\` or \`--sync-architecture\` first`);
|
|
229
|
+
}
|
|
230
|
+
let header;
|
|
231
|
+
try {
|
|
232
|
+
header = readArchitectureHeader(readFileSync(archPath, "utf8"));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
if (err instanceof AddModuleRefusedError) {
|
|
236
|
+
throw new UpdateRefusedError(err.message);
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
let spoke;
|
|
241
|
+
try {
|
|
242
|
+
spoke = readSpokeShape(targetDir);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
if (err instanceof TopologyError)
|
|
246
|
+
throw new UpdateRefusedError(err.message);
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
const excludedRoutes = excludedRoutePaths(header.surfaces);
|
|
250
|
+
const spokeExclusionList = spoke !== undefined ? spokeExclusions(opts.templatesDir, spoke) : undefined;
|
|
251
|
+
const outOfScope = (destRel) => {
|
|
252
|
+
if (isExcluded(destRel, excludedRoutes))
|
|
253
|
+
return true;
|
|
254
|
+
if (spoke !== undefined && spokeExclusionList !== undefined) {
|
|
255
|
+
return isSpokeExcluded(destRel, { ...spoke, exclusions: spokeExclusionList });
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
};
|
|
259
|
+
// ── The running version's rendered copies ─────────────────────────────
|
|
260
|
+
const runningRaw = walkLocalTemplates(opts.templatesDir);
|
|
261
|
+
const newMap = renderSnapshot(runningRaw, record.projectName, version);
|
|
262
|
+
// ── A cache of rendered copies by version, fetched at most once each ──
|
|
263
|
+
const versionCache = new Map();
|
|
264
|
+
async function loadVersionMap(v) {
|
|
265
|
+
const cached = versionCache.get(v);
|
|
266
|
+
if (cached !== undefined)
|
|
267
|
+
return cached;
|
|
268
|
+
let result;
|
|
269
|
+
if (v === version) {
|
|
270
|
+
result = newMap;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
try {
|
|
274
|
+
const raw = await fetchTemplateSnapshot({
|
|
275
|
+
version: v,
|
|
276
|
+
...(opts.registryUrl !== undefined ? { registryUrl: opts.registryUrl } : {}),
|
|
277
|
+
...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
|
|
278
|
+
});
|
|
279
|
+
result = renderSnapshot(raw, record.projectName, v);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
result = { error: err.message };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
versionCache.set(v, result);
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
// ── The backfill: fetch the old base and build the synthetic record ──
|
|
289
|
+
let knownSnapshotPaths;
|
|
290
|
+
if (backfillPatternVersion !== undefined) {
|
|
291
|
+
const base = await loadVersionMap(backfillPatternVersion);
|
|
292
|
+
if ("error" in base) {
|
|
293
|
+
throw new UpdateRefusedError(`backfill: the ${backfillPatternVersion} template could not be read: ${base.error}`);
|
|
294
|
+
}
|
|
295
|
+
knownSnapshotPaths = new Set(base.keys());
|
|
296
|
+
const files = {};
|
|
297
|
+
for (const [path, bytes] of [...base.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
298
|
+
if (outOfScope(path))
|
|
299
|
+
continue;
|
|
300
|
+
if (!existsSync(join(targetDir, ...path.split("/"))))
|
|
301
|
+
continue;
|
|
302
|
+
files[path] = { sha256: sha256Hex(bytes), version: backfillPatternVersion };
|
|
303
|
+
}
|
|
304
|
+
record = { schema: 1, projectName: record.projectName, files };
|
|
305
|
+
}
|
|
306
|
+
// ── Every recorded row (D4) ────────────────────────────────────────────
|
|
307
|
+
const updated = [];
|
|
308
|
+
const merged = [];
|
|
309
|
+
const added = [];
|
|
310
|
+
const removed = [];
|
|
311
|
+
const current = [];
|
|
312
|
+
const review = [];
|
|
313
|
+
const skipped = [];
|
|
314
|
+
const nextFiles = { ...record.files };
|
|
315
|
+
const writes = [];
|
|
316
|
+
const deletes = [];
|
|
317
|
+
for (const path of Object.keys(record.files).sort()) {
|
|
318
|
+
const rec = record.files[path];
|
|
319
|
+
if (outOfScope(path)) {
|
|
320
|
+
skipped.push({ path, reason: "outside this site's scope" });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const abs = join(targetDir, ...path.split("/"));
|
|
324
|
+
if (!existsSync(abs)) {
|
|
325
|
+
skipped.push({ path, reason: "deleted locally" });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const diskBytes = readFileSync(abs);
|
|
329
|
+
const diskHash = sha256Hex(diskBytes);
|
|
330
|
+
const edited = diskHash !== rec.sha256;
|
|
331
|
+
const newBytes = newMap.get(path);
|
|
332
|
+
if (!edited) {
|
|
333
|
+
if (newBytes === undefined) {
|
|
334
|
+
deletes.push(path);
|
|
335
|
+
delete nextFiles[path];
|
|
336
|
+
removed.push(path);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const newHash = sha256Hex(newBytes);
|
|
340
|
+
if (newHash === rec.sha256)
|
|
341
|
+
continue; // no template change
|
|
342
|
+
writes.push({ path, bytes: Buffer.from(newBytes) });
|
|
343
|
+
nextFiles[path] = { sha256: newHash, version };
|
|
344
|
+
updated.push(path);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// Edited.
|
|
348
|
+
if (newBytes === undefined) {
|
|
349
|
+
review.push({ path, reason: `edited here, removed from the template in ${version}` });
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const newHash = sha256Hex(newBytes);
|
|
353
|
+
if (newHash === rec.sha256)
|
|
354
|
+
continue; // template unchanged for this file
|
|
355
|
+
if (diskHash === newHash) {
|
|
356
|
+
nextFiles[path] = { sha256: newHash, version };
|
|
357
|
+
current.push(path);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// Template changed, and the edit does not already match it: three-way merge.
|
|
361
|
+
const base = await loadVersionMap(rec.version);
|
|
362
|
+
if ("error" in base) {
|
|
363
|
+
review.push({
|
|
364
|
+
path,
|
|
365
|
+
reason: `edited here, and the ${rec.version} template could not be read: ${base.error}`,
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const baseBytes = base.get(path);
|
|
370
|
+
if (baseBytes === undefined) {
|
|
371
|
+
review.push({
|
|
372
|
+
path,
|
|
373
|
+
reason: `edited here, and the ${rec.version} template could not be read: not present in that version's snapshot`,
|
|
374
|
+
});
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (sha256Hex(baseBytes) !== rec.sha256) {
|
|
378
|
+
review.push({
|
|
379
|
+
path,
|
|
380
|
+
reason: `edited here, and the ${rec.version} template copy does not match the record`,
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (!isTextFile(path)) {
|
|
385
|
+
review.push({ path, reason: "edited here; binary files are not merged" });
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const mergeResult = mergeThreeWay({
|
|
389
|
+
ours: diskBytes.toString("utf8"),
|
|
390
|
+
base: Buffer.from(baseBytes).toString("utf8"),
|
|
391
|
+
theirs: Buffer.from(newBytes).toString("utf8"),
|
|
392
|
+
oursLabel: "yours",
|
|
393
|
+
baseLabel: `template ${rec.version}`,
|
|
394
|
+
theirsLabel: `template ${version}`,
|
|
395
|
+
});
|
|
396
|
+
if (mergeResult.status === "merged") {
|
|
397
|
+
const mergedBytes = Buffer.from(mergeResult.text, "utf8");
|
|
398
|
+
writes.push({ path, bytes: mergedBytes });
|
|
399
|
+
// D1/D5 — the row keeps the TEMPLATE's hash, never the merged disk
|
|
400
|
+
// bytes': that is what lets the next update still see the owner's
|
|
401
|
+
// edit (the disk bytes keep differing from the recorded hash) and
|
|
402
|
+
// merge against the right base, instead of reading a now-current
|
|
403
|
+
// file as "edited" against its own past self forever.
|
|
404
|
+
nextFiles[path] = { sha256: newHash, version };
|
|
405
|
+
merged.push(path);
|
|
406
|
+
}
|
|
407
|
+
else if (mergeResult.status === "conflict") {
|
|
408
|
+
review.push({
|
|
409
|
+
path,
|
|
410
|
+
reason: `conflicts with the template change ${rec.version} → ${version}`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
review.push({ path, reason: `the three-way merge could not run: ${mergeResult.reason}` });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// ── Every recordable path the running template has that this record does not (D4) ──
|
|
418
|
+
const knownPaths = new Set([...Object.keys(record.files), ...(knownSnapshotPaths ?? [])]);
|
|
419
|
+
for (const path of [...newMap.keys()].sort()) {
|
|
420
|
+
if (Object.prototype.hasOwnProperty.call(record.files, path))
|
|
421
|
+
continue;
|
|
422
|
+
const abs = join(targetDir, ...path.split("/"));
|
|
423
|
+
const newBytes = newMap.get(path);
|
|
424
|
+
if (existsSync(abs)) {
|
|
425
|
+
const diskBytes = readFileSync(abs);
|
|
426
|
+
if (sha256Hex(diskBytes) !== sha256Hex(newBytes)) {
|
|
427
|
+
skipped.push({ path, reason: "exists, not in the record" });
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (outOfScope(path))
|
|
432
|
+
continue; // rule i/ii
|
|
433
|
+
const highest = highestMissingAncestor(path, targetDir);
|
|
434
|
+
if (highest !== undefined && containsKnownPath(highest, knownPaths))
|
|
435
|
+
continue; // rule iii
|
|
436
|
+
writes.push({ path, bytes: Buffer.from(newBytes) });
|
|
437
|
+
nextFiles[path] = { sha256: sha256Hex(newBytes), version };
|
|
438
|
+
added.push(path);
|
|
439
|
+
}
|
|
440
|
+
// ── Hold back the template tests of every file left alone (#8704) ────
|
|
441
|
+
// A test this run would move forward (update, merge, add, remove, or
|
|
442
|
+
// advance as current) that imports a file under review or skipped stays
|
|
443
|
+
// exactly as the site has it, and its row keeps its recorded version — so
|
|
444
|
+
// the tests on disk always match the code they exercise. The imports are
|
|
445
|
+
// read from both the site's copy and the running template's: either one
|
|
446
|
+
// exercising the held file is enough. A held test-support file
|
|
447
|
+
// (`__tests__/fixtures.ts`) holds the tests importing it in turn, until no
|
|
448
|
+
// further test is held.
|
|
449
|
+
const heldFiles = new Set([...review, ...skipped].map((row) => row.path));
|
|
450
|
+
const heldBackByFile = new Map();
|
|
451
|
+
const heldTests = new Set();
|
|
452
|
+
const moving = [...updated, ...merged, ...added, ...removed, ...current]
|
|
453
|
+
.filter((path) => TEST_FILE.test(path))
|
|
454
|
+
.sort();
|
|
455
|
+
for (let grew = heldFiles.size > 0; grew;) {
|
|
456
|
+
grew = false;
|
|
457
|
+
for (const path of moving) {
|
|
458
|
+
if (heldTests.has(path))
|
|
459
|
+
continue;
|
|
460
|
+
const sources = [];
|
|
461
|
+
const abs = join(targetDir, ...path.split("/"));
|
|
462
|
+
if (existsSync(abs))
|
|
463
|
+
sources.push(readFileSync(abs, "utf8"));
|
|
464
|
+
const newBytes = newMap.get(path);
|
|
465
|
+
if (newBytes !== undefined)
|
|
466
|
+
sources.push(Buffer.from(newBytes).toString("utf8"));
|
|
467
|
+
const imported = new Set(sources.flatMap((text) => importedHeldFiles(path, text, heldFiles)));
|
|
468
|
+
if (imported.size === 0)
|
|
469
|
+
continue;
|
|
470
|
+
heldTests.add(path);
|
|
471
|
+
heldFiles.add(path);
|
|
472
|
+
grew = true;
|
|
473
|
+
for (const file of imported) {
|
|
474
|
+
heldBackByFile.set(file, [...(heldBackByFile.get(file) ?? []), path]);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const path of heldTests) {
|
|
479
|
+
const recorded = record.files[path];
|
|
480
|
+
if (recorded !== undefined)
|
|
481
|
+
nextFiles[path] = recorded;
|
|
482
|
+
else
|
|
483
|
+
delete nextFiles[path];
|
|
484
|
+
}
|
|
485
|
+
const notHeld = (path) => !heldTests.has(path);
|
|
486
|
+
const heldBack = [...heldBackByFile.entries()]
|
|
487
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
488
|
+
.map(([file, tests]) => ({ file, tests }));
|
|
489
|
+
// ── Apply: writes and removals, then the record (D4 Order) ───────────
|
|
490
|
+
for (const write of writes.filter((w) => notHeld(w.path))) {
|
|
491
|
+
const abs = join(targetDir, ...write.path.split("/"));
|
|
492
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
493
|
+
writeFileSync(abs, write.bytes);
|
|
494
|
+
}
|
|
495
|
+
for (const path of deletes.filter(notHeld)) {
|
|
496
|
+
rmSync(join(targetDir, ...path.split("/")));
|
|
497
|
+
}
|
|
498
|
+
const backfilled = backfillPatternVersion !== undefined
|
|
499
|
+
? { version: backfillPatternVersion, files: Object.keys(record.files).length }
|
|
500
|
+
: undefined;
|
|
501
|
+
const moved = {
|
|
502
|
+
updated: updated.filter(notHeld),
|
|
503
|
+
merged: merged.filter(notHeld),
|
|
504
|
+
added: added.filter(notHeld),
|
|
505
|
+
removed: removed.filter(notHeld),
|
|
506
|
+
current: current.filter(notHeld),
|
|
507
|
+
};
|
|
508
|
+
const recordChanged = backfilled !== undefined || Object.values(moved).some((paths) => paths.length > 0);
|
|
509
|
+
if (recordChanged) {
|
|
510
|
+
writeFileSync(recordPath, serializeTemplateRecord({ schema: 1, projectName: record.projectName, files: nextFiles }));
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
targetDir,
|
|
514
|
+
version,
|
|
515
|
+
...moved,
|
|
516
|
+
review,
|
|
517
|
+
skipped,
|
|
518
|
+
heldBack,
|
|
519
|
+
...(backfilled !== undefined ? { backfilled } : {}),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/** The printed report (D9): one section per non-empty list, in order. */
|
|
523
|
+
export function formatUpdateReport(result) {
|
|
524
|
+
const lines = [`[create-ailk] update → ${result.targetDir}`];
|
|
525
|
+
if (result.backfilled !== undefined) {
|
|
526
|
+
lines.push(`Record written from ${result.backfilled.version}: ${result.backfilled.files} files`);
|
|
527
|
+
}
|
|
528
|
+
const sections = [
|
|
529
|
+
["Updated", result.updated],
|
|
530
|
+
["Merged", result.merged],
|
|
531
|
+
["Added", result.added],
|
|
532
|
+
["Removed", result.removed],
|
|
533
|
+
["Already current", result.current],
|
|
534
|
+
];
|
|
535
|
+
for (const [label, paths] of sections) {
|
|
536
|
+
if (paths.length === 0)
|
|
537
|
+
continue;
|
|
538
|
+
lines.push(`${label} (${paths.length}):`);
|
|
539
|
+
lines.push(...paths.map((path) => ` ${path}`));
|
|
540
|
+
}
|
|
541
|
+
if (result.review.length > 0) {
|
|
542
|
+
lines.push(`Needs review (${result.review.length}):`);
|
|
543
|
+
lines.push(...result.review.map((row) => ` ${row.path} — ${row.reason}`));
|
|
544
|
+
}
|
|
545
|
+
for (const { file, tests } of result.heldBack) {
|
|
546
|
+
lines.push(`Held back with ${file} (${tests.length}):`);
|
|
547
|
+
lines.push(...tests.map((path) => ` ${path}`));
|
|
548
|
+
}
|
|
549
|
+
if (result.skipped.length > 0) {
|
|
550
|
+
lines.push(`Skipped (${result.skipped.length}):`);
|
|
551
|
+
lines.push(...result.skipped.map((row) => ` ${row.path} — ${row.reason}`));
|
|
552
|
+
}
|
|
553
|
+
const nothingHappened = result.backfilled === undefined &&
|
|
554
|
+
result.updated.length === 0 &&
|
|
555
|
+
result.merged.length === 0 &&
|
|
556
|
+
result.added.length === 0 &&
|
|
557
|
+
result.removed.length === 0 &&
|
|
558
|
+
result.current.length === 0 &&
|
|
559
|
+
result.review.length === 0 &&
|
|
560
|
+
result.skipped.length === 0;
|
|
561
|
+
if (nothingHappened) {
|
|
562
|
+
return `[create-ailk] Already up to date (create-ailk ${result.version}).\n`;
|
|
563
|
+
}
|
|
564
|
+
return lines.join("\n") + "\n";
|
|
565
|
+
}
|
|
566
|
+
export const UPDATE_HELP = `create-ailk update — bring template fixes into a site's copied app code.
|
|
567
|
+
|
|
568
|
+
Usage:
|
|
569
|
+
create-ailk update [<target-dir>]
|
|
570
|
+
|
|
571
|
+
Arguments:
|
|
572
|
+
<target-dir> The existing site. Defaults to the current dir.
|
|
573
|
+
|
|
574
|
+
Options:
|
|
575
|
+
--help, -h Print this message.
|
|
576
|
+
|
|
577
|
+
Reads .ailk-template-files.json (written beside .ailk-site-manifest.json by
|
|
578
|
+
every template scaffold) to tell an untouched copy from an edit. An
|
|
579
|
+
untouched apps/web, apps/api or apps/mcp file whose template copy changed is
|
|
580
|
+
overwritten. An edited file whose template copy also changed is three-way
|
|
581
|
+
merged with \`git\` (git must be on PATH); a conflict leaves the file alone
|
|
582
|
+
and is listed under "Needs review". Every template test that imports a file
|
|
583
|
+
left under "Needs review" or "Skipped" (by a relative import) is held at the
|
|
584
|
+
site's version with it, its record entry is not advanced, and it is listed
|
|
585
|
+
under "Held back with <file>" — reconcile the file, then run update again to
|
|
586
|
+
bring its tests forward. Every other file in the run still updates.
|
|
587
|
+
A site with no record gets one on its first run, from the version its
|
|
588
|
+
.ailk-site-manifest.json already records. Commit before running, and review
|
|
589
|
+
the diff after.
|
|
590
|
+
`;
|
|
591
|
+
/** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
|
|
592
|
+
export function parseUpdateArgs(argv) {
|
|
593
|
+
const parsed = parseArgs({
|
|
594
|
+
args: argv,
|
|
595
|
+
strict: true,
|
|
596
|
+
allowPositionals: true,
|
|
597
|
+
options: {
|
|
598
|
+
help: { type: "boolean", default: false, short: "h" },
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
const [target, ...rest] = parsed.positionals;
|
|
602
|
+
if (rest.length > 0) {
|
|
603
|
+
throw new ParseError(`update takes at most one target directory, got ${parsed.positionals.length} arguments`);
|
|
604
|
+
}
|
|
605
|
+
return { target, help: parsed.values.help === true };
|
|
606
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tar": "^7.5.21"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@working-theory/schema": "0.
|
|
32
|
-
"@working-theory/templates": "0.
|
|
31
|
+
"@working-theory/schema": "0.42.0",
|
|
32
|
+
"@working-theory/templates": "0.42.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "_base",
|
|
3
3
|
"requires": [],
|
|
4
|
-
"$comment": "The BASE LAYER (#5618 D1) — not a template. Every declared template reaches this tree by `extends: \"_base\"`; scaffold() installs it through installBundleTree before any module bundle and before the template's own tree, only when --template is named. It carries the seed content every template brings itself and no module owns: /pricing, /privacy
|
|
5
|
-
"postInstall": "Base layer installed: seed copy for /pricing, /privacy and /
|
|
4
|
+
"$comment": "The BASE LAYER (#5618 D1) — not a template. Every declared template reaches this tree by `extends: \"_base\"`; scaffold() installs it through installBundleTree before any module bundle and before the template's own tree, only when --template is named. It carries the seed content every template brings itself and no module owns: /pricing, /privacy, /terms (spec 5462 D16) and /working-agreement (#8661 D10). No installMap: every file here is unmapped and lands at its own path under the scaffold root's content/, so a template overrides a base page by shipping the same path. Never published as a registry bundle; the leading underscore is the skip signal for the bundle producer and the site-templates/ directory scans.",
|
|
5
|
+
"postInstall": "Base layer installed: seed copy for /pricing, /privacy, /terms and /working-agreement is in content/en/ - replace the placeholder copy before you launch."
|
|
6
6
|
}
|