@sarthak_krishak/inkforge 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/Readme.md +179 -101
  2. package/cli/index.js +309 -135
  3. package/package.json +1 -1
package/Readme.md CHANGED
@@ -1,189 +1,267 @@
1
1
  # InkForge
2
2
 
3
- > **Beautiful, customizable terminal UI components — installed in one command, owned forever.**
3
+ <div align="center">
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.
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)
6
11
 
7
- ```bash
8
- npx inkforge add spinner
9
- npx inkforge add progressbar
10
- npx inkforge add spinner progressbar
11
- ```
12
+ **Beautiful terminal UI components for React/Ink — set up in 2 commands, owned forever.**
13
+
14
+ *shadcn/ui for your terminal. Built for the AI coding agent era.*
15
+
16
+ </div>
12
17
 
13
18
  ---
14
19
 
15
- ## Why InkForge?
20
+ ## What is InkForge?
16
21
 
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.
22
+ InkForge gives you polished, production-ready terminal components — spinners, progress bars, and morethat 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.
18
23
 
19
- InkForge solves this with a simple philosophy:
24
+ ```bash
25
+ npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
26
+ npx inkforge init
27
+ npm start
28
+ ```
20
29
 
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 |
30
+ That's the entire setup. Three commands. You're running.
27
31
 
28
32
  ---
29
33
 
30
- ## Installation
34
+ ## Quick Start
31
35
 
32
- InkForge has no runtime dependency. You use the CLI once to copy components into your project, then the CLI is no longer needed.
33
-
34
- Your project needs these peer dependencies:
36
+ ### Step 1 Create a new folder and open a terminal inside it
35
37
 
36
38
  ```bash
37
- npm install react ink
39
+ mkdir my-cli-app
40
+ cd my-cli-app
38
41
  ```
39
42
 
40
- ---
41
-
42
- ## Usage
43
+ ### Step 2 — Install InkForge and its dependencies
43
44
 
44
- ### Interactive mode (recommended)
45
+ ```bash
46
+ npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
47
+ ```
45
48
 
46
- Run with no arguments to get an interactive component selector:
49
+ ### Step 3 Run init
47
50
 
48
51
  ```bash
49
- npx inkforge add
52
+ npx inkforge init
50
53
  ```
51
54
 
52
- Use `↑↓` to move, `space` to select, `enter` to confirm.
55
+ This one command automatically creates everything:
53
56
 
54
- ### Direct mode
57
+ | Created | What it does |
58
+ |---|---|
59
+ | `package.json` | Configured with `type: module` and a `start` script |
60
+ | `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 |
55
65
 
56
- Add components by name:
66
+ ### Step 4 — Start your app
57
67
 
58
68
  ```bash
59
- npx inkforge add spinner
60
- npx inkforge add progressbar
61
- npx inkforge add spinner progressbar # multiple at once
69
+ npm start
62
70
  ```
63
71
 
64
- ### List all components
72
+ You'll see animated spinners and a filling progress bar in your terminal immediately.
65
73
 
66
- ```bash
67
- npx inkforge list
74
+ ---
75
+
76
+ ## Project structure after init
77
+
78
+ ```
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
68
92
  ```
69
93
 
70
94
  ---
71
95
 
72
- ## Components
73
-
74
- ### `Spinner`
96
+ ## Using components in your app
75
97
 
76
- Animated loading indicator with 5 variants and theme support.
98
+ Open `app.jsx` and build from there. The import paths are set up automatically:
77
99
 
78
100
  ```jsx
79
- import { Spinner } from './components/inkforge/Spinner';
101
+ import React, { useState, useEffect } from 'react';
102
+ import { render, Box, Text } from 'ink';
103
+ import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
104
+ import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
105
+
106
+ function App() {
107
+ const [progress, setProgress] = useState(0);
108
+ const [done, setDone] = useState(false);
109
+
110
+ useEffect(() => {
111
+ const t = setInterval(() => {
112
+ setProgress(p => {
113
+ if (p >= 100) { clearInterval(t); setDone(true); return 100; }
114
+ return p + 5;
115
+ });
116
+ }, 150);
117
+ return () => clearInterval(t);
118
+ }, []);
119
+
120
+ return (
121
+ <Box flexDirection="column" padding={1} gap={1}>
122
+ <Text bold color="cyan">My CLI App</Text>
123
+ <Spinner label="Building..." done={done} doneText="✓ Build complete!" />
124
+ <ProgressBar value={progress} label="Progress" />
125
+ </Box>
126
+ );
127
+ }
128
+
129
+ render(<App />);
130
+ ```
80
131
 
81
- // Basic
82
- <Spinner label="Loading..." />
132
+ ---
83
133
 
84
- // Variants: dots | line | bounce | arc | simple
85
- <Spinner variant="bounce" label="Processing..." />
134
+ ## Components
86
135
 
87
- // Themes: default | cyberpunk
88
- <Spinner variant="dots" theme="cyberpunk" label="Hacking..." />
136
+ ### Spinner
89
137
 
90
- // Done state
91
- <Spinner label="Deploying..." done={isDone} doneText="✓ Deployed!" />
138
+ Animated loading indicator with 5 variants and a done state.
139
+
140
+ ```jsx
141
+ // Variants
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
+ // Done state — switches to a completion message automatically
149
+ <Spinner
150
+ label="Deploying to production..."
151
+ done={isDone}
152
+ doneText="✓ Deployed successfully!"
153
+ />
92
154
  ```
