@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.
@@ -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,14 @@
1
+ export class Command {
2
+ constructor(options = {}) {
3
+ this.options = options;
4
+ this.pwd = process.cwd();
5
+ }
6
+
7
+ getOptions() {
8
+ return this.options;
9
+ }
10
+
11
+ getCWD() {
12
+ return this.pwd;
13
+ }
14
+ }
@@ -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
+ }