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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Screenshot Capture for Browser Automation
|
|
3
|
+
* @module agents/browser/screenshot
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Screenshot Capture - Handles screenshot taking and management
|
|
11
|
+
*/
|
|
12
|
+
class ScreenshotCapture {
|
|
13
|
+
/**
|
|
14
|
+
* Create a new ScreenshotCapture instance
|
|
15
|
+
* @param {string} outputDir - Output directory for screenshots
|
|
16
|
+
*/
|
|
17
|
+
constructor(outputDir) {
|
|
18
|
+
this.outputDir = outputDir;
|
|
19
|
+
this.screenshots = [];
|
|
20
|
+
this.counter = 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure the output directory exists
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
async ensureDir() {
|
|
28
|
+
await fs.ensureDir(this.outputDir);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Capture a screenshot
|
|
33
|
+
* @param {import('playwright').Page} page - Playwright page
|
|
34
|
+
* @param {Object} options - Capture options
|
|
35
|
+
* @param {string} [options.name] - Screenshot name
|
|
36
|
+
* @param {boolean} [options.fullPage=false] - Capture full page
|
|
37
|
+
* @param {string} [options.type='png'] - Image type (png/jpeg)
|
|
38
|
+
* @param {number} [options.quality] - JPEG quality (0-100)
|
|
39
|
+
* @returns {Promise<string>} Screenshot file path
|
|
40
|
+
*/
|
|
41
|
+
async capture(page, options = {}) {
|
|
42
|
+
await this.ensureDir();
|
|
43
|
+
|
|
44
|
+
const timestamp = Date.now();
|
|
45
|
+
this.counter++;
|
|
46
|
+
|
|
47
|
+
const name = options.name || `screenshot-${this.counter}`;
|
|
48
|
+
const extension = options.type || 'png';
|
|
49
|
+
const filename = `${timestamp}-${name}.${extension}`;
|
|
50
|
+
const filePath = path.join(this.outputDir, filename);
|
|
51
|
+
|
|
52
|
+
const screenshotOptions = {
|
|
53
|
+
path: filePath,
|
|
54
|
+
fullPage: options.fullPage || false,
|
|
55
|
+
type: extension,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (extension === 'jpeg' && options.quality) {
|
|
59
|
+
screenshotOptions.quality = options.quality;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await page.screenshot(screenshotOptions);
|
|
63
|
+
|
|
64
|
+
const metadata = {
|
|
65
|
+
path: filePath,
|
|
66
|
+
filename,
|
|
67
|
+
name,
|
|
68
|
+
timestamp,
|
|
69
|
+
fullPage: options.fullPage || false,
|
|
70
|
+
url: page.url(),
|
|
71
|
+
title: await page.title(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.screenshots.push(metadata);
|
|
75
|
+
|
|
76
|
+
return filePath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Capture an element screenshot
|
|
81
|
+
* @param {import('playwright').Page} page - Playwright page
|
|
82
|
+
* @param {string} selector - Element selector
|
|
83
|
+
* @param {Object} options - Capture options
|
|
84
|
+
* @returns {Promise<string>} Screenshot file path
|
|
85
|
+
*/
|
|
86
|
+
async captureElement(page, selector, options = {}) {
|
|
87
|
+
await this.ensureDir();
|
|
88
|
+
|
|
89
|
+
const timestamp = Date.now();
|
|
90
|
+
this.counter++;
|
|
91
|
+
|
|
92
|
+
const name = options.name || `element-${this.counter}`;
|
|
93
|
+
const extension = options.type || 'png';
|
|
94
|
+
const filename = `${timestamp}-${name}.${extension}`;
|
|
95
|
+
const filePath = path.join(this.outputDir, filename);
|
|
96
|
+
|
|
97
|
+
const element = page.locator(selector);
|
|
98
|
+
await element.screenshot({ path: filePath });
|
|
99
|
+
|
|
100
|
+
const metadata = {
|
|
101
|
+
path: filePath,
|
|
102
|
+
filename,
|
|
103
|
+
name,
|
|
104
|
+
timestamp,
|
|
105
|
+
selector,
|
|
106
|
+
url: page.url(),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.screenshots.push(metadata);
|
|
110
|
+
|
|
111
|
+
return filePath;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all captured screenshots
|
|
116
|
+
* @returns {Array<Object>}
|
|
117
|
+
*/
|
|
118
|
+
getAll() {
|
|
119
|
+
return [...this.screenshots];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the latest screenshot
|
|
124
|
+
* @returns {Object|null}
|
|
125
|
+
*/
|
|
126
|
+
getLatest() {
|
|
127
|
+
if (this.screenshots.length === 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return this.screenshots[this.screenshots.length - 1];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get a screenshot by name
|
|
135
|
+
* @param {string} name
|
|
136
|
+
* @returns {Object|undefined}
|
|
137
|
+
*/
|
|
138
|
+
getByName(name) {
|
|
139
|
+
return this.screenshots.find(s => s.name === name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Clear screenshot history
|
|
144
|
+
*/
|
|
145
|
+
clearHistory() {
|
|
146
|
+
this.screenshots = [];
|
|
147
|
+
this.counter = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Delete all screenshots from disk
|
|
152
|
+
* @returns {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
async cleanup() {
|
|
155
|
+
for (const screenshot of this.screenshots) {
|
|
156
|
+
try {
|
|
157
|
+
await fs.remove(screenshot.path);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// Ignore errors
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.clearHistory();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get screenshot count
|
|
167
|
+
* @returns {number}
|
|
168
|
+
*/
|
|
169
|
+
get count() {
|
|
170
|
+
return this.screenshots.length;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = ScreenshotCapture;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Test Code Generator for Browser Automation
|
|
3
|
+
* @module agents/browser/test-generator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Test Code Generator - Generates Playwright test code from action history
|
|
11
|
+
*/
|
|
12
|
+
class TestGenerator {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.templates = {
|
|
15
|
+
playwright: this.generatePlaywrightTest.bind(this),
|
|
16
|
+
jest: this.generateJestTest.bind(this),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate test code from action history
|
|
22
|
+
* @param {Array} history - Action history
|
|
23
|
+
* @param {Object} options - Generation options
|
|
24
|
+
* @param {string} [options.name='Generated Test'] - Test name
|
|
25
|
+
* @param {string} [options.format='playwright'] - Test format
|
|
26
|
+
* @param {string} [options.output] - Output file path
|
|
27
|
+
* @returns {Promise<string>} Generated test code
|
|
28
|
+
*/
|
|
29
|
+
async generateTest(history, options = {}) {
|
|
30
|
+
const format = options.format || 'playwright';
|
|
31
|
+
const generator = this.templates[format];
|
|
32
|
+
|
|
33
|
+
if (!generator) {
|
|
34
|
+
throw new Error(`Unknown test format: ${format}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const code = generator(history, options);
|
|
38
|
+
|
|
39
|
+
if (options.output) {
|
|
40
|
+
await fs.ensureDir(path.dirname(options.output));
|
|
41
|
+
await fs.writeFile(options.output, code, 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return code;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate Playwright test
|
|
49
|
+
* @param {Array} history
|
|
50
|
+
* @param {Object} options
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
generatePlaywrightTest(history, options = {}) {
|
|
54
|
+
const testName = options.name || 'Generated Test';
|
|
55
|
+
const lines = [
|
|
56
|
+
`import { test, expect } from '@playwright/test';`,
|
|
57
|
+
``,
|
|
58
|
+
`test('${this.escapeString(testName)}', async ({ page }) => {`,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const item of history) {
|
|
62
|
+
const action = item.action;
|
|
63
|
+
const code = this.actionToPlaywrightCode(action);
|
|
64
|
+
if (code) {
|
|
65
|
+
lines.push(` ${code}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
lines.push(`});`);
|
|
70
|
+
lines.push(``);
|
|
71
|
+
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert action to Playwright code
|
|
77
|
+
* @param {Object} action
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
actionToPlaywrightCode(action) {
|
|
81
|
+
switch (action.type) {
|
|
82
|
+
case 'navigate':
|
|
83
|
+
return `await page.goto('${this.escapeString(action.url)}');`;
|
|
84
|
+
|
|
85
|
+
case 'click':
|
|
86
|
+
return `await page.click('${this.escapeSelector(action.selector)}');`;
|
|
87
|
+
|
|
88
|
+
case 'fill':
|
|
89
|
+
return `await page.fill('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
|
|
90
|
+
|
|
91
|
+
case 'select':
|
|
92
|
+
return `await page.selectOption('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
|
|
93
|
+
|
|
94
|
+
case 'wait':
|
|
95
|
+
return `await page.waitForTimeout(${action.delay});`;
|
|
96
|
+
|
|
97
|
+
case 'screenshot':
|
|
98
|
+
const name = action.name || 'screenshot';
|
|
99
|
+
return `await page.screenshot({ path: 'screenshots/${name}.png'${action.fullPage ? ', fullPage: true' : ''} });`;
|
|
100
|
+
|
|
101
|
+
case 'assert':
|
|
102
|
+
if (action.expectedText) {
|
|
103
|
+
return `await expect(page.locator('text="${this.escapeString(action.expectedText)}"')).toBeVisible();`;
|
|
104
|
+
}
|
|
105
|
+
return `await expect(page.locator('${this.escapeSelector(action.selector)}')).toBeVisible();`;
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
return `// Unknown action: ${action.type}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate Jest + Puppeteer style test
|
|
114
|
+
* @param {Array} history
|
|
115
|
+
* @param {Object} options
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
generateJestTest(history, options = {}) {
|
|
119
|
+
const testName = options.name || 'Generated Test';
|
|
120
|
+
const lines = [
|
|
121
|
+
`const puppeteer = require('puppeteer');`,
|
|
122
|
+
``,
|
|
123
|
+
`describe('${this.escapeString(testName)}', () => {`,
|
|
124
|
+
` let browser;`,
|
|
125
|
+
` let page;`,
|
|
126
|
+
``,
|
|
127
|
+
` beforeAll(async () => {`,
|
|
128
|
+
` browser = await puppeteer.launch();`,
|
|
129
|
+
` page = await browser.newPage();`,
|
|
130
|
+
` });`,
|
|
131
|
+
``,
|
|
132
|
+
` afterAll(async () => {`,
|
|
133
|
+
` await browser.close();`,
|
|
134
|
+
` });`,
|
|
135
|
+
``,
|
|
136
|
+
` test('should complete flow', async () => {`,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const item of history) {
|
|
140
|
+
const action = item.action;
|
|
141
|
+
const code = this.actionToPuppeteerCode(action);
|
|
142
|
+
if (code) {
|
|
143
|
+
lines.push(` ${code}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines.push(` });`);
|
|
148
|
+
lines.push(`});`);
|
|
149
|
+
lines.push(``);
|
|
150
|
+
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Convert action to Puppeteer code
|
|
156
|
+
* @param {Object} action
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
actionToPuppeteerCode(action) {
|
|
160
|
+
switch (action.type) {
|
|
161
|
+
case 'navigate':
|
|
162
|
+
return `await page.goto('${this.escapeString(action.url)}');`;
|
|
163
|
+
|
|
164
|
+
case 'click':
|
|
165
|
+
return `await page.click('${this.escapeSelector(action.selector)}');`;
|
|
166
|
+
|
|
167
|
+
case 'fill':
|
|
168
|
+
return `await page.type('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
|
|
169
|
+
|
|
170
|
+
case 'select':
|
|
171
|
+
return `await page.select('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
|
|
172
|
+
|
|
173
|
+
case 'wait':
|
|
174
|
+
return `await new Promise(r => setTimeout(r, ${action.delay}));`;
|
|
175
|
+
|
|
176
|
+
case 'screenshot':
|
|
177
|
+
const name = action.name || 'screenshot';
|
|
178
|
+
return `await page.screenshot({ path: 'screenshots/${name}.png'${action.fullPage ? ', fullPage: true' : ''} });`;
|
|
179
|
+
|
|
180
|
+
case 'assert':
|
|
181
|
+
return `await page.waitForSelector('${this.escapeSelector(action.selector)}');`;
|
|
182
|
+
|
|
183
|
+
default:
|
|
184
|
+
return `// Unknown action: ${action.type}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate test from MUSUBI specification
|
|
190
|
+
* @param {Object} specification - MUSUBI specification
|
|
191
|
+
* @param {Object} options
|
|
192
|
+
* @returns {string}
|
|
193
|
+
*/
|
|
194
|
+
generateFromSpec(specification, options = {}) {
|
|
195
|
+
const lines = [
|
|
196
|
+
`import { test, expect } from '@playwright/test';`,
|
|
197
|
+
``,
|
|
198
|
+
`/**`,
|
|
199
|
+
` * Tests generated from MUSUBI specification: ${specification.title || 'Unknown'}`,
|
|
200
|
+
` */`,
|
|
201
|
+
``,
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
for (const req of specification.requirements || []) {
|
|
205
|
+
const testCode = this.requirementToTest(req);
|
|
206
|
+
lines.push(testCode);
|
|
207
|
+
lines.push(``);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Convert a requirement to a test
|
|
215
|
+
* @param {Object} requirement
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
requirementToTest(requirement) {
|
|
219
|
+
const testName = `${requirement.id}: ${requirement.title || requirement.action || 'Requirement'}`;
|
|
220
|
+
|
|
221
|
+
const lines = [
|
|
222
|
+
`test('${this.escapeString(testName)}', async ({ page }) => {`,
|
|
223
|
+
` // Pattern: ${requirement.pattern}`,
|
|
224
|
+
` // Statement: ${requirement.statement || ''}`,
|
|
225
|
+
` `,
|
|
226
|
+
` // TODO: Implement test based on requirement`,
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
// Add hints based on pattern
|
|
230
|
+
if (requirement.pattern === 'event-driven' && requirement.trigger) {
|
|
231
|
+
lines.push(` // Trigger: ${requirement.trigger}`);
|
|
232
|
+
lines.push(` // Action: ${requirement.action}`);
|
|
233
|
+
} else if (requirement.pattern === 'state-driven' && requirement.condition) {
|
|
234
|
+
lines.push(` // Condition: ${requirement.condition}`);
|
|
235
|
+
lines.push(` // Action: ${requirement.action}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
lines.push(` `);
|
|
239
|
+
lines.push(` throw new Error('Test not implemented');`);
|
|
240
|
+
lines.push(`});`);
|
|
241
|
+
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Escape a string for JavaScript
|
|
247
|
+
* @param {string} str
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
escapeString(str) {
|
|
251
|
+
if (!str) return '';
|
|
252
|
+
return str
|
|
253
|
+
.replace(/\\/g, '\\\\')
|
|
254
|
+
.replace(/'/g, "\\'")
|
|
255
|
+
.replace(/"/g, '\\"')
|
|
256
|
+
.replace(/\n/g, '\\n')
|
|
257
|
+
.replace(/\r/g, '\\r');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Escape a CSS selector for JavaScript
|
|
262
|
+
* @param {string} selector
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
escapeSelector(selector) {
|
|
266
|
+
if (!selector) return '*';
|
|
267
|
+
return selector.replace(/'/g, "\\'");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = TestGenerator;
|