create-lumen 2.0.0-alpha.0 → 2.0.0-alpha.2

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 (74) hide show
  1. package/README.md +35 -12
  2. package/bin/cli.js +1 -0
  3. package/package.json +3 -2
  4. package/schema/fixtures/v2/invalid/architecture-preset-type-mismatch.json +35 -0
  5. package/schema/fixtures/v2/invalid/harness-empty-commands.json +37 -0
  6. package/schema/fixtures/v2/invalid/harness-missing-command.json +41 -0
  7. package/schema/fixtures/v2/invalid/invalid-validation-mode.json +34 -0
  8. package/schema/fixtures/v2/invalid/shadcn-without-tailwind.json +1 -1
  9. package/schema/fixtures/v2/valid/harness-full.json +44 -0
  10. package/schema/fixtures/v2/valid/react-vite-type-based.json +1 -1
  11. package/schema/fixtures/v2/valid/validation-none.json +41 -0
  12. package/schema/fixtures/v2/valid/validation-relaxed.json +41 -0
  13. package/schema/lumen.config.v2.json +54 -2
  14. package/skills/create-lumen/SKILL.md +110 -0
  15. package/skills/create-lumen/references/manifest-v2.md +9 -0
  16. package/src/agent-context.js +114 -0
  17. package/src/cli-args.js +65 -3
  18. package/src/configure.js +4 -0
  19. package/src/css.js +14 -30
  20. package/src/dependencies.js +28 -18
  21. package/src/engine/capabilities.js +174 -4
  22. package/src/engine/composition.js +644 -0
  23. package/src/engine/index.js +1 -0
  24. package/src/injector.js +294 -446
  25. package/src/main.js +20 -7
  26. package/src/manifest/emit.js +49 -3
  27. package/src/manifest/schema.js +65 -7
  28. package/src/prompts.js +459 -247
  29. package/src/readme.js +23 -2
  30. package/src/scaffold.js +17 -0
  31. package/templates/architectures/hybrid/main.jsx +10 -0
  32. package/templates/architectures/hybrid/main.tsx +10 -0
  33. package/templates/architectures/hybrid/src/App.jsx +20 -0
  34. package/templates/architectures/hybrid/src/App.tsx +20 -0
  35. package/templates/architectures/hybrid/src/shared/components/ui/Button.jsx +31 -0
  36. package/templates/architectures/hybrid/src/shared/components/ui/Button.tsx +36 -0
  37. package/templates/architectures/hybrid/src/shared/components/ui/index.js +1 -0
  38. package/templates/architectures/hybrid/src/shared/components/ui/index.ts +1 -0
  39. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.jsx +28 -0
  40. package/templates/architectures/hybrid/src/shared/hooks/useLocalStorage.ts +28 -0
  41. package/templates/architectures/hybrid/src/shared/layouts/.gitkeep +0 -0
  42. package/templates/architectures/hybrid/src/shared/stores/.gitkeep +0 -0
  43. package/templates/architectures/hybrid/src/shared/styles/.gitkeep +0 -0
  44. package/templates/architectures/hybrid/src/shared/types/index.js +20 -0
  45. package/templates/architectures/hybrid/src/shared/types/index.ts +16 -0
  46. package/templates/architectures/hybrid/src/shared/utils/cn.jsx +3 -0
  47. package/templates/architectures/hybrid/src/shared/utils/cn.ts +5 -0
  48. package/templates/architectures/none/main.jsx +10 -0
  49. package/templates/architectures/none/main.tsx +10 -0
  50. package/templates/architectures/none/src/App.jsx +15 -0
  51. package/templates/architectures/none/src/App.tsx +15 -0
  52. package/templates/architectures/none/src/styles/.gitkeep +1 -0
  53. package/templates/architectures/none/src/types/index.js +20 -0
  54. package/templates/architectures/none/src/types/index.ts +16 -0
  55. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.jsx +11 -0
  56. package/templates/conditional/testing/jest/hybrid/src/__tests__/App.test.tsx +11 -0
  57. package/templates/conditional/testing/jest/none/src/__tests__/App.test.jsx +11 -0
  58. package/templates/conditional/testing/jest/none/src/__tests__/App.test.tsx +11 -0
  59. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.jsx +10 -0
  60. package/templates/conditional/testing/vitest/hybrid/src/__tests__/App.test.tsx +10 -0
  61. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.jsx +10 -0
  62. package/templates/conditional/testing/vitest/none/src/__tests__/App.test.tsx +10 -0
  63. package/templates/css/tailwind/src/globals.css +10 -0
  64. package/templates/css/tailwind/src/themes.css +86 -4
  65. package/templates/ui/shadcn/lib/utils.js +6 -0
  66. package/templates/ui/shadcn/lib/utils.ts +6 -0
  67. package/templates/ui/shadcn/primitives/Button.jsx +48 -0
  68. package/templates/ui/shadcn/primitives/Button.tsx +59 -0
  69. package/templates/ui/shadcn/primitives/Card.jsx +55 -0
  70. package/templates/ui/shadcn/primitives/Card.tsx +73 -0
  71. package/templates/ui/shadcn/primitives/index.js +9 -0
  72. package/templates/ui/shadcn/primitives/index.ts +9 -0
  73. package/templates/css/bootstrap/src/themes.css +0 -14
  74. /package/templates/{css/bootstrap/src/globals.css → architectures/hybrid/src/features/.gitkeep} +0 -0
