Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2/**
3 * 🎨 Artery Emacs Bridge
4 *
5 * Connects artery services to Emacs via JSON lines on stdin/stdout.
6 * Designed to be spawned as a subprocess from artery.el
7 *
8 * Protocol:
9 * Input (from Emacs):
10 * {"type": "eval", "code": "..."} - Eval JS in DAW
11 * {"type": "connect-daw"} - Connect to DAW CDP
12 * {"type": "disconnect-daw"} - Disconnect from DAW
13 * {"type": "get-state"} - Get current state
14 *
15 * Output (to Emacs):
16 * {"type": "daw-state", "bpm": 120, "playing": true}
17 * {"type": "daw-log", "timestamp": "...", "logType": "info", "message": "..."}
18 * {"type": "daw-connected", "title": "...", "url": "..."}
19 * {"type": "daw-disconnected"}
20 * {"type": "server-status", "status": "running|error|unknown"}
21 * {"type": "error", "message": "..."}
22 */
23
24import * as readline from 'readline';
25import DAWDebugger from './daw-debug.mjs';
26import http from 'http';
27import https from 'https';
28
29// State
30let dawDebugger = null;
31let dawStateInterval = null;
32let serverCheckInterval = null;
33
34// Send JSON message to Emacs
35function emit(msg) {
36 console.log(JSON.stringify(msg));
37}
38
39// Check AC server status
40async function checkServerStatus() {
41 const serverUrl = process.env.AC_SERVER_URL || 'https://localhost:8888';
42
43 return new Promise((resolve) => {
44 const protocol = serverUrl.startsWith('https') ? https : http;
45 const req = protocol.get(serverUrl, {
46 timeout: 3000,
47 rejectUnauthorized: false // Allow self-signed certs in dev
48 }, (res) => {
49 resolve(res.statusCode < 400 ? 'running' : 'error');
50 });
51
52 req.on('error', () => resolve('error'));
53 req.on('timeout', () => {
54 req.destroy();
55 resolve('error');
56 });
57 });
58}
59
60// Connect to DAW
61async function connectDAW() {
62 if (dawDebugger && dawDebugger.connected) {
63 emit({ type: 'error', message: 'Already connected to DAW' });
64 return;
65 }
66
67 dawDebugger = new DAWDebugger({
68 json: true,
69 onLog: (entry) => {
70 emit({
71 type: 'daw-log',
72 timestamp: new Date(entry.timestamp).toISOString(),
73 logType: entry.type,
74 message: entry.text,
75 source: entry.source
76 });
77 },
78 onConnect: (targetInfo) => {
79 emit({
80 type: 'daw-connected',
81 title: targetInfo.title,
82 url: targetInfo.url
83 });
84
85 // Start polling DAW state
86 if (dawStateInterval) clearInterval(dawStateInterval);
87 dawStateInterval = setInterval(async () => {
88 try {
89 const state = await dawDebugger.getDAWState();
90 if (state) {
91 emit({
92 type: 'daw-state',
93 bpm: state.bpm,
94 playing: state.playing,
95 time: state.time
96 });
97 }
98 } catch (e) {
99 // Ignore polling errors
100 }
101 }, 500);
102 },
103 onDisconnect: () => {
104 emit({ type: 'daw-disconnected' });
105 if (dawStateInterval) {
106 clearInterval(dawStateInterval);
107 dawStateInterval = null;
108 }
109 }
110 });
111
112 try {
113 await dawDebugger.connect();
114 } catch (err) {
115 emit({ type: 'error', message: `DAW connection failed: ${err.message}` });
116 dawDebugger = null;
117 }
118}
119
120// Disconnect from DAW
121function disconnectDAW() {
122 if (dawDebugger) {
123 dawDebugger.disconnect();
124 dawDebugger = null;
125 }
126 if (dawStateInterval) {
127 clearInterval(dawStateInterval);
128 dawStateInterval = null;
129 }
130 emit({ type: 'daw-disconnected' });
131}
132
133// Evaluate code in DAW
134async function evalInDAW(code) {
135 if (!dawDebugger || !dawDebugger.connected) {
136 emit({ type: 'error', message: 'Not connected to DAW' });
137 return;
138 }
139
140 try {
141 const result = await dawDebugger.evaluate(code);
142 emit({ type: 'eval-result', result });
143 } catch (err) {
144 emit({ type: 'error', message: `Eval failed: ${err.message}` });
145 }
146}
147
148// Get current state
149async function getState() {
150 const serverStatus = await checkServerStatus();
151
152 let dawState = null;
153 if (dawDebugger && dawDebugger.connected) {
154 try {
155 dawState = await dawDebugger.getDAWState();
156 } catch (e) {
157 // Ignore
158 }
159 }
160
161 emit({
162 type: 'state',
163 server: serverStatus,
164 daw: dawDebugger?.connected ? {
165 connected: true,
166 title: dawDebugger.targetInfo?.title,
167 ...dawState
168 } : { connected: false }
169 });
170}
171
172// Handle incoming messages from Emacs
173async function handleMessage(msg) {
174 try {
175 const { type, ...params } = msg;
176
177 switch (type) {
178 case 'connect-daw':
179 await connectDAW();
180 break;
181
182 case 'disconnect-daw':
183 disconnectDAW();
184 break;
185
186 case 'eval':
187 await evalInDAW(params.code);
188 break;
189
190 case 'get-state':
191 await getState();
192 break;
193
194 default:
195 emit({ type: 'error', message: `Unknown message type: ${type}` });
196 }
197 } catch (err) {
198 emit({ type: 'error', message: err.message });
199 }
200}
201
202// --- Main ---
203
204// Start server status polling
205serverCheckInterval = setInterval(async () => {
206 const status = await checkServerStatus();
207 emit({ type: 'server-status', status });
208}, 5000);
209
210// Initial server check
211checkServerStatus().then(status => {
212 emit({ type: 'server-status', status });
213});
214
215// Auto-connect to DAW on startup
216setTimeout(connectDAW, 1000);
217
218// Read JSON lines from stdin
219const rl = readline.createInterface({
220 input: process.stdin,
221 terminal: false
222});
223
224rl.on('line', async (line) => {
225 try {
226 const msg = JSON.parse(line);
227 await handleMessage(msg);
228 } catch (e) {
229 emit({ type: 'error', message: `Parse error: ${e.message}` });
230 }
231});
232
233// Handle shutdown
234process.on('SIGINT', () => {
235 disconnectDAW();
236 if (serverCheckInterval) clearInterval(serverCheckInterval);
237 process.exit(0);
238});
239
240process.on('SIGTERM', () => {
241 disconnectDAW();
242 if (serverCheckInterval) clearInterval(serverCheckInterval);
243 process.exit(0);
244});
245
246// Send ready message
247emit({ type: 'ready', version: '1.0.0' });
248
249console.error('🎨 Artery Emacs Bridge started');