orizon 0.1.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 (3) hide show
  1. package/bin/cli.mjs +52 -0
  2. package/bin/init.mjs +638 -0
  3. package/package.json +8 -2
package/bin/cli.mjs ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(
9
+ readFileSync(resolve(__dirname, "..", "package.json"), "utf8"),
10
+ );
11
+
12
+ const [command] = process.argv.slice(2);
13
+
14
+ function printHelp() {
15
+ console.log(`
16
+ orizon v${pkg.version}
17
+
18
+ Usage:
19
+ npx orizon <command>
20
+
21
+ Commands:
22
+ init Set up Orizon in your React + Vite project
23
+
24
+ Options:
25
+ --version Show version number
26
+ --help Show this help message
27
+ `);
28
+ }
29
+
30
+ switch (command) {
31
+ case "init": {
32
+ const { init } = await import("./init.mjs");
33
+ await init();
34
+ break;
35
+ }
36
+
37
+ case "--version":
38
+ case "-v":
39
+ console.log(pkg.version);
40
+ break;
41
+
42
+ case "--help":
43
+ case "-h":
44
+ case undefined:
45
+ printHelp();
46
+ break;
47
+
48
+ default:
49
+ console.error(`Unknown command: ${command}\n`);
50
+ printHelp();
51
+ process.exit(1);
52
+ }
package/bin/init.mjs ADDED
@@ -0,0 +1,638 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
+ import { join, relative, basename } from "node:path";
5
+ import { execSync } from "node:child_process";
6
+ import { createInterface } from "node:readline";
7
+ import pc from "picocolors";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Logging helpers
11
+ // ---------------------------------------------------------------------------
12
+ const log = {
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))),
21
+ };
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
+
67
+ // ---------------------------------------------------------------------------
68
+ // Detect package manager
69
+ // ---------------------------------------------------------------------------
70
+ function detectPM(cwd) {
71
+ if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
72
+ return { name: "bun", install: "bun add -D", installProd: "bun add", create: "bunx", exec: "bunx" };
73
+ if (existsSync(join(cwd, "pnpm-lock.yaml")))
74
+ return { name: "pnpm", install: "pnpm add -D", installProd: "pnpm add", create: "pnpm create", exec: "pnpm dlx" };
75
+ if (existsSync(join(cwd, "yarn.lock")))
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" };
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Check if a package is already installed (deps or devDeps)
82
+ // ---------------------------------------------------------------------------
83
+ function isInstalled(pkg, pkgJson) {
84
+ const deps = pkgJson.dependencies || {};
85
+ const devDeps = pkgJson.devDependencies || {};
86
+ return Boolean(deps[pkg] || devDeps[pkg]);
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Sanitize project name for npm
91
+ // ---------------------------------------------------------------------------
92
+ function sanitizeName(name) {
93
+ return name
94
+ .toLowerCase()
95
+ .replace(/\s+/g, "-")
96
+ .replace(/[^a-z0-9\-_.]/g, "")
97
+ .replace(/^[.\-_]+/, "");
98
+ }
99
+
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";
106
+
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);
114
+ }
115
+
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);
125
+ }
126
+ }
127
+
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);
146
+ }
147
+
148
+ await sleep(300);
149
+
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);
162
+ }
163
+
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);
178
+ }
179
+
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;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Main init function
197
+ // ---------------------------------------------------------------------------
198
+ export async function init() {
199
+ let cwd = process.cwd();
200
+ let scaffolded = false;
201
+ let projectName = "";
202
+
203
+ console.log();
204
+ console.log(
205
+ pc.bold(pc.cyan(" ⬡ Orizon")) + pc.dim(" — project setup wizard"),
206
+ );
207
+ console.log();
208
+ log.divider();
209
+
210
+ // -----------------------------------------------------------------------
211
+ // Pre-flight: Detect project or scaffold a new one
212
+ // -----------------------------------------------------------------------
213
+ const pkgPath = join(cwd, "package.json");
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.");
224
+ process.exit(1);
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
+ }
237
+
238
+ const result = await scaffoldProject(cwd);
239
+ cwd = result.projectDir;
240
+ projectName = result.projectName;
241
+ scaffolded = true;
242
+
243
+ console.log();
244
+ log.divider();
245
+ console.log();
246
+ log.info("Project created! Now configuring Orizon...");
247
+ }
248
+
249
+ // -----------------------------------------------------------------------
250
+ // From here on, we're in a valid Vite project (either existing or new)
251
+ // -----------------------------------------------------------------------
252
+ const viteConfigPath = findViteConfig(cwd);
253
+ const pm = detectPM(cwd);
254
+ const currentPkgJson = JSON.parse(
255
+ readFileSync(join(cwd, "package.json"), "utf8"),
256
+ );
257
+
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");
320
+ }
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
+ ];
337
+
338
+ const formMissing = formDeps.filter(
339
+ (d) => !isInstalled(d.pkg, currentPkgJson),
340
+ );
341
+ const formAlreadyInstalled = formDeps.filter((d) =>
342
+ isInstalled(d.pkg, currentPkgJson),
343
+ );
344
+
345
+ for (const d of formAlreadyInstalled) {
346
+ log.skip(`${d.pkg} — already installed`);
347
+ }
348
+
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);
473
+
474
+ const cssSearchOrder = [
475
+ "src/index.css",
476
+ "src/main.css",
477
+ "src/styles.css",
478
+ "src/globals.css",
479
+ "src/app.css",
480
+ "src/App.css",
481
+ ];
482
+
483
+ let cssPath = null;
484
+ let cssCreated = false;
485
+
486
+ for (const candidate of cssSearchOrder) {
487
+ const full = join(cwd, candidate);
488
+ if (existsSync(full)) {
489
+ cssPath = full;
490
+ log.info(`Found: ${pc.bold(candidate)}`);
491
+ break;
492
+ }
493
+ }
494
+
495
+ if (!cssPath) {
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");
610
+ }
611
+
612
+ // =======================================================================
613
+ // Done!
614
+ // =======================================================================
615
+ console.log();
616
+ log.divider();
617
+ console.log();
618
+ console.log(pc.green(pc.bold(" ✓ Orizon setup complete!")));
619
+ console.log();
620
+
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
+ );
632
+ console.log();
633
+ console.log(" Example with Form + Zod:");
634
+ console.log(
635
+ pc.dim(" const [form] = Form.useForm({ schema: myZodSchema });"),
636
+ );
637
+ console.log();
638
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizon",
3
- "version": "0.1.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",
@@ -17,6 +17,9 @@
17
17
  "typescript",
18
18
  "accessible"
19
19
  ],
20
+ "bin": {
21
+ "orizon": "./bin/cli.mjs"
22
+ },
20
23
  "sideEffects": false,
21
24
  "main": "./dist/orizon.cjs.js",
22
25
  "module": "./dist/orizon.es.js",
@@ -31,6 +34,7 @@
31
34
  },
32
35
  "files": [
33
36
  "dist",
37
+ "bin",
34
38
  "README.md",
35
39
  "LICENSE"
36
40
  ],
@@ -58,7 +62,9 @@
58
62
  "optional": true
59
63
  }
60
64
  },
61
- "dependencies": {},
65
+ "dependencies": {
66
+ "picocolors": "^1.1.1"
67
+ },
62
68
  "devDependencies": {
63
69
  "@base-ui/react": "^1.2.0",
64
70
  "@eslint/js": "^9.39.1",