orizon 0.2.0 → 0.2.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 (2) hide show
  1. package/bin/init.mjs +537 -156
  2. package/package.json +1 -1
package/bin/init.mjs CHANGED
@@ -1,32 +1,80 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
- import { resolve, join } from "node:path";
4
+ import { join, relative, basename } from "node:path";
5
5
  import { execSync } from "node:child_process";
6
+ import { createInterface } from "node:readline";
6
7
  import pc from "picocolors";
7
8
 
8
9
  // ---------------------------------------------------------------------------
9
10
  // Logging helpers
10
11
  // ---------------------------------------------------------------------------
11
12
  const log = {
12
- step: (msg) => console.log(pc.cyan("●") + " " + msg),
13
- success: (msg) => console.log(pc.green("✔") + " " + msg),
14
- skip: (msg) => console.log(pc.yellow("") + " " + pc.dim(msg)),
15
- error: (msg) => console.error(pc.red("") + " " + msg),
16
- info: (msg) => console.log(pc.dim(" " + msg)),
13
+ step: (num, total, msg) =>
14
+ console.log(`\n ${pc.cyan(`[${num}/${total}]`)} ${pc.bold(msg)}`),
15
+ success: (msg) => console.log(` ${pc.green("")} ${msg}`),
16
+ skip: (msg) => console.log(` ${pc.yellow("")} ${pc.dim(msg)}`),
17
+ error: (msg) => console.error(` ${pc.red("")} ${msg}`),
18
+ info: (msg) => console.log(` ${pc.dim(msg)}`),
19
+ detail: (msg) => console.log(` ${msg}`),
20
+ divider: () => console.log(pc.dim(" " + "─".repeat(50))),
17
21
  };
18
22
 
