cronus-ui 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/dist/commands/add-page.d.ts +108 -0
  4. package/dist/commands/add-page.js +642 -0
  5. package/dist/commands/add.d.ts +9 -0
  6. package/dist/commands/add.js +114 -0
  7. package/dist/commands/ai.d.ts +14 -0
  8. package/dist/commands/ai.js +69 -0
  9. package/dist/commands/compose.d.ts +82 -0
  10. package/dist/commands/compose.js +403 -0
  11. package/dist/commands/diff.d.ts +8 -0
  12. package/dist/commands/diff.js +55 -0
  13. package/dist/commands/init.d.ts +9 -0
  14. package/dist/commands/init.js +53 -0
  15. package/dist/commands/list.d.ts +7 -0
  16. package/dist/commands/list.js +28 -0
  17. package/dist/commands/theme.d.ts +23 -0
  18. package/dist/commands/theme.js +735 -0
  19. package/dist/commands/upgrade.d.ts +51 -0
  20. package/dist/commands/upgrade.js +840 -0
  21. package/dist/compose/data-slots.d.ts +71 -0
  22. package/dist/compose/data-slots.js +104 -0
  23. package/dist/compose/manifest.d.ts +90 -0
  24. package/dist/compose/manifest.js +224 -0
  25. package/dist/compose/plan.d.ts +164 -0
  26. package/dist/compose/plan.js +506 -0
  27. package/dist/compose/preview.d.ts +10 -0
  28. package/dist/compose/preview.js +48 -0
  29. package/dist/compose/reload.d.ts +56 -0
  30. package/dist/compose/reload.js +138 -0
  31. package/dist/compose/render.d.ts +123 -0
  32. package/dist/compose/render.js +404 -0
  33. package/dist/compose/templates.d.ts +22 -0
  34. package/dist/compose/templates.js +76 -0
  35. package/dist/compose.d.ts +10 -0
  36. package/dist/compose.js +8 -0
  37. package/dist/config.d.ts +94 -0
  38. package/dist/config.js +38 -0
  39. package/dist/index.d.ts +3 -0
  40. package/dist/index.js +184 -0
  41. package/dist/registry.d.ts +59 -0
  42. package/dist/registry.js +96 -0
  43. package/dist/utils.d.ts +72 -0
  44. package/dist/utils.js +186 -0
  45. package/package.json +68 -0
  46. package/templates/apps/chat.json +44 -0
  47. package/templates/apps/finance.json +44 -0
  48. package/templates/apps/landing-agency.json +31 -0
  49. package/templates/apps/landing-agents.json +32 -0
  50. package/templates/apps/landing-broadcast.json +29 -0
  51. package/templates/apps/landing-care.json +28 -0
  52. package/templates/apps/landing-coverage.json +23 -0
  53. package/templates/apps/landing-docs.json +29 -0
  54. package/templates/apps/landing-glass.json +28 -0
  55. package/templates/apps/landing-ops.json +29 -0
  56. package/templates/apps/landing-premium.json +31 -0
  57. package/templates/apps/landing-secure.json +31 -0
  58. package/templates/apps/landing-shop.json +27 -0
  59. package/templates/apps/landing-studio.json +30 -0
  60. package/templates/apps/landing.json +23 -0
  61. package/templates/apps/mail.json +44 -0
  62. package/templates/apps/saas.json +64 -0
  63. package/templates/apps/store.json +75 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Anchored replacement of a block's `@cronus:data <name>` data const and its brand
