create-lumen 1.2.0 → 2.0.0-alpha.1

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 (146) hide show
  1. package/README.md +71 -9
  2. package/bin/cli.js +18 -4
  3. package/package.json +15 -4
  4. package/schema/fixtures/index.js +62 -0
  5. package/schema/fixtures/v2/invalid/architecture-preset-type-mismatch.json +35 -0
  6. package/schema/fixtures/v2/invalid/harness-empty-commands.json +37 -0
  7. package/schema/fixtures/v2/invalid/harness-missing-command.json +41 -0
  8. package/schema/fixtures/v2/invalid/invalid-manifest-version.json +33 -0
  9. package/schema/fixtures/v2/invalid/invalid-validation-mode.json +34 -0
  10. package/schema/fixtures/v2/invalid/malformed-paths.json +33 -0
  11. package/schema/fixtures/v2/invalid/missing-required-fields.json +17 -0
  12. package/schema/fixtures/v2/invalid/next-type-based.json +33 -0
  13. package/schema/fixtures/v2/invalid/next-vite.json +33 -0
  14. package/schema/fixtures/v2/invalid/react-bundler.json +34 -0
  15. package/schema/fixtures/v2/invalid/react-hybrid.json +33 -0
  16. package/schema/fixtures/v2/invalid/react-variant-mismatch.json +33 -0
  17. package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +33 -0
  18. package/schema/fixtures/v2/invalid/v1-flat-config.json +11 -0
  19. package/schema/fixtures/v2/valid/full-kitchen-sink.json +35 -0
  20. package/schema/fixtures/v2/valid/harness-full.json +44 -0
  21. package/schema/fixtures/v2/valid/next-app-router-feature-based.json +35 -0
  22. package/schema/fixtures/v2/valid/next-app-router-hybrid.json +35 -0
  23. package/schema/fixtures/v2/valid/next-app-router-none.json +33 -0
  24. package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +35 -0
  25. package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +35 -0
  26. package/schema/fixtures/v2/valid/next-pages-router-none.json +33 -0
  27. package/schema/fixtures/v2/valid/react-vite-feature-based.json +33 -0
  28. package/schema/fixtures/v2/valid/react-vite-none.json +33 -0
  29. package/schema/fixtures/v2/valid/react-vite-type-based.json +33 -0
  30. package/schema/fixtures/v2/valid/validation-none.json +41 -0
  31. package/schema/fixtures/v2/valid/validation-relaxed.json +41 -0
  32. package/schema/lumen.config.v2.json +269 -0
  33. package/skills/create-lumen/SKILL.md +110 -0
  34. package/skills/create-lumen/references/manifest-v2.md +9 -0
  35. package/src/agent-context.js +114 -0
  36. package/src/cli-args.js +294 -0
  37. package/src/css.js +20 -9
  38. package/src/dependencies.js +12 -6
  39. package/src/engine/capabilities.js +218 -0
  40. package/src/engine/index.js +2 -0
  41. package/src/engine/providers/base-provider.js +219 -0
  42. package/src/injector.js +26 -13
  43. package/src/main.js +66 -10
  44. package/src/manifest/emit.js +166 -0
  45. package/src/manifest/index.js +11 -0
  46. package/src/manifest/paths.js +128 -0
  47. package/src/manifest/schema.js +270 -0
  48. package/src/prompts.js +26 -6
  49. package/templates/conditional/linting/eslint/next/eslint.config.mjs +16 -0
  50. package/templates/css/tailwind/postcss.config.mjs +5 -0
  51. /package/templates/architectures/{component-based → type-based}/main.jsx +0 -0
  52. /package/templates/architectures/{component-based → type-based}/main.tsx +0 -0
  53. /package/templates/architectures/{component-based → type-based}/src/App.jsx +0 -0
  54. /package/templates/architectures/{component-based → type-based}/src/App.tsx +0 -0
  55. /package/templates/architectures/{component-based → type-based}/src/components/common/Button.jsx +0 -0
  56. /package/templates/architectures/{component-based → type-based}/src/components/common/Button.tsx +0 -0
  57. /package/templates/architectures/{component-based → type-based}/src/components/common/index.js +0 -0
  58. /package/templates/architectures/{component-based → type-based}/src/components/common/index.ts +0 -0
  59. /package/templates/architectures/{component-based → type-based}/src/components/form/.gitkeep +0 -0
  60. /package/templates/architectures/{component-based → type-based}/src/config/.gitkeep +0 -0
  61. /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.js +0 -0
  62. /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.ts +0 -0
  63. /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.jsx +0 -0
  64. /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.ts +0 -0
  65. /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  66. /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  67. /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  68. /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  69. /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.js +0 -0
  70. /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.ts +0 -0
  71. /package/templates/architectures/{component-based → type-based}/src/providers/.gitkeep +0 -0
  72. /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.jsx +0 -0
  73. /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.tsx +0 -0
  74. /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.js +0 -0
  75. /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.ts +0 -0
  76. /package/templates/architectures/{component-based → type-based}/src/services/.gitkeep +0 -0
  77. /package/templates/architectures/{component-based → type-based}/src/store/.gitkeep +0 -0
  78. /package/templates/architectures/{component-based → type-based}/src/styles/.gitkeep +0 -0
  79. /package/templates/architectures/{component-based → type-based}/src/types/index.js +0 -0
  80. /package/templates/architectures/{component-based → type-based}/src/types/index.ts +0 -0
  81. /package/templates/architectures/{component-based → type-based}/src/utils/cn.jsx +0 -0
  82. /package/templates/architectures/{component-based → type-based}/src/utils/cn.ts +0 -0
  83. /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.js +0 -0
  84. /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.ts +0 -0
  85. /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.js +0 -0
  86. /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.ts +0 -0
  87. /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.js +0 -0
  88. /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.ts +0 -0
  89. /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.js +0 -0
  90. /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.ts +0 -0
  91. /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.js +0 -0
  92. /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.ts +0 -0
  93. /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.js +0 -0
  94. /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.ts +0 -0
  95. /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
  96. /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
  97. /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  98. /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  99. /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
  100. /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
  101. /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
  102. /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
  103. /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  104. /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  105. /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
  106. /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
  107. /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.jsx +0 -0
  108. /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.tsx +0 -0
  109. /package/templates/conditional/router/{component-based → type-based}/src/App.jsx +0 -0
  110. /package/templates/conditional/router/{component-based → type-based}/src/App.tsx +0 -0
  111. /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  112. /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  113. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.jsx +0 -0
  114. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.tsx +0 -0
  115. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.jsx +0 -0
  116. /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.tsx +0 -0
  117. /package/templates/conditional/router/{component-based → type-based}/src/router/index.jsx +0 -0
  118. /package/templates/conditional/router/{component-based → type-based}/src/router/index.tsx +0 -0
  119. /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.jsx +0 -0
  120. /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.tsx +0 -0
  121. /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.jsx +0 -0
  122. /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.ts +0 -0
  123. /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.jsx +0 -0
  124. /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.tsx +0 -0
  125. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.js +0 -0
  126. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.ts +0 -0
  127. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.js +0 -0
  128. /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.ts +0 -0
  129. /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.jsx +0 -0
  130. /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.ts +0 -0
  131. /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
  132. /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
  133. /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
  134. /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
  135. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.jsx +0 -0
  136. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.tsx +0 -0
  137. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  138. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  139. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  140. /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
  141. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.jsx +0 -0
  142. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.tsx +0 -0
  143. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
  144. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
  145. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
  146. /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Path mapping for lumen.config.json — #15
