Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2// KidLisp utility commands - quick actions via CDP
3import http from 'http';
4import WebSocket from 'ws';
5
6// Try multiple CDP host/port combinations (same as artery.mjs)
7async function findWorkingCDPHost() {
8 const candidates = [
9 { host: 'host.docker.internal', port: 9333 },
10 { host: 'host.docker.internal', port: 9222 },
11 { host: 'localhost', port: 9333 },
12 { host: 'localhost', port: 9222 },
13 { host: '172.17.0.1', port: 9224 },
14 { host: '172.17.0.1', port: 9223 },
15 { host: '172.17.0.1', port: 9222 },
16 ];
17
18 for (const { host, port } of candidates) {
19 try {
20 const works = await new Promise((resolve) => {
21 const req = http.get({
22 hostname: host,
23 port: port,
24 path: '/json',
25 timeout: 1000,
26 headers: { 'Host': 'localhost' }
27 }, (res) => {
28 let data = '';
29 res.on('data', (chunk) => data += chunk);
30 res.on('end', () => resolve(data.length > 0));
31 });
32 req.on('error', () => resolve(false));
33 req.on('timeout', () => { req.destroy(); resolve(false); });
34 });
35 if (works) return { host, port };
36 } catch (e) {}
37 }
38
39 throw new Error('CDP not available - is VS Code running with --remote-debugging-port?');
40}
41
42async function getTargets() {
43 const { host, port } = await findWorkingCDPHost();
44 return new Promise((resolve, reject) => {
45 http.get({
46 hostname: host,
47 port: port,
48 path: '/json',
49 headers: { 'Host': 'localhost' }
50 }, (res) => {
51 let data = '';
52 res.on('data', chunk => data += chunk);
53 res.on('end', () => {
54 const targets = JSON.parse(data);
55 // Attach the host/port for later use
56 resolve({ targets, host, port });
57 });
58 }).on('error', reject);
59 });
60}
61
62async function connectToKidLisp() {
63 const { targets, host, port } = await getTargets();
64 const kidlisp = targets.find(t => t.url && t.url.includes('kidlisp.com'));
65
66 if (!kidlisp) {
67 throw new Error('KidLisp window not found. Open it first with: artery K');
68 }
69
70 let wsUrl = kidlisp.webSocketDebuggerUrl;
71 // Fix localhost if needed (some CDP configs return localhost instead of the actual host)
72 if (wsUrl.includes('localhost')) {
73 wsUrl = wsUrl.replace(/ws:\/\/localhost:(\d+)/, `ws://${host}:${port}`);
74 }
75
76 const ws = new WebSocket(wsUrl);
77
78 await new Promise((resolve, reject) => {
79 ws.on('open', resolve);
80 ws.on('error', reject);
81 setTimeout(() => reject(new Error('Connection timeout')), 5000);
82 });
83
84 return ws;
85}
86
87let messageIdCounter = 100000;
88
89async function evaluate(ws, expression) {
90 return new Promise((resolve, reject) => {
91 const id = messageIdCounter++;
92 let timer;
93
94 const handler = (data) => {
95 const msg = JSON.parse(data);
96 if (msg.id === id) {
97 clearTimeout(timer);
98 ws.off('message', handler);
99 if (msg.result?.exceptionDetails) {
100 reject(new Error(msg.result.exceptionDetails.exception?.description || 'Error'));
101 } else {
102 resolve(msg.result?.result?.value);
103 }
104 }
105 };
106
107 ws.on('message', handler);
108 ws.send(JSON.stringify({
109 id,
110 method: 'Runtime.evaluate',
111 params: { expression, returnByValue: true }
112 }));
113
114 timer = setTimeout(() => {
115 ws.off('message', handler);
116 reject(new Error('Timeout waiting for evaluation result'));
117 }, 10000);
118 });
119}
120
121// Commands
122const commands = {
123 async 'close-tabs'() {
124 const ws = await connectToKidLisp();
125 const result = await evaluate(ws, `
126 (function() {
127 const tabs = document.querySelectorAll('.tab-close');
128 const count = tabs.length;
129 tabs.forEach(btn => btn.click());
130 return { closedTabs: count };
131 })()
132 `);
133 console.log(`✓ Closed ${result.closedTabs} tab(s)`);
134 ws.close();
135 },
136
137 async 'list-tabs'() {
138 const ws = await connectToKidLisp();
139 const tabs = await evaluate(ws, `
140 Array.from(document.querySelectorAll('.tab')).map(tab => ({
141 name: tab.querySelector('.tab-name')?.textContent || 'unnamed',
142 active: tab.classList.contains('active')
143 }))
144 `);
145 if (tabs.length === 0) {
146 console.log('No tabs open');
147 } else {
148 console.log('Open tabs:');
149 tabs.forEach(t => console.log(` ${t.active ? '→' : ' '} ${t.name}`));
150 }
151 ws.close();
152 },
153
154 async clear() {
155 const ws = await connectToKidLisp();
156 await evaluate(ws, `monaco.editor.getEditors()[0].setValue('')`);
157 console.log('✓ Editor cleared');
158 ws.close();
159 },
160
161 async stop() {
162 const ws = await connectToKidLisp();
163 await evaluate(ws, `document.getElementById('stop-button')?.click()`);
164 console.log('✓ Stopped');
165 ws.close();
166 },
167
168 async play() {
169 const ws = await connectToKidLisp();
170 await evaluate(ws, `document.getElementById('send-button')?.click()`);
171 console.log('✓ Playing');
172 ws.close();
173 },
174
175 async 'clear-console'() {
176 const ws = await connectToKidLisp();
177 await evaluate(ws, `document.getElementById('console-output').innerHTML = ''`);
178 console.log('✓ Console cleared');
179 ws.close();
180 },
181
182 async theme() {
183 const ws = await connectToKidLisp();
184 await evaluate(ws, `document.getElementById('theme-toggle')?.click()`);
185 const theme = await evaluate(ws, `document.documentElement.getAttribute('data-theme')`);
186 console.log(`✓ Theme: ${theme}`);
187 ws.close();
188 },
189
190 // Card commands
191 async 'card-next'() {
192 const ws = await connectToKidLisp();
193 await evaluate(ws, `document.getElementById('card-next-btn')?.click()`);
194 console.log('✓ Next card');
195 ws.close();
196 },
197
198 async 'card-prev'() {
199 const ws = await connectToKidLisp();
200 await evaluate(ws, `document.getElementById('card-prev-btn')?.click()`);
201 console.log('✓ Previous card');
202 ws.close();
203 },
204
205 async 'card-flip'() {
206 const ws = await connectToKidLisp();
207 await evaluate(ws, `document.getElementById('card-flip-btn')?.click()`);
208 console.log('✓ Card flipped');
209 ws.close();
210 },
211
212 async 'card-spread'() {
213 const ws = await connectToKidLisp();
214 await evaluate(ws, `document.getElementById('card-spread-btn')?.click()`);
215 console.log('✓ Spread toggled');
216 ws.close();
217 },
218
219 async help() {
220 console.log('KidLisp Utilities');
221 console.log('');
222 console.log('Usage: node kidlisp-util.mjs <command>');
223 console.log('');
224 console.log('Commands:');
225 console.log(' close-tabs Close all open tabs');
226 console.log(' list-tabs List open tabs');
227 console.log(' clear Clear the editor');
228 console.log(' stop Stop playback');
229 console.log(' play Start playback');
230 console.log(' clear-console Clear the console');
231 console.log(' theme Toggle theme');
232 console.log('');
233 console.log('Card commands:');
234 console.log(' card-next Go to next card');
235 console.log(' card-prev Go to previous card');
236 console.log(' card-flip Flip the top card');
237 console.log(' card-spread Toggle spread view');
238 console.log('');
239 console.log(' help Show this help');
240 }
241};
242
243// Main
244const cmd = process.argv[2] || 'help';
245if (commands[cmd]) {
246 commands[cmd]().catch(e => {
247 console.error('Error:', e.message);
248 process.exit(1);
249 });
250} else {
251 console.error(`Unknown command: ${cmd}`);
252 commands.help();
253 process.exit(1);
254}