musubi-sdd 2.2.0 → 3.0.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.ja.md +1 -1
- package/README.md +1 -1
- package/bin/musubi-browser.js +457 -0
- package/bin/musubi-convert.js +179 -0
- package/bin/musubi-gui.js +270 -0
- package/package.json +13 -3
- package/src/agents/browser/action-executor.js +255 -0
- package/src/agents/browser/ai-comparator.js +255 -0
- package/src/agents/browser/context-manager.js +207 -0
- package/src/agents/browser/index.js +265 -0
- package/src/agents/browser/nl-parser.js +408 -0
- package/src/agents/browser/screenshot.js +174 -0
- package/src/agents/browser/test-generator.js +271 -0
- package/src/converters/index.js +285 -0
- package/src/converters/ir/types.js +508 -0
- package/src/converters/parsers/musubi-parser.js +759 -0
- package/src/converters/parsers/speckit-parser.js +1001 -0
- package/src/converters/writers/musubi-writer.js +808 -0
- package/src/converters/writers/speckit-writer.js +718 -0
- package/src/gui/public/index.html +856 -0
- package/src/gui/server.js +352 -0
- package/src/gui/services/file-watcher.js +119 -0
- package/src/gui/services/index.js +16 -0
- package/src/gui/services/project-scanner.js +547 -0
- package/src/gui/services/traceability-service.js +372 -0
- package/src/gui/services/workflow-service.js +242 -0
- package/src/templates/skills/browser-agent.md +164 -0
- package/src/templates/skills/web-gui.md +188 -0
package/README.ja.md
CHANGED
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
> 🤖 **7 AI Coding Agents** × 📋 **
|
|
20
|
+
> 🤖 **7 AI Coding Agents** × 📋 **27 Specialized Skills** × ⚖️ **Constitutional Governance**
|
|
21
21
|
|
|
22
22
|
MUSUBI (結び - "connection/binding") is a comprehensive **Specification Driven Development (SDD)** framework that synthesizes the best features from 6 leading frameworks into a production-ready tool for multiple AI coding agents.
|
|
23
23
|
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CLI for Browser Automation Agent
|
|
5
|
+
* @module bin/musubi-browser
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { program } = require('commander');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const fs = require('fs-extra');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Lazy load browser agent to speed up CLI startup
|
|
15
|
+
let BrowserAgent = null;
|
|
16
|
+
|
|
17
|
+
function loadBrowserAgent() {
|
|
18
|
+
if (!BrowserAgent) {
|
|
19
|
+
BrowserAgent = require('../src/agents/browser');
|
|
20
|
+
}
|
|
21
|
+
return BrowserAgent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('musubi-browser')
|
|
26
|
+
.description('Browser automation with natural language commands')
|
|
27
|
+
.version('1.0.0');
|
|
28
|
+
|
|
29
|
+
// Interactive mode (default)
|
|
30
|
+
program
|
|
31
|
+
.command('interactive', { isDefault: true })
|
|
32
|
+
.alias('i')
|
|
33
|
+
.description('Start interactive browser automation session')
|
|
34
|
+
.option('--headless', 'Run in headless mode', true)
|
|
35
|
+
.option('--no-headless', 'Run with visible browser')
|
|
36
|
+
.option('-b, --browser <type>', 'Browser type (chromium/firefox/webkit)', 'chromium')
|
|
37
|
+
.option('-o, --output <dir>', 'Screenshot output directory', './screenshots')
|
|
38
|
+
.option('-t, --timeout <ms>', 'Default timeout in milliseconds', '30000')
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
await runInteractive(options);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Execute a single command
|
|
44
|
+
program
|
|
45
|
+
.command('run <command>')
|
|
46
|
+
.description('Execute a single natural language command')
|
|
47
|
+
.option('--headless', 'Run in headless mode', true)
|
|
48
|
+
.option('--no-headless', 'Run with visible browser')
|
|
49
|
+
.option('-b, --browser <type>', 'Browser type', 'chromium')
|
|
50
|
+
.option('-o, --output <dir>', 'Screenshot output directory', './screenshots')
|
|
51
|
+
.action(async (command, options) => {
|
|
52
|
+
await runCommand(command, options);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Execute commands from a script file
|
|
56
|
+
program
|
|
57
|
+
.command('script <file>')
|
|
58
|
+
.description('Execute commands from a script file')
|
|
59
|
+
.option('--headless', 'Run in headless mode', true)
|
|
60
|
+
.option('--no-headless', 'Run with visible browser')
|
|
61
|
+
.option('-b, --browser <type>', 'Browser type', 'chromium')
|
|
62
|
+
.option('-o, --output <dir>', 'Screenshot output directory', './screenshots')
|
|
63
|
+
.action(async (file, options) => {
|
|
64
|
+
await runScript(file, options);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Compare screenshots
|
|
68
|
+
program
|
|
69
|
+
.command('compare <expected> <actual>')
|
|
70
|
+
.description('Compare two screenshots using AI')
|
|
71
|
+
.option('--threshold <value>', 'Similarity threshold (0-1)', '0.95')
|
|
72
|
+
.option('-d, --description <text>', 'What to verify')
|
|
73
|
+
.action(async (expected, actual, options) => {
|
|
74
|
+
await compareScreenshots(expected, actual, options);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Generate test code
|
|
78
|
+
program
|
|
79
|
+
.command('generate-test')
|
|
80
|
+
.description('Generate Playwright test from action history')
|
|
81
|
+
.option('-n, --name <name>', 'Test name', 'Generated Test')
|
|
82
|
+
.option('-o, --output <file>', 'Output file path')
|
|
83
|
+
.option('-f, --format <format>', 'Test format (playwright/jest)', 'playwright')
|
|
84
|
+
.option('-H, --history <file>', 'Action history JSON file')
|
|
85
|
+
.action(async (options) => {
|
|
86
|
+
await generateTest(options);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run interactive browser session
|
|
91
|
+
*/
|
|
92
|
+
async function runInteractive(options) {
|
|
93
|
+
const Agent = loadBrowserAgent();
|
|
94
|
+
|
|
95
|
+
console.log(chalk.cyan('\n🌐 MUSUBI Browser Agent - Interactive Mode'));
|
|
96
|
+
console.log(chalk.gray('Type browser commands in natural language. Type "help" for commands.\n'));
|
|
97
|
+
|
|
98
|
+
const agent = new Agent({
|
|
99
|
+
headless: options.headless,
|
|
100
|
+
browser: options.browser,
|
|
101
|
+
outputDir: options.output,
|
|
102
|
+
timeout: parseInt(options.timeout, 10),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await agent.launch();
|
|
107
|
+
console.log(chalk.green('✓ Browser launched\n'));
|
|
108
|
+
|
|
109
|
+
const rl = readline.createInterface({
|
|
110
|
+
input: process.stdin,
|
|
111
|
+
output: process.stdout,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const prompt = () => {
|
|
115
|
+
rl.question(chalk.yellow('browser> '), async (input) => {
|
|
116
|
+
const command = input.trim();
|
|
117
|
+
|
|
118
|
+
if (!command) {
|
|
119
|
+
prompt();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (command === 'exit' || command === 'quit' || command === 'q') {
|
|
124
|
+
console.log(chalk.gray('\nClosing browser...'));
|
|
125
|
+
await agent.close();
|
|
126
|
+
rl.close();
|
|
127
|
+
process.exit(0);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (command === 'help' || command === '?') {
|
|
132
|
+
showHelp();
|
|
133
|
+
prompt();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (command === 'history') {
|
|
138
|
+
showHistory(agent);
|
|
139
|
+
prompt();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (command === 'clear') {
|
|
144
|
+
agent.clearHistory();
|
|
145
|
+
console.log(chalk.green('✓ History cleared'));
|
|
146
|
+
prompt();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (command.startsWith('save-test ')) {
|
|
151
|
+
const outputPath = command.slice(10).trim();
|
|
152
|
+
await saveTest(agent, outputPath);
|
|
153
|
+
prompt();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
const result = await agent.execute(command);
|
|
160
|
+
const elapsed = Date.now() - startTime;
|
|
161
|
+
|
|
162
|
+
if (result.success) {
|
|
163
|
+
console.log(chalk.green(`✓ Done (${elapsed}ms)`));
|
|
164
|
+
if (result.results) {
|
|
165
|
+
for (const r of result.results) {
|
|
166
|
+
if (r.data?.path) {
|
|
167
|
+
console.log(chalk.gray(` Screenshot: ${r.data.path}`));
|
|
168
|
+
}
|
|
169
|
+
if (r.data?.url) {
|
|
170
|
+
console.log(chalk.gray(` URL: ${r.data.url}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
console.log(chalk.red(`✗ Failed: ${result.error}`));
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.log(chalk.red(`✗ Error: ${error.message}`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
prompt();
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
prompt();
|
|
186
|
+
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Run a single command
|
|
195
|
+
*/
|
|
196
|
+
async function runCommand(command, options) {
|
|
197
|
+
const Agent = loadBrowserAgent();
|
|
198
|
+
|
|
199
|
+
console.log(chalk.cyan('🌐 MUSUBI Browser Agent'));
|
|
200
|
+
console.log(chalk.gray(`Command: ${command}\n`));
|
|
201
|
+
|
|
202
|
+
const agent = new Agent({
|
|
203
|
+
headless: options.headless,
|
|
204
|
+
browser: options.browser,
|
|
205
|
+
outputDir: options.output,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await agent.launch();
|
|
210
|
+
const result = await agent.execute(command);
|
|
211
|
+
|
|
212
|
+
if (result.success) {
|
|
213
|
+
console.log(chalk.green('✓ Command executed successfully'));
|
|
214
|
+
|
|
215
|
+
if (result.results) {
|
|
216
|
+
for (const r of result.results) {
|
|
217
|
+
console.log(chalk.gray(` ${r.type}: ${JSON.stringify(r.data)}`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
console.log(chalk.red(`✗ Failed: ${result.error}`));
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
227
|
+
process.exitCode = 1;
|
|
228
|
+
} finally {
|
|
229
|
+
await agent.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Run commands from a script file
|
|
235
|
+
*/
|
|
236
|
+
async function runScript(file, options) {
|
|
237
|
+
const Agent = loadBrowserAgent();
|
|
238
|
+
|
|
239
|
+
if (!await fs.pathExists(file)) {
|
|
240
|
+
console.error(chalk.red(`Script file not found: ${file}`));
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
245
|
+
const commands = content
|
|
246
|
+
.split('\n')
|
|
247
|
+
.map(line => line.trim())
|
|
248
|
+
.filter(line => line && !line.startsWith('#'));
|
|
249
|
+
|
|
250
|
+
console.log(chalk.cyan('🌐 MUSUBI Browser Agent - Script Mode'));
|
|
251
|
+
console.log(chalk.gray(`Script: ${file}`));
|
|
252
|
+
console.log(chalk.gray(`Commands: ${commands.length}\n`));
|
|
253
|
+
|
|
254
|
+
const agent = new Agent({
|
|
255
|
+
headless: options.headless,
|
|
256
|
+
browser: options.browser,
|
|
257
|
+
outputDir: options.output,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await agent.launch();
|
|
262
|
+
|
|
263
|
+
let passed = 0;
|
|
264
|
+
let failed = 0;
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < commands.length; i++) {
|
|
267
|
+
const command = commands[i];
|
|
268
|
+
process.stdout.write(chalk.gray(`[${i + 1}/${commands.length}] ${command.slice(0, 50)}...`));
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const result = await agent.execute(command);
|
|
272
|
+
if (result.success) {
|
|
273
|
+
console.log(chalk.green(' ✓'));
|
|
274
|
+
passed++;
|
|
275
|
+
} else {
|
|
276
|
+
console.log(chalk.red(` ✗ ${result.error}`));
|
|
277
|
+
failed++;
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.log(chalk.red(` ✗ ${error.message}`));
|
|
281
|
+
failed++;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(chalk.cyan('Results:'));
|
|
287
|
+
console.log(chalk.green(` Passed: ${passed}`));
|
|
288
|
+
console.log(chalk.red(` Failed: ${failed}`));
|
|
289
|
+
|
|
290
|
+
if (failed > 0) {
|
|
291
|
+
process.exitCode = 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
} finally {
|
|
298
|
+
await agent.close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Compare two screenshots
|
|
304
|
+
*/
|
|
305
|
+
async function compareScreenshots(expected, actual, options) {
|
|
306
|
+
const Agent = loadBrowserAgent();
|
|
307
|
+
const { AIComparator } = Agent;
|
|
308
|
+
|
|
309
|
+
console.log(chalk.cyan('🖼️ Screenshot Comparison'));
|
|
310
|
+
console.log(chalk.gray(`Expected: ${expected}`));
|
|
311
|
+
console.log(chalk.gray(`Actual: ${actual}`));
|
|
312
|
+
console.log(chalk.gray(`Threshold: ${options.threshold}\n`));
|
|
313
|
+
|
|
314
|
+
const comparator = new AIComparator({
|
|
315
|
+
threshold: parseFloat(options.threshold),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const result = await comparator.compare(expected, actual, {
|
|
320
|
+
description: options.description,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (result.passed) {
|
|
324
|
+
console.log(chalk.green(`✓ PASSED - Similarity: ${result.similarity}%`));
|
|
325
|
+
} else {
|
|
326
|
+
console.log(chalk.red(`✗ FAILED - Similarity: ${result.similarity}% (threshold: ${result.threshold}%)`));
|
|
327
|
+
|
|
328
|
+
if (result.differences.length > 0) {
|
|
329
|
+
console.log(chalk.yellow('\nDifferences:'));
|
|
330
|
+
for (const diff of result.differences) {
|
|
331
|
+
console.log(chalk.yellow(` - ${diff}`));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Generate report
|
|
339
|
+
const report = comparator.generateReport(result);
|
|
340
|
+
const reportPath = path.join(path.dirname(actual), 'comparison-report.md');
|
|
341
|
+
await fs.writeFile(reportPath, report);
|
|
342
|
+
console.log(chalk.gray(`\nReport saved: ${reportPath}`));
|
|
343
|
+
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Generate test code from history
|
|
352
|
+
*/
|
|
353
|
+
async function generateTest(options) {
|
|
354
|
+
const Agent = loadBrowserAgent();
|
|
355
|
+
const { TestGenerator } = Agent;
|
|
356
|
+
|
|
357
|
+
console.log(chalk.cyan('📝 Generate Test Code'));
|
|
358
|
+
|
|
359
|
+
const generator = new TestGenerator();
|
|
360
|
+
|
|
361
|
+
let history = [];
|
|
362
|
+
if (options.history) {
|
|
363
|
+
if (!await fs.pathExists(options.history)) {
|
|
364
|
+
console.error(chalk.red(`History file not found: ${options.history}`));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
history = JSON.parse(await fs.readFile(options.history, 'utf-8'));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const code = await generator.generateTest(history, {
|
|
372
|
+
name: options.name,
|
|
373
|
+
format: options.format,
|
|
374
|
+
output: options.output,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (options.output) {
|
|
378
|
+
console.log(chalk.green(`✓ Test saved to: ${options.output}`));
|
|
379
|
+
} else {
|
|
380
|
+
console.log('\n' + code);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
385
|
+
process.exitCode = 1;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Show help
|
|
391
|
+
*/
|
|
392
|
+
function showHelp() {
|
|
393
|
+
console.log(chalk.cyan('\nAvailable Commands:'));
|
|
394
|
+
console.log(chalk.gray(' Navigation:'));
|
|
395
|
+
console.log(' "https://example.com を開く" or "go to https://example.com"');
|
|
396
|
+
console.log(chalk.gray(' Click:'));
|
|
397
|
+
console.log(' "ログインボタンをクリック" or "click login button"');
|
|
398
|
+
console.log(chalk.gray(' Fill:'));
|
|
399
|
+
console.log(' "メール欄に「test@example.com」と入力" or "type test@example.com in email field"');
|
|
400
|
+
console.log(chalk.gray(' Wait:'));
|
|
401
|
+
console.log(' "3秒待つ" or "wait 3 seconds"');
|
|
402
|
+
console.log(chalk.gray(' Screenshot:'));
|
|
403
|
+
console.log(' "スクリーンショット" or "take screenshot"');
|
|
404
|
+
console.log(chalk.gray(' Assert:'));
|
|
405
|
+
console.log(' "「ログイン成功」が表示される" or "verify Login Success is visible"');
|
|
406
|
+
console.log(chalk.gray('\nSession Commands:'));
|
|
407
|
+
console.log(' history - Show action history');
|
|
408
|
+
console.log(' clear - Clear action history');
|
|
409
|
+
console.log(' save-test <file> - Save history as Playwright test');
|
|
410
|
+
console.log(' exit/quit/q - Close browser and exit');
|
|
411
|
+
console.log('');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Show action history
|
|
416
|
+
*/
|
|
417
|
+
function showHistory(agent) {
|
|
418
|
+
const history = agent.getActionHistory();
|
|
419
|
+
|
|
420
|
+
if (history.length === 0) {
|
|
421
|
+
console.log(chalk.gray('No actions in history.'));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
console.log(chalk.cyan(`\nAction History (${history.length} actions):`));
|
|
426
|
+
for (let i = 0; i < history.length; i++) {
|
|
427
|
+
const item = history[i];
|
|
428
|
+
const status = item.result?.success ? chalk.green('✓') : chalk.red('✗');
|
|
429
|
+
console.log(` ${i + 1}. ${status} ${item.action.type}: ${item.action.raw || JSON.stringify(item.action)}`);
|
|
430
|
+
}
|
|
431
|
+
console.log('');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Save test from history
|
|
436
|
+
*/
|
|
437
|
+
async function saveTest(agent, outputPath) {
|
|
438
|
+
const history = agent.getActionHistory();
|
|
439
|
+
|
|
440
|
+
if (history.length === 0) {
|
|
441
|
+
console.log(chalk.yellow('No actions in history to save.'));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const code = await agent.generateTest({
|
|
447
|
+
name: 'Interactive Session Test',
|
|
448
|
+
output: outputPath,
|
|
449
|
+
});
|
|
450
|
+
console.log(chalk.green(`✓ Test saved to: ${outputPath}`));
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.log(chalk.red(`Error saving test: ${error.message}`));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Parse arguments
|
|
457
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MUSUBI Convert CLI
|
|
5
|
+
*
|
|
6
|
+
* Convert between MUSUBI and Spec Kit formats
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* musubi-convert from-speckit <path> [--output <dir>]
|
|
10
|
+
* musubi-convert to-speckit [--output <dir>]
|
|
11
|
+
* musubi-convert validate <format> [path]
|
|
12
|
+
* musubi-convert roundtrip <path>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { program } = require('commander');
|
|
18
|
+
const {
|
|
19
|
+
convertFromSpeckit,
|
|
20
|
+
convertToSpeckit,
|
|
21
|
+
validateFormat,
|
|
22
|
+
testRoundtrip
|
|
23
|
+
} = require('../src/converters');
|
|
24
|
+
const packageJson = require('../package.json');
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('musubi-convert')
|
|
28
|
+
.description('Convert between MUSUBI and Spec Kit formats')
|
|
29
|
+
.version(packageJson.version);
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('from-speckit <path>')
|
|
33
|
+
.description('Convert Spec Kit project to MUSUBI format')
|
|
34
|
+
.option('-o, --output <dir>', 'Output directory', '.')
|
|
35
|
+
.option('--dry-run', 'Preview changes without writing')
|
|
36
|
+
.option('-v, --verbose', 'Verbose output')
|
|
37
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
38
|
+
.option('--preserve-raw', 'Keep original content in comments')
|
|
39
|
+
.action(async (sourcePath, options) => {
|
|
40
|
+
try {
|
|
41
|
+
console.log('🔄 Converting Spec Kit → MUSUBI...\n');
|
|
42
|
+
|
|
43
|
+
const result = await convertFromSpeckit(sourcePath, {
|
|
44
|
+
output: options.output,
|
|
45
|
+
dryRun: options.dryRun,
|
|
46
|
+
force: options.force,
|
|
47
|
+
verbose: options.verbose,
|
|
48
|
+
preserveRaw: options.preserveRaw,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(`\n✅ Conversion complete!`);
|
|
52
|
+
console.log(` Files written: ${result.filesConverted}`);
|
|
53
|
+
console.log(` Output: ${result.outputPath}`);
|
|
54
|
+
|
|
55
|
+
if (result.warnings.length > 0) {
|
|
56
|
+
console.log(`\n⚠️ Warnings (${result.warnings.length}):`);
|
|
57
|
+
result.warnings.forEach(w => console.log(` - ${w}`));
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`\n❌ Conversion failed: ${error.message}`);
|
|
61
|
+
if (options.verbose) {
|
|
62
|
+
console.error(error.stack);
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('to-speckit')
|
|
70
|
+
.description('Convert current MUSUBI project to Spec Kit format')
|
|
71
|
+
.option('-s, --source <dir>', 'Source directory', '.')
|
|
72
|
+
.option('-o, --output <dir>', 'Output directory (will create .specify inside)', '.')
|
|
73
|
+
.option('--dry-run', 'Preview changes without writing')
|
|
74
|
+
.option('-v, --verbose', 'Verbose output')
|
|
75
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
76
|
+
.option('--preserve-raw', 'Keep original content in comments')
|
|
77
|
+
.action(async (options) => {
|
|
78
|
+
try {
|
|
79
|
+
console.log('🔄 Converting MUSUBI → Spec Kit...\n');
|
|
80
|
+
|
|
81
|
+
const result = await convertToSpeckit({
|
|
82
|
+
source: options.source,
|
|
83
|
+
output: options.output,
|
|
84
|
+
dryRun: options.dryRun,
|
|
85
|
+
force: options.force,
|
|
86
|
+
verbose: options.verbose,
|
|
87
|
+
preserveRaw: options.preserveRaw,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(`\n✅ Conversion complete!`);
|
|
91
|
+
console.log(` Files written: ${result.filesConverted}`);
|
|
92
|
+
console.log(` Output: ${result.outputPath}`);
|
|
93
|
+
|
|
94
|
+
if (result.warnings.length > 0) {
|
|
95
|
+
console.log(`\n⚠️ Warnings (${result.warnings.length}):`);
|
|
96
|
+
result.warnings.forEach(w => console.log(` - ${w}`));
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`\n❌ Conversion failed: ${error.message}`);
|
|
100
|
+
if (options.verbose) {
|
|
101
|
+
console.error(error.stack);
|
|
102
|
+
}
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('validate <format> [path]')
|
|
109
|
+
.description('Validate project format (speckit or musubi)')
|
|
110
|
+
.option('-v, --verbose', 'Verbose output')
|
|
111
|
+
.action(async (format, projectPath, options) => {
|
|
112
|
+
try {
|
|
113
|
+
const path = projectPath || '.';
|
|
114
|
+
console.log(`🔍 Validating ${format} project at ${path}...\n`);
|
|
115
|
+
|
|
116
|
+
const result = await validateFormat(format, path);
|
|
117
|
+
|
|
118
|
+
if (result.valid) {
|
|
119
|
+
console.log(`✅ Valid ${format} project`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(`❌ Invalid ${format} project`);
|
|
122
|
+
console.log('\nErrors:');
|
|
123
|
+
result.errors.forEach(e => console.log(` - ${e}`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (result.warnings.length > 0) {
|
|
128
|
+
console.log('\n⚠️ Warnings:');
|
|
129
|
+
result.warnings.forEach(w => console.log(` - ${w}`));
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`\n❌ Validation failed: ${error.message}`);
|
|
133
|
+
if (options.verbose) {
|
|
134
|
+
console.error(error.stack);
|
|
135
|
+
}
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
program
|
|
141
|
+
.command('roundtrip <path>')
|
|
142
|
+
.description('Test roundtrip conversion (A → B → A\')')
|
|
143
|
+
.option('-v, --verbose', 'Show detailed diff')
|
|
144
|
+
.action(async (projectPath, options) => {
|
|
145
|
+
try {
|
|
146
|
+
console.log(`🔄 Testing roundtrip conversion at ${projectPath}...\n`);
|
|
147
|
+
|
|
148
|
+
const result = await testRoundtrip(projectPath, {
|
|
149
|
+
verbose: options.verbose,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (result.passed) {
|
|
153
|
+
console.log(`✅ Roundtrip test PASSED`);
|
|
154
|
+
} else {
|
|
155
|
+
console.log(`❌ Roundtrip test FAILED`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(` Similarity: ${result.similarity}%`);
|
|
159
|
+
|
|
160
|
+
if (!result.passed || options.verbose) {
|
|
161
|
+
if (result.differences.length > 0) {
|
|
162
|
+
console.log('\n📋 Differences:');
|
|
163
|
+
result.differences.forEach(d => console.log(` - ${d}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!result.passed) {
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(`\n❌ Roundtrip test failed: ${error.message}`);
|
|
172
|
+
if (options.verbose) {
|
|
173
|
+
console.error(error.stack);
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
program.parse();
|