3
+ * literal. Both mechanisms are marker/literal driven and FAIL LOUD when the anchor
4
+ * is absent — the composer must never silently target a missing slot or brand
5
+ * (the same guarantee `registry:check` enforces at build time, re-checked here
6
+ * against the bytes actually on disk).
7
+ *
8
+ * The data-slot replacement operates on DATA CONSTS ONLY (never JSX): the region
9
+ * between `/* @cronus:data X *\/` and the FIRST following `/* @cronus:data-end *\/`
10
+ * is replaced wholesale with a freshly-serialized const, keeping the markers so a
11
+ * re-compose stays idempotent. The brand replacement, by contrast, targets JSX
12
+ * TEXT (the chrome wordmark/copyright), so its injected value is JSX-escaped —
13
+ * source-side literal matching alone does NOT make the destination context safe.
14
+ */
15
+ /** The exact marker pair for a named data-slot (mirrors build-registry's `dataSlotMarkers`). */
16
+ export declare function dataSlotMarkers(name: string): {
17
+ open: string;
18
+ close: string;
19
+ };
20
+ /** Thrown when a required data-slot marker or brand literal is missing from a source. */
21
+ export declare class DataSlotError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ /**
25
+ * Replace the body delimited by a named data-slot's markers with `replacement`
26
+ * (the full const declaration, e.g. `const NAVBAR_LINKS = [...];`). The markers
27
+ * themselves are preserved so the result stays re-composable. Throws
28
+ * {@link DataSlotError} when the opening marker is absent or ambiguous (appears
29
+ * more than once), or when no closing marker follows it — never a silent no-op.
30
+ *
31
+ * The close marker is GENERIC (`/* @cronus:data-end *\/`) and shared by every slot,
32
+ * so a block with two data-slots carries two close markers. We therefore pair the
33
+ * open marker with the FIRST close marker that follows it (its own region end),
34
+ * rather than rejecting any source in which the shared close marker repeats. This
35
+ * is deterministic and unambiguous because slot regions are flat siblings (never
36
+ * nested), so the first `data-end` after an `@cronus:data <name>` open always
37
+ * belongs to that same slot. The only ambiguity we still reject is a duplicated
38
+ * OPEN marker for this named slot (we would not know which region to rewrite).
39
+ *
40
+ * `source` — the on-disk block copy; `name` — the slot id; `replacement` — the
41
+ * new const source (no surrounding markers/newlines; they are re-added).
42
+ */
43
+ export declare function replaceDataSlot(source: string, name: string, replacement: string): string;
44
+ /**
45
+ * JSX-escape a value destined for a JSX **text** context (element children). The
46
+ * chrome brand literal (`Cronus`) lives inside JSX text nodes — the navbar/footer
47
+ * wordmark `<span>…</span>` and the footer copyright `<p>…</p>` — so a raw
48
+ * substitution of a brand containing `<`, `>`, `{` or `}` would emit invalid TSX
49
+ * (`<`/`>` are token starts) or an undefined JSX expression container (`{Store}`),
50
+ * breaking `next build`. Escaping here makes ANY brand safe regardless of source,
51
+ * which is stronger than validate-and-reject. `&` is escaped first so we never
52
+ * double-escape the entities we introduce. A brand with none of these characters
53
+ * is returned unchanged (no-op for safe brands like `Acme`).
54
+ */
55
+ export declare function escapeJsxText(value: string): string;
56
+ /**
57
+ * Replace EVERY occurrence of a brand `literal` in the source with `brand`. Used
58
+ * for the chrome wordmark/copyright (`Cronus` → the app brand), which sit in JSX
59
+ * text nodes. Throws {@link DataSlotError} when the literal is absent so a
60
+ * renamed/removed anchor fails loud instead of leaving the placeholder brand.
61
+ *
62
+ * The `literal` MATCH is literal (not regex), so special characters in the search
63
+ * literal do not misbehave. The injected `brand`, however, lands in a JSX text
64
+ * context, so it is JSX-escaped ({@link escapeJsxText}) before substitution —
65
+ * otherwise a brand like `Tom > Jerry`, `Ben <Labs>`, `Acme {Store}` or `A & B`
66
+ * would emit invalid TSX / an undefined JSX expression and break `next build`.
67
+ * Escaping is a no-op for brands with no `& < > { }`, so safe brands are
68
+ * byte-identical to a raw substitution.
69
+ */
70
+ export declare function replaceBrandLiteral(source: string, literal: string, brand: string): string;
71
+ //# sourceMappingURL=data-slots.d.ts.map
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Anchored replacement of a block's `@cronus:data <name>` data const and its brand
3
+ * literal. Both mechanisms are marker/literal driven and FAIL LOUD when the anchor
4
+ * is absent — the composer must never silently target a missing slot or brand
5
+ * (the same guarantee `registry:check` enforces at build time, re-checked here
6
+ * against the bytes actually on disk).
7
+ *
8
+ * The data-slot replacement operates on DATA CONSTS ONLY (never JSX): the region
9
+ * between `/* @cronus:data X *\/` and the FIRST following `/* @cronus:data-end *\/`
10
+ * is replaced wholesale with a freshly-serialized const, keeping the markers so a
11
+ * re-compose stays idempotent. The brand replacement, by contrast, targets JSX
12
+ * TEXT (the chrome wordmark/copyright), so its injected value is JSX-escaped —
13
+ * source-side literal matching alone does NOT make the destination context safe.
14
+ */
15
+ /** The exact marker pair for a named data-slot (mirrors build-registry's `dataSlotMarkers`). */
16
+ export function dataSlotMarkers(name) {
17
+ return { open: `/* @cronus:data ${name} */`, close: "/* @cronus:data-end */" };
18
+ }
19
+ /** Thrown when a required data-slot marker or brand literal is missing from a source. */
20
+ export class DataSlotError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "DataSlotError";
24
+ }
25
+ }
26
+ /**
27
+ * Replace the body delimited by a named data-slot's markers with `replacement`
28
+ * (the full const declaration, e.g. `const NAVBAR_LINKS = [...];`). The markers
29
+ * themselves are preserved so the result stays re-composable. Throws
30
+ * {@link DataSlotError} when the opening marker is absent or ambiguous (appears
31
+ * more than once), or when no closing marker follows it — never a silent no-op.
32
+ *
33
+ * The close marker is GENERIC (`/* @cronus:data-end *\/`) and shared by every slot,
34
+ * so a block with two data-slots carries two close markers. We therefore pair the
35
+ * open marker with the FIRST close marker that follows it (its own region end),
36
+ * rather than rejecting any source in which the shared close marker repeats. This
37
+ * is deterministic and unambiguous because slot regions are flat siblings (never
38
+ * nested), so the first `data-end` after an `@cronus:data <name>` open always
39
+ * belongs to that same slot. The only ambiguity we still reject is a duplicated
40
+ * OPEN marker for this named slot (we would not know which region to rewrite).
41
+ *
42
+ * `source` — the on-disk block copy; `name` — the slot id; `replacement` — the
43
+ * new const source (no surrounding markers/newlines; they are re-added).
44
+ */
45
+ export function replaceDataSlot(source, name, replacement) {
46
+ const { open, close } = dataSlotMarkers(name);
47
+ const openAt = source.indexOf(open);
48
+ if (openAt === -1) {
49
+ throw new DataSlotError(`data-slot "${name}": opening marker \`${open}\` not found in source`);
50
+ }
51
+ if (source.indexOf(open, openAt + open.length) !== -1) {
52
+ throw new DataSlotError(`data-slot "${name}": opening marker \`${open}\` appears more than once`);
53
+ }
54
+ // Pair with the FIRST close marker after this slot's open marker — that is this
55
+ // slot's region end. Other slots' close markers appearing later in the source
56
+ // are irrelevant (they close their own regions), so we do NOT reject them.
57
+ const closeAt = source.indexOf(close, openAt + open.length);
58
+ if (closeAt === -1) {
59
+ throw new DataSlotError(`data-slot "${name}": closing marker \`${close}\` not found after the opening marker`);
60
+ }
61
+ const before = source.slice(0, openAt);
62
+ const after = source.slice(closeAt + close.length);
63
+ return `${before}${open}\n${replacement}\n${close}${after}`;
64
+ }
65
+ /**
66
+ * JSX-escape a value destined for a JSX **text** context (element children). The
67
+ * chrome brand literal (`Cronus`) lives inside JSX text nodes — the navbar/footer
68
+ * wordmark `<span>…</span>` and the footer copyright `<p>…</p>` — so a raw
69
+ * substitution of a brand containing `<`, `>`, `{` or `}` would emit invalid TSX
70
+ * (`<`/`>` are token starts) or an undefined JSX expression container (`{Store}`),
71
+ * breaking `next build`. Escaping here makes ANY brand safe regardless of source,
72
+ * which is stronger than validate-and-reject. `&` is escaped first so we never
73
+ * double-escape the entities we introduce. A brand with none of these characters
74
+ * is returned unchanged (no-op for safe brands like `Acme`).
75
+ */
76
+ export function escapeJsxText(value) {
77
+ return value
78
+ .replace(/&/g, "&amp;")
79
+ .replace(/</g, "&lt;")
80
+ .replace(/>/g, "&gt;")
81
+ .replace(/\{/g, "&#123;")
82
+ .replace(/\}/g, "&#125;");
83
+ }
84
+ /**
85
+ * Replace EVERY occurrence of a brand `literal` in the source with `brand`. Used
86
+ * for the chrome wordmark/copyright (`Cronus` → the app brand), which sit in JSX
87
+ * text nodes. Throws {@link DataSlotError} when the literal is absent so a
88
+ * renamed/removed anchor fails loud instead of leaving the placeholder brand.
89
+ *
90
+ * The `literal` MATCH is literal (not regex), so special characters in the search
91
+ * literal do not misbehave. The injected `brand`, however, lands in a JSX text
92
+ * context, so it is JSX-escaped ({@link escapeJsxText}) before substitution —
93
+ * otherwise a brand like `Tom > Jerry`, `Ben <Labs>`, `Acme {Store}` or `A & B`
94
+ * would emit invalid TSX / an undefined JSX expression and break `next build`.
95
+ * Escaping is a no-op for brands with no `& < > { }`, so safe brands are
96
+ * byte-identical to a raw substitution.
97
+ */
98
+ export function replaceBrandLiteral(source, literal, brand) {
99
+ if (!source.includes(literal)) {
100
+ throw new DataSlotError(`brand literal "${literal}" not found in source`);
101
+ }
102
+ return source.split(literal).join(escapeJsxText(brand));
103
+ }
104
+ //# sourceMappingURL=data-slots.js.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * App-manifest types + a strict, hand-rolled structural parser/validator (no new
3
+ * dep). The parser is deliberately paranoid: manifests arrive from disk (bundled
4
+ * template or `--manifest` file) and drive code generation, so an unknown
5
+ * `planVersion` or any unknown field is a HARD ERROR (D3 graft) rather than a
6
+ * silently-ignored typo. Only structural/shape checks live here — semantic checks
7
+ * that need the registry index/meta (does the block exist? does its kind fit the
8
+ * slot?) live in `plan.ts`.
9
+ */
10
+ /** The only plan schema version F1 understands. Unknown values are rejected. */
11
+ export declare const SUPPORTED_PLAN_VERSION = 1;
12
+ /** A block reference in a page: a bare slug (default variant) or `{ block, variant }`. */
13
+ export type BlockRef = string | {
14
+ block: string;
15
+ variant?: string;
16
+ };
17
+ /** One route in the app. `nav` presence lists it in the generated navbar/footer. */
18
+ export interface Page {
19
+ /** App Router route, e.g. "/", "/products", "/products/[id]". */
20
+ route: string;
21
+ /** <title> metadata for the page. */
22
+ title: string;
23
+ /** Label shown in the nav; omit to keep the page out of the nav. */
24
+ nav?: string;
25
+ /** Which chrome group the page renders under (key of `chrome`). */
26
+ chrome: string;
27
+ /** The blocks stacked (top-to-bottom) inside the page's <main>. */
28
+ blocks: BlockRef[];
29
+ }
30
+ /** One chrome group → the route group + its layout furniture. F1 supports site/bare. */
31
+ export interface ChromeGroup {
32
+ /** Navbar block slug (chrome kind). Omit for a bare/centered group. */
33
+ navbar?: string;
34
+ /** Footer block slug (chrome kind). Omit for a bare/centered group. */
35
+ footer?: string;
36
+ /** App-shell block slug (F2 sidebar shell). Present in the type; unused in F1. */
37
+ block?: string;
38
+ }
39
+ export type ChromeMap = Record<string, ChromeGroup>;
40
+ /** App-level defaults baked into the generated app. */
41
+ export interface ManifestDefaults {
42
+ theme?: string;
43
+ mode?: string;
44
+ /** Brand wordmark; `__APP_NAME__` is substituted with the project name. */
45
+ brand?: string;
46
+ }
47
+ /** The manifest body (everything under the top-level `manifest` key). */
48
+ export interface ManifestBody {
49
+ title: string;
50
+ description: string;
51
+ chrome: ChromeMap;
52
+ pages: Page[];
53
+ /** Next special-file blocks by kind, e.g. `{ "not-found": "not-found" }`. */
54
+ extras?: Record<string, string>;
55
+ defaults?: ManifestDefaults;
56
+ }
57
+ /** A full app-template manifest as stored in `packages/cli/templates/apps/*.json`. */
58
+ export interface AppManifest {
59
+ name: string;
60
+ type: "registry:app";
61
+ planVersion: number;
62
+ manifest: ManifestBody;
63
+ }
64
+ /** Thrown by {@link parseManifest} with EVERY structural problem found, aggregated. */
65
+ export declare class ManifestError extends Error {
66
+ readonly errors: string[];
67
+ constructor(errors: string[]);
68
+ }
69
+ /**
70
+ * Parse + strictly validate a raw (JSON-parsed) manifest value into a typed
71
+ * {@link AppManifest}. Aggregates ALL structural problems and throws a single
72
+ * {@link ManifestError} listing them (never on the first). Rejects an unknown
73
+ * `planVersion` and any unknown field at every level (strict D3 graft) so a
74
+ * template typo can never be silently ignored by the generator.
75
+ */
76
+ export declare function parseManifest(raw: unknown): AppManifest;
77
+ /** Normalize a block reference to its `{ slug, variant }` parts (default variant = undefined). */
78
+ export declare function blockRefParts(ref: BlockRef): {
79
+ slug: string;
80
+ variant?: string;
81
+ };
82
+ /**
83
+ * A deterministic content fingerprint of a parsed manifest, used as compose
84
+ * provenance persisted in `composed{}.manifestHash`. Keyed on the manifest's
85
+ * meaningful shape (name + planVersion + body) via canonical sorted-key JSON, so
86
+ * two manifests fingerprint equal iff they describe the same app — independent of
87
+ * source key order or whitespace. Pure (no Date/random) so it stays byte-stable.
88
+ */
89
+ export declare function manifestFingerprint(manifest: AppManifest): string;
90
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1,224 @@
1
+ /**
2
+ * App-manifest types + a strict, hand-rolled structural parser/validator (no new
3
+ * dep). The parser is deliberately paranoid: manifests arrive from disk (bundled
4
+ * template or `--manifest` file) and drive code generation, so an unknown
5
+ * `planVersion` or any unknown field is a HARD ERROR (D3 graft) rather than a
6
+ * silently-ignored typo. Only structural/shape checks live here — semantic checks
7
+ * that need the registry index/meta (does the block exist? does its kind fit the
8
+ * slot?) live in `plan.ts`.
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ /** The only plan schema version F1 understands. Unknown values are rejected. */
12
+ export const SUPPORTED_PLAN_VERSION = 1;
13
+ /** Thrown by {@link parseManifest} with EVERY structural problem found, aggregated. */
14
+ export class ManifestError extends Error {
15
+ errors;
16
+ constructor(errors) {
17
+ super(`Invalid app manifest:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
18
+ this.name = "ManifestError";
19
+ this.errors = errors;
20
+ }
21
+ }
22
+ /** True for a plain (non-array, non-null) object. */
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ /** Push an "unknown field" error for every key of `obj` not in `allowed`. */
27
+ function rejectUnknownKeys(obj, allowed, where, errors) {
28
+ for (const key of Object.keys(obj)) {
29
+ if (!allowed.includes(key)) {
30
+ errors.push(`${where}: unknown field "${key}" (allowed: ${allowed.join(", ")})`);
31
+ }
32
+ }
33
+ }
34
+ const MANIFEST_TOP_KEYS = ["name", "type", "planVersion", "manifest"];
35
+ const MANIFEST_BODY_KEYS = [
36
+ "title",
37
+ "description",
38
+ "chrome",
39
+ "pages",
40
+ "extras",
41
+ "defaults",
42
+ ];
43
+ const PAGE_KEYS = ["route", "title", "nav", "chrome", "blocks"];
44
+ const CHROME_GROUP_KEYS = ["navbar", "footer", "block"];
45
+ const DEFAULTS_KEYS = ["theme", "mode", "brand"];
46
+ const BLOCKREF_KEYS = ["block", "variant"];
47
+ function validateBlockRef(value, where, errors) {
48
+ if (typeof value === "string") {
49
+ if (value.length === 0)
50
+ errors.push(`${where}: block reference is an empty string`);
51
+ return;
52
+ }
53
+ if (!isRecord(value)) {
54
+ errors.push(`${where}: block reference must be a string or { block, variant }`);
55
+ return;
56
+ }
57
+ rejectUnknownKeys(value, BLOCKREF_KEYS, where, errors);
58
+ if (typeof value.block !== "string" || value.block.length === 0) {
59
+ errors.push(`${where}: block reference object needs a non-empty "block" string`);
60
+ }
61
+ if (value.variant !== undefined && typeof value.variant !== "string") {
62
+ errors.push(`${where}: block "variant" must be a string when present`);
63
+ }
64
+ }
65
+ function validateChromeGroup(value, where, errors) {
66
+ if (!isRecord(value)) {
67
+ errors.push(`${where}: chrome group must be an object`);
68
+ return;
69
+ }
70
+ rejectUnknownKeys(value, CHROME_GROUP_KEYS, where, errors);
71
+ for (const key of ["navbar", "footer", "block"]) {
72
+ if (value[key] !== undefined && typeof value[key] !== "string") {
73
+ errors.push(`${where}: "${key}" must be a string when present`);
74
+ }
75
+ }
76
+ }
77
+ function validatePage(value, index, errors) {
78
+ const where = `pages[${index}]`;
79
+ if (!isRecord(value)) {
80
+ errors.push(`${where}: page must be an object`);
81
+ return;
82
+ }
83
+ rejectUnknownKeys(value, PAGE_KEYS, where, errors);
84
+ if (typeof value.route !== "string" || value.route.length === 0) {
85
+ errors.push(`${where}: "route" must be a non-empty string`);
86
+ }
87
+ if (typeof value.title !== "string" || value.title.length === 0) {
88
+ errors.push(`${where}: "title" must be a non-empty string`);
89
+ }
90
+ if (value.nav !== undefined && (typeof value.nav !== "string" || value.nav.length === 0)) {
91
+ errors.push(`${where}: "nav" must be a non-empty string when present`);
92
+ }
93
+ if (typeof value.chrome !== "string" || value.chrome.length === 0) {
94
+ errors.push(`${where}: "chrome" must be a non-empty string`);
95
+ }
96
+ if (!Array.isArray(value.blocks) || value.blocks.length === 0) {
97
+ errors.push(`${where}: "blocks" must be a non-empty array`);
98
+ }
99
+ else {
100
+ value.blocks.forEach((b, i) => {
101
+ validateBlockRef(b, `${where}.blocks[${i}]`, errors);
102
+ });
103
+ }
104
+ }
105
+ function validateBody(value, errors) {
106
+ if (!isRecord(value)) {
107
+ errors.push(`"manifest" must be an object`);
108
+ return;
109
+ }
110
+ rejectUnknownKeys(value, MANIFEST_BODY_KEYS, "manifest", errors);
111
+ if (typeof value.title !== "string" || value.title.length === 0) {
112
+ errors.push(`manifest.title must be a non-empty string`);
113
+ }
114
+ if (typeof value.description !== "string") {
115
+ errors.push(`manifest.description must be a string`);
116
+ }
117
+ if (!isRecord(value.chrome)) {
118
+ errors.push(`manifest.chrome must be an object`);
119
+ }
120
+ else {
121
+ for (const [key, group] of Object.entries(value.chrome)) {
122
+ validateChromeGroup(group, `manifest.chrome.${key}`, errors);
123
+ }
124
+ }
125
+ if (!Array.isArray(value.pages) || value.pages.length === 0) {
126
+ errors.push(`manifest.pages must be a non-empty array`);
127
+ }
128
+ else {
129
+ value.pages.forEach((p, i) => {
130
+ validatePage(p, i, errors);
131
+ });
132
+ }
133
+ if (value.extras !== undefined) {
134
+ if (!isRecord(value.extras)) {
135
+ errors.push(`manifest.extras must be an object when present`);
136
+ }
137
+ else {
138
+ for (const [key, slug] of Object.entries(value.extras)) {
139
+ if (typeof slug !== "string" || slug.length === 0) {
140
+ errors.push(`manifest.extras.${key} must be a non-empty string`);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ if (value.defaults !== undefined) {
146
+ if (!isRecord(value.defaults)) {
147
+ errors.push(`manifest.defaults must be an object when present`);
148
+ }
149
+ else {
150
+ rejectUnknownKeys(value.defaults, DEFAULTS_KEYS, "manifest.defaults", errors);
151
+ for (const key of DEFAULTS_KEYS) {
152
+ if (value.defaults[key] !== undefined && typeof value.defaults[key] !== "string") {
153
+ errors.push(`manifest.defaults.${key} must be a string when present`);
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ /**
160
+ * Parse + strictly validate a raw (JSON-parsed) manifest value into a typed
161
+ * {@link AppManifest}. Aggregates ALL structural problems and throws a single
162
+ * {@link ManifestError} listing them (never on the first). Rejects an unknown
163
+ * `planVersion` and any unknown field at every level (strict D3 graft) so a
164
+ * template typo can never be silently ignored by the generator.
165
+ */
166
+ export function parseManifest(raw) {
167
+ const errors = [];
168
+ if (!isRecord(raw)) {
169
+ throw new ManifestError(["manifest must be a JSON object"]);
170
+ }
171
+ rejectUnknownKeys(raw, MANIFEST_TOP_KEYS, "manifest root", errors);
172
+ if (typeof raw.name !== "string" || raw.name.length === 0) {
173
+ errors.push(`"name" must be a non-empty string`);
174
+ }
175
+ if (raw.type !== "registry:app") {
176
+ errors.push(`"type" must be "registry:app"`);
177
+ }
178
+ if (raw.planVersion !== SUPPORTED_PLAN_VERSION) {
179
+ errors.push(`unsupported "planVersion" ${JSON.stringify(raw.planVersion)} — ` +
180
+ `this CLI only understands planVersion ${SUPPORTED_PLAN_VERSION}.`);
181
+ }
182
+ validateBody(raw.manifest, errors);
183
+ if (errors.length > 0)
184
+ throw new ManifestError(errors);
185
+ // Every branch above is proven; the cast is safe.
186
+ return raw;
187
+ }
188
+ /** Normalize a block reference to its `{ slug, variant }` parts (default variant = undefined). */
189
+ export function blockRefParts(ref) {
190
+ if (typeof ref === "string")
191
+ return { slug: ref };
192
+ return ref.variant !== undefined
193
+ ? { slug: ref.block, variant: ref.variant }
194
+ : { slug: ref.block };
195
+ }
196
+ /** Canonical (sorted-key) JSON of a value — deterministic regardless of key order. */
197
+ function canonicalJson(value) {
198
+ if (value === null || typeof value !== "object")
199
+ return JSON.stringify(value);
200
+ if (Array.isArray(value))
201
+ return `[${value.map(canonicalJson).join(",")}]`;
202
+ const obj = value;
203
+ const entries = Object.keys(obj)
204
+ .sort()
205
+ .map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`);
206
+ return `{${entries.join(",")}}`;
207
+ }
208
+ /**
209
+ * A deterministic content fingerprint of a parsed manifest, used as compose
210
+ * provenance persisted in `composed{}.manifestHash`. Keyed on the manifest's
211
+ * meaningful shape (name + planVersion + body) via canonical sorted-key JSON, so
212
+ * two manifests fingerprint equal iff they describe the same app — independent of
213
+ * source key order or whitespace. Pure (no Date/random) so it stays byte-stable.
214
+ */
215
+ export function manifestFingerprint(manifest) {
216
+ const canonical = canonicalJson({
217
+ name: manifest.name,
218
+ type: manifest.type,
219
+ planVersion: manifest.planVersion,
220
+ manifest: manifest.manifest,
221
+ });
222
+ return createHash("sha256").update(canonical).digest("hex");
223
+ }
224
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Pure plan builder + validator. Turns a parsed {@link AppManifest} + caller
3
+ * `choices` into a {@link ComposePlan} the renderer consumes, and aggregates ALL
4
+ * semantic errors (block exists? kind fits slot? routes unique/valid? no
5
+ * dynamic-slug collisions Next rejects? chrome refs exist? extras resolve to a
6
+ * page-kind special file? deps safe?) before returning — never bailing on the
7
+ * first. Nothing here touches the filesystem; the command feeds it the registry
8
+ * index, meta, and shipped chrome sources.
9
+ */
10
+ import type { ComposedChoices } from "../config.js";
11
+ import type { RegistryIndex } from "../registry.js";
12
+ import { type AppManifest } from "./manifest.js";
13
+ /** Semantic kind of a block (mirrors build-registry's BlockKind, read from meta). */
14
+ export type BlockKind = "page" | "section" | "chrome" | "email";
15
+ /** A brand-token anchor as recorded in meta. */
16
+ export interface BrandToken {
17
+ token: string;
18
+ literal: string;
19
+ }
20
+ /**
21
+ * A block variant as recorded in meta: its id + the registry `item` the composer
22
+ * installs (bare `<slug>` for the default, `<slug>--<id>` otherwise) + the
23
+ * component `exportName` a generated page imports.
24
+ */
25
+ export interface VariantMetaRef {
26
+ id: string;
27
+ item: string;
28
+ exportName: string;
29
+ }
30
+ /** The subset of `registry/meta.json` the planner reads. */
31
+ export interface ComposeMetaBlock {
32
+ exportName: string;
33
+ kind: BlockKind;
34
+ dataSlots: string[];
35
+ brandTokens: BrandToken[];
36
+ /** Declared variants (default first); empty when the block has none. */
37
+ variants?: VariantMetaRef[];
38
+ }
39
+ export interface ComposeMeta {
40
+ blocks: Record<string, ComposeMetaBlock>;
41
+ }
42
+ /**
43
+ * A resolved block inside a page: its family slug, the registry `item` name to
44
+ * install (bare `<slug>`, or `<slug>--<variant>` for a non-default variant), the
45
+ * `exportName` a generated page imports, the block kind, and the selected
46
+ * `variant` id when a non-default variant was chosen (absent for the default).
47
+ */
48
+ export interface PlanBlock {
49
+ slug: string;
50
+ /** Registry item name to install/import from (`<slug>` or `<slug>--<variant>`). */
51
+ item: string;
52
+ exportName: string;
53
+ kind: BlockKind;
54
+ /** Selected non-default variant id (absent = default variant). */
55
+ variant?: string;
56
+ }
57
+ /** A resolved page ready to render. */
58
+ export interface PlanPage {
59
+ route: string;
60
+ title: string;
61
+ nav?: string;
62
+ chrome: string;
63
+ blocks: PlanBlock[];
64
+ }
65
+ /** A resolved chrome group (route group + its navbar/footer/shell slugs). */
66
+ export interface PlanChrome {
67
+ group: string;
68
+ navbar?: string;
69
+ footer?: string;
70
+ /** App-shell chrome block slug (sidebar shell), when the group uses one. */
71
+ block?: string;
72
+ }
73
+ /**
74
+ * A resolved Next special-file block (from `manifest.extras`). `file` is the
75
+ * emitted Next special-file path (e.g. `app/not-found.tsx`) and the block is a
76
+ * page-kind block wrapped by the renderer under the golden rule.
77
+ */
78
+ export interface PlanExtra {
79
+ /** The extras KEY = the Next special-file name (e.g. "not-found"). */
80
+ key: string;
81
+ /** Emitted file path under `app/` (e.g. "app/not-found.tsx"). */
82
+ file: string;
83
+ /** The resolved block slug for this special file. */
84
+ slug: string;
85
+ /** The block's meta export name (what the wrapper imports). */
86
+ exportName: string;
87
+ }
88
+ /** The fully-resolved, validated plan the renderer turns into files. */
89
+ export interface ComposePlan {
90
+ /** The template/manifest name — the key of the `composed{}` record. */
91
+ templateName: string;
92
+ /** The project name (brand default + `__APP_NAME__` + legacy snapshot-dir fallback). */
93
+ appName: string;
94
+ planVersion: number;
95
+ title: string;
96
+ description: string;
97
+ choices: ComposedChoices;
98
+ pages: PlanPage[];
99
+ chromes: PlanChrome[];
100
+ /** Resolved Next special-file blocks (from `manifest.extras`), sorted by key. */
101
+ extras: PlanExtra[];
102
+ /**
103
+ * Every unique registry ITEM the plan installs (pages + chrome + extras),
104
+ * sorted. Item names, not family slugs: a non-default variant contributes its
105
+ * `<slug>--<variant>` item, so this is what the composer resolves/installs.
106
+ */
107
+ blockSlugs: string[];
108
+ /** Chrome block slugs used by any group, sorted. */
109
+ chromeSlugs: string[];
110
+ /** Shipped source per chrome slug (for the in-place data-slot/brand rewrite). */
111
+ chromeSources: Record<string, string>;
112
+ /** Declared data-slots per chrome slug (from meta). */
113
+ dataSlotsBySlug: Record<string, string[]>;
114
+ /** Declared brand tokens per chrome slug (from meta). */
115
+ brandTokensBySlug: Record<string, BrandToken[]>;
116
+ /** Whether any group uses a navbar / footer / shell (drives wrapper emission). */
117
+ usesNavbar: boolean;
118
+ usesFooter: boolean;
119
+ usesShell: boolean;
120
+ navbarSlug?: string;
121
+ footerSlug?: string;
122
+ shellSlug?: string;
123
+ navbarExportName?: string;
124
+ footerExportName?: string;
125
+ shellExportName?: string;
126
+ }
127
+ /** Thrown by {@link buildComposePlan} carrying EVERY semantic error found. */
128
+ export declare class ComposePlanError extends Error {
129
+ readonly errors: string[];
130
+ constructor(errors: string[]);
131
+ }
132
+ /** Options a caller passes to shape the plan (F1: brand, seed, page subset). */
133
+ export interface ComposeChoiceInput {
134
+ /** Brand wordmark; falls back to manifest default (with `__APP_NAME__` filled) then appName. */
135
+ brand?: string;
136
+ /** Aesthetic PRNG seed (recorded when present; F1 renderer does not vary on it yet). */
137
+ seed?: number;
138
+ /** Route subset to include (F2); when omitted, all manifest pages are used. */
139
+ pages?: string[];
140
+ /**
141
+ * Per-family variant override (`{ login: "split" }`); a present entry wins over
142
+ * the manifest ref's declared variant for that slug. Recorded in `composed{}`.
143
+ */
144
+ variants?: Record<string, string>;
145
+ /** Project name, used to fill `__APP_NAME__` in the manifest brand default. */
146
+ appName?: string;
147
+ }
148
+ /**
149
+ * Build a {@link ComposePlan} from a parsed manifest, caller choices, the registry
150
+ * index, meta, and the shipped chrome sources. Aggregates ALL semantic errors and
151
+ * THROWS {@link ComposePlanError} when invalid. The returned plan is a pure
152
+ * function of its inputs (stable ordering, no I/O/Date).
153
+ */
154
+ export declare function buildComposePlan(manifest: AppManifest, input: ComposeChoiceInput, index: RegistryIndex, meta: ComposeMeta, chromeSources: Record<string, string>): ComposePlan;
155
+ /**
156
+ * Validate-only entry point: builds the plan and returns the aggregated error
157
+ * list (empty when valid) instead of throwing. Handy for `--dry-run` callers and
158
+ * tests that assert on the full error set. The successful plan is returned too.
159
+ */
160
+ export declare function validateComposePlan(manifest: AppManifest, input: ComposeChoiceInput, index: RegistryIndex, meta: ComposeMeta, chromeSources: Record<string, string>): {
161
+ plan?: ComposePlan;
162
+ errors: string[];
163
+ };
164
+ //# sourceMappingURL=plan.d.ts.map