23
+ // ---------------------------------------------------------------------------
24
+ // Prompt helper — ask y/n question
25
+ // ---------------------------------------------------------------------------
26
+ function ask(question, defaultYes = true) {
27
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
28
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
29
+
30
+ return new Promise((resolve) => {
31
+ rl.question(
32
+ `\n ${pc.yellow("?")} ${question} ${pc.dim(hint)} `,
33
+ (answer) => {
34
+ rl.close();
35
+ const trimmed = answer.trim().toLowerCase();
36
+ if (trimmed === "") resolve(defaultYes);
37
+ else resolve(trimmed === "y" || trimmed === "yes");
38
+ },
39
+ );
40
+ });
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Prompt helper — ask for text input
45
+ // ---------------------------------------------------------------------------
46
+ function askText(question, defaultValue = "") {
47
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
48
+ const hint = defaultValue ? pc.dim(` (${defaultValue})`) : "";
49
+
50
+ return new Promise((resolve) => {
51
+ rl.question(
52
+ `\n ${pc.yellow("?")} ${question}${hint} `,
53
+ (answer) => {
54
+ rl.close();
55
+ const trimmed = answer.trim();
56
+ resolve(trimmed || defaultValue);
57
+ },
58
+ );
59
+ });
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Sleep helper for pacing output
64
+ // ---------------------------------------------------------------------------
65
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
66
+
19
67
  // ---------------------------------------------------------------------------
20
68
  // Detect package manager
21
69
  // ---------------------------------------------------------------------------
22
70
  function detectPM(cwd) {
23
71
  if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
24
- return { name: "bun", install: "bun add -D" };
72
+ return { name: "bun", install: "bun add -D", installProd: "bun add", create: "bunx", exec: "bunx" };
25
73
  if (existsSync(join(cwd, "pnpm-lock.yaml")))
26
- return { name: "pnpm", install: "pnpm add -D" };
74
+ return { name: "pnpm", install: "pnpm add -D", installProd: "pnpm add", create: "pnpm create", exec: "pnpm dlx" };
27
75
  if (existsSync(join(cwd, "yarn.lock")))
28
- return { name: "yarn", install: "yarn add -D" };
29
- return { name: "npm", install: "npm install -D" };
76
+ return { name: "yarn", install: "yarn add -D", installProd: "yarn add", create: "yarn create", exec: "yarn dlx" };
77
+ return { name: "npm", install: "npm install -D", installProd: "npm install", create: "npm create", exec: "npx" };
30
78
  }
31
79
 
32
80
  // ---------------------------------------------------------------------------
@@ -39,180 +87,389 @@ function isInstalled(pkg, pkgJson) {
39
87
  }
40
88
 
41
89
  // ---------------------------------------------------------------------------
42
- // Patch vite.config.ts add tailwindcss import + plugin
90
+ // Sanitize project name for npm
43
91
  // ---------------------------------------------------------------------------
44
- function patchViteConfig(filePath) {
45
- let content = readFileSync(filePath, "utf8");
46
- let modified = false;
47
-
48
- // 1. Add import if missing
49
- if (!content.includes("@tailwindcss/vite")) {
50
- // Find the last import statement and add after it
51
- const importRegex = /^import\s.+$/gm;
52
- let lastImportMatch = null;
53
- let match;
54
- while ((match = importRegex.exec(content)) !== null) {
55
- lastImportMatch = match;
56
- }
92
+ function sanitizeName(name) {
93
+ return name
94
+ .toLowerCase()
95
+ .replace(/\s+/g, "-")
96
+ .replace(/[^a-z0-9\-_.]/g, "")
97
+ .replace(/^[.\-_]+/, "");
98
+ }
57
99
 
58
- const importLine = 'import tailwindcss from "@tailwindcss/vite";';
100
+ // ---------------------------------------------------------------------------
101
+ // Scaffold a new React + Vite + TS project
102
+ // ---------------------------------------------------------------------------
103
+ async function scaffoldProject(cwd) {
104
+ const dirName = basename(cwd);
105
+ const defaultName = sanitizeName(dirName) || "my-orizon-app";
59
106
 
60
- if (lastImportMatch) {
61
- const insertPos = lastImportMatch.index + lastImportMatch[0].length;
62
- content =
63
- content.slice(0, insertPos) + "\n" + importLine + content.slice(insertPos);
64
- } else {
65
- // No imports found — add at top
66
- content = importLine + "\n" + content;
67
- }
68
- modified = true;
69
- log.success("Added tailwindcss import to " + filePath.split(/[\\/]/).pop());
70
- } else {
71
- log.skip("tailwindcss import already in vite config");
107
+ // Ask project name
108
+ const rawName = await askText("What is your project name?", defaultName);
109
+ const projectName = sanitizeName(rawName);
110
+
111
+ if (!projectName) {
112
+ log.error("Invalid project name.");
113
+ process.exit(1);
72
114
  }
73
115
 
74
- // 2. Add plugin if missing
75
- if (!content.includes("tailwindcss()")) {
76
- // Find plugins: [ and insert tailwindcss() after the opening bracket
77
- const pluginsMatch = content.match(/plugins\s*:\s*\[/);
78
- if (pluginsMatch) {
79
- const insertPos = pluginsMatch.index + pluginsMatch[0].length;
80
- content =
81
- content.slice(0, insertPos) +
82
- "tailwindcss(), " +
83
- content.slice(insertPos);
84
- modified = true;
85
- log.success("Added tailwindcss() plugin to vite config");
86
- } else {
87
- log.error(
88
- "Could not find plugins array in vite config — please add tailwindcss() manually",
89
- );
116
+ const projectDir = join(cwd, projectName);
117
+
118
+ if (existsSync(projectDir)) {
119
+ // Check if directory is non-empty
120
+ const { readdirSync } = await import("node:fs");
121
+ const contents = readdirSync(projectDir);
122
+ if (contents.length > 0) {
123
+ log.error(`Directory "${projectName}" already exists and is not empty.`);
124
+ process.exit(1);
90
125
  }
91
- } else {
92
- log.skip("tailwindcss() plugin already in vite config");
93
126
  }
94
127
 
95
- if (modified) {
96
- writeFileSync(filePath, content, "utf8");
128
+ console.log();
129
+ log.info(`Creating ${pc.bold(projectName)} with React + Vite + TypeScript...`);
130
+ log.info(pc.dim(`Directory: ${projectDir}`));
131
+ console.log();
132
+
133
+ // Step A: Create Vite project
134
+ log.info("Scaffolding Vite project...");
135
+ const createCmd = `npm create vite@latest ${projectName} -- --template react-ts`;
136
+ log.info(pc.dim(`$ ${createCmd}`));
137
+ await sleep(200);
138
+
139
+ try {
140
+ execSync(createCmd, { cwd, stdio: "pipe" });
141
+ log.success("Created Vite + React + TypeScript project");
142
+ } catch (err) {
143
+ log.error("Failed to create Vite project.");
144
+ log.info("Make sure you have npm installed and try again.");
145
+ process.exit(1);
97
146
  }
98
- }
99
147
 
100
- // ---------------------------------------------------------------------------
101
- // Patch CSS file — prepend required @import / @source lines
102
- // ---------------------------------------------------------------------------
103
- function patchCSS(filePath, created) {
104
- let content = created ? "" : readFileSync(filePath, "utf8");
105
- const requiredLines = [
106
- '@import "tailwindcss";',
107
- '@import "orizon/preset.css";',
108
- '@source "../node_modules/orizon/dist";',
109
- ];
148
+ await sleep(300);
110
149
 
111
- const toAdd = [];
112
- for (const line of requiredLines) {
113
- if (!content.includes(line)) {
114
- toAdd.push(line);
115
- }
150
+ // Step B: Install base dependencies
151
+ log.info("Installing base dependencies...");
152
+ const installCmd = "npm install";
153
+ log.info(pc.dim(`$ ${installCmd}`));
154
+ await sleep(200);
155
+
156
+ try {
157
+ execSync(installCmd, { cwd: projectDir, stdio: "pipe" });
158
+ log.success("Installed base dependencies");
159
+ } catch {
160
+ log.error("Failed to install base dependencies.");
161
+ process.exit(1);
116
162
  }
117
163
 
118
- if (toAdd.length === 0) {
119
- log.skip("CSS file already has all required imports");
120
- return;
164
+ await sleep(300);
165
+
166
+ // Step C: Install Orizon
167
+ log.info("Installing Orizon...");
168
+ const orizonCmd = "npm install orizon";
169
+ log.info(pc.dim(`$ ${orizonCmd}`));
170
+ await sleep(200);
171
+
172
+ try {
173
+ execSync(orizonCmd, { cwd: projectDir, stdio: "pipe" });
174
+ log.success("Installed orizon");
175
+ } catch {
176
+ log.error("Failed to install orizon.");
177
+ process.exit(1);
121
178
  }
122
179
 
123
- // Prepend missing lines at the top
124
- content = toAdd.join("\n") + (content ? "\n" + content : "\n");
125
- writeFileSync(filePath, content, "utf8");
126
- log.success(
127
- `Added ${toAdd.length} line${toAdd.length > 1 ? "s" : ""} to ${filePath.split(/[\\/]/).pop()}`,
128
- );
180
+ return { projectDir, projectName };
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Find vite config in a directory
185
+ // ---------------------------------------------------------------------------
186
+ function findViteConfig(dir) {
187
+ const candidates = ["vite.config.ts", "vite.config.js", "vite.config.mts"];
188
+ for (const name of candidates) {
189
+ const full = join(dir, name);
190
+ if (existsSync(full)) return full;
191
+ }
192
+ return null;
129
193
  }
130
194
 
131
195
  // ---------------------------------------------------------------------------
132
196
  // Main init function
133
197
  // ---------------------------------------------------------------------------
134
198
  export async function init() {
135
- const cwd = process.cwd();
199
+ let cwd = process.cwd();
200
+ let scaffolded = false;
201
+ let projectName = "";
136
202
 
137
203
  console.log();
138
- console.log(pc.bold(" Orizon") + pc.dim(" — setting up your project"));
204
+ console.log(
205
+ pc.bold(pc.cyan(" ⬡ Orizon")) + pc.dim(" — project setup wizard"),
206
+ );
139
207
  console.log();
208
+ log.divider();
140
209
 
141
210
  // -----------------------------------------------------------------------
142
- // Step 0: Validate we're in a project
211
+ // Pre-flight: Detect project or scaffold a new one
143
212
  // -----------------------------------------------------------------------
144
213
  const pkgPath = join(cwd, "package.json");
145
- if (!existsSync(pkgPath)) {
146
- log.error("No package.json found in current directory.");
147
- log.info("Run this command from the root of your project.");
214
+ const hasProject = existsSync(pkgPath);
215
+ const hasViteConfig = hasProject && findViteConfig(cwd) !== null;
216
+
217
+ if (hasProject && hasViteConfig) {
218
+ // Existing Vite project — proceed with setup
219
+ log.success("Detected existing Vite project");
220
+ } else if (hasProject && !hasViteConfig) {
221
+ // Has package.json but not a Vite project
222
+ log.info("Found package.json but no vite.config — not a Vite project.");
223
+ log.error("Currently only React + Vite projects are supported.");
148
224
  process.exit(1);
149
- }
225
+ } else {
226
+ // No project at all — offer to create one
227
+ log.info("No project detected in this directory.");
228
+ console.log();
229
+ log.info("Orizon can create a new React + Vite + TypeScript project for you");
230
+ log.info("and set up everything automatically.");
231
+
232
+ const proceed = await ask("Create a new project?");
233
+ if (!proceed) {
234
+ log.info("No worries! To set up an existing project, run this from its root directory.");
235
+ process.exit(0);
236
+ }
150
237
 
151
- // Check for vite config
152
- const viteConfigTS = join(cwd, "vite.config.ts");
153
- const viteConfigJS = join(cwd, "vite.config.js");
154
- const viteConfigMTS = join(cwd, "vite.config.mts");
155
- const viteConfigPath = existsSync(viteConfigTS)
156
- ? viteConfigTS
157
- : existsSync(viteConfigJS)
158
- ? viteConfigJS
159
- : existsSync(viteConfigMTS)
160
- ? viteConfigMTS
161
- : null;
162
-
163
- if (!viteConfigPath) {
164
- log.error("No vite.config.ts/js found. Currently only Vite projects are supported.");
165
- process.exit(1);
166
- }
238
+ const result = await scaffoldProject(cwd);
239
+ cwd = result.projectDir;
240
+ projectName = result.projectName;
241
+ scaffolded = true;
167
242
 
168
- log.success("Detected Vite project");
243
+ console.log();
244
+ log.divider();
245
+ console.log();
246
+ log.info("Project created! Now configuring Orizon...");
247
+ }
169
248
 
170
249
  // -----------------------------------------------------------------------
171
- // Step 1: Detect package manager
250
+ // From here on, we're in a valid Vite project (either existing or new)
172
251
  // -----------------------------------------------------------------------
252
+ const viteConfigPath = findViteConfig(cwd);
173
253
  const pm = detectPM(cwd);
174
- log.info(`Using ${pc.bold(pm.name)}`);
175
- console.log();
254
+ const currentPkgJson = JSON.parse(
255
+ readFileSync(join(cwd, "package.json"), "utf8"),
256
+ );
176
257
 
177
- // -----------------------------------------------------------------------
178
- // Step 2: Install dependencies
179
- // -----------------------------------------------------------------------
180
- log.step("Installing dependencies...");
181
-
182
- const pkgJson = JSON.parse(readFileSync(pkgPath, "utf8"));
183
- const toInstall = [];
184
-
185
- if (!isInstalled("tailwindcss", pkgJson)) toInstall.push("tailwindcss");
186
- if (!isInstalled("@tailwindcss/vite", pkgJson))
187
- toInstall.push("@tailwindcss/vite");
188
-
189
- if (toInstall.length > 0) {
190
- const cmd = `${pm.install} ${toInstall.join(" ")}`;
191
- log.info(pc.dim(`$ ${cmd}`));
192
- try {
193
- execSync(cmd, { cwd, stdio: "pipe" });
194
- log.success(`Installed ${toInstall.join(", ")}`);
195
- } catch (err) {
196
- log.error(`Failed to install dependencies. Run manually:\n ${cmd}`);
197
- process.exit(1);
258
+ const TOTAL_STEPS = scaffolded ? 6 : 5;
259
+ let stepNum = scaffolded ? 2 : 1; // if scaffolded, step 1 was "Create project"
260
+
261
+ if (scaffolded) {
262
+ log.step(1, TOTAL_STEPS, "Create project");
263
+ log.success(`Created ${pc.bold(projectName)} with React + Vite + TypeScript`);
264
+ log.success("Installed base dependencies & Orizon");
265
+ }
266
+
267
+ if (!scaffolded) {
268
+ log.info(`Package manager: ${pc.bold(pm.name)}`);
269
+ log.info(`Config file: ${pc.bold(viteConfigPath.split(/[\\/]/).pop())}`);
270
+ }
271
+
272
+ // =======================================================================
273
+ // STEP: Install Tailwind CSS dependencies
274
+ // =======================================================================
275
+ log.step(stepNum, TOTAL_STEPS, "Tailwind CSS dependencies");
276
+ log.info("Orizon uses Tailwind CSS v4 for styling.");
277
+ log.info("These packages are required for Tailwind to work with Vite.");
278
+ await sleep(300);
279
+
280
+ const tailwindDeps = [
281
+ { pkg: "tailwindcss", label: "Tailwind CSS core" },
282
+ { pkg: "@tailwindcss/vite", label: "Tailwind CSS Vite plugin" },
283
+ ];
284
+
285
+ const tailwindMissing = tailwindDeps.filter(
286
+ (d) => !isInstalled(d.pkg, currentPkgJson),
287
+ );
288
+ const tailwindInstalled = tailwindDeps.filter((d) =>
289
+ isInstalled(d.pkg, currentPkgJson),
290
+ );
291
+
292
+ for (const d of tailwindInstalled) {
293
+ log.skip(`${d.pkg} — already installed`);
294
+ }
295
+
296
+ if (tailwindMissing.length > 0) {
297
+ for (const d of tailwindMissing) {
298
+ log.detail(`${pc.bold(d.pkg)} ${pc.dim(`— ${d.label}`)}`);
299
+ }
300
+
301
+ const proceed = scaffolded
302
+ ? true
303
+ : await ask(
304
+ `Install ${tailwindMissing.length} Tailwind package${tailwindMissing.length > 1 ? "s" : ""}?`,
305
+ );
306
+ if (proceed) {
307
+ const cmd = `${pm.install} ${tailwindMissing.map((d) => d.pkg).join(" ")}`;
308
+ log.info(pc.dim(`Running: ${cmd}`));
309
+ await sleep(200);
310
+ try {
311
+ execSync(cmd, { cwd, stdio: "pipe" });
312
+ for (const d of tailwindMissing) {
313
+ log.success(`Installed ${d.pkg}`);
314
+ }
315
+ } catch {
316
+ log.error(`Failed to install. Run manually:\n ${cmd}`);
317
+ }
318
+ } else {
319
+ log.skip("Skipped — you can install these manually later");
198
320
  }
199
- } else {
200
- log.skip("tailwindcss & @tailwindcss/vite already installed");
201
321
  }
322
+ stepNum++;
323
+
324
+ // =======================================================================
325
+ // STEP: Install Form & Validation dependencies
326
+ // =======================================================================
327
+ log.step(stepNum, TOTAL_STEPS, "Form & validation dependencies");
328
+ log.info("These packages power the Form component with Zod validation.");
329
+ log.info("Needed for: Form, Form.Item, Form.useForm, Form.List");
330
+ await sleep(300);
331
+
332
+ const formDeps = [
333
+ { pkg: "react-hook-form", label: "Form state management" },
334
+ { pkg: "@hookform/resolvers", label: "Schema validation adapters" },
335
+ { pkg: "zod", label: "Schema validation library" },
336
+ ];
202
337
 
203
- console.log();
338
+ const formMissing = formDeps.filter(
339
+ (d) => !isInstalled(d.pkg, currentPkgJson),
340
+ );
341
+ const formAlreadyInstalled = formDeps.filter((d) =>
342
+ isInstalled(d.pkg, currentPkgJson),
343
+ );
204
344
 
205
- // -----------------------------------------------------------------------
206
- // Step 3: Patch vite config
207
- // -----------------------------------------------------------------------
208
- log.step("Configuring Vite...");
209
- patchViteConfig(viteConfigPath);
210
- console.log();
345
+ for (const d of formAlreadyInstalled) {
346
+ log.skip(`${d.pkg} already installed`);
347
+ }
211
348
 
212
- // -----------------------------------------------------------------------
213
- // Step 4: Patch CSS
214
- // -----------------------------------------------------------------------
215
- log.step("Setting up CSS...");
349
+ if (formMissing.length > 0) {
350
+ for (const d of formMissing) {
351
+ log.detail(`${pc.bold(d.pkg)} ${pc.dim(`— ${d.label}`)}`);
352
+ }
353
+
354
+ const proceed = scaffolded
355
+ ? true
356
+ : await ask(
357
+ `Install ${formMissing.length} form package${formMissing.length > 1 ? "s" : ""}?`,
358
+ );
359
+ if (proceed) {
360
+ const cmd = `${pm.install} ${formMissing.map((d) => d.pkg).join(" ")}`;
361
+ log.info(pc.dim(`Running: ${cmd}`));
362
+ await sleep(200);
363
+ try {
364
+ execSync(cmd, { cwd, stdio: "pipe" });
365
+ for (const d of formMissing) {
366
+ log.success(`Installed ${d.pkg}`);
367
+ }
368
+ } catch {
369
+ log.error(`Failed to install. Run manually:\n ${cmd}`);
370
+ }
371
+ } else {
372
+ log.skip("Skipped — Form component will not work without these");
373
+ }
374
+ }
375
+ stepNum++;
376
+
377
+ // =======================================================================
378
+ // STEP: Configure Vite
379
+ // =======================================================================
380
+ log.step(stepNum, TOTAL_STEPS, "Configure Vite");
381
+ log.info("The Tailwind CSS Vite plugin must be added to your vite config.");
382
+ log.info(`File: ${pc.bold(viteConfigPath.split(/[\\/]/).pop())}`);
383
+ await sleep(300);
384
+
385
+ const viteContent = readFileSync(viteConfigPath, "utf8");
386
+ const viteNeedsImport = !viteContent.includes("@tailwindcss/vite");
387
+ const viteNeedsPlugin = !viteContent.includes("tailwindcss()");
388
+
389
+ if (viteNeedsImport || viteNeedsPlugin) {
390
+ log.info("Changes needed:");
391
+ if (viteNeedsImport) {
392
+ log.detail(
393
+ pc.green("+ ") +
394
+ pc.cyan('import tailwindcss from "@tailwindcss/vite"'),
395
+ );
396
+ }
397
+ if (viteNeedsPlugin) {
398
+ log.detail(
399
+ pc.green("+ ") +
400
+ pc.cyan("tailwindcss()") +
401
+ pc.dim(" in plugins array"),
402
+ );
403
+ }
404
+
405
+ const proceed = scaffolded ? true : await ask("Apply these changes?");
406
+ if (proceed) {
407
+ let content = readFileSync(viteConfigPath, "utf8");
408
+ let modified = false;
409
+
410
+ // Add import
411
+ if (viteNeedsImport) {
412
+ const importRegex = /^import\s.+$/gm;
413
+ let lastImportMatch = null;
414
+ let match;
415
+ while ((match = importRegex.exec(content)) !== null) {
416
+ lastImportMatch = match;
417
+ }
418
+
419
+ const importLine = 'import tailwindcss from "@tailwindcss/vite";';
420
+ if (lastImportMatch) {
421
+ const insertPos =
422
+ lastImportMatch.index + lastImportMatch[0].length;
423
+ content =
424
+ content.slice(0, insertPos) +
425
+ "\n" +
426
+ importLine +
427
+ content.slice(insertPos);
428
+ } else {
429
+ content = importLine + "\n" + content;
430
+ }
431
+ modified = true;
432
+ log.success("Added tailwindcss import");
433
+ }
434
+
435
+ // Add plugin
436
+ if (viteNeedsPlugin) {
437
+ const pluginsMatch = content.match(/plugins\s*:\s*\[/);
438
+ if (pluginsMatch) {
439
+ const insertPos = pluginsMatch.index + pluginsMatch[0].length;
440
+ content =
441
+ content.slice(0, insertPos) +
442
+ "tailwindcss(), " +
443
+ content.slice(insertPos);
444
+ modified = true;
445
+ log.success("Added tailwindcss() to plugins");
446
+ } else {
447
+ log.error(
448
+ "Could not find plugins array — please add tailwindcss() manually",
449
+ );
450
+ }
451
+ }
452
+
453
+ if (modified) {
454
+ writeFileSync(viteConfigPath, content, "utf8");
455
+ }
456
+ } else {
457
+ log.skip("Skipped — remember to add tailwindcss to your vite config");
458
+ }
459
+ } else {
460
+ log.skip("Vite config already has tailwindcss configured");
461
+ }
462
+ stepNum++;
463
+
464
+ // =======================================================================
465
+ // STEP: Set up CSS
466
+ // =======================================================================
467
+ log.step(stepNum, TOTAL_STEPS, "Set up CSS");
468
+ log.info("Orizon needs 3 lines at the top of your main CSS file:");
469
+ log.detail(pc.cyan('@import "tailwindcss";'));
470
+ log.detail(pc.cyan('@import "orizon/preset.css";'));
471
+ log.detail(pc.cyan('@source "../node_modules/orizon/dist";'));
472
+ await sleep(300);
216
473
 
217
474
  const cssSearchOrder = [
218
475
  "src/index.css",
@@ -230,28 +487,152 @@ export async function init() {
230
487
  const full = join(cwd, candidate);
231
488
  if (existsSync(full)) {
232
489
  cssPath = full;
490
+ log.info(`Found: ${pc.bold(candidate)}`);
233
491
  break;
234
492
  }
235
493
  }
236
494
 
237
495
  if (!cssPath) {
238
- // Create src/index.css
239
- const srcDir = join(cwd, "src");
240
- if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
241
- cssPath = join(srcDir, "index.css");
242
- cssCreated = true;
243
- log.info("Creating src/index.css");
496
+ log.info("No existing CSS file found.");
497
+ const proceed = scaffolded ? true : await ask("Create src/index.css?");
498
+ if (proceed) {
499
+ const srcDir = join(cwd, "src");
500
+ if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
501
+ cssPath = join(srcDir, "index.css");
502
+ cssCreated = true;
503
+ } else {
504
+ log.skip("Skipped — you'll need to add the CSS imports manually");
505
+ }
506
+ }
507
+
508
+ if (cssPath) {
509
+ const currentCSS = cssCreated ? "" : readFileSync(cssPath, "utf8");
510
+ const requiredLines = [
511
+ '@import "tailwindcss";',
512
+ '@import "orizon/preset.css";',
513
+ '@source "../node_modules/orizon/dist";',
514
+ ];
515
+ const linesToAdd = requiredLines.filter((l) => !currentCSS.includes(l));
516
+
517
+ if (linesToAdd.length > 0) {
518
+ log.info(`Lines to add to ${pc.bold(cssPath.split(/[\\/]/).pop())}:`);
519
+ for (const line of linesToAdd) {
520
+ log.detail(pc.green("+ ") + pc.cyan(line));
521
+ }
522
+
523
+ const proceed = scaffolded ? true : await ask("Add these lines?");
524
+ if (proceed) {
525
+ let content = cssCreated ? "" : readFileSync(cssPath, "utf8");
526
+ content = linesToAdd.join("\n") + (content ? "\n" + content : "\n");
527
+ writeFileSync(cssPath, content, "utf8");
528
+ log.success(
529
+ `Added ${linesToAdd.length} line${linesToAdd.length > 1 ? "s" : ""} to ${cssPath.split(/[\\/]/).pop()}`,
530
+ );
531
+ } else {
532
+ log.skip("Skipped CSS changes");
533
+ }
534
+ } else {
535
+ log.skip("CSS file already has all required imports");
536
+ }
537
+ }
538
+ stepNum++;
539
+
540
+ // =======================================================================
541
+ // STEP: Link stylesheet in index.html
542
+ // =======================================================================
543
+ log.step(stepNum, TOTAL_STEPS, "Link stylesheet in HTML");
544
+ log.info(
545
+ "Per Tailwind CSS docs, the stylesheet should be linked in index.html.",
546
+ );
547
+ await sleep(300);
548
+
549
+ const htmlPath = join(cwd, "index.html");
550
+
551
+ if (existsSync(htmlPath) && cssPath) {
552
+ const cssRelFromRoot = "/" + relative(cwd, cssPath).replace(/\\/g, "/");
553
+ const htmlContent = readFileSync(htmlPath, "utf8");
554
+
555
+ // Check if already linked (ignoring commented-out versions)
556
+ const commentPattern = /<!--[\s\S]*?-->/g;
557
+ const withoutComments = htmlContent.replace(commentPattern, "");
558
+ const alreadyLinked = withoutComments.includes(`href="${cssRelFromRoot}"`);
559
+
560
+ if (!alreadyLinked) {
561
+ const linkTag = `<link rel="stylesheet" href="${cssRelFromRoot}">`;
562
+ log.info(`Add to ${pc.bold("index.html")} <head>:`);
563
+ log.detail(pc.green("+ ") + pc.cyan(linkTag));
564
+
565
+ if (
566
+ htmlContent.includes(`<!--`) &&
567
+ htmlContent.includes(cssRelFromRoot)
568
+ ) {
569
+ log.info(
570
+ pc.dim("Note: found a commented-out link — it will be replaced."),
571
+ );
572
+ }
573
+
574
+ const proceed = scaffolded
575
+ ? true
576
+ : await ask("Add stylesheet link to index.html?");
577
+ if (proceed) {
578
+ let content = htmlContent;
579
+
580
+ // Remove commented-out link if present
581
+ const commentedLinkRegex = new RegExp(
582
+ `\\s*<!--\\s*<link[^>]*href="${cssRelFromRoot.replace(/\//g, "\\/")}"[^>]*>\\s*-->`,
583
+ "g",
584
+ );
585
+ content = content.replace(commentedLinkRegex, "");
586
+
587
+ // Insert before </head>
588
+ const headCloseIndex = content.indexOf("</head>");
589
+ if (headCloseIndex !== -1) {
590
+ const indent = " ";
591
+ content =
592
+ content.slice(0, headCloseIndex) +
593
+ `${indent}<link rel="stylesheet" href="${cssRelFromRoot}">\n ` +
594
+ content.slice(headCloseIndex);
595
+ writeFileSync(htmlPath, content, "utf8");
596
+ log.success("Added stylesheet link to index.html");
597
+ } else {
598
+ log.error(
599
+ "Could not find </head> tag — please add the link manually",
600
+ );
601
+ }
602
+ } else {
603
+ log.skip("Skipped — make sure your CSS file is imported somewhere");
604
+ }
605
+ } else {
606
+ log.skip("Stylesheet already linked in index.html");
607
+ }
608
+ } else if (!existsSync(htmlPath)) {
609
+ log.skip("No index.html found — skipping");
244
610
  }
245
611
 
246
- patchCSS(cssPath, cssCreated);
612
+ // =======================================================================
613
+ // Done!
614
+ // =======================================================================
615
+ console.log();
616
+ log.divider();
617
+ console.log();
618
+ console.log(pc.green(pc.bold(" ✓ Orizon setup complete!")));
247
619
  console.log();
248
620
 
249
- // -----------------------------------------------------------------------
250
- // Step 5: Done!
251
- // -----------------------------------------------------------------------
252
- console.log(pc.green(pc.bold(" Orizon is ready!")));
621
+ if (scaffolded) {
622
+ console.log(" Get started:");
623
+ console.log(pc.cyan(` cd ${projectName}`));
624
+ console.log(pc.cyan(" npm run dev"));
625
+ console.log();
626
+ }
627
+
628
+ console.log(" Import components:");
629
+ console.log(
630
+ pc.cyan(' import { Button, Form, Input, Select } from "orizon";'),
631
+ );
253
632
  console.log();
254
- console.log(" You can now import components:");
255
- console.log(pc.cyan(' import { Button } from "orizon";'));
633
+ console.log(" Example with Form + Zod:");
634
+ console.log(
635
+ pc.dim(" const [form] = Form.useForm({ schema: myZodSchema });"),
636
+ );
256
637
  console.log();
257
638
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizon",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Ant Design API on shadcn/ui primitives — 68 components for React with Tailwind CSS",
5
5
  "type": "module",
6
6
  "license": "MIT",