@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Command } from '../../../lib/Command.js';
|
|
2
|
+
import { mkdir, writeFile, access } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export class NewWidgetCommand extends Command {
|
|
6
|
+
async process() {
|
|
7
|
+
const options = this.getOptions();
|
|
8
|
+
|
|
9
|
+
// Validate --name option
|
|
10
|
+
if (!options.name) {
|
|
11
|
+
console.error('Error: --name option is required');
|
|
12
|
+
console.error('Usage: widgets new --name "Hero"');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Normalize name (ensure PascalCase and "Widget" suffix)
|
|
17
|
+
const widgetName = normalizeName(options.name);
|
|
18
|
+
|
|
19
|
+
// Get CWD and build target path
|
|
20
|
+
const cwd = this.getCWD();
|
|
21
|
+
const widgetsDir = join(cwd, 'src', 'components', 'widgets');
|
|
22
|
+
const widgetPath = join(widgetsDir, widgetName);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Create widgets directory if it doesn't exist
|
|
26
|
+
await mkdir(widgetsDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Check if widget already exists
|
|
29
|
+
try {
|
|
30
|
+
await access(widgetPath);
|
|
31
|
+
console.error(`Error: Widget already exists at ${widgetPath}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
} catch {
|
|
34
|
+
// Widget doesn't exist, proceed
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create widget directory
|
|
38
|
+
await mkdir(widgetPath);
|
|
39
|
+
|
|
40
|
+
// Generate and write all 5 files
|
|
41
|
+
const configContent = generateConfigTemplate(widgetName);
|
|
42
|
+
const adminContent = generateAdminTemplate(widgetName);
|
|
43
|
+
const defaultsContent = generateDefaultsTemplate(widgetName);
|
|
44
|
+
const indexContent = generateIndexTemplate(widgetName);
|
|
45
|
+
const fetcherContent = generateFetcherTemplate();
|
|
46
|
+
|
|
47
|
+
await writeFile(join(widgetPath, 'config.js'), configContent, 'utf-8');
|
|
48
|
+
await writeFile(join(widgetPath, 'admin.js'), adminContent, 'utf-8');
|
|
49
|
+
await writeFile(join(widgetPath, 'defaults.js'), defaultsContent, 'utf-8');
|
|
50
|
+
await writeFile(join(widgetPath, 'index.js'), indexContent, 'utf-8');
|
|
51
|
+
await writeFile(join(widgetPath, 'fetcher.js'), fetcherContent, 'utf-8');
|
|
52
|
+
|
|
53
|
+
// Output success message and instructions
|
|
54
|
+
console.log(`✓ Created widget at: ${widgetPath}`);
|
|
55
|
+
console.log();
|
|
56
|
+
console.log('Files created:');
|
|
57
|
+
console.log(` - config.js`);
|
|
58
|
+
console.log(` - admin.js`);
|
|
59
|
+
console.log(` - defaults.js`);
|
|
60
|
+
console.log(` - index.js`);
|
|
61
|
+
console.log(` - fetcher.js`);
|
|
62
|
+
console.log();
|
|
63
|
+
console.log('Next steps:');
|
|
64
|
+
console.log();
|
|
65
|
+
console.log('1. Add to src/components/widgets/index.js:');
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(` export { default as ${widgetName} } from './${widgetName}/index.js';`);
|
|
68
|
+
console.log();
|
|
69
|
+
console.log('2. Add to src/components/widgets/admin.js:');
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(` import * as ${widgetName} from "./${widgetName}/admin";`);
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(' Then include in the export:');
|
|
74
|
+
console.log();
|
|
75
|
+
console.log(` export default {`);
|
|
76
|
+
console.log(` // other widgets...`);
|
|
77
|
+
console.log(` ${widgetName},`);
|
|
78
|
+
console.log(` };`);
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error creating widget:', error.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Helper function to normalize widget name
|
|
88
|
+
function normalizeName(name) {
|
|
89
|
+
// Remove any non-alphanumeric characters except hyphens
|
|
90
|
+
let normalized = name.replace(/[^a-zA-Z0-9-]/g, '');
|
|
91
|
+
|
|
92
|
+
// Convert to PascalCase
|
|
93
|
+
normalized = normalized
|
|
94
|
+
.split('-')
|
|
95
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
96
|
+
.join('');
|
|
97
|
+
|
|
98
|
+
// Ensure first letter is uppercase
|
|
99
|
+
normalized = normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
100
|
+
|
|
101
|
+
// Add "Widget" suffix if not already present
|
|
102
|
+
if (!normalized.endsWith('Widget')) {
|
|
103
|
+
normalized += 'Widget';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return normalized;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Helper function to convert PascalCase to kebab-case
|
|
110
|
+
function kebabCase(str) {
|
|
111
|
+
return str
|
|
112
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
113
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
114
|
+
.toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Template generators
|
|
118
|
+
function generateConfigTemplate(widgetName) {
|
|
119
|
+
// Remove "Widget" suffix for display name
|
|
120
|
+
const displayName = widgetName.replace(/Widget$/, '');
|
|
121
|
+
|
|
122
|
+
return `export default {
|
|
123
|
+
title: '${displayName}',
|
|
124
|
+
category: 'content',
|
|
125
|
+
primaryField: 'title',
|
|
126
|
+
deprecated: false
|
|
127
|
+
};
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function generateAdminTemplate(widgetName) {
|
|
132
|
+
return `import Defaults from "./defaults";
|
|
133
|
+
import Config from "./config";
|
|
134
|
+
|
|
135
|
+
const Admin = {
|
|
136
|
+
title: { type: "text" },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export { Admin, Defaults, Config };
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function generateDefaultsTemplate(widgetName) {
|
|
144
|
+
return `export default {
|
|
145
|
+
title: 'Default Title',
|
|
146
|
+
};
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function generateIndexTemplate(widgetName) {
|
|
151
|
+
const kebabName = kebabCase(widgetName);
|
|
152
|
+
|
|
153
|
+
return `import React from 'react';
|
|
154
|
+
|
|
155
|
+
export default function ${widgetName}({ title, __ctx, __data }) {
|
|
156
|
+
return (
|
|
157
|
+
<div className="${kebabName}">
|
|
158
|
+
<h2>{title}</h2>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function generateFetcherTemplate() {
|
|
166
|
+
return `/**
|
|
167
|
+
* Optional: Server-side data fetching for dynamic widgets
|
|
168
|
+
* Uncomment and implement if your widget needs to fetch data
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
/*
|
|
172
|
+
export default async function fetcher(settings, context) {
|
|
173
|
+
// Fetch data based on settings
|
|
174
|
+
// Available in context: { locale, slug, ... }
|
|
175
|
+
|
|
176
|
+
const data = await fetch('your-api-endpoint', {
|
|
177
|
+
// your fetch logic
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return data;
|
|
181
|
+
}
|
|
182
|
+
*/
|
|
183
|
+
|
|
184
|
+
export default null;
|
|
185
|
+
`;
|
|
186
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { createWidgetsCommand } from './commands/widgets/index.js';
|
|
5
|
+
import { createSkillsCommand } from './commands/skills/index.js';
|
|
6
|
+
import { createCommandsCommand } from './commands/commands/index.js';
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('raketa-cli')
|
|
10
|
+
.description('Collection of useful CLI tools for humans and coding agents')
|
|
11
|
+
.version('1.0.0');
|
|
12
|
+
|
|
13
|
+
program.addCommand(createWidgetsCommand());
|
|
14
|
+
program.addCommand(createSkillsCommand());
|
|
15
|
+
program.addCommand(createCommandsCommand());
|
|
16
|
+
|
|
17
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { readFile, readdir, access, mkdir, cp, rm } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execFile } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse skill metadata from SKILL.md frontmatter
|
|
12
|
+
* @param {string} skillMdPath - Path to SKILL.md file
|
|
13
|
+
* @returns {Promise<{name: string, description: string}|null>} Parsed metadata or null if invalid
|
|
14
|
+
*/
|
|
15
|
+
export async function parseSkillMetadata(skillMdPath) {
|
|
16
|
+
try {
|
|
17
|
+
// Read file content
|
|
18
|
+
const content = await readFile(skillMdPath, 'utf-8');
|
|
19
|
+
|
|
20
|
+
// Extract YAML frontmatter
|
|
21
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
22
|
+
const match = content.match(frontmatterRegex);
|
|
23
|
+
|
|
24
|
+
if (!match) {
|
|
25
|
+
return null; // No frontmatter found
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const frontmatter = match[1];
|
|
29
|
+
let metadata;
|
|
30
|
+
|
|
31
|
+
// Try to parse YAML
|
|
32
|
+
try {
|
|
33
|
+
metadata = yaml.load(frontmatter);
|
|
34
|
+
} catch (yamlError) {
|
|
35
|
+
// If YAML parsing fails, try manual extraction as fallback
|
|
36
|
+
// This handles cases where description contains unquoted colons
|
|
37
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
38
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
39
|
+
|
|
40
|
+
if (nameMatch && descMatch) {
|
|
41
|
+
metadata = {
|
|
42
|
+
name: nameMatch[1].trim(),
|
|
43
|
+
description: descMatch[1].trim()
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate required fields
|
|
51
|
+
if (!metadata || typeof metadata.description !== 'string' || !metadata.description.trim()) {
|
|
52
|
+
return null; // Missing or empty description
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: metadata.name || null,
|
|
57
|
+
description: metadata.description.trim()
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// File read error or other unexpected error
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format skill name and description with colors
|
|
68
|
+
* @param {string} skillName - Name of the skill
|
|
69
|
+
* @param {string} description - Description of the skill (optional)
|
|
70
|
+
* @returns {string} Formatted output string
|
|
71
|
+
*/
|
|
72
|
+
export function formatSkillWithDescription(skillName, description) {
|
|
73
|
+
if (description) {
|
|
74
|
+
// Skill name on one line (bold cyan), description on next line (dim/light gray), blank line after
|
|
75
|
+
return ` ${chalk.bold.cyan(skillName)}\n ${chalk.dim(description)}\n`;
|
|
76
|
+
}
|
|
77
|
+
// Fallback for backward compatibility
|
|
78
|
+
return ` ${skillName}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ensure remote repository is cloned and up-to-date
|
|
83
|
+
* @param {string} repoUrl - Git repository URL
|
|
84
|
+
* @param {string} cachePath - Local path to cache the repository
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
* @throws {Error} With specific error messages for different failure scenarios
|
|
87
|
+
*/
|
|
88
|
+
export async function ensureRemoteRepo(repoUrl, cachePath) {
|
|
89
|
+
try {
|
|
90
|
+
// Check if cache directory exists
|
|
91
|
+
await access(cachePath);
|
|
92
|
+
|
|
93
|
+
// Directory exists, pull updates
|
|
94
|
+
try {
|
|
95
|
+
await execFileAsync('git', ['-C', cachePath, 'pull', 'origin', 'main'], {
|
|
96
|
+
timeout: 30000
|
|
97
|
+
});
|
|
98
|
+
} catch (pullError) {
|
|
99
|
+
// If pull fails, we can still use cached data
|
|
100
|
+
// Just log a warning but don't throw
|
|
101
|
+
console.warn('Warning: Could not update cached repository, using existing data');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
} catch (accessError) {
|
|
105
|
+
// Directory doesn't exist, clone it
|
|
106
|
+
try {
|
|
107
|
+
// Create parent directory
|
|
108
|
+
const parentDir = join(cachePath, '..');
|
|
109
|
+
await mkdir(parentDir, { recursive: true });
|
|
110
|
+
|
|
111
|
+
// Clone repository
|
|
112
|
+
await execFileAsync('git', ['clone', repoUrl, cachePath], {
|
|
113
|
+
timeout: 30000
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
} catch (cloneError) {
|
|
117
|
+
// Handle specific git errors
|
|
118
|
+
if (cloneError.code === 'ENOENT') {
|
|
119
|
+
throw new Error('git is not installed. Please install git to use this command.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const stderr = cloneError.stderr || '';
|
|
123
|
+
|
|
124
|
+
if (stderr.includes('Could not resolve host') || stderr.includes('Failed to connect')) {
|
|
125
|
+
throw new Error('Cannot reach GitHub. Check your internet connection.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (stderr.includes('Permission denied') || stderr.includes('publickey')) {
|
|
129
|
+
throw new Error('SSH authentication failed. Ensure your SSH key is configured for GitHub.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (stderr.includes('Repository not found')) {
|
|
133
|
+
throw new Error('Remote repository not found or not accessible.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Generic error
|
|
137
|
+
throw new Error(`Failed to clone repository: ${cloneError.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get all skills with their descriptions from a directory
|
|
144
|
+
* @param {string} skillsPath - Path to skills directory
|
|
145
|
+
* @returns {Promise<Array<{name: string, description: string}>>} Array of skills with descriptions
|
|
146
|
+
*/
|
|
147
|
+
export async function getSkillsWithDescriptions(skillsPath) {
|
|
148
|
+
const skills = [];
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Read directory contents
|
|
152
|
+
const items = await readdir(skillsPath, { withFileTypes: true });
|
|
153
|
+
|
|
154
|
+
// Filter for directories only
|
|
155
|
+
const directories = items.filter(dirent => dirent.isDirectory());
|
|
156
|
+
|
|
157
|
+
// Process each directory
|
|
158
|
+
for (const dir of directories) {
|
|
159
|
+
const skillMdPath = join(skillsPath, dir.name, 'SKILL.md');
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Check if SKILL.md exists
|
|
163
|
+
await access(skillMdPath);
|
|
164
|
+
|
|
165
|
+
// Parse metadata
|
|
166
|
+
const metadata = await parseSkillMetadata(skillMdPath);
|
|
167
|
+
|
|
168
|
+
// Only include if description is valid
|
|
169
|
+
if (metadata && metadata.description) {
|
|
170
|
+
skills.push({
|
|
171
|
+
name: dir.name,
|
|
172
|
+
description: metadata.description
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Skip directories without valid SKILL.md or description
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// If directory doesn't exist or can't be read, return empty array
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return skills;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Copy skill directory from cache to destination
|
|
190
|
+
* @param {string} skillName - Name of the skill to copy
|
|
191
|
+
* @param {string} cachePath - Source cache path
|
|
192
|
+
* @param {string} destPath - Destination path
|
|
193
|
+
* @param {boolean} overwrite - Whether to overwrite existing skills
|
|
194
|
+
* @returns {Promise<void>}
|
|
195
|
+
* @throws {Error} If skill doesn't exist in cache or copy fails
|
|
196
|
+
*/
|
|
197
|
+
export async function copySkillFromCache(skillName, cachePath, destPath, overwrite = false) {
|
|
198
|
+
// Validate skill exists in cache
|
|
199
|
+
const sourceSkillPath = join(cachePath, skillName);
|
|
200
|
+
const sourceSkillMd = join(sourceSkillPath, 'SKILL.md');
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await access(sourceSkillPath);
|
|
204
|
+
await access(sourceSkillMd);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error(`Skill '${skillName}' not found in remote repository`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create destination parent directory if needed
|
|
210
|
+
await mkdir(destPath, { recursive: true });
|
|
211
|
+
|
|
212
|
+
// Check if skill already exists at destination
|
|
213
|
+
const destSkillPath = join(destPath, skillName);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await access(destSkillPath);
|
|
217
|
+
// Skill exists
|
|
218
|
+
if (overwrite) {
|
|
219
|
+
console.log(chalk.yellow(`Skill '${skillName}' already exists, overwriting...`));
|
|
220
|
+
await rm(destSkillPath, { recursive: true, force: true });
|
|
221
|
+
} else {
|
|
222
|
+
throw new Error(`Skill '${skillName}' already exists at ${destSkillPath}`);
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// If access fails, skill doesn't exist - continue with copy
|
|
226
|
+
// If it's another error, rethrow
|
|
227
|
+
if (error.code !== 'ENOENT') {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Copy the skill directory
|
|
233
|
+
try {
|
|
234
|
+
await cp(sourceSkillPath, destSkillPath, { recursive: true });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw new Error(`Failed to copy skill: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Verify copy succeeded by checking for SKILL.md in destination
|
|
240
|
+
const destSkillMd = join(destSkillPath, 'SKILL.md');
|
|
241
|
+
try {
|
|
242
|
+
await access(destSkillMd);
|
|
243
|
+
} catch {
|
|
244
|
+
throw new Error(`Skill copy verification failed: SKILL.md not found at ${destSkillMd}`);
|
|
245
|
+
}
|
|
246
|
+
}
|