@raketa-cloud/cli 1.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/CLAUDE.md +248 -0
- package/README.md +153 -0
- package/bin/tmux +27 -0
- package/package.json +20 -0
- package/src/commands/commands/index.js +27 -0
- package/src/commands/commands/list/index.js +43 -0
- package/src/commands/commands/new/index.js +109 -0
- package/src/commands/skills/index.js +89 -0
- package/src/commands/skills/install/index.js +40 -0
- package/src/commands/skills/install-global/index.js +40 -0
- package/src/commands/skills/list/index.js +43 -0
- package/src/commands/skills/list-all/index.js +56 -0
- package/src/commands/skills/list-remote/index.js +35 -0
- package/src/commands/skills/new/index.js +109 -0
- package/src/commands/skills/rm/index.js +65 -0
- package/src/commands/skills/rm-global/index.js +66 -0
- package/src/commands/widgets/index.js +29 -0
- package/src/commands/widgets/list/index.js +41 -0
- package/src/commands/widgets/new/index.js +186 -0
- package/src/index.js +17 -0
- package/src/lib/Command.js +14 -0
- package/src/lib/skillsUtils.js +246 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Development Commands
|
|
6
|
+
|
|
7
|
+
### Local Development Setup
|
|
8
|
+
```bash
|
|
9
|
+
npm install # Install dependencies
|
|
10
|
+
npm link # Link CLI globally to use local version
|
|
11
|
+
raketa-cli --help # Test the CLI
|
|
12
|
+
npm unlink -g @raketa-cloud/cli # Unlink when done
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
After `npm link`, the `raketa-cli` command uses your local development version. Changes to source files are immediately available.
|
|
16
|
+
|
|
17
|
+
### Testing Commands
|
|
18
|
+
There is no test suite currently. Manual testing is done by running commands:
|
|
19
|
+
```bash
|
|
20
|
+
raketa-cli widgets list
|
|
21
|
+
raketa-cli skills list-all
|
|
22
|
+
raketa-cli commands list
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### No Build/Compilation
|
|
26
|
+
This project uses ES modules directly with no build step. Changes are immediately available after saving.
|
|
27
|
+
|
|
28
|
+
## Architecture Overview
|
|
29
|
+
|
|
30
|
+
### Command Registration Flow
|
|
31
|
+
|
|
32
|
+
The CLI is built on Commander.js with a factory pattern for command namespaces:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/index.js (entry point)
|
|
36
|
+
├── createWidgetsCommand() → returns widgets namespace
|
|
37
|
+
├── createSkillsCommand() → returns skills namespace
|
|
38
|
+
└── createCommandsCommand() → returns commands namespace
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Each namespace factory (e.g., `createWidgetsCommand()`) is defined in its own directory and registers multiple sub-commands.
|
|
42
|
+
|
|
43
|
+
### Command Implementation Pattern
|
|
44
|
+
|
|
45
|
+
All commands follow this structure:
|
|
46
|
+
|
|
47
|
+
1. **Command Class** - Extends base `Command` class from `src/lib/Command.js`
|
|
48
|
+
2. **Single Process Method** - Async `process()` method is the entry point
|
|
49
|
+
3. **Error Handling** - Try/catch with `console.error()` and `process.exit(1)`
|
|
50
|
+
4. **Options Access** - Use `this.getOptions()` to access CLI flags/arguments
|
|
51
|
+
5. **Path Resolution** - Use `this.getCWD()` to get current working directory
|
|
52
|
+
|
|
53
|
+
Example structure:
|
|
54
|
+
```javascript
|
|
55
|
+
import { Command } from '../../../lib/Command.js';
|
|
56
|
+
|
|
57
|
+
export class MyCommand extends Command {
|
|
58
|
+
async process() {
|
|
59
|
+
try {
|
|
60
|
+
const cwd = this.getCWD();
|
|
61
|
+
const options = this.getOptions();
|
|
62
|
+
// Implementation here
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error:', error.message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Adding a New Command
|
|
72
|
+
|
|
73
|
+
To add a new sub-command to an existing namespace:
|
|
74
|
+
|
|
75
|
+
1. Create directory: `src/commands/<namespace>/<command>/index.js`
|
|
76
|
+
2. Create command class extending `Command` with `async process()` method
|
|
77
|
+
3. Register in namespace file: `src/commands/<namespace>/index.js`
|
|
78
|
+
4. Add to namespace using Commander's `.command()` and `.action()` pattern
|
|
79
|
+
|
|
80
|
+
Example registration:
|
|
81
|
+
```javascript
|
|
82
|
+
widgets
|
|
83
|
+
.command('my-command')
|
|
84
|
+
.description('What this command does')
|
|
85
|
+
.option('--name <value>', 'Option description')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
const command = new MyCommand(options);
|
|
88
|
+
await command.process();
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Path Resolution Patterns
|
|
93
|
+
|
|
94
|
+
Three common patterns used throughout the codebase:
|
|
95
|
+
|
|
96
|
+
1. **Project-relative paths** (from where command is run):
|
|
97
|
+
```javascript
|
|
98
|
+
const cwd = this.getCWD(); // process.cwd()
|
|
99
|
+
const widgetsPath = join(cwd, 'src', 'components', 'widgets');
|
|
100
|
+
const skillsPath = join(cwd, '.claude', 'skills');
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
2. **Home directory paths** (for global resources):
|
|
104
|
+
```javascript
|
|
105
|
+
import { homedir } from 'os';
|
|
106
|
+
const globalSkillsPath = join(homedir(), '.claude', 'skills');
|
|
107
|
+
const cachePath = join(homedir(), '.claude', 'cache', 'remote-skills');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
3. **Relative to source file** (for discovery):
|
|
111
|
+
```javascript
|
|
112
|
+
import { fileURLToPath } from 'url';
|
|
113
|
+
import { dirname } from 'path';
|
|
114
|
+
|
|
115
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
116
|
+
const commandsDir = join(dirname(currentFile), '..', '..');
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Skills System Architecture
|
|
120
|
+
|
|
121
|
+
The skills system manages Claude AI agent capabilities with dual scope (local and global):
|
|
122
|
+
|
|
123
|
+
**Data Flow:**
|
|
124
|
+
```
|
|
125
|
+
Remote Repo (GitHub)
|
|
126
|
+
↓ git clone/pull
|
|
127
|
+
Cache (~/.claude/cache/remote-skills/)
|
|
128
|
+
↓ copy
|
|
129
|
+
Local (./.claude/skills/) OR Global (~/.claude/skills/)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Key Components:**
|
|
133
|
+
- `src/lib/skillsUtils.js` - Shared utilities for skills management
|
|
134
|
+
- Remote repository: `git@github.com:studioraketa/claude-skills.git`
|
|
135
|
+
- SKILL.md format: YAML frontmatter with `name` and `description` fields
|
|
136
|
+
|
|
137
|
+
**Local vs Global:**
|
|
138
|
+
- Local skills: `.claude/skills/` (project-specific)
|
|
139
|
+
- Global skills: `~/.claude/skills/` (system-wide)
|
|
140
|
+
- Each has separate install/remove commands
|
|
141
|
+
- `list-all` shows both with section headers
|
|
142
|
+
|
|
143
|
+
### File Operations
|
|
144
|
+
|
|
145
|
+
Uses modern `fs/promises` API throughout:
|
|
146
|
+
- `readdir(path, { withFileTypes: true })` - List directories
|
|
147
|
+
- `access(path)` - Check existence
|
|
148
|
+
- `mkdir(path, { recursive: true })` - Create directories
|
|
149
|
+
- `writeFile(path, content, 'utf-8')` - Write files
|
|
150
|
+
- `cp(source, dest, { recursive: true })` - Copy directories
|
|
151
|
+
- `rm(path, { recursive: true, force: true })` - Delete directories
|
|
152
|
+
|
|
153
|
+
Templates are embedded as template literals in command files, not stored as separate files.
|
|
154
|
+
|
|
155
|
+
### Error Handling Guidelines
|
|
156
|
+
|
|
157
|
+
**Validation Errors:**
|
|
158
|
+
```javascript
|
|
159
|
+
if (!options.name) {
|
|
160
|
+
console.error('Error: --name option is required');
|
|
161
|
+
console.error('Usage: widgets new --name "Hero"');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**File Operations:**
|
|
167
|
+
```javascript
|
|
168
|
+
try {
|
|
169
|
+
await access(skillPath);
|
|
170
|
+
} catch {
|
|
171
|
+
console.error(`Error: Skill '${skillName}' not found`);
|
|
172
|
+
console.error(`Run 'skills list' to see available skills`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**External Integration (git operations):**
|
|
178
|
+
```javascript
|
|
179
|
+
// Provide specific guidance for common errors
|
|
180
|
+
if (error.code === 'ENOENT') {
|
|
181
|
+
throw new Error('git is not installed. Please install git to use this command.');
|
|
182
|
+
}
|
|
183
|
+
if (stderr.includes('Permission denied')) {
|
|
184
|
+
throw new Error('SSH authentication failed. Ensure your SSH key is configured.');
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Always include helpful next steps in error messages.
|
|
189
|
+
|
|
190
|
+
### Console Output Conventions
|
|
191
|
+
|
|
192
|
+
- **Errors:** `console.error('Error: <message>')` followed by `process.exit(1)`
|
|
193
|
+
- **Warnings:** `console.warn('Warning: <message>')` (non-fatal)
|
|
194
|
+
- **Success:** `console.log('✓ Created ...')` with checkmark
|
|
195
|
+
- **Instructions:** Multi-line with blank lines for readability
|
|
196
|
+
- **Colors:** Use chalk for emphasis (cyan for names, dim for descriptions, yellow for warnings)
|
|
197
|
+
|
|
198
|
+
### Shared Utilities
|
|
199
|
+
|
|
200
|
+
`src/lib/skillsUtils.js` provides:
|
|
201
|
+
- `parseSkillMetadata(skillMdPath)` - Extracts YAML frontmatter from SKILL.md
|
|
202
|
+
- `formatSkillWithDescription(name, desc)` - Consistent colored output
|
|
203
|
+
- `ensureRemoteRepo(repoUrl, cachePath)` - Clone/update remote skills repo
|
|
204
|
+
- `getSkillsWithDescriptions(skillsPath)` - List skills with metadata
|
|
205
|
+
- `copySkillFromCache(skillName, cachePath, destPath, overwrite)` - Install skill
|
|
206
|
+
|
|
207
|
+
Use these instead of duplicating logic across commands.
|
|
208
|
+
|
|
209
|
+
## Project Conventions
|
|
210
|
+
|
|
211
|
+
### Naming Conventions
|
|
212
|
+
- Widget names: PascalCase with "Widget" suffix (e.g., `HeroWidget`)
|
|
213
|
+
- Skill names: kebab-case (e.g., `my-skill`)
|
|
214
|
+
- Command names: kebab-case with namespace (e.g., `namespace.command`)
|
|
215
|
+
|
|
216
|
+
### File Structure Conventions
|
|
217
|
+
- Widgets: `src/components/widgets/<WidgetName>/` with 5 template files
|
|
218
|
+
- Skills: `.claude/skills/<skill-name>/SKILL.md` (YAML frontmatter required)
|
|
219
|
+
- Commands: `src/commands/<namespace>/<command>/index.js`
|
|
220
|
+
|
|
221
|
+
### Import/Export Style
|
|
222
|
+
- Use ES module syntax (`import`/`export`)
|
|
223
|
+
- Named exports for command classes
|
|
224
|
+
- Named exports for factory functions (`createWidgetsCommand`)
|
|
225
|
+
- Use `.js` extensions in import paths (required for ES modules)
|
|
226
|
+
|
|
227
|
+
## Important Context
|
|
228
|
+
|
|
229
|
+
### Dependencies
|
|
230
|
+
Only 3 production dependencies:
|
|
231
|
+
- **commander** (v14.0.2) - CLI framework
|
|
232
|
+
- **chalk** (v5.3.0) - Terminal colors
|
|
233
|
+
- **js-yaml** (v4.1.0) - YAML parsing for skill metadata
|
|
234
|
+
|
|
235
|
+
Keep dependencies minimal. Prefer Node.js built-ins when possible.
|
|
236
|
+
|
|
237
|
+
### Git Integration
|
|
238
|
+
Skills commands use git via `child_process.execFile()` with 30-second timeout. Git operations are error-tolerant:
|
|
239
|
+
- Clone failures provide specific guidance (SSH, git not installed, etc.)
|
|
240
|
+
- Pull failures in cache update are warnings only (cached data still usable)
|
|
241
|
+
- Network errors show user-friendly messages
|
|
242
|
+
|
|
243
|
+
### ES Modules
|
|
244
|
+
This project uses `"type": "module"` in package.json:
|
|
245
|
+
- Use `import`/`export` syntax
|
|
246
|
+
- Use `import.meta.url` instead of `__dirname`
|
|
247
|
+
- Use `.js` extensions in all imports
|
|
248
|
+
- No CommonJS (`require`) syntax
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Raketa CLI
|
|
2
|
+
|
|
3
|
+
A collection of useful CLI tools for humans and coding agents.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Raketa CLI is a command-line toolkit designed to streamline the creation and management of three key artifact types in your projects:
|
|
8
|
+
|
|
9
|
+
- **Widgets** - React UI components
|
|
10
|
+
- **Skills** - Claude AI agent capabilities and prompts
|
|
11
|
+
- **Commands** - Custom CLI commands
|
|
12
|
+
|
|
13
|
+
## Exploring Available Commands
|
|
14
|
+
|
|
15
|
+
To see all available commands and options:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @raketa-cloud/cli --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
To get detailed help for a specific command:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @raketa-cloud/cli widgets --help
|
|
25
|
+
npx @raketa-cloud/cli skills --help
|
|
26
|
+
npx @raketa-cloud/cli commands --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Each command namespace has multiple sub-commands. Use the `--help` flag to discover what's available and explore the full capabilities of the tool.
|
|
30
|
+
|
|
31
|
+
## Usage Examples
|
|
32
|
+
|
|
33
|
+
### Working with Widgets
|
|
34
|
+
|
|
35
|
+
List all widgets in your project:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx @raketa-cloud/cli widgets list
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Create a new widget:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @raketa-cloud/cli widgets new --name MyWidget
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Working with Skills
|
|
48
|
+
|
|
49
|
+
List all skills (local and global):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx @raketa-cloud/cli skills list-all
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
View available skills from the remote repository:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx @raketa-cloud/cli skills list-remote
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Install a skill locally to your project:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @raketa-cloud/cli skills install my-skill-name
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Install a skill globally for system-wide use:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx @raketa-cloud/cli skills install-global my-skill-name
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Create a new custom skill:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx @raketa-cloud/cli skills new --name my-custom-skill
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Working with Commands
|
|
80
|
+
|
|
81
|
+
List all custom commands:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx @raketa-cloud/cli commands list
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Create a new command:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx @raketa-cloud/cli commands new --name "namespace.command"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Local Development
|
|
94
|
+
|
|
95
|
+
If you want to develop or contribute to Raketa CLI itself, follow these steps:
|
|
96
|
+
|
|
97
|
+
### Setup
|
|
98
|
+
|
|
99
|
+
1. Clone the repository and navigate to the project directory:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone <repository-url>
|
|
103
|
+
cd raketa-cli
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
2. Install dependencies:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
3. Link the package globally to use your local version:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm link
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
After running `npm link`, the `raketa-cli` command will use your local development version instead of the published package. Any changes you make to the source code will be immediately available.
|
|
119
|
+
|
|
120
|
+
### Testing Your Changes
|
|
121
|
+
|
|
122
|
+
Run commands directly to test your changes:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
raketa-cli widgets list
|
|
126
|
+
raketa-cli skills --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Cleanup
|
|
130
|
+
|
|
131
|
+
When you're done developing, unlink the package:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm unlink -g @raketa-cloud/cli
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This removes the global symlink and restores the default behavior.
|
|
138
|
+
|
|
139
|
+
## Project Structure
|
|
140
|
+
|
|
141
|
+
The CLI is built using Commander.js and follows a modular command structure:
|
|
142
|
+
|
|
143
|
+
- `src/commands/` - Command implementations organized by namespace
|
|
144
|
+
- `src/index.js` - Main entry point and command registration
|
|
145
|
+
- Each command extends a base `Command` class for consistency
|
|
146
|
+
|
|
147
|
+
## Contributing
|
|
148
|
+
|
|
149
|
+
Contributions are welcome! Please follow the existing patterns when adding new commands or features.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
See LICENSE file for details.
|
package/bin/tmux
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Get the absolute path to the project root (where this script lives)
|
|
4
|
+
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
# Session name
|
|
7
|
+
SESSION="raketa-cli"
|
|
8
|
+
|
|
9
|
+
# Check if session already exists
|
|
10
|
+
tmux has-session -t $SESSION 2>/dev/null
|
|
11
|
+
|
|
12
|
+
if [ $? != 0 ]; then
|
|
13
|
+
tmux new-session -d -s $SESSION -n "ai" -c "$PROJECT_ROOT"
|
|
14
|
+
tmux send-keys -t $SESSION "claude --dangerously-skip-permissions" C-m
|
|
15
|
+
|
|
16
|
+
tmux new-window -t $SESSION -n "editor" -c "$PROJECT_ROOT"
|
|
17
|
+
tmux send-keys -t $SESSION "vim" C-m
|
|
18
|
+
|
|
19
|
+
# Create blank window in project root
|
|
20
|
+
tmux new-window -t $SESSION -c "$PROJECT_ROOT"
|
|
21
|
+
|
|
22
|
+
# Select the blank window
|
|
23
|
+
tmux select-window -t $SESSION:3
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Attach to the session
|
|
27
|
+
tmux attach-session -t $SESSION
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raketa-cloud/cli",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Collection of useful CLI tools for humans and coding agents. ",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Vestimir Markov",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"raketa-cli": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"chalk": "^5.3.0",
|
|
17
|
+
"commander": "^14.0.2",
|
|
18
|
+
"js-yaml": "^4.1.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createCommand } from 'commander';
|
|
2
|
+
import { ListCommandsCommand } from './list/index.js';
|
|
3
|
+
import { NewCommandCommand } from './new/index.js';
|
|
4
|
+
|
|
5
|
+
export function createCommandsCommand() {
|
|
6
|
+
const commands = createCommand('commands');
|
|
7
|
+
commands.description('Manage CLI commands');
|
|
8
|
+
|
|
9
|
+
commands
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List all commands')
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
const command = new ListCommandsCommand(options);
|
|
14
|
+
await command.process();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
commands
|
|
18
|
+
.command('new')
|
|
19
|
+
.description('Create a new command')
|
|
20
|
+
.option('--name <value>', 'Command name in format "namespace.command"')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
const command = new NewCommandCommand(options);
|
|
23
|
+
await command.process();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return commands;
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Command } from '../../../lib/Command.js';
|
|
2
|
+
import { readdir } from 'fs/promises';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
export class ListCommandsCommand extends Command {
|
|
7
|
+
async process() {
|
|
8
|
+
try {
|
|
9
|
+
// Get the path to src/commands directory
|
|
10
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
11
|
+
const commandsDir = join(dirname(currentFile), '..', '..');
|
|
12
|
+
|
|
13
|
+
// Read all namespace directories
|
|
14
|
+
const namespaces = await readdir(commandsDir, { withFileTypes: true });
|
|
15
|
+
|
|
16
|
+
// Filter only directories
|
|
17
|
+
const namespaceDirs = namespaces.filter(dirent => dirent.isDirectory());
|
|
18
|
+
|
|
19
|
+
// Iterate through each namespace
|
|
20
|
+
for (const namespace of namespaceDirs) {
|
|
21
|
+
const namespacePath = join(commandsDir, namespace.name);
|
|
22
|
+
|
|
23
|
+
// Read commands in this namespace
|
|
24
|
+
const items = await readdir(namespacePath, { withFileTypes: true });
|
|
25
|
+
const commands = items.filter(dirent =>
|
|
26
|
+
dirent.isDirectory() && dirent.name !== 'node_modules'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Print namespace and its commands
|
|
30
|
+
if (commands.length > 0) {
|
|
31
|
+
console.log(namespace.name);
|
|
32
|
+
commands.forEach(cmd => {
|
|
33
|
+
console.log(` ${cmd.name}`);
|
|
34
|
+
});
|
|
35
|
+
console.log(); // Blank line between namespaces
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Error listing commands:', error.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from '../../../lib/Command.js';
|
|
2
|
+
import { mkdir, writeFile, access } from 'fs/promises';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
export class NewCommandCommand extends Command {
|
|
7
|
+
async process() {
|
|
8
|
+
const options = this.getOptions();
|
|
9
|
+
|
|
10
|
+
// Validate --name option
|
|
11
|
+
if (!options.name) {
|
|
12
|
+
console.error('Error: --name option is required');
|
|
13
|
+
console.error('Usage: commands new --name "namespace.command"');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Parse namespace and command
|
|
18
|
+
const parts = options.name.split('.');
|
|
19
|
+
if (parts.length !== 2) {
|
|
20
|
+
console.error('Error: --name must be in format "namespace.command"');
|
|
21
|
+
console.error('Example: commands new --name "commands.install"');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const [namespace, commandName] = parts;
|
|
26
|
+
|
|
27
|
+
// Validate names (alphanumeric and hyphens only)
|
|
28
|
+
const validNameRegex = /^[a-z0-9-]+$/;
|
|
29
|
+
if (!validNameRegex.test(namespace) || !validNameRegex.test(commandName)) {
|
|
30
|
+
console.error('Error: Namespace and command names must contain only lowercase letters, numbers, and hyphens');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Calculate paths
|
|
35
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
36
|
+
const commandsDir = join(dirname(currentFile), '..', '..');
|
|
37
|
+
const namespacePath = join(commandsDir, namespace);
|
|
38
|
+
const commandPath = join(namespacePath, commandName);
|
|
39
|
+
const indexPath = join(commandPath, 'index.js');
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Check if command already exists
|
|
43
|
+
try {
|
|
44
|
+
await access(indexPath);
|
|
45
|
+
console.error(`Error: Command already exists at ${indexPath}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
} catch {
|
|
48
|
+
// File doesn't exist, proceed
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create directories
|
|
52
|
+
await mkdir(commandPath, { recursive: true });
|
|
53
|
+
|
|
54
|
+
// Generate class name (PascalCase)
|
|
55
|
+
const className = toPascalCase(commandName) + 'Command';
|
|
56
|
+
|
|
57
|
+
// Generate boilerplate
|
|
58
|
+
const boilerplate = generateBoilerplate(className, commandName);
|
|
59
|
+
|
|
60
|
+
// Write file
|
|
61
|
+
await writeFile(indexPath, boilerplate, 'utf-8');
|
|
62
|
+
|
|
63
|
+
// Output success and instructions
|
|
64
|
+
console.log(`✓ Created command at: ${indexPath}`);
|
|
65
|
+
console.log();
|
|
66
|
+
console.log('Next steps:');
|
|
67
|
+
console.log(`1. Implement the logic in ${indexPath}`);
|
|
68
|
+
console.log(`2. Add the following to ${join(namespacePath, 'index.js')}:`);
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(' Import statement (add at top):');
|
|
71
|
+
console.log(` import { ${className} } from './${commandName}/index.js';`);
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(` Command registration (add in create${toPascalCase(namespace)}Command function):`);
|
|
74
|
+
console.log(` ${namespace}`);
|
|
75
|
+
console.log(` .command('${commandName}')`);
|
|
76
|
+
console.log(` .description('Description of ${commandName} command')`);
|
|
77
|
+
console.log(` .action(async (options) => {`);
|
|
78
|
+
console.log(` const command = new ${className}(options);`);
|
|
79
|
+
console.log(` await command.process();`);
|
|
80
|
+
console.log(` });`);
|
|
81
|
+
console.log();
|
|
82
|
+
console.log(`3. If the namespace is new, register it in /src/index.js:`);
|
|
83
|
+
console.log(` import { create${toPascalCase(namespace)}Command } from './commands/${namespace}/index.js';`);
|
|
84
|
+
console.log(` program.addCommand(create${toPascalCase(namespace)}Command());`);
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Error creating command:', error.message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toPascalCase(str) {
|
|
94
|
+
return str
|
|
95
|
+
.split('-')
|
|
96
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
97
|
+
.join('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function generateBoilerplate(className, commandName) {
|
|
101
|
+
return `import { Command } from '../../../lib/Command.js';
|
|
102
|
+
|
|
103
|
+
export class ${className} extends Command {
|
|
104
|
+
async process() {
|
|
105
|
+
console.log(\`${commandName} called with \${JSON.stringify(this.getOptions())} from \${this.getCWD()}\`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
}
|