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.
- package/README.md +71 -9
- package/bin/cli.js +18 -4
- package/package.json +15 -4
- package/schema/fixtures/index.js +62 -0
- package/schema/fixtures/v2/invalid/architecture-preset-type-mismatch.json +35 -0
- package/schema/fixtures/v2/invalid/harness-empty-commands.json +37 -0
- package/schema/fixtures/v2/invalid/harness-missing-command.json +41 -0
- package/schema/fixtures/v2/invalid/invalid-manifest-version.json +33 -0
- package/schema/fixtures/v2/invalid/invalid-validation-mode.json +34 -0
- package/schema/fixtures/v2/invalid/malformed-paths.json +33 -0
- package/schema/fixtures/v2/invalid/missing-required-fields.json +17 -0
- package/schema/fixtures/v2/invalid/next-type-based.json +33 -0
- package/schema/fixtures/v2/invalid/next-vite.json +33 -0
- package/schema/fixtures/v2/invalid/react-bundler.json +34 -0
- package/schema/fixtures/v2/invalid/react-hybrid.json +33 -0
- package/schema/fixtures/v2/invalid/react-variant-mismatch.json +33 -0
- package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +33 -0
- package/schema/fixtures/v2/invalid/v1-flat-config.json +11 -0
- package/schema/fixtures/v2/valid/full-kitchen-sink.json +35 -0
- package/schema/fixtures/v2/valid/harness-full.json +44 -0
- package/schema/fixtures/v2/valid/next-app-router-feature-based.json +35 -0
- package/schema/fixtures/v2/valid/next-app-router-hybrid.json +35 -0
- package/schema/fixtures/v2/valid/next-app-router-none.json +33 -0
- package/schema/fixtures/v2/valid/next-pages-router-feature-based.json +35 -0
- package/schema/fixtures/v2/valid/next-pages-router-hybrid.json +35 -0
- package/schema/fixtures/v2/valid/next-pages-router-none.json +33 -0
- package/schema/fixtures/v2/valid/react-vite-feature-based.json +33 -0
- package/schema/fixtures/v2/valid/react-vite-none.json +33 -0
- package/schema/fixtures/v2/valid/react-vite-type-based.json +33 -0
- package/schema/fixtures/v2/valid/validation-none.json +41 -0
- package/schema/fixtures/v2/valid/validation-relaxed.json +41 -0
- package/schema/lumen.config.v2.json +269 -0
- package/skills/create-lumen/SKILL.md +110 -0
- package/skills/create-lumen/references/manifest-v2.md +9 -0
- package/src/agent-context.js +114 -0
- package/src/cli-args.js +294 -0
- package/src/css.js +20 -9
- package/src/dependencies.js +12 -6
- package/src/engine/capabilities.js +218 -0
- package/src/engine/index.js +2 -0
- package/src/engine/providers/base-provider.js +219 -0
- package/src/injector.js +26 -13
- package/src/main.js +66 -10
- package/src/manifest/emit.js +166 -0
- package/src/manifest/index.js +11 -0
- package/src/manifest/paths.js +128 -0
- package/src/manifest/schema.js +270 -0
- package/src/prompts.js +26 -6
- package/templates/conditional/linting/eslint/next/eslint.config.mjs +16 -0
- package/templates/css/tailwind/postcss.config.mjs +5 -0
- /package/templates/architectures/{component-based → type-based}/main.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/main.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/App.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/App.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/Button.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/Button.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/index.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/common/index.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/components/form/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/config/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useApp.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/hooks/useLocalStorage.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/pages/Home/index.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/AppProvider.tsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/providers/appcontext.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/services/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/store/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/styles/.gitkeep +0 -0
- /package/templates/architectures/{component-based → type-based}/src/types/index.js +0 -0
- /package/templates/architectures/{component-based → type-based}/src/types/index.ts +0 -0
- /package/templates/architectures/{component-based → type-based}/src/utils/cn.jsx +0 -0
- /package/templates/architectures/{component-based → type-based}/src/utils/cn.ts +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.js +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/config/axios.config.ts +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.js +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/axios.client.ts +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.js +0 -0
- /package/templates/conditional/axios/{component-based → type-based}/src/services/user.service.ts +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.js +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/config/api.config.ts +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.js +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/api.client.ts +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.js +0 -0
- /package/templates/conditional/fetch/{component-based → type-based}/src/services/user.service.ts +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/huge/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/bootstrap/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.jsx +0 -0
- /package/templates/conditional/icons/lucide/{component-based → type-based}/tailwind/src/pages/Home/Home.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/bootstrap/src/layouts/MainLayout.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/App.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/App.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/AuthGuard.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/guards/GuestGuard.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/index.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/src/router/index.tsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.jsx +0 -0
- /package/templates/conditional/router/{component-based → type-based}/tailwind/src/layouts/MainLayout.tsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.jsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/pages/Home/counterSlice.ts +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.jsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/providers/StoreProvider.tsx +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.js +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/hooks.ts +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.js +0 -0
- /package/templates/conditional/state/redux/{component-based → type-based}/src/store/index.ts +0 -0
- /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.jsx +0 -0
- /package/templates/conditional/state/zustand/{component-based → type-based}/src/store/useCounterStore.ts +0 -0
- /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
- /package/templates/conditional/testing/jest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
- /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.jsx +0 -0
- /package/templates/conditional/testing/vitest/{component-based → type-based}/src/__tests__/App.test.tsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.jsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/components/common/Button.tsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/css/component-styles/bootstrap/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.jsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/components/common/Button.tsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.jsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/layouts/MainLayout.tsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.jsx +0 -0
- /package/templates/css/component-styles/tailwind/{component-based → type-based}/src/pages/Home/Home.tsx +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
FRAMEWORK_CAPABILITIES,
|
|
4
|
+
getFrameworkKey,
|
|
5
|
+
} from "../capabilities.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Abstract BaseProvider seam (ADR 0001 / create-lumen#25).
|
|
9
|
+
* Encapsulates framework capabilities, base template file plans,
|
|
10
|
+
* default package sets, and post-write lifecycle hooks.
|
|
11
|
+
*/
|
|
12
|
+
export class BaseProvider {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} config
|
|
15
|
+
* @param {string} config.name - framework name (e.g. "react", "next")
|
|
16
|
+
* @param {string} config.variant - framework variant (e.g. "vite", "app-router", "pages-router")
|
|
17
|
+
* @param {string[]} [config.capabilities]
|
|
18
|
+
*/
|
|
19
|
+
constructor({ name, variant, capabilities = [] }) {
|
|
20
|
+
if (new.target === BaseProvider) {
|
|
21
|
+
throw new TypeError("Cannot instantiate abstract BaseProvider directly");
|
|
22
|
+
}
|
|
23
|
+
this.name = name;
|
|
24
|
+
this.variant = variant;
|
|
25
|
+
this.capabilities = new Set(capabilities);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return array of declared capabilities.
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
getCapabilities() {
|
|
33
|
+
return Array.from(this.capabilities);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if provider has a capability.
|
|
38
|
+
* @param {string} capability
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
hasCapability(capability) {
|
|
42
|
+
return this.capabilities.has(capability);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return relative file path pattern to the base template directory.
|
|
47
|
+
* Pattern: templates/bases/<provider>/<variant>/<lang>/
|
|
48
|
+
* @param {{ lang?: string }} [opts]
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
getFilePlan({ lang = "ts" } = {}) {
|
|
52
|
+
return path.posix.join("templates", "bases", this.name, this.variant, lang);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return base package dependencies and devDependencies.
|
|
57
|
+
* @param {object} [_options]
|
|
58
|
+
* @returns {{ dependencies: Record<string, string>, devDependencies: Record<string, string> }}
|
|
59
|
+
*/
|
|
60
|
+
getPackageSet(_options = {}) {
|
|
61
|
+
return {
|
|
62
|
+
dependencies: {},
|
|
63
|
+
devDependencies: {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Return post-write hooks to execute after files are written.
|
|
69
|
+
* @param {object} [_options]
|
|
70
|
+
* @returns {Array<{ name: string, run: (context: object) => Promise<void> | void }>}
|
|
71
|
+
*/
|
|
72
|
+
getPostWriteHooks(_options = {}) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* React + Vite concrete provider.
|
|
79
|
+
*/
|
|
80
|
+
export class ViteReactProvider extends BaseProvider {
|
|
81
|
+
constructor() {
|
|
82
|
+
const desc = FRAMEWORK_CAPABILITIES["react:vite"];
|
|
83
|
+
super({
|
|
84
|
+
name: "react",
|
|
85
|
+
variant: "vite",
|
|
86
|
+
capabilities: desc.capabilities,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getPackageSet({ options = {} } = {}) {
|
|
91
|
+
const isTs = options.language === "ts";
|
|
92
|
+
const deps = {
|
|
93
|
+
react: "^19.0.0",
|
|
94
|
+
"react-dom": "^19.0.0",
|
|
95
|
+
};
|
|
96
|
+
const devDeps = {
|
|
97
|
+
vite: "^6.0.0",
|
|
98
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
99
|
+
};
|
|
100
|
+
if (isTs) {
|
|
101
|
+
devDeps.typescript = "^5.7.0";
|
|
102
|
+
devDeps["@types/react"] = "^19.0.0";
|
|
103
|
+
devDeps["@types/react-dom"] = "^19.0.0";
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
dependencies: deps,
|
|
107
|
+
devDependencies: devDeps,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Next.js App Router concrete provider.
|
|
114
|
+
*/
|
|
115
|
+
export class NextAppRouterProvider extends BaseProvider {
|
|
116
|
+
constructor() {
|
|
117
|
+
const desc = FRAMEWORK_CAPABILITIES["next:app-router"];
|
|
118
|
+
super({
|
|
119
|
+
name: "next",
|
|
120
|
+
variant: "app-router",
|
|
121
|
+
capabilities: desc.capabilities,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getPackageSet({ options = {} } = {}) {
|
|
126
|
+
const isTs = options.language === "ts";
|
|
127
|
+
const deps = {
|
|
128
|
+
next: "^15.0.0",
|
|
129
|
+
react: "^19.0.0",
|
|
130
|
+
"react-dom": "^19.0.0",
|
|
131
|
+
};
|
|
132
|
+
const devDeps = {};
|
|
133
|
+
if (isTs) {
|
|
134
|
+
devDeps.typescript = "^5.7.0";
|
|
135
|
+
devDeps["@types/react"] = "^19.0.0";
|
|
136
|
+
devDeps["@types/react-dom"] = "^19.0.0";
|
|
137
|
+
devDeps["@types/node"] = "^20.0.0";
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
dependencies: deps,
|
|
141
|
+
devDependencies: devDeps,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Next.js Pages Router concrete provider.
|
|
148
|
+
*/
|
|
149
|
+
export class NextPagesRouterProvider extends BaseProvider {
|
|
150
|
+
constructor() {
|
|
151
|
+
const desc = FRAMEWORK_CAPABILITIES["next:pages-router"];
|
|
152
|
+
super({
|
|
153
|
+
name: "next",
|
|
154
|
+
variant: "pages-router",
|
|
155
|
+
capabilities: desc.capabilities,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getPackageSet({ options = {} } = {}) {
|
|
160
|
+
const isTs = options.language === "ts";
|
|
161
|
+
const deps = {
|
|
162
|
+
next: "^15.0.0",
|
|
163
|
+
react: "^19.0.0",
|
|
164
|
+
"react-dom": "^19.0.0",
|
|
165
|
+
};
|
|
166
|
+
const devDeps = {};
|
|
167
|
+
if (isTs) {
|
|
168
|
+
devDeps.typescript = "^5.7.0";
|
|
169
|
+
devDeps["@types/react"] = "^19.0.0";
|
|
170
|
+
devDeps["@types/react-dom"] = "^19.0.0";
|
|
171
|
+
devDeps["@types/node"] = "^20.0.0";
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
dependencies: deps,
|
|
175
|
+
devDependencies: devDeps,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Provider Registry
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
const providerRegistry = new Map();
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Register a BaseProvider instance in the registry.
|
|
187
|
+
* @param {BaseProvider} provider
|
|
188
|
+
*/
|
|
189
|
+
export function registerProvider(provider) {
|
|
190
|
+
if (!(provider instanceof BaseProvider)) {
|
|
191
|
+
throw new TypeError("provider must be an instance of BaseProvider");
|
|
192
|
+
}
|
|
193
|
+
const key = getFrameworkKey(provider.name, provider.variant);
|
|
194
|
+
providerRegistry.set(key, provider);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Retrieve registered provider by framework name and variant.
|
|
199
|
+
* @param {string} name
|
|
200
|
+
* @param {string} variant
|
|
201
|
+
* @returns {BaseProvider | null}
|
|
202
|
+
*/
|
|
203
|
+
export function getProvider(name, variant) {
|
|
204
|
+
const key = getFrameworkKey(name, variant);
|
|
205
|
+
return providerRegistry.get(key) || null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* List all registered providers.
|
|
210
|
+
* @returns {BaseProvider[]}
|
|
211
|
+
*/
|
|
212
|
+
export function listProviders() {
|
|
213
|
+
return Array.from(providerRegistry.values());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Auto-register default providers
|
|
217
|
+
registerProvider(new ViteReactProvider());
|
|
218
|
+
registerProvider(new NextAppRouterProvider());
|
|
219
|
+
registerProvider(new NextPagesRouterProvider());
|
package/src/injector.js
CHANGED
|
@@ -118,12 +118,12 @@ export async function injectFormatter(projectPath, templatesDir, responses) {
|
|
|
118
118
|
|
|
119
119
|
// Wire eslint-config-prettier only for the eslint + prettier combination
|
|
120
120
|
if (responses.linter === "eslint" && responses.formatter === "prettier") {
|
|
121
|
-
await wireEslintPrettier(projectPath, responses.language);
|
|
121
|
+
await wireEslintPrettier(projectPath, responses.language, responses.framework);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async function wireEslintPrettier(projectPath, language) {
|
|
126
|
-
const ext = language === "ts" ? "ts" : "js";
|
|
125
|
+
async function wireEslintPrettier(projectPath, language, framework) {
|
|
126
|
+
const ext = framework === "next" ? "mjs" : language === "ts" ? "ts" : "js";
|
|
127
127
|
const eslintConfigPath = path.join(projectPath, `eslint.config.${ext}`);
|
|
128
128
|
try {
|
|
129
129
|
let content = await fsp.readFile(eslintConfigPath, "utf8");
|
|
@@ -158,6 +158,11 @@ async function wireEslintPrettier(projectPath, language) {
|
|
|
158
158
|
/(export default tseslint\.config\([\s\S]*?)(\n\s*\);)/,
|
|
159
159
|
pushLast
|
|
160
160
|
);
|
|
161
|
+
} else if (result.includes("const eslintConfig = [")) {
|
|
162
|
+
result = result.replace(
|
|
163
|
+
/(const eslintConfig = \[[\s\S]*?)(\n\s*\];)/,
|
|
164
|
+
pushLast
|
|
165
|
+
);
|
|
161
166
|
} else {
|
|
162
167
|
result = result.replace(
|
|
163
168
|
/(export default \[[\s\S]*?)(\n\s*\];)/,
|
|
@@ -238,9 +243,9 @@ export async function injectConditionals(projectPath, templatesDir, responses, a
|
|
|
238
243
|
|
|
239
244
|
// 6. Linter
|
|
240
245
|
if (responses.linter === "eslint") {
|
|
241
|
-
await injectLinter(projectPath, templatesDir, "eslint", language);
|
|
246
|
+
await injectLinter(projectPath, templatesDir, "eslint", language, responses.framework);
|
|
242
247
|
} else if (responses.linter === "oxlint") {
|
|
243
|
-
await injectLinter(projectPath, templatesDir, "oxlint", language);
|
|
248
|
+
await injectLinter(projectPath, templatesDir, "oxlint", language, responses.framework);
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
// 7. Create feature script (feature-based only)
|
|
@@ -254,7 +259,7 @@ export async function injectConditionals(projectPath, templatesDir, responses, a
|
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
// Remove .gitkeep placeholders from src/ directories that now contain real
|
|
257
|
-
// files (e.g.
|
|
262
|
+
// files (e.g. type-based `services/.gitkeep` after the axios/fetch client
|
|
258
263
|
// overlay lands). A folder with nothing but its placeholder keeps it.
|
|
259
264
|
async function pruneRedundantGitkeeps(projectPath) {
|
|
260
265
|
const walk = async (dir) => {
|
|
@@ -378,16 +383,24 @@ async function injectTesting(projectPath, templatesDir, framework, language, arc
|
|
|
378
383
|
} catch {}
|
|
379
384
|
}
|
|
380
385
|
|
|
381
|
-
async function injectLinter(projectPath, templatesDir, linter, language) {
|
|
386
|
+
export async function injectLinter(projectPath, templatesDir, linter, language, framework) {
|
|
382
387
|
const lintingDir = path.join(templatesDir, "conditional", "linting", linter);
|
|
383
388
|
|
|
384
389
|
if (linter === "eslint") {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
390
|
+
if (framework === "next") {
|
|
391
|
+
const configSrc = path.join(lintingDir, "next", "eslint.config.mjs");
|
|
392
|
+
try {
|
|
393
|
+
await fsp.access(configSrc);
|
|
394
|
+
await fsp.copyFile(configSrc, path.join(projectPath, "eslint.config.mjs"));
|
|
395
|
+
} catch {}
|
|
396
|
+
} else {
|
|
397
|
+
const configExt = language === "ts" ? "ts" : "js";
|
|
398
|
+
const configSrc = path.join(lintingDir, `eslint.config.${configExt}`);
|
|
399
|
+
try {
|
|
400
|
+
await fsp.access(configSrc);
|
|
401
|
+
await fsp.copyFile(configSrc, path.join(projectPath, `eslint.config.${configExt}`));
|
|
402
|
+
} catch {}
|
|
403
|
+
}
|
|
391
404
|
} else if (linter === "oxlint") {
|
|
392
405
|
const configSrc = path.join(lintingDir, "oxlintrc.json");
|
|
393
406
|
try {
|
package/src/main.js
CHANGED
|
@@ -16,21 +16,25 @@ import { runBaseInstall, runGitInit, runViteCreate } from "@/scaffold.js";
|
|
|
16
16
|
import { installAllDeps } from "@/dependencies.js";
|
|
17
17
|
import { generateReadme } from "@/readme.js";
|
|
18
18
|
import { runProjectFormat } from "@/format.js";
|
|
19
|
+
import { emitManifest } from "@/manifest/emit.js";
|
|
20
|
+
import { emitAgentContext } from "@/agent-context.js";
|
|
21
|
+
|
|
22
|
+
import { parseCliArgs, loadManifestSource, manifestToResponses, PRESETS } from "@/cli-args.js";
|
|
19
23
|
|
|
20
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
25
|
const __dirname = path.dirname(__filename);
|
|
22
26
|
const TEMPLATES_DIR = path.join(__dirname, "../templates");
|
|
23
27
|
const CURRENT_DIR = process.cwd();
|
|
24
28
|
|
|
25
|
-
export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd = CURRENT_DIR } = {}) {
|
|
26
|
-
const
|
|
27
|
-
const cliArg = nameArg ||
|
|
29
|
+
export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd = CURRENT_DIR, rawArgs } = {}) {
|
|
30
|
+
const cli = parseCliArgs(rawArgs || process.argv.slice(2));
|
|
31
|
+
const cliArg = nameArg || cli.projectName || null;
|
|
28
32
|
|
|
29
33
|
if (cliArg && cliArg.trim()) {
|
|
30
34
|
return cliArg.trim();
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
if (quickSetup) {
|
|
37
|
+
if (quickSetup || cli.quickSetup || cli.manifest || cli.template) {
|
|
34
38
|
const defaultName = path.basename(cwd).trim() || "my-app";
|
|
35
39
|
return defaultName === "." ? "my-app" : defaultName;
|
|
36
40
|
}
|
|
@@ -39,11 +43,22 @@ export function resolveProjectName({ quickSetup = false, projectName: nameArg, c
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export async function main(options = {}) {
|
|
42
|
-
const {
|
|
46
|
+
const {
|
|
47
|
+
quickSetup = false,
|
|
48
|
+
projectName: nameArg,
|
|
49
|
+
manifest: manifestSource,
|
|
50
|
+
template: templateName,
|
|
51
|
+
} = options;
|
|
43
52
|
const pkg = getPkgManager();
|
|
44
53
|
|
|
54
|
+
const isNonInteractive = Boolean(quickSetup || manifestSource || templateName);
|
|
55
|
+
|
|
45
56
|
// 1. Project name
|
|
46
|
-
let projectName = resolveProjectName({
|
|
57
|
+
let projectName = resolveProjectName({
|
|
58
|
+
quickSetup: isNonInteractive,
|
|
59
|
+
projectName: nameArg,
|
|
60
|
+
cwd: CURRENT_DIR,
|
|
61
|
+
});
|
|
47
62
|
|
|
48
63
|
if (projectName) {
|
|
49
64
|
log.step(chalk.gray(`Project name: ${chalk.bold(projectName)}`));
|
|
@@ -61,8 +76,25 @@ export async function main(options = {}) {
|
|
|
61
76
|
// 2. Handle existing folder
|
|
62
77
|
await confirmEmptyFolder(projectName);
|
|
63
78
|
|
|
64
|
-
// 3. Collect user preferences
|
|
65
|
-
|
|
79
|
+
// 3. Collect user preferences (interactive or non-interactive)
|
|
80
|
+
let responses;
|
|
81
|
+
if (manifestSource) {
|
|
82
|
+
const manifestObj = loadManifestSource(manifestSource, CURRENT_DIR);
|
|
83
|
+
responses = manifestToResponses(manifestObj, projectName);
|
|
84
|
+
} else if (templateName) {
|
|
85
|
+
const preset = PRESETS[templateName];
|
|
86
|
+
if (!preset) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Unknown template preset "${templateName}". Available presets: ${Object.keys(PRESETS).join(", ")}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
responses = {
|
|
92
|
+
projectName,
|
|
93
|
+
...preset,
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
responses = await getUserInputs(projectName, { quickSetup });
|
|
97
|
+
}
|
|
66
98
|
|
|
67
99
|
const projectPath = path.resolve(CURRENT_DIR, projectName);
|
|
68
100
|
|
|
@@ -104,7 +136,7 @@ export async function main(options = {}) {
|
|
|
104
136
|
// 7. Inject architecture templates
|
|
105
137
|
const archSpin = spinner();
|
|
106
138
|
archSpin.start(
|
|
107
|
-
`Setting up ${responses.architecture === "feature-based" ? "feature-based" : "
|
|
139
|
+
`Setting up ${responses.architecture === "feature-based" ? "feature-based" : "type-based"} architecture...`
|
|
108
140
|
);
|
|
109
141
|
try {
|
|
110
142
|
await injectArchitecture(
|
|
@@ -205,6 +237,30 @@ export async function main(options = {}) {
|
|
|
205
237
|
// 12.5 Copy .env.example into the project
|
|
206
238
|
await copyEnvExample(projectPath, TEMPLATES_DIR);
|
|
207
239
|
|
|
240
|
+
// 12.6 Emit lumen.config.json (manifest v2) — byte-deterministic, Zod-validated
|
|
241
|
+
const manifestSpin = spinner();
|
|
242
|
+
manifestSpin.start("Writing lumen.config.json...");
|
|
243
|
+
try {
|
|
244
|
+
await emitManifest(projectPath, responses);
|
|
245
|
+
manifestSpin.stop(chalk.green("lumen.config.json written."));
|
|
246
|
+
} catch (err) {
|
|
247
|
+
manifestSpin.stop(chalk.red("Failed to write lumen.config.json."));
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 12.7 Emit .lumen agent context dir (only when agentDocs is enabled)
|
|
252
|
+
if (responses.agentDocs === true || responses._manifestSource?.agentDocs === true) {
|
|
253
|
+
const agentSpin = spinner();
|
|
254
|
+
agentSpin.start("Writing .lumen agent context...");
|
|
255
|
+
try {
|
|
256
|
+
await emitAgentContext(projectPath, responses._manifestSource ?? responses);
|
|
257
|
+
agentSpin.stop(chalk.green(".lumen agent context written."));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
agentSpin.stop(chalk.red("Failed to write .lumen agent context."));
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
208
264
|
// 13. Generate README.md + LICENSE (if requested)
|
|
209
265
|
if (responses.readme) {
|
|
210
266
|
const readmeSpin = spinner();
|
|
@@ -234,7 +290,7 @@ export async function main(options = {}) {
|
|
|
234
290
|
log.step(chalk.green("\nProject setup complete!"));
|
|
235
291
|
log.message(
|
|
236
292
|
chalk.gray(
|
|
237
|
-
` Architecture: ${responses.architecture === "feature-based" ? "Feature-based" : "
|
|
293
|
+
` Architecture: ${responses.architecture === "feature-based" ? "Feature-based" : "Type-based"}`
|
|
238
294
|
)
|
|
239
295
|
);
|
|
240
296
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { promises as fsp } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { parseManifest, SCHEMA_URL, MANIFEST_VERSION, HARNESS_DEFAULT_COMMANDS } from "./schema.js";
|
|
4
|
+
import { resolvePaths } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
const LINT_COMMANDS = {
|
|
7
|
+
eslint: "eslint .",
|
|
8
|
+
oxlint: "oxlint .",
|
|
9
|
+
biome: "biome check .",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const FORMAT_COMMANDS = {
|
|
13
|
+
prettier: "prettier --check .",
|
|
14
|
+
oxfmt: "oxfmt --check .",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build default harness commands from tooling choices.
|
|
19
|
+
* Skips lint/format when tooling is "none"; skips types when language is not "ts".
|
|
20
|
+
* Test + build are always included so commands.length >= 1.
|
|
21
|
+
* @param {{ linter?: string, formatter?: string, language?: string }} tooling
|
|
22
|
+
* @returns {Array<{ name: string, command: string, required: boolean }>}
|
|
23
|
+
*/
|
|
24
|
+
export function defaultHarnessCommands(tooling = {}) {
|
|
25
|
+
const { linter = "none", formatter = "none", language = "ts" } = tooling;
|
|
26
|
+
const commands = [];
|
|
27
|
+
if (linter && linter !== "none" && LINT_COMMANDS[linter]) {
|
|
28
|
+
commands.push({ name: "lint", command: LINT_COMMANDS[linter], required: true });
|
|
29
|
+
}
|
|
30
|
+
if (formatter && formatter !== "none" && FORMAT_COMMANDS[formatter]) {
|
|
31
|
+
commands.push({ name: "format", command: FORMAT_COMMANDS[formatter], required: true });
|
|
32
|
+
}
|
|
33
|
+
if (language === "ts") {
|
|
34
|
+
commands.push({ name: "types", command: "tsc -b", required: true });
|
|
35
|
+
}
|
|
36
|
+
commands.push({ name: "test", command: "vitest run", required: true });
|
|
37
|
+
commands.push({ name: "build", command: "npm run build", required: true });
|
|
38
|
+
return commands.length > 0 ? commands : [...HARNESS_DEFAULT_COMMANDS];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build an ordered manifest v2 object from scaffolder responses.
|
|
43
|
+
* Stable key ordering for byte-determinism.
|
|
44
|
+
* @param {object} responses - result from getUserInputs (includes docsLanguage, cssFramework, etc.)
|
|
45
|
+
* @param {object} [opts]
|
|
46
|
+
* @param {string} [opts.frameworkName] - default "react"
|
|
47
|
+
* @param {string} [opts.frameworkVariant] - default "vite" or derived from framework
|
|
48
|
+
* @returns {object} ordered manifest object ready to JSON.stringify
|
|
49
|
+
*/
|
|
50
|
+
export function buildManifest(responses, opts = {}) {
|
|
51
|
+
if (responses.rawManifest) {
|
|
52
|
+
return parseManifest(responses.rawManifest);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const frameworkName = opts.frameworkName || responses.frameworkName || "react";
|
|
56
|
+
const frameworkVariant =
|
|
57
|
+
opts.frameworkVariant ||
|
|
58
|
+
responses.frameworkVariant ||
|
|
59
|
+
(frameworkName === "next" ? "app-router" : "vite");
|
|
60
|
+
|
|
61
|
+
const framework = {
|
|
62
|
+
name: frameworkName,
|
|
63
|
+
variant: frameworkVariant,
|
|
64
|
+
};
|
|
65
|
+
// Include optional axes only if present in responses (future overlays)
|
|
66
|
+
if (responses.bundler) framework.bundler = responses.bundler;
|
|
67
|
+
if (responses.adapter) framework.adapter = responses.adapter;
|
|
68
|
+
|
|
69
|
+
const styling = {
|
|
70
|
+
engine: responses.cssFramework || "tailwind",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const rawArch =
|
|
74
|
+
responses.architecturePreset || responses.architecture || "feature-based";
|
|
75
|
+
const normalizedArch = rawArch === "component-based" ? "type-based" : rawArch;
|
|
76
|
+
const architecture = {
|
|
77
|
+
preset: normalizedArch,
|
|
78
|
+
type: normalizedArch,
|
|
79
|
+
validation:
|
|
80
|
+
responses.architectureValidation || responses.validation || "strict",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const ui = {
|
|
84
|
+
kit: responses.uiKit || responses.ui || "none",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const docs = {
|
|
88
|
+
language: responses.docsLanguage || "en",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const paths = resolvePaths({
|
|
92
|
+
architecture: architecture.type,
|
|
93
|
+
framework,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const tooling = {
|
|
97
|
+
language: responses.language || "ts",
|
|
98
|
+
linter: responses.linter || "none",
|
|
99
|
+
formatter: responses.formatter || "none",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const harness = responses.harness ?? {
|
|
103
|
+
commands: defaultHarnessCommands(tooling),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const manifest = {
|
|
107
|
+
$schema: SCHEMA_URL,
|
|
108
|
+
manifestVersion: MANIFEST_VERSION,
|
|
109
|
+
framework,
|
|
110
|
+
styling,
|
|
111
|
+
architecture,
|
|
112
|
+
harness,
|
|
113
|
+
ui,
|
|
114
|
+
docs,
|
|
115
|
+
paths,
|
|
116
|
+
tooling,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Optional cross-cutting flags — only include if explicitly set (keeps minimal manifest byte-stable)
|
|
120
|
+
if (typeof responses.reactCompiler === "boolean") manifest.reactCompiler = responses.reactCompiler;
|
|
121
|
+
if (typeof responses.agentDocs === "boolean") manifest.agentDocs = responses.agentDocs;
|
|
122
|
+
|
|
123
|
+
// Enforce stable top-level key order for determinism (like formatter pass)
|
|
124
|
+
const ordered = {};
|
|
125
|
+
const topOrder = [
|
|
126
|
+
"$schema",
|
|
127
|
+
"manifestVersion",
|
|
128
|
+
"framework",
|
|
129
|
+
"styling",
|
|
130
|
+
"architecture",
|
|
131
|
+
"harness",
|
|
132
|
+
"ui",
|
|
133
|
+
"docs",
|
|
134
|
+
"paths",
|
|
135
|
+
"tooling",
|
|
136
|
+
"reactCompiler",
|
|
137
|
+
"agentDocs",
|
|
138
|
+
];
|
|
139
|
+
for (const k of topOrder) {
|
|
140
|
+
if (k in manifest) ordered[k] = manifest[k];
|
|
141
|
+
}
|
|
142
|
+
// Include any extra keys not in order (should be none, but for forward compat)
|
|
143
|
+
for (const k of Object.keys(manifest)) {
|
|
144
|
+
if (!(k in ordered)) ordered[k] = manifest[k];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Validate via Zod before returning (throws with user-friendly message on invalid)
|
|
148
|
+
// Use parse to apply defaults ($schema) and ensure strictness
|
|
149
|
+
return parseManifest(ordered);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Emit lumen.config.json to projectPath.
|
|
154
|
+
* Validates via Zod, writes with 2-space indent + trailing newline.
|
|
155
|
+
* @param {string} projectPath - absolute path to scaffolded project
|
|
156
|
+
* @param {object} responses
|
|
157
|
+
* @param {object} [opts] - same as buildManifest opts
|
|
158
|
+
* @returns {Promise<string>} absolute path to written file
|
|
159
|
+
*/
|
|
160
|
+
export async function emitManifest(projectPath, responses, opts = {}) {
|
|
161
|
+
const manifest = responses._manifestSource || buildManifest(responses, opts);
|
|
162
|
+
const outPath = path.join(projectPath, "lumen.config.json");
|
|
163
|
+
const content = JSON.stringify(manifest, null, 2) + "\n";
|
|
164
|
+
await fsp.writeFile(outPath, content, "utf8");
|
|
165
|
+
return outPath;
|
|
166
|
+
}
|