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.
Files changed (132) hide show
  1. package/README.md +64 -7
  2. package/dist/cli.js +35 -0
  3. package/dist/copy.d.ts +41 -0
  4. package/dist/copy.js +87 -21
  5. package/dist/lib/fetch-template-snapshot.d.ts +21 -0
  6. package/dist/lib/fetch-template-snapshot.js +205 -0
  7. package/dist/lib/merge-three-way.d.ts +31 -0
  8. package/dist/lib/merge-three-way.js +65 -0
  9. package/dist/module-architecture.js +55 -2
  10. package/dist/programmatic.d.ts +7 -1
  11. package/dist/programmatic.js +32 -2
  12. package/dist/spoke-checkout.d.ts +7 -0
  13. package/dist/spoke-checkout.js +8 -1
  14. package/dist/surfaces.js +67 -2
  15. package/dist/template-record.d.ts +59 -0
  16. package/dist/template-record.js +183 -0
  17. package/dist/update.d.ts +48 -0
  18. package/dist/update.js +504 -0
  19. package/package.json +3 -3
  20. package/templates/.claude/cloudparity-repo-hooks.json +9 -0
  21. package/templates/.claude/hooks/architecture-check.sh +8 -0
  22. package/templates/.claude/rules/loop-contract.md +129 -0
  23. package/templates/.claude/settings.json +3 -2
  24. package/templates/.env.example +13 -0
  25. package/templates/CLAUDE.md +1 -1
  26. package/templates/CONVENTIONS.md +3 -2
  27. package/templates/_gitignore +162 -0
  28. package/templates/apps/api/CLAUDE.md +14 -10
  29. package/templates/apps/api/package.json +2 -1
  30. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
  31. package/templates/apps/api/src/__tests__/setup.ts +7 -0
  32. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
  33. package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
  34. package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
  35. package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
  36. package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
  37. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
  38. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
  39. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
  40. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
  41. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
  42. package/templates/apps/api/src/middleware/auth.ts +8 -4
  43. package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
  44. package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
  45. package/templates/apps/api/src/routes/webhooks/README.md +33 -5
  46. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
  47. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
  48. package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
  49. package/templates/apps/api/src/server.ts +14 -2
  50. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
  51. package/templates/apps/api/src/services/audit-log.ts +72 -10
  52. package/templates/apps/api/src/services/default-tenant.ts +12 -3
  53. package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
  54. package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
  55. package/templates/apps/api/vercel.json +1 -1
  56. package/templates/apps/mcp/CLAUDE.md +19 -7
  57. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +95 -8
  58. package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
  59. package/templates/apps/mcp/src/tools/proposals.ts +27 -0
  60. package/templates/apps/mcp/src/workspace-server.ts +16 -0
  61. package/templates/apps/web/_gitignore +6 -0
  62. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
  63. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
  64. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
  65. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
  66. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
  67. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
  68. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
  69. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
  70. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
  71. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
  72. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
  73. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
  74. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
  75. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
  76. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
  77. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
  78. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
  79. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
  80. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
  81. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
  82. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
  83. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
  84. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
  85. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
  86. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
  87. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
  88. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
  89. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
  90. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
  91. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
  92. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
  93. package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
  94. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
  95. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
  96. package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
  97. package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
  98. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +13 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
  100. package/templates/apps/web/app/[locale]/layout.tsx +23 -2
  101. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
  102. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
  103. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
  104. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
  105. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
  106. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
  107. package/templates/apps/web/app/robots.txt/route.ts +13 -10
  108. package/templates/apps/web/jest.config.cjs +3 -0
  109. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
  110. package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
  111. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +96 -7
  112. package/templates/apps/web/lib/account-config.ts +31 -16
  113. package/templates/apps/web/lib/consent-visitor.ts +103 -0
  114. package/templates/apps/web/lib/relay.ts +20 -0
  115. package/templates/apps/web/messages/en.json +67 -4
  116. package/templates/apps/web/next.config.mjs +8 -2
  117. package/templates/database/CHANGELOG.md +39 -0
  118. package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
  119. package/templates/database/__tests__/person-notes.db.test.ts +249 -0
  120. package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
  121. package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
  122. package/templates/database/_gitignore +8 -0
  123. package/templates/database/inbox/schema.prisma +38 -12
  124. package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
  125. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
  126. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
  127. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
  128. package/templates/database/ops/schema.prisma +16 -0
  129. package/templates/database/package.json +1 -1
  130. package/templates/project.yaml +6 -3
  131. package/versions.json +35 -35
  132. package/templates/.claude/rules/preserve-existing-style.md +0 -51
