@sarthak_krishak/inkforge 0.1.0 → 0.2.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 +106 -141
- package/cli/index.js +309 -135
- package/package.json +1 -1
package/Readme.md
CHANGED
|
@@ -1,189 +1,154 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sarthak_krishak/inkforge
|
|
2
2
|
|
|
3
|
-
> **Beautiful
|
|
3
|
+
> **Beautiful terminal UI components — ready in 2 commands.**
|
|
4
4
|
|
|
5
|
-
InkForge is a copy-paste component library for building Terminal UIs with
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
npx inkforge add spinner
|
|
9
|
-
npx inkforge add progressbar
|
|
10
|
-
npx inkforge add spinner progressbar
|
|
11
|
-
```
|
|
5
|
+
InkForge is a copy-paste component library for building Terminal UIs with React and Ink.
|
|
6
|
+
Think of it as **shadcn/ui for your terminal** — you get the actual source code dropped
|
|
7
|
+
into your project. You own it. Edit it, delete it, do whatever you want with it.
|
|
12
8
|
|
|
13
9
|
---
|
|
14
10
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
Modern AI coding tools (Claude Code, Aider, OpenCode) live in the terminal — but building beautiful terminal UIs from scratch is painful. Every developer reinvents spinners, progress bars, and diff viewers from raw ANSI escape codes.
|
|
11
|
+
## Setup — 2 commands, then you're done
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
### 1. Install
|
|
20
14
|
|
|
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 |
|
|
15
|
+
Create an empty folder, open a terminal inside it, and run:
|
|
27
16
|
|
|
28
|
-
|
|
17
|
+
\`\`\`bash
|
|
18
|
+
npm install @sarthak_krishak/inkforge react ink vite-node vite @vitejs/plugin-react
|
|
19
|
+
\`\`\`
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
### 2. Init
|
|
31
22
|
|
|
32
|
-
|
|
23
|
+
\`\`\`bash
|
|
24
|
+
npx inkforge init
|
|
25
|
+
\`\`\`
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
This automatically creates everything you need:
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
| File / Folder | What it is |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `package.json` | Created with `type: module` and a `start` script |
|
|
32
|
+
| `vite.config.js` | Vite config with React plugin |
|
|
33
|
+
| `app.jsx` | A working starter app using both components |
|
|
34
|
+
| `src/components/inkforge/Spinner/` | Spinner source — yours to edit |
|
|
35
|
+
| `src/components/inkforge/ProgressBar/` | ProgressBar source — yours to edit |
|
|
36
|
+
| `src/components/inkforge/core/colors.js` | Theme color definitions |
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
### 3. Run
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
\`\`\`bash
|
|
41
|
+
npm start
|
|
42
|
+
\`\`\`
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
You'll see animated spinners and a filling progress bar immediately. Done.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
---
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
## Your project structure after init
|
|
49
|
+
|
|
50
|
+
\`\`\`
|
|
51
|
+
my-folder/
|
|
52
|
+
src/
|
|
53
|
+
components/
|
|
54
|
+
inkforge/
|
|
55
|
+
Spinner/
|
|
56
|
+
index.jsx ← edit this freely
|
|
57
|
+
ProgressBar/
|
|
58
|
+
index.jsx ← edit this freely
|
|
59
|
+
core/
|
|
60
|
+
colors.js
|
|
61
|
+
app.jsx ← your app starts here
|
|
62
|
+
vite.config.js
|
|
63
|
+
package.json
|
|
64
|
+
\`\`\`
|
|
51
65
|
|
|
52
|
-
|
|
66
|
+
---
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
## Using the components in your own app
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
Open `app.jsx` and build from there:
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
\`\`\`jsx
|
|
73
|
+
import React from 'react';
|
|
74
|
+
import { render, Box, Text } from 'ink';
|
|
75
|
+
import { Spinner } from './src/components/inkforge/Spinner/index.jsx';
|
|
76
|
+
import { ProgressBar } from './src/components/inkforge/ProgressBar/index.jsx';
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
function App() {
|
|
79
|
+
return (
|
|
80
|
+
<Box flexDirection="column" padding={1} gap={1}>
|
|
81
|
+
<Text bold color="cyan">My CLI App</Text>
|
|
82
|
+
<Spinner variant="dots" label="Loading..." />
|
|
83
|
+
<ProgressBar value={75} label="Progress" />
|
|
84
|
+
</Box>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
65
87
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```
|
|
88
|
+
render(<App />);
|
|
89
|
+
\`\`\`
|
|
69
90
|
|
|
70
91
|
---
|
|
71
92
|
|
|
72
|
-
##
|
|
93
|
+
## Spinner
|
|
73
94
|
|
|
74
|
-
|
|
95
|
+
\`\`\`jsx
|
|
96
|
+
<Spinner variant="dots" label="Fetching..." />
|
|
97
|
+
<Spinner variant="bounce" label="Processing..." theme="cyberpunk" />
|
|
98
|
+
<Spinner variant="arc" label="Compiling..." />
|
|
99
|
+
<Spinner variant="line" label="Connecting..." />
|
|
100
|
+
<Spinner variant="simple" label="Waiting..." />
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```jsx
|
|
79
|
-
import { Spinner } from './components/inkforge/Spinner';
|
|
80
|
-
|
|
81
|
-
// Basic
|
|
82
|
-
<Spinner label="Loading..." />
|
|
83
|
-
|
|
84
|
-
// Variants: dots | line | bounce | arc | simple
|
|
85
|
-
<Spinner variant="bounce" label="Processing..." />
|
|
86
|
-
|
|
87
|
-
// Themes: default | cyberpunk
|
|
88
|
-
<Spinner variant="dots" theme="cyberpunk" label="Hacking..." />
|
|
89
|
-
|
|
90
|
-
// Done state
|
|
102
|
+
// Done state — switches to completion message
|
|
91
103
|
<Spinner label="Deploying..." done={isDone} doneText="✓ Deployed!" />
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
**Props**
|
|
104
|
+
\`\`\`
|
|
95
105
|
|
|
96
|
-
| Prop |
|
|
97
|
-
|
|
98
|
-
| `variant` | `
|
|
99
|
-
| `label` | `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `doneText` | `string` | `'✓ Done'` | Text shown when `done` is true |
|
|
106
|
+
| Prop | Default | Options |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `variant` | `dots` | `dots` `line` `bounce` `arc` `simple` |
|
|
109
|
+
| `label` | `Loading...` | any string |
|
|
110
|
+
| `theme` | `default` | `default` `cyberpunk` |
|
|
111
|
+
| `interval` | `120` | ms number |
|
|
112
|
+
| `done` | `false` | boolean |
|
|
113
|
+
| `doneText` | `✓ Done` | any string |
|
|
105
114
|
|
|
106
115
|
---
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Fillable progress bar with 3 visual variants.
|
|
117
|
+
## ProgressBar
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
import { ProgressBar } from './components/inkforge/ProgressBar';
|
|
114
|
-
|
|
115
|
-
// Basic
|
|
119
|
+
\`\`\`jsx
|
|
116
120
|
<ProgressBar value={60} label="Build" />
|
|
117
|
-
|
|
118
|
-
// Show raw value instead of percent
|
|
119
121
|
<ProgressBar value={45} total={200} label="Files" showValue />
|
|
120
|
-
|
|
121
|
-
// Variants: default | thin | block
|
|
122
122
|
<ProgressBar value={progress} variant="thin" label="Upload" />
|
|
123
|
-
<ProgressBar value={progress} variant="block" label="Memory" />
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<ProgressBar value={progress} color="#E5C07B" label="CPU" />
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**Props**
|
|
130
|
-
|
|
131
|
-
| Prop | Type | Default | Description |
|
|
132
|
-
|---|---|---|---|
|
|
133
|
-
| `value` | `number` | `0` | Current progress value |
|
|
134
|
-
| `total` | `number` | `100` | Maximum value |
|
|
135
|
-
| `width` | `number` | `30` | Bar width in characters |
|
|
136
|
-
| `label` | `string` | `''` | Label shown before the bar |
|
|
137
|
-
| `showPercent` | `boolean` | `true` | Show `%` on the right |
|
|
138
|
-
| `showValue` | `boolean` | `false` | Show `value/total` instead |
|
|
139
|
-
| `color` | `string` | theme success | Fill color |
|
|
140
|
-
| `bgColor` | `string` | theme muted | Empty bar color |
|
|
141
|
-
| `theme` | `string` | `'default'` | Color theme: `default`, `cyberpunk` |
|
|
142
|
-
| `variant` | `string` | `'default'` | Bar style: `default`, `thin`, `block` |
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## Themes
|
|
123
|
+
<ProgressBar value={progress} variant="block" label="Memory" color="#E5C07B" />
|
|
124
|
+
<ProgressBar value={progress} theme="cyberpunk" label="Hack" />
|
|
125
|
+
\`\`\`
|
|
147
126
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
| Theme | Description | Best for |
|
|
127
|
+
| Prop | Default | Options |
|
|
151
128
|
|---|---|---|
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
129
|
+
| `value` | `0` | number |
|
|
130
|
+
| `total` | `100` | number |
|
|
131
|
+
| `width` | `30` | character count |
|
|
132
|
+
| `label` | `''` | any string |
|
|
133
|
+
| `showPercent` | `true` | boolean |
|
|
134
|
+
| `showValue` | `false` | boolean |
|
|
135
|
+
| `variant` | `default` | `default` `thin` `block` |
|
|
136
|
+
| `theme` | `default` | `default` `cyberpunk` |
|
|
137
|
+
| `color` | theme color | hex string |
|
|
154
138
|
|
|
155
139
|
---
|
|
156
140
|
|
|
157
|
-
##
|
|
141
|
+
## Add more components later
|
|
158
142
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- [ ] Table — Structured data display
|
|
166
|
-
- [ ] StatusBar — Footer with agent state
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## Contributing
|
|
171
|
-
|
|
172
|
-
Contributions are welcome. Open an issue first to discuss what you'd like to add.
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
git clone https://github.com/yourusername/inkforge
|
|
176
|
-
cd inkforge
|
|
177
|
-
npm install
|
|
178
|
-
npm run demo # See components in action
|
|
179
|
-
```
|
|
143
|
+
\`\`\`bash
|
|
144
|
+
npx inkforge add spinner # add just spinner
|
|
145
|
+
npx inkforge add progressbar # add just progressbar
|
|
146
|
+
npx inkforge add # interactive selector
|
|
147
|
+
npx inkforge list # see all available
|
|
148
|
+
\`\`\`
|
|
180
149
|
|
|
181
150
|
---
|
|
182
151
|
|
|
183
152
|
## License
|
|
184
153
|
|
|
185
|
-
MIT —
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
*Built for the era of AI coding agents. Inspired by [shadcn/ui](https://ui.shadcn.com).*
|
|
154
|
+
MIT — [Sarthak](https://github.com/yourusername)
|
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();
|