create-ailk 0.6.0 → 0.7.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 +44 -5
- package/component-catalog.json +146 -21
- package/component-catalog.md +9 -6
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +124 -13
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +76 -2
- package/dist/lib/extract-module-bundle.js +219 -8
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +27 -4
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +7 -0
- package/dist/parse-args.js +23 -1
- package/dist/programmatic.d.ts +46 -0
- package/dist/programmatic.js +144 -19
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +494 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +149 -28
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +58 -2
- package/dist/template-manifest.js +196 -69
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +162 -0
- package/site-templates/marketing-site/DESIGN.md +162 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +162 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/minimalism-site/DESIGN.md +162 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/site-templates/saas-product/DESIGN.md +162 -0
- package/site-templates/simple-service/DESIGN.md +162 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
- package/site-templates/simple-service/ailk-template.json +4 -3
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
- package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +8 -8
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +111 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +10 -5
- package/templates/DESIGN.md +162 -0
- package/templates/README.md +134 -38
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
- package/templates/apps/api/src/config/index.ts +19 -6
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/server.ts +6 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/web/PRODUCT.md +86 -0
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
- package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +113 -51
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +64 -12
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +4 -8
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +134 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +52 -37
- package/templates/apps/web/middleware.ts +1 -1
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
- package/templates/content/_site.mdx +41 -3
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +105 -0
- package/templates/content/en/ContactPage/contact.mdx +44 -85
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +276 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
- package/templates/content/en/SoftwareProduct/product.mdx +175 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +3 -2
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/apps/web/public/logo-dark.svg +0 -21
- package/templates/apps/web/public/logo-light.svg +0 -21
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type ModulePatch } from "../module-architecture.js";
|
|
1
2
|
import { type TemplateLayout } from "../site-chrome.js";
|
|
2
|
-
import type
|
|
3
|
+
import { type MarkerMap, type WorkspaceVersionMap } from "../substitute.js";
|
|
3
4
|
/** Exported for `fetch-template.ts`/`fetch-module.ts`'s Content-Length guard (#5514) — one cap, not two literals. */
|
|
4
5
|
export declare const MAX_TOTAL_SIZE_BYTES: number;
|
|
5
6
|
/**
|
|
@@ -42,6 +43,12 @@ export interface ExtractModuleBundleOptions {
|
|
|
42
43
|
templateExtends?: string;
|
|
43
44
|
/** The call-scoped ownership registry (#5618 D4). See `InstallBundleTreeOptions`. */
|
|
44
45
|
ownership?: LayerOwnership;
|
|
46
|
+
/** The scaffold's marker map (#6460). See `InstallBundleTreeOptions`. */
|
|
47
|
+
markers?: MarkerMap;
|
|
48
|
+
/** What to do with a destination that already exists (#6653 D3). See `InstallBundleTreeOptions`. */
|
|
49
|
+
existingFilePolicy?: ExistingFilePolicy;
|
|
50
|
+
/** A caller check on the manifest's `patches` (#6653 D3). See `InstallBundleTreeOptions`. */
|
|
51
|
+
checkPatches?: (patches: readonly ModulePatch[]) => void;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Extract a fetched module bundle into `targetDir`, merging its files onto
|
|
@@ -89,8 +96,64 @@ export interface InstallBundleTreeOptions {
|
|
|
89
96
|
ownership?: LayerOwnership;
|
|
90
97
|
/** Canonical `@working-theory/*` versions for `workspace:*` substitution in patch targets. */
|
|
91
98
|
versions?: WorkspaceVersionMap;
|
|
99
|
+
/**
|
|
100
|
+
* The scaffold's marker map (#6460) — `copy.ts`'s `buildScaffoldMarkers`,
|
|
101
|
+
* the SAME object `copyTemplate` substituted the base `templates/` copy
|
|
102
|
+
* with, threaded here so a `{{MARKER}}` resolves to the same value whichever
|
|
103
|
+
* layer authored the file it sits in. Applied by `copyTree` and the
|
|
104
|
+
* installMap loop below, to text files only (`copy.ts`'s own `isTextFile`),
|
|
105
|
+
* so the classifier is one definition rather than two. Absent ⟹ files land
|
|
106
|
+
* byte-for-byte as committed, the pre-#6460 behaviour.
|
|
107
|
+
*
|
|
108
|
+
* EVERY layer that lands on the snapshot gets it, modules included — not
|
|
109
|
+
* just the base layer and the site template. A bundle fetched by version
|
|
110
|
+
* from the paid registry may have been produced by an older create-ailk
|
|
111
|
+
* that never saw this pass, so a bundle author should know that the five
|
|
112
|
+
* mapped keys, and only those, now resolve in their `.md`/`.json`/`.ts`
|
|
113
|
+
* files; `substituteMarkers` leaves every other `{{KEY}}` alone.
|
|
114
|
+
*/
|
|
115
|
+
markers?: MarkerMap;
|
|
116
|
+
/**
|
|
117
|
+
* What to do with a destination that already exists (#6653 D3). Default
|
|
118
|
+
* `"overwrite"` — the scaffold's layer-over-layer behaviour, unchanged.
|
|
119
|
+
* `"skip-existing"` — the `add-module` merge onto a tree a customer has
|
|
120
|
+
* lived in: an existing file is never written; when its bytes differ from
|
|
121
|
+
* what this install would write it is reported `skipped` with the change
|
|
122
|
+
* the bundle wanted, and when they are equal it is not reported at all.
|
|
123
|
+
* `patches` are NOT gated by it — they are the one mechanism designed to
|
|
124
|
+
* edit an existing file, and they are already idempotent.
|
|
125
|
+
*/
|
|
126
|
+
existingFilePolicy?: ExistingFilePolicy;
|
|
127
|
+
/**
|
|
128
|
+
* Run against the manifest's `patches` after they validate and BEFORE any
|
|
129
|
+
* of them is computed or anything is written (#6653 D3) — a throw rejects
|
|
130
|
+
* the tree with `targetDir` untouched. `add-module` uses it to hold a
|
|
131
|
+
* bundle's patches to the plus-config allowlist.
|
|
132
|
+
*/
|
|
133
|
+
checkPatches?: (patches: readonly ModulePatch[]) => void;
|
|
92
134
|
}
|
|
93
|
-
/**
|
|
135
|
+
/** See `InstallBundleTreeOptions.existingFilePolicy`. */
|
|
136
|
+
export type ExistingFilePolicy = "overwrite" | "skip-existing";
|
|
137
|
+
/**
|
|
138
|
+
* One file this install landed, patched, or declined to land (#6653 D3). A
|
|
139
|
+
* destination whose bytes already equal the install's produces no row.
|
|
140
|
+
* `change` / `wanted` is a unified diff (`diffTwoStrings`) — for `skipped`,
|
|
141
|
+
* the exact edit the bundle wanted, so the row is a manual-review item and
|
|
142
|
+
* never a bare path.
|
|
143
|
+
*/
|
|
144
|
+
export type InstallReportRow = {
|
|
145
|
+
path: string;
|
|
146
|
+
action: "written";
|
|
147
|
+
} | {
|
|
148
|
+
path: string;
|
|
149
|
+
action: "patched";
|
|
150
|
+
change: string;
|
|
151
|
+
} | {
|
|
152
|
+
path: string;
|
|
153
|
+
action: "skipped";
|
|
154
|
+
wanted: string;
|
|
155
|
+
};
|
|
156
|
+
/** `installBundleTree`'s result. */
|
|
94
157
|
export interface InstallBundleTreeResult {
|
|
95
158
|
/**
|
|
96
159
|
* The installed template's validated `layout` block (#5612 D1), unresolved
|
|
@@ -98,6 +161,8 @@ export interface InstallBundleTreeResult {
|
|
|
98
161
|
* module install, or a template whose manifest declares no `layout`.
|
|
99
162
|
*/
|
|
100
163
|
layout?: TemplateLayout;
|
|
164
|
+
/** One row per file written, patched or skipped, in install order (#6653 D3). */
|
|
165
|
+
report: InstallReportRow[];
|
|
101
166
|
}
|
|
102
167
|
/**
|
|
103
168
|
* Apply one resolved manifest's `installMap`, plain content and `patches`
|
|
@@ -110,3 +175,12 @@ export interface InstallBundleTreeResult {
|
|
|
110
175
|
* rejected tree leaves `targetDir` exactly as it was.
|
|
111
176
|
*/
|
|
112
177
|
export declare function installBundleTree(options: InstallBundleTreeOptions): InstallBundleTreeResult;
|
|
178
|
+
/**
|
|
179
|
+
* A unified diff of `before` → `after` for the report (#6653 D3) — the one
|
|
180
|
+
* helper behind both a `skipped` row's `wanted` and a `patched` row's
|
|
181
|
+
* `change`. Line-based LCS; the inputs are config and content files, small
|
|
182
|
+
* enough that the quadratic table is not a concern. `isText` false yields the
|
|
183
|
+
* binary summary instead, reading each string's length as its byte count
|
|
184
|
+
* (the caller decodes binary content as latin1).
|
|
185
|
+
*/
|
|
186
|
+
export declare function diffTwoStrings(path: string, before: string, after: string, isText: boolean): string;
|
|
@@ -83,8 +83,10 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
|
83
83
|
import { pipeline } from "node:stream/promises";
|
|
84
84
|
import { createGunzip } from "node:zlib";
|
|
85
85
|
import * as tar from "tar";
|
|
86
|
+
import { isTextFile } from "../copy.js";
|
|
86
87
|
import { coOwnedModuleDestinations, } from "../module-architecture.js";
|
|
87
88
|
import { validateTemplateLayout, } from "../site-chrome.js";
|
|
89
|
+
import { substituteMarkers, } from "../substitute.js";
|
|
88
90
|
import { computeModulePatches, writeModulePatches } from "./apply-module-patches.js";
|
|
89
91
|
/** Soft limits to prevent decompression-bomb / zip-bomb attacks. Same values as extractBundle. */
|
|
90
92
|
const MAX_ENTRIES = 1000;
|
|
@@ -98,6 +100,8 @@ const AILK_MODULE_JSON = "ailk-module.json";
|
|
|
98
100
|
* Never copied into the scaffold either.
|
|
99
101
|
*/
|
|
100
102
|
const AILK_TEMPLATE_JSON = "ailk-template.json";
|
|
103
|
+
/** A template's hand-and-generated roster document — never installed (#6658 D7). */
|
|
104
|
+
const TEMPLATE_MANIFEST_MD = "TEMPLATE-MANIFEST.md";
|
|
101
105
|
/** The op names valid for each patch `kind` (spec 5418 § 3 D1's ModulePatchOp union, partitioned). */
|
|
102
106
|
const VALID_OPS_BY_KIND = {
|
|
103
107
|
json: new Set(["merge-keys", "append-unique"]),
|
|
@@ -446,6 +450,13 @@ export async function extractModuleBundle(options) {
|
|
|
446
450
|
: {}),
|
|
447
451
|
...(options.ownership !== undefined ? { ownership: options.ownership } : {}),
|
|
448
452
|
...(options.versions !== undefined ? { versions: options.versions } : {}),
|
|
453
|
+
...(options.markers !== undefined ? { markers: options.markers } : {}),
|
|
454
|
+
...(options.existingFilePolicy !== undefined
|
|
455
|
+
? { existingFilePolicy: options.existingFilePolicy }
|
|
456
|
+
: {}),
|
|
457
|
+
...(options.checkPatches !== undefined
|
|
458
|
+
? { checkPatches: options.checkPatches }
|
|
459
|
+
: {}),
|
|
449
460
|
});
|
|
450
461
|
}
|
|
451
462
|
finally {
|
|
@@ -463,7 +474,7 @@ export async function extractModuleBundle(options) {
|
|
|
463
474
|
* rejected tree leaves `targetDir` exactly as it was.
|
|
464
475
|
*/
|
|
465
476
|
export function installBundleTree(options) {
|
|
466
|
-
const { sourceRoot, name, templateRequires, templateExtends, ownership } = options;
|
|
477
|
+
const { sourceRoot, name, templateRequires, templateExtends, ownership, markers } = options;
|
|
467
478
|
const targetAbs = resolve(options.targetDir);
|
|
468
479
|
const isTemplate = templateRequires !== undefined;
|
|
469
480
|
const subject = isTemplate ? `template "${name}"` : `module "${name}"`;
|
|
@@ -491,6 +502,7 @@ export function installBundleTree(options) {
|
|
|
491
502
|
throw new Error(`${subject}: a template must not carry patches — content, never code (spec 5462 D9)`);
|
|
492
503
|
}
|
|
493
504
|
validatePatches(patches, targetAbs, subject);
|
|
505
|
+
options.checkPatches?.(patches);
|
|
494
506
|
// The exclusion set copyTree/the content-only guard both use — built from
|
|
495
507
|
// NORMALIZED installMap keys (#5514) so a "./x" key excludes the same
|
|
496
508
|
// string `rel`'s traversal below computes, rather than colliding with it
|
|
@@ -503,6 +515,11 @@ export function installBundleTree(options) {
|
|
|
503
515
|
...Object.keys(installMap).map((key) => normalize(key).split(sep).join("/")),
|
|
504
516
|
]);
|
|
505
517
|
if (isTemplate) {
|
|
518
|
+
// #6658 D7 — a template's roster of record lands as JSON, through its
|
|
519
|
+
// `installMap` (`TEMPLATE-MANIFEST.json` → `.ailk-site-manifest.json`).
|
|
520
|
+
// The Markdown is the template's own documentation: bundles still carry
|
|
521
|
+
// it, but it never reaches a scaffold's root.
|
|
522
|
+
excluded.add(TEMPLATE_MANIFEST_MD);
|
|
506
523
|
validateTemplateContentOnly(sourceRoot, excluded, subject);
|
|
507
524
|
}
|
|
508
525
|
// #5618 D4 — the additive-overlap check, at the same seam as the
|
|
@@ -526,6 +543,17 @@ export function installBundleTree(options) {
|
|
|
526
543
|
// installMap destination) is read from the STAGED tree here, since
|
|
527
544
|
// targetDir would not have it yet at this point.
|
|
528
545
|
const installMapDestinationToSource = new Map(Object.entries(installMap).map(([source, destination]) => [destination, source]));
|
|
546
|
+
// #6460 — a patch target is read RAW from the staged tree here, while
|
|
547
|
+
// `copyTree` below writes the same path SUBSTITUTED, and
|
|
548
|
+
// `writeModulePatches` runs after `copyTree`. So a file a bundle both
|
|
549
|
+
// delivers as plain content AND patches would end up carrying unsubstituted
|
|
550
|
+
// markers. Unreachable today and asserted as such rather than guarded: the
|
|
551
|
+
// only patch targets any slice declares are `apps/web/package.json`,
|
|
552
|
+
// `next.config.mjs` and `jest.config.cjs`, all three delivered by the base
|
|
553
|
+
// `templates/` snapshot rather than by a bundle — so this reader falls
|
|
554
|
+
// through to the `targetAbs` branch and reads already-substituted bytes —
|
|
555
|
+
// and a template is refused `patches` outright above. A bundle that starts
|
|
556
|
+
// delivering one of its own patch targets must substitute here too.
|
|
529
557
|
const readPatchTarget = (target) => {
|
|
530
558
|
const mappedSource = installMapDestinationToSource.get(target);
|
|
531
559
|
if (mappedSource !== undefined) {
|
|
@@ -539,6 +567,20 @@ export function installBundleTree(options) {
|
|
|
539
567
|
const targetAbsPath = resolve(targetAbs, target);
|
|
540
568
|
return existsSync(targetAbsPath) ? readFileSync(targetAbsPath, "utf8") : undefined;
|
|
541
569
|
};
|
|
570
|
+
// #6653 — under skip-existing, a patch target the tree ALSO delivers would be
|
|
571
|
+
// computed from the bundle's copy and written over the site's file, which
|
|
572
|
+
// the policy exists to leave alone. No slice does this today; refuse it.
|
|
573
|
+
if (options.existingFilePolicy === "skip-existing") {
|
|
574
|
+
for (const patch of patches) {
|
|
575
|
+
const target = toPosix(patch.target);
|
|
576
|
+
const staged = resolve(sourceRoot, patch.target);
|
|
577
|
+
const delivered = [...installMapDestinationToSource.keys()].some((d) => toPosix(d) === target) ||
|
|
578
|
+
(!excluded.has(target) && existsSync(staged) && statSync(staged).isFile());
|
|
579
|
+
if (delivered) {
|
|
580
|
+
throw new Error(`${subject}: patch target ${target} is also delivered by the bundle — refused, because it would replace the site's own file`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
542
584
|
const computedPatches = computeModulePatches(patches, {
|
|
543
585
|
versions: options.versions ?? {},
|
|
544
586
|
readTarget: readPatchTarget,
|
|
@@ -550,18 +592,38 @@ export function installBundleTree(options) {
|
|
|
550
592
|
// every installMap source are excluded from the whole-tree copy — the
|
|
551
593
|
// manifest never lands in the scaffold, and a mapped source lands ONLY
|
|
552
594
|
// at its mapped destination, never also at its original path.
|
|
553
|
-
|
|
595
|
+
const report = [];
|
|
596
|
+
const policy = options.existingFilePolicy ?? "overwrite";
|
|
597
|
+
copyTree(sourceRoot, targetAbs, excluded, markers, policy, report);
|
|
554
598
|
for (const [source, destination] of Object.entries(installMap)) {
|
|
555
599
|
const destAbs = resolve(targetAbs, destination);
|
|
556
600
|
mkdirSync(join(destAbs, ".."), { recursive: true });
|
|
557
|
-
|
|
601
|
+
// BOTH ends decide text-ness (#6460), because an installMap entry is free
|
|
602
|
+
// to rename across the boundary and the two mismatches are not
|
|
603
|
+
// symmetric: a text source landing at a binary name merely misses a
|
|
604
|
+
// substitution, while a BINARY source landing at a text name would be
|
|
605
|
+
// read as utf8 and rewritten, turning every invalid byte sequence into
|
|
606
|
+
// U+FFFD — silent, unrecoverable corruption. Requiring both makes the
|
|
607
|
+
// safe direction structural rather than a property of today's manifests.
|
|
608
|
+
const row = writeOneFile(resolve(sourceRoot, source), destAbs, toPosix(destination), [source, destination], markers, policy);
|
|
609
|
+
if (row !== undefined)
|
|
610
|
+
report.push(row);
|
|
558
611
|
}
|
|
559
612
|
// patches write AFTER every file the tree brought is in place (D5) —
|
|
560
613
|
// after installMap, before postInstall — so a patch can never race a
|
|
561
614
|
// file its own ownedFiles/installMap just delivered, and (since it was
|
|
562
615
|
// COMPUTED above, before either ran) overwrites rather than conflicts
|
|
563
616
|
// with content copyTree/installMap just placed at the same path.
|
|
617
|
+
// computeModulePatches already dropped every already-applied target, so an
|
|
618
|
+
// idempotent replay produces no `patched` row. Diffed before the write, while
|
|
619
|
+
// `readPatchTarget` still returns the pre-patch bytes.
|
|
620
|
+
const patchedRows = computedPatches.map(({ target, content }) => ({
|
|
621
|
+
path: toPosix(target),
|
|
622
|
+
action: "patched",
|
|
623
|
+
change: diffTwoStrings(toPosix(target), readPatchTarget(target) ?? "", content, true),
|
|
624
|
+
}));
|
|
564
625
|
writeModulePatches(computedPatches, targetAbs);
|
|
626
|
+
report.push(...patchedRows);
|
|
565
627
|
// #5618 D4 — record this layer's destinations only now that every one of
|
|
566
628
|
// them is in place (a later layer's overwrite is judged against the layer
|
|
567
629
|
// that actually wrote the bytes). Patch targets are NOT recorded: a patch
|
|
@@ -577,9 +639,12 @@ export function installBundleTree(options) {
|
|
|
577
639
|
// validateTemplateLayout); returned as-is (defaults unfilled) for the
|
|
578
640
|
// caller to resolve alongside whatever the code registry's own
|
|
579
641
|
// `TemplateManifest.layout` says.
|
|
580
|
-
return
|
|
581
|
-
|
|
582
|
-
|
|
642
|
+
return {
|
|
643
|
+
report,
|
|
644
|
+
...(isTemplate && manifest?.layout !== undefined
|
|
645
|
+
? { layout: manifest.layout }
|
|
646
|
+
: {}),
|
|
647
|
+
};
|
|
583
648
|
}
|
|
584
649
|
/**
|
|
585
650
|
* Recursively copy every file from `srcRoot` into `destRoot`, creating parent
|
|
@@ -589,8 +654,11 @@ export function installBundleTree(options) {
|
|
|
589
654
|
* `safeExtractTarGz` has validated every entry, so no additional
|
|
590
655
|
* path-safety check is needed here — `srcRoot` is our own staging tree, not
|
|
591
656
|
* attacker-controlled input.
|
|
657
|
+
*
|
|
658
|
+
* `markers`, when given, is substituted into every TEXT file on the way
|
|
659
|
+
* through (#6460) — see `InstallBundleTreeOptions.markers`.
|
|
592
660
|
*/
|
|
593
|
-
function copyTree(srcRoot, destRoot, excluded
|
|
661
|
+
function copyTree(srcRoot, destRoot, excluded, markers, policy, report) {
|
|
594
662
|
const stack = [srcRoot];
|
|
595
663
|
while (stack.length > 0) {
|
|
596
664
|
const dir = stack.pop();
|
|
@@ -608,12 +676,155 @@ function copyTree(srcRoot, destRoot, excluded = new Set()) {
|
|
|
608
676
|
if (excluded.has(rel.split(sep).join("/")))
|
|
609
677
|
continue;
|
|
610
678
|
mkdirSync(join(destPath, ".."), { recursive: true });
|
|
611
|
-
|
|
679
|
+
const row = writeOneFile(srcPath, destPath, toPosix(rel), [rel], markers, policy);
|
|
680
|
+
if (row !== undefined)
|
|
681
|
+
report.push(row);
|
|
612
682
|
}
|
|
613
683
|
// symlinks: never present — safeExtractTarGz rejects them outright.
|
|
614
684
|
}
|
|
615
685
|
}
|
|
616
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Land one file at `destAbs`, substituting `markers` into it when EVERY path
|
|
689
|
+
* in `relsForType` is text by `copy.ts`'s own `isTextFile` (#6460) — the
|
|
690
|
+
* source's relative path alone for a plain-content copy, source AND
|
|
691
|
+
* destination for an installMap entry, which may rename across the
|
|
692
|
+
* text/binary boundary. Unanimity rather than either-or, so binary content is
|
|
693
|
+
* never round-tripped through a utf8 string by construction; see the
|
|
694
|
+
* installMap loop for why the two mismatch directions are not symmetric.
|
|
695
|
+
*
|
|
696
|
+
* With no `markers`, or with any non-text path, this is exactly the
|
|
697
|
+
* `copyFileSync` it replaced.
|
|
698
|
+
*
|
|
699
|
+
* `isTextFile` is an allowlist of extensions, NOT "anything that parses as
|
|
700
|
+
* text": `.css`, `.html`, `.svg`, `.sh`, `.toml` and `.sql` are outside it, so
|
|
701
|
+
* a marker authored in one of those survives verbatim. That is pre-existing
|
|
702
|
+
* `copyTemplate` behaviour, deliberately shared rather than forked here.
|
|
703
|
+
*
|
|
704
|
+
* The source's mode rides along, because `copyFileSync` preserves it and
|
|
705
|
+
* `writeFileSync` does not: no tree this installs carries an executable file
|
|
706
|
+
* today, but a module slice is free to ship one, and a substituted `.mjs`
|
|
707
|
+
* silently losing its `+x` surfaces as "the scaffold builds but the hook never
|
|
708
|
+
* runs". Masked to the permission bits — `statSync().mode` carries the
|
|
709
|
+
* `S_IFREG` type bits too, which `open(2)` ignores but which have no business
|
|
710
|
+
* being passed. Like `copyFileSync`, this only sets the mode when the file is
|
|
711
|
+
* CREATED: an overwrite (the layer-over-layer case) keeps the existing one.
|
|
712
|
+
*
|
|
713
|
+
* `policy` (#6653 D3) is checked here, once, so `copyTree` and the installMap
|
|
714
|
+
* loop cannot diverge. Under `"skip-existing"` an existing destination is
|
|
715
|
+
* compared against the bytes this call WOULD write (post-substitution): equal
|
|
716
|
+
* → no row, nothing written; different → a `skipped` row carrying the diff.
|
|
717
|
+
* `path` is the scaffold-relative name the returned row carries.
|
|
718
|
+
*/
|
|
719
|
+
function writeOneFile(srcAbs, destAbs, path, relsForType, markers, policy) {
|
|
720
|
+
const isText = relsForType.every(isTextFile);
|
|
721
|
+
const substitute = markers !== undefined && isText;
|
|
722
|
+
if (policy === "skip-existing" && existsSync(destAbs)) {
|
|
723
|
+
const wanted = substitute
|
|
724
|
+
? Buffer.from(substituteMarkers(readFileSync(srcAbs, "utf8"), markers))
|
|
725
|
+
: readFileSync(srcAbs);
|
|
726
|
+
const existing = readFileSync(destAbs);
|
|
727
|
+
if (existing.equals(wanted))
|
|
728
|
+
return undefined;
|
|
729
|
+
// latin1 keeps one char per byte, so a binary pair's lengths stay byte counts.
|
|
730
|
+
const encoding = isText ? "utf8" : "latin1";
|
|
731
|
+
return {
|
|
732
|
+
path,
|
|
733
|
+
action: "skipped",
|
|
734
|
+
wanted: diffTwoStrings(path, existing.toString(encoding), wanted.toString(encoding), isText),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
if (!substitute) {
|
|
738
|
+
copyFileSync(srcAbs, destAbs);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
writeFileSync(destAbs, substituteMarkers(readFileSync(srcAbs, "utf8"), markers), {
|
|
742
|
+
mode: statSync(srcAbs).mode & 0o777,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
return { path, action: "written" };
|
|
746
|
+
}
|
|
747
|
+
function toPosix(rel) {
|
|
748
|
+
return normalize(rel).split(sep).join("/");
|
|
749
|
+
}
|
|
750
|
+
/** Lines of context around each change in `diffTwoStrings`'s hunks. */
|
|
751
|
+
const DIFF_CONTEXT = 3;
|
|
752
|
+
/**
|
|
753
|
+
* A unified diff of `before` → `after` for the report (#6653 D3) — the one
|
|
754
|
+
* helper behind both a `skipped` row's `wanted` and a `patched` row's
|
|
755
|
+
* `change`. Line-based LCS; the inputs are config and content files, small
|
|
756
|
+
* enough that the quadratic table is not a concern. `isText` false yields the
|
|
757
|
+
* binary summary instead, reading each string's length as its byte count
|
|
758
|
+
* (the caller decodes binary content as latin1).
|
|
759
|
+
*/
|
|
760
|
+
export function diffTwoStrings(path, before, after, isText) {
|
|
761
|
+
if (!isText) {
|
|
762
|
+
return `binary content differs (${before.length} bytes existing vs ${after.length} bytes bundle)`;
|
|
763
|
+
}
|
|
764
|
+
// A final newline terminates the last line; it is not an empty line after it.
|
|
765
|
+
const lines = (text) => (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n");
|
|
766
|
+
const a = before === "" ? [] : lines(before);
|
|
767
|
+
const b = after === "" ? [] : lines(after);
|
|
768
|
+
// lcs[i][j] = LCS length of a[i..] and b[j..].
|
|
769
|
+
const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
770
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
771
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
772
|
+
lcs[i][j] =
|
|
773
|
+
a[i] === b[j]
|
|
774
|
+
? lcs[i + 1][j + 1] + 1
|
|
775
|
+
: Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const ops = [];
|
|
779
|
+
let i = 0;
|
|
780
|
+
let j = 0;
|
|
781
|
+
while (i < a.length || j < b.length) {
|
|
782
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
783
|
+
ops.push({ kind: " ", line: a[i], ai: i + 1, bi: j + 1 });
|
|
784
|
+
i++;
|
|
785
|
+
j++;
|
|
786
|
+
}
|
|
787
|
+
else if (i < a.length && (j === b.length || lcs[i + 1][j] >= lcs[i][j + 1])) {
|
|
788
|
+
ops.push({ kind: "-", line: a[i], ai: i + 1, bi: j });
|
|
789
|
+
i++;
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
ops.push({ kind: "+", line: b[j], ai: i, bi: j + 1 });
|
|
793
|
+
j++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const out = [`--- a/${path}`, `+++ b/${path}`];
|
|
797
|
+
if (ops.every((op) => op.kind === " ")) {
|
|
798
|
+
out.push("(the files differ only in their final newline)");
|
|
799
|
+
return out.join("\n");
|
|
800
|
+
}
|
|
801
|
+
let k = 0;
|
|
802
|
+
while (k < ops.length) {
|
|
803
|
+
if (ops[k].kind === " ") {
|
|
804
|
+
k++;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
// Grow a hunk from this change until DIFF_CONTEXT*2 unchanged lines separate it from the next.
|
|
808
|
+
const start = Math.max(0, k - DIFF_CONTEXT);
|
|
809
|
+
let end = k;
|
|
810
|
+
let unchanged = 0;
|
|
811
|
+
while (end < ops.length && unchanged <= DIFF_CONTEXT * 2) {
|
|
812
|
+
unchanged = ops[end].kind === " " ? unchanged + 1 : 0;
|
|
813
|
+
end++;
|
|
814
|
+
}
|
|
815
|
+
end = Math.min(ops.length, end - Math.max(0, unchanged - DIFF_CONTEXT));
|
|
816
|
+
const hunk = ops.slice(start, end);
|
|
817
|
+
const oldLines = hunk.filter((op) => op.kind !== "+").length;
|
|
818
|
+
const newLines = hunk.filter((op) => op.kind !== "-").length;
|
|
819
|
+
const oldStart = hunk.find((op) => op.kind !== "+")?.ai ?? hunk[0].ai;
|
|
820
|
+
const newStart = hunk.find((op) => op.kind !== "-")?.bi ?? hunk[0].bi;
|
|
821
|
+
out.push(`@@ -${oldStart},${oldLines} +${newStart},${newLines} @@`);
|
|
822
|
+
for (const op of hunk)
|
|
823
|
+
out.push(`${op.kind}${op.line}`);
|
|
824
|
+
k = end;
|
|
825
|
+
}
|
|
826
|
+
return out.join("\n");
|
|
827
|
+
}
|
|
617
828
|
/**
|
|
618
829
|
* `extractBundle`'s validating-filter tar extraction, ported verbatim (see
|
|
619
830
|
* file doc for why this is a port and not an import). Extracts `tarGzPath`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const LICENSE_KEY_FILE = "ailk.key";
|
|
2
|
+
export declare const LICENSE_ENV_FILE: string;
|
|
3
|
+
export declare const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
|
|
4
|
+
/**
|
|
5
|
+
* Throws unless `key` is 1–4096 characters of `[A-Za-z0-9._-]`. The message
|
|
6
|
+
* describes the rule, never the value.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertLicenseKeyShape(key: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
|
|
11
|
+
* `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
|
|
12
|
+
* `ailk.key` when no existing line covers it. Idempotent — a second call with
|
|
13
|
+
* the same key changes no byte.
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeLicenseKeyFiles(dir: string, key: string): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a scaffold keeps its licence key (#6683 § 3 D9).
|
|
3
|
+
*
|
|
4
|
+
* Two copies, both owner-only and both gitignored:
|
|
5
|
+
*
|
|
6
|
+
* - `ailk.key` at the scaffold root — the key on its own, for a person or a
|
|
7
|
+
* tool that needs it again (`license set` re-pushes it to Vercel);
|
|
8
|
+
* - `apps/api/.env` — one `AILK_LICENSE_KEY=<key>` line, because `apps/api`
|
|
9
|
+
* is the scaffold's runtime reader and loads `.env` from its own directory.
|
|
10
|
+
*
|
|
11
|
+
* `assertLicenseKeyShape` is the ONE validator every entry point calls
|
|
12
|
+
* (`--license`, `license set <key>`, `license set -`): a compact JWS is
|
|
13
|
+
* base64url segments joined by dots, so anything else — whitespace, a newline
|
|
14
|
+
* that would inject a second `.env` line — is refused, and the refusal never
|
|
15
|
+
* repeats the value (it may be a real key in a shell history or a CI log).
|
|
16
|
+
*
|
|
17
|
+
* Pure module: no `import.meta.url`, so jest imports it directly.
|
|
18
|
+
*/
|
|
19
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
export const LICENSE_KEY_FILE = "ailk.key";
|
|
22
|
+
export const LICENSE_ENV_FILE = join("apps", "api", ".env");
|
|
23
|
+
export const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
|
|
24
|
+
const KEY_SHAPE = /^[A-Za-z0-9._-]{1,4096}$/;
|
|
25
|
+
/** `.gitignore` lines that already ignore a root `ailk.key`. */
|
|
26
|
+
const KEY_IGNORE_LINES = new Set([
|
|
27
|
+
"ailk.key",
|
|
28
|
+
"/ailk.key",
|
|
29
|
+
"**/ailk.key",
|
|
30
|
+
"*.key",
|
|
31
|
+
"/*.key",
|
|
32
|
+
"**/*.key",
|
|
33
|
+
]);
|
|
34
|
+
/** A line assigning the variable, with or without `export`. */
|
|
35
|
+
const ENV_LINE = new RegExp(`^\\s*(?:export\\s+)?${LICENSE_ENV_VAR}\\s*=`);
|
|
36
|
+
/**
|
|
37
|
+
* Throws unless `key` is 1–4096 characters of `[A-Za-z0-9._-]`. The message
|
|
38
|
+
* describes the rule, never the value.
|
|
39
|
+
*/
|
|
40
|
+
export function assertLicenseKeyShape(key) {
|
|
41
|
+
if (!KEY_SHAPE.test(key)) {
|
|
42
|
+
throw new Error("the licence key is malformed: expected 1-4096 characters of A-Z a-z 0-9 . _ - (a compact JWS, no whitespace or newline). Copy it again from your welcome page.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write `content` only when it differs, with mode 0600 either way. An existing
|
|
47
|
+
* file is tightened BEFORE the write — `mode` applies only on creation, so a
|
|
48
|
+
* looser pre-existing `.env` would otherwise hold the key for a moment at its
|
|
49
|
+
* old permissions.
|
|
50
|
+
*/
|
|
51
|
+
function writeOwnerOnly(path, content) {
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
if (existsSync(path))
|
|
54
|
+
chmodSync(path, 0o600);
|
|
55
|
+
if (!existsSync(path) || readFileSync(path, "utf8") !== content) {
|
|
56
|
+
writeFileSync(path, content, { mode: 0o600 });
|
|
57
|
+
}
|
|
58
|
+
chmodSync(path, 0o600);
|
|
59
|
+
}
|
|
60
|
+
/** `text` plus `line`, starting on a fresh line. */
|
|
61
|
+
function appendLine(text, line) {
|
|
62
|
+
const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
|
|
63
|
+
return `${text}${sep}${line}\n`;
|
|
64
|
+
}
|
|
65
|
+
/** Replace the first assignment line, drop any later duplicate, else append. */
|
|
66
|
+
function upsertEnvLine(existing, key) {
|
|
67
|
+
const line = `${LICENSE_ENV_VAR}=${key}`;
|
|
68
|
+
const lines = existing.split("\n");
|
|
69
|
+
let seen = false;
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const l of lines) {
|
|
72
|
+
if (ENV_LINE.test(l)) {
|
|
73
|
+
if (!seen)
|
|
74
|
+
out.push(line);
|
|
75
|
+
seen = true;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
out.push(l);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return seen ? out.join("\n") : appendLine(existing, line);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
|
|
85
|
+
* `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
|
|
86
|
+
* `ailk.key` when no existing line covers it. Idempotent — a second call with
|
|
87
|
+
* the same key changes no byte.
|
|
88
|
+
*/
|
|
89
|
+
export function writeLicenseKeyFiles(dir, key) {
|
|
90
|
+
assertLicenseKeyShape(key);
|
|
91
|
+
writeOwnerOnly(join(dir, LICENSE_KEY_FILE), `${key}\n`);
|
|
92
|
+
const envPath = join(dir, LICENSE_ENV_FILE);
|
|
93
|
+
const env = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
94
|
+
writeOwnerOnly(envPath, upsertEnvLine(env, key));
|
|
95
|
+
const ignorePath = join(dir, ".gitignore");
|
|
96
|
+
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
|
|
97
|
+
const covered = ignore
|
|
98
|
+
.split("\n")
|
|
99
|
+
.some((l) => KEY_IGNORE_LINES.has(l.trim()));
|
|
100
|
+
if (!covered)
|
|
101
|
+
writeFileSync(ignorePath, appendLine(ignore, LICENSE_KEY_FILE));
|
|
102
|
+
}
|
|
@@ -18,7 +18,12 @@
|
|
|
18
18
|
* gate (K1's regression floor).
|
|
19
19
|
*/
|
|
20
20
|
import type { ModuleName } from "../parse-args.js";
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
|
|
23
|
+
* `/pro` route and there will not be one (#6439, operator decision OD-2) —
|
|
24
|
+
* pricing is where every level and the commerce add-on are actually listed.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
|
|
22
27
|
/**
|
|
23
28
|
* Thrown for both honest-denial states (no key, and an invalid/lapsed key).
|
|
24
29
|
* The CLI does not distinguish them in its exit code — both are "this run
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { FetchModuleError, fetchModule } from "./fetch-module.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
|
|
4
|
+
* `/pro` route and there will not be one (#6439, operator decision OD-2) —
|
|
5
|
+
* pricing is where every level and the commerce add-on are actually listed.
|
|
6
|
+
*/
|
|
7
|
+
export const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
|
|
3
8
|
/**
|
|
4
9
|
* Thrown for both honest-denial states (no key, and an invalid/lapsed key).
|
|
5
10
|
* The CLI does not distinguish them in its exit code — both are "this run
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
export declare function proCallout(): string;
|
|
2
|
+
/** The closing prerequisites note on a free install (#6440 AC4). */
|
|
3
|
+
export declare const FREE_PREREQUISITES_NOTE = "No database, no licence key and no vendor API key are needed to run or build this site.";
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Wording is locked in `docs/internal/specs/ailk-onboard-site-spec.md` §2.1 and
|
|
6
|
-
* `docs/internal/specs/ailk-onboard-site-spec.md` §5.1 (Sam-verifiable AC).
|
|
7
|
-
*
|
|
8
|
-
* The message is always shown — no flags, no env gating. Free-tier users
|
|
9
|
-
* see it as a discovery surface for Pro; Pro users see it as a reminder
|
|
10
|
-
* of the `/onboard-site` skill they can install.
|
|
5
|
+
* The same note on a licensed install (#6480): it drops the licence-key claim,
|
|
6
|
+
* which would contradict the key the customer just installed with.
|
|
11
7
|
*/
|
|
12
|
-
export declare
|
|
8
|
+
export declare const LICENSED_PREREQUISITES_NOTE = "No database and no vendor API key are needed to run or build this site.";
|
|
9
|
+
/**
|
|
10
|
+
* Whether an install delivered paid content — any paid module in the effective
|
|
11
|
+
* module set, `union(template.requires, --modules)`, the same union
|
|
12
|
+
* `scaffold()` gates on. A paid template is covered by that union too:
|
|
13
|
+
* `templateDelivery` derives "paid" from `requires` alone.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isLicensedInstall(opts: {
|
|
16
|
+
template?: string;
|
|
17
|
+
modules?: string[];
|
|
18
|
+
}): boolean;
|
package/dist/lib/pro-callout.js
CHANGED
|
@@ -5,16 +5,39 @@
|
|
|
5
5
|
* Wording is locked in `docs/internal/specs/ailk-onboard-site-spec.md` §2.1 and
|
|
6
6
|
* `docs/internal/specs/ailk-onboard-site-spec.md` §5.1 (Sam-verifiable AC).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* of the `/onboard-site` skill they can install.
|
|
8
|
+
* Shown on a free install only (#6480): it is a discovery surface for Pro,
|
|
9
|
+
* and a licensed install already owns the tier it pitches.
|
|
11
10
|
*/
|
|
11
|
+
import { normalizeModules } from "../module-architecture.js";
|
|
12
|
+
import { resolveTemplate } from "../template-manifest.js";
|
|
13
|
+
import { classifyModules } from "./module-tier.js";
|
|
12
14
|
export function proCallout() {
|
|
13
15
|
return [
|
|
14
16
|
"Want AI-guided setup? Pro's `/onboard-site` interviews you about",
|
|
15
17
|
"your business, generates a vertical-aware page plan, configures",
|
|
16
18
|
"Stripe / auth / lead routing / analytics, and sets up your",
|
|
17
19
|
"GitHub→Vercel deploy — all in one orchestrated flow. Or invoke the",
|
|
18
|
-
"free scaffolders one at a time → ailaunchkit.ai/
|
|
20
|
+
"free scaffolders one at a time → ailaunchkit.ai/pricing",
|
|
19
21
|
].join("\n");
|
|
20
22
|
}
|
|
23
|
+
/** The closing prerequisites note on a free install (#6440 AC4). */
|
|
24
|
+
export const FREE_PREREQUISITES_NOTE = "No database, no licence key and no vendor API key are needed to run or build this site.";
|
|
25
|
+
/**
|
|
26
|
+
* The same note on a licensed install (#6480): it drops the licence-key claim,
|
|
27
|
+
* which would contradict the key the customer just installed with.
|
|
28
|
+
*/
|
|
29
|
+
export const LICENSED_PREREQUISITES_NOTE = "No database and no vendor API key are needed to run or build this site.";
|
|
30
|
+
/**
|
|
31
|
+
* Whether an install delivered paid content — any paid module in the effective
|
|
32
|
+
* module set, `union(template.requires, --modules)`, the same union
|
|
33
|
+
* `scaffold()` gates on. A paid template is covered by that union too:
|
|
34
|
+
* `templateDelivery` derives "paid" from `requires` alone.
|
|
35
|
+
*/
|
|
36
|
+
export function isLicensedInstall(opts) {
|
|
37
|
+
const requires = opts.template !== undefined ? resolveTemplate(opts.template).requires : [];
|
|
38
|
+
const modules = normalizeModules([
|
|
39
|
+
...requires,
|
|
40
|
+
...(opts.modules ?? ["marketing"]),
|
|
41
|
+
]);
|
|
42
|
+
return classifyModules(modules).paid.length > 0;
|
|
43
|
+
}
|