luxlabs 1.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,393 @@
1
+ /**
2
+ * Test Command - Control webview for AI self-testing
3
+ *
4
+ * This command allows CLI/AI to interact with running dev servers:
5
+ * - Take screenshots
6
+ * - Click elements
7
+ * - Type text
8
+ * - Execute JavaScript
9
+ * - Navigate pages
10
+ */
11
+
12
+ const net = require('net');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const chalk = require('chalk');
16
+ const { LUX_STUDIO_DIR } = require('../lib/config');
17
+
18
+ // Socket path for webview control
19
+ const SOCKET_PATH = path.join(LUX_STUDIO_DIR, 'webview-control.sock');
20
+
21
+ /**
22
+ * Send a command to Lux Studio via Unix socket
23
+ */
24
+ function sendCommand(command) {
25
+ return new Promise((resolve, reject) => {
26
+ if (!fs.existsSync(SOCKET_PATH)) {
27
+ reject(new Error('Lux Studio is not running or webview control is not available'));
28
+ return;
29
+ }
30
+
31
+ const client = net.createConnection(SOCKET_PATH);
32
+ let buffer = '';
33
+
34
+ client.on('connect', () => {
35
+ client.write(JSON.stringify(command) + '\n');
36
+ });
37
+
38
+ client.on('data', (data) => {
39
+ buffer += data.toString();
40
+ const lines = buffer.split('\n');
41
+ buffer = lines.pop() || '';
42
+
43
+ for (const line of lines) {
44
+ if (line.trim()) {
45
+ try {
46
+ const result = JSON.parse(line);
47
+ client.end();
48
+ resolve(result);
49
+ } catch (err) {
50
+ reject(new Error(`Failed to parse response: ${err.message}`));
51
+ }
52
+ }
53
+ }
54
+ });
55
+
56
+ client.on('error', (err) => {
57
+ reject(new Error(`Socket error: ${err.message}`));
58
+ });
59
+
60
+ client.on('close', () => {
61
+ if (buffer.trim()) {
62
+ try {
63
+ resolve(JSON.parse(buffer));
64
+ } catch {
65
+ // No response
66
+ }
67
+ }
68
+ });
69
+
70
+ // Timeout after 10 seconds (screenshots can take longer)
71
+ setTimeout(() => {
72
+ client.destroy();
73
+ reject(new Error('Command timed out'));
74
+ }, 10000);
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Generate a unique command ID
80
+ */
81
+ function generateId() {
82
+ return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
83
+ }
84
+
85
+ /**
86
+ * Take a screenshot of the webview
87
+ */
88
+ async function screenshot(interfaceId, outputPath) {
89
+ const id = generateId();
90
+ const result = await sendCommand({
91
+ id,
92
+ type: 'screenshot',
93
+ appId: interfaceId, // Keep appId for backwards compatibility with Lux Studio
94
+ payload: { outputPath },
95
+ });
96
+
97
+ if (result.success) {
98
+ console.log(chalk.green('Screenshot captured!'));
99
+ if (result.screenshotPath) {
100
+ console.log(chalk.cyan(' Path:'), result.screenshotPath);
101
+ } else if (result.data?.path) {
102
+ console.log(chalk.cyan(' Path:'), result.data.path);
103
+ }
104
+ } else {
105
+ console.log(chalk.red('Screenshot failed:'), result.error);
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * Click an element in the webview
113
+ */
114
+ async function click(interfaceId, selector) {
115
+ const id = generateId();
116
+ const result = await sendCommand({
117
+ id,
118
+ type: 'click',
119
+ appId: interfaceId,
120
+ payload: { selector },
121
+ });
122
+
123
+ if (result.success) {
124
+ console.log(chalk.green('Clicked:'), selector);
125
+ } else {
126
+ console.log(chalk.red('Click failed:'), result.error);
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ /**
133
+ * Type text into an element
134
+ */
135
+ async function type(interfaceId, selector, text) {
136
+ const id = generateId();
137
+ const result = await sendCommand({
138
+ id,
139
+ type: 'type',
140
+ appId: interfaceId,
141
+ payload: { selector, text },
142
+ });
143
+
144
+ if (result.success) {
145
+ console.log(chalk.green('Typed text into:'), selector);
146
+ } else {
147
+ console.log(chalk.red('Type failed:'), result.error);
148
+ }
149
+
150
+ return result;
151
+ }
152
+
153
+ /**
154
+ * Execute JavaScript in the webview
155
+ */
156
+ async function evaluate(interfaceId, code) {
157
+ const id = generateId();
158
+ const result = await sendCommand({
159
+ id,
160
+ type: 'eval',
161
+ appId: interfaceId,
162
+ payload: { code },
163
+ });
164
+
165
+ if (result.success) {
166
+ console.log(chalk.green('Eval result:'), JSON.stringify(result.data, null, 2));
167
+ } else {
168
+ console.log(chalk.red('Eval failed:'), result.error);
169
+ }
170
+
171
+ return result;
172
+ }
173
+
174
+ /**
175
+ * Get current URL of the webview
176
+ */
177
+ async function getUrl(interfaceId) {
178
+ const id = generateId();
179
+ const result = await sendCommand({
180
+ id,
181
+ type: 'get-url',
182
+ appId: interfaceId,
183
+ });
184
+
185
+ if (result.success) {
186
+ console.log(chalk.green('Current URL:'), result.data?.url);
187
+ } else {
188
+ console.log(chalk.red('Get URL failed:'), result.error);
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Navigate to a URL
196
+ */
197
+ async function navigate(interfaceId, url) {
198
+ const id = generateId();
199
+ const result = await sendCommand({
200
+ id,
201
+ type: 'navigate',
202
+ appId: interfaceId,
203
+ payload: { url },
204
+ });
205
+
206
+ if (result.success) {
207
+ console.log(chalk.green('Navigated to:'), url);
208
+ } else {
209
+ console.log(chalk.red('Navigate failed:'), result.error);
210
+ }
211
+
212
+ return result;
213
+ }
214
+
215
+ /**
216
+ * Wait for a duration
217
+ */
218
+ async function wait(interfaceId, ms) {
219
+ const id = generateId();
220
+ const result = await sendCommand({
221
+ id,
222
+ type: 'wait',
223
+ appId: interfaceId,
224
+ payload: { ms: parseInt(ms, 10) },
225
+ });
226
+
227
+ if (result.success) {
228
+ console.log(chalk.green('Waited:'), ms, 'ms');
229
+ } else {
230
+ console.log(chalk.red('Wait failed:'), result.error);
231
+ }
232
+
233
+ return result;
234
+ }
235
+
236
+ /**
237
+ * Start the interface preview in Lux Studio
238
+ */
239
+ async function startPreview(interfaceId) {
240
+ const id = generateId();
241
+ const result = await sendCommand({
242
+ id,
243
+ type: 'start-preview',
244
+ appId: interfaceId,
245
+ });
246
+
247
+ if (result.success) {
248
+ console.log(chalk.green('Preview started for:'), interfaceId);
249
+ } else {
250
+ console.log(chalk.red('Start preview failed:'), result.error);
251
+ }
252
+
253
+ return result;
254
+ }
255
+
256
+ /**
257
+ * Show help for test commands
258
+ */
259
+ function showHelp() {
260
+ console.log(chalk.cyan('\nUsage:'));
261
+ console.log(' lux test screenshot <interface-id> [output-path] Take a screenshot');
262
+ console.log(' lux test click <interface-id> <selector> Click an element');
263
+ console.log(' lux test type <interface-id> <selector> <text> Type into an element');
264
+ console.log(' lux test eval <interface-id> <code> Execute JavaScript');
265
+ console.log(' lux test url <interface-id> Get current URL');
266
+ console.log(' lux test navigate <interface-id> <url> Navigate to URL');
267
+ console.log(' lux test wait <interface-id> <ms> Wait for duration');
268
+ console.log('');
269
+ console.log(chalk.cyan('Examples:'));
270
+ console.log(' lux test screenshot my-interface ./screenshot.png');
271
+ console.log(' lux test click my-interface "button.submit"');
272
+ console.log(' lux test type my-interface "#email" "user@example.com"');
273
+ console.log(' lux test eval my-interface "document.title"');
274
+ console.log(' lux test navigate my-interface "http://localhost:3000/login"');
275
+ console.log('');
276
+ console.log(chalk.dim('Note: <interface-id> is the interface ID or name from "lux servers"'));
277
+ console.log('');
278
+ }
279
+
280
+ /**
281
+ * Handle test command
282
+ */
283
+ async function handleTest(args = []) {
284
+ const [subcommand, ...rest] = args;
285
+
286
+ if (!subcommand || subcommand === 'help') {
287
+ showHelp();
288
+ return;
289
+ }
290
+
291
+ try {
292
+ switch (subcommand) {
293
+ case 'screenshot': {
294
+ const [interfaceId, outputPath] = rest;
295
+ if (!interfaceId) {
296
+ console.log(chalk.red('Error: interface-id is required'));
297
+ showHelp();
298
+ return;
299
+ }
300
+ await screenshot(interfaceId, outputPath);
301
+ break;
302
+ }
303
+
304
+ case 'click': {
305
+ const [interfaceId, selector] = rest;
306
+ if (!interfaceId || !selector) {
307
+ console.log(chalk.red('Error: interface-id and selector are required'));
308
+ showHelp();
309
+ return;
310
+ }
311
+ await click(interfaceId, selector);
312
+ break;
313
+ }
314
+
315
+ case 'type': {
316
+ const [interfaceId, selector, ...textParts] = rest;
317
+ const text = textParts.join(' ');
318
+ if (!interfaceId || !selector || !text) {
319
+ console.log(chalk.red('Error: interface-id, selector, and text are required'));
320
+ showHelp();
321
+ return;
322
+ }
323
+ await type(interfaceId, selector, text);
324
+ break;
325
+ }
326
+
327
+ case 'eval': {
328
+ const [interfaceId, ...codeParts] = rest;
329
+ const code = codeParts.join(' ');
330
+ if (!interfaceId || !code) {
331
+ console.log(chalk.red('Error: interface-id and code are required'));
332
+ showHelp();
333
+ return;
334
+ }
335
+ await evaluate(interfaceId, code);
336
+ break;
337
+ }
338
+
339
+ case 'url': {
340
+ const [interfaceId] = rest;
341
+ if (!interfaceId) {
342
+ console.log(chalk.red('Error: interface-id is required'));
343
+ showHelp();
344
+ return;
345
+ }
346
+ await getUrl(interfaceId);
347
+ break;
348
+ }
349
+
350
+ case 'navigate':
351
+ case 'nav': {
352
+ const [interfaceId, url] = rest;
353
+ if (!interfaceId || !url) {
354
+ console.log(chalk.red('Error: interface-id and url are required'));
355
+ showHelp();
356
+ return;
357
+ }
358
+ await navigate(interfaceId, url);
359
+ break;
360
+ }
361
+
362
+ case 'wait': {
363
+ const [interfaceId, ms] = rest;
364
+ if (!interfaceId || !ms) {
365
+ console.log(chalk.red('Error: interface-id and duration (ms) are required'));
366
+ showHelp();
367
+ return;
368
+ }
369
+ await wait(interfaceId, ms);
370
+ break;
371
+ }
372
+
373
+ default:
374
+ console.log(chalk.red(`Unknown test command: ${subcommand}`));
375
+ showHelp();
376
+ }
377
+ } catch (err) {
378
+ console.log(chalk.red('Error:'), err.message);
379
+ }
380
+ }
381
+
382
+ module.exports = {
383
+ handleTest,
384
+ screenshot,
385
+ click,
386
+ type,
387
+ evaluate,
388
+ getUrl,
389
+ navigate,
390
+ wait,
391
+ startPreview,
392
+ sendCommand,
393
+ };