@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.
- package/README.md +32 -17
- package/bin/bridge-server.js +36 -0
- package/bin/paint.js +542 -102
- package/bin/terminal-server.js +105 -0
- package/package.json +7 -8
- package/public/dev-editor-inspector.js +92 -104
- package/src/app/api/claude/apply/route.ts +2 -2
- package/src/app/api/project/scan/route.ts +1 -1
- package/src/app/api/proxy/[[...path]]/route.ts +4 -4
- package/src/app/docs/DocsClient.tsx +1 -1
- package/src/app/docs/page.tsx +0 -1
- package/src/app/page.tsx +1 -1
- package/src/bridge/api-handlers.ts +1 -1
- package/src/bridge/proxy-handler.ts +4 -4
- package/src/bridge/server.ts +135 -39
- package/src/components/ConnectModal.tsx +1 -2
- package/src/components/PreviewFrame.tsx +2 -2
- package/src/components/ResponsiveToolbar.tsx +1 -2
- package/src/components/common/ColorPicker.tsx +7 -9
- package/src/components/common/UnitInput.tsx +1 -1
- package/src/components/common/VariableColorPicker.tsx +0 -1
- package/src/components/left-panel/ComponentsPanel.tsx +3 -3
- package/src/components/left-panel/LayerNode.tsx +1 -1
- package/src/components/left-panel/icons.tsx +1 -1
- package/src/components/left-panel/terminal/TerminalPanel.tsx +2 -2
- package/src/components/right-panel/ElementLogBox.tsx +1 -3
- package/src/components/right-panel/changes/ChangesPanel.tsx +12 -12
- package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +2 -2
- package/src/components/right-panel/claude/DiffViewer.tsx +1 -1
- package/src/components/right-panel/claude/ProjectRootSelector.tsx +7 -7
- package/src/components/right-panel/claude/SetupFlow.tsx +1 -1
- package/src/components/right-panel/console/ConsolePanel.tsx +4 -4
- package/src/components/right-panel/design/BackgroundSection.tsx +2 -2
- package/src/components/right-panel/design/GradientEditor.tsx +6 -6
- package/src/components/right-panel/design/LayoutSection.tsx +4 -4
- package/src/components/right-panel/design/PositionSection.tsx +2 -2
- package/src/components/right-panel/design/SVGSection.tsx +2 -3
- package/src/components/right-panel/design/ShadowBlurSection.tsx +5 -5
- package/src/components/right-panel/design/TextSection.tsx +5 -5
- package/src/components/right-panel/design/icons.tsx +1 -1
- package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +2 -2
- package/src/components/right-panel/design/inputs/CompactInput.tsx +2 -2
- package/src/components/right-panel/design/inputs/DraggableLabel.tsx +2 -1
- package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +1 -1
- package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +3 -3
- package/src/components/right-panel/design/inputs/SectionHeader.tsx +2 -1
- package/src/hooks/useDOMTree.ts +0 -1
- package/src/hooks/usePostMessage.ts +4 -3
- package/src/hooks/useTargetUrl.ts +1 -1
- package/src/inspector/DOMTraverser.ts +2 -2
- package/src/inspector/HoverHighlighter.ts +6 -6
- package/src/inspector/SelectionHighlighter.ts +4 -4
- package/src/lib/classifyElement.ts +1 -2
- package/src/lib/claude-bin.ts +1 -1
- package/src/lib/clientProjectScanner.ts +13 -13
- package/src/lib/cssVariableUtils.ts +1 -1
- package/src/lib/folderPicker.ts +4 -1
- package/src/lib/projectScanner.ts +15 -15
- package/src/lib/tailwindClassParser.ts +1 -1
- package/src/lib/textShadowUtils.ts +1 -1
- package/src/lib/utils.ts +4 -4
- package/src/proxy.ts +1 -1
- package/src/store/treeSlice.ts +2 -2
- package/src/server/terminal-server.ts +0 -104
- 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
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
host:
|
|
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 =
|
|
31
|
-
const arg =
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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('--
|
|
47
|
-
parsed.
|
|
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(
|
|
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(
|
|
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(
|
|
72
|
-
fs.unlinkSync(
|
|
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(
|
|
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(
|
|
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
|
|
115
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
151
|
-
fs.writeSync(logFd, `\n[${now()}] starting pAInt server\n`)
|
|
152
|
-
|
|
153
|
-
const child = spawn(
|
|
289
|
+
const webChild = spawnDetached(
|
|
154
290
|
process.execPath,
|
|
155
|
-
[
|
|
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
|
-
|
|
160
|
-
stdio: ['ignore', logFd, logFd],
|
|
301
|
+
logFile: WEB_LOG_FILE,
|
|
161
302
|
},
|
|
162
303
|
)
|
|
163
304
|
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
+
logs: {
|
|
336
|
+
web: WEB_LOG_FILE,
|
|
337
|
+
},
|
|
172
338
|
})
|
|
173
339
|
|
|
174
|
-
console.log(`pAInt started
|
|
175
|
-
console.log(`
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
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.
|
|
362
|
+
console.error(`Failed to stop web process ${existing.webPid}`)
|
|
196
363
|
process.exit(1)
|
|
197
364
|
}
|
|
198
365
|
|
|
199
|
-
console.log(`Stopped pAInt (
|
|
366
|
+
console.log(`Stopped pAInt (web ${existing.webPid}).`)
|
|
200
367
|
}
|
|
201
368
|
|
|
202
|
-
function
|
|
203
|
-
const existing = readState()
|
|
204
|
-
if (!existing || !isProcessAlive(existing.
|
|
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(
|
|
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(`
|
|
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
|
-
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
681
|
+
await startApp(options)
|
|
243
682
|
break
|
|
244
683
|
case 'stop':
|
|
245
|
-
|
|
684
|
+
stopApp()
|
|
246
685
|
break
|
|
247
686
|
case 'restart':
|
|
248
|
-
|
|
249
|
-
|
|
687
|
+
stopApp()
|
|
688
|
+
await startApp(options)
|
|
250
689
|
break
|
|
251
690
|
case 'status':
|
|
252
|
-
|
|
691
|
+
appStatus()
|
|
253
692
|
break
|
|
254
693
|
case 'logs':
|
|
255
|
-
|
|
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
|
+
})
|