@sarthak_krishak/inkforge 0.2.1 → 0.4.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.
- package/Readme.md +91 -94
- package/cli/index.js +198 -276
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://react.dev)
|
|
10
10
|
[](https://github.com/vadimdemedes/ink)
|
|
11
11
|
|
|
12
|
-
**Beautiful terminal UI components for React/Ink —
|
|
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
|
|
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 —
|
|
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
|
|
41
|
+
This creates everything your project needs:
|
|
56
42
|
|
|
57
|
-
| Created |
|
|
43
|
+
| Created | Description |
|
|
58
44
|
|---|---|
|
|
59
|
-
| `package.json` | Configured with `type: module
|
|
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` |
|
|
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
|
|
49
|
+
### Step 3 — Install dependencies
|
|
67
50
|
|
|
68
51
|
```bash
|
|
69
|
-
npm
|
|
52
|
+
npm install
|
|
70
53
|
```
|
|
71
54
|
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
##
|
|
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..."
|
|
124
|
-
<
|
|
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
|
-
//
|
|
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
|
|
162
|
-
| `color` | `string` | theme primary |
|
|
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
|
|
165
|
-
| `done` | `boolean` | `false` | When `true`, switches to
|
|
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
|
-
|
|
179
|
-
<ProgressBar value={
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
| Theme | Colors | Best for |
|
|
198
|
+
| Theme | Accent | Best for |
|
|
215
199
|
|---|---|---|
|
|
216
|
-
| `default` | Blue
|
|
217
|
-
| `cyberpunk` | Neon cyan
|
|
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
|
|
205
|
+
## CLI Reference
|
|
222
206
|
|
|
223
207
|
```bash
|
|
224
|
-
#
|
|
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
|
|
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
|
|
216
|
+
# Interactive component picker (arrow keys + space)
|
|
233
217
|
npx inkforge add
|
|
234
218
|
|
|
235
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
239
|
+
- [x] CLI — `init`, `add`, `list` commands
|
|
256
240
|
- [ ] DiffViewer — git-style diff display for AI coding agents
|
|
257
|
-
- [ ] StreamingOutput — token-by-token
|
|
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
|
|
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/
|
|
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
|
|
4
|
-
import fs from
|
|
5
|
-
import path from
|
|
6
|
-
import { fileURLToPath } from
|
|
7
|
-
import readline from
|
|
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:
|
|
13
|
-
green:
|
|
14
|
-
yellow:
|
|
15
|
-
red:
|
|
16
|
-
bold:
|
|
17
|
-
dim:
|
|
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:
|
|
23
|
-
description:
|
|
24
|
-
file:
|
|
25
|
-
dir:
|
|
26
|
-
usage:
|
|
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:
|
|
30
|
-
description:
|
|
31
|
-
file:
|
|
32
|
-
dir:
|
|
33
|
-
usage:
|
|
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
|
-
// ──
|
|
39
|
+
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
38
40
|
function getDestDir() {
|
|
39
41
|
const cwd = process.cwd();
|
|
40
|
-
return fs.existsSync(path.join(cwd,
|
|
41
|
-
? path.join(cwd,
|
|
42
|
-
: path.join(cwd,
|
|
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,
|
|
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
|
|
56
|
-
const destDir
|
|
57
|
-
const srcFile
|
|
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
|
|
74
|
+
const destFile = path.join(destFolder, 'index.jsx');
|
|
60
75
|
|
|
61
76
|
if (!fs.existsSync(srcFile)) {
|
|
62
|
-
console.log(c.red(
|
|
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)
|
|
67
|
-
console.log(c.yellow(
|
|
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,
|
|
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,
|
|
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
|
-
// ──
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
187
|
-
|
|
188
|
-
const cwd
|
|
189
|
-
const
|
|
190
|
-
|
|
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(
|
|
149
|
+
console.log(c.green(' ✓ package.json created'));
|
|
205
150
|
} else {
|
|
206
|
-
|
|
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 (
|
|
210
|
-
|
|
211
|
-
|
|
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(
|
|
222
|
-
console.log(c.green(
|
|
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(
|
|
160
|
+
console.log(c.dim(' ↷ package.json already configured'));
|
|
225
161
|
}
|
|
226
162
|
}
|
|
227
163
|
|
|
228
|
-
//
|
|
229
|
-
const vitePath = path.join(cwd,
|
|
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
|
-
|
|
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(
|
|
174
|
+
console.log(c.dim(' ↷ vite.config.js already exists'));
|
|
241
175
|
}
|
|
242
176
|
|
|
243
|
-
//
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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 './components/inkforge/Spinner/index.jsx';
|
|
190
|
+
// import { ProgressBar } from './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
|
-
<
|
|
279
|
-
<
|
|
280
|
-
<
|
|
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(
|
|
207
|
+
console.log(c.dim(' ↷ app.jsx already exists'));
|
|
292
208
|
}
|
|
293
209
|
|
|
294
|
-
//
|
|
295
|
-
console.log(c.bold(c.green(
|
|
296
|
-
console.log(
|
|
297
|
-
console.log(c.
|
|
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
|
|
223
|
+
// ── Interactive selector ──────────────────────────────────────────────────────
|
|
301
224
|
async function interactiveSelect() {
|
|
302
|
-
const keys
|
|
303
|
-
const entries
|
|
304
|
-
let
|
|
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(
|
|
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
|
|
234
|
+
function renderMenu() {
|
|
312
235
|
process.stdout.write(`\x1b[${entries.length + 4}A\x1b[0J`);
|
|
313
|
-
console.log(c.bold(
|
|
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(
|
|
316
|
-
const active
|
|
317
|
-
const 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(
|
|
243
|
+
console.log(c.dim('\n ↑↓ move space select enter confirm q quit'));
|
|
323
244
|
}
|
|
324
245
|
|
|
325
|
-
console.log(
|
|
326
|
-
entries.forEach(() => console.log(
|
|
327
|
-
console.log(
|
|
328
|
-
|
|
246
|
+
console.log('\n');
|
|
247
|
+
entries.forEach(() => console.log(''));
|
|
248
|
+
console.log('\n');
|
|
249
|
+
renderMenu();
|
|
329
250
|
|
|
330
|
-
return new Promise(
|
|
331
|
-
process.stdin.on(
|
|
251
|
+
return new Promise(resolve => {
|
|
252
|
+
process.stdin.on('keypress', (str, key) => {
|
|
332
253
|
if (!key) return;
|
|
333
|
-
if (key.name ===
|
|
334
|
-
if (key.name ===
|
|
335
|
-
if (key.name ===
|
|
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 ===
|
|
340
|
-
|
|
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(
|
|
263
|
+
console.log(c.dim('\n Cancelled.\n'));
|
|
346
264
|
resolve([]);
|
|
347
265
|
}
|
|
348
|
-
|
|
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(
|
|
273
|
+
process.stdout.write('\x1b[?25h');
|
|
356
274
|
}
|
|
357
275
|
}
|
|
358
276
|
|
|
359
277
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
360
279
|
program
|
|
361
|
-
.name(
|
|
362
|
-
.description(
|
|
363
|
-
.version(
|
|
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(
|
|
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(
|
|
374
|
-
.description(
|
|
292
|
+
.command('list')
|
|
293
|
+
.description('List all available components')
|
|
375
294
|
.action(() => {
|
|
376
|
-
console.log(c.bold(
|
|
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(
|
|
387
|
-
.description(
|
|
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(
|
|
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(
|
|
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(
|
|
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();
|