@sarthak_krishak/inkforge 0.2.1 → 0.3.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 +91 -94
  2. package/cli/index.js +198 -276
  3. package/package.json +1 -1
package/Readme.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![React](https://img.shields.io/badge/React-18%2B-61DAFB?style=flat-square&logo=react&logoColor=white)](https://react.dev)
10
10
  [![Ink](https://img.shields.io/badge/Ink-5%2B-000000?style=flat-square)](https://github.com/vadimdemedes/ink)
11
11
 
12
- **Beautiful terminal UI components for React/Ink — set up in 2 commands, owned forever.**
12
+ **Beautiful terminal UI components for React/Ink — scaffolded in one command, owned forever.**
13
13
 
14
14
  *shadcn/ui for your terminal. Built for the AI coding agent era.*
15
15
 
@@ -19,83 +19,62 @@
19
19
 
20
20
  ## What is InkForge?
21
21
 
22
- InkForge gives you polished, production-ready terminal components — spinners, progress bars, and more — that you **actually own**. Instead of a locked npm dependency you can't touch, InkForge copies the source code directly into your project. Read it. Edit it. Make it yours.
23
-
24
- ```bash
25
- npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
26
- npx inkforge init
27
- npm start
28
- ```
29
-
30
- That's the entire setup. Three commands. You're running.
22
+ InkForge gives you polished, production-ready terminal components — spinners, progress bars, and more — that you **actually own**. Instead of a locked npm dependency buried in `node_modules`, InkForge copies the source code directly into your project. Read it. Edit it. Make it yours.
31
23
 
32
24
  ---
33
25
 
34
26
  ## Quick Start
35
27
 
36
- ### Step 1 — Create a new folder and open a terminal inside it
28
+ ### Step 1 — Create a new empty folder and open a terminal inside it
37
29
 
38
30
  ```bash
39
31
  mkdir my-cli-app
40
32
  cd my-cli-app
41
33
  ```
42
34
 
43
- ### Step 2 — Install InkForge and its dependencies
44
-
45
- ```bash
46
- npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
47
- ```
48
-
49
- ### Step 3 — Run init
35
+ ### Step 2 — Scaffold the project
50
36
 
51
37
  ```bash
52
- npx inkforge init
38
+ npx @sarthak_krishak/inkforge init
53
39
  ```
54
40
 
55
- This one command automatically creates everything:
41
+ This creates everything your project needs:
56
42
 
57
- | Created | What it does |
43
+ | Created | Description |
58
44
  |---|---|
59
- | `package.json` | Configured with `type: module` and a `start` script |
45
+ | `package.json` | Configured with `type: module`, `start` script, and all dependencies listed |
60
46
  | `vite.config.js` | Vite + React plugin config |
61
- | `app.jsx` | A working starter app with live examples |
62
- | `src/components/inkforge/Spinner/` | Spinner source — yours to own and edit |
63
- | `src/components/inkforge/ProgressBar/` | ProgressBar source — yours to own and edit |
64
- | `src/components/inkforge/core/colors.js` | Theme color system |
47
+ | `app.jsx` | Starter app edit this to build your CLI |
65
48
 
66
- ### Step 4Start your app
49
+ ### Step 3Install dependencies
67
50
 
68
51
  ```bash
69
- npm start
52
+ npm install
70
53
  ```
71
54
 
72
- You'll see animated spinners and a filling progress bar in your terminal immediately.
55
+ ### Step 4 Add the components you want
73
56
 
74
- ---
57
+ ```bash
58
+ npx inkforge add spinner
59
+ npx inkforge add progressbar
60
+ ```
75
61
 
76
- ## Project structure after init
62
+ This copies the component source files directly into your project. You own them now.
77
63
 
64
+ ### Step 5 — Open `app.jsx`, import your components, and run
65
+
66
+ ```jsx
67
+ import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
68
+ import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
78
69
  ```
79
- my-cli-app/
80
- ├── src/
81
- │ └── components/
82
- │ └── inkforge/
83
- │ ├── Spinner/
84
- │ │ └── index.jsx ← edit freely
85
- │ ├── ProgressBar/
86
- │ │ └── index.jsx ← edit freely
87
- │ └── core/
88
- │ └── colors.js ← theme colors
89
- ├── app.jsx ← your app starts here
90
- ├── vite.config.js
91
- └── package.json
70
+
71
+ ```bash
72
+ npm start
92
73
  ```
93
74
 
94
75
  ---
95
76
 
96
- ## Using components in your app
97
-
98
- Open `app.jsx` and build from there. The import paths are set up automatically:
77
+ ## Full example `app.jsx`
99
78
 
100
79
  ```jsx
101
80
  import React, { useState, useEffect } from 'react';
@@ -120,8 +99,10 @@ function App() {
120
99
  return (
121
100
  <Box flexDirection="column" padding={1} gap={1}>
122
101
  <Text bold color="cyan">My CLI App</Text>
123
- <Spinner label="Building..." done={done} doneText="✓ Build complete!" />
124
- <ProgressBar value={progress} label="Progress" />
102
+ <Spinner label="Building..." done={done} doneText="✓ Build complete!" />
103
+ <Spinner label="Installing..." done={done} doneText="✓ Packages ready!" variant="bounce" theme="cyberpunk" />
104
+ <ProgressBar value={progress} label="Progress " />
105
+ <ProgressBar value={progress} label="Memory " variant="block" color="#E5C07B" />
125
106
  </Box>
126
107
  );
127
108
  }
@@ -131,6 +112,26 @@ render(<App />);
131
112
 
132
113
  ---
133
114
 
115
+ ## Project structure after setup
116
+
117
+ ```
118
+ my-cli-app/
119
+ ├── src/
120
+ │ └── components/
121
+ │ └── inkforge/
122
+ │ ├── Spinner/
123
+ │ │ └── index.jsx ← edit freely
124
+ │ ├── ProgressBar/
125
+ │ │ └── index.jsx ← edit freely
126
+ │ └── core/
127
+ │ └── colors.js ← theme colors
128
+ ├── app.jsx ← your app starts here
129
+ ├── vite.config.js
130
+ └── package.json
131
+ ```
132
+
133
+ ---
134
+
134
135
  ## Components
135
136
 
136
137
  ### Spinner
@@ -138,19 +139,14 @@ render(<App />);
138
139
  Animated loading indicator with 5 variants and a done state.
139
140
 
140
141
  ```jsx
141
- // Variants
142
142
  <Spinner variant="dots" label="Fetching data..." />
143
143
  <Spinner variant="bounce" label="Processing request..." theme="cyberpunk" />
144
144
  <Spinner variant="arc" label="Compiling..." />
145
145
  <Spinner variant="line" label="Connecting..." />
146
146
  <Spinner variant="simple" label="Waiting..." />
147
147
 
148
- // Done state — switches to a completion message automatically
149
- <Spinner
150
- label="Deploying to production..."
151
- done={isDone}
152
- doneText="✓ Deployed successfully!"
153
- />
148
+ // Switches to a completion message when done={true}
149
+ <Spinner label="Deploying..." done={isDone} doneText="✓ Deployed!" />
154
150
  ```
155
151
 
156
152
  #### Spinner Props
@@ -158,11 +154,11 @@ Animated loading indicator with 5 variants and a done state.
158
154
  | Prop | Type | Default | Description |
159
155
  |---|---|---|---|
160
156
  | `variant` | `string` | `'dots'` | Animation style: `dots` `line` `bounce` `arc` `simple` |
161
- | `label` | `string` | `'Loading...'` | Text displayed next to the spinner |
162
- | `color` | `string` | theme primary | Override spinner color (any hex value) |
157
+ | `label` | `string` | `'Loading...'` | Text shown next to the spinner |
158
+ | `color` | `string` | theme primary | Spinner color (any hex value) |
163
159
  | `theme` | `string` | `'default'` | Color theme: `'default'` or `'cyberpunk'` |
164
- | `interval` | `number` | `120` | Animation frame speed in milliseconds |
165
- | `done` | `boolean` | `false` | When `true`, switches to the done state |
160
+ | `interval` | `number` | `120` | Animation speed in milliseconds |
161
+ | `done` | `boolean` | `false` | When `true`, switches to done state |
166
162
  | `doneText` | `string` | `'✓ Done'` | Message shown when `done` is `true` |
167
163
 
168
164
  ---
@@ -172,22 +168,12 @@ Animated loading indicator with 5 variants and a done state.
172
168
  Fillable progress bar with 3 visual styles.
173
169
 
174
170
  ```jsx
175
- // Basic usage
176
171
  <ProgressBar value={75} label="Build" />
177
-
178
- // Show raw value instead of percentage
179
- <ProgressBar value={45} total={200} label="Files processed" showValue />
180
-
181
- // Visual variants
182
- <ProgressBar value={progress} variant="default" label="Download " />
183
- <ProgressBar value={progress} variant="thin" label="Upload " />
184
- <ProgressBar value={progress} variant="block" label="Memory " />
185
-
186
- // Custom color
187
- <ProgressBar value={progress} color="#E5C07B" label="CPU usage" />
188
-
189
- // Cyberpunk theme
190
- <ProgressBar value={progress} theme="cyberpunk" label="Hack " />
172
+ <ProgressBar value={45} total={200} label="Files" showValue />
173
+ <ProgressBar value={progress} variant="thin" label="Upload" />
174
+ <ProgressBar value={progress} variant="block" label="Memory" />
175
+ <ProgressBar value={progress} color="#E5C07B" label="CPU" />
176
+ <ProgressBar value={progress} theme="cyberpunk" label="Hack" />
191
177
  ```
192
178
 
193
179
  #### ProgressBar Props
@@ -196,7 +182,7 @@ Fillable progress bar with 3 visual styles.
196
182
  |---|---|---|---|
197
183
  | `value` | `number` | `0` | Current progress value |
198
184
  | `total` | `number` | `100` | Maximum value |
199
- | `width` | `number` | `30` | Bar width in terminal characters |
185
+ | `width` | `number` | `30` | Bar width in characters |
200
186
  | `label` | `string` | `''` | Label shown before the bar |
201
187
  | `showPercent` | `boolean` | `true` | Show percentage on the right |
202
188
  | `showValue` | `boolean` | `false` | Show `value/total` instead of percentage |
@@ -209,30 +195,28 @@ Fillable progress bar with 3 visual styles.
209
195
 
210
196
  ## Themes
211
197
 
212
- All components support two built-in themes via the `theme` prop:
213
-
214
- | Theme | Colors | Best for |
198
+ | Theme | Accent | Best for |
215
199
  |---|---|---|
216
- | `default` | Blue accent, green fill, gray empty | Production tools, professional CLIs |
217
- | `cyberpunk` | Neon cyan, bright green, dark muted | Dev tools, personal projects, AI agents |
200
+ | `default` | Blue / green | Production tools, professional CLIs |
201
+ | `cyberpunk` | Neon cyan / bright green | Dev tools, personal projects, AI agents |
218
202
 
219
203
  ---
220
204
 
221
- ## CLI Commands
205
+ ## CLI Reference
222
206
 
223
207
  ```bash
224
- # Set up a new project from scratch (run once after install)
225
- npx inkforge init
208
+ # Scaffold a new project in an empty folder (run once)
209
+ npx @sarthak_krishak/inkforge init
226
210
 
227
- # Add a specific component to an existing project
211
+ # Add components to your project
228
212
  npx inkforge add spinner
229
213
  npx inkforge add progressbar
230
214
  npx inkforge add spinner progressbar
231
215
 
232
- # Interactive component selector (arrow keys + space to pick)
216
+ # Interactive component picker (arrow keys + space)
233
217
  npx inkforge add
234
218
 
235
- # List all available components
219
+ # See all available components
236
220
  npx inkforge list
237
221
  ```
238
222
 
@@ -240,11 +224,11 @@ npx inkforge list
240
224
 
241
225
  ## Why own your components?
242
226
 
243
- With a standard npm package, the code lives locked inside `node_modules`. You can't edit it without forking the whole repo.
227
+ With a standard npm package, the code is locked inside `node_modules`. You can't change it without forking the entire repo.
244
228
 
245
- With InkForge, after running `npx inkforge add spinner`, the file lives at `src/components/inkforge/Spinner/index.jsx` — inside **your** project. Open it, change the animation frames, add a new variant, tweak the colors. No fork needed. No PR required. It's your code now.
229
+ With InkForge, after `npx inkforge add spinner`, the file is at `src/components/inkforge/Spinner/index.jsx` — inside **your** project. Change the animation frames. Add a new variant. Tweak the colors. No fork needed. No PR required. It's your code.
246
230
 
247
- This is the same philosophy that made [shadcn/ui](https://ui.shadcn.com) popular for web development. InkForge brings it to the terminal.
231
+ This is the same philosophy that made [shadcn/ui](https://ui.shadcn.com) the most popular component library for web React. InkForge brings it to the terminal.
248
232
 
249
233
  ---
250
234
 
@@ -252,16 +236,29 @@ This is the same philosophy that made [shadcn/ui](https://ui.shadcn.com) popular
252
236
 
253
237
  - [x] Spinner — 5 variants, done state, theme support
254
238
  - [x] ProgressBar — 3 variants, custom colors, theme support
255
- - [x] CLI installer — `init`, `add`, `list` commands
239
+ - [x] CLI — `init`, `add`, `list` commands
256
240
  - [ ] DiffViewer — git-style diff display for AI coding agents
257
- - [ ] StreamingOutput — token-by-token streaming display
241
+ - [ ] StreamingOutput — token-by-token AI response display
258
242
  - [ ] PromptInput — input with history and autocomplete
259
243
  - [ ] Select / MultiSelect — keyboard-navigable menus
260
244
  - [ ] Table — structured data display
261
- - [ ] StatusBar — footer with agent state and metrics
245
+ - [ ] StatusBar — footer with agent state
246
+
247
+ ---
248
+
249
+ ## Contributing
250
+
251
+ PRs and issues welcome. Open an issue first to discuss new components.
252
+
253
+ ```bash
254
+ git clone https://github.com/sarthak-krishak/inkforge
255
+ cd inkforge
256
+ npm install
257
+ npm run demo
258
+ ```
262
259
 
263
260
  ---
264
261
 
265
262
  ## License
266
263
 
267
- MIT © [Sarthak Krishak](https://github.com/SarthakKrishak)
264
+ MIT © [Sarthak Krishak](https://github.com/sarthak-krishak)
package/cli/index.js CHANGED
@@ -1,417 +1,339 @@
1
1
  #!/usr/bin/env node
2
2
 
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";
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';
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
 
11
+ // ── Color helpers ─────────────────────────────────────────────────────────────
11
12
  const c = {
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`,
13
+ cyan: s => `\x1b[36m${s}\x1b[0m`,
14
+ green: s => `\x1b[32m${s}\x1b[0m`,
15
+ yellow: s => `\x1b[33m${s}\x1b[0m`,
16
+ red: s => `\x1b[31m${s}\x1b[0m`,
17
+ bold: s => `\x1b[1m${s}\x1b[0m`,
18
+ dim: s => `\x1b[2m${s}\x1b[0m`,
18
19
  };
19
20
 
21
+ // ── Component registry ────────────────────────────────────────────────────────
20
22
  const REGISTRY = {
21
23
  spinner: {
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..." />',
24
+ name: 'Spinner',
25
+ description: 'Animated loading spinner with 5 variants',
26
+ file: 'Spinner/index.jsx',
27
+ dir: 'Spinner',
28
+ usage: '<Spinner variant="dots" label="Loading..." />',
27
29
  },
28
30
  progressbar: {
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" />',
31
+ name: 'ProgressBar',
32
+ description: 'Fillable progress bar with 3 variants',
33
+ file: 'ProgressBar/index.jsx',
34
+ dir: 'ProgressBar',
35
+ usage: '<ProgressBar value={60} label="Build" />',
34
36
  },
35
37
  };
36
38
 
37
- // ── Paths ─────────────────────────────────────────────────────────────────────
39
+ // ── Path helpers ──────────────────────────────────────────────────────────────
38
40
  function getDestDir() {
39
41
  const cwd = process.cwd();
40
- return fs.existsSync(path.join(cwd, "src"))
41
- ? path.join(cwd, "src", "components", "inkforge")
42
- : path.join(cwd, "components", "inkforge");
42
+ return fs.existsSync(path.join(cwd, 'src'))
43
+ ? path.join(cwd, 'src', 'components', 'inkforge')
44
+ : path.join(cwd, 'components', 'inkforge');
43
45
  }
44
46
 
45
47
  function getTemplatesDir() {
46
- return path.join(__dirname, "..", "src", "components");
48
+ return path.join(__dirname, '..', 'src', 'components');
47
49
  }
48
50
 
49
51
  function ensureDir(p) {
50
52
  if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
51
53
  }
52
54
 
55
+ // ── Copy core/colors.js ───────────────────────────────────────────────────────
56
+ function copyCore(destDir) {
57
+ const coreDir = path.join(destDir, 'core');
58
+ const destFile = path.join(coreDir, 'colors.js');
59
+ if (fs.existsSync(destFile)) return;
60
+
61
+ const srcFile = path.join(__dirname, '..', 'src', 'core', 'colors.js');
62
+ if (!fs.existsSync(srcFile)) return;
63
+
64
+ ensureDir(coreDir);
65
+ fs.copyFileSync(srcFile, destFile);
66
+ }
67
+
53
68
  // ── Copy one component ────────────────────────────────────────────────────────
54
69
  function copyComponent(key, silent = false) {
55
- const entry = REGISTRY[key];
56
- const destDir = getDestDir();
57
- const srcFile = path.join(getTemplatesDir(), entry.file);
70
+ const entry = REGISTRY[key];
71
+ const destDir = getDestDir();
72
+ const srcFile = path.join(getTemplatesDir(), entry.file);
58
73
  const destFolder = path.join(destDir, entry.dir);
59
- const destFile = path.join(destFolder, "index.jsx");
74
+ const destFile = path.join(destFolder, 'index.jsx');
60
75
 
61
76
  if (!fs.existsSync(srcFile)) {
62
- console.log(c.red(`✗ Template not found for ${entry.name}.`));
77
+ console.log(c.red(` ✗ Template not found for ${entry.name}. Try reinstalling inkforge.`));
63
78
  return false;
64
79
  }
65
80
 
66
- if (fs.existsSync(destFile) && !silent) {
67
- console.log(c.yellow(`⚠ ${entry.name} already exists — skipping.`));
81
+ if (fs.existsSync(destFile)) {
82
+ if (!silent) console.log(c.yellow(` ${entry.name} already exists — skipping.`));
68
83
  return false;
69
84
  }
70
85
 
71
86
  ensureDir(destFolder);
72
87
 
73
- let src = fs.readFileSync(srcFile, "utf8");
88
+ let src = fs.readFileSync(srcFile, 'utf8');
74
89
  src = src.replace(
75
90
  /from ['"].*?core\/colors\.js['"]/,
76
- `from '../core/colors.js'`,
91
+ `from '../core/colors.js'`
77
92
  );
78
- fs.writeFileSync(destFile, src, "utf8");
93
+ fs.writeFileSync(destFile, src, 'utf8');
79
94
 
80
- if (!silent) {
81
- console.log(c.green(` ✓ ${entry.name}`));
82
- }
95
+ if (!silent) console.log(c.green(` ✓ ${entry.name} added`));
83
96
  return true;
84
97
  }
85
98
 
86
- // ── Copy core/colors.js ───────────────────────────────────────────────────────
87
- function copyCore(destDir) {
88
- const coreDir = path.join(destDir, "core");
89
- const destFile = path.join(coreDir, "colors.js");
90
- if (fs.existsSync(destFile)) return;
91
-
92
- const srcFile = path.join(__dirname, "..", "src", "core", "colors.js");
93
- if (!fs.existsSync(srcFile)) return;
94
-
95
- ensureDir(coreDir);
96
- fs.copyFileSync(srcFile, destFile);
97
- }
98
-
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
- }
99
+ // ── Print import + usage hint after adding ────────────────────────────────────
100
+ function printUsageHint(keys) {
101
+ console.log(c.bold(c.green('\n Components added! Use them in app.jsx:\n')));
102
+ console.log(c.dim(' // Add these imports at the top of app.jsx:'));
103
+ keys.forEach(key => {
104
+ const entry = REGISTRY[key];
105
+ if (!entry) return;
106
+ const importPath = `./src/components/inkforge/${entry.dir}/index.jsx`;
107
+ console.log(c.dim(` import { ${entry.name} } from '${importPath}';`));
108
+ });
109
+ console.log('');
110
+ console.log(c.dim(' // Then use them in your JSX:'));
111
+ keys.forEach(key => {
112
+ const entry = REGISTRY[key];
113
+ if (!entry) return;
114
+ console.log(c.dim(` ${entry.usage}`));
115
+ });
116
+ console.log(c.dim('\n Then run: ') + c.cyan('npm start\n'));
182
117
  }
183
118
 
184
119
  // ── INIT command ──────────────────────────────────────────────────────────────
185
120
  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");
121
+ console.log(c.bold(c.cyan('\n InkForge — initializing your project\n')));
122
+
123
+ const cwd = process.cwd();
124
+ const pkgPath = path.join(cwd, 'package.json');
125
+
126
+ // Step 1: Create package.json with all dependencies listed
127
+ const folderName = path.basename(cwd);
128
+ const pkg = {
129
+ name: folderName,
130
+ version: '1.0.0',
131
+ type: 'module',
132
+ scripts: {
133
+ start: 'vite-node app.jsx',
134
+ },
135
+ dependencies: {
136
+ '@sarthak_krishak/inkforge': 'latest',
137
+ ink: '^5.0.0',
138
+ react: '^18.3.0',
139
+ },
140
+ devDependencies: {
141
+ '@vitejs/plugin-react': '^4.3.0',
142
+ vite: '^5.4.0',
143
+ 'vite-node': '^2.1.0',
144
+ },
145
+ };
191
146
 
192
- // ── Step 1: Create package.json if it doesn't exist ──────────────────────
193
147
  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
148
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
204
- console.log(c.green(" ✓ package.json created"));
149
+ console.log(c.green(' ✓ package.json created'));
205
150
  } else {
206
- // patch existing one
207
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
151
+ const existing = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
208
152
  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
- }
153
+ if (existing.type !== 'module') { existing.type = 'module'; changed = true; }
154
+ if (!existing.scripts) { existing.scripts = {}; }
155
+ if (!existing.scripts.start) { existing.scripts.start = 'vite-node app.jsx'; changed = true; }
220
156
  if (changed) {
221
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
222
- console.log(c.green(" ✓ package.json updated"));
157
+ fs.writeFileSync(pkgPath, JSON.stringify(existing, null, 2));
158
+ console.log(c.green(' ✓ package.json updated'));
223
159
  } else {
224
- console.log(c.dim(" ↷ package.json already configured"));
160
+ console.log(c.dim(' ↷ package.json already configured'));
225
161
  }
226
162
  }
227
163
 
228
- // ── Step 2: Create vite.config.js ────────────────────────────────────────
229
- const vitePath = path.join(cwd, "vite.config.js");
164
+ // Step 2: Create vite.config.js
165
+ const vitePath = path.join(cwd, 'vite.config.js');
230
166
  if (!fs.existsSync(vitePath)) {
231
- fs.writeFileSync(
232
- vitePath,
233
- `import { defineConfig } from 'vite';
167
+ fs.writeFileSync(vitePath,
168
+ `import { defineConfig } from 'vite';
234
169
  import react from '@vitejs/plugin-react';
235
170
  export default defineConfig({ plugins: [react()] });
236
- `,
237
- );
238
- console.log(c.green(" ✓ vite.config.js created"));
171
+ `);
172
+ console.log(c.green(' ✓ vite.config.js created'));
239
173
  } else {
240
- console.log(c.dim(" ↷ vite.config.js already exists"));
174
+ console.log(c.dim(' ↷ vite.config.js already exists'));
241
175
  }
242
176
 
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");
177
+ // Step 3: Create starter app.jsx — no components imported yet
178
+ const appPath = path.join(cwd, 'app.jsx');
253
179
  if (!fs.existsSync(appPath)) {
254
- fs.writeFileSync(
255
- appPath,
256
- `import React, { useState, useEffect } from 'react';
180
+ fs.writeFileSync(appPath,
181
+ `import React from 'react';
257
182
  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
183
 
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
- }, []);
184
+ // Add components first, then import them here:
185
+ //
186
+ // npx inkforge add spinner
187
+ // npx inkforge add progressbar
188
+ //
189
+ // import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
190
+ // import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
274
191
 
192
+ function App() {
275
193
  return (
276
194
  <Box flexDirection="column" padding={1} gap={1}>
277
195
  <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" />
196
+ <Text dimColor>Add components by running:</Text>
197
+ <Text dimColor> npx inkforge add spinner</Text>
198
+ <Text dimColor> npx inkforge add progressbar</Text>
282
199
  </Box>
283
200
  );
284
201
  }
285
202
 
286
203
  render(<App />);
287
- `,
288
- );
289
- console.log(c.green(" ✓ app.jsx created"));
204
+ `);
205
+ console.log(c.green(' ✓ app.jsx created'));
290
206
  } else {
291
- console.log(c.dim(" ↷ app.jsx already exists — skipping"));
207
+ console.log(c.dim(' ↷ app.jsx already exists'));
292
208
  }
