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