@sarthak_krishak/inkforge 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/Readme.md +106 -141
  2. package/cli/index.js +309 -135
  3. package/package.json +1 -1
package/Readme.md CHANGED
@@ -1,189 +1,154 @@
1
- # InkForge
1
+ # @sarthak_krishak/inkforge
2
2
 
3
- > **Beautiful, customizable terminal UI components — installed in one command, owned forever.**
3
+ > **Beautiful terminal UI components — ready in 2 commands.**
4
4
 
5
- InkForge is a copy-paste component library for building Terminal UIs with [React](https://react.dev) and [Ink](https://github.com/vadimdemedes/ink). Think of it as **shadcn/ui for your terminal** — instead of installing a locked npm dependency, you get the actual source code dropped into your project. You own it. You can read it, edit it, delete it.
6
-
7
- ```bash
8
- npx inkforge add spinner
9
- npx inkforge add progressbar
10
- npx inkforge add spinner progressbar
11
- ```
5
+ InkForge is a copy-paste component library for building Terminal UIs with React and Ink.
6
+ Think of it as **shadcn/ui for your terminal** — you get the actual source code dropped
7
+ into your project. You own it. Edit it, delete it, do whatever you want with it.
12
8
 
13
9
  ---
14
10
 
15
- ## Why InkForge?
16
-
17
- Modern AI coding tools (Claude Code, Aider, OpenCode) live in the terminal — but building beautiful terminal UIs from scratch is painful. Every developer reinvents spinners, progress bars, and diff viewers from raw ANSI escape codes.
11
+ ## Setup — 2 commands, then you're done
18
12
 
19
- InkForge solves this with a simple philosophy:
13
+ ### 1. Install
20
14
 
21
- | Principle | What it means |
22
- |---|---|
23
- | **Own your code** | `npx inkforge add spinner` drops the actual `.jsx` file into your project — no runtime dependency |
24
- | **Copy-paste first** | Every component is plain, readable JSX. No magic. No black boxes. |
25
- | **AI-native by default** | Built for the components AI coding agents actually need |
26
- | **Terminal-aware** | Designed for ANSI constraints and tested on real terminals |
15
+ Create an empty folder, open a terminal inside it, and run:
27
16
 
28
- ---
17
+ \`\`\`bash
18
+ npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
19
+ \`\`\`
29
20
 
30
- ## Installation
21
+ ### 2. Init
31
22
 
32
- InkForge has no runtime dependency. You use the CLI once to copy components into your project, then the CLI is no longer needed.
23
+ \`\`\`bash
24
+ npx inkforge init
25
+ \`\`\`
33
26
 
34
- Your project needs these peer dependencies:
27
+ This automatically creates everything you need:
35
28
 
36
- ```bash
37
- npm install react ink
38
- ```
29
+ | File / Folder | What it is |
30
+ |---|---|
31
+ | `package.json` | Created with `type: module` and a `start` script |
32
+ | `vite.config.js` | Vite config with React plugin |
33
+ | `app.jsx` | A working starter app using both components |
34
+ | `src/components/inkforge/Spinner/` | Spinner source — yours to edit |
35
+ | `src/components/inkforge/ProgressBar/` | ProgressBar source — yours to edit |
36
+ | `src/components/inkforge/core/colors.js` | Theme color definitions |
39
37
 
40
- ---
38
+ ### 3. Run
41
39
 
42
- ## Usage
40
+ \`\`\`bash
41
+ npm start
42
+ \`\`\`
43
43
 
44
- ### Interactive mode (recommended)
44
+ You'll see animated spinners and a filling progress bar immediately. Done.
45
45
 
46
- Run with no arguments to get an interactive component selector:
46
+ ---
47
47
 
48
- ```bash
49
- npx inkforge add
50
- ```
48
+ ## Your project structure after init
49
+
50
+ \`\`\`
51
+ my-folder/
52
+ src/
53
+ components/
54
+ inkforge/
55
+ Spinner/
56
+ index.jsx ← edit this freely
57
+ ProgressBar/
58
+ index.jsx ← edit this freely
59
+ core/
60
+ colors.js
61
+ app.jsx ← your app starts here
62
+ vite.config.js
63
+ package.json
64
+ \`\`\`
51
65
 
52
- Use `↑↓` to move, `space` to select, `enter` to confirm.
66
+ ---
53
67
 
54
- ### Direct mode
68
+ ## Using the components in your own app
55
69
 
56
- Add components by name:
70
+ Open `app.jsx` and build from there:
57
71
 
58
- ```bash
59
- npx inkforge add spinner
60
- npx inkforge add progressbar
61
- npx inkforge add spinner progressbar # multiple at once
62
- ```
72
+ \`\`\`jsx
73
+ import React from 'react';
74
+ import { render, Box, Text } from 'ink';
75
+ import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
76
+ import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
63
77
 
64
- ### List all components
78
+ function App() {
79
+ return (
80
+ <Box flexDirection="column" padding={1} gap={1}>
81
+ <Text bold color="cyan">My CLI App</Text>
82
+ <Spinner variant="dots" label="Loading..." />
83
+ <ProgressBar value={75} label="Progress" />
84
+ </Box>
85
+ );
86
+ }
65
87
 
66
- ```bash
67
- npx inkforge list
68
- ```
88
+ render(<App />);
89
+ \`\`\`
69
90
 
70
91
  ---
71
92
 
72
- ## Components
93
+ ## Spinner
73
94
 
74
- ### `Spinner`
95
+ \`\`\`jsx
96
+ <Spinner variant="dots" label="Fetching..." />
97
+ <Spinner variant="bounce" label="Processing..." theme="cyberpunk" />
98
+ <Spinner variant="arc" label="Compiling..." />
99
+ <Spinner variant="line" label="Connecting..." />
100
+ <Spinner variant="simple" label="Waiting..." />
75
101
 
76
- Animated loading indicator with 5 variants and theme support.
77
-
78
- ```jsx
79
- import { Spinner } from './components/inkforge/Spinner';
80
-
81
- // Basic
82
- <Spinner label="Loading..." />
83
-
84
- // Variants: dots | line | bounce | arc | simple
85
- <Spinner variant="bounce" label="Processing..." />
86
-
87
- // Themes: default | cyberpunk
88
- <Spinner variant="dots" theme="cyberpunk" label="Hacking..." />
89
-
90
- // Done state
102
+ // Done state switches to completion message
91
103
  <Spinner label="Deploying..." done={isDone} doneText="✓ Deployed!" />
92
- ```
93
-
94
- **Props**
104
+ \`\`\`
95
105
 
96
- | Prop | Type | Default | Description |
97
- |---|---|---|---|
98
- | `variant` | `string` | `'dots'` | Animation style: `dots`, `line`, `bounce`, `arc`, `simple` |
99
- | `label` | `string` | `'Loading...'` | Text shown next to spinner |
100
- | `color` | `string` | theme primary | Override spinner color (hex or named) |
101
- | `theme` | `string` | `'default'` | Color theme: `default`, `cyberpunk` |
102
- | `interval` | `number` | `80` | Animation speed in ms |
103
- | `done` | `boolean` | `false` | Switch to completion state |
104
- | `doneText` | `string` | `'✓ Done'` | Text shown when `done` is true |
106
+ | Prop | Default | Options |
107
+ |---|---|---|
108
+ | `variant` | `dots` | `dots` `line` `bounce` `arc` `simple` |
109
+ | `label` | `Loading...` | any string |
110
+ | `theme` | `default` | `default` `cyberpunk` |
111
+ | `interval` | `120` | ms number |
112
+ | `done` | `false` | boolean |
113
+ | `doneText` | `✓ Done` | any string |
105
114
 
106
115
  ---
107
116
 
108
- ### `ProgressBar`
109
-
110
- Fillable progress bar with 3 visual variants.
117
+ ## ProgressBar
111
118
 
112
- ```jsx
113
- import { ProgressBar } from './components/inkforge/ProgressBar';
114
-
115
- // Basic
119
+ \`\`\`jsx
116
120
  <ProgressBar value={60} label="Build" />
117
-
118
- // Show raw value instead of percent
119
121
  <ProgressBar value={45} total={200} label="Files" showValue />
120
-
121
- // Variants: default | thin | block
122
122
  <ProgressBar value={progress} variant="thin" label="Upload" />
123
- <ProgressBar value={progress} variant="block" label="Memory" />
124
-
125
- // Custom color
126
- <ProgressBar value={progress} color="#E5C07B" label="CPU" />
127
- ```
128
-
129
- **Props**
130
-
131
- | Prop | Type | Default | Description |
132
- |---|---|---|---|
133
- | `value` | `number` | `0` | Current progress value |
134
- | `total` | `number` | `100` | Maximum value |
135
- | `width` | `number` | `30` | Bar width in characters |
136
- | `label` | `string` | `''` | Label shown before the bar |
137
- | `showPercent` | `boolean` | `true` | Show `%` on the right |
138
- | `showValue` | `boolean` | `false` | Show `value/total` instead |
139
- | `color` | `string` | theme success | Fill color |
140
- | `bgColor` | `string` | theme muted | Empty bar color |
141
- | `theme` | `string` | `'default'` | Color theme: `default`, `cyberpunk` |
142
- | `variant` | `string` | `'default'` | Bar style: `default`, `thin`, `block` |
143
-
144
- ---
145
-
146
- ## Themes
123
+ <ProgressBar value={progress} variant="block" label="Memory" color="#E5C07B" />
124
+ <ProgressBar value={progress} theme="cyberpunk" label="Hack" />
125
+ \`\`\`
147
126
 
148
- InkForge ships with two pre-built themes. Pass `theme` to any component:
149
-
150
- | Theme | Description | Best for |
127
+ | Prop | Default | Options |
151
128
  |---|---|---|
152
- | `default` | Clean, professional, blue accent | Production tools |
153
- | `cyberpunk` | High contrast, neon cyan/magenta | Dev tools, personal projects |
129
+ | `value` | `0` | number |
130
+ | `total` | `100` | number |
131
+ | `width` | `30` | character count |
132
+ | `label` | `''` | any string |
133
+ | `showPercent` | `true` | boolean |
134
+ | `showValue` | `false` | boolean |
135
+ | `variant` | `default` | `default` `thin` `block` |
136
+ | `theme` | `default` | `default` `cyberpunk` |
137
+ | `color` | theme color | hex string |
154
138
 
155
139
  ---
156
140
 
157
- ## Roadmap
141
+ ## Add more components later
158
142
 
159
- - [x] Spinner
160
- - [x] ProgressBar
161
- - [ ] DiffViewer AI-native code diff display
162
- - [ ] StreamingOutput — Token-by-token streaming display
163
- - [ ] PromptInput Input with history and autocomplete
164
- - [ ] Select / MultiSelect — Keyboard-navigable menus
165
- - [ ] Table — Structured data display
166
- - [ ] StatusBar — Footer with agent state
167
-
168
- ---
169
-
170
- ## Contributing
171
-
172
- Contributions are welcome. Open an issue first to discuss what you'd like to add.
173
-
174
- ```bash
175
- git clone https://github.com/yourusername/inkforge
176
- cd inkforge
177
- npm install
178
- npm run demo # See components in action
179
- ```
143
+ \`\`\`bash
144
+ npx inkforge add spinner # add just spinner
145
+ npx inkforge add progressbar # add just progressbar
146
+ npx inkforge add # interactive selector
147
+ npx inkforge list # see all available
148
+ \`\`\`
180
149
 
181
150
  ---
182
151
 
183
152
  ## License
184
153
 
185
- MIT — see [LICENSE](./LICENSE)
186
-
187
- ---
188
-
189
- *Built for the era of AI coding agents. Inspired by [shadcn/ui](https://ui.shadcn.com).*
154
+ MIT — [Sarthak](https://github.com/yourusername)
package/cli/index.js CHANGED
@@ -1,182 +1,350 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // cli/index.js inkforge CLI
4
- // Usage: npx inkforge add <component>
5
- // npx inkforge list
6
-
7
- import { program } from 'commander';
8
- import fs from 'fs';
9
- import path from 'path';
10
- import { fileURLToPath } from 'url';
11
- import readline from 'readline';
3
+ import { program } from "commander";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import readline from "readline";
12
8
 
13
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
10
 
15
- // ─── Color helpers (no chalk dependency needed for CLI) ───────────────────────
16
11
  const c = {
17
- cyan: s => `\x1b[36m${s}\x1b[0m`,
18
- green: s => `\x1b[32m${s}\x1b[0m`,
19
- yellow: s => `\x1b[33m${s}\x1b[0m`,
20
- red: s => `\x1b[31m${s}\x1b[0m`,
21
- bold: s => `\x1b[1m${s}\x1b[0m`,
22
- dim: s => `\x1b[2m${s}\x1b[0m`,
23
- reset: s => `\x1b[0m${s}\x1b[0m`,
12
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
13
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
14
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
15
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
16
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
17
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
24
18
  };
25
19
 
26
- // ─── Component registry ───────────────────────────────────────────────────────
27
20
  const REGISTRY = {
28
21
  spinner: {
29
- name: 'Spinner',
30
- description: 'Animated loading spinner with 5 variants',
31
- file: 'Spinner/index.jsx',
32
- dir: 'Spinner',
33
- usage: "<Spinner variant=\"dots\" label=\"Loading...\" />",
22
+ name: "Spinner",
23
+ description: "Animated loading spinner with 5 variants",
24
+ file: "Spinner/index.jsx",
25
+ dir: "Spinner",
26
+ usage: '<Spinner variant="dots" label="Loading..." />',
34
27
  },
35
28
  progressbar: {
36
- name: 'ProgressBar',
37
- description: 'Fillable progress bar with 3 variants',
38
- file: 'ProgressBar/index.jsx',
39
- dir: 'ProgressBar',
40
- usage: "<ProgressBar value={60} label=\"Build\" />",
29
+ name: "ProgressBar",
30
+ description: "Fillable progress bar with 3 variants",
31
+ file: "ProgressBar/index.jsx",
32
+ dir: "ProgressBar",
33
+ usage: '<ProgressBar value={60} label="Build" />',
41
34
  },
42
35
  };
43
36
 
44
- // ─── Helpers ──────────────────────────────────────────────────────────────────
45
- function getComponentsDir() {
37
+ // ── Paths ─────────────────────────────────────────────────────────────────────
38
+ function getDestDir() {
46
39
  const cwd = process.cwd();
47
- // Try src/components first, then components/
48
- const candidates = [
49
- path.join(cwd, 'src', 'components', 'inkforge'),
50
- path.join(cwd, 'components', 'inkforge'),
51
- ];
52
- // Return the first parent that exists, defaulting to src/components/inkforge
53
- if (fs.existsSync(path.join(cwd, 'src'))) return candidates[0];
54
- return candidates[1];
40
+ return fs.existsSync(path.join(cwd, "src"))
41
+ ? path.join(cwd, "src", "components", "inkforge")
42
+ : path.join(cwd, "components", "inkforge");
55
43
  }
56
44
 
57
45
  function getTemplatesDir() {
58
- return path.join(__dirname, '..', 'src', 'components');
46
+ return path.join(__dirname, "..", "src", "components");
59
47
  }
60
48
 
61
- function ensureDir(dirPath) {
62
- if (!fs.existsSync(dirPath)) {
63
- fs.mkdirSync(dirPath, { recursive: true });
64
- }
49
+ function ensureDir(p) {
50
+ if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
65
51
  }
66
52
 
67
- function copyComponent(key) {
68
- const entry = REGISTRY[key];
69
- const srcDir = getTemplatesDir();
70
- const destDir = getComponentsDir();
71
- const srcFile = path.join(srcDir, entry.file);
53
+ // ── Copy one component ────────────────────────────────────────────────────────
54
+ function copyComponent(key, silent = false) {
55
+ const entry = REGISTRY[key];
56
+ const destDir = getDestDir();
57
+ const srcFile = path.join(getTemplatesDir(), entry.file);
72
58
  const destFolder = path.join(destDir, entry.dir);
73
- const destFile = path.join(destFolder, 'index.jsx');
59
+ const destFile = path.join(destFolder, "index.jsx");
74
60
 
75
- // Check template exists (guards against broken installs)
76
61
  if (!fs.existsSync(srcFile)) {
77
- console.log(c.red(`✗ Template not found for ${entry.name}. Try reinstalling inkforge.`));
62
+ console.log(c.red(`✗ Template not found for ${entry.name}.`));
78
63
  return false;
79
64
  }
80
65
 
81
- // Warn if already exists
82
- if (fs.existsSync(destFile)) {
83
- console.log(c.yellow(`⚠ ${entry.name} already exists at ${path.relative(process.cwd(), destFile)}`));
84
- console.log(c.dim(' Skipping. Delete the file first if you want a fresh copy.\n'));
66
+ if (fs.existsSync(destFile) && !silent) {
67
+ console.log(c.yellow(`⚠ ${entry.name} already exists — skipping.`));
85
68
  return false;
86
69
  }
87
70
 
88
71
  ensureDir(destFolder);
89
72
 
90
- // Read template and rewrite the import path to be relative to the user's project
91
- let src = fs.readFileSync(srcFile, 'utf8');
92
- // Replace the internal core path with a sensible relative path
73
+ let src = fs.readFileSync(srcFile, "utf8");
93
74
  src = src.replace(
94
75
  /from ['"].*?core\/colors\.js['"]/,
95
- `from '../core/colors.js'`
76
+ `from '../core/colors.js'`,
96
77
  );
78
+ fs.writeFileSync(destFile, src, "utf8");
97
79
 
98
- fs.writeFileSync(destFile, src, 'utf8');
99
-
100
- // Also copy core/colors.js if not present
101
- ensureCoreColors(destDir);
102
-
103
- console.log(c.green(`✓ Added ${c.bold(entry.name)}`));
104
- console.log(c.dim(` → ${path.relative(process.cwd(), destFile)}\n`));
105
- console.log(c.cyan(' Usage:'));
106
- console.log(c.dim(` import { ${entry.name} } from './${path.relative(process.cwd(), destFolder).replace(/\\/g, '/')}';`));
107
- console.log(c.dim(` ${entry.usage}\n`));
80
+ if (!silent) {
81
+ console.log(c.green(` ✓ ${entry.name}`));
82
+ }
108
83
  return true;
109
84
  }
110
85
 
111
- function ensureCoreColors(destDir) {
112
- const coreDir = path.join(destDir, 'core');
113
- const destFile = path.join(coreDir, 'colors.js');
86
+ // ── Copy core/colors.js ───────────────────────────────────────────────────────
87
+ function copyCore(destDir) {
88
+ const coreDir = path.join(destDir, "core");
89
+ const destFile = path.join(coreDir, "colors.js");
114
90
  if (fs.existsSync(destFile)) return;
115
91
 
116
- const srcFile = path.join(__dirname, '..', 'src', 'core', 'colors.js');
92
+ const srcFile = path.join(__dirname, "..", "src", "core", "colors.js");
117
93
  if (!fs.existsSync(srcFile)) return;
118
94
 
119
95
  ensureDir(coreDir);
120
96
  fs.copyFileSync(srcFile, destFile);
121
- console.log(c.dim(` → Also copied core/colors.js\n`));
122
97
  }
123
98
 
124
- // ─── Interactive selector ─────────────────────────────────────────────────────
99
+ // ── Write vite.config.js ──────────────────────────────────────────────────────
100
+ function writeViteConfig() {
101
+ const dest = path.join(process.cwd(), "vite.config.js");
102
+ if (fs.existsSync(dest)) return;
103
+ fs.writeFileSync(
104
+ dest,
105
+ `import { defineConfig } from 'vite';
106
+ import react from '@vitejs/plugin-react';
107
+ export default defineConfig({ plugins: [react()] });
108
+ `,
109
+ );
110
+ console.log(c.green(" ✓ vite.config.js"));
111
+ }
112
+
113
+ // ── Write starter app.jsx ─────────────────────────────────────────────────────
114
+ function writeAppJsx() {
115
+ const dest = path.join(process.cwd(), "app.jsx");
116
+ if (fs.existsSync(dest)) {
117
+ console.log(c.dim(" ↷ app.jsx already exists — skipping"));
118
+ return;
119
+ }
120
+ fs.writeFileSync(
121
+ dest,
122
+ `import React, { useState, useEffect } from 'react';
123
+ import { render, Box, Text } from 'ink';
124
+ import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
125
+ import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
126
+
127
+ function App() {
128
+ const [progress, setProgress] = useState(0);
129
+ const [done, setDone] = useState(false);
130
+
131
+ useEffect(() => {
132
+ const t = setInterval(() => {
133
+ setProgress(p => {
134
+ if (p >= 100) { clearInterval(t); setDone(true); return 100; }
135
+ return p + 5;
136
+ });
137
+ }, 150);
138
+ return () => clearInterval(t);
139
+ }, []);
140
+
141
+ return (
142
+ <Box flexDirection="column" padding={1} gap={1}>
143
+ <Text bold color="cyan">My CLI App</Text>
144
+ <Spinner label="Building..." done={done} doneText="✓ Done!" />
145
+ <ProgressBar value={progress} label="Progress" />
146
+ </Box>
147
+ );
148
+ }
149
+
150
+ render(<App />);
151
+ `,
152
+ );
153
+ console.log(c.green(" ✓ app.jsx (starter file)"));
154
+ }
155
+
156
+ // ── Patch package.json with start script ──────────────────────────────────────
157
+ function patchPackageJson() {
158
+ const pkgPath = path.join(process.cwd(), "package.json");
159
+ if (!fs.existsSync(pkgPath)) return;
160
+
161
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
162
+
163
+ let changed = false;
164
+
165
+ // Add "type": "module"
166
+ if (pkg.type !== "module") {
167
+ pkg.type = "module";
168
+ changed = true;
169
+ }
170
+
171
+ // Add start script
172
+ if (!pkg.scripts) pkg.scripts = {};
173
+ if (!pkg.scripts.start) {
174
+ pkg.scripts.start = "vite-node app.jsx";
175
+ changed = true;
176
+ }
177
+
178
+ if (changed) {
179
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
180
+ console.log(c.green(" ✓ package.json (added type:module + start script)"));
181
+ }
182
+ }
183
+
184
+ // ── INIT command ──────────────────────────────────────────────────────────────
185
+ function runInit() {
186
+ console.log(c.bold(c.cyan("\n InkForge — initializing your project\n")));
187
+
188
+ const cwd = process.cwd();
189
+ const destDir = getDestDir();
190
+ const pkgPath = path.join(cwd, "package.json");
191
+
192
+ // ── Step 1: Create package.json if it doesn't exist ──────────────────────
193
+ if (!fs.existsSync(pkgPath)) {
194
+ const folderName = path.basename(cwd);
195
+ const pkg = {
196
+ name: folderName,
197
+ version: "1.0.0",
198
+ type: "module",
199
+ scripts: {
200
+ start: "vite-node app.jsx",
201
+ },
202
+ };
203
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
204
+ console.log(c.green(" ✓ package.json created"));
205
+ } else {
206
+ // patch existing one
207
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
208
+ let changed = false;
209
+ if (pkg.type !== "module") {
210
+ pkg.type = "module";
211
+ changed = true;
212
+ }
213
+ if (!pkg.scripts) {
214
+ pkg.scripts = {};
215
+ }
216
+ if (!pkg.scripts.start) {
217
+ pkg.scripts.start = "vite-node app.jsx";
218
+ changed = true;
219
+ }
220
+ if (changed) {
221
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
222
+ console.log(c.green(" ✓ package.json updated"));
223
+ } else {
224
+ console.log(c.dim(" ↷ package.json already configured"));
225
+ }
226
+ }
227
+
228
+ // ── Step 2: Create vite.config.js ────────────────────────────────────────
229
+ const vitePath = path.join(cwd, "vite.config.js");
230
+ if (!fs.existsSync(vitePath)) {
231
+ fs.writeFileSync(
232
+ vitePath,
233
+ `import { defineConfig } from 'vite';
234
+ import react from '@vitejs/plugin-react';
235
+ export default defineConfig({ plugins: [react()] });
236
+ `,
237
+ );
238
+ console.log(c.green(" ✓ vite.config.js created"));
239
+ } else {
240
+ console.log(c.dim(" ↷ vite.config.js already exists"));
241
+ }
242
+
243
+ // ── Step 3: Copy all components ───────────────────────────────────────────
244
+ ensureDir(destDir);
245
+ Object.keys(REGISTRY).forEach((key) => copyComponent(key, true));
246
+ copyCore(destDir);
247
+ console.log(c.green(" ✓ Spinner component added"));
248
+ console.log(c.green(" ✓ ProgressBar component added"));
249
+ console.log(c.green(" ✓ core/colors.js added"));
250
+
251
+ // ── Step 4: Create app.jsx ────────────────────────────────────────────────
252
+ const appPath = path.join(cwd, "app.jsx");
253
+ if (!fs.existsSync(appPath)) {
254
+ fs.writeFileSync(
255
+ appPath,
256
+ `import React, { useState, useEffect } from 'react';
257
+ import { render, Box, Text } from 'ink';
258
+ import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
259
+ import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
260
+
261
+ function App() {
262
+ const [progress, setProgress] = useState(0);
263
+ const [done, setDone] = useState(false);
264
+
265
+ useEffect(() => {
266
+ const t = setInterval(() => {
267
+ setProgress(p => {
268
+ if (p >= 100) { clearInterval(t); setDone(true); return 100; }
269
+ return p + 5;
270
+ });
271
+ }, 150);
272
+ return () => clearInterval(t);
273
+ }, []);
274
+
275
+ return (
276
+ <Box flexDirection="column" padding={1} gap={1}>
277
+ <Text bold color="cyan">My CLI App</Text>
278
+ <Spinner label="Building..." done={done} doneText="✓ Build complete!" />
279
+ <Spinner label="Installing..." done={done} doneText="✓ Packages ready!" variant="bounce" theme="cyberpunk" />
280
+ <ProgressBar value={progress} label="Progress " />
281
+ <ProgressBar value={progress} label="Memory " variant="block" color="#E5C07B" />
282
+ </Box>
283
+ );
284
+ }
285
+
286
+ render(<App />);
287
+ `,
288
+ );
289
+ console.log(c.green(" ✓ app.jsx created"));
290
+ } else {
291
+ console.log(c.dim(" ↷ app.jsx already exists — skipping"));
292
+ }
293
+
294
+ // ── Done ──────────────────────────────────────────────────────────────────
295
+ console.log(c.bold(c.green("\n Everything is ready!\n")));
296
+ console.log(" Run your app:");
297
+ console.log(c.bold(c.cyan("\n npm start\n")));
298
+ }
299
+
300
+ // ── Interactive selector for `add` ────────────────────────────────────────────
125
301
  async function interactiveSelect() {
126
- const keys = Object.keys(REGISTRY);
127
- const entries = keys.map(k => REGISTRY[k]);
128
- let cursor = 0;
302
+ const keys = Object.keys(REGISTRY);
303
+ const entries = keys.map((k) => REGISTRY[k]);
304
+ let cursor = 0;
129
305
  const selected = new Set();
130
306
 
131
- // Hide cursor, enable raw mode
132
- process.stdout.write('\x1b[?25l');
307
+ process.stdout.write("\x1b[?25l");
133
308
  readline.emitKeypressEvents(process.stdin);
134
309
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
135
310
 
136
311
  function render() {
137
- // Clear previous lines
138
312
  process.stdout.write(`\x1b[${entries.length + 4}A\x1b[0J`);
139
- console.log(c.bold('\n Select components to add:\n'));
313
+ console.log(c.bold("\n Select components to add:\n"));
140
314
  entries.forEach((e, i) => {
141
- const checked = selected.has(keys[i]) ? c.green('') : c.dim('');
142
- const active = i === cursor ? c.cyan('') : ' ';
143
- const name = i === cursor ? c.bold(c.cyan(e.name)) : e.name;
144
- const desc = c.dim(e.description);
145
- console.log(` ${active}${checked} ${name.padEnd(18)}${desc}`);
315
+ const checked = selected.has(keys[i]) ? c.green("") : c.dim("");
316
+ const active = i === cursor ? c.cyan("") : " ";
317
+ const name = i === cursor ? c.bold(c.cyan(e.name)) : e.name;
318
+ console.log(
319
+ ` ${active}${checked} ${name.padEnd(18)}${c.dim(e.description)}`,
320
+ );
146
321
  });
147
- console.log(c.dim('\n ↑↓ move space select enter confirm q quit'));
322
+ console.log(c.dim("\n ↑↓ move space select enter confirm q quit"));
148
323
  }
149
324
 
150
- // Initial render — print blank lines first so clear works
151
- console.log('\n');
152
- entries.forEach(() => console.log(''));
153
- console.log('\n');
325
+ console.log("\n");
326
+ entries.forEach(() => console.log(""));
327
+ console.log("\n");
154
328
  render();
155
329
 
156
- return new Promise(resolve => {
157
- process.stdin.on('keypress', (str, key) => {
330
+ return new Promise((resolve) => {
331
+ process.stdin.on("keypress", (str, key) => {
158
332
  if (!key) return;
159
-
160
- if (key.name === 'up') cursor = (cursor - 1 + keys.length) % keys.length;
161
- if (key.name === 'down') cursor = (cursor + 1) % keys.length;
162
-
163
- if (key.name === 'space') {
164
- const k = keys[cursor];
165
- if (selected.has(k)) selected.delete(k);
166
- else selected.add(k);
333
+ if (key.name === "up") cursor = (cursor - 1 + keys.length) % keys.length;
334
+ if (key.name === "down") cursor = (cursor + 1) % keys.length;
335
+ if (key.name === "space") {
336
+ if (selected.has(keys[cursor])) selected.delete(keys[cursor]);
337
+ else selected.add(keys[cursor]);
167
338
  }
168
-
169
- if (key.name === 'return') {
339
+ if (key.name === "return") {
170
340
  cleanup();
171
341
  resolve([...selected]);
172
342
  }
173
-
174
- if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
343
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
175
344
  cleanup();
176
- console.log(c.dim('\n Cancelled.\n'));
345
+ console.log(c.dim("\n Cancelled.\n"));
177
346
  resolve([]);
178
347
  }
179
-
180
348
  render();
181
349
  });
182
350
  });
@@ -184,60 +352,66 @@ async function interactiveSelect() {
184
352
  function cleanup() {
185
353
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
186
354
  process.stdin.pause();
187
- process.stdout.write('\x1b[?25h'); // show cursor
355
+ process.stdout.write("\x1b[?25h");
188
356
  }
189
357
  }
190
358
 
191
- // ─── Commands ─────────────────────────────────────────────────────────────────
359
+ // ── Commands ──────────────────────────────────────────────────────────────────
360
+ program
361
+ .name("inkforge")
362
+ .description("InkForge — beautiful terminal UI components for React/Ink")
363
+ .version("0.1.0");
364
+
192
365
  program
193
- .name('inkforge')
194
- .description('InkForge — beautiful terminal UI components for React/Ink')
195
- .version('0.1.0');
366
+ .command("init")
367
+ .description(
368
+ "Set up InkForge in your project — run this once after installing",
369
+ )
370
+ .action(runInit);
196
371
 
197
- // inkforge list
198
372
  program
199
- .command('list')
200
- .description('List all available components')
373
+ .command("list")
374
+ .description("List all available components")
201
375
  .action(() => {
202
- console.log(c.bold('\n InkForge Components\n'));
376
+ console.log(c.bold("\n InkForge Components\n"));
203
377
  Object.entries(REGISTRY).forEach(([key, entry]) => {
204
- console.log(` ${c.cyan(entry.name.padEnd(16))} ${c.dim(entry.description)}`);
378
+ console.log(
379
+ ` ${c.cyan(entry.name.padEnd(16))} ${c.dim(entry.description)}`,
380
+ );
205
381
  console.log(c.dim(` npx inkforge add ${key}\n`));
206
382
  });
207
383
  });
208
384
 
209
- // inkforge add [component...]
210
385
  program
211
- .command('add [components...]')
212
- .description('Add one or more components to your project')
386
+ .command("add [components...]")
387
+ .description("Add a specific component to your project")
213
388
  .action(async (components) => {
214
- console.log(c.bold(c.cyan('\n InkForge\n')));
389
+ console.log(c.bold(c.cyan("\n InkForge\n")));
215
390
 
216
- // No args — show interactive selector
217
391
  if (!components || components.length === 0) {
218
392
  const chosen = await interactiveSelect();
219
393
  if (chosen.length === 0) return;
220
- console.log('');
221
- chosen.forEach(key => copyComponent(key));
394
+ console.log("");
395
+ chosen.forEach((key) => copyComponent(key));
396
+ copyCore(getDestDir());
222
397
  return;
223
398
  }
224
399
 
225
- // Args provided — add them directly
226
- let anyFailed = false;
227
400
  for (const name of components) {
228
- const key = name.toLowerCase().replace(/[^a-z]/g, '');
401
+ const key = name.toLowerCase().replace(/[^a-z]/g, "");
229
402
  if (!REGISTRY[key]) {
230
403
  console.log(c.red(`✗ Unknown component: "${name}"`));
231
- console.log(c.dim(` Run ${c.cyan('npx inkforge list')} to see available components.\n`));
232
- anyFailed = true;
404
+ console.log(
405
+ c.dim(` Run npx inkforge list to see available components.\n`),
406
+ );
233
407
  continue;
234
408
  }
235
409
  copyComponent(key);
236
410
  }
237
-
238
- if (!anyFailed) {
239
- console.log(c.green(c.bold(' All done! Components are yours to own and edit.\n')));
240
- }
411
+ copyCore(getDestDir());
412
+ console.log(
413
+ c.green(c.bold("\n Done! Components are yours to own and edit.\n")),
414
+ );
241
415
  });
242
416
 
243
- program.parse();
417
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarthak_krishak/inkforge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Beautiful terminal UI components for React/Ink — shadcn/ui for your terminal",
5
5
  "type": "module",
6
6
  "main": "src/index.jsx",