@sarthak_krishak/inkforge 0.2.0 → 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 +204 -94
  2. package/cli/index.js +198 -276
  3. package/package.json +1 -1
package/Readme.md CHANGED
@@ -1,154 +1,264 @@
1
- # @sarthak_krishak/inkforge
1
+ # InkForge
2
2
 
3
- > **Beautiful terminal UI components — ready in 2 commands.**
3
+ <div align="center">
4
4
 
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.
5
+ [![npm version](https://img.shields.io/npm/v/@sarthak_krishak/inkforge?color=CB3837&logo=npm&logoColor=white&style=flat-square)](https://www.npmjs.com/package/@sarthak_krishak/inkforge)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@sarthak_krishak/inkforge?color=CB3837&logo=npm&logoColor=white&style=flat-square)](https://www.npmjs.com/package/@sarthak_krishak/inkforge)
7
+ [![license](https://img.shields.io/npm/l/@sarthak_krishak/inkforge?color=blue&style=flat-square)](./LICENSE)
8
+ [![node](https://img.shields.io/node/v/@sarthak_krishak/inkforge?color=339933&logo=node.js&logoColor=white&style=flat-square)](https://nodejs.org)
9
+ [![React](https://img.shields.io/badge/React-18%2B-61DAFB?style=flat-square&logo=react&logoColor=white)](https://react.dev)
10
+ [![Ink](https://img.shields.io/badge/Ink-5%2B-000000?style=flat-square)](https://github.com/vadimdemedes/ink)
11
+
12
+ **Beautiful terminal UI components for React/Ink — scaffolded in one command, owned forever.**
13
+
14
+ *shadcn/ui for your terminal. Built for the AI coding agent era.*
15
+
16
+ </div>
8
17
 
9
18
  ---
10
19
 
11
- ## Setup 2 commands, then you're done
20
+ ## What is InkForge?
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 buried in `node_modules`, InkForge copies the source code directly into your project. Read it. Edit it. Make it yours.
23
+
24
+ ---
12
25
 
13
- ### 1. Install
26
+ ## Quick Start
14
27
 
15
- Create an empty folder, open a terminal inside it, and run:
28
+ ### Step 1 — Create a new empty folder and open a terminal inside it
16
29
 
17
- \`\`\`bash
18
- npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
19
- \`\`\`
30
+ ```bash
31
+ mkdir my-cli-app
32
+ cd my-cli-app
33
+ ```
20
34
 
21
- ### 2. Init
35
+ ### Step 2 — Scaffold the project
22
36
 
23
- \`\`\`bash
24
- npx inkforge init
25
- \`\`\`
37
+ ```bash
38
+ npx @sarthak_krishak/inkforge init
39
+ ```
26
40
 
27
- This automatically creates everything you need:
41
+ This creates everything your project needs:
28
42
 
29
- | File / Folder | What it is |
43
+ | Created | Description |
30
44
  |---|---|
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 |
45
+ | `package.json` | Configured with `type: module`, `start` script, and all dependencies listed |
46
+ | `vite.config.js` | Vite + React plugin config |
47
+ | `app.jsx` | Starter app edit this to build your CLI |
37
48
 
38
- ### 3. Run
49
+ ### Step 3 — Install dependencies
39
50
 
40
- \`\`\`bash
41
- npm start
42
- \`\`\`
51
+ ```bash
52
+ npm install
53
+ ```
43
54
 
44
- You'll see animated spinners and a filling progress bar immediately. Done.
55
+ ### Step 4 Add the components you want
45
56
 
46
- ---
57
+ ```bash
58
+ npx inkforge add spinner
59
+ npx inkforge add progressbar
60
+ ```
47
61
 
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
- \`\`\`
62
+ This copies the component source files directly into your project. You own them now.
65
63
 
66
- ---
64
+ ### Step 5 — Open `app.jsx`, import your components, and run
67
65
 
68
- ## Using the components in your own app
66
+ ```jsx
67
+ import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
68
+ import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
69
+ ```
69
70
 
70
- Open `app.jsx` and build from there:
71
+ ```bash
72
+ npm start
73
+ ```
71
74
 
72
- \`\`\`jsx
73
- import React from 'react';
75
+ ---
76
+
77
+ ## Full example `app.jsx`
78
+
79
+ ```jsx
80
+ import React, { useState, useEffect } from 'react';
74
81
  import { render, Box, Text } from 'ink';
75
82
  import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
76
83
  import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
77
84
 
78
85
  function App() {
86
+ const [progress, setProgress] = useState(0);
87
+ const [done, setDone] = useState(false);
88
+
89
+ useEffect(() => {
90
+ const t = setInterval(() => {
91
+ setProgress(p => {
92
+ if (p >= 100) { clearInterval(t); setDone(true); return 100; }
93
+ return p + 5;
94
+ });
95
+ }, 150);
96
+ return () => clearInterval(t);
97
+ }, []);
98
+
79
99
  return (
80
100
  <Box flexDirection="column" padding={1} gap={1}>
81
101
  <Text bold color="cyan">My CLI App</Text>
82
- <Spinner variant="dots" label="Loading..." />
83
- <ProgressBar value={75} 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" />
84
106
  </Box>
85
107
  );
86
108
  }
87
109
 
88
110
  render(<App />);
89
- \`\`\`
111
+ ```
90
112
 
91
113
  ---
92
114
 
93
- ## Spinner
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
+ ---
94
134
 
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..." />
135
+ ## Components
101
136
 
102
- // Done state — switches to completion message
137
+ ### Spinner
138
+
139
+ Animated loading indicator with 5 variants and a done state.
140
+
141
+ ```jsx
142
+ <Spinner variant="dots" label="Fetching data..." />
143
+ <Spinner variant="bounce" label="Processing request..." theme="cyberpunk" />
144
+ <Spinner variant="arc" label="Compiling..." />
145
+ <Spinner variant="line" label="Connecting..." />
146
+ <Spinner variant="simple" label="Waiting..." />
147
+
148
+ // Switches to a completion message when done={true}
103
149
  <Spinner label="Deploying..." done={isDone} doneText="✓ Deployed!" />
104
- \`\`\`
150
+ ```
105
151
 
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 |
152
+ #### Spinner Props
153
+
154
+ | Prop | Type | Default | Description |
155
+ |---|---|---|---|
156
+ | `variant` | `string` | `'dots'` | Animation style: `dots` `line` `bounce` `arc` `simple` |
157
+ | `label` | `string` | `'Loading...'` | Text shown next to the spinner |
158
+ | `color` | `string` | theme primary | Spinner color (any hex value) |
159
+ | `theme` | `string` | `'default'` | Color theme: `'default'` or `'cyberpunk'` |
160
+ | `interval` | `number` | `120` | Animation speed in milliseconds |
161
+ | `done` | `boolean` | `false` | When `true`, switches to done state |
162
+ | `doneText` | `string` | `'✓ Done'` | Message shown when `done` is `true` |
114
163
 
115
164
  ---
116
165
 
117
- ## ProgressBar
166
+ ### ProgressBar
118
167
 
119
- \`\`\`jsx
120
- <ProgressBar value={60} label="Build" />
168
+ Fillable progress bar with 3 visual styles.
169
+
170
+ ```jsx
171
+ <ProgressBar value={75} label="Build" />
121
172
  <ProgressBar value={45} total={200} label="Files" showValue />
122
173
  <ProgressBar value={progress} variant="thin" label="Upload" />
123
- <ProgressBar value={progress} variant="block" label="Memory" color="#E5C07B" />
174
+ <ProgressBar value={progress} variant="block" label="Memory" />
175
+ <ProgressBar value={progress} color="#E5C07B" label="CPU" />
124
176
  <ProgressBar value={progress} theme="cyberpunk" label="Hack" />
125
- \`\`\`
177
+ ```
178
+
179
+ #### ProgressBar Props
180
+
181
+ | Prop | Type | Default | Description |
182
+ |---|---|---|---|
183
+ | `value` | `number` | `0` | Current progress value |
184
+ | `total` | `number` | `100` | Maximum value |
185
+ | `width` | `number` | `30` | Bar width in characters |
186
+ | `label` | `string` | `''` | Label shown before the bar |
187
+ | `showPercent` | `boolean` | `true` | Show percentage on the right |
188
+ | `showValue` | `boolean` | `false` | Show `value/total` instead of percentage |
189
+ | `color` | `string` | theme success | Fill color (any hex value) |
190
+ | `bgColor` | `string` | theme muted | Empty bar color (any hex value) |
191
+ | `theme` | `string` | `'default'` | Color theme: `'default'` or `'cyberpunk'` |
192
+ | `variant` | `string` | `'default'` | Bar style: `'default'` `'thin'` `'block'` |
193
+
194
+ ---
126
195
 
127
- | Prop | Default | Options |
196
+ ## Themes
197
+
198
+ | Theme | Accent | Best for |
128
199
  |---|---|---|
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 |
200
+ | `default` | Blue / green | Production tools, professional CLIs |
201
+ | `cyberpunk` | Neon cyan / bright green | Dev tools, personal projects, AI agents |
202
+
203
+ ---
204
+
205
+ ## CLI Reference
206
+
207
+ ```bash
208
+ # Scaffold a new project in an empty folder (run once)
209
+ npx @sarthak_krishak/inkforge init
210
+
211
+ # Add components to your project
212
+ npx inkforge add spinner
213
+ npx inkforge add progressbar
214
+ npx inkforge add spinner progressbar
215
+
216
+ # Interactive component picker (arrow keys + space)
217
+ npx inkforge add
218
+
219
+ # See all available components
220
+ npx inkforge list
221
+ ```
138
222
 
139
223
  ---
140
224
 
141
- ## Add more components later
225
+ ## Why own your components?
226
+
227
+ With a standard npm package, the code is locked inside `node_modules`. You can't change it without forking the entire repo.
228
+
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.
230
+
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.
232
+
233
+ ---
234
+
235
+ ## Roadmap
236
+
237
+ - [x] Spinner — 5 variants, done state, theme support
238
+ - [x] ProgressBar — 3 variants, custom colors, theme support
239
+ - [x] CLI — `init`, `add`, `list` commands
240
+ - [ ] DiffViewer — git-style diff display for AI coding agents
241
+ - [ ] StreamingOutput — token-by-token AI response display
242
+ - [ ] PromptInput — input with history and autocomplete
243
+ - [ ] Select / MultiSelect — keyboard-navigable menus
244
+ - [ ] Table — structured data display
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.
142
252
 
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
- \`\`\`
253
+ ```bash
254
+ git clone https://github.com/sarthak-krishak/inkforge
255
+ cd inkforge
256
+ npm install
257
+ npm run demo
258
+ ```
149
259
 
150
260
  ---
151
261
 
152
262
  ## License
153
263
 
154
- MIT [Sarthak](https://github.com/yourusername)
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.0",
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",