293
209
 
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")));
210
+ // Done — print next steps
211
+ console.log(c.bold(c.green('\n Project scaffolded!\n')));
212
+ console.log(' Next steps:\n');
213
+ console.log(' ' + c.cyan('npm install'));
214
+ console.log(c.dim(' installs all dependencies\n'));
215
+ console.log(' ' + c.cyan('npx inkforge add spinner'));
216
+ console.log(c.dim(' adds the Spinner component\n'));
217
+ console.log(' ' + c.cyan('npx inkforge add progressbar'));
218
+ console.log(c.dim(' adds the ProgressBar component\n'));
219
+ console.log(' ' + c.cyan('npm start'));
220
+ console.log(c.dim(' runs your app\n'));
298
221
  }
299
222
 
300
- // ── Interactive selector for `add` ────────────────────────────────────────────
223
+ // ── Interactive selector ──────────────────────────────────────────────────────
301
224
  async function interactiveSelect() {
302
- const keys = Object.keys(REGISTRY);
303
- const entries = keys.map((k) => REGISTRY[k]);
304
- let cursor = 0;
225
+ const keys = Object.keys(REGISTRY);
226
+ const entries = keys.map(k => REGISTRY[k]);
227
+ let cursor = 0;
305
228
  const selected = new Set();
306
229
 
307
- process.stdout.write("\x1b[?25l");
230
+ process.stdout.write('\x1b[?25l');
308
231
  readline.emitKeypressEvents(process.stdin);
309
232
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
310
233
 
311
- function render() {
234
+ function renderMenu() {
312
235
  process.stdout.write(`\x1b[${entries.length + 4}A\x1b[0J`);
313
- console.log(c.bold("\n Select components to add:\n"));
236
+ console.log(c.bold('\n Select components to add:\n'));
314
237
  entries.forEach((e, i) => {
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
- );
238
+ const checked = selected.has(keys[i]) ? c.green('') : c.dim('');
239
+ const active = i === cursor ? c.cyan('') : ' ';
240
+ const name = i === cursor ? c.bold(c.cyan(e.name)) : e.name;
241
+ console.log(` ${active}${checked} ${name.padEnd(18)}${c.dim(e.description)}`);
321
242
  });
322
- console.log(c.dim("\n ↑↓ move space select enter confirm q quit"));
243
+ console.log(c.dim('\n ↑↓ move space select enter confirm q quit'));
323
244
  }
324
245
 
325
- console.log("\n");
326
- entries.forEach(() => console.log(""));
327
- console.log("\n");
328
- render();
246
+ console.log('\n');
247
+ entries.forEach(() => console.log(''));
248
+ console.log('\n');
249
+ renderMenu();
329
250
 
330
- return new Promise((resolve) => {
331
- process.stdin.on("keypress", (str, key) => {
251
+ return new Promise(resolve => {
252
+ process.stdin.on('keypress', (str, key) => {
332
253
  if (!key) return;
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") {
254
+ if (key.name === 'up') cursor = (cursor - 1 + keys.length) % keys.length;
255
+ if (key.name === 'down') cursor = (cursor + 1) % keys.length;
256
+ if (key.name === 'space') {
336
257
  if (selected.has(keys[cursor])) selected.delete(keys[cursor]);
337
258
  else selected.add(keys[cursor]);
338
259
  }
339
- if (key.name === "return") {
340
- cleanup();
341
- resolve([...selected]);
342
- }
343
- if (key.name === "q" || (key.ctrl && key.name === "c")) {
260
+ if (key.name === 'return') { cleanup(); resolve([...selected]); }
261
+ if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
344
262
  cleanup();
345
- console.log(c.dim("\n Cancelled.\n"));
263
+ console.log(c.dim('\n Cancelled.\n'));
346
264
  resolve([]);
347
265
  }
348
- render();
266
+ renderMenu();
349
267
  });
350
268
  });
351
269
 
352
270
  function cleanup() {
353
271
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
354
272
  process.stdin.pause();
355
- process.stdout.write("\x1b[?25h");
273
+ process.stdout.write('\x1b[?25h');
356
274
  }
357
275
  }
358
276
 
359
277
  // ── Commands ──────────────────────────────────────────────────────────────────
278
+
360
279
  program
361
- .name("inkforge")
362
- .description("InkForge — beautiful terminal UI components for React/Ink")
363
- .version("0.1.0");
280
+ .name('inkforge')
281
+ .description('InkForge — beautiful terminal UI components for React/Ink')
282
+ .version('0.1.0');
364
283
 
284
+ // inkforge init
365
285
  program
366
- .command("init")
367
- .description(
368
- "Set up InkForge in your project — run this once after installing",
369
- )
286
+ .command('init')
287
+ .description('Scaffold a new InkForge project — run this once in an empty folder')
370
288
  .action(runInit);
371
289
 
290
+ // inkforge list
372
291
  program
373
- .command("list")
374
- .description("List all available components")
292
+ .command('list')
293
+ .description('List all available components')
375
294
  .action(() => {
376
- console.log(c.bold("\n InkForge Components\n"));
295
+ console.log(c.bold('\n InkForge — available components\n'));
377
296
  Object.entries(REGISTRY).forEach(([key, entry]) => {
378
- console.log(
379
- ` ${c.cyan(entry.name.padEnd(16))} ${c.dim(entry.description)}`,
380
- );
297
+ console.log(` ${c.cyan(entry.name.padEnd(16))} ${c.dim(entry.description)}`);
381
298
  console.log(c.dim(` npx inkforge add ${key}\n`));
382
299
  });
383
300
  });
384
301
 
302
+ // inkforge add [components...]
385
303
  program
386
- .command("add [components...]")
387
- .description("Add a specific component to your project")
304
+ .command('add [components...]')
305
+ .description('Add one or more components to your project')
388
306
  .action(async (components) => {
389
- console.log(c.bold(c.cyan("\n InkForge\n")));
307
+ console.log(c.bold(c.cyan('\n InkForge\n')));
390
308
 
309
+ // No args — show interactive selector
391
310
  if (!components || components.length === 0) {
392
311
  const chosen = await interactiveSelect();
393
312
  if (chosen.length === 0) return;
394
- console.log("");
395
- chosen.forEach((key) => copyComponent(key));
313
+ console.log('');
314
+ chosen.forEach(key => copyComponent(key));
396
315
  copyCore(getDestDir());
316
+ printUsageHint(chosen);
397
317
  return;
398
318
  }
399
319
 
320
+ // Args provided — add directly
321
+ const added = [];
400
322
  for (const name of components) {
401
- const key = name.toLowerCase().replace(/[^a-z]/g, "");
323
+ const key = name.toLowerCase().replace(/[^a-z]/g, '');
402
324
  if (!REGISTRY[key]) {
403
- console.log(c.red(`✗ Unknown component: "${name}"`));
404
- console.log(
405
- c.dim(` Run npx inkforge list to see available components.\n`),
406
- );
325
+ console.log(c.red(` ✗ Unknown component: "${name}"`));
326
+ console.log(c.dim(` Run ${c.cyan('npx inkforge list')} to see available components.\n`));
407
327
  continue;
408
328
  }
409
- copyComponent(key);
329
+ const ok = copyComponent(key);
330
+ if (ok) added.push(key);
331
+ }
332
+
333
+ if (added.length > 0) {
334
+ copyCore(getDestDir());
335
+ printUsageHint(added);
410
336
  }
411
- copyCore(getDestDir());
412
- console.log(
413
- c.green(c.bold("\n Done! Components are yours to own and edit.\n")),
414
- );
415
337
  });
416
338
 
417
- program.parse();
339
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarthak_krishak/inkforge",
3
- "version": "0.2.1",
3
+ "version": "0.3.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",