3
+ * Resolves the 6 generator target directories from framework + architecture.
4
+ * Paths are relative to project root and use posix separators.
5
+ */
6
+
7
+ export const PATH_KEYS = ["features", "components", "services", "hooks", "pages", "ui"];
8
+
9
+ /**
10
+ * @param {{ architecture: string, framework?: { name?: string, variant?: string } }} opts
11
+ * @returns {{ features: string, components: string, services: string, hooks: string, pages: string, ui: string }}
12
+ */
13
+ export function resolvePaths({ architecture, framework = {} } = {}) {
14
+ const arch = architecture || "feature-based";
15
+ const fwName = framework.name || "react";
16
+ const variant = framework.variant || (fwName === "next" ? "app-router" : "vite");
17
+
18
+ // Helper to pick Next.js pages root based on variant
19
+ const nextPagesForVariant = (v) => (v === "pages-router" ? "pages" : "app");
20
+ const isNext = fwName === "next";
21
+
22
+ // -- feature-based (React + Vite canonical, also Next feature-based)
23
+ if (arch === "feature-based") {
24
+ if (isNext) {
25
+ const nextRoot = nextPagesForVariant(variant);
26
+ // Next keeps features in src/features, shared code in src/shared, routing in app/ or pages/
27
+ // Use posix paths regardless of OS
28
+ return {
29
+ features: "src/features",
30
+ components: "src/shared/components",
31
+ services: "src/shared/services",
32
+ hooks: "src/shared/hooks",
33
+ pages: nextRoot, // app/ or pages/ at project root for Next
34
+ ui: "src/shared/components/ui",
35
+ };
36
+ }
37
+ return {
38
+ features: "src/features",
39
+ components: "src/shared/components",
40
+ services: "src/shared/services",
41
+ hooks: "src/shared/hooks",
42
+ pages: "src/app/router",
43
+ ui: "src/shared/components/ui",
44
+ };
45
+ }
46
+
47
+ // -- type-based (React + Vite only; component-based supported as legacy alias)
48
+ if (arch === "type-based" || arch === "component-based") {
49
+ if (isNext) {
50
+ // Not a valid combo per schema, but provide a deterministic fallback
51
+ // to keep resolver total (hybrid should be used for Next)
52
+ const nextRoot = nextPagesForVariant(variant);
53
+ return {
54
+ features: "src/components",
55
+ components: "src/components",
56
+ services: "src/services",
57
+ hooks: "src/hooks",
58
+ pages: nextRoot,
59
+ ui: "src/components/ui",
60
+ };
61
+ }
62
+ return {
63
+ features: "src/features",
64
+ components: "src/components",
65
+ services: "src/services",
66
+ hooks: "src/hooks",
67
+ pages: "src/pages",
68
+ ui: "src/ui",
69
+ };
70
+ }
71
+
72
+ // -- hybrid (Next.js: feature + shared). For React, fallback to feature-based
73
+ if (arch === "hybrid") {
74
+ if (isNext) {
75
+ const nextRoot = nextPagesForVariant(variant);
76
+ return {
77
+ features: "src/features",
78
+ components: "src/shared/components",
79
+ services: "src/shared/services",
80
+ hooks: "src/shared/hooks",
81
+ pages: nextRoot,
82
+ ui: "src/shared/components/ui",
83
+ };
84
+ }
85
+ // React hybrid = hybrid not valid, but map to feature-based for determinism
86
+ return {
87
+ features: "src/features",
88
+ components: "src/shared/components",
89
+ services: "src/shared/services",
90
+ hooks: "src/shared/hooks",
91
+ pages: "src/app/router",
92
+ ui: "src/shared/components/ui",
93
+ };
94
+ }
95
+
96
+ // -- none (minimal flat src/ — no imposed convention)
97
+ if (arch === "none") {
98
+ if (isNext) {
99
+ const nextRoot = nextPagesForVariant(variant);
100
+ return {
101
+ features: "src",
102
+ components: "src/components",
103
+ services: "src/services",
104
+ hooks: "src/hooks",
105
+ pages: nextRoot,
106
+ ui: "src/components/ui",
107
+ };
108
+ }
109
+ return {
110
+ features: "src",
111
+ components: "src/components",
112
+ services: "src/services",
113
+ hooks: "src/hooks",
114
+ pages: "src/pages",
115
+ ui: "src/components/ui",
116
+ };
117
+ }
118
+
119
+ // Fallback: treat unknown as feature-based
120
+ return {
121
+ features: "src/features",
122
+ components: "src/shared/components",
123
+ services: "src/shared/services",
124
+ hooks: "src/shared/hooks",
125
+ pages: isNext ? nextPagesForVariant(variant) : "src/app/router",
126
+ ui: "src/shared/components/ui",
127
+ };
128
+ }
@@ -0,0 +1,270 @@
1
+ import { z } from "zod";
2
+ import { fromZodError } from "zod-validation-error";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Constants - keep in sync with docs/manifest-v2.md and ROADMAP
6
+ // ---------------------------------------------------------------------------
7
+ export const MANIFEST_VERSION = 2;
8
+ export const SCHEMA_URL = "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json";
9
+
10
+ export const FRAMEWORK_NAMES = ["react", "next"];
11
+ export const FRAMEWORK_VARIANTS = ["vite", "app-router", "pages-router"];
12
+ export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
13
+ export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
14
+
15
+ export const STYLING_ENGINES = ["tailwind", "bootstrap", "none"];
16
+ export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "hybrid", "none"];
17
+ // Legacy alias: docs used architecture.type, canonical is architecture.preset.
18
+ // Keep both in sync; parseManifest normalizes to expose both keys.
19
+ export const ARCHITECTURE_TYPES = ARCHITECTURE_PRESETS;
20
+ export const ARCHITECTURE_VALIDATIONS = ["strict", "relaxed", "none"];
21
+ export const UI_KITS = ["shadcn", "none"];
22
+ export const DOCS_LANGUAGES = ["en", "es"];
23
+ export const TOOLING_LANGUAGES = ["ts", "js"];
24
+ export const TOOLING_LINTERS = ["eslint", "oxlint", "biome", "none"];
25
+ export const TOOLING_FORMATTERS = ["prettier", "oxfmt", "none"];
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Sub-schemas
29
+ // ---------------------------------------------------------------------------
30
+ const frameworkSchema = z
31
+ .object({
32
+ name: z.enum(FRAMEWORK_NAMES),
33
+ variant: z.enum(FRAMEWORK_VARIANTS),
34
+ bundler: z.enum(FRAMEWORK_BUNDLERS).optional(),
35
+ adapter: z.enum(FRAMEWORK_ADAPTERS).optional(),
36
+ })
37
+ .strict();
38
+
39
+ const stylingSchema = z
40
+ .object({
41
+ engine: z.enum(STYLING_ENGINES),
42
+ })
43
+ .strict();
44
+
45
+ const architectureSchema = z
46
+ .object({
47
+ // Canonical key per docs/manifest-v2.md; legacy `type` kept as alias.
48
+ preset: z.enum(ARCHITECTURE_PRESETS).optional(),
49
+ type: z.enum(ARCHITECTURE_TYPES).optional(),
50
+ validation: z.enum(ARCHITECTURE_VALIDATIONS).optional().default("strict"),
51
+ })
52
+ .strict()
53
+ .superRefine((val, ctx) => {
54
+ if (!val.preset && !val.type) {
55
+ ctx.addIssue({
56
+ code: "custom",
57
+ path: ["preset"],
58
+ message: 'architecture must define "preset" or legacy "type"',
59
+ });
60
+ }
61
+ if (val.preset && val.type && val.preset !== val.type) {
62
+ ctx.addIssue({
63
+ code: "custom",
64
+ path: ["preset"],
65
+ message: `architecture.preset "${val.preset}" must match architecture.type "${val.type}"`,
66
+ });
67
+ }
68
+ });
69
+
70
+ export const HARNESS_DEFAULT_COMMANDS = [
71
+ { name: "lint", command: "oxlint .", required: true },
72
+ { name: "format", command: "oxfmt --check .", required: true },
73
+ { name: "types", command: "tsc -b", required: true },
74
+ { name: "test", command: "vitest run", required: true },
75
+ { name: "build", command: "npm run build", required: true },
76
+ ];
77
+
78
+ const harnessCommandSchema = z
79
+ .object({
80
+ name: z.string().min(1),
81
+ command: z.string().min(1),
82
+ required: z.boolean().optional().default(true),
83
+ description: z.string().optional(),
84
+ })
85
+ .strict();
86
+
87
+ const harnessSchema = z
88
+ .object({
89
+ commands: z.array(harnessCommandSchema).min(1),
90
+ })
91
+ .strict();
92
+
93
+ const uiSchema = z
94
+ .object({
95
+ kit: z.enum(UI_KITS),
96
+ })
97
+ .strict();
98
+
99
+ const docsSchema = z
100
+ .object({
101
+ language: z.enum(DOCS_LANGUAGES),
102
+ })
103
+ .strict();
104
+
105
+ const pathsSchema = z
106
+ .object({
107
+ features: z.string().min(1),
108
+ components: z.string().min(1),
109
+ services: z.string().min(1),
110
+ hooks: z.string().min(1),
111
+ pages: z.string().min(1),
112
+ ui: z.string().min(1),
113
+ })
114
+ .strict();
115
+
116
+ const toolingSchema = z
117
+ .object({
118
+ language: z.enum(TOOLING_LANGUAGES),
119
+ linter: z.enum(TOOLING_LINTERS),
120
+ formatter: z.enum(TOOLING_FORMATTERS),
121
+ })
122
+ .strict();
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Top-level manifest v2 - strict, no extra keys except $schema
126
+ // ---------------------------------------------------------------------------
127
+ export const manifestSchemaV2 = z
128
+ .object({
129
+ $schema: z.string().url().optional().default(SCHEMA_URL),
130
+ manifestVersion: z.literal(MANIFEST_VERSION),
131
+ framework: frameworkSchema,
132
+ styling: stylingSchema,
133
+ architecture: architectureSchema,
134
+ harness: harnessSchema.optional(),
135
+ ui: uiSchema,
136
+ docs: docsSchema,
137
+ paths: pathsSchema,
138
+ tooling: toolingSchema,
139
+ reactCompiler: z.boolean().optional(),
140
+ agentDocs: z.boolean().optional(),
141
+ })
142
+ .strict()
143
+ .superRefine((val, ctx) => {
144
+ const { framework, architecture, styling, ui } = val;
145
+
146
+ // framework.variant must match framework.name
147
+ if (framework.name === "react" && framework.variant !== "vite") {
148
+ ctx.addIssue({
149
+ code: "custom",
150
+ path: ["framework", "variant"],
151
+ message: `framework.variant must be "vite" when framework.name is "react" (got "${framework.variant}")`,
152
+ });
153
+ }
154
+ if (framework.name === "next" && framework.variant === "vite") {
155
+ ctx.addIssue({
156
+ code: "custom",
157
+ path: ["framework", "variant"],
158
+ message: `framework.variant must be "app-router" or "pages-router" when framework.name is "next" (got "vite")`,
159
+ });
160
+ }
161
+
162
+ // bundler only for Next.js
163
+ if (framework.bundler !== undefined && framework.name !== "next") {
164
+ ctx.addIssue({
165
+ code: "custom",
166
+ path: ["framework", "bundler"],
167
+ message: `framework.bundler is only valid when framework.name is "next"`,
168
+ });
169
+ }
170
+
171
+ // adapter only for Next.js (behavior in #22)
172
+ if (framework.adapter !== undefined && framework.name !== "next") {
173
+ ctx.addIssue({
174
+ code: "custom",
175
+ path: ["framework", "adapter"],
176
+ message: `framework.adapter is only valid when framework.name is "next"`,
177
+ });
178
+ }
179
+
180
+ // architecture scoping per framework (#34) — canonical `preset`, legacy `type` fallback
181
+ const archValue = architecture.preset ?? architecture.type;
182
+ const allowedByFramework =
183
+ framework.name === "react"
184
+ ? ["feature-based", "type-based", "none"]
185
+ : ["feature-based", "hybrid", "none"];
186
+ if (!allowedByFramework.includes(archValue)) {
187
+ ctx.addIssue({
188
+ code: "custom",
189
+ path: ["architecture", "type"],
190
+ message: `architecture.type "${archValue}" is not valid for framework "${framework.name}" (allowed: ${allowedByFramework.join(", ")})`,
191
+ });
192
+ }
193
+
194
+ // shadcn requires tailwind (#33)
195
+ if (ui.kit === "shadcn" && styling.engine !== "tailwind") {
196
+ ctx.addIssue({
197
+ code: "custom",
198
+ path: ["ui", "kit"],
199
+ message: `ui.kit "shadcn" requires styling.engine "tailwind" (got "${styling.engine}")`,
200
+ });
201
+ }
202
+ });
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // v1 detection - flat shape from create-lumen 1.x
206
+ // ---------------------------------------------------------------------------
207
+ const V1_FLAT_KEYS = ["css", "cssFramework", "architecture", "language", "router", "stateManagement", "iconLibrary", "apiClient", "testing"];
208
+
209
+ export function isV1Manifest(raw) {
210
+ if (!raw || typeof raw !== "object") return false;
211
+ // v1 had framework as string or missing manifestVersion
212
+ if (raw.manifestVersion === 1) return true;
213
+ if (typeof raw.framework === "string") return true;
214
+ if (raw.framework === undefined && V1_FLAT_KEYS.some((k) => k in raw)) return true;
215
+ // flat config without nested framework object but with css/architecture as string
216
+ if (typeof raw.architecture === "string" && typeof raw.framework !== "object") return true;
217
+ return false;
218
+ }
219
+
220
+ export function formatZodError(error) {
221
+ try {
222
+ const ve = fromZodError(error, {
223
+ prefix: "Manifest validation failed",
224
+ prefixSeparator: ": ",
225
+ issueSeparator: "; ",
226
+ });
227
+ return ve.message;
228
+ } catch {
229
+ return error.message;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Parse and validate a manifest v2 object.
235
+ * - Throws with migration-oriented message for v1 manifests (#14 AC)
236
+ * - Throws with user-friendly field path + expected shape for malformed v2
237
+ * @param {unknown} raw
238
+ * @returns {import("zod").infer<typeof manifestSchemaV2>}
239
+ */
240
+ export function parseManifest(raw) {
241
+ if (isV1Manifest(raw)) {
242
+ throw new Error(
243
+ `Manifest v1 detected (flat config). create-lumen v2 uses a nested manifest (manifestVersion: 2).\n` +
244
+ `Please migrate: delete the old config and re-run create-lumen, or see the migration guide (docs/migration/v1-to-v2.md).\n` +
245
+ `Raw keys: ${Object.keys(raw || {}).join(", ")}`
246
+ );
247
+ }
248
+
249
+ const result = manifestSchemaV2.safeParse(raw);
250
+ if (!result.success) {
251
+ const msg = formatZodError(result.error);
252
+ const err = new Error(msg);
253
+ err.cause = result.error;
254
+ throw err;
255
+ }
256
+ // Normalize: always expose architecture.preset + architecture.type (synced)
257
+ // + architecture.validation default, for backward/forward compat.
258
+ const data = result.data;
259
+ const preset = data.architecture.preset ?? data.architecture.type;
260
+ const type = data.architecture.type ?? data.architecture.preset;
261
+ data.architecture = {
262
+ preset,
263
+ type,
264
+ validation: data.architecture.validation ?? "strict",
265
+ };
266
+ return data;
267
+ }
268
+
269
+ // Re-export type helper for JSDoc (z.infer)
270
+ // /** @typedef {z.infer<typeof manifestSchemaV2>} ManifestV2 */
package/src/prompts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { cancel, confirm, isCancel, note, select, text } from "@clack/prompts";
1
+ import { confirm, isCancel, note, select } from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { configExists, loadConfig, saveConfig } from "@/config-cache.js";
4
4
 
@@ -12,7 +12,9 @@ function formatConfig(responses) {
12
12
  const arch =
13
13
  responses.architecture === "feature-based"
14
14
  ? "Feature-based"
15
- : "Component-based";
15
+ : responses.architecture === "type-based" || responses.architecture === "component-based"
16
+ ? "Type-based"
17
+ : responses.architecture;
16
18
  const css =
17
19
  responses.cssFramework === "none"
18
20
  ? "None"
@@ -32,6 +34,7 @@ function formatConfig(responses) {
32
34
  ? "Redux Toolkit"
33
35
  : "Zustand";
34
36
 
37
+ const docsLang = responses.docsLanguage === "es" ? "ES" : responses.docsLanguage === "en" ? "EN" : "—";
35
38
  return [
36
39
  `• ${chalk.bold("Architecture:")} ${chalk.green(arch)}`,
37
40
  `• ${chalk.bold("Language:")} ${chalk.green(lang)}`,
@@ -43,14 +46,15 @@ function formatConfig(responses) {
43
46
  `• ${chalk.bold("API Client:")} ${responses.apiClient === "axios" ? chalk.green("Axios") : responses.apiClient === "fetch" ? chalk.yellow("Fetch") : chalk.red("None")}`,
44
47
  `• ${chalk.bold("Linter:")} ${responses.linter === "oxlint" ? chalk.green("Oxlint") : responses.linter === "eslint" ? chalk.yellow("ESLint") : chalk.red("None")}`,
45
48
  `• ${chalk.bold("Formatter:")} ${responses.formatter === "oxfmt" ? chalk.green("Oxfmt") : responses.formatter === "prettier" ? chalk.yellow("Prettier") : chalk.red("None")}`,
49
+ `• ${chalk.bold("Docs:")} ${chalk.cyan(docsLang)}`,
46
50
  `• ${chalk.bold("Git:")} ${responses.gitInit ? chalk.green("Yes") : chalk.red("No")}`,
47
51
  `• ${chalk.bold("README:")} ${responses.readme ? chalk.green("Yes") : chalk.red("No")}`,
48
52
  ].join("\n");
49
53
  }
50
54
 
51
55
  export async function getUserInputs(projectName, { quickSetup = false } = {}) {
52
- let oldConfig = {};
53
- let useOldConfig = false;
56
+ let oldConfig;
57
+ let useOldConfig;
54
58
 
55
59
  // Non-interactive quick setup: apply defaults without prompting.
56
60
  if (quickSetup) {
@@ -66,6 +70,7 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
66
70
  apiClient: "none",
67
71
  linter: "eslint",
68
72
  formatter: "prettier",
73
+ docsLanguage: "en",
69
74
  gitInit: true,
70
75
  readme: true,
71
76
  };
@@ -101,6 +106,9 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
101
106
  if (oldConfig.formatter === undefined) {
102
107
  oldConfig.formatter = "none";
103
108
  }
109
+ if (oldConfig.docsLanguage === undefined) {
110
+ oldConfig.docsLanguage = "en";
111
+ }
104
112
  return { ...oldConfig, projectName };
105
113
  }
106
114
  }
@@ -126,6 +134,7 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
126
134
  apiClient: "none",
127
135
  linter: "eslint",
128
136
  formatter: "prettier",
137
+ docsLanguage: "en",
129
138
  gitInit: true,
130
139
  readme: true,
131
140
  };
@@ -143,8 +152,8 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
143
152
  hint: "Scales to large apps — code grouped by business domain",
144
153
  },
145
154
  {
146
- label: "Component-based",
147
- value: "component-based",
155
+ label: "Type-based",
156
+ value: "type-based",
148
157
  hint: "Small apps / component libraries — code grouped by UI type",
149
158
  },
150
159
  ],
@@ -261,6 +270,16 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
261
270
  if (isCancel(formatter)) onCancel();
262
271
  }
263
272
 
273
+ const docsLanguage = await select({
274
+ message: "Which docs language do you want?",
275
+ options: [
276
+ { label: "English", value: "en" },
277
+ { label: "Español", value: "es" },
278
+ ],
279
+ initialValue: "en",
280
+ });
281
+ if (isCancel(docsLanguage)) onCancel();
282
+
264
283
  const readme = await confirm({
265
284
  message: "Would you like to generate a README.md and LICENSE?",
266
285
  initialValue: true,
@@ -279,6 +298,7 @@ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
279
298
  apiClient,
280
299
  linter,
281
300
  formatter,
301
+ docsLanguage,
282
302
  gitInit,
283
303
  readme,
284
304
  };
@@ -0,0 +1,16 @@
1
+ import { dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { FlatCompat } from "@eslint/eslintrc";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const compat = new FlatCompat({
9
+ baseDirectory: __dirname,
10
+ });
11
+
12
+ const eslintConfig = [
13
+ ...compat.extends("next/core-web-vitals"),
14
+ ];
15
+
16
+ export default eslintConfig;
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };