@semos-labs/create-glyph 0.1.74
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/dist/index.js +325 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var bold = (s) => `\x1B[1m${s}\x1B[22m`;
|
|
7
|
+
var dim = (s) => `\x1B[2m${s}\x1B[22m`;
|
|
8
|
+
var cyan = (s) => `\x1B[36m${s}\x1B[39m`;
|
|
9
|
+
var green = (s) => `\x1B[32m${s}\x1B[39m`;
|
|
10
|
+
var red = (s) => `\x1B[31m${s}\x1B[39m`;
|
|
11
|
+
function detectPackageManager() {
|
|
12
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
13
|
+
if (ua.startsWith("bun")) return "bun";
|
|
14
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
15
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
16
|
+
const execPath = process.argv0 || process.execPath || "";
|
|
17
|
+
if (execPath.includes("bun")) return "bun";
|
|
18
|
+
if (execPath.includes("pnpm")) return "pnpm";
|
|
19
|
+
if (execPath.includes("yarn")) return "yarn";
|
|
20
|
+
return "npm";
|
|
21
|
+
}
|
|
22
|
+
function getInstallCommand(pm) {
|
|
23
|
+
switch (pm) {
|
|
24
|
+
case "bun":
|
|
25
|
+
return "bun install";
|
|
26
|
+
case "pnpm":
|
|
27
|
+
return "pnpm install";
|
|
28
|
+
case "yarn":
|
|
29
|
+
return "yarn";
|
|
30
|
+
case "npm":
|
|
31
|
+
return "npm install";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getRunCommand(pm) {
|
|
35
|
+
switch (pm) {
|
|
36
|
+
case "bun":
|
|
37
|
+
return "bun dev";
|
|
38
|
+
case "pnpm":
|
|
39
|
+
return "pnpm dev";
|
|
40
|
+
case "yarn":
|
|
41
|
+
return "yarn dev";
|
|
42
|
+
case "npm":
|
|
43
|
+
return "npm run dev";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function templatePackageJson(name) {
|
|
47
|
+
return JSON.stringify(
|
|
48
|
+
{
|
|
49
|
+
name,
|
|
50
|
+
version: "0.0.1",
|
|
51
|
+
private: true,
|
|
52
|
+
type: "module",
|
|
53
|
+
scripts: {
|
|
54
|
+
dev: "tsx src/main.tsx",
|
|
55
|
+
build: "tsup src/main.tsx --format esm --target node18"
|
|
56
|
+
},
|
|
57
|
+
dependencies: {
|
|
58
|
+
"@semos-labs/glyph": "latest",
|
|
59
|
+
react: "^19.0.0"
|
|
60
|
+
},
|
|
61
|
+
devDependencies: {
|
|
62
|
+
"@types/node": "^20",
|
|
63
|
+
"@types/react": "^19",
|
|
64
|
+
tsx: "^4",
|
|
65
|
+
tsup: "^8",
|
|
66
|
+
typescript: "^5.5"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
null,
|
|
70
|
+
2
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
function templateTsconfig() {
|
|
74
|
+
return JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
compilerOptions: {
|
|
77
|
+
lib: ["ESNext"],
|
|
78
|
+
target: "ES2022",
|
|
79
|
+
module: "ESNext",
|
|
80
|
+
moduleResolution: "bundler",
|
|
81
|
+
jsx: "react-jsx",
|
|
82
|
+
strict: true,
|
|
83
|
+
skipLibCheck: true,
|
|
84
|
+
esModuleInterop: true,
|
|
85
|
+
forceConsistentCasingInFileNames: true,
|
|
86
|
+
declaration: true,
|
|
87
|
+
sourceMap: true,
|
|
88
|
+
noEmit: true
|
|
89
|
+
},
|
|
90
|
+
include: ["src"]
|
|
91
|
+
},
|
|
92
|
+
null,
|
|
93
|
+
2
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function templateMainTsx(name) {
|
|
97
|
+
const displayName = name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
98
|
+
return `import React, { useState, useCallback } from "react";
|
|
99
|
+
import {
|
|
100
|
+
render,
|
|
101
|
+
Box,
|
|
102
|
+
Text,
|
|
103
|
+
Input,
|
|
104
|
+
Button,
|
|
105
|
+
Checkbox,
|
|
106
|
+
Keybind,
|
|
107
|
+
Progress,
|
|
108
|
+
Spacer,
|
|
109
|
+
useApp,
|
|
110
|
+
} from "@semos-labs/glyph";
|
|
111
|
+
|
|
112
|
+
// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
113
|
+
|
|
114
|
+
interface Todo {
|
|
115
|
+
id: number;
|
|
116
|
+
text: string;
|
|
117
|
+
done: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let nextId = 1;
|
|
121
|
+
|
|
122
|
+
// \u2500\u2500 App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
123
|
+
|
|
124
|
+
function App() {
|
|
125
|
+
const { exit } = useApp();
|
|
126
|
+
|
|
127
|
+
const [todos, setTodos] = useState<Todo[]>([
|
|
128
|
+
{ id: nextId++, text: "Learn Glyph basics", done: true },
|
|
129
|
+
{ id: nextId++, text: "Build something cool", done: false },
|
|
130
|
+
{ id: nextId++, text: "Ship it", done: false },
|
|
131
|
+
]);
|
|
132
|
+
const [newTodo, setNewTodo] = useState("");
|
|
133
|
+
|
|
134
|
+
const doneCount = todos.filter((t) => t.done).length;
|
|
135
|
+
const progress = todos.length > 0 ? doneCount / todos.length : 0;
|
|
136
|
+
|
|
137
|
+
const handleAdd = useCallback(() => {
|
|
138
|
+
const text = newTodo.trim();
|
|
139
|
+
if (!text) return;
|
|
140
|
+
setTodos((prev) => [...prev, { id: nextId++, text, done: false }]);
|
|
141
|
+
setNewTodo("");
|
|
142
|
+
}, [newTodo]);
|
|
143
|
+
|
|
144
|
+
const handleToggle = useCallback((id: number) => {
|
|
145
|
+
setTodos((prev) =>
|
|
146
|
+
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
|
|
147
|
+
);
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
const handleDelete = useCallback((id: number) => {
|
|
151
|
+
setTodos((prev) => prev.filter((t) => t.id !== id));
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Box
|
|
156
|
+
style={{
|
|
157
|
+
flexDirection: "column",
|
|
158
|
+
width: "100%",
|
|
159
|
+
height: "100%",
|
|
160
|
+
padding: 1,
|
|
161
|
+
gap: 1,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{/* Quit shortcut */}
|
|
165
|
+
<Keybind keypress="q" onPress={() => exit()} />
|
|
166
|
+
|
|
167
|
+
{/* Header */}
|
|
168
|
+
<Box style={{ flexDirection: "row", alignItems: "center" }}>
|
|
169
|
+
<Text style={{ bold: true, color: "cyanBright" }}>\u2726 ${displayName}</Text>
|
|
170
|
+
<Spacer />
|
|
171
|
+
<Text style={{ dim: true }}>
|
|
172
|
+
{doneCount}/{todos.length} done
|
|
173
|
+
</Text>
|
|
174
|
+
</Box>
|
|
175
|
+
|
|
176
|
+
{/* Progress bar */}
|
|
177
|
+
<Progress value={progress} />
|
|
178
|
+
|
|
179
|
+
{/* Add todo */}
|
|
180
|
+
<Box style={{ flexDirection: "row", gap: 1 }}>
|
|
181
|
+
<Box style={{ flexGrow: 1 }}>
|
|
182
|
+
<Input
|
|
183
|
+
value={newTodo}
|
|
184
|
+
onChange={setNewTodo}
|
|
185
|
+
placeholder="What needs to be done?"
|
|
186
|
+
onSubmit={handleAdd}
|
|
187
|
+
style={{ bg: "blackBright", paddingX: 1 }}
|
|
188
|
+
focusedStyle={{ bg: "white", color: "black", paddingX: 1 }}
|
|
189
|
+
/>
|
|
190
|
+
</Box>
|
|
191
|
+
<Button
|
|
192
|
+
label=" add "
|
|
193
|
+
onPress={handleAdd}
|
|
194
|
+
style={{ bg: "blackBright", paddingX: 1 }}
|
|
195
|
+
focusedStyle={{ bg: "cyan", color: "black", paddingX: 1 }}
|
|
196
|
+
/>
|
|
197
|
+
</Box>
|
|
198
|
+
|
|
199
|
+
{/* Todo list */}
|
|
200
|
+
<Box style={{ flexDirection: "column" }}>
|
|
201
|
+
{todos.length === 0 && (
|
|
202
|
+
<Text style={{ dim: true, italic: true }}>No todos yet. Add one above!</Text>
|
|
203
|
+
)}
|
|
204
|
+
{todos.map((todo) => (
|
|
205
|
+
<Box key={todo.id} style={{ flexDirection: "row", gap: 1, alignItems: "center" }}>
|
|
206
|
+
<Checkbox
|
|
207
|
+
checked={todo.done}
|
|
208
|
+
onChange={() => handleToggle(todo.id)}
|
|
209
|
+
label={todo.text}
|
|
210
|
+
style={todo.done ? { dim: true } : {}}
|
|
211
|
+
focusedStyle={{ color: "cyanBright" }}
|
|
212
|
+
/>
|
|
213
|
+
<Spacer />
|
|
214
|
+
<Button
|
|
215
|
+
label=" \xD7 "
|
|
216
|
+
onPress={() => handleDelete(todo.id)}
|
|
217
|
+
style={{ dim: true }}
|
|
218
|
+
focusedStyle={{ color: "red" }}
|
|
219
|
+
/>
|
|
220
|
+
</Box>
|
|
221
|
+
))}
|
|
222
|
+
</Box>
|
|
223
|
+
|
|
224
|
+
{/* Footer */}
|
|
225
|
+
<Spacer />
|
|
226
|
+
<Text style={{ dim: true }}>
|
|
227
|
+
tab navigate \xB7 space toggle \xB7 enter submit \xB7 q quit
|
|
228
|
+
</Text>
|
|
229
|
+
</Box>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
render(<App />);
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
function templateGitignore() {
|
|
237
|
+
return `node_modules/
|
|
238
|
+
dist/
|
|
239
|
+
*.tsbuildinfo
|
|
240
|
+
.DS_Store
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
function printUsage() {
|
|
244
|
+
console.log();
|
|
245
|
+
console.log(` ${bold("create-glyph")} ${dim("\u2014 scaffold a new Glyph terminal UI app")}`);
|
|
246
|
+
console.log();
|
|
247
|
+
console.log(` ${bold("Usage:")}`);
|
|
248
|
+
console.log(` ${cyan("bun create @semos-labs/glyph")} ${dim("<project-name>")}`);
|
|
249
|
+
console.log(` ${cyan("npm create @semos-labs/glyph")} ${dim("<project-name>")}`);
|
|
250
|
+
console.log(` ${cyan("pnpm create @semos-labs/glyph")} ${dim("<project-name>")}`);
|
|
251
|
+
console.log(` ${cyan("yarn create @semos-labs/glyph")} ${dim("<project-name>")}`);
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(` ${bold("Options:")}`);
|
|
254
|
+
console.log(` ${cyan("-h, --help")} Show this help message`);
|
|
255
|
+
console.log();
|
|
256
|
+
}
|
|
257
|
+
function main() {
|
|
258
|
+
const args = process.argv.slice(2);
|
|
259
|
+
if (args.includes("-h") || args.includes("--help") || args.length === 0) {
|
|
260
|
+
printUsage();
|
|
261
|
+
if (args.length === 0) {
|
|
262
|
+
console.log(
|
|
263
|
+
` ${red("\u2717")} Missing project name. Pass it as an argument.
|
|
264
|
+
`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
process.exit(args.includes("-h") || args.includes("--help") ? 0 : 1);
|
|
268
|
+
}
|
|
269
|
+
const projectName = args.find((a) => !a.startsWith("-"));
|
|
270
|
+
if (!projectName) {
|
|
271
|
+
console.log(`
|
|
272
|
+
${red("\u2717")} Missing project name.
|
|
273
|
+
`);
|
|
274
|
+
printUsage();
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
if (!/^[a-zA-Z0-9_@][a-zA-Z0-9._\-/]*$/.test(projectName)) {
|
|
278
|
+
console.log(
|
|
279
|
+
`
|
|
280
|
+
${red("\u2717")} Invalid project name: ${bold(projectName)}
|
|
281
|
+
`
|
|
282
|
+
);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
286
|
+
const dirName = path.basename(targetDir);
|
|
287
|
+
const pm = detectPackageManager();
|
|
288
|
+
console.log();
|
|
289
|
+
console.log(` ${cyan("\u25C6")} ${bold("Creating")} ${green(dirName)}${dim("...")}`);
|
|
290
|
+
if (fs.existsSync(targetDir)) {
|
|
291
|
+
const entries = fs.readdirSync(targetDir);
|
|
292
|
+
if (entries.length > 0) {
|
|
293
|
+
console.log(
|
|
294
|
+
`
|
|
295
|
+
${red("\u2717")} Directory ${bold(dirName)} already exists and is not empty.
|
|
296
|
+
`
|
|
297
|
+
);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
|
|
302
|
+
const files = [
|
|
303
|
+
["package.json", templatePackageJson(dirName)],
|
|
304
|
+
["tsconfig.json", templateTsconfig()],
|
|
305
|
+
["src/main.tsx", templateMainTsx(dirName)],
|
|
306
|
+
[".gitignore", templateGitignore()]
|
|
307
|
+
];
|
|
308
|
+
for (const [filePath, content] of files) {
|
|
309
|
+
const fullPath = path.join(targetDir, filePath);
|
|
310
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
311
|
+
console.log(` ${dim("\u251C")} ${green("+")} ${filePath}`);
|
|
312
|
+
}
|
|
313
|
+
console.log();
|
|
314
|
+
console.log(` ${green("\u2713")} Project created!`);
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(` ${bold("Next steps:")}`);
|
|
317
|
+
console.log();
|
|
318
|
+
console.log(` ${cyan("cd")} ${dirName}`);
|
|
319
|
+
console.log(` ${cyan(getInstallCommand(pm))}`);
|
|
320
|
+
console.log(` ${cyan(getRunCommand(pm))}`);
|
|
321
|
+
console.log();
|
|
322
|
+
console.log(` ${dim("Happy hacking! \u2726")}`);
|
|
323
|
+
console.log();
|
|
324
|
+
}
|
|
325
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@semos-labs/create-glyph",
|
|
3
|
+
"version": "0.1.74",
|
|
4
|
+
"description": "Scaffold a new Glyph terminal UI app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-glyph": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20",
|
|
19
|
+
"tsup": "^8",
|
|
20
|
+
"typescript": "^5.5"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"glyph",
|
|
24
|
+
"create",
|
|
25
|
+
"scaffold",
|
|
26
|
+
"terminal",
|
|
27
|
+
"tui",
|
|
28
|
+
"cli",
|
|
29
|
+
"react"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/nick-skriabin/glyph.git"
|
|
35
|
+
}
|
|
36
|
+
}
|