create-lumiose 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,9 @@ Every project starts agent-ready: `AGENTS.md`, `CLAUDE.md` and the `lumiose` ski
18
18
  registers the Lumiose MCP server for Claude Code and Codex. `create-lumiose --help` lists every
19
19
  option (`--yes`, `--force`, `--mcp`, `--format json`).
20
20
 
21
+ Every template sets `lumiose({ csp: "strict" })` in `vite.config.ts`: a strict Content Security
22
+ Policy from the first page ([Security](https://itsjavi.com/lumiose/docs/concepts/security/)).
23
+
21
24
  Next: [Getting started](https://itsjavi.com/lumiose/docs/getting-started/).
22
25
 
23
26
  ## License
package/dist/main.d.ts CHANGED
@@ -7,4 +7,11 @@ export interface CreateIO {
7
7
  /** `npm_config_user_agent`, for the next-step commands. */
8
8
  userAgent?: string;
9
9
  }
10
- export declare function main(argv: string[], io?: CreateIO): Promise<number>;
10
+ export interface CreateOptions {
11
+ /**
12
+ * Offer @lumiose/ui (the `--ui` flag and its question). Default: once the package is on npm
13
+ * (TASK-389); tests turn it on against the workspace package.
14
+ */
15
+ offerUi?: boolean;
16
+ }
17
+ export declare function main(argv: string[], io?: CreateIO, options?: CreateOptions): Promise<number>;
package/dist/main.js CHANGED
@@ -7,8 +7,8 @@ import { relative, resolve } from "node:path";
7
7
  import { parseArgs } from "node:util";
8
8
  import { mcpCommand, mcpNote } from "@lumiose/cli/agents";
9
9
  import pkg from "../package.json" with { type: "json" };
10
- import { DEFAULT_TEMPLATE, scaffold, ScaffoldError, TEMPLATES, } from "./scaffold.js";
11
- const USAGE = `Usage: create-lumiose [dir] [options]
10
+ import { DEFAULT_TEMPLATE, scaffold, ScaffoldError, TEMPLATES, UI_PACKAGE, } from "./scaffold.js";
11
+ const usage = (offerUi) => `Usage: create-lumiose [dir] [options]
12
12
 
13
13
  Create a Lumiose app in <dir> (asked for when missing in a terminal; default: lumiose-app).
14
14
 
@@ -19,7 +19,11 @@ Options:
19
19
  --force Write into a non-empty directory (same-name files are overwritten)
20
20
  --mcp Register the Lumiose MCP server for Claude Code (.mcp.json) and Codex
21
21
  (.codex/config.toml), run with your package manager
22
- --format <format> human (default) or json: { command: "create", ok, ... } on stdout
22
+ ${offerUi
23
+ ? ` --ui Add @lumiose/ui: accessible, unstyled components with the default theme,
24
+ and their agent docs and skill
25
+ `
26
+ : ""} --format <format> human (default) or json: { command: "create", ok, ... } on stdout
23
27
  -h, --help Show this help
24
28
  -v, --version Show the version
25
29
 
@@ -33,7 +37,8 @@ Examples:
33
37
  create-lumiose my-site -t static --yes --format json
34
38
  pnpm create lumiose my-app --mcp
35
39
  `;
36
- export async function main(argv, io = processIO()) {
40
+ export async function main(argv, io = processIO(), options = {}) {
41
+ const offerUi = options.offerUi ?? UI_PACKAGE?.published === true;
37
42
  const json = argv.some((arg, index) => arg === "--format=json" || (arg === "--format" && argv[index + 1] === "json"));
38
43
  const usageError = (message) => {
39
44
  if (json) {
@@ -54,6 +59,7 @@ export async function main(argv, io = processIO()) {
54
59
  yes: { type: "boolean", short: "y" },
55
60
  force: { type: "boolean" },
56
61
  mcp: { type: "boolean" },
62
+ ui: { type: "boolean" },
57
63
  format: { type: "string" },
58
64
  help: { type: "boolean", short: "h" },
59
65
  version: { type: "boolean", short: "v" },
@@ -68,7 +74,7 @@ export async function main(argv, io = processIO()) {
68
74
  return usageError(`--format must be "human" or "json", not "${values.format}".`);
69
75
  }
70
76
  if (values.help) {
71
- io.write(USAGE);
77
+ io.write(usage(offerUi));
72
78
  return 0;
73
79
  }
74
80
  if (values.version) {
@@ -77,6 +83,9 @@ export async function main(argv, io = processIO()) {
77
83
  }
78
84
  if (positionals.length > 1)
79
85
  return usageError(`Unexpected argument "${positionals[1]}".`);
86
+ if (values.ui && !offerUi) {
87
+ return usageError("--ui comes with the first release of @lumiose/ui, which is not on npm yet.");
88
+ }
80
89
  if (values.template !== undefined && !isTemplate(values.template)) {
81
90
  return usageError(`Unknown template "${values.template}". Pick one of: ${TEMPLATES.map((t) => t.name).join(", ")}.`);
82
91
  }
@@ -89,6 +98,11 @@ export async function main(argv, io = processIO()) {
89
98
  if (template === undefined && ask)
90
99
  template = await askTemplate(ask);
91
100
  template ??= DEFAULT_TEMPLATE;
101
+ let ui = values.ui ?? false;
102
+ if (values.ui === undefined && offerUi && ask) {
103
+ const answer = await ask("Add @lumiose/ui, accessible UI components with a default theme? (y/N): ");
104
+ ui = /^y(?:es)?$/i.test(answer.trim());
105
+ }
92
106
  const pm = packageManager(io.userAgent);
93
107
  let result;
94
108
  try {
@@ -98,6 +112,7 @@ export async function main(argv, io = processIO()) {
98
112
  ...(values.name === undefined ? {} : { name: values.name }),
99
113
  force: values.force ?? false,
100
114
  ...(values.mcp ? { mcp: pm } : {}),
115
+ ...(ui ? { ui } : {}),
101
116
  });
102
117
  }
103
118
  catch (error) {
@@ -123,6 +138,7 @@ export async function main(argv, io = processIO()) {
123
138
  template: result.template.name,
124
139
  mode: result.template.mode,
125
140
  files: result.files,
141
+ ui,
126
142
  next,
127
143
  ...(values.mcp ? { mcp: { ...mcpCommand(pm), note: mcpNote(pm) } } : {}),
128
144
  }));
@@ -130,6 +146,9 @@ export async function main(argv, io = processIO()) {
130
146
  else {
131
147
  io.write(`\nCreated ${result.name} in ${result.dir} (template: ${result.template.name}, mode: ${result.template.mode}).\n` +
132
148
  `Agent-ready: AGENTS.md, CLAUDE.md and the lumiose skill (.claude/skills/lumiose).\n` +
149
+ (ui
150
+ ? "UI: @lumiose/ui with its default theme, and the lumiose-ui skill (.claude/skills/lumiose-ui).\n"
151
+ : "") +
133
152
  (values.mcp
134
153
  ? `MCP: .mcp.json (Claude Code) and .codex/config.toml (Codex) run the Lumiose MCP server.\n${mcpNote(pm)}\n`
135
154
  : "") +
@@ -8,6 +8,16 @@ export interface Template {
8
8
  }
9
9
  export declare const TEMPLATES: readonly Template[];
10
10
  export declare const DEFAULT_TEMPLATE: TemplateName;
11
+ /**
12
+ * The @lumiose/ui this create-lumiose was released with: it keeps its own versions (decision-54).
13
+ * `published` is false while the package is private (TASK-389): the option stays hidden until then.
14
+ */
15
+ export declare const UI_PACKAGE: {
16
+ version: string;
17
+ published: boolean;
18
+ } | undefined;
19
+ /** The stylesheets a project with @lumiose/ui imports in its root layout. */
20
+ export declare const UI_STYLESHEETS: string[];
11
21
  /** A problem with the target (not empty, invalid name): the CLI exits with 1. */
12
22
  export declare class ScaffoldError extends Error {
13
23
  name: string;
@@ -25,6 +35,8 @@ export interface ScaffoldOptions {
25
35
  * this package manager.
26
36
  */
27
37
  mcp?: PackageManager;
38
+ /** Add @lumiose/ui: the dependency, its stylesheet and default theme, and its agent files. */
39
+ ui?: boolean;
28
40
  }
29
41
  export interface ScaffoldResult {
30
42
  /** Absolute target directory. */
package/dist/scaffold.js CHANGED
@@ -1,8 +1,11 @@
1
1
  // Writes a new Lumiose project: a template's `src/` (and extras), the generated package.json,
2
- // tsconfig.json (extending the generated one, decision-25), vite.config.ts with the template's `lumiose({ mode })` (decision-21),
3
- // .gitignore and README, then the agent files (AGENTS.md, CLAUDE.md, the lumiose skill,
4
- // .gitattributes) from `@lumiose/cli/agents`, shared with `lumiose agents init`.
2
+ // tsconfig.json (extending the generated one, decision-25), vite.config.ts with the template's
3
+ // `lumiose({ mode })` (decision-21) and `csp: "strict"` (decision-56), .gitignore and README, then the agent files (AGENTS.md, CLAUDE.md, the lumiose skill,
4
+ // .gitattributes) from `@lumiose/cli/agents`, shared with `lumiose agents init`. With `ui`, the app
5
+ // also depends on @lumiose/ui, imports its stylesheet and default theme in the root layout, and
6
+ // gets its agent files (decision-64).
5
7
  import { existsSync } from "node:fs";
8
+ import { createRequire } from "node:module";
6
9
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
7
10
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
8
11
  import { fileURLToPath } from "node:url";
@@ -38,6 +41,22 @@ const VERSIONS = {
38
41
  oxlint: "^1.87.0",
39
42
  oxfmt: "^0.72.0",
40
43
  };
44
+ /**
45
+ * The @lumiose/ui this create-lumiose was released with: it keeps its own versions (decision-54).
46
+ * `published` is false while the package is private (TASK-389): the option stays hidden until then.
47
+ */
48
+ export const UI_PACKAGE = readUiPackage();
49
+ function readUiPackage() {
50
+ try {
51
+ const manifest = createRequire(import.meta.url)("@lumiose/ui/package.json");
52
+ return { version: manifest.version, published: manifest.private !== true };
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ /** The stylesheets a project with @lumiose/ui imports in its root layout. */
59
+ export const UI_STYLESHEETS = ["@lumiose/ui/styles.css", "@lumiose/ui/themes/default.css"];
41
60
  /** A problem with the target (not empty, invalid name): the CLI exits with 1. */
42
61
  export class ScaffoldError extends Error {
43
62
  name = "ScaffoldError";
@@ -47,6 +66,9 @@ export async function scaffold(options) {
47
66
  const template = TEMPLATES.find((candidate) => candidate.name === options.template);
48
67
  if (!template)
49
68
  throw new ScaffoldError(`Unknown template "${options.template}".`);
69
+ if (options.ui && UI_PACKAGE === undefined) {
70
+ throw new ScaffoldError("@lumiose/ui is not available in this create-lumiose.");
71
+ }
50
72
  const name = options.name ?? packageName(basename(dir));
51
73
  if (!isValidPackageName(name)) {
52
74
  throw new ScaffoldError(`"${name}" is not a valid package name: use lowercase letters, digits, "-", "." and "_" (pass --name).`);
@@ -60,11 +82,21 @@ export async function scaffold(options) {
60
82
  const text = await readFile(join(templateDir, file), "utf8");
61
83
  files.set(file, text.replaceAll("{{name}}", name));
62
84
  }
63
- files.set("package.json", packageJson(name, template));
85
+ // The root layout imports @lumiose/ui's styles for every page; the minimal template has no
86
+ // layout, so its only page does.
87
+ let uiEntry;
88
+ if (options.ui) {
89
+ uiEntry = files.has("src/routes/layout.stsx")
90
+ ? "src/routes/layout.stsx"
91
+ : "src/routes/page.stsx";
92
+ const imports = UI_STYLESHEETS.map((sheet) => `import "${sheet}";\n`).join("");
93
+ files.set(uiEntry, imports + files.get(uiEntry));
94
+ }
95
+ files.set("package.json", packageJson(name, template, options.ui ?? false));
64
96
  files.set("tsconfig.json", formatTsconfig());
65
97
  files.set("vite.config.ts", viteConfig(template));
66
98
  files.set(".gitignore", "node_modules\ndist\n.lumiose\n");
67
- files.set("README.md", readme(name, template));
99
+ files.set("README.md", readme(name, template, uiEntry));
68
100
  for (const [file, text] of files) {
69
101
  await mkdir(dirname(join(dir, file)), { recursive: true });
70
102
  await writeFile(join(dir, file), text);
@@ -92,7 +124,7 @@ export function packageName(text) {
92
124
  export function isValidPackageName(name) {
93
125
  return /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/.test(name) && name.length <= 214;
94
126
  }
95
- function packageJson(name, template) {
127
+ function packageJson(name, template, ui) {
96
128
  const server = template.mode === "server";
97
129
  // The adapter serves `lumiose preview` everywhere and is the production server of `ssr`.
98
130
  const adapter = { "@lumiose/adapter-node": VERSIONS.lumiose };
@@ -110,7 +142,11 @@ function packageJson(name, template) {
110
142
  lint: "lumiose lint",
111
143
  fmt: "lumiose fmt",
112
144
  },
113
- dependencies: { ...(server ? adapter : {}), lumiose: VERSIONS.lumiose },
145
+ dependencies: {
146
+ ...(server ? adapter : {}),
147
+ ...(ui ? { "@lumiose/ui": `^${UI_PACKAGE.version}` } : {}),
148
+ lumiose: VERSIONS.lumiose,
149
+ },
114
150
  devDependencies: {
115
151
  ...(server ? {} : adapter),
116
152
  "@lumiose/cli": VERSIONS.lumiose,
@@ -129,13 +165,21 @@ function viteConfig(template) {
129
165
  import { defineConfig } from "vite";
130
166
 
131
167
  export default defineConfig({
132
- // The default mode of every route: "static" (built HTML), "server" (rendered per request) or
133
- // "spa" (rendered in the browser). A route or layout overrides it with \`export const mode\`.
134
- plugins: [lumiose({ mode: "${template.mode}" })],
168
+ plugins: [
169
+ lumiose({
170
+ // The default mode of every route: "static" (built HTML), "server" (rendered per request) or
171
+ // "spa" (rendered in the browser). A route or layout overrides it with \`export const mode\`.
172
+ mode: "${template.mode}",
173
+ // A strict Content Security Policy: a nonce on pages rendered per request, hashes on
174
+ // prerendered ones. The scripts and styles Lumiose and your JSX write get them.
175
+ csp: "strict",
176
+ }),
177
+ ],
135
178
  });
136
179
  `;
137
180
  }
138
- function readme(name, template) {
181
+ /** The project README; `uiEntry` is the file that imports @lumiose/ui's styles, if any. */
182
+ function readme(name, template, uiEntry) {
139
183
  const server = template.mode === "server";
140
184
  return `# ${name}
141
185
 
@@ -153,12 +197,18 @@ ${template.description}. Made with Lumiose (\`create-lumiose --template ${templa
153
197
 
154
198
  - \`src/routes/\`: one folder per URL segment, with a \`page.stsx\` page or a \`route.ts\` endpoint;
155
199
  \`layout.stsx\` wraps the pages below it. Other files there are colocated modules.
156
- - \`vite.config.ts\`: \`lumiose({ mode: "${template.mode}" })\` sets the default route mode.
200
+ - \`vite.config.ts\`: \`lumiose({ mode: "${template.mode}", csp: "strict" })\` sets the default route mode
201
+ and a strict Content Security Policy.
157
202
  - \`.lumiose/\`: generated types and shadow files (git-ignored).
158
203
 
159
204
  \`.stsx\` files are TSX with raw-CSS \`<style>\` blocks. The docs for your Lumiose version are in
160
205
  \`node_modules/lumiose/docs\` (\`npx lumiose docs <topic>\`). Coding agents: see \`AGENTS.md\`.
161
- `;
206
+ ${uiEntry === undefined
207
+ ? ""
208
+ : `
209
+ UI components come from \`@lumiose/ui\`: accessible and unstyled, with its default theme imported in
210
+ \`${uiEntry}\`. Their docs are in \`node_modules/@lumiose/ui/docs\` (\`npx lumiose docs ui\`).
211
+ `}`;
162
212
  }
163
213
  async function listFiles(dir, base = dir) {
164
214
  const entries = await readdir(dir, { withFileTypes: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lumiose",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Create a Lumiose app: `pnpm create lumiose my-app --template ssr`. Every project starts agent-ready (AGENTS.md, CLAUDE.md, the lumiose skill).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,11 +17,12 @@
17
17
  ],
18
18
  "type": "module",
19
19
  "dependencies": {
20
- "@lumiose/cli": "0.4.0",
21
- "lumiose": "0.4.0"
20
+ "@lumiose/cli": "0.5.0",
21
+ "lumiose": "0.5.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@lumiose/adapter-node": "0.4.0",
24
+ "@lumiose/adapter-node": "0.5.0",
25
+ "@lumiose/ui": "0.0.0",
25
26
  "typescript": "^7.0.2",
26
27
  "vite": "^8.3.4"
27
28
  },