@@ -0,0 +1,183 @@
1
+ /**
2
+ * `.ailk-template-files.json` — the per-file record of what a scaffold's
3
+ * copied app code is (#8522 D1/D2).
4
+ *
5
+ * Written beside `.ailk-site-manifest.json` at the site root, it lists every
6
+ * `apps/web/`, `apps/api/` and `apps/mcp/` file the base `templates/` copy
7
+ * wrote and no later layer rewrote, with the sha256 of the template's copy
8
+ * of that file and the create-ailk version it came from. `create-ailk
9
+ * update` (`update.ts`) reads it to tell an untouched copy from an owner's
10
+ * edit; `scaffold()` (`programmatic.ts`) writes it, as its last write, from
11
+ * the `onWrite` hook `copyTemplate` (`copy.ts`) calls once per file.
12
+ *
13
+ * Pure module: no `import.meta.url`, no filesystem write — `planTemplateRecord`
14
+ * only READS the scaffold's own disk to confirm a copied path's final bytes
15
+ * (a later layer may have rewritten it, D2); every write goes through the
16
+ * caller.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { PAID_SECTIONS_PATH } from "./paid-sections.js";
22
+ import { SITE_CHROME_PATH } from "./site-chrome.js";
23
+ import { SITE_FONTS_PATH } from "./site-fonts.js";
24
+ import { NAV_CONFIG_PATH } from "./surfaces.js";
25
+ import { SPOKE_SEAM_FILES } from "./topology.js";
26
+ /** Where the per-file record lives, beside `.ailk-site-manifest.json`. */
27
+ export const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
28
+ /**
29
+ * The app paths a generator rewrites after `copyTemplate` lands its copy
30
+ * (§1): the nav, the site chrome, the site fonts, the paid-sections
31
+ * registry, and the four spoke seams. Composed from each writer's own
32
+ * exported path constant, so a renamed constant carries its path along
33
+ * here too (#8522 D2).
34
+ */
35
+ export const REGENERATED_APP_PATHS = [
36
+ NAV_CONFIG_PATH,
37
+ SITE_CHROME_PATH,
38
+ SITE_FONTS_PATH,
39
+ PAID_SECTIONS_PATH,
40
+ ...SPOKE_SEAM_FILES,
41
+ ];
42
+ const REGENERATED_SET = new Set(REGENERATED_APP_PATHS);
43
+ const RECORDABLE_APP_ROOT = /^apps\/(web|api|mcp)\//;
44
+ /**
45
+ * Whether a destination path (`/`-separated, as `templateDestination`
46
+ * returns it) is one `update` ever records or touches (#8522 D2): under one
47
+ * of the three app trees, not a `package.json`, not an `.env*` file, no
48
+ * `.claude` segment, and not a path a generator rewrites after the copy.
49
+ */
50
+ export function isRecordable(destRel) {
51
+ if (!RECORDABLE_APP_ROOT.test(destRel))
52
+ return false;
53
+ const segments = destRel.split("/");
54
+ const base = segments[segments.length - 1] ?? "";
55
+ if (base === "package.json")
56
+ return false;
57
+ if (base.startsWith(".env"))
58
+ return false;
59
+ if (segments.includes(".claude"))
60
+ return false;
61
+ if (REGENERATED_SET.has(destRel))
62
+ return false;
63
+ return true;
64
+ }
65
+ /** The hex sha256 digest of `bytes` — the one hash every record row uses. */
66
+ export function sha256Hex(bytes) {
67
+ return createHash("sha256").update(bytes).digest("hex");
68
+ }
69
+ /** Thrown by `parseTemplateRecord` on a malformed or unsafe record. */
70
+ export class TemplateRecordError extends Error {
71
+ constructor(message) {
72
+ super(message);
73
+ this.name = "TemplateRecordError";
74
+ }
75
+ }
76
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
77
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
78
+ /**
79
+ * Whether `key` is traversal-safe: no `..`, no empty segment, no backslash,
80
+ * no leading `/` (D1). This alone does not make `key` a path `update` may
81
+ * touch — `parseTemplateRecord` also requires `isRecordable(key)`, so a
82
+ * tampered record can never direct a write or delete outside the three app
83
+ * trees nor at a non-recordable path (`package.json`, `.env*`, a
84
+ * `REGENERATED_APP_PATHS` entry, …) inside them.
85
+ */
86
+ function isSafeRecordKey(key) {
87
+ if (key.length === 0 || key.startsWith("/") || key.includes("\\")) {
88
+ return false;
89
+ }
90
+ const segments = key.split("/");
91
+ return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
92
+ }
93
+ /**
94
+ * Parse `.ailk-template-files.json` text, refusing — naming the field — on:
95
+ * JSON that does not parse; `schema !== 1`; an unknown top-level key; an
96
+ * empty `projectName`; a `files` key that is not a recordable path; a
97
+ * `sha256` that is not 64 lowercase hex; a `version` that is not a semver
98
+ * string (#8522 D1).
99
+ */
100
+ export function parseTemplateRecord(text) {
101
+ let doc;
102
+ try {
103
+ doc = JSON.parse(text);
104
+ }
105
+ catch (err) {
106
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} is not valid JSON: ${err.message}`);
107
+ }
108
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
109
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} is not a JSON object`);
110
+ }
111
+ const record = doc;
112
+ const allowed = new Set(["schema", "projectName", "files"]);
113
+ for (const key of Object.keys(record)) {
114
+ if (!allowed.has(key)) {
115
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} has an unknown top-level key "${key}"`);
116
+ }
117
+ }
118
+ if (record["schema"] !== 1) {
119
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "schema" must be 1, got ${JSON.stringify(record["schema"])}`);
120
+ }
121
+ const projectName = record["projectName"];
122
+ if (typeof projectName !== "string" || projectName.length === 0) {
123
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "projectName" must be a non-empty string`);
124
+ }
125
+ const filesRaw = record["files"];
126
+ if (typeof filesRaw !== "object" || filesRaw === null || Array.isArray(filesRaw)) {
127
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files" must be an object`);
128
+ }
129
+ const files = {};
130
+ for (const [key, value] of Object.entries(filesRaw)) {
131
+ if (!isSafeRecordKey(key)) {
132
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files" has an unsafe key "${key}"`);
133
+ }
134
+ if (!isRecordable(key)) {
135
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files" has a key "${key}" that is not a recordable path`);
136
+ }
137
+ if (typeof value !== "object" || value === null) {
138
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files"["${key}"] must be an object`);
139
+ }
140
+ const row = value;
141
+ const sha = row["sha256"];
142
+ if (typeof sha !== "string" || !SHA256_PATTERN.test(sha)) {
143
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files"["${key}"].sha256 must be 64 lowercase hex characters`);
144
+ }
145
+ const version = row["version"];
146
+ if (typeof version !== "string" || !VERSION_PATTERN.test(version)) {
147
+ throw new TemplateRecordError(`${TEMPLATE_RECORD_PATH} "files"["${key}"].version must be a semver string`);
148
+ }
149
+ files[key] = { sha256: sha, version };
150
+ }
151
+ return { schema: 1, projectName, files };
152
+ }
153
+ /** `record` as `scaffold()` and `update` write it: sorted keys, trailing newline. */
154
+ export function serializeTemplateRecord(record) {
155
+ const files = {};
156
+ for (const key of Object.keys(record.files).sort()) {
157
+ files[key] = record.files[key];
158
+ }
159
+ return (JSON.stringify({ schema: record.schema, projectName: record.projectName, files }, null, 2) +
160
+ "\n");
161
+ }
162
+ /**
163
+ * The record a fresh scaffold writes (#8522 D2): one row per copied path
164
+ * whose file still exists and whose current bytes still hash to the copy —
165
+ * a later layer (a module bundle's patch, a generator) that rewrote the
166
+ * file drops it from the record, exactly as it is absent from the record a
167
+ * `create-ailk update` keeps current for it.
168
+ */
169
+ export function planTemplateRecord(opts) {
170
+ const files = {};
171
+ for (const path of [...opts.copied.keys()].sort()) {
172
+ const hash = opts.copied.get(path);
173
+ if (hash === undefined)
174
+ continue;
175
+ const abs = join(opts.targetDir, ...path.split("/"));
176
+ if (!existsSync(abs))
177
+ continue;
178
+ if (sha256Hex(readFileSync(abs)) !== hash)
179
+ continue;
180
+ files[path] = { sha256: hash, version: opts.version };
181
+ }
182
+ return { schema: 1, projectName: opts.projectName, files };
183
+ }
@@ -0,0 +1,48 @@
1
+ /** A refusal the CLI exits 2 on, thrown before anything is written. */
2
+ export declare class UpdateRefusedError extends Error {
3
+ readonly exitCode = 2;
4
+ constructor(message: string);
5
+ }
6
+ export interface UpdateSiteOptions {
7
+ /** The existing site. */
8
+ targetDir: string;
9
+ /** The running package's own `templates/` directory. */
10
+ templatesDir: string;
11
+ /** The running create-ailk version. */
12
+ version: string;
13
+ /** Overrides `AILK_NPM_REGISTRY_URL` / the public npm registry default. */
14
+ registryUrl?: string;
15
+ /** Injected fetch for tests. Never a live network call in CI. */
16
+ fetchImpl?: typeof fetch;
17
+ }
18
+ export interface UpdateResult {
19
+ targetDir: string;
20
+ version: string;
21
+ updated: string[];
22
+ merged: string[];
23
+ added: string[];
24
+ removed: string[];
25
+ current: string[];
26
+ review: Array<{
27
+ path: string;
28
+ reason: string;
29
+ }>;
30
+ skipped: Array<{
31
+ path: string;
32
+ reason: string;
33
+ }>;
34
+ backfilled?: {
35
+ version: string;
36
+ files: number;
37
+ };
38
+ }
39
+ export declare function updateSite(opts: UpdateSiteOptions): Promise<UpdateResult>;
40
+ /** The printed report (D9): one section per non-empty list, in order. */
41
+ export declare function formatUpdateReport(result: UpdateResult): string;
42
+ export interface UpdateArgs {
43
+ target: string | undefined;
44
+ help: boolean;
45
+ }
46
+ export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file whose template copy changed is\noverwritten. An edited file whose template copy also changed is three-way\nmerged with `git` (git must be on PATH); a conflict leaves the file alone\nand is listed under \"Needs review\" \u2014 nothing else in the run is held back.\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. Commit before running, and review\nthe diff after.\n";
47
+ /** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
48
+ export declare function parseUpdateArgs(argv: string[]): UpdateArgs;