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.
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview MUSUBI Web GUI CLI
5
+ * @description Command-line interface for the Web GUI Dashboard
6
+ * @module bin/musubi-gui
7
+ */
8
+
9
+ const { Command } = require('commander');
10
+ const path = require('path');
11
+ const chalk = require('chalk');
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('musubi-gui')
17
+ .description('MUSUBI Web GUI Dashboard - Visual interface for SDD workflow')
18
+ .version('0.1.0');
19
+
20
+ /**
21
+ * Start command - Launch the GUI server
22
+ */
23
+ program
24
+ .command('start')
25
+ .description('Start the Web GUI server')
26
+ .option('-p, --port <port>', 'Server port', '3000')
27
+ .option('-d, --dir <directory>', 'Project directory', process.cwd())
28
+ .option('--no-open', 'Do not open browser automatically')
29
+ .action(async (options) => {
30
+ try {
31
+ const Server = require('../src/gui/server');
32
+ const projectPath = path.resolve(options.dir);
33
+
34
+ console.log(chalk.blue('šŸ”® MUSUBI Web GUI'));
35
+ console.log(chalk.gray(`Project: ${projectPath}`));
36
+
37
+ const server = new Server(projectPath, {
38
+ port: parseInt(options.port, 10),
39
+ });
40
+
41
+ await server.start();
42
+
43
+ console.log(chalk.green(`\nāœ“ Server running at http://localhost:${options.port}`));
44
+ console.log(chalk.gray('Press Ctrl+C to stop\n'));
45
+
46
+ // Open browser if requested
47
+ if (options.open !== false) {
48
+ const open = await import('open');
49
+ await open.default(`http://localhost:${options.port}`);
50
+ }
51
+
52
+ // Handle shutdown
53
+ process.on('SIGINT', async () => {
54
+ console.log(chalk.yellow('\n\nShutting down...'));
55
+ await server.stop();
56
+ process.exit(0);
57
+ });
58
+
59
+ } catch (error) {
60
+ console.error(chalk.red(`Error: ${error.message}`));
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ /**
66
+ * Build command - Build the frontend
67
+ */
68
+ program
69
+ .command('build')
70
+ .description('Build the frontend for production')
71
+ .option('-o, --outdir <directory>', 'Output directory', 'dist')
72
+ .action(async (options) => {
73
+ try {
74
+ const { execSync } = require('child_process');
75
+ const guiDir = path.join(__dirname, '..', 'src', 'gui', 'client');
76
+
77
+ console.log(chalk.blue('Building frontend...'));
78
+
79
+ // Check if client directory exists
80
+ const fs = require('fs');
81
+ if (!fs.existsSync(guiDir)) {
82
+ console.log(chalk.yellow('Frontend client not found. Creating minimal build...'));
83
+ return;
84
+ }
85
+
86
+ execSync('npm run build', {
87
+ cwd: guiDir,
88
+ stdio: 'inherit',
89
+ });
90
+
91
+ console.log(chalk.green(`āœ“ Frontend built to ${options.outdir}`));
92
+ } catch (error) {
93
+ console.error(chalk.red(`Build failed: ${error.message}`));
94
+ process.exit(1);
95
+ }
96
+ });
97
+
98
+ /**
99
+ * Dev command - Start development mode with hot reload
100
+ */
101
+ program
102
+ .command('dev')
103
+ .description('Start in development mode with hot reload')
104
+ .option('-p, --port <port>', 'Server port', '3000')
105
+ .option('-d, --dir <directory>', 'Project directory', process.cwd())
106
+ .action(async (options) => {
107
+ try {
108
+ const Server = require('../src/gui/server');
109
+ const projectPath = path.resolve(options.dir);
110
+
111
+ console.log(chalk.blue('šŸ”® MUSUBI Web GUI (Development Mode)'));
112
+ console.log(chalk.gray(`Project: ${projectPath}`));
113
+
114
+ const server = new Server(projectPath, {
115
+ port: parseInt(options.port, 10),
116
+ dev: true,
117
+ });
118
+
119
+ await server.start();
120
+
121
+ console.log(chalk.green(`\nāœ“ Server running at http://localhost:${options.port}`));
122
+ console.log(chalk.cyan('Hot reload enabled'));
123
+ console.log(chalk.gray('Press Ctrl+C to stop\n'));
124
+
125
+ // Handle shutdown
126
+ process.on('SIGINT', async () => {
127
+ console.log(chalk.yellow('\n\nShutting down...'));
128
+ await server.stop();
129
+ process.exit(0);
130
+ });
131
+
132
+ } catch (error) {
133
+ console.error(chalk.red(`Error: ${error.message}`));
134
+ process.exit(1);
135
+ }
136
+ });
137
+
138
+ /**
139
+ * Status command - Show project status
140
+ */
141
+ program
142
+ .command('status')
143
+ .description('Show project status summary')
144
+ .option('-d, --dir <directory>', 'Project directory', process.cwd())
145
+ .option('--json', 'Output as JSON')
146
+ .action(async (options) => {
147
+ try {
148
+ const ProjectScanner = require('../src/gui/services/project-scanner');
149
+ const projectPath = path.resolve(options.dir);
150
+
151
+ const scanner = new ProjectScanner(projectPath);
152
+ const project = await scanner.scan();
153
+
154
+ if (options.json) {
155
+ console.log(JSON.stringify(project, null, 2));
156
+ return;
157
+ }
158
+
159
+ console.log(chalk.blue('\nšŸ”® MUSUBI Project Status\n'));
160
+ console.log(chalk.white(`Project: ${chalk.bold(project.name)}`));
161
+ console.log(chalk.white(`Path: ${project.path}`));
162
+ console.log('');
163
+
164
+ if (project.hasSteering) {
165
+ console.log(chalk.green('āœ“ Steering directory found'));
166
+ if (project.constitution) {
167
+ console.log(chalk.green(` āœ“ Constitution: ${project.constitution.articles.length} articles`));
168
+ }
169
+ if (project.steering) {
170
+ const docs = ['product', 'structure', 'tech'].filter(d => project.steering[d]);
171
+ console.log(chalk.green(` āœ“ Documents: ${docs.join(', ')}`));
172
+ }
173
+ } else {
174
+ console.log(chalk.yellow('⚠ No steering directory (run musubi init)'));
175
+ }
176
+
177
+ if (project.hasSpecs) {
178
+ console.log(chalk.green(`āœ“ Specs: ${project.specs.length} specification files`));
179
+
180
+ let totalReqs = 0;
181
+ let totalTasks = 0;
182
+ let completedTasks = 0;
183
+
184
+ for (const spec of project.specs) {
185
+ totalReqs += spec.requirements.length;
186
+ totalTasks += spec.tasks.length;
187
+ completedTasks += spec.tasks.filter(t => t.completed).length;
188
+ }
189
+
190
+ console.log(chalk.white(` • Requirements: ${totalReqs}`));
191
+ console.log(chalk.white(` • Tasks: ${completedTasks}/${totalTasks} completed`));
192
+ } else {
193
+ console.log(chalk.gray('ā—‹ No specs yet'));
194
+ }
195
+
196
+ console.log('');
197
+ } catch (error) {
198
+ console.error(chalk.red(`Error: ${error.message}`));
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ /**
204
+ * Matrix command - Show traceability matrix
205
+ */
206
+ program
207
+ .command('matrix')
208
+ .description('Display traceability matrix')
209
+ .option('-d, --dir <directory>', 'Project directory', process.cwd())
210
+ .option('--json', 'Output as JSON')
211
+ .action(async (options) => {
212
+ try {
213
+ const TraceabilityService = require('../src/gui/services/traceability-service');
214
+ const projectPath = path.resolve(options.dir);
215
+
216
+ const service = new TraceabilityService(projectPath);
217
+ const matrix = await service.buildMatrix();
218
+
219
+ if (options.json) {
220
+ console.log(JSON.stringify(matrix, null, 2));
221
+ return;
222
+ }
223
+
224
+ console.log(chalk.blue('\nšŸ”® Traceability Matrix\n'));
225
+
226
+ const coverage = await service.getCoverage();
227
+ console.log(chalk.white('Coverage:'));
228
+ console.log(chalk.white(` Total Requirements: ${coverage.total}`));
229
+ console.log(chalk.white(` Linked: ${coverage.linked} (${coverage.linkedPercent}%)`));
230
+ console.log(chalk.white(` Implemented: ${coverage.implemented} (${coverage.implementedPercent}%)`));
231
+ console.log('');
232
+
233
+ if (matrix.requirements.length === 0) {
234
+ console.log(chalk.gray('No requirements found.'));
235
+ return;
236
+ }
237
+
238
+ console.log(chalk.white('Requirements:'));
239
+ for (const req of matrix.requirements) {
240
+ const statusColor = req.status === 'implemented' ? 'green' :
241
+ req.status === 'tasked' ? 'yellow' :
242
+ req.status === 'designed' ? 'cyan' : 'gray';
243
+ console.log(chalk[statusColor](` ${req.id}: ${req.title} [${req.status}]`));
244
+
245
+ if (req.links.designs.length > 0) {
246
+ console.log(chalk.gray(` → Designs: ${req.links.designs.map(d => d.id).join(', ')}`));
247
+ }
248
+ if (req.links.tasks.length > 0) {
249
+ console.log(chalk.gray(` → Tasks: ${req.links.tasks.map(t => t.id).join(', ')}`));
250
+ }
251
+ }
252
+
253
+ console.log('');
254
+
255
+ const gaps = await service.findGaps();
256
+ if (gaps.length > 0) {
257
+ console.log(chalk.yellow('Gaps:'));
258
+ for (const gap of gaps) {
259
+ console.log(chalk.yellow(` ⚠ ${gap.message}`));
260
+ }
261
+ console.log('');
262
+ }
263
+
264
+ } catch (error) {
265
+ console.error(chalk.red(`Error: ${error.message}`));
266
+ process.exit(1);
267
+ }
268
+ });
269
+
270
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musubi-sdd",
3
- "version": "2.2.0",
3
+ "version": "3.0.0",
4
4
  "description": "Ultimate Specification Driven Development Tool with 27 Agents for 7 AI Coding Platforms + MCP Integration (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -20,7 +20,10 @@
20
20
  "musubi-change": "bin/musubi-change.js",
21
21
  "musubi-workflow": "bin/musubi-workflow.js",
22
22
  "musubi-remember": "bin/musubi-remember.js",
23
- "musubi-resolve": "bin/musubi-resolve.js"
23
+ "musubi-resolve": "bin/musubi-resolve.js",
24
+ "musubi-convert": "bin/musubi-convert.js",
25
+ "musubi-browser": "bin/musubi-browser.js",
26
+ "musubi-gui": "bin/musubi-gui.js"
24
27
  },
25
28
  "scripts": {
26
29
  "test": "jest",
@@ -54,11 +57,18 @@
54
57
  "dependencies": {
55
58
  "@octokit/rest": "^22.0.1",
56
59
  "chalk": "^4.1.2",
60
+ "chokidar": "^3.5.3",
57
61
  "commander": "^11.0.0",
62
+ "cors": "^2.8.5",
63
+ "express": "^4.18.2",
58
64
  "fs-extra": "^11.3.2",
59
65
  "glob": "^10.5.0",
66
+ "gray-matter": "^4.0.3",
60
67
  "inquirer": "^9.0.0",
61
- "js-yaml": "^4.1.0"
68
+ "js-yaml": "^4.1.0",
69
+ "open": "^10.1.0",
70
+ "playwright": "^1.40.0",
71
+ "ws": "^8.14.2"
62
72
  },
63
73
  "devDependencies": {
64
74
  "eslint": "^8.50.0",
@@ -0,0 +1,255 @@
1
+ /**
2
+ * @fileoverview Action Executor for Browser Automation
3
+ * @module agents/browser/action-executor
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} ActionResult
8
+ * @property {boolean} success - Whether the action succeeded
9
+ * @property {string} type - Action type
10
+ * @property {string} [error] - Error message if failed
11
+ * @property {*} [data] - Result data
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} ExecutionContext
16
+ * @property {import('playwright').Page} page - Playwright page instance
17
+ * @property {import('./screenshot')} screenshot - Screenshot capture instance
18
+ * @property {number} timeout - Default timeout
19
+ */
20
+
21
+ /**
22
+ * Action Executor - Executes parsed actions using Playwright
23
+ */
24
+ class ActionExecutor {
25
+ constructor() {
26
+ this.defaultTimeout = 30000;
27
+ }
28
+
29
+ /**
30
+ * Execute a single action
31
+ * @param {import('./nl-parser').Action} action - Action to execute
32
+ * @param {ExecutionContext} context - Execution context
33
+ * @returns {Promise<ActionResult>}
34
+ */
35
+ async execute(action, context) {
36
+ const { page, timeout = this.defaultTimeout } = context;
37
+
38
+ try {
39
+ switch (action.type) {
40
+ case 'navigate':
41
+ return await this.executeNavigate(action, page, timeout);
42
+ case 'click':
43
+ return await this.executeClick(action, page, timeout);
44
+ case 'fill':
45
+ return await this.executeFill(action, page, timeout);
46
+ case 'select':
47
+ return await this.executeSelect(action, page, timeout);
48
+ case 'wait':
49
+ return await this.executeWait(action);
50
+ case 'screenshot':
51
+ return await this.executeScreenshot(action, context);
52
+ case 'assert':
53
+ return await this.executeAssert(action, page, timeout);
54
+ default:
55
+ return {
56
+ success: false,
57
+ type: action.type,
58
+ error: `Unknown action type: ${action.type}`,
59
+ };
60
+ }
61
+ } catch (error) {
62
+ return {
63
+ success: false,
64
+ type: action.type,
65
+ error: error.message,
66
+ };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Execute navigate action
72
+ * @param {Object} action
73
+ * @param {import('playwright').Page} page
74
+ * @param {number} timeout
75
+ * @returns {Promise<ActionResult>}
76
+ */
77
+ async executeNavigate(action, page, timeout) {
78
+ await page.goto(action.url, { timeout, waitUntil: 'domcontentloaded' });
79
+
80
+ return {
81
+ success: true,
82
+ type: 'navigate',
83
+ data: { url: action.url, currentUrl: page.url() },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Execute click action
89
+ * @param {Object} action
90
+ * @param {import('playwright').Page} page
91
+ * @param {number} timeout
92
+ * @returns {Promise<ActionResult>}
93
+ */
94
+ async executeClick(action, page, timeout) {
95
+ const selectors = action.selector.split(',').map(s => s.trim());
96
+
97
+ // Try each selector until one works
98
+ for (const selector of selectors) {
99
+ try {
100
+ await page.click(selector, { timeout });
101
+ return {
102
+ success: true,
103
+ type: 'click',
104
+ data: { selector },
105
+ };
106
+ } catch (e) {
107
+ // Try next selector
108
+ }
109
+ }
110
+
111
+ // If none worked, try with the original selector and let it fail
112
+ await page.click(action.selector, { timeout });
113
+
114
+ return {
115
+ success: true,
116
+ type: 'click',
117
+ data: { selector: action.selector },
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Execute fill action
123
+ * @param {Object} action
124
+ * @param {import('playwright').Page} page
125
+ * @param {number} timeout
126
+ * @returns {Promise<ActionResult>}
127
+ */
128
+ async executeFill(action, page, timeout) {
129
+ const selectors = action.selector.split(',').map(s => s.trim());
130
+
131
+ // Try each selector until one works
132
+ for (const selector of selectors) {
133
+ try {
134
+ await page.fill(selector, action.value, { timeout });
135
+ return {
136
+ success: true,
137
+ type: 'fill',
138
+ data: { selector, value: action.value },
139
+ };
140
+ } catch (e) {
141
+ // Try next selector
142
+ }
143
+ }
144
+
145
+ // If none worked, try with the original selector
146
+ await page.fill(action.selector, action.value, { timeout });
147
+
148
+ return {
149
+ success: true,
150
+ type: 'fill',
151
+ data: { selector: action.selector, value: action.value },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Execute select action
157
+ * @param {Object} action
158
+ * @param {import('playwright').Page} page
159
+ * @param {number} timeout
160
+ * @returns {Promise<ActionResult>}
161
+ */
162
+ async executeSelect(action, page, timeout) {
163
+ const selectors = action.selector.split(',').map(s => s.trim());
164
+
165
+ for (const selector of selectors) {
166
+ try {
167
+ await page.selectOption(selector, action.value, { timeout });
168
+ return {
169
+ success: true,
170
+ type: 'select',
171
+ data: { selector, value: action.value },
172
+ };
173
+ } catch (e) {
174
+ // Try next selector
175
+ }
176
+ }
177
+
178
+ await page.selectOption(action.selector, action.value, { timeout });
179
+
180
+ return {
181
+ success: true,
182
+ type: 'select',
183
+ data: { selector: action.selector, value: action.value },
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Execute wait action
189
+ * @param {Object} action
190
+ * @returns {Promise<ActionResult>}
191
+ */
192
+ async executeWait(action) {
193
+ await new Promise(resolve => setTimeout(resolve, action.delay));
194
+
195
+ return {
196
+ success: true,
197
+ type: 'wait',
198
+ data: { delay: action.delay },
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Execute screenshot action
204
+ * @param {Object} action
205
+ * @param {ExecutionContext} context
206
+ * @returns {Promise<ActionResult>}
207
+ */
208
+ async executeScreenshot(action, context) {
209
+ const { page, screenshot } = context;
210
+
211
+ const path = await screenshot.capture(page, {
212
+ name: action.name,
213
+ fullPage: action.fullPage,
214
+ });
215
+
216
+ return {
217
+ success: true,
218
+ type: 'screenshot',
219
+ data: { path, fullPage: action.fullPage },
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Execute assert action
225
+ * @param {Object} action
226
+ * @param {import('playwright').Page} page
227
+ * @param {number} timeout
228
+ * @returns {Promise<ActionResult>}
229
+ */
230
+ async executeAssert(action, page, timeout) {
231
+ const locator = page.locator(action.selector);
232
+
233
+ await locator.waitFor({ state: 'visible', timeout });
234
+
235
+ let text = null;
236
+ if (action.expectedText) {
237
+ text = await locator.textContent();
238
+ if (!text.includes(action.expectedText)) {
239
+ return {
240
+ success: false,
241
+ type: 'assert',
242
+ error: `Expected "${action.expectedText}" but found "${text}"`,
243
+ };
244
+ }
245
+ }
246
+
247
+ return {
248
+ success: true,
249
+ type: 'assert',
250
+ data: { selector: action.selector, visible: true, text },
251
+ };
252
+ }
253
+ }
254
+
255
+ module.exports = ActionExecutor;