@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 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
+ }