93
155
 
94
- **Props**
156
+ #### Spinner Props
95
157
 
96
158
  | Prop | Type | Default | Description |
97
159
  |---|---|---|---|
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 |
160
+ | `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) |
163
+ | `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 |
166
+ | `doneText` | `string` | `'✓ Done'` | Message shown when `done` is `true` |
105
167
 
106
168
  ---
107
169
 
108
- ### `ProgressBar`
170
+ ### ProgressBar
109
171
 
110
- Fillable progress bar with 3 visual variants.
172
+ Fillable progress bar with 3 visual styles.
111
173
 
112
174
  ```jsx
113
- import { ProgressBar } from './components/inkforge/ProgressBar';
114
-
115
- // Basic
116
- <ProgressBar value={60} label="Build" />
175
+ // Basic usage
176
+ <ProgressBar value={75} label="Build" />
117
177
 
118
- // Show raw value instead of percent
119
- <ProgressBar value={45} total={200} label="Files" showValue />
178
+ // Show raw value instead of percentage
179
+ <ProgressBar value={45} total={200} label="Files processed" showValue />
120
180
 
121
- // Variants: default | thin | block
122
- <ProgressBar value={progress} variant="thin" label="Upload" />
123
- <ProgressBar value={progress} variant="block" label="Memory" />
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 " />
124
185
 
125
186
  // Custom color
126
- <ProgressBar value={progress} color="#E5C07B" label="CPU" />
187
+ <ProgressBar value={progress} color="#E5C07B" label="CPU usage" />
188
+
189
+ // Cyberpunk theme
190
+ <ProgressBar value={progress} theme="cyberpunk" label="Hack " />
127
191
  ```
128
192
 
129
- **Props**
193
+ #### ProgressBar Props
130
194
 
131
195
  | Prop | Type | Default | Description |
132
196
  |---|---|---|---|
133
197
  | `value` | `number` | `0` | Current progress value |
134
198
  | `total` | `number` | `100` | Maximum value |
135
- | `width` | `number` | `30` | Bar width in characters |
199
+ | `width` | `number` | `30` | Bar width in terminal characters |
136
200
  | `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` |
201
+ | `showPercent` | `boolean` | `true` | Show percentage on the right |
202
+ | `showValue` | `boolean` | `false` | Show `value/total` instead of percentage |
203
+ | `color` | `string` | theme success | Fill color (any hex value) |
204
+ | `bgColor` | `string` | theme muted | Empty bar color (any hex value) |
205
+ | `theme` | `string` | `'default'` | Color theme: `'default'` or `'cyberpunk'` |
206
+ | `variant` | `string` | `'default'` | Bar style: `'default'` `'thin'` `'block'` |
143
207
 
144
208
  ---
145
209
 
146
210
  ## Themes
147
211
 
148
- InkForge ships with two pre-built themes. Pass `theme` to any component:
212
+ All components support two built-in themes via the `theme` prop:
149
213
 
150
- | Theme | Description | Best for |
214
+ | Theme | Colors | Best for |
151
215
  |---|---|---|
152
- | `default` | Clean, professional, blue accent | Production tools |
153
- | `cyberpunk` | High contrast, neon cyan/magenta | Dev tools, personal projects |
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 |
154
218
 
155
219
  ---
156
220
 
157
- ## Roadmap
221
+ ## CLI Commands
222
+
223
+ ```bash
224
+ # Set up a new project from scratch (run once after install)
225
+ npx inkforge init
226
+
227
+ # Add a specific component to an existing project
228
+ npx inkforge add spinner
229
+ npx inkforge add progressbar
230
+ npx inkforge add spinner progressbar
158
231
 
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
232
+ # Interactive component selector (arrow keys + space to pick)
233
+ npx inkforge add
234
+
235
+ # List all available components
236
+ npx inkforge list
237
+ ```
167
238
 
168
239
  ---
169
240
 
170
- ## Contributing
241
+ ## Why own your components?
171
242
 
172
- Contributions are welcome. Open an issue first to discuss what you'd like to add.
243
+ With a standard npm package, the code lives locked inside `node_modules`. You can't edit it without forking the whole repo.
173
244
 
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
- ```
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.
246
+
247
+ This is the same philosophy that made [shadcn/ui](https://ui.shadcn.com) popular for web development. InkForge brings it to the terminal.
180
248
 
181
249
  ---
182
250
 
183
- ## License
251
+ ## Roadmap
184
252
 
185
- MITsee [LICENSE](./LICENSE)
253
+ - [x] Spinner 5 variants, done state, theme support
254
+ - [x] ProgressBar — 3 variants, custom colors, theme support
255
+ - [x] CLI installer — `init`, `add`, `list` commands
256
+ - [ ] DiffViewer — git-style diff display for AI coding agents
257
+ - [ ] StreamingOutput — token-by-token streaming display
258
+ - [ ] PromptInput — input with history and autocomplete
259
+ - [ ] Select / MultiSelect — keyboard-navigable menus
260
+ - [ ] Table — structured data display
261
+ - [ ] StatusBar — footer with agent state and metrics
186
262
 
187
263
  ---
188
264
 
189
- *Built for the era of AI coding agents. Inspired by [shadcn/ui](https://ui.shadcn.com).*
265
+ ## License
266
+
267
+ MIT © [Sarthak Krishak](https://github.com/SarthakKrishak)
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.1",
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",