create-giggles-app 0.1.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 ADDED
@@ -0,0 +1,14 @@
1
+ # create-giggles-app
2
+
3
+ the easiest way to build a terminal app. powered by [giggles](https://www.npmjs.com/package/giggles).
4
+
5
+ ```
6
+ npx create giggles-app
7
+ ```
8
+
9
+ you can also use pnpm or yarn:
10
+
11
+ ```
12
+ pnpm create-giggles-app
13
+ yarn create giggles-app
14
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ // src/index.ts
3
+ import * as p from '@clack/prompts';
4
+ import { execSync } from 'child_process';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+
8
+ var GIGGLES_VERSION = '0.2.2';
9
+ function detectPackageManager() {
10
+ const userAgent = process.env.npm_config_user_agent ?? '';
11
+ if (userAgent.startsWith('yarn')) return 'yarn';
12
+ if (userAgent.startsWith('pnpm')) return 'pnpm';
13
+ return 'npm';
14
+ }
15
+ function copyDir(src, dest, rename) {
16
+ fs.mkdirSync(dest, { recursive: true });
17
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
18
+ const srcPath = path.join(src, entry.name);
19
+ const destName = rename?.[entry.name] ?? entry.name;
20
+ const destPath = path.join(dest, destName);
21
+ if (entry.isDirectory()) {
22
+ copyDir(srcPath, destPath, rename);
23
+ } else {
24
+ fs.copyFileSync(srcPath, destPath);
25
+ }
26
+ }
27
+ }
28
+ async function main() {
29
+ p.intro('create-giggles-app');
30
+ const argName = process.argv[2];
31
+ const options = await p.group(
32
+ {
33
+ name: () =>
34
+ p.text({
35
+ message: 'Project name?',
36
+ placeholder: 'my-tui',
37
+ initialValue: argName ?? '',
38
+ validate: (value) => {
39
+ if (!value.trim()) return 'Project name is required.';
40
+ if (/[^\w\-.]/.test(value)) return 'Project name contains invalid characters.';
41
+ }
42
+ }),
43
+ location: ({ results }) =>
44
+ p.text({
45
+ message: 'Where should we create the project?',
46
+ placeholder: `./${results.name}`,
47
+ initialValue: `./${results.name}`
48
+ }),
49
+ language: () =>
50
+ p.select({
51
+ message: 'Language?',
52
+ options: [
53
+ { value: 'typescript', label: 'TypeScript', hint: 'recommended' },
54
+ { value: 'javascript', label: 'JavaScript' }
55
+ ]
56
+ }),
57
+ linting: () =>
58
+ p.confirm({
59
+ message: 'Add ESLint + Prettier?',
60
+ initialValue: true
61
+ }),
62
+ install: () =>
63
+ p.confirm({
64
+ message: 'Install dependencies?',
65
+ initialValue: true
66
+ })
67
+ },
68
+ {
69
+ onCancel: () => {
70
+ p.cancel('Cancelled.');
71
+ process.exit(0);
72
+ }
73
+ }
74
+ );
75
+ const projectDir = path.resolve(options.location);
76
+ const templatesDir = path.resolve(import.meta.dirname, '..', 'templates');
77
+ const isTs = options.language === 'typescript';
78
+ const pm = detectPackageManager();
79
+ const s = p.spinner();
80
+ if (fs.existsSync(projectDir)) {
81
+ const files = fs.readdirSync(projectDir);
82
+ if (files.length > 0) {
83
+ p.cancel(`Directory ${options.location} is not empty.`);
84
+ process.exit(1);
85
+ }
86
+ }
87
+ fs.mkdirSync(projectDir, { recursive: true });
88
+ s.start('Scaffolding project...');
89
+ copyDir(path.join(templatesDir, 'base'), projectDir, {
90
+ _gitignore: '.gitignore'
91
+ });
92
+ const langDir = isTs ? 'typescript' : 'javascript';
93
+ copyDir(path.join(templatesDir, langDir), projectDir);
94
+ if (options.linting) {
95
+ copyDir(path.join(templatesDir, 'linting'), projectDir, {
96
+ _prettierrc: '.prettierrc',
97
+ _prettierignore: '.prettierignore'
98
+ });
99
+ }
100
+ const ext = isTs ? 'tsx' : 'jsx';
101
+ const scripts = {
102
+ dev: `tsx --watch src/index.${ext}`,
103
+ build: 'tsup',
104
+ start: 'node dist/index.js'
105
+ };
106
+ const deps = {
107
+ giggles: `^${GIGGLES_VERSION}`,
108
+ ink: '^6.6.0',
109
+ react: '^19.2.4'
110
+ };
111
+ const devDeps = {
112
+ tsx: '^4.21.0',
113
+ tsup: '^8.5.1'
114
+ };
115
+ if (isTs) {
116
+ devDeps['@types/react'] = '^19.2.13';
117
+ devDeps['typescript'] = '^5.0.3';
118
+ }
119
+ if (options.linting) {
120
+ scripts['lint'] = 'prettier --write . && eslint . --fix';
121
+ devDeps['prettier'] = '^2.8.7';
122
+ devDeps['@trivago/prettier-plugin-sort-imports'] = '^4.3.0';
123
+ devDeps['eslint'] = '^9.0.0';
124
+ devDeps['eslint-plugin-react'] = '^7.32.2';
125
+ devDeps['eslint-plugin-react-hooks'] = '^7.0.1';
126
+ devDeps['typescript-eslint'] = '^8.0.0';
127
+ }
128
+ const pkg = {
129
+ name: options.name,
130
+ version: '0.1.0',
131
+ type: 'module',
132
+ scripts,
133
+ dependencies: deps,
134
+ devDependencies: devDeps
135
+ };
136
+ fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
137
+ s.stop('Project scaffolded.');
138
+ if (options.install) {
139
+ s.start('Installing dependencies...');
140
+ try {
141
+ execSync(`${pm} install`, { cwd: projectDir, stdio: 'ignore' });
142
+ s.stop('Dependencies installed.');
143
+ } catch {
144
+ s.stop('Failed to install dependencies.');
145
+ p.log.warning(`Run \`${pm} install\` manually in the project directory.`);
146
+ }
147
+ }
148
+ const relative = path.relative(process.cwd(), projectDir);
149
+ p.note([`cd ${relative}`, `${pm} dev`].join('\n'), 'Next steps');
150
+ p.outro('Happy building!');
151
+ }
152
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "create-giggles-app",
3
+ "version": "0.1.0",
4
+ "description": "scaffold a giggles terminal app",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/zion-off/giggles.git",
9
+ "directory": "tools/create-giggles-app"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "create-giggles-app": "dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "templates"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "lint": "prettier --write . && eslint . --fix"
22
+ },
23
+ "dependencies": {
24
+ "@clack/prompts": "^0.10.0"
25
+ },
26
+ "devDependencies": {
27
+ "@trivago/prettier-plugin-sort-imports": "^4.3.0",
28
+ "@types/node": "^22.0.0",
29
+ "eslint": "^9.0.0",
30
+ "eslint-plugin-react": "^7.32.2",
31
+ "eslint-plugin-react-hooks": "^7.0.1",
32
+ "prettier": "^2.8.7",
33
+ "tsup": "^8.5.1",
34
+ "typescript": "^5.0.3",
35
+ "typescript-eslint": "^8.0.0"
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ .DS_Store
@@ -0,0 +1,33 @@
1
+ import { FocusGroup, GigglesProvider, useFocus, useKeybindings } from 'giggles';
2
+ import { useState } from 'react';
3
+ import { Box, Text, render } from 'ink';
4
+
5
+ function MenuItem({ label }) {
6
+ const focus = useFocus();
7
+ const [selected, setSelected] = useState(false);
8
+
9
+ useKeybindings(focus, {
10
+ enter: () => setSelected(!selected)
11
+ });
12
+
13
+ return (
14
+ <Text color={focus.focused ? 'green' : 'white'}>
15
+ {focus.focused ? '> ' : ' '}
16
+ {label}
17
+ {selected ? ' ✓' : ''}
18
+ </Text>
19
+ );
20
+ }
21
+
22
+ render(
23
+ <GigglesProvider>
24
+ <Box flexDirection="column">
25
+ <Text bold>My Menu (j/k to navigate, enter to select)</Text>
26
+ <FocusGroup direction="vertical">
27
+ <MenuItem label="Start Game" />
28
+ <MenuItem label="Settings" />
29
+ <MenuItem label="Exit" />
30
+ </FocusGroup>
31
+ </Box>
32
+ </GigglesProvider>
33
+ );
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.jsx'],
5
+ format: ['esm'],
6
+ target: 'node18',
7
+ clean: true,
8
+ external: ['react', 'ink']
9
+ });
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ pnpm-lock.yaml
@@ -0,0 +1,17 @@
1
+ {
2
+ "printWidth": 120,
3
+ "useTabs": false,
4
+ "tabWidth": 2,
5
+ "trailingComma": "none",
6
+ "semi": true,
7
+ "singleQuote": true,
8
+ "bracketSpacing": true,
9
+ "arrowParens": "always",
10
+ "jsxSingleQuote": false,
11
+ "bracketSameLine": false,
12
+ "endOfLine": "lf",
13
+ "importOrder": ["^react$", "^react/(.*)$", "^ink", "^@/(.*)$", "^[./]"],
14
+ "importOrderSeparation": false,
15
+ "importOrderSortSpecifiers": true,
16
+ "plugins": ["@trivago/prettier-plugin-sort-imports"]
17
+ }
@@ -0,0 +1,30 @@
1
+ import react from 'eslint-plugin-react';
2
+ import reactHooks from 'eslint-plugin-react-hooks';
3
+ import tseslint from 'typescript-eslint';
4
+
5
+ export default [
6
+ ...tseslint.configs.recommended,
7
+ {
8
+ files: ['**/*.{ts,tsx}'],
9
+ plugins: {
10
+ react,
11
+ 'react-hooks': reactHooks
12
+ },
13
+ rules: {
14
+ ...react.configs.recommended.rules,
15
+ 'react/prop-types': 'off',
16
+ 'react/react-in-jsx-scope': 'off',
17
+ 'react-hooks/rules-of-hooks': 'error',
18
+ 'react-hooks/exhaustive-deps': 'warn',
19
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]
20
+ },
21
+ settings: {
22
+ react: {
23
+ version: 'detect'
24
+ }
25
+ }
26
+ },
27
+ {
28
+ ignores: ['dist/**']
29
+ }
30
+ ];
@@ -0,0 +1,33 @@
1
+ import { FocusGroup, GigglesProvider, useFocus, useKeybindings } from 'giggles';
2
+ import { useState } from 'react';
3
+ import { Box, Text, render } from 'ink';
4
+
5
+ function MenuItem({ label }: { label: string }) {
6
+ const focus = useFocus();
7
+ const [selected, setSelected] = useState(false);
8
+
9
+ useKeybindings(focus, {
10
+ enter: () => setSelected(!selected)
11
+ });
12
+
13
+ return (
14
+ <Text color={focus.focused ? 'green' : 'white'}>
15
+ {focus.focused ? '> ' : ' '}
16
+ {label}
17
+ {selected ? ' ✓' : ''}
18
+ </Text>
19
+ );
20
+ }
21
+
22
+ render(
23
+ <GigglesProvider>
24
+ <Box flexDirection="column">
25
+ <Text bold>My Menu (j/k to navigate, enter to select)</Text>
26
+ <FocusGroup direction="vertical">
27
+ <MenuItem label="Start Game" />
28
+ <MenuItem label="Settings" />
29
+ <MenuItem label="Exit" />
30
+ </FocusGroup>
31
+ </Box>
32
+ </GigglesProvider>
33
+ );
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "jsx": "react-jsx",
11
+ "baseUrl": ".",
12
+ "paths": {
13
+ "@/*": ["src/*"]
14
+ }
15
+ },
16
+ "include": ["src"]
17
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.tsx'],
5
+ format: ['esm'],
6
+ target: 'node18',
7
+ clean: true,
8
+ dts: true,
9
+ external: ['react', 'ink']
10
+ });