@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.
- package/Readme.md +204 -94
- package/cli/index.js +198 -276
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -1,154 +1,264 @@
|
|
|
1
|
-
#
|
|
1
|
+
# InkForge
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@sarthak_krishak/inkforge)
|
|
6
|
+
[](https://www.npmjs.com/package/@sarthak_krishak/inkforge)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](https://react.dev)
|
|
10
|
+
[](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
|
-
##
|
|
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
|
-
|
|
26
|
+
## Quick Start
|
|
14
27
|
|
|
15
|
-
Create
|
|
28
|
+
### Step 1 — Create a new empty folder and open a terminal inside it
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
```bash
|
|
31
|
+
mkdir my-cli-app
|
|
32
|
+
cd my-cli-app
|
|
33
|
+
```
|
|
20
34
|
|
|
21
|
-
### 2
|
|
35
|
+
### Step 2 — Scaffold the project
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
npx inkforge init
|
|
25
|
-
|
|
37
|
+
```bash
|
|
38
|
+
npx @sarthak_krishak/inkforge init
|
|
39
|
+
```
|
|
26
40
|
|
|
27
|
-
This
|
|
41
|
+
This creates everything your project needs:
|
|
28
42
|
|
|
29
|
-
|
|
|
43
|
+
| Created | Description |
|
|
30
44
|
|---|---|
|
|
31
|
-
| `package.json` |
|
|
32
|
-
| `vite.config.js` | Vite
|
|
33
|
-
| `app.jsx` |
|
|
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
|
|
49
|
+
### Step 3 — Install dependencies
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
npm
|
|
42
|
-
|
|
51
|
+
```bash
|
|
52
|
+
npm install
|
|
53
|
+
```
|
|
43
54
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
```bash
|
|
72
|
+
npm start
|
|
73
|
+
```
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
83
|
-
<
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
|
109
|
-
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
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
|
-
|
|
166
|
+
### ProgressBar
|
|
118
167
|
|
|
119
|
-
|
|
120
|
-
|
|
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"
|
|
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
|
-
|
|
196
|
+
## Themes
|
|
197
|
+
|
|
198
|
+
| Theme | Accent | Best for |
|
|
128
199
|
|---|---|---|
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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 './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
|
-
<
|
|
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();
|