package/src/main.js CHANGED
@@ -17,6 +17,7 @@ import { installAllDeps } from "@/dependencies.js";
17
17
  import { generateReadme } from "@/readme.js";
18
18
  import { runProjectFormat } from "@/format.js";
19
19
  import { emitManifest } from "@/manifest/emit.js";
20
+ import { emitAgentContext } from "@/agent-context.js";
20
21
 
21
22
  import { parseCliArgs, loadManifestSource, manifestToResponses, PRESETS } from "@/cli-args.js";
22
23
 
@@ -47,6 +48,7 @@ export async function main(options = {}) {
47
48
  projectName: nameArg,
48
49
  manifest: manifestSource,
49
50
  template: templateName,
51
+ arch: archArg,
50
52
  } = options;
51
53
  const pkg = getPkgManager();
52
54
 
@@ -92,7 +94,7 @@ export async function main(options = {}) {
92
94
  ...preset,
93
95
  };
94
96
  } else {
95
- responses = await getUserInputs(projectName, { quickSetup });
97
+ responses = await getUserInputs(projectName, { quickSetup, initialArch: archArg });
96
98
  }
97
99
 
98
100
  const projectPath = path.resolve(CURRENT_DIR, projectName);
@@ -135,7 +137,7 @@ export async function main(options = {}) {
135
137
  // 7. Inject architecture templates
136
138
  const archSpin = spinner();
137
139
  archSpin.start(
138
- `Setting up ${responses.architecture === "feature-based" ? "feature-based" : "type-based"} architecture...`
140
+ `Setting up ${responses.architecture} architecture...`
139
141
  );
140
142
  try {
141
143
  await injectArchitecture(
@@ -193,11 +195,9 @@ export async function main(options = {}) {
193
195
  // 10. CSS framework setup
194
196
  const cssSpin = spinner();
195
197
  const cssLabel =
196
- responses.cssFramework === "none"
197
- ? "CSS reset"
198
- : responses.cssFramework === "tailwind"
198
+ responses.cssFramework === "tailwind"
199
199
  ? "Tailwind CSS"
200
- : "Bootstrap";
200
+ : "CSS reset";
201
201
  if (responses.cssFramework !== "none") {
202
202
  cssSpin.start(`Setting up ${cssLabel}...`);
203
203
  }
@@ -247,6 +247,19 @@ export async function main(options = {}) {
247
247
  throw err;
248
248
  }
249
249
 
250
+ // 12.7 Emit .lumen agent context dir (only when agentDocs is enabled)
251
+ if (responses.agentDocs === true || responses._manifestSource?.agentDocs === true) {
252
+ const agentSpin = spinner();
253
+ agentSpin.start("Writing .lumen agent context...");
254
+ try {
255
+ await emitAgentContext(projectPath, responses._manifestSource ?? responses);
256
+ agentSpin.stop(chalk.green(".lumen agent context written."));
257
+ } catch (err) {
258
+ agentSpin.stop(chalk.red("Failed to write .lumen agent context."));
259
+ throw err;
260
+ }
261
+ }
262
+
250
263
  // 13. Generate README.md + LICENSE (if requested)
251
264
  if (responses.readme) {
252
265
  const readmeSpin = spinner();
@@ -276,7 +289,7 @@ export async function main(options = {}) {
276
289
  log.step(chalk.green("\nProject setup complete!"));
277
290
  log.message(
278
291
  chalk.gray(
279
- ` Architecture: ${responses.architecture === "feature-based" ? "Feature-based" : "Type-based"}`
292
+ ` Architecture: ${responses.architecture}`
280
293
  )
281
294
  );
282
295
 
@@ -1,8 +1,43 @@
1
1
  import { promises as fsp } from "fs";
2
2
  import path from "path";
3
- import { parseManifest, SCHEMA_URL, MANIFEST_VERSION } from "./schema.js";
3
+ import { parseManifest, SCHEMA_URL, MANIFEST_VERSION, HARNESS_DEFAULT_COMMANDS } from "./schema.js";
4
4
  import { resolvePaths } from "./paths.js";
5
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
+
6
41
  /**
7
42
  * Build an ordered manifest v2 object from scaffolder responses.
8
43
  * Stable key ordering for byte-determinism.
@@ -35,9 +70,14 @@ export function buildManifest(responses, opts = {}) {
35
70
  engine: responses.cssFramework || "tailwind",
36
71
  };
37
72
 
38
- const rawArch = responses.architecture || "feature-based";
73
+ const rawArch =
74
+ responses.architecturePreset || responses.architecture || "feature-based";
75
+ const normalizedArch = rawArch === "component-based" ? "type-based" : rawArch;
39
76
  const architecture = {
40
- type: rawArch === "component-based" ? "type-based" : rawArch,
77
+ preset: normalizedArch,
78
+ type: normalizedArch,
79
+ validation:
80
+ responses.architectureValidation || responses.validation || "strict",
41
81
  };
42
82
 
43
83
  const ui = {
@@ -59,12 +99,17 @@ export function buildManifest(responses, opts = {}) {
59
99
  formatter: responses.formatter || "none",
60
100
  };
61
101
 
102
+ const harness = responses.harness ?? {
103
+ commands: defaultHarnessCommands(tooling),
104
+ };
105
+
62
106
  const manifest = {
63
107
  $schema: SCHEMA_URL,
64
108
  manifestVersion: MANIFEST_VERSION,
65
109
  framework,
66
110
  styling,
67
111
  architecture,
112
+ harness,
68
113
  ui,
69
114
  docs,
70
115
  paths,
@@ -83,6 +128,7 @@ export function buildManifest(responses, opts = {}) {
83
128
  "framework",
84
129
  "styling",
85
130
  "architecture",
131
+ "harness",
86
132
  "ui",
87
133
  "docs",
88
134
  "paths",
@@ -12,8 +12,12 @@ export const FRAMEWORK_VARIANTS = ["vite", "app-router", "pages-router"];
12
12
  export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
13
13
  export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
14
14
 
15
- export const STYLING_ENGINES = ["tailwind", "bootstrap", "none"];
16
- export const ARCHITECTURE_TYPES = ["feature-based", "type-based", "hybrid", "none"];
15
+ export const STYLING_ENGINES = ["tailwind", "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"];
17
21
  export const UI_KITS = ["shadcn", "none"];
18
22
  export const DOCS_LANGUAGES = ["en", "es"];
19
23
  export const TOOLING_LANGUAGES = ["ts", "js"];
@@ -40,7 +44,49 @@ const stylingSchema = z
40
44
 
41
45
  const architectureSchema = z
42
46
  .object({
43
- type: z.enum(ARCHITECTURE_TYPES),
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),
44
90
  })
45
91
  .strict();
46
92
 
@@ -85,6 +131,7 @@ export const manifestSchemaV2 = z
85
131
  framework: frameworkSchema,
86
132
  styling: stylingSchema,
87
133
  architecture: architectureSchema,
134
+ harness: harnessSchema.optional(),
88
135
  ui: uiSchema,
89
136
  docs: docsSchema,
90
137
  paths: pathsSchema,
@@ -130,16 +177,17 @@ export const manifestSchemaV2 = z
130
177
  });
131
178
  }
132
179
 
133
- // architecture scoping per framework (#34)
180
+ // architecture scoping per framework (#34) — canonical `preset`, legacy `type` fallback
181
+ const archValue = architecture.preset ?? architecture.type;
134
182
  const allowedByFramework =
135
183
  framework.name === "react"
136
184
  ? ["feature-based", "type-based", "none"]
137
185
  : ["feature-based", "hybrid", "none"];
138
- if (!allowedByFramework.includes(architecture.type)) {
186
+ if (!allowedByFramework.includes(archValue)) {
139
187
  ctx.addIssue({
140
188
  code: "custom",
141
189
  path: ["architecture", "type"],
142
- message: `architecture.type "${architecture.type}" is not valid for framework "${framework.name}" (allowed: ${allowedByFramework.join(", ")})`,
190
+ message: `architecture.type "${archValue}" is not valid for framework "${framework.name}" (allowed: ${allowedByFramework.join(", ")})`,
143
191
  });
144
192
  }
145
193
 
@@ -205,7 +253,17 @@ export function parseManifest(raw) {
205
253
  err.cause = result.error;
206
254
  throw err;
207
255
  }
208
- return result.data;
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;
209
267
  }
210
268
 
211
269
  // Re-export type helper for JSDoc (z.infer)