@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.
- package/Readme.md +179 -101
- package/cli/index.js +309 -135
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -1,189 +1,267 @@
|
|
|
1
1
|
# InkForge
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
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)
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
##
|
|
20
|
+
## What is InkForge?
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
InkForge gives you polished, production-ready terminal components — spinners, progress bars, and more — that you **actually own**. Instead of a locked npm dependency you can't touch, InkForge copies the source code directly into your project. Read it. Edit it. Make it yours.
|
|
18
23
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
34
|
+
## Quick Start
|
|
31
35
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
```bash
|
|
46
|
+
npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
|
|
47
|
+
```
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
### Step 3 — Run init
|
|
47
50
|
|
|
48
51
|
```bash
|
|
49
|
-
npx inkforge
|
|
52
|
+
npx inkforge init
|
|
50
53
|
```
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
This one command automatically creates everything:
|
|
53
56
|
|
|
54
|
-
|
|
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
|
-
|
|
66
|
+
### Step 4 — Start your app
|
|
57
67
|
|
|
58
68
|
```bash
|
|
59
|
-
|
|
60
|
-
npx inkforge add progressbar
|
|
61
|
-
npx inkforge add spinner progressbar # multiple at once
|
|
69
|
+
npm start
|
|
62
70
|
```
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
You'll see animated spinners and a filling progress bar in your terminal immediately.
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
##
|
|
73
|
-
|
|
74
|
-
### `Spinner`
|
|
96
|
+
## Using components in your app
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
Open `app.jsx` and build from there. The import paths are set up automatically:
|
|
77
99
|
|
|
78
100
|
```jsx
|
|
79
|
-
import {
|
|
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
|
-
|
|
82
|
-
<Spinner label="Loading..." />
|
|
132
|
+
---
|
|
83
133
|
|
|
84
|
-
|
|
85
|
-
<Spinner variant="bounce" label="Processing..." />
|
|
134
|
+
## Components
|
|
86
135
|
|
|
87
|
-
|
|
88
|
-
<Spinner variant="dots" theme="cyberpunk" label="Hacking..." />
|
|
136
|
+
### Spinner
|
|
89
137
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
156
|
+
#### Spinner Props
|
|
95
157
|
|
|
96
158
|
| Prop | Type | Default | Description |
|
|
97
159
|
|---|---|---|---|
|
|
98
|
-
| `variant` | `string` | `'dots'` | Animation style: `dots
|
|
99
|
-
| `label` | `string` | `'Loading...'` | Text
|
|
100
|
-
| `color` | `string` | theme primary | Override spinner color (hex
|
|
101
|
-
| `theme` | `string` | `'default'` | Color theme: `default
|
|
102
|
-
| `interval` | `number` | `
|
|
103
|
-
| `done` | `boolean` | `false` |
|
|
104
|
-
| `doneText` | `string` | `'✓ Done'` |
|
|
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
|
-
###
|
|
170
|
+
### ProgressBar
|
|
109
171
|
|
|
110
|
-
Fillable progress bar with 3 visual
|
|
172
|
+
Fillable progress bar with 3 visual styles.
|
|
111
173
|
|
|
112
174
|
```jsx
|
|
113
|
-
|
|
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
|
|
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
|
-
//
|
|
122
|
-
<ProgressBar value={progress} variant="
|
|
123
|
-
<ProgressBar value={progress} variant="
|
|
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
|
-
|
|
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
|
|
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
|
|
142
|
-
| `variant` | `string` | `'default'` | Bar style: `default
|
|
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
|
-
|
|
212
|
+
All components support two built-in themes via the `theme` prop:
|
|
149
213
|
|
|
150
|
-
| Theme |
|
|
214
|
+
| Theme | Colors | Best for |
|
|
151
215
|
|---|---|---|
|
|
152
|
-
| `default` |
|
|
153
|
-
| `cyberpunk` |
|
|
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
|
-
##
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
##
|
|
241
|
+
## Why own your components?
|
|
171
242
|
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
##
|
|
251
|
+
## Roadmap
|
|
184
252
|
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
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:
|
|
18
|
-
green:
|
|
19
|
-
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
20
|
-
red:
|
|
21
|
-
bold:
|
|
22
|
-
dim:
|
|
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:
|
|
30
|
-
description:
|
|
31
|
-
file:
|
|
32
|
-
dir:
|
|
33
|
-
usage:
|
|
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:
|
|
37
|
-
description:
|
|
38
|
-
file:
|
|
39
|
-
dir:
|
|
40
|
-
usage:
|
|
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
|
-
//
|
|
45
|
-
function
|
|
37
|
+
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
38
|
+
function getDestDir() {
|
|
46
39
|
const cwd = process.cwd();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
path.join(cwd,
|
|
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,
|
|
46
|
+
return path.join(__dirname, "..", "src", "components");
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
function ensureDir(
|
|
62
|
-
if (!fs.existsSync(
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const destDir
|
|
71
|
-
const srcFile
|
|
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
|
|
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}
|
|
62
|
+
console.log(c.red(`✗ Template not found for ${entry.name}.`));
|
|
78
63
|
return false;
|
|
79
64
|
}
|
|
80
65
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
127
|
-
const entries = keys.map(k => REGISTRY[k]);
|
|
128
|
-
let
|
|
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
|
-
|
|
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(
|
|
313
|
+
console.log(c.bold("\n Select components to add:\n"));
|
|
140
314
|
entries.forEach((e, i) => {
|
|
141
|
-
const checked
|
|
142
|
-
const active
|
|
143
|
-
const name
|
|
144
|
-
|
|
145
|
-
|
|
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(
|
|
322
|
+
console.log(c.dim("\n ↑↓ move space select enter confirm q quit"));
|
|
148
323
|
}
|
|
149
324
|
|
|
150
|
-
|
|
151
|
-
console.log(
|
|
152
|
-
|
|
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(
|
|
330
|
+
return new Promise((resolve) => {
|
|
331
|
+
process.stdin.on("keypress", (str, key) => {
|
|
158
332
|
if (!key) return;
|
|
159
|
-
|
|
160
|
-
if (key.name ===
|
|
161
|
-
if (key.name ===
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
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(
|
|
355
|
+
process.stdout.write("\x1b[?25h");
|
|
188
356
|
}
|
|
189
357
|
}
|
|
190
358
|
|
|
191
|
-
//
|
|
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
|
-
.
|
|
194
|
-
.description(
|
|
195
|
-
|
|
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(
|
|
200
|
-
.description(
|
|
373
|
+
.command("list")
|
|
374
|
+
.description("List all available components")
|
|
201
375
|
.action(() => {
|
|
202
|
-
console.log(c.bold(
|
|
376
|
+
console.log(c.bold("\n InkForge Components\n"));
|
|
203
377
|
Object.entries(REGISTRY).forEach(([key, entry]) => {
|
|
204
|
-
console.log(
|
|
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(
|
|
212
|
-
.description(
|
|
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(
|
|
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(
|
|
232
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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();
|