@antigenic-oss/paint 0.1.0 → 0.2.1

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.
Files changed (65) hide show
  1. package/README.md +32 -17
  2. package/bin/bridge-server.js +36 -0
  3. package/bin/paint.js +542 -102
  4. package/bin/terminal-server.js +105 -0
  5. package/package.json +7 -8
  6. package/public/dev-editor-inspector.js +92 -104
  7. package/src/app/api/claude/apply/route.ts +2 -2
  8. package/src/app/api/project/scan/route.ts +1 -1
  9. package/src/app/api/proxy/[[...path]]/route.ts +4 -4
  10. package/src/app/docs/DocsClient.tsx +1 -1
  11. package/src/app/docs/page.tsx +0 -1
  12. package/src/app/page.tsx +1 -1
  13. package/src/bridge/api-handlers.ts +1 -1
  14. package/src/bridge/proxy-handler.ts +4 -4
  15. package/src/bridge/server.ts +135 -39
  16. package/src/components/ConnectModal.tsx +1 -2
  17. package/src/components/PreviewFrame.tsx +2 -2
  18. package/src/components/ResponsiveToolbar.tsx +1 -2
  19. package/src/components/common/ColorPicker.tsx +7 -9
  20. package/src/components/common/UnitInput.tsx +1 -1
  21. package/src/components/common/VariableColorPicker.tsx +0 -1
  22. package/src/components/left-panel/ComponentsPanel.tsx +3 -3
  23. package/src/components/left-panel/LayerNode.tsx +1 -1
  24. package/src/components/left-panel/icons.tsx +1 -1
  25. package/src/components/left-panel/terminal/TerminalPanel.tsx +2 -2
  26. package/src/components/right-panel/ElementLogBox.tsx +1 -3
  27. package/src/components/right-panel/changes/ChangesPanel.tsx +12 -12
  28. package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +2 -2
  29. package/src/components/right-panel/claude/DiffViewer.tsx +1 -1
  30. package/src/components/right-panel/claude/ProjectRootSelector.tsx +7 -7
  31. package/src/components/right-panel/claude/SetupFlow.tsx +1 -1
  32. package/src/components/right-panel/console/ConsolePanel.tsx +4 -4
  33. package/src/components/right-panel/design/BackgroundSection.tsx +2 -2
  34. package/src/components/right-panel/design/GradientEditor.tsx +6 -6
  35. package/src/components/right-panel/design/LayoutSection.tsx +4 -4
  36. package/src/components/right-panel/design/PositionSection.tsx +2 -2
  37. package/src/components/right-panel/design/SVGSection.tsx +2 -3
  38. package/src/components/right-panel/design/ShadowBlurSection.tsx +5 -5
  39. package/src/components/right-panel/design/TextSection.tsx +5 -5
  40. package/src/components/right-panel/design/icons.tsx +1 -1
  41. package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +2 -2
  42. package/src/components/right-panel/design/inputs/CompactInput.tsx +2 -2
  43. package/src/components/right-panel/design/inputs/DraggableLabel.tsx +2 -1
  44. package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +1 -1
  45. package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +3 -3
  46. package/src/components/right-panel/design/inputs/SectionHeader.tsx +2 -1
  47. package/src/hooks/useDOMTree.ts +0 -1
  48. package/src/hooks/usePostMessage.ts +4 -3
  49. package/src/hooks/useTargetUrl.ts +1 -1
  50. package/src/inspector/DOMTraverser.ts +2 -2
  51. package/src/inspector/HoverHighlighter.ts +6 -6
  52. package/src/inspector/SelectionHighlighter.ts +4 -4
  53. package/src/lib/classifyElement.ts +1 -2
  54. package/src/lib/claude-bin.ts +1 -1
  55. package/src/lib/clientProjectScanner.ts +13 -13
  56. package/src/lib/cssVariableUtils.ts +1 -1
  57. package/src/lib/folderPicker.ts +4 -1
  58. package/src/lib/projectScanner.ts +15 -15
  59. package/src/lib/tailwindClassParser.ts +1 -1
  60. package/src/lib/textShadowUtils.ts +1 -1
  61. package/src/lib/utils.ts +4 -4
  62. package/src/proxy.ts +1 -1
  63. package/src/store/treeSlice.ts +2 -2
  64. package/src/server/terminal-server.ts +0 -104
  65. package/tsconfig.server.json +0 -12
package/bin/paint.js CHANGED
@@ -3,13 +3,36 @@
3
3
  const fs = require('node:fs')
4
4
  const os = require('node:os')
5
5
  const path = require('node:path')
6
+ const http = require('node:http')
7
+ const https = require('node:https')
6
8
  const { spawn, spawnSync } = require('node:child_process')
7
9
 
8
10
  const APP_ROOT = path.resolve(__dirname, '..')
9
11
  const STATE_DIR = path.join(os.homedir(), '.paint')
10
- const STATE_FILE = path.join(STATE_DIR, 'server.json')
11
- const LOG_FILE = path.join(STATE_DIR, 'server.log')
12
- const NEXT_BIN = path.join(APP_ROOT, 'node_modules', 'next', 'dist', 'bin', 'next')
12
+
13
+ const APP_STATE_FILE = path.join(STATE_DIR, 'server.json')
14
+ const TERMINAL_STATE_FILE = path.join(STATE_DIR, 'terminal.json')
15
+ const BRIDGE_STATE_FILE = path.join(STATE_DIR, 'bridge.json')
16
+
17
+ const WEB_LOG_FILE = path.join(STATE_DIR, 'web.log')
18
+ const TERMINAL_LOG_FILE = path.join(STATE_DIR, 'terminal.log')
19
+ const BRIDGE_LOG_FILE = path.join(STATE_DIR, 'bridge.log')
20
+
21
+ const NEXT_BIN = path.join(
22
+ APP_ROOT,
23
+ 'node_modules',
24
+ 'next',
25
+ 'dist',
26
+ 'bin',
27
+ 'next',
28
+ )
29
+ const TERMINAL_SERVER_BIN = path.join(APP_ROOT, 'bin', 'terminal-server.js')
30
+ const BRIDGE_SERVER_BIN = path.join(APP_ROOT, 'bin', 'bridge-server.js')
31
+
32
+ const DEFAULT_HOST = '127.0.0.1'
33
+ const DEFAULT_WEB_PORT = 4000
34
+ const DEFAULT_TERMINAL_PORT = 4001
35
+ const DEFAULT_BRIDGE_PORT = 4002
13
36
 
14
37
  function ensureStateDir() {
15
38
  fs.mkdirSync(STATE_DIR, { recursive: true })
@@ -20,17 +43,35 @@ function now() {
20
43
  }
21
44
 
22
45
  function parseArgs(argv) {
46
+ const mode = ['bridge', 'terminal'].includes(argv[0]) ? argv[0] : 'app'
47
+ const command = mode === 'app' ? argv[0] || 'help' : argv[1] || 'help'
48
+ const args = mode === 'app' ? argv.slice(1) : argv.slice(2)
49
+
23
50
  const parsed = {
24
- command: argv[0] || 'help',
25
- port: 4000,
26
- host: '127.0.0.1',
51
+ mode,
52
+ command,
53
+ host: DEFAULT_HOST,
54
+ port: DEFAULT_WEB_PORT,
55
+ terminalPort: DEFAULT_TERMINAL_PORT,
56
+ bridgePort: DEFAULT_BRIDGE_PORT,
27
57
  rebuild: false,
28
58
  }
29
59
 
30
- for (let i = 1; i < argv.length; i += 1) {
31
- const arg = argv[i]
32
- if (arg === '--port' && argv[i + 1]) {
33
- parsed.port = Number(argv[i + 1])
60
+ for (let i = 0; i < args.length; i += 1) {
61
+ const arg = args[i]
62
+
63
+ if (arg === '--host' && args[i + 1]) {
64
+ parsed.host = args[i + 1]
65
+ i += 1
66
+ continue
67
+ }
68
+ if (arg.startsWith('--host=')) {
69
+ parsed.host = arg.slice('--host='.length)
70
+ continue
71
+ }
72
+
73
+ if (arg === '--port' && args[i + 1]) {
74
+ parsed.port = Number(args[i + 1])
34
75
  i += 1
35
76
  continue
36
77
  }
@@ -38,15 +79,27 @@ function parseArgs(argv) {
38
79
  parsed.port = Number(arg.slice('--port='.length))
39
80
  continue
40
81
  }
41
- if (arg === '--host' && argv[i + 1]) {
42
- parsed.host = argv[i + 1]
82
+
83
+ if (arg === '--terminal-port' && args[i + 1]) {
84
+ parsed.terminalPort = Number(args[i + 1])
43
85
  i += 1
44
86
  continue
45
87
  }
46
- if (arg.startsWith('--host=')) {
47
- parsed.host = arg.slice('--host='.length)
88
+ if (arg.startsWith('--terminal-port=')) {
89
+ parsed.terminalPort = Number(arg.slice('--terminal-port='.length))
90
+ continue
91
+ }
92
+
93
+ if (arg === '--bridge-port' && args[i + 1]) {
94
+ parsed.bridgePort = Number(args[i + 1])
95
+ i += 1
96
+ continue
97
+ }
98
+ if (arg.startsWith('--bridge-port=')) {
99
+ parsed.bridgePort = Number(arg.slice('--bridge-port='.length))
48
100
  continue
49
101
  }
102
+
50
103
  if (arg === '--rebuild') {
51
104
  parsed.rebuild = true
52
105
  }
@@ -55,21 +108,21 @@ function parseArgs(argv) {
55
108
  return parsed
56
109
  }
57
110
 
58
- function readState() {
111
+ function readState(filePath) {
59
112
  try {
60
- return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))
113
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
61
114
  } catch {
62
115
  return null
63
116
  }
64
117
  }
65
118
 
66
- function writeState(state) {
67
- fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))
119
+ function writeState(filePath, state) {
120
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2))
68
121
  }
69
122
 
70
- function removeState() {
71
- if (fs.existsSync(STATE_FILE)) {
72
- fs.unlinkSync(STATE_FILE)
123
+ function removeState(filePath) {
124
+ if (fs.existsSync(filePath)) {
125
+ fs.unlinkSync(filePath)
73
126
  }
74
127
  }
75
128
 
@@ -83,9 +136,110 @@ function isProcessAlive(pid) {
83
136
  }
84
137
  }
85
138
 
139
+ function stopProcess(pid) {
140
+ if (!Number.isInteger(pid) || pid <= 0) return true
141
+
142
+ if (process.platform === 'win32') {
143
+ const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
144
+ stdio: 'ignore',
145
+ })
146
+ return result.status === 0
147
+ }
148
+
149
+ try {
150
+ process.kill(-pid, 'SIGTERM')
151
+ return true
152
+ } catch {
153
+ try {
154
+ process.kill(pid, 'SIGTERM')
155
+ return true
156
+ } catch {
157
+ return false
158
+ }
159
+ }
160
+ }
161
+
162
+ function validatePort(port, flagName) {
163
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
164
+ console.error(`Invalid ${flagName} value: ${port}`)
165
+ process.exit(1)
166
+ }
167
+ }
168
+
169
+ function spawnDetached(command, args, opts) {
170
+ const { env, logFile } = opts
171
+ const fd = fs.openSync(logFile, 'a')
172
+ fs.writeSync(fd, `\n[${now()}] spawn: ${command} ${args.join(' ')}\n`)
173
+
174
+ const child = spawn(command, args, {
175
+ cwd: APP_ROOT,
176
+ env,
177
+ detached: true,
178
+ stdio: ['ignore', fd, fd],
179
+ })
180
+
181
+ child.unref()
182
+ return child
183
+ }
184
+
185
+ function delay(ms) {
186
+ return new Promise((resolve) => setTimeout(resolve, ms))
187
+ }
188
+
189
+ function probeHttp(url, validator) {
190
+ return new Promise((resolve) => {
191
+ const parsed = new URL(url)
192
+ const client = parsed.protocol === 'https:' ? https : http
193
+ const req = client.request(
194
+ {
195
+ protocol: parsed.protocol,
196
+ hostname: parsed.hostname,
197
+ port: parsed.port,
198
+ path: `${parsed.pathname}${parsed.search}`,
199
+ method: 'GET',
200
+ timeout: 1200,
201
+ },
202
+ (res) => {
203
+ const chunks = []
204
+ res.on('data', (chunk) => chunks.push(chunk))
205
+ res.on('end', () => {
206
+ const body = Buffer.concat(chunks).toString('utf8')
207
+ if (typeof validator === 'function') {
208
+ resolve(Boolean(validator(res, body)))
209
+ return
210
+ }
211
+ resolve(res.statusCode >= 200 && res.statusCode < 400)
212
+ })
213
+ },
214
+ )
215
+
216
+ req.on('timeout', () => {
217
+ req.destroy()
218
+ resolve(false)
219
+ })
220
+
221
+ req.on('error', () => resolve(false))
222
+ req.end()
223
+ })
224
+ }
225
+
226
+ async function waitForHttp(url, timeoutMs = 30000, validator) {
227
+ const deadline = Date.now() + timeoutMs
228
+ while (Date.now() < deadline) {
229
+ // eslint-disable-next-line no-await-in-loop
230
+ const ok = await probeHttp(url, validator)
231
+ if (ok) return true
232
+ // eslint-disable-next-line no-await-in-loop
233
+ await delay(300)
234
+ }
235
+ return false
236
+ }
237
+
86
238
  function ensureNextInstalled() {
87
239
  if (!fs.existsSync(NEXT_BIN)) {
88
- console.error('pAInt runtime is missing Next.js binaries. Reinstall the package.')
240
+ console.error(
241
+ 'pAInt runtime is missing Next.js binaries. Reinstall the package.',
242
+ )
89
243
  process.exit(1)
90
244
  }
91
245
  }
@@ -99,7 +253,9 @@ function ensureBuilt(forceRebuild) {
99
253
  return
100
254
  }
101
255
 
102
- console.log(forceRebuild ? 'Rebuilding pAInt…' : 'Building pAInt for first run…')
256
+ console.log(
257
+ forceRebuild ? 'Rebuilding pAInt…' : 'Building pAInt for first run…',
258
+ )
103
259
  const result = spawnSync(process.execPath, [NEXT_BIN, 'build'], {
104
260
  cwd: APP_ROOT,
105
261
  env: process.env,
@@ -111,156 +267,440 @@ function ensureBuilt(forceRebuild) {
111
267
  }
112
268
  }
113
269
 
114
- function stopProcess(pid) {
115
- if (process.platform === 'win32') {
116
- const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
117
- stdio: 'ignore',
118
- })
119
- return result.status === 0
120
- }
270
+ async function startApp(options) {
271
+ validatePort(options.port, '--port')
121
272
 
122
- try {
123
- process.kill(-pid, 'SIGTERM')
124
- return true
125
- } catch {
126
- try {
127
- process.kill(pid, 'SIGTERM')
128
- return true
129
- } catch {
130
- return false
131
- }
132
- }
133
- }
134
-
135
- function startServer({ port, host, rebuild }) {
136
- if (!Number.isInteger(port) || port <= 0 || port > 65535) {
137
- console.error(`Invalid --port value: ${port}`)
138
- process.exit(1)
273
+ const existing = readState(APP_STATE_FILE)
274
+ if (existing && isProcessAlive(existing.webPid)) {
275
+ console.log(
276
+ `pAInt is already running (web pid ${existing.webPid}) at http://${existing.host}:${existing.port}`,
277
+ )
278
+ return
139
279
  }
140
280
 
141
- const existing = readState()
142
- if (existing && isProcessAlive(existing.pid)) {
143
- console.log(`pAInt is already running (pid ${existing.pid}) at http://${existing.host}:${existing.port}`)
144
- process.exit(0)
281
+ if (existing) {
282
+ stopProcess(existing.webPid)
283
+ removeState(APP_STATE_FILE)
145
284
  }
146
285
 
147
- ensureBuilt(rebuild)
286
+ ensureBuilt(options.rebuild)
148
287
  ensureStateDir()
149
288
 
150
- const logFd = fs.openSync(LOG_FILE, 'a')
151
- fs.writeSync(logFd, `\n[${now()}] starting pAInt server\n`)
152
-
153
- const child = spawn(
289
+ const webChild = spawnDetached(
154
290
  process.execPath,
155
- [NEXT_BIN, 'start', '--port', String(port), '--hostname', host],
291
+ [
292
+ NEXT_BIN,
293
+ 'start',
294
+ '--port',
295
+ String(options.port),
296
+ '--hostname',
297
+ options.host,
298
+ ],
156
299
  {
157
- cwd: APP_ROOT,
158
300
  env: process.env,
159
- detached: true,
160
- stdio: ['ignore', logFd, logFd],
301
+ logFile: WEB_LOG_FILE,
161
302
  },
162
303
  )
163
304
 
164
- child.unref()
305
+ const webReady = await waitForHttp(
306
+ `http://${options.host}:${options.port}/api/claude/status`,
307
+ 25000,
308
+ (res, body) => {
309
+ const ct = String(res.headers['content-type'] || '')
310
+ return (
311
+ res.statusCode >= 200 &&
312
+ res.statusCode < 400 &&
313
+ ct.includes('application/json') &&
314
+ body.includes('"available"')
315
+ )
316
+ },
317
+ )
318
+
319
+ if (!webReady) {
320
+ stopProcess(webChild.pid)
321
+ removeState(APP_STATE_FILE)
322
+ console.error('Failed to start pAInt web server cleanly.')
323
+ console.error(
324
+ `- web server did not become ready at http://${options.host}:${options.port}/`,
325
+ )
326
+ console.error(`Web logs: ${WEB_LOG_FILE}`)
327
+ process.exit(1)
328
+ }
165
329
 
166
- writeState({
167
- pid: child.pid,
168
- host,
169
- port,
330
+ writeState(APP_STATE_FILE, {
331
+ webPid: webChild.pid,
332
+ host: options.host,
333
+ port: options.port,
170
334
  startedAt: now(),
171
- logFile: LOG_FILE,
335
+ logs: {
336
+ web: WEB_LOG_FILE,
337
+ },
172
338
  })
173
339
 
174
- console.log(`pAInt started (pid ${child.pid}) at http://${host}:${port}`)
175
- console.log(`Logs: ${LOG_FILE}`)
340
+ console.log(`pAInt started at http://${options.host}:${options.port}`)
341
+ console.log(`Web pid: ${webChild.pid}`)
342
+ console.log(`Web logs: ${WEB_LOG_FILE}`)
176
343
  }
177
344
 
178
- function stopServer() {
179
- const existing = readState()
345
+ function stopApp() {
346
+ const existing = readState(APP_STATE_FILE)
180
347
  if (!existing) {
181
348
  console.log('pAInt is not running.')
182
349
  return
183
350
  }
184
351
 
185
- if (!isProcessAlive(existing.pid)) {
186
- removeState()
352
+ const alive = isProcessAlive(existing.webPid)
353
+ const ok = alive ? stopProcess(existing.webPid) : true
354
+ removeState(APP_STATE_FILE)
355
+
356
+ if (!alive) {
187
357
  console.log('pAInt was not running. Cleared stale state.')
188
358
  return
189
359
  }
190
360
 
191
- const ok = stopProcess(existing.pid)
192
- removeState()
193
-
194
361
  if (!ok) {
195
- console.error(`Failed to stop process ${existing.pid}`)
362
+ console.error(`Failed to stop web process ${existing.webPid}`)
196
363
  process.exit(1)
197
364
  }
198
365
 
199
- console.log(`Stopped pAInt (pid ${existing.pid}).`)
366
+ console.log(`Stopped pAInt (web ${existing.webPid}).`)
200
367
  }
201
368
 
202
- function serverStatus() {
203
- const existing = readState()
204
- if (!existing || !isProcessAlive(existing.pid)) {
369
+ function appStatus() {
370
+ const existing = readState(APP_STATE_FILE)
371
+ if (!existing || !isProcessAlive(existing.webPid)) {
205
372
  console.log('pAInt is not running.')
206
373
  return
207
374
  }
208
375
 
209
- console.log(`pAInt is running (pid ${existing.pid})`)
376
+ console.log('pAInt is running')
377
+ console.log(`Web: up (pid ${existing.webPid})`)
210
378
  console.log(`URL: http://${existing.host}:${existing.port}`)
211
379
  console.log(`Started: ${existing.startedAt}`)
212
- console.log(`Logs: ${existing.logFile}`)
380
+ if (existing.logs?.web) console.log(`Web logs: ${existing.logs.web}`)
381
+ }
382
+
383
+ function appLogs() {
384
+ if (!fs.existsSync(WEB_LOG_FILE)) {
385
+ console.log('No web logs found yet.')
386
+ return
387
+ }
388
+ process.stdout.write('===== web.log =====\n')
389
+ process.stdout.write(fs.readFileSync(WEB_LOG_FILE, 'utf8'))
390
+ }
391
+
392
+ async function startTerminal(options) {
393
+ validatePort(options.terminalPort, '--terminal-port')
394
+
395
+ const existing = readState(TERMINAL_STATE_FILE)
396
+ if (existing && isProcessAlive(existing.terminalPid)) {
397
+ console.log(
398
+ `Terminal is already running (pid ${existing.terminalPid}) at ws://localhost:${existing.terminalPort}/ws`,
399
+ )
400
+ return
401
+ }
402
+
403
+ if (existing) {
404
+ stopProcess(existing.terminalPid)
405
+ removeState(TERMINAL_STATE_FILE)
406
+ }
407
+
408
+ ensureStateDir()
409
+
410
+ const terminalChild = spawnDetached(process.execPath, [TERMINAL_SERVER_BIN], {
411
+ env: {
412
+ ...process.env,
413
+ TERMINAL_PORT: String(options.terminalPort),
414
+ },
415
+ logFile: TERMINAL_LOG_FILE,
416
+ })
417
+
418
+ const terminalReady = await waitForHttp(
419
+ `http://127.0.0.1:${options.terminalPort}/health`,
420
+ 25000,
421
+ (res, body) =>
422
+ res.statusCode >= 200 && res.statusCode < 400 && body.trim() === 'ok',
423
+ )
424
+
425
+ if (!terminalReady) {
426
+ stopProcess(terminalChild.pid)
427
+ removeState(TERMINAL_STATE_FILE)
428
+ console.error(
429
+ `Terminal failed to become ready at http://127.0.0.1:${options.terminalPort}/health`,
430
+ )
431
+ console.error(`Terminal logs: ${TERMINAL_LOG_FILE}`)
432
+ process.exit(1)
433
+ }
434
+
435
+ writeState(TERMINAL_STATE_FILE, {
436
+ terminalPid: terminalChild.pid,
437
+ terminalPort: options.terminalPort,
438
+ startedAt: now(),
439
+ logs: {
440
+ terminal: TERMINAL_LOG_FILE,
441
+ },
442
+ })
443
+
444
+ console.log(`Terminal started at ws://localhost:${options.terminalPort}/ws`)
445
+ console.log(`Terminal pid: ${terminalChild.pid}`)
446
+ console.log(`Terminal logs: ${TERMINAL_LOG_FILE}`)
447
+ }
448
+
449
+ function stopTerminal() {
450
+ const existing = readState(TERMINAL_STATE_FILE)
451
+ if (!existing) {
452
+ console.log('Terminal is not running.')
453
+ return
454
+ }
455
+
456
+ const alive = isProcessAlive(existing.terminalPid)
457
+ const ok = alive ? stopProcess(existing.terminalPid) : true
458
+ removeState(TERMINAL_STATE_FILE)
459
+
460
+ if (!alive) {
461
+ console.log('Terminal was not running. Cleared stale state.')
462
+ return
463
+ }
464
+
465
+ if (!ok) {
466
+ console.error(`Failed to stop terminal process ${existing.terminalPid}`)
467
+ process.exit(1)
468
+ }
469
+
470
+ console.log(`Stopped terminal (pid ${existing.terminalPid}).`)
471
+ }
472
+
473
+ function terminalStatus() {
474
+ const existing = readState(TERMINAL_STATE_FILE)
475
+ if (!existing || !isProcessAlive(existing.terminalPid)) {
476
+ console.log('Terminal is not running.')
477
+ return
478
+ }
479
+
480
+ console.log('Terminal is running')
481
+ console.log(`PID: ${existing.terminalPid}`)
482
+ console.log(`WS: ws://localhost:${existing.terminalPort}/ws`)
483
+ console.log(`Started: ${existing.startedAt}`)
484
+ if (existing.logs?.terminal)
485
+ console.log(`Terminal logs: ${existing.logs.terminal}`)
486
+ }
487
+
488
+ function terminalLogs() {
489
+ if (!fs.existsSync(TERMINAL_LOG_FILE)) {
490
+ console.log('No terminal logs found yet.')
491
+ return
492
+ }
493
+ process.stdout.write('===== terminal.log =====\n')
494
+ process.stdout.write(fs.readFileSync(TERMINAL_LOG_FILE, 'utf8'))
495
+ }
496
+
497
+ async function startBridge(options) {
498
+ validatePort(options.bridgePort, '--bridge-port')
499
+
500
+ const existing = readState(BRIDGE_STATE_FILE)
501
+ if (existing && isProcessAlive(existing.bridgePid)) {
502
+ console.log(
503
+ `Bridge is already running (pid ${existing.bridgePid}) at http://127.0.0.1:${existing.bridgePort}`,
504
+ )
505
+ return
506
+ }
507
+
508
+ if (existing) {
509
+ stopProcess(existing.bridgePid)
510
+ removeState(BRIDGE_STATE_FILE)
511
+ }
512
+
513
+ ensureStateDir()
514
+
515
+ const child = spawnDetached(process.execPath, [BRIDGE_SERVER_BIN], {
516
+ env: {
517
+ ...process.env,
518
+ BRIDGE_PORT: String(options.bridgePort),
519
+ },
520
+ logFile: BRIDGE_LOG_FILE,
521
+ })
522
+
523
+ const ready = await waitForHttp(
524
+ `http://127.0.0.1:${options.bridgePort}/health`,
525
+ 25000,
526
+ )
527
+
528
+ if (!ready) {
529
+ stopProcess(child.pid)
530
+ removeState(BRIDGE_STATE_FILE)
531
+ console.error(
532
+ `Bridge failed to become ready at http://127.0.0.1:${options.bridgePort}/health`,
533
+ )
534
+ console.error(`Bridge logs: ${BRIDGE_LOG_FILE}`)
535
+ process.exit(1)
536
+ }
537
+
538
+ writeState(BRIDGE_STATE_FILE, {
539
+ bridgePid: child.pid,
540
+ bridgePort: options.bridgePort,
541
+ startedAt: now(),
542
+ logs: {
543
+ bridge: BRIDGE_LOG_FILE,
544
+ },
545
+ })
546
+
547
+ console.log(`Bridge started at http://127.0.0.1:${options.bridgePort}`)
548
+ console.log(`Bridge pid: ${child.pid}`)
549
+ console.log(`Bridge logs: ${BRIDGE_LOG_FILE}`)
550
+ }
551
+
552
+ function stopBridge() {
553
+ const existing = readState(BRIDGE_STATE_FILE)
554
+ if (!existing) {
555
+ console.log('Bridge is not running.')
556
+ return
557
+ }
558
+
559
+ const alive = isProcessAlive(existing.bridgePid)
560
+ const ok = alive ? stopProcess(existing.bridgePid) : true
561
+ removeState(BRIDGE_STATE_FILE)
562
+
563
+ if (!alive) {
564
+ console.log('Bridge was not running. Cleared stale state.')
565
+ return
566
+ }
567
+
568
+ if (!ok) {
569
+ console.error(`Failed to stop bridge process ${existing.bridgePid}`)
570
+ process.exit(1)
571
+ }
572
+
573
+ console.log(`Stopped bridge (pid ${existing.bridgePid}).`)
574
+ }
575
+
576
+ function bridgeStatus() {
577
+ const existing = readState(BRIDGE_STATE_FILE)
578
+ if (!existing || !isProcessAlive(existing.bridgePid)) {
579
+ console.log('Bridge is not running.')
580
+ return
581
+ }
582
+
583
+ console.log('Bridge is running')
584
+ console.log(`PID: ${existing.bridgePid}`)
585
+ console.log(`URL: http://127.0.0.1:${existing.bridgePort}`)
586
+ console.log(`Started: ${existing.startedAt}`)
587
+ if (existing.logs?.bridge) console.log(`Bridge logs: ${existing.logs.bridge}`)
588
+ }
589
+
590
+ function bridgeLogs() {
591
+ if (!fs.existsSync(BRIDGE_LOG_FILE)) {
592
+ console.log('No bridge logs found yet.')
593
+ return
594
+ }
595
+ process.stdout.write('===== bridge.log =====\n')
596
+ process.stdout.write(fs.readFileSync(BRIDGE_LOG_FILE, 'utf8'))
213
597
  }
214
598
 
215
599
  function showHelp() {
216
600
  console.log(`paint - pAInt server manager
217
601
 
218
- Usage:
602
+ App usage:
219
603
  paint start [--port 4000] [--host 127.0.0.1] [--rebuild]
220
604
  paint stop
221
605
  paint restart [--port 4000] [--host 127.0.0.1] [--rebuild]
222
606
  paint status
223
607
  paint logs
608
+
609
+ Terminal usage:
610
+ paint terminal start [--terminal-port 4001]
611
+ paint terminal stop
612
+ paint terminal restart [--terminal-port 4001]
613
+ paint terminal status
614
+ paint terminal logs
615
+
616
+ Bridge usage:
617
+ paint bridge start [--bridge-port 4002]
618
+ paint bridge stop
619
+ paint bridge restart [--bridge-port 4002]
620
+ paint bridge status
621
+ paint bridge logs
622
+
623
+ General:
224
624
  paint help
225
625
  `)
226
626
  }
227
627
 
228
- function showLogs() {
229
- if (!fs.existsSync(LOG_FILE)) {
230
- console.log('No logs found yet.')
231
- return
628
+ async function main() {
629
+ const options = parseArgs(process.argv.slice(2))
630
+
631
+ if (options.mode === 'bridge') {
632
+ switch (options.command) {
633
+ case 'start':
634
+ await startBridge(options)
635
+ return
636
+ case 'stop':
637
+ stopBridge()
638
+ return
639
+ case 'restart':
640
+ stopBridge()
641
+ await startBridge(options)
642
+ return
643
+ case 'status':
644
+ bridgeStatus()
645
+ return
646
+ case 'logs':
647
+ bridgeLogs()
648
+ return
649
+ default:
650
+ showHelp()
651
+ return
652
+ }
232
653
  }
233
- const content = fs.readFileSync(LOG_FILE, 'utf8')
234
- process.stdout.write(content)
235
- }
236
654
 
237
- function main() {
238
- const options = parseArgs(process.argv.slice(2))
655
+ if (options.mode === 'terminal') {
656
+ switch (options.command) {
657
+ case 'start':
658
+ await startTerminal(options)
659
+ return
660
+ case 'stop':
661
+ stopTerminal()
662
+ return
663
+ case 'restart':
664
+ stopTerminal()
665
+ await startTerminal(options)
666
+ return
667
+ case 'status':
668
+ terminalStatus()
669
+ return
670
+ case 'logs':
671
+ terminalLogs()
672
+ return
673
+ default:
674
+ showHelp()
675
+ return
676
+ }
677
+ }
239
678
 
240
679
  switch (options.command) {
241
680
  case 'start':
242
- startServer(options)
681
+ await startApp(options)
243
682
  break
244
683
  case 'stop':
245
- stopServer()
684
+ stopApp()
246
685
  break
247
686
  case 'restart':
248
- stopServer()
249
- startServer(options)
687
+ stopApp()
688
+ await startApp(options)
250
689
  break
251
690
  case 'status':
252
- serverStatus()
691
+ appStatus()
253
692
  break
254
693
  case 'logs':
255
- showLogs()
694
+ appLogs()
256
695
  break
257
- case 'help':
258
- case '--help':
259
- case '-h':
260
696
  default:
261
697
  showHelp()
262
698
  break
263
699
  }
264
700
  }
265
701
 
266
- main()
702
+ main().catch((err) => {
703
+ const msg = err instanceof Error ? err.message : String(err)
704
+ console.error(`paint failed: ${msg}`)
705
+ process.exit(1)
706
+ })