create-ailk 0.19.1 → 0.20.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 +64 -7
- package/dist/cli.js +35 -0
- package/dist/copy.d.ts +41 -0
- package/dist/copy.js +87 -21
- 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 +32 -2
- package/dist/spoke-checkout.d.ts +7 -0
- package/dist/spoke-checkout.js +8 -1
- package/dist/surfaces.js +67 -2
- package/dist/template-record.d.ts +59 -0
- package/dist/template-record.js +183 -0
- package/dist/update.d.ts +48 -0
- package/dist/update.js +504 -0
- package/package.json +3 -3
- 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/_gitignore +162 -0
- package/templates/apps/api/CLAUDE.md +14 -10
- 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/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/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 +95 -8
- 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/_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]/consent-banner.tsx +32 -0
- package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +13 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -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/robots.txt/route.ts +13 -10
- package/templates/apps/web/jest.config.cjs +3 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +96 -7
- 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/relay.ts +20 -0
- package/templates/apps/web/messages/en.json +67 -4
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/database/CHANGELOG.md +39 -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/_gitignore +8 -0
- package/templates/database/inbox/schema.prisma +38 -12
- 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/project.yaml +6 -3
- package/versions.json +35 -35
- package/templates/.claude/rules/preserve-existing-style.md +0 -51
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `git merge-file` wrapper — the three-way text merge `create-ailk update`
|
|
3
|
+
* runs for a copied app file the site owner edited and the template also
|
|
4
|
+
* changed (#8522 D5).
|
|
5
|
+
*
|
|
6
|
+
* `git merge-file` is the reference diff3 merge: every AILK owner already
|
|
7
|
+
* has `git` on `PATH` (it is how they cloned or will push this site), and
|
|
8
|
+
* using it adds no new npm dependency. The three versions are written to a
|
|
9
|
+
* fresh temp directory (never the site's own tree) and read back as text;
|
|
10
|
+
* nothing is left behind.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
/**
|
|
17
|
+
* Run `git merge-file -p` over the three texts in a `mkdtemp` directory, no
|
|
18
|
+
* shell, argv holding only temp-file paths and the three labels (#8522 D5).
|
|
19
|
+
*
|
|
20
|
+
* Exit 0 is a clean merge (`merged`). Exit 1-127 is a conflicting hunk
|
|
21
|
+
* (`conflict`) — the file is never written with conflict markers by this
|
|
22
|
+
* function; the caller decides what to do with a conflict, and nothing here
|
|
23
|
+
* ever applies one. Any other exit, or a spawn failure (no `git` on `PATH`),
|
|
24
|
+
* is `error`.
|
|
25
|
+
*/
|
|
26
|
+
export function mergeThreeWay(opts) {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), "ailk-merge-"));
|
|
28
|
+
try {
|
|
29
|
+
const oursPath = join(dir, "ours");
|
|
30
|
+
const basePath = join(dir, "base");
|
|
31
|
+
const theirsPath = join(dir, "theirs");
|
|
32
|
+
writeFileSync(oursPath, opts.ours);
|
|
33
|
+
writeFileSync(basePath, opts.base);
|
|
34
|
+
writeFileSync(theirsPath, opts.theirs);
|
|
35
|
+
const result = spawnSync("git", [
|
|
36
|
+
"merge-file",
|
|
37
|
+
"-p",
|
|
38
|
+
"-L",
|
|
39
|
+
opts.oursLabel,
|
|
40
|
+
"-L",
|
|
41
|
+
opts.baseLabel,
|
|
42
|
+
"-L",
|
|
43
|
+
opts.theirsLabel,
|
|
44
|
+
oursPath,
|
|
45
|
+
basePath,
|
|
46
|
+
theirsPath,
|
|
47
|
+
], { shell: false });
|
|
48
|
+
if (result.error !== undefined) {
|
|
49
|
+
return { status: "error", reason: result.error.message };
|
|
50
|
+
}
|
|
51
|
+
if (result.status === 0) {
|
|
52
|
+
return { status: "merged", text: result.stdout.toString("utf8") };
|
|
53
|
+
}
|
|
54
|
+
if (result.status !== null && result.status >= 1 && result.status <= 127) {
|
|
55
|
+
return { status: "conflict" };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
status: "error",
|
|
59
|
+
reason: `git merge-file exited ${result.status ?? `signal ${result.signal}`}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `architecture.yaml` template files — a fixed template IS the drift this
|
|
22
22
|
* removes.
|
|
23
23
|
*/
|
|
24
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
25
25
|
import { join, sep } from "node:path";
|
|
26
26
|
import { CONTENT_PAYLOADS, } from "./content-payloads.js";
|
|
27
27
|
import { importClosure } from "./lib/import-graph.js";
|
|
@@ -183,12 +183,18 @@ const MEMBER_AREA_FILES = [
|
|
|
183
183
|
"apps/api/src/routes/member/request-form.ts",
|
|
184
184
|
"apps/api/src/routes/member/billing.ts",
|
|
185
185
|
"apps/api/src/routes/member/requests.ts",
|
|
186
|
+
// #8515 D13 — the client's onboarding read and write.
|
|
187
|
+
"apps/api/src/routes/member/onboarding.ts",
|
|
186
188
|
"apps/api/src/routes/member/artifact-review.ts",
|
|
187
189
|
"apps/api/src/routes/member/profile.ts",
|
|
188
190
|
"apps/api/src/routes/member/deletion-request.ts",
|
|
189
191
|
// #8438 D6 — the member's own consents; the same gate as the writes above.
|
|
190
192
|
"apps/api/src/routes/member/consents.ts",
|
|
191
193
|
"apps/api/src/services/member-area.ts",
|
|
194
|
+
// #8516 D8 — whether proposal payment runs, and the Checkout Session;
|
|
195
|
+
// imported only by member-area files (pieces.ts and the two member
|
|
196
|
+
// artifact routes), and itself imports only base files.
|
|
197
|
+
"apps/api/src/services/proposal-payments.ts",
|
|
192
198
|
"apps/api/src/lib/member-store.ts",
|
|
193
199
|
// #8226 D12 — the member-request match CLI; both import member-store.ts,
|
|
194
200
|
// which a build without the member slice does not carry.
|
|
@@ -1488,6 +1494,13 @@ export const MODULE_SLICES = {
|
|
|
1488
1494
|
"apps/api/src/routes/slack/index.ts",
|
|
1489
1495
|
"apps/api/src/routes/slack/actions.ts",
|
|
1490
1496
|
"apps/api/src/routes/waitlist-signups/index.ts",
|
|
1497
|
+
// #7479 — the listing-draft forwarded-visitor suite exercises the three
|
|
1498
|
+
// project-listings draft routes through the real build(), and those
|
|
1499
|
+
// routes register only under isModuleEnabled("signal-collectors"), so
|
|
1500
|
+
// the suite strips with this slice (the #7389 shape for
|
|
1501
|
+
// schedule-forwarded-rate-limit.test.ts under scheduling) instead of
|
|
1502
|
+
// failing every keyless scaffold's apps/api jest gate.
|
|
1503
|
+
"apps/api/src/__tests__/listings-forwarded-rate-limit.test.ts",
|
|
1491
1504
|
// apps/api/src/routes/project-listings/{start,patch-draft,submit,public,
|
|
1492
1505
|
// me,respond,resume-token}.ts and services/{project-listings,
|
|
1493
1506
|
// listing-config}.ts deliberately NOT listed — see the comment above
|
|
@@ -2201,11 +2214,51 @@ export function missingArchitectureEnforcement(siteDir) {
|
|
|
2201
2214
|
missing.push("package.json scripts.preflight (runs pnpm architecture)");
|
|
2202
2215
|
}
|
|
2203
2216
|
const postToolUse = readJson(".claude/settings.json")?.hooks?.PostToolUse ?? [];
|
|
2217
|
+
const ARCH_PNPM_RE = /\bpnpm\b.*\barchitecture\b/;
|
|
2218
|
+
// CloudParity's repo-owned-hook command grammar (#8509 D2/D6): no
|
|
2219
|
+
// $CLAUDE_PROJECT_DIR prefix, no arguments, no shell operators.
|
|
2220
|
+
const ARCH_HOOK_SCRIPT_RE = /^bash \.claude\/hooks\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)$/;
|
|
2221
|
+
/**
|
|
2222
|
+
* A command "runs architecture" either in today's inline form (matched on
|
|
2223
|
+
* the command string itself, so a pre-#8509 scaffold still verifies) or in
|
|
2224
|
+
* the script form CloudParity's declaration grammar requires — read the
|
|
2225
|
+
* named script's own text, so a deleted or emptied script still reports
|
|
2226
|
+
* missing even though the command string is unchanged.
|
|
2227
|
+
*/
|
|
2228
|
+
const commandRunsArchitecture = (command) => {
|
|
2229
|
+
if (ARCH_PNPM_RE.test(command))
|
|
2230
|
+
return true;
|
|
2231
|
+
const match = ARCH_HOOK_SCRIPT_RE.exec(command);
|
|
2232
|
+
if (!match)
|
|
2233
|
+
return false;
|
|
2234
|
+
const scriptRel = match[1];
|
|
2235
|
+
// The charset `ARCH_HOOK_SCRIPT_RE` allows per segment admits "." and
|
|
2236
|
+
// "..", which CloudParity's own grammar forbids (#8509 spec §1) — reject
|
|
2237
|
+
// them here too, so a crafted command string can't walk this read
|
|
2238
|
+
// outside siteDir/.claude/hooks/.
|
|
2239
|
+
if (scriptRel.split("/").some((seg) => seg === "." || seg === ".."))
|
|
2240
|
+
return false;
|
|
2241
|
+
const hooksDir = join(siteDir, ".claude", "hooks");
|
|
2242
|
+
const scriptPath = join(hooksDir, scriptRel);
|
|
2243
|
+
if (!existsSync(scriptPath))
|
|
2244
|
+
return false;
|
|
2245
|
+
try {
|
|
2246
|
+
const realHooksDir = realpathSync(hooksDir);
|
|
2247
|
+
const realScript = realpathSync(scriptPath);
|
|
2248
|
+
if (!realScript.startsWith(realHooksDir + sep) || !lstatSync(realScript).isFile()) {
|
|
2249
|
+
return false;
|
|
2250
|
+
}
|
|
2251
|
+
return ARCH_PNPM_RE.test(readFileSync(realScript, "utf8"));
|
|
2252
|
+
}
|
|
2253
|
+
catch {
|
|
2254
|
+
return false;
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2204
2257
|
const hooked = postToolUse.some((entry) => {
|
|
2205
2258
|
const tools = (entry.matcher ?? "").split("|");
|
|
2206
2259
|
return (tools.includes("Edit") &&
|
|
2207
2260
|
tools.includes("Write") &&
|
|
2208
|
-
(entry.hooks ?? []).some((h) =>
|
|
2261
|
+
(entry.hooks ?? []).some((h) => commandRunsArchitecture(h.command ?? "")));
|
|
2209
2262
|
});
|
|
2210
2263
|
if (!hooked) {
|
|
2211
2264
|
missing.push(".claude/settings.json PostToolUse hook on Edit|Write (runs pnpm architecture)");
|
package/dist/programmatic.d.ts
CHANGED
|
@@ -195,8 +195,14 @@ export interface ScaffoldResult {
|
|
|
195
195
|
*
|
|
196
196
|
* Throws ValidationError if targetDir already exists and is non-empty.
|
|
197
197
|
* Throws Error if the template copy fails for any other reason.
|
|
198
|
+
*
|
|
199
|
+
* @param copied — INTERNAL (#8522 D2): when passed, every recordable path
|
|
200
|
+
* `copyTemplate` writes is hashed into this map (destination path -> the
|
|
201
|
+
* sha256 of the bytes written), for `scaffold()` to build
|
|
202
|
+
* `.ailk-template-files.json` from once every later layer has landed. No
|
|
203
|
+
* existing caller passes it, so their behaviour is unchanged.
|
|
198
204
|
*/
|
|
199
|
-
export declare function scaffoldSync(opts: ScaffoldOptions): ScaffoldResult;
|
|
205
|
+
export declare function scaffoldSync(opts: ScaffoldOptions, copied?: Map<string, string>): ScaffoldResult;
|
|
200
206
|
/**
|
|
201
207
|
* Scaffold an AI Launch Kit starter project into targetDir (async wrapper).
|
|
202
208
|
*
|
package/dist/programmatic.js
CHANGED
|
@@ -48,6 +48,7 @@ import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from
|
|
|
48
48
|
import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, isUnservedSurfaceContent, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
49
49
|
import { syncRoutes } from "./sync-routes.js";
|
|
50
50
|
import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, templateTreeDir, } from "./template-manifest.js";
|
|
51
|
+
import { TEMPLATE_RECORD_PATH, isRecordable, planTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
|
|
51
52
|
import { SPOKE_ONLY_FILES, TopologyError, applySpokeTransforms, isSpokeExcluded, normalizeHubSignInUrl, normalizeHubUrl, readSiteTopology, spokeExclusions, spokeDestination, spokeModuleRefusal, spokeModules, writeHubOriginEnv, writeHubSignInEnv, } from "./topology.js";
|
|
52
53
|
import { validateTarget } from "./validate.js";
|
|
53
54
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -148,8 +149,14 @@ function resolveShapeTemplate(shape) {
|
|
|
148
149
|
*
|
|
149
150
|
* Throws ValidationError if targetDir already exists and is non-empty.
|
|
150
151
|
* Throws Error if the template copy fails for any other reason.
|
|
152
|
+
*
|
|
153
|
+
* @param copied — INTERNAL (#8522 D2): when passed, every recordable path
|
|
154
|
+
* `copyTemplate` writes is hashed into this map (destination path -> the
|
|
155
|
+
* sha256 of the bytes written), for `scaffold()` to build
|
|
156
|
+
* `.ailk-template-files.json` from once every later layer has landed. No
|
|
157
|
+
* existing caller passes it, so their behaviour is unchanged.
|
|
151
158
|
*/
|
|
152
|
-
export function scaffoldSync(opts) {
|
|
159
|
+
export function scaffoldSync(opts, copied) {
|
|
153
160
|
// Validate target — throws ValidationError on rejection.
|
|
154
161
|
const targetAbs = validateTarget(opts.targetDir);
|
|
155
162
|
const projectName = opts.projectName ?? basename(targetAbs);
|
|
@@ -177,6 +184,14 @@ export function scaffoldSync(opts) {
|
|
|
177
184
|
: SPOKE_ONLY_FILES),
|
|
178
185
|
],
|
|
179
186
|
markers: buildScaffoldMarkers(projectName),
|
|
187
|
+
...(copied !== undefined
|
|
188
|
+
? {
|
|
189
|
+
onWrite: (relPath, bytes) => {
|
|
190
|
+
if (isRecordable(relPath))
|
|
191
|
+
copied.set(relPath, sha256Hex(bytes));
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
180
195
|
});
|
|
181
196
|
// #7426 D5/D6 — the root files a spoke rewrites, and the spoke seams.
|
|
182
197
|
if (spoke !== undefined)
|
|
@@ -495,6 +510,10 @@ export async function scaffold(opts) {
|
|
|
495
510
|
const surfaces = template !== undefined
|
|
496
511
|
? [...new Set([...(opts.surfaces ?? []), ...rosterSurfaces(template)])].sort()
|
|
497
512
|
: opts.surfaces;
|
|
513
|
+
// #8522 D2 — every recordable path `copyTemplate` writes, hashed as it is
|
|
514
|
+
// written: the input to the per-file record `scaffold()` writes below,
|
|
515
|
+
// once every later layer has landed.
|
|
516
|
+
const copied = new Map();
|
|
498
517
|
const result = scaffoldSync({
|
|
499
518
|
...opts,
|
|
500
519
|
modules,
|
|
@@ -509,7 +528,7 @@ export async function scaffold(opts) {
|
|
|
509
528
|
// #6813 D8 step 6 — the resolved design's OWN layout (never its name):
|
|
510
529
|
// `resolveSiteChrome` reads a layout object now, not a `StarterDesign`.
|
|
511
530
|
...(resolvedDesign !== undefined ? { designLayout: resolvedDesign.pkg.layout } : {}),
|
|
512
|
-
});
|
|
531
|
+
}, copied);
|
|
513
532
|
// Threaded to extractModuleBundle for `package.json` patch targets'
|
|
514
533
|
// `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
|
|
515
534
|
// `scaffoldSync` already reads for `copyTemplate`, re-read here since
|
|
@@ -785,6 +804,17 @@ export async function scaffold(opts) {
|
|
|
785
804
|
templatesDir: TEMPLATES_DIR,
|
|
786
805
|
markers,
|
|
787
806
|
}));
|
|
807
|
+
// #8522 D2 — the per-file record, as this scaffold's LAST write (same
|
|
808
|
+
// guard and roster as the site manifest just above): a kept copied
|
|
809
|
+
// path only when its final bytes still hash to the copy — a later
|
|
810
|
+
// layer (a module bundle's patch, a generator) that rewrote the file
|
|
811
|
+
// is absent here too.
|
|
812
|
+
writeFileSync(join(result.targetDir, TEMPLATE_RECORD_PATH), serializeTemplateRecord(planTemplateRecord({
|
|
813
|
+
targetDir: result.targetDir,
|
|
814
|
+
copied,
|
|
815
|
+
version: markers["CREATE_AILK_VERSION"] ?? "",
|
|
816
|
+
projectName,
|
|
817
|
+
})));
|
|
788
818
|
}
|
|
789
819
|
}
|
|
790
820
|
// #7536 D3 — the registry follows `apps/web/package.json`, which a module
|
package/dist/spoke-checkout.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export declare const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
|
|
|
8
8
|
export declare const SPOKE_CHECKOUT_CONFIG_PATH = "apps/web/lib/checkout-config.ts";
|
|
9
9
|
/** The two return pages: Stripe's success URL and every refused buy's target. */
|
|
10
10
|
export declare const SPOKE_CHECKOUT_PAGE_FILES: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* The marker `renderPage()` stamps into both generated return pages' header
|
|
13
|
+
* comment (D3). `apps/web`'s structured-data test cannot import create-ailk
|
|
14
|
+
* (the import boundary), so it carries this text as a literal, kept in sync
|
|
15
|
+
* by `packages/create-ailk/__tests__/spoke-checkout.test.ts` (#8513 AC-5).
|
|
16
|
+
*/
|
|
17
|
+
export declare const SPOKE_CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
|
|
11
18
|
/** The variables a commerce spoke depends on (D9). The package reads three of them. */
|
|
12
19
|
export declare const SPOKE_CHECKOUT_ENV_CATALOG: Readonly<Record<string, EnvCatalogEntry>>;
|
|
13
20
|
/** The names a commerce spoke declares although only the package reads them (D9). */
|
package/dist/spoke-checkout.js
CHANGED
|
@@ -38,6 +38,13 @@ export const SPOKE_CHECKOUT_PAGE_FILES = Object.freeze([
|
|
|
38
38
|
"apps/web/app/[locale]/checkout/thank-you/page.tsx",
|
|
39
39
|
"apps/web/app/[locale]/checkout/unavailable/page.tsx",
|
|
40
40
|
]);
|
|
41
|
+
/**
|
|
42
|
+
* The marker `renderPage()` stamps into both generated return pages' header
|
|
43
|
+
* comment (D3). `apps/web`'s structured-data test cannot import create-ailk
|
|
44
|
+
* (the import boundary), so it carries this text as a literal, kept in sync
|
|
45
|
+
* by `packages/create-ailk/__tests__/spoke-checkout.test.ts` (#8513 AC-5).
|
|
46
|
+
*/
|
|
47
|
+
export const SPOKE_CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
|
|
41
48
|
/** The variables a commerce spoke depends on (D9). The package reads three of them. */
|
|
42
49
|
export const SPOKE_CHECKOUT_ENV_CATALOG = Object.freeze({
|
|
43
50
|
STRIPE_SECRET_KEY: {
|
|
@@ -245,7 +252,7 @@ function renderConfig(site) {
|
|
|
245
252
|
/** One return page (D7): a server component that reads nothing but its copy. */
|
|
246
253
|
function renderPage(opts) {
|
|
247
254
|
return [
|
|
248
|
-
`// ${opts.title} —
|
|
255
|
+
`// ${opts.title} — ${SPOKE_CHECKOUT_PAGE_MARKER}.`,
|
|
249
256
|
...opts.about.map((line) => `// ${line}`),
|
|
250
257
|
`import { SectionRenderer } from "${UI_SECTIONS_PACKAGE}";`,
|
|
251
258
|
`import type { PageSection } from "${VALIDATION_PACKAGE}";`,
|
package/dist/surfaces.js
CHANGED
|
@@ -94,6 +94,11 @@ export const CORE_ROUTE_DIRS = [
|
|
|
94
94
|
// 404s. Neither `GATEABLE_SURFACES` nor `MODULE_SURFACES.commerce` gains
|
|
95
95
|
// an entry — the claim decides whether the pages serve, not the scaffold.
|
|
96
96
|
`${ROUTE_ROOT}/purchase`,
|
|
97
|
+
// #8438 D17/D21 — the one unsubscribe landing every signed link calls.
|
|
98
|
+
// Consent logging is part of the never-gated privacy floor (CLAUDE.md §
|
|
99
|
+
// License & tier hygiene's never-gate list), so this ships unconditionally
|
|
100
|
+
// like `/about`/`/contact`/`/faq` above — never a `GATEABLE_SURFACES` entry.
|
|
101
|
+
`${ROUTE_ROOT}/unsubscribe`,
|
|
97
102
|
];
|
|
98
103
|
/**
|
|
99
104
|
* Directory names skipped by `listTemplateRouteDirs`'s walker even though they
|
|
@@ -666,12 +671,20 @@ export const ROUTE_SINCE = {
|
|
|
666
671
|
// version, the same rule #7430/#8232 applied.
|
|
667
672
|
"(authed)/account/security": "0.38.0",
|
|
668
673
|
"(authed)/account/notifications": "0.38.0",
|
|
674
|
+
// #8438 D21 — User settings' fifth tab, Privacy, and the public
|
|
675
|
+
// unsubscribe landing every signed link calls. Genuinely new, so they
|
|
676
|
+
// attach at today's fixed-group version, the same rule #8339 applied.
|
|
677
|
+
"(authed)/account/privacy": "0.40.0",
|
|
678
|
+
unsubscribe: "0.40.0",
|
|
669
679
|
// #8234 — the member Artifacts pages and a version's file route. Genuinely
|
|
670
680
|
// new, so they attach at today's fixed-group version, the same rule #7430
|
|
671
681
|
// applied.
|
|
672
682
|
"(authed)/account/artifacts": "0.38.0",
|
|
673
683
|
"(authed)/account/artifacts/[pieceId]": "0.38.0",
|
|
674
684
|
"(authed)/account/artifacts/[pieceId]/versions/[version]/file": "0.38.0",
|
|
685
|
+
// #8515 — the client's onboarding page. Genuinely new, so it attaches at
|
|
686
|
+
// today's fixed-group version, the same rule #7430 applied.
|
|
687
|
+
"(authed)/account/onboarding": "0.40.0",
|
|
675
688
|
// The leads inbox (#5678). The ROUTE ships in every scaffold, Free
|
|
676
689
|
// included — lead capture is a never-gate surface, so reading a lead back
|
|
677
690
|
// is one too. Whether it shows a ROW is a separate question with two
|
|
@@ -774,7 +787,13 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
774
787
|
"apps/web/lib/auth-routes.ts",
|
|
775
788
|
],
|
|
776
789
|
"(auth)/sign-in": ["apps/web/lib/auth-env.ts"],
|
|
777
|
-
|
|
790
|
+
// #8438 D13/D21 — the sign-up card's agreements gate also carries the
|
|
791
|
+
// carried-cookie mint, `sign-up.actions.ts`, beside it in this route's own
|
|
792
|
+
// directory.
|
|
793
|
+
"(auth)/sign-up": [
|
|
794
|
+
"apps/web/lib/auth-env.ts",
|
|
795
|
+
`${ROUTE_ROOT}/(auth)/sign-up/sign-up.actions.ts`,
|
|
796
|
+
],
|
|
778
797
|
"(auth)/forgot-password": ["apps/web/lib/auth-env.ts"],
|
|
779
798
|
"(auth)/reset-password": ["apps/web/lib/auth-env.ts"],
|
|
780
799
|
// #7430 — the group's layout reads `isAccountPath()` to give the account
|
|
@@ -789,6 +808,17 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
789
808
|
"apps/web/lib/server-api.ts",
|
|
790
809
|
"apps/web/lib/site-brand.tsx",
|
|
791
810
|
"apps/web/lib/site-logo.tsx",
|
|
811
|
+
// #8438 D13/D21 — two brand-new files in this already-existing route
|
|
812
|
+
// directory; a scaffold whose `(authed)/account` predates this unit
|
|
813
|
+
// needs them added, which the directory-level sync alone would not do
|
|
814
|
+
// (sync never overwrites or adds to a directory it already has).
|
|
815
|
+
// `acceptCarriedAgreementsAction` (the carried-cookie CONSUME side) lives
|
|
816
|
+
// in this directory's own `agreements-actions.ts`, never in
|
|
817
|
+
// `(auth)/sign-up` — see that file's header: `(auth)/**` is a sign-in
|
|
818
|
+
// seed a sign-in-less spoke drops, so a cross-directory reach from here
|
|
819
|
+
// into it would dangle.
|
|
820
|
+
`${ROUTE_ROOT}/(authed)/account/agreements-prompt.tsx`,
|
|
821
|
+
`${ROUTE_ROOT}/(authed)/account/agreements-actions.ts`,
|
|
792
822
|
],
|
|
793
823
|
// #7430 — the three child routes also import files in their PARENT's
|
|
794
824
|
// directory. A scaffold whose `(authed)/account` predates #7430 keeps its
|
|
@@ -875,12 +905,31 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
875
905
|
`${ROUTE_ROOT}/(authed)/account/sign-in-method.ts`,
|
|
876
906
|
],
|
|
877
907
|
// #8339 D11 — Notifications holds the former Privacy section's
|
|
878
|
-
// cookie-settings link, moved verbatim from General.
|
|
908
|
+
// cookie-settings link, moved verbatim from General. #8438 D15 — that
|
|
909
|
+
// link moved on again, to Privacy; Notifications now reads the registry
|
|
910
|
+
// through `member.consents` instead, so it keeps the same dependency set.
|
|
879
911
|
"(authed)/account/notifications": [
|
|
880
912
|
"apps/web/lib/account-config.ts",
|
|
881
913
|
"apps/web/lib/server-api.ts",
|
|
882
914
|
`${ROUTE_ROOT}/(authed)/account/account-data.ts`,
|
|
883
915
|
`${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
|
|
916
|
+
`${ROUTE_ROOT}/(authed)/account/privacy/actions.ts`,
|
|
917
|
+
],
|
|
918
|
+
// #8438 D14 — Privacy's five sections: the summary, the cookie-choice
|
|
919
|
+
// read from the browser cookie, data-use consents and read-only
|
|
920
|
+
// agreements (both through `member.consents`), and Request account
|
|
921
|
+
// deletion, moved here unchanged from General (D16) — so Privacy needs
|
|
922
|
+
// General's own deletion files too, the same cross-directory rule #8339
|
|
923
|
+
// D11 applied to Notifications' cookie-settings link.
|
|
924
|
+
"(authed)/account/privacy": [
|
|
925
|
+
"apps/web/lib/account-config.ts",
|
|
926
|
+
"apps/web/lib/server-api.ts",
|
|
927
|
+
`${ROUTE_ROOT}/(authed)/account/account-data.ts`,
|
|
928
|
+
`${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
|
|
929
|
+
`${ROUTE_ROOT}/(authed)/account/sign-in-method.ts`,
|
|
930
|
+
`${ROUTE_ROOT}/(authed)/account/profile-actions.ts`,
|
|
931
|
+
`${ROUTE_ROOT}/(authed)/account/general/deletion-request.tsx`,
|
|
932
|
+
`${ROUTE_ROOT}/(authed)/account/general/profile-source.ts`,
|
|
884
933
|
],
|
|
885
934
|
"(authed)/account/billing": [
|
|
886
935
|
"apps/web/lib/account-config.ts",
|
|
@@ -924,6 +973,17 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
924
973
|
`${ROUTE_ROOT}/(authed)/account/artifacts/artifact-actions.ts`,
|
|
925
974
|
],
|
|
926
975
|
"(authed)/account/artifacts/[pieceId]/versions/[version]/file": ["apps/web/lib/server-api.ts"],
|
|
976
|
+
// #8515 D12 — the onboarding page imports its parent's account-data.ts
|
|
977
|
+
// and error-row.tsx (which renders retry-button.tsx); its actions forward
|
|
978
|
+
// the credential `forwardedCookieInit` returns. account-data.ts reads the
|
|
979
|
+
// account config, as the Artifacts pages' does.
|
|
980
|
+
"(authed)/account/onboarding": [
|
|
981
|
+
"apps/web/lib/account-config.ts",
|
|
982
|
+
"apps/web/lib/server-api.ts",
|
|
983
|
+
`${ROUTE_ROOT}/(authed)/account/account-data.ts`,
|
|
984
|
+
`${ROUTE_ROOT}/(authed)/account/error-row.tsx`,
|
|
985
|
+
`${ROUTE_ROOT}/(authed)/account/retry-button.tsx`,
|
|
986
|
+
],
|
|
927
987
|
"(authed)/account/support": [
|
|
928
988
|
"apps/web/lib/account-config.ts",
|
|
929
989
|
"apps/web/lib/server-api.ts",
|
|
@@ -997,10 +1057,13 @@ export const ROUTE_MESSAGE_NAMESPACES = {
|
|
|
997
1057
|
"(authed)/account/general": ["account"],
|
|
998
1058
|
"(authed)/account/security": ["account"],
|
|
999
1059
|
"(authed)/account/notifications": ["account"],
|
|
1060
|
+
"(authed)/account/privacy": ["account"],
|
|
1000
1061
|
"(authed)/account/billing": ["account"],
|
|
1001
1062
|
// #8234 — the two Artifacts pages; the file route renders no string.
|
|
1002
1063
|
"(authed)/account/artifacts": ["account"],
|
|
1003
1064
|
"(authed)/account/artifacts/[pieceId]": ["account"],
|
|
1065
|
+
// #8515 — the onboarding page.
|
|
1066
|
+
"(authed)/account/onboarding": ["account"],
|
|
1004
1067
|
"(authed)/admin": ["admin"],
|
|
1005
1068
|
"(authed)/leads": ["leadsInbox"],
|
|
1006
1069
|
// #5977 — every string on both editor routes comes from the `edit`
|
|
@@ -1012,6 +1075,8 @@ export const ROUTE_MESSAGE_NAMESPACES = {
|
|
|
1012
1075
|
"(authed)/waitlist": ["waitlistDashboard.gate"],
|
|
1013
1076
|
// #7744 — the /team hub's empty state reads the `team` namespace.
|
|
1014
1077
|
team: ["team"],
|
|
1078
|
+
// #8438 D21 — the unsubscribe landing's own namespace.
|
|
1079
|
+
unsubscribe: ["unsubscribe"],
|
|
1015
1080
|
};
|
|
1016
1081
|
/** Template-relative path of the generated site-nav registry. */
|
|
1017
1082
|
export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Where the per-file record lives, beside `.ailk-site-manifest.json`. */
|
|
2
|
+
export declare const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
|
|
3
|
+
/**
|
|
4
|
+
* The app paths a generator rewrites after `copyTemplate` lands its copy
|
|
5
|
+
* (§1): the nav, the site chrome, the site fonts, the paid-sections
|
|
6
|
+
* registry, and the four spoke seams. Composed from each writer's own
|
|
7
|
+
* exported path constant, so a renamed constant carries its path along
|
|
8
|
+
* here too (#8522 D2).
|
|
9
|
+
*/
|
|
10
|
+
export declare const REGENERATED_APP_PATHS: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Whether a destination path (`/`-separated, as `templateDestination`
|
|
13
|
+
* returns it) is one `update` ever records or touches (#8522 D2): under one
|
|
14
|
+
* of the three app trees, not a `package.json`, not an `.env*` file, no
|
|
15
|
+
* `.claude` segment, and not a path a generator rewrites after the copy.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isRecordable(destRel: string): boolean;
|
|
18
|
+
/** The hex sha256 digest of `bytes` — the one hash every record row uses. */
|
|
19
|
+
export declare function sha256Hex(bytes: Uint8Array): string;
|
|
20
|
+
export interface TemplateFileRow {
|
|
21
|
+
sha256: string;
|
|
22
|
+
version: string;
|
|
23
|
+
}
|
|
24
|
+
export interface TemplateRecord {
|
|
25
|
+
schema: 1;
|
|
26
|
+
projectName: string;
|
|
27
|
+
files: Record<string, TemplateFileRow>;
|
|
28
|
+
}
|
|
29
|
+
/** Thrown by `parseTemplateRecord` on a malformed or unsafe record. */
|
|
30
|
+
export declare class TemplateRecordError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse `.ailk-template-files.json` text, refusing — naming the field — on:
|
|
35
|
+
* JSON that does not parse; `schema !== 1`; an unknown top-level key; an
|
|
36
|
+
* empty `projectName`; a `files` key that is not a recordable path; a
|
|
37
|
+
* `sha256` that is not 64 lowercase hex; a `version` that is not a semver
|
|
38
|
+
* string (#8522 D1).
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseTemplateRecord(text: string): TemplateRecord;
|
|
41
|
+
/** `record` as `scaffold()` and `update` write it: sorted keys, trailing newline. */
|
|
42
|
+
export declare function serializeTemplateRecord(record: TemplateRecord): string;
|
|
43
|
+
export interface PlanTemplateRecordOptions {
|
|
44
|
+
/** The freshly scaffolded site. */
|
|
45
|
+
targetDir: string;
|
|
46
|
+
/** Destination path -> sha256 of the bytes `copyTemplate`'s `onWrite` wrote, for every recordable path. */
|
|
47
|
+
copied: ReadonlyMap<string, string>;
|
|
48
|
+
/** The create-ailk version that wrote this scaffold. */
|
|
49
|
+
version: string;
|
|
50
|
+
projectName: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The record a fresh scaffold writes (#8522 D2): one row per copied path
|
|
54
|
+
* whose file still exists and whose current bytes still hash to the copy —
|
|
55
|
+
* a later layer (a module bundle's patch, a generator) that rewrote the
|
|
56
|
+
* file drops it from the record, exactly as it is absent from the record a
|
|
57
|
+
* `create-ailk update` keeps current for it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function planTemplateRecord(opts: PlanTemplateRecordOptions): TemplateRecord;
|