create-ailk 0.14.0 → 0.16.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 +124 -0
- package/component-catalog.json +3 -3
- package/dist/add-module.d.ts +7 -0
- package/dist/add-module.js +80 -3
- package/dist/add-route.js +26 -2
- package/dist/adopt.d.ts +74 -0
- package/dist/adopt.js +314 -0
- package/dist/apply-design.d.ts +12 -1
- package/dist/apply-design.js +36 -4
- package/dist/cli.js +103 -3
- package/dist/derive-architecture.d.ts +37 -2
- package/dist/derive-architecture.js +92 -7
- package/dist/design-package.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/chrome-readers.d.ts +21 -0
- package/dist/lib/chrome-readers.js +100 -0
- package/dist/lib/declared-env.d.ts +142 -0
- package/dist/lib/declared-env.js +985 -0
- package/dist/lib/env-catalog.d.ts +17 -0
- package/dist/lib/env-catalog.js +234 -0
- package/dist/lib/extract-module-bundle.d.ts +20 -0
- package/dist/lib/extract-module-bundle.js +53 -6
- package/dist/lib/license-key-files.d.ts +6 -1
- package/dist/lib/license-key-files.js +9 -4
- package/dist/lib/module-tier.d.ts +2 -0
- package/dist/lib/module-tier.js +4 -0
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +82 -0
- package/dist/parse-args.d.ts +27 -0
- package/dist/parse-args.js +89 -0
- package/dist/programmatic.d.ts +28 -0
- package/dist/programmatic.js +181 -11
- package/dist/seed-flows.d.ts +65 -0
- package/dist/seed-flows.js +350 -0
- package/dist/site-chrome.d.ts +4 -0
- package/dist/site-chrome.js +34 -3
- package/dist/site-manifest.d.ts +55 -5
- package/dist/site-manifest.js +121 -12
- package/dist/site-roster.d.ts +13 -0
- package/dist/site-roster.js +22 -8
- package/dist/spoke-checkout.d.ts +42 -0
- package/dist/spoke-checkout.js +359 -0
- package/dist/surfaces.js +7 -0
- package/dist/sync-routes.js +29 -5
- package/dist/topology.d.ts +246 -0
- package/dist/topology.js +942 -0
- package/package.json +3 -3
- package/site-templates/designs/minimalism/DESIGN.md +3 -3
- package/templates/.env.example +10 -0
- package/templates/apps/api/CLAUDE.md +21 -4
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
- package/templates/apps/api/src/bin/spokes.ts +62 -1
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +99 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
- package/templates/apps/api/src/lib/agent-store.ts +95 -3
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
- package/templates/apps/api/src/lib/site.ts +37 -0
- package/templates/apps/api/src/lib/spoke-store.ts +41 -3
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
- package/templates/apps/api/src/middleware/auth.ts +129 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
- package/templates/apps/api/src/openapi/spec.ts +38 -2
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
- package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +282 -0
- package/templates/apps/api/src/routes/leads/index.ts +69 -6
- package/templates/apps/api/src/routes/webhooks/README.md +39 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +133 -2
- package/templates/apps/api/src/server.ts +105 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +492 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
- package/templates/apps/api/src/services/default-tenant.ts +41 -7
- package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
- package/templates/apps/api/src/services/lead-routing.ts +278 -30
- package/templates/apps/api/src/services/person.ts +541 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
- package/templates/apps/mcp/__tests__/index.test.ts +5 -5
- package/templates/apps/mcp/src/index.ts +6 -4
- package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
- package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
- package/templates/apps/web/.env.example +58 -5
- package/templates/apps/web/CLAUDE.md +1 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
- package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +105 -11
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +12 -8
- package/templates/apps/web/app/[locale]/layout.tsx +83 -18
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
- package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
- package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +104 -1
- package/templates/apps/web/app/openapi.json/route.ts +72 -7
- package/templates/apps/web/auth.ts +29 -2
- package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
- package/templates/apps/web/components/signed-in-nav.tsx +17 -3
- package/templates/apps/web/instrumentation.ts +7 -4
- package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
- package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
- package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
- package/templates/apps/web/lib/__tests__/relay.test.ts +317 -0
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
- package/templates/apps/web/lib/account-config.ts +130 -1
- package/templates/apps/web/lib/api-origin.generated.ts +11 -0
- package/templates/apps/web/lib/closing-footer-path.ts +50 -0
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
- package/templates/apps/web/lib/hub-session/config.ts +104 -0
- package/templates/apps/web/lib/hub-session/index.ts +23 -0
- package/templates/apps/web/lib/hub-session/seal.ts +82 -0
- package/templates/apps/web/lib/hub-session/session.ts +180 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
- package/templates/apps/web/lib/listings-config.ts +31 -0
- package/templates/apps/web/lib/listings.tsx +7 -3
- package/templates/apps/web/lib/meeting-slugs.ts +11 -1
- package/templates/apps/web/lib/next-path.ts +35 -0
- package/templates/apps/web/lib/relay.ts +209 -3
- package/templates/apps/web/lib/server-api.ts +11 -1
- package/templates/apps/web/lib/site-brand.tsx +22 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -0
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +32 -3
- package/templates/apps/web/package.json +1 -1
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/agents.db.test.ts +85 -0
- package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
- package/templates/database/__tests__/member-area.db.test.ts +245 -0
- package/templates/database/__tests__/person.db.test.ts +584 -0
- package/templates/database/auth/schema.prisma +24 -0
- package/templates/database/config/schema.prisma +13 -0
- package/templates/database/inbox/schema.prisma +111 -0
- package/templates/database/jest.config.cjs +7 -11
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
- package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
- package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/person-backfill.sql +132 -0
- package/templates/database/tenancy.ts +53 -0
- package/templates/tools/check-declared-env.ts +37 -0
- package/templates/tools/declared-env.ts +1163 -0
- package/versions.json +35 -34
package/dist/adopt.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create-ailk adopt [<target-dir>]` — record, once, what an existing site
|
|
3
|
+
* is (#7875).
|
|
4
|
+
*
|
|
5
|
+
* A site scaffolded before #7426, or not scaffolded by create-ailk at all,
|
|
6
|
+
* has no `topology` in its `.ailk-site-manifest.json`, so every create-ailk
|
|
7
|
+
* command that asks `readSiteTopology` treats it as a hub — wrong for a site
|
|
8
|
+
* trimmed to a spoke by hand. Adopt writes the two fields the keystone added
|
|
9
|
+
* (D10 of spec 7426), and nothing else:
|
|
10
|
+
*
|
|
11
|
+
* - `topology` is derived from the tree against `SPOKE_REMOVED_ROOTS`
|
|
12
|
+
* (D1) — all removed roots absent is a spoke, all present a hub, a mix
|
|
13
|
+
* is refused. Never a flag: the tree is the evidence;
|
|
14
|
+
* - `patternVersion` is the running create-ailk version, the value a
|
|
15
|
+
* scaffold records (D8 — on an adopted site, the version it was adopted
|
|
16
|
+
* against);
|
|
17
|
+
* - an existing manifest gains the two fields, every other byte kept
|
|
18
|
+
* (`planTopologyRecord`, D3); a site with none gets one built by the
|
|
19
|
+
* scaffold's own writer from `--template` and the module set
|
|
20
|
+
* (`planAdoptedSiteManifest`, D4/D5);
|
|
21
|
+
* - a manifest already recording either field is refused (D2): adopt runs
|
|
22
|
+
* once. A recorded module a spoke cannot carry is a warning, never a
|
|
23
|
+
* refusal (D6) — `add-module` is the admission gate, not adopt.
|
|
24
|
+
*
|
|
25
|
+
* It writes the manifest — never a seam file, `architecture.yaml`, anything
|
|
26
|
+
* under `apps/api`, and never another subcommand (`sync-routes --apply`
|
|
27
|
+
* included). The one exception is a spoke's `project.yaml` (#7874 D9): on a
|
|
28
|
+
* site it derives as a spoke, adopt also appends an `env_contract` row for
|
|
29
|
+
* every env var the site's code reads that has none, so its `pnpm
|
|
30
|
+
* env:declared` check has a contract to read. A hub's `project.yaml` is never
|
|
31
|
+
* touched. Every refusal throws before any write.
|
|
32
|
+
*
|
|
33
|
+
* Pure module: no `import.meta.url`, so jest can import it directly — the
|
|
34
|
+
* package-rooted directories are supplied by the CLI, the `add-route.ts`
|
|
35
|
+
* split. Spec: docs/specs/7875-create-ailk-adopt-spec.md.
|
|
36
|
+
*/
|
|
37
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { basename, join, resolve } from "node:path";
|
|
39
|
+
import { parseArgs } from "node:util";
|
|
40
|
+
import { readArchitectureHeader } from "./add-module-core.js";
|
|
41
|
+
import { buildScaffoldMarkers } from "./copy.js";
|
|
42
|
+
import { EnvContractError, planEnvRows } from "./lib/declared-env.js";
|
|
43
|
+
import { ENV_CATALOG } from "./lib/env-catalog.js";
|
|
44
|
+
import { normalizeModules } from "./module-architecture.js";
|
|
45
|
+
import { ParseError, RECOGNIZED_MODULES, } from "./parse-args.js";
|
|
46
|
+
import { planAdoptedSiteManifest, planTopologyRecord, } from "./site-manifest.js";
|
|
47
|
+
import { LEGACY_SITE_ROSTER_PATH, SITE_ROSTER_PATH, readSiteRoster, resolveRosterTemplate, } from "./site-roster.js";
|
|
48
|
+
import { SHAPE_STARTERS } from "./template-manifest.js";
|
|
49
|
+
import { MCP_SEAM_PATH, SPOKE_REMOVED_ROOTS, WEB_SEAM_PATH, renderMcpSeam, renderWebSeam, spokeModuleRefusal, } from "./topology.js";
|
|
50
|
+
/** A refusal the CLI exits 2 on, thrown before anything is written. */
|
|
51
|
+
export class AdoptRefusedError extends Error {
|
|
52
|
+
exitCode = 2;
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "AdoptRefusedError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The generated seam files, by the app directory each lives in, with their renders. */
|
|
59
|
+
const SEAMS = [
|
|
60
|
+
{ app: "apps/web", path: WEB_SEAM_PATH, render: renderWebSeam },
|
|
61
|
+
{ app: "apps/mcp", path: MCP_SEAM_PATH, render: renderMcpSeam },
|
|
62
|
+
];
|
|
63
|
+
function isDirectory(path) {
|
|
64
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* D1 — the topology the tree shows: every `SPOKE_REMOVED_ROOTS` entry absent
|
|
68
|
+
* is a spoke, every one present a hub. Anything else is refused.
|
|
69
|
+
*/
|
|
70
|
+
function deriveTopology(targetDir) {
|
|
71
|
+
const present = SPOKE_REMOVED_ROOTS.filter((root) => isDirectory(join(targetDir, root)));
|
|
72
|
+
if (present.length === 0)
|
|
73
|
+
return "spoke";
|
|
74
|
+
if (present.length === SPOKE_REMOVED_ROOTS.length)
|
|
75
|
+
return "hub";
|
|
76
|
+
const absent = SPOKE_REMOVED_ROOTS.filter((root) => !present.includes(root));
|
|
77
|
+
throw new AdoptRefusedError(`adopt: ${targetDir} has ${present.join(", ")} but not ${absent.join(", ")}, so it is neither a hub nor a spoke`);
|
|
78
|
+
}
|
|
79
|
+
/** D5 — the module set for a site with no manifest: `--modules`, else the header. */
|
|
80
|
+
function moduleSet(targetDir, flag) {
|
|
81
|
+
if (flag !== undefined) {
|
|
82
|
+
try {
|
|
83
|
+
return normalizeModules(flag.split(","));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw new AdoptRefusedError(`--modules is not a module list: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const archPath = join(targetDir, "architecture.yaml");
|
|
90
|
+
if (!existsSync(archPath)) {
|
|
91
|
+
throw new AdoptRefusedError(`${targetDir} has no architecture.yaml to read its module set from; name it with --modules <a,b>`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return readArchitectureHeader(readFileSync(archPath, "utf8")).modules;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
throw new AdoptRefusedError(`${err.message.replace(/\.$/, "")}. Name the module set with --modules <a,b> instead.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** The recorded module names this package recognizes; anything else is ignored. */
|
|
101
|
+
function recordedModules(value) {
|
|
102
|
+
if (!Array.isArray(value))
|
|
103
|
+
return [];
|
|
104
|
+
return value.filter((m) => RECOGNIZED_MODULES.includes(m));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Adopt the site at `opts.targetDir`: write the manifest recording its
|
|
108
|
+
* topology and pattern version. Every refusal throws before the one write.
|
|
109
|
+
*
|
|
110
|
+
* @throws AdoptRefusedError (exit 2) on a refusal this command names, and
|
|
111
|
+
* the `AddModuleRefusedError` (exit 2) the roster readers and manifest
|
|
112
|
+
* writer throw on a manifest they cannot read or extend.
|
|
113
|
+
*/
|
|
114
|
+
export function adoptSite(opts) {
|
|
115
|
+
const targetDir = resolve(opts.targetDir);
|
|
116
|
+
if (!isDirectory(targetDir)) {
|
|
117
|
+
throw new Error(`adopt target is not a directory: ${targetDir}. Point adopt at an existing site.`);
|
|
118
|
+
}
|
|
119
|
+
const topology = deriveTopology(targetDir);
|
|
120
|
+
const markers = buildScaffoldMarkers(basename(targetDir));
|
|
121
|
+
const patternVersion = markers["CREATE_AILK_VERSION"];
|
|
122
|
+
if (patternVersion === undefined || patternVersion === "") {
|
|
123
|
+
// A once-only field: never record an empty version a second run cannot fix.
|
|
124
|
+
throw new Error("adopt: this create-ailk has no version to record");
|
|
125
|
+
}
|
|
126
|
+
const record = { topology, patternVersion };
|
|
127
|
+
const roster = readSiteRoster(targetDir);
|
|
128
|
+
let text;
|
|
129
|
+
let modules;
|
|
130
|
+
if (roster !== undefined) {
|
|
131
|
+
if (roster.path === LEGACY_SITE_ROSTER_PATH) {
|
|
132
|
+
throw new AdoptRefusedError(`${LEGACY_SITE_ROSTER_PATH} is the pre-#6658 roster name; rename it to ${SITE_ROSTER_PATH}, then re-run adopt.`);
|
|
133
|
+
}
|
|
134
|
+
for (const key of ["topology", "patternVersion"]) {
|
|
135
|
+
if (key in roster.doc) {
|
|
136
|
+
throw new AdoptRefusedError(`${roster.path} already records "${key}", so this site is already adopted. To move it to a new create-ailk version, see the README § Keeping a spoke current.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const [flag, value] of [
|
|
140
|
+
["--template", opts.template],
|
|
141
|
+
["--modules", opts.modules],
|
|
142
|
+
]) {
|
|
143
|
+
if (value !== undefined) {
|
|
144
|
+
throw new AdoptRefusedError(`${roster.path} already records the site's template and modules; ${flag} would contradict it. Re-run adopt without ${flag}.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
text = planTopologyRecord(roster.text, record, roster.path);
|
|
148
|
+
modules = recordedModules(roster.doc["modules"]);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (opts.template === undefined) {
|
|
152
|
+
throw new AdoptRefusedError(`${targetDir} has no ${SITE_ROSTER_PATH}; name the template it was built from with --template <name> (one of: ${Object.keys(SHAPE_STARTERS).join(", ")}).`);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
resolveRosterTemplate(opts.template);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
throw new AdoptRefusedError(`--template "${opts.template}" names no declared template: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
modules = moduleSet(targetDir, opts.modules);
|
|
161
|
+
text = planAdoptedSiteManifest({
|
|
162
|
+
targetDir,
|
|
163
|
+
template: opts.template,
|
|
164
|
+
modules,
|
|
165
|
+
...record,
|
|
166
|
+
siteTemplatesDir: opts.siteTemplatesDir,
|
|
167
|
+
templatesDir: opts.templatesDir,
|
|
168
|
+
markers,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const warnings = topology === "spoke"
|
|
172
|
+
? modules.flatMap((m) => spokeModuleRefusal(m) ?? [])
|
|
173
|
+
: [];
|
|
174
|
+
const seams = SEAMS.filter(({ app }) => isDirectory(join(targetDir, app))).map(({ path, render }) => {
|
|
175
|
+
const abs = join(targetDir, path);
|
|
176
|
+
if (!existsSync(abs))
|
|
177
|
+
return { path, state: "absent" };
|
|
178
|
+
return {
|
|
179
|
+
path,
|
|
180
|
+
state: readFileSync(abs, "utf8") === render(topology) ? "present" : "differs",
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
// #7874 D9 — a spoke's env_contract rows, planned before any write: a
|
|
184
|
+
// block adopt cannot read is a refusal, and nothing is written.
|
|
185
|
+
const yamlPath = join(targetDir, "project.yaml");
|
|
186
|
+
let env = { state: "hub" };
|
|
187
|
+
let yamlNext;
|
|
188
|
+
if (topology === "spoke") {
|
|
189
|
+
if (!existsSync(yamlPath)) {
|
|
190
|
+
env = { state: "no-project-yaml" };
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const yamlText = readFileSync(yamlPath, "utf8");
|
|
194
|
+
try {
|
|
195
|
+
const plan = planEnvRows(targetDir, yamlText, {
|
|
196
|
+
topology,
|
|
197
|
+
catalog: ENV_CATALOG,
|
|
198
|
+
});
|
|
199
|
+
if (plan.text === yamlText) {
|
|
200
|
+
env = { state: "unchanged" };
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
env = { state: "written", added: plan.added };
|
|
204
|
+
yamlNext = plan.text;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (err instanceof EnvContractError) {
|
|
209
|
+
throw new AdoptRefusedError(`adopt: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
writeFileSync(join(targetDir, SITE_ROSTER_PATH), text);
|
|
216
|
+
if (yamlNext !== undefined)
|
|
217
|
+
writeFileSync(yamlPath, yamlNext);
|
|
218
|
+
return {
|
|
219
|
+
targetDir,
|
|
220
|
+
path: SITE_ROSTER_PATH,
|
|
221
|
+
topology,
|
|
222
|
+
patternVersion,
|
|
223
|
+
created: roster === undefined,
|
|
224
|
+
seams,
|
|
225
|
+
warnings,
|
|
226
|
+
env,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** The printed report: the path written, the record, the seams, the warnings. */
|
|
230
|
+
export function formatAdoptReport(result) {
|
|
231
|
+
const lines = [
|
|
232
|
+
`[create-ailk] adopt → ${result.targetDir}`,
|
|
233
|
+
`${result.created ? "Wrote" : "Updated"} ${result.path}`,
|
|
234
|
+
` topology: ${result.topology}`,
|
|
235
|
+
` patternVersion: ${result.patternVersion}`,
|
|
236
|
+
];
|
|
237
|
+
if (result.seams.length > 0) {
|
|
238
|
+
lines.push(`Seam files (against the ${result.topology} render):`);
|
|
239
|
+
const width = Math.max(...result.seams.map((s) => s.path.length));
|
|
240
|
+
lines.push(...result.seams.map((s) => ` ${s.path.padEnd(width)} ${s.state}`));
|
|
241
|
+
}
|
|
242
|
+
const env = formatAdoptEnv(result.env);
|
|
243
|
+
if (env !== undefined)
|
|
244
|
+
lines.push(env);
|
|
245
|
+
lines.push(...result.warnings.map((w) => `warning: ${w}`));
|
|
246
|
+
return lines.join("\n") + "\n";
|
|
247
|
+
}
|
|
248
|
+
/** The report's one env_contract line (#7874 D9); none on a hub. */
|
|
249
|
+
function formatAdoptEnv(env) {
|
|
250
|
+
switch (env.state) {
|
|
251
|
+
case "written":
|
|
252
|
+
return `env_contract: ${env.added.length} rows added (${env.added.join(", ")})`;
|
|
253
|
+
case "unchanged":
|
|
254
|
+
return "env_contract: every read already declared";
|
|
255
|
+
case "no-project-yaml":
|
|
256
|
+
return "env_contract: no project.yaml, nothing written";
|
|
257
|
+
case "hub":
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export const ADOPT_HELP = `create-ailk adopt — record what an existing site is, once.
|
|
262
|
+
|
|
263
|
+
Usage:
|
|
264
|
+
create-ailk adopt [<target-dir>] [--template <name>] [--modules <a,b>]
|
|
265
|
+
|
|
266
|
+
Arguments:
|
|
267
|
+
<target-dir> The existing site. Defaults to the current dir.
|
|
268
|
+
|
|
269
|
+
Options:
|
|
270
|
+
--template <name> The template the site was built from. Required
|
|
271
|
+
when the site has no .ailk-site-manifest.json;
|
|
272
|
+
refused when it has one.
|
|
273
|
+
--modules <a,b> The site's module set, when it has no manifest.
|
|
274
|
+
Overrides architecture.yaml's "# Modules:" header.
|
|
275
|
+
--help, -h Print this message.
|
|
276
|
+
|
|
277
|
+
Writes "topology" and "patternVersion" into .ailk-site-manifest.json. On a
|
|
278
|
+
spoke it also appends an env_contract row to project.yaml for every
|
|
279
|
+
environment variable the site's code reads that has none, so pnpm
|
|
280
|
+
env:declared has a contract to check; it never edits or removes a row, and
|
|
281
|
+
it never touches a hub's project.yaml. Nothing else is written. The
|
|
282
|
+
topology is read from the tree: no apps/api and no database/ is a spoke,
|
|
283
|
+
both is a hub, one without the other is refused.
|
|
284
|
+
patternVersion is this create-ailk's version. A site with no manifest gets
|
|
285
|
+
one, built the way a scaffold builds it. A manifest that already records
|
|
286
|
+
either field is refused: adopt runs once. A recorded module a spoke cannot
|
|
287
|
+
carry is a warning, not a refusal.
|
|
288
|
+
`;
|
|
289
|
+
/**
|
|
290
|
+
* Parse the argv AFTER the `adopt` word. Strict `node:util` parseArgs, like
|
|
291
|
+
* `parseAddRouteArgs`; the values themselves are validated by `adoptSite`.
|
|
292
|
+
*/
|
|
293
|
+
export function parseAdoptArgs(argv) {
|
|
294
|
+
const parsed = parseArgs({
|
|
295
|
+
args: argv,
|
|
296
|
+
strict: true,
|
|
297
|
+
allowPositionals: true,
|
|
298
|
+
options: {
|
|
299
|
+
template: { type: "string" },
|
|
300
|
+
modules: { type: "string" },
|
|
301
|
+
help: { type: "boolean", default: false, short: "h" },
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
const [target, ...rest] = parsed.positionals;
|
|
305
|
+
if (rest.length > 0) {
|
|
306
|
+
throw new ParseError(`adopt takes at most one target directory, got ${parsed.positionals.length} arguments`);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
target,
|
|
310
|
+
template: parsed.values.template,
|
|
311
|
+
modules: parsed.values.modules,
|
|
312
|
+
help: parsed.values.help === true,
|
|
313
|
+
};
|
|
314
|
+
}
|
package/dist/apply-design.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HomeSeedOutcome } from "./design-home.js";
|
|
2
|
+
import { type UnreadChromeKnob } from "./lib/chrome-readers.js";
|
|
2
3
|
export interface ApplyDesignOptions {
|
|
3
4
|
/** The design: a shipped name, or a directory holding a DESIGN.md. */
|
|
4
5
|
design: string;
|
|
@@ -10,6 +11,8 @@ export interface ApplyDesignOptions {
|
|
|
10
11
|
/** The overlay's `templates/` (#7743 D7) — where a treeless shape's home lives. Default: `templates` beside `siteTemplatesDir`. */
|
|
11
12
|
templatesDir?: string;
|
|
12
13
|
catalogPath: string;
|
|
14
|
+
/** #7781 — apply the design even when a reader file of the site ignores a chrome knob it sets; the pairs are reported instead of refused. */
|
|
15
|
+
allowUnreadChrome?: boolean;
|
|
13
16
|
/** Where a relative design path resolves from. Default process.cwd(). */
|
|
14
17
|
cwd?: string;
|
|
15
18
|
/** Read for AILK_LICENSE_KEY only (D6). Default process.env. */
|
|
@@ -60,13 +63,21 @@ export interface ApplyDesignResult {
|
|
|
60
63
|
fonts?: string[];
|
|
61
64
|
/** #7720 D4.2 — faces were written, but the site's layout.tsx does not read lib/site-fonts. */
|
|
62
65
|
fontsUnwired?: true;
|
|
66
|
+
/** #7781 — chrome knobs the design set that a reader file of the site does not read; present only when non-empty, which only `allowUnreadChrome` lets through. */
|
|
67
|
+
unreadChrome?: UnreadChromeKnob[];
|
|
68
|
+
}
|
|
69
|
+
/** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
|
|
70
|
+
export declare class UnreadChromeError extends Error {
|
|
71
|
+
readonly unread: readonly UnreadChromeKnob[];
|
|
72
|
+
constructor(unread: readonly UnreadChromeKnob[]);
|
|
63
73
|
}
|
|
64
74
|
export interface ApplyDesignArgs {
|
|
65
75
|
design: string | undefined;
|
|
66
76
|
target: string | undefined;
|
|
77
|
+
allowUnreadChrome: boolean;
|
|
67
78
|
help: boolean;
|
|
68
79
|
}
|
|
69
|
-
export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated\napps/web/lib/site-fonts.ts, the look fields of sections the site's pages\nalready place, and the design/layout/theme record in .ailk-site-manifest.json.\nNever adds, removes or renames a route or page, never touches a content field\n(except a placeholder home's sections, below) or the brand: block, and leaves\nevery other page's structured data unchanged.\n\nA design may seed the placeholder home's sections \u2014 its home arc \u2014 while they\nare still a placeholder: the shape's own, or a design's arc of them. It never\nchanges a home whose sections were edited; it reports it and leaves it. A\nseeded home takes the design's looks; look values on the sections it replaced\nare not kept. A design that ships no arc restores the shape's own sections on\na home that still wears another design's untouched arc.\n\nA design other than standard needs a Pro or Pro Plus key: the site's own\nailk.key (put one there with create-ailk license set <key>), else\nAILK_LICENSE_KEY. A malformed package, a preset the validator refuses or a\nmissing key is refused before any file is written.\n\nA preset sets the look fields it names and never unsets one: a field it does\nnot name keeps whatever value the page has. Running it again changes nothing.\n";
|
|
80
|
+
export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --allow-unread-chrome Apply the design even when a file of the site does\n not read a chrome setting the design sets (a site\n scaffolded before the setting existed). Without it\n that is refused; with it each such file is reported.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated\napps/web/lib/site-fonts.ts, the look fields of sections the site's pages\nalready place, and the design/layout/theme record in .ailk-site-manifest.json.\nNever adds, removes or renames a route or page, never touches a content field\n(except a placeholder home's sections, below) or the brand: block, and leaves\nevery other page's structured data unchanged.\n\nA design may seed the placeholder home's sections \u2014 its home arc \u2014 while they\nare still a placeholder: the shape's own, or a design's arc of them. It never\nchanges a home whose sections were edited; it reports it and leaves it. A\nseeded home takes the design's looks; look values on the sections it replaced\nare not kept. A design that ships no arc restores the shape's own sections on\na home that still wears another design's untouched arc.\n\nA design other than standard needs a Pro or Pro Plus key: the site's own\nailk.key (put one there with create-ailk license set <key>), else\nAILK_LICENSE_KEY. A malformed package, a preset the validator refuses or a\nmissing key is refused before any file is written.\n\nA preset sets the look fields it names and never unsets one: a field it does\nnot name keeps whatever value the page has. Running it again changes nothing.\n";
|
|
70
81
|
/**
|
|
71
82
|
* Parse the argv AFTER the `apply-design` word (D1). Strict `node:util`
|
|
72
83
|
* parseArgs, like `parseSyncRoutesArgs`: exactly one positional, the design
|
package/dist/apply-design.js
CHANGED
|
@@ -22,6 +22,7 @@ import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
|
|
|
22
22
|
import { HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, } from "./design-home.js";
|
|
23
23
|
import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
|
|
24
24
|
import { resolveDesign, spliceThemeBlock } from "./design-scaffold.js";
|
|
25
|
+
import { chromeKnobReaders, unreadChromeKnobs } from "./lib/chrome-readers.js";
|
|
25
26
|
import { LICENSE_KEY_FILE } from "./lib/license-key-files.js";
|
|
26
27
|
import { CONVERSION_HINT_URL, LicenseRequiredError, assertDesignAllowed, deriveLevel, } from "./lib/module-license-gate.js";
|
|
27
28
|
import { LEVEL_LABEL } from "./lib/module-tier.js";
|
|
@@ -29,6 +30,17 @@ import { ParseError } from "./parse-args.js";
|
|
|
29
30
|
import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
|
|
30
31
|
import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
|
|
31
32
|
import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, } from "./site-roster.js";
|
|
33
|
+
/** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
|
|
34
|
+
export class UnreadChromeError extends Error {
|
|
35
|
+
unread;
|
|
36
|
+
constructor(unread) {
|
|
37
|
+
super(`design chrome would not render: the site's own copy of these files does not read the knob the design sets (update them from the current create-ailk overlay, or pass --allow-unread-chrome to apply anyway):\n${unread
|
|
38
|
+
.map(({ knob, file }) => ` ${knob} — ${file}`)
|
|
39
|
+
.join("\n")}`);
|
|
40
|
+
this.unread = unread;
|
|
41
|
+
this.name = "UnreadChromeError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
32
44
|
/** The site's locale layout — the one reader of the generated site-fonts.ts. */
|
|
33
45
|
const SITE_LAYOUT_PATH = "apps/web/app/[locale]/layout.tsx";
|
|
34
46
|
export const APPLY_DESIGN_HELP = `create-ailk apply-design — make an existing site wear a design.
|
|
@@ -45,6 +57,10 @@ Arguments:
|
|
|
45
57
|
Options:
|
|
46
58
|
--target <site-dir> An already-scaffolded repo (contains
|
|
47
59
|
architecture.yaml). Defaults to the current dir.
|
|
60
|
+
--allow-unread-chrome Apply the design even when a file of the site does
|
|
61
|
+
not read a chrome setting the design sets (a site
|
|
62
|
+
scaffolded before the setting existed). Without it
|
|
63
|
+
that is refused; with it each such file is reported.
|
|
48
64
|
--help, -h Print this message.
|
|
49
65
|
|
|
50
66
|
Writes only the look: DESIGN.md (replaced whole by the design's, keeping the
|
|
@@ -83,6 +99,7 @@ export function parseApplyDesignArgs(argv) {
|
|
|
83
99
|
allowPositionals: true,
|
|
84
100
|
options: {
|
|
85
101
|
target: { type: "string" },
|
|
102
|
+
"allow-unread-chrome": { type: "boolean", default: false },
|
|
86
103
|
help: { type: "boolean", default: false, short: "h" },
|
|
87
104
|
},
|
|
88
105
|
});
|
|
@@ -94,7 +111,12 @@ export function parseApplyDesignArgs(argv) {
|
|
|
94
111
|
if (design === undefined && !help) {
|
|
95
112
|
throw new ParseError("apply-design needs a design. Usage: create-ailk apply-design <name|path> [--target <site-dir>]");
|
|
96
113
|
}
|
|
97
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
design,
|
|
116
|
+
target: parsed.values.target,
|
|
117
|
+
allowUnreadChrome: parsed.values["allow-unread-chrome"] === true,
|
|
118
|
+
help,
|
|
119
|
+
};
|
|
98
120
|
}
|
|
99
121
|
/** `abs` relative to `root`, with `/` separators. */
|
|
100
122
|
function siteRelative(root, abs) {
|
|
@@ -189,12 +211,18 @@ export async function applyDesign(opts) {
|
|
|
189
211
|
throw new LicenseRequiredError(`design "${resolved.display}" is a Pro feature — this site's licence level is ${LEVEL_LABEL[level]}; put a Pro or Pro Plus key in the site with create-ailk license set <key>, or set AILK_LICENSE_KEY → ${CONVERSION_HINT_URL}`);
|
|
190
212
|
}
|
|
191
213
|
}
|
|
214
|
+
// #7781 — after the licence gate, before any plan: every chrome knob the design sets must be read by
|
|
215
|
+
// the site's own copy of each overlay file that reads it. The reader map is derived from the overlay.
|
|
216
|
+
const roster = readSiteRoster(targetAbs);
|
|
217
|
+
const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
|
|
218
|
+
const templatesDir = opts.templatesDir ?? join(dirname(opts.siteTemplatesDir), "templates");
|
|
219
|
+
const unreadChrome = unreadChromeKnobs(chrome, chromeKnobReaders(templatesDir), targetAbs);
|
|
220
|
+
if (unreadChrome.length > 0 && opts.allowUnreadChrome !== true)
|
|
221
|
+
throw new UnreadChromeError(unreadChrome);
|
|
192
222
|
// C6 / D4 — plan every write in memory.
|
|
193
223
|
const theme = compileDesignTheme(pkg);
|
|
194
224
|
const siteMdxCurrent = readFileSync(siteMdxPath, "utf8");
|
|
195
225
|
const siteMdxNext = spliceThemeBlock(siteMdxCurrent, theme, siteMdxPath);
|
|
196
|
-
const roster = readSiteRoster(targetAbs);
|
|
197
|
-
const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
|
|
198
226
|
const chromePath = join(targetAbs, SITE_CHROME_PATH);
|
|
199
227
|
const chromeCurrent = existsSync(chromePath) ? readFileSync(chromePath, "utf8") : undefined;
|
|
200
228
|
const chromeNext = renderSiteChrome(chrome);
|
|
@@ -229,7 +257,7 @@ export async function applyDesign(opts) {
|
|
|
229
257
|
? ""
|
|
230
258
|
: readFileSync(shapeHomePath(shape, {
|
|
231
259
|
siteTemplatesDir: opts.siteTemplatesDir,
|
|
232
|
-
templatesDir
|
|
260
|
+
templatesDir,
|
|
233
261
|
}), "utf8"),
|
|
234
262
|
shippedSeeds,
|
|
235
263
|
catalog: catalog(),
|
|
@@ -328,6 +356,7 @@ export async function applyDesign(opts) {
|
|
|
328
356
|
...(roster !== undefined ? { rosterPath: roster.path } : {}),
|
|
329
357
|
...(fontFamilies !== undefined ? { fonts: fontFamilies } : {}),
|
|
330
358
|
...(fontsUnwired ? { fontsUnwired: true } : {}),
|
|
359
|
+
...(unreadChrome.length > 0 ? { unreadChrome } : {}),
|
|
331
360
|
};
|
|
332
361
|
}
|
|
333
362
|
/** The changed-file report (D9). */
|
|
@@ -364,6 +393,9 @@ export function formatApplyDesignReport(result) {
|
|
|
364
393
|
if (result.fontsUnwired === true) {
|
|
365
394
|
lines.push(`fonts not loaded: ${SITE_LAYOUT_PATH} predates lib/site-fonts — update the layout from the current create-ailk overlay`);
|
|
366
395
|
}
|
|
396
|
+
for (const { knob, file } of result.unreadChrome ?? []) {
|
|
397
|
+
lines.push(`chrome not rendered: ${knob} — ${file} does not read it`);
|
|
398
|
+
}
|
|
367
399
|
if (result.manifest === "legacy-roster") {
|
|
368
400
|
lines.push(`manifest not recorded: ${result.rosterPath} predates the design record`);
|
|
369
401
|
}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* addModule(), print the report, exit (2 on a refusal it names)
|
|
8
8
|
* on an `add-route` subcommand: add one route surface to an existing
|
|
9
9
|
* site, print the report, exit (2 on a refusal it names) (#6947)
|
|
10
|
+
* on an `adopt` subcommand: record an existing site's topology and
|
|
11
|
+
* pattern version in its manifest, print the report, exit (2 on a
|
|
12
|
+
* refusal it names) (#7875)
|
|
13
|
+
* on a `seed-flows` subcommand: send a spoke's declared flows/*.json to
|
|
14
|
+
* its hub with the key from AILK_SPOKE_KEY, print one line per flow,
|
|
15
|
+
* exit 1 if any failed (2 on a refusal it names, nothing sent) (#7876)
|
|
10
16
|
* on a `sync-routes` subcommand: parse its own argv, run syncRoutesSync(),
|
|
11
17
|
* print the report, exit (1 iff routes are missing in check mode)
|
|
12
18
|
* on a `license set` subcommand: write the key into the scaffold, push it
|
|
@@ -40,6 +46,7 @@ import { basename, dirname, resolve } from "node:path";
|
|
|
40
46
|
import { fileURLToPath } from "node:url";
|
|
41
47
|
import { ADD_MODULE_HELP, AddModuleRefusedError, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
|
|
42
48
|
import { ADD_ROUTE_HELP, AddRouteRefusedError, addRouteToScaffold, formatAddRouteReport, parseAddRouteArgs, } from "./add-route.js";
|
|
49
|
+
import { ADOPT_HELP, AdoptRefusedError, adoptSite, formatAdoptReport, parseAdoptArgs, } from "./adopt.js";
|
|
43
50
|
import { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
|
|
44
51
|
import { CLAUDE_MD_CHECK_PATH } from "./claude-md.js";
|
|
45
52
|
import { buildScaffoldMarkers } from "./copy.js";
|
|
@@ -48,6 +55,7 @@ import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall
|
|
|
48
55
|
import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
|
|
49
56
|
import { PLUGIN_INSTALL_LINES, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
|
|
50
57
|
import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
|
|
58
|
+
import { SEED_FLOWS_HELP, SeedFlowsRefusedError, parseSeedFlowsArgs, seedFlows, } from "./seed-flows.js";
|
|
51
59
|
import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
|
|
52
60
|
import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
|
|
53
61
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -161,6 +169,62 @@ async function main() {
|
|
|
161
169
|
}
|
|
162
170
|
process.exit(0);
|
|
163
171
|
}
|
|
172
|
+
// `adopt [dir]` (#7875): record an existing site's topology and pattern
|
|
173
|
+
// version in its manifest, once. Writes that one file and nothing else. A
|
|
174
|
+
// refusal it names (a half-trimmed tree, a second run, a missing or
|
|
175
|
+
// contradicting --template / --modules, a legacy roster) exits 2.
|
|
176
|
+
if (argv[0] === "adopt") {
|
|
177
|
+
const args = parseAdoptArgs(argv.slice(1));
|
|
178
|
+
if (args.help) {
|
|
179
|
+
process.stdout.write(ADOPT_HELP);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const result = adoptSite({
|
|
184
|
+
targetDir: args.target ?? process.cwd(),
|
|
185
|
+
templatesDir: resolve(PKG_ROOT, "templates"),
|
|
186
|
+
siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
|
|
187
|
+
template: args.template,
|
|
188
|
+
modules: args.modules,
|
|
189
|
+
});
|
|
190
|
+
process.stdout.write(formatAdoptReport(result));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (!(err instanceof AdoptRefusedError) &&
|
|
194
|
+
!(err instanceof AddModuleRefusedError)) {
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
process.stderr.write(`[create-ailk] ${err.message}\n`);
|
|
198
|
+
process.exit(err.exitCode);
|
|
199
|
+
}
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
// `seed-flows [dir]` (#7876): send a spoke's declared flows/*.json to its
|
|
203
|
+
// hub with the spoke key from AILK_SPOKE_KEY. Exit 1 when any flow failed;
|
|
204
|
+
// a refusal it names (a hub, a bad file, no key, no valid hub origin)
|
|
205
|
+
// exits 2 before anything is sent.
|
|
206
|
+
if (argv[0] === "seed-flows") {
|
|
207
|
+
const args = parseSeedFlowsArgs(argv.slice(1));
|
|
208
|
+
if (args.help) {
|
|
209
|
+
process.stdout.write(SEED_FLOWS_HELP);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const result = await seedFlows({
|
|
214
|
+
targetDir: args.target ?? process.cwd(),
|
|
215
|
+
hubUrl: args.hubUrl,
|
|
216
|
+
dryRun: args.dryRun,
|
|
217
|
+
onLine: (line) => process.stdout.write(`${line}\n`),
|
|
218
|
+
});
|
|
219
|
+
process.exit(result.exitCode);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (!(err instanceof SeedFlowsRefusedError))
|
|
223
|
+
throw err;
|
|
224
|
+
process.stderr.write(`[create-ailk] ${err.message}\n`);
|
|
225
|
+
process.exit(err.exitCode);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
164
228
|
// Subcommand dispatch, ahead of the flag parser: `sync-routes` has its own
|
|
165
229
|
// argv shape (#4986). Check mode exits 1 iff a template route is missing;
|
|
166
230
|
// locally modified routes are reported but do not fail; --apply exits 0.
|
|
@@ -203,6 +267,7 @@ async function main() {
|
|
|
203
267
|
siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
|
|
204
268
|
templatesDir: resolve(PKG_ROOT, "templates"),
|
|
205
269
|
catalogPath: resolve(PKG_ROOT, "component-catalog.json"),
|
|
270
|
+
allowUnreadChrome: args.allowUnreadChrome,
|
|
206
271
|
});
|
|
207
272
|
process.stdout.write(formatApplyDesignReport(result));
|
|
208
273
|
process.exit(0);
|
|
@@ -246,6 +311,10 @@ async function main() {
|
|
|
246
311
|
"\n" +
|
|
247
312
|
ADD_ROUTE_HELP +
|
|
248
313
|
"\n" +
|
|
314
|
+
ADOPT_HELP +
|
|
315
|
+
"\n" +
|
|
316
|
+
SEED_FLOWS_HELP +
|
|
317
|
+
"\n" +
|
|
249
318
|
APPLY_DESIGN_HELP +
|
|
250
319
|
"\n" +
|
|
251
320
|
LICENSE_SET_HELP);
|
|
@@ -318,7 +387,8 @@ async function main() {
|
|
|
318
387
|
// marketing-site-free` still rides the no-key case (record D11): its
|
|
319
388
|
// `requires` is all-free, so it installs from the shipped
|
|
320
389
|
// `site-templates/` tree with no key and no request.
|
|
321
|
-
const { targetDir: targetAbs, surfaces, starter, modules: effectiveModules, } = await scaffold(toScaffoldOptions(opts));
|
|
390
|
+
const { targetDir: targetAbs, surfaces, starter, modules: effectiveModules, droppedModules, hubOrigin, hubSignInOrigin, } = await scaffold(toScaffoldOptions(opts));
|
|
391
|
+
const isSpoke = opts.topology === "spoke";
|
|
322
392
|
// The starter scaffold() resolved — from --template, or from --shape (#6754 D4).
|
|
323
393
|
const templateName = starter?.template ?? opts.template;
|
|
324
394
|
if (opts.install) {
|
|
@@ -346,6 +416,34 @@ async function main() {
|
|
|
346
416
|
...(starter !== undefined
|
|
347
417
|
? [`Starter: shape ${starter.shape}, design ${starter.design}.`]
|
|
348
418
|
: []),
|
|
419
|
+
// #7426 — a spoke says so, and names what its hosts must carry (D3).
|
|
420
|
+
...(isSpoke
|
|
421
|
+
? [
|
|
422
|
+
`Topology: spoke — no apps/api, no database, ${hubSignInOrigin !== undefined ? "sign-in through the hub" : "no sign-in"}${opts.mcp === false ? ", no apps/mcp" : "; apps/mcp is a stateless front"}.`,
|
|
423
|
+
...(droppedModules !== undefined && droppedModules.length > 0
|
|
424
|
+
? [
|
|
425
|
+
`Not carried on a spoke: ${droppedModules.join(", ")} (your licence includes ${droppedModules.length === 1 ? "it" : "them"}; a spoke does not admit ${droppedModules.length === 1 ? "it" : "them"} yet).`,
|
|
426
|
+
]
|
|
427
|
+
: []),
|
|
428
|
+
"",
|
|
429
|
+
`Every host serving this spoke's ${opts.mcp === false ? "apps/web" : "apps/web and apps/mcp"} must carry:`,
|
|
430
|
+
` AILK_API_URL=${hubOrigin ?? "<your hub origin>"} # written to apps/web/.env.local`,
|
|
431
|
+
" AILK_SPOKE_KEY # issued by the hub; never written here",
|
|
432
|
+
" AILK_VISITOR_FORWARD_KEY # issued by the hub; never written here",
|
|
433
|
+
// #7894 D11 — the hub sign-in mode's client, issued on the hub.
|
|
434
|
+
...(hubSignInOrigin !== undefined
|
|
435
|
+
? [
|
|
436
|
+
` AILK_HUB_SIGN_IN_URL=${hubSignInOrigin} # written to apps/web/.env.local`,
|
|
437
|
+
" AILK_HUB_SESSION_SECRET # generated into apps/web/.env.local; keep it secret",
|
|
438
|
+
" AILK_HUB_CLIENT_ID # this spoke's id on the hub; never written here",
|
|
439
|
+
" AILK_HUB_CLIENT_SECRET # issued by the hub; never written here",
|
|
440
|
+
"",
|
|
441
|
+
"On the hub, issue the client id and secret with:",
|
|
442
|
+
" pnpm --filter @working-theory/api spokes sign-in-client --id <spoke_id> --redirect-uri <this site's origin>/api/hub-session/callback",
|
|
443
|
+
]
|
|
444
|
+
: []),
|
|
445
|
+
]
|
|
446
|
+
: []),
|
|
349
447
|
"",
|
|
350
448
|
];
|
|
351
449
|
// Prerequisites (#6440 AC4) — every one the scaffold enforces, and nothing
|
|
@@ -374,13 +472,15 @@ async function main() {
|
|
|
374
472
|
lines.push("Next steps:");
|
|
375
473
|
lines.push(` cd ${targetAbs}`);
|
|
376
474
|
if (!opts.install) {
|
|
377
|
-
lines.push(" pnpm install # also generates the Prisma client");
|
|
475
|
+
lines.push(isSpoke ? " pnpm install" : " pnpm install # also generates the Prisma client");
|
|
378
476
|
}
|
|
379
477
|
lines.push(" pnpm dev # http://localhost:3000");
|
|
380
478
|
lines.push(" pnpm build # production build, when you are ready to deploy");
|
|
381
479
|
lines.push("");
|
|
382
480
|
lines.push("Open http://localhost:3000 to see the home page.");
|
|
383
|
-
|
|
481
|
+
if (!isSpoke) {
|
|
482
|
+
lines.push("Adding auth, saved flows, checkout or database-backed content later? README.md \u00a7 \"When you need a database\" has the one-command setup.");
|
|
483
|
+
}
|
|
384
484
|
lines.push("");
|
|
385
485
|
lines.push("Claude Code: the AI Launch Kit skills come from the ailk plugin. Trust this folder and Claude Code loads it, or run in a session:");
|
|
386
486
|
for (const line of PLUGIN_INSTALL_LINES)
|