@shawnstack/quickforge 1.5.3 → 1.5.5
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 +108 -12
- package/dist/assets/{AgentProfilesPage-BToo_R3Y.js → AgentProfilesPage-nVhgwanY.js} +1 -1
- package/dist/assets/ChatPanelHost-u0K5IWMF.js +242 -0
- package/dist/assets/{PluginsPage-DwzV2vQ4.js → PluginsPage-BVRTC0rz.js} +1 -1
- package/dist/assets/ScheduledTasksPage-D37TE2cM.js +2 -0
- package/dist/assets/{SharedConversationPage-CHE9qABz.js → SharedConversationPage-D5hnzsZC.js} +1 -1
- package/dist/assets/TerminalDock-NvH9esAS.js +2 -0
- package/dist/assets/WorkspaceInspector-DbnO1fei.js +3 -0
- package/dist/assets/{WorkspaceReaderDialog-Bai7v3V0.js → WorkspaceReaderDialog-BcxIbNBq.js} +1 -1
- package/dist/assets/diff-line-counts-N83e7__F.js +10 -0
- package/dist/assets/{icons-DzxBk7tb.js → icons-Uo4Gd-eK.js} +1 -1
- package/dist/assets/index-DiaCCmXE.js +1482 -0
- package/dist/assets/index-KdiXReMI.css +3 -0
- package/dist/assets/{monaco-dMY7_GLO.js → monaco-DtXl4zfe.js} +1 -1
- package/dist/assets/{react-vendor-DsAeMFcm.js → react-vendor-BjDQPVuK.js} +1 -1
- package/dist/assets/useAppTheme-Bm6HIzLF.js +1 -0
- package/dist/favicon.svg +16 -16
- package/dist/index.html +4 -4
- package/dist/pwa-icon-192.png +0 -0
- package/dist/pwa-icon-512.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/sw.js +1 -1
- package/package.json +2 -1
- package/server/agent-manager.mjs +8 -1
- package/server/index.mjs +76 -6
- package/server/public-api.mjs +196 -0
- package/server/routes/system.mjs +2 -1
- package/server/update-supervisor.mjs +121 -0
- package/dist/assets/ChatPanelHost-BTqhhkWK.js +0 -242
- package/dist/assets/ScheduledTasksPage-Cbm6LVk3.js +0 -2
- package/dist/assets/TerminalDock-Loi8A4pJ.js +0 -2
- package/dist/assets/WorkspaceInspector-Nf5xELW7.js +0 -3
- package/dist/assets/diff-line-counts-CCPYa_e0.js +0 -10
- package/dist/assets/index-Bt_dRvdG.js +0 -1476
- package/dist/assets/index-BzaZg9Br.css +0 -3
package/dist/sw.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shawnstack/quickforge",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "AI chat application with Agent access modes for local workspace tools. React + Vite + Tailwind CSS frontend, local Node.js storage server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/shawnstack/quickforge#readme",
|
|
30
30
|
"type": "module",
|
|
31
|
+
"main": "./server/public-api.mjs",
|
|
31
32
|
"bin": {
|
|
32
33
|
"quickforge": "bin/quickforge.mjs",
|
|
33
34
|
"qf": "bin/quickforge.mjs"
|
package/server/agent-manager.mjs
CHANGED
|
@@ -1551,13 +1551,20 @@ async function persistSession(session) {
|
|
|
1551
1551
|
// Write to storage atomically (read-modify-write within queue)
|
|
1552
1552
|
try {
|
|
1553
1553
|
await writeSessionValue(sessionId, sessionData)
|
|
1554
|
+
let archivedAt
|
|
1554
1555
|
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
1556
|
+
const existingMetadata = data[sessionId]
|
|
1557
|
+
archivedAt = existingMetadata?.archivedAt
|
|
1555
1558
|
data[sessionId] = {
|
|
1556
1559
|
...metadata,
|
|
1557
|
-
pinnedAt:
|
|
1560
|
+
pinnedAt: existingMetadata?.pinnedAt,
|
|
1561
|
+
...(archivedAt ? { archivedAt } : {}),
|
|
1558
1562
|
}
|
|
1559
1563
|
return data
|
|
1560
1564
|
})
|
|
1565
|
+
if (archivedAt) {
|
|
1566
|
+
await writeSessionValue(sessionId, { ...sessionData, archivedAt })
|
|
1567
|
+
}
|
|
1561
1568
|
} catch (err) {
|
|
1562
1569
|
logger.error(`Failed to persist session ${sessionId}:`, err, { sessionId })
|
|
1563
1570
|
}
|
package/server/index.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { handleChannelsApi } from './routes/channels.mjs'
|
|
|
31
31
|
import { handleModelsApi } from './routes/models.mjs'
|
|
32
32
|
import { serveStatic } from './routes/static.mjs'
|
|
33
33
|
import { logger, flushLogger } from './utils/logger.mjs'
|
|
34
|
-
import { getPackageInfo, checkForUpdates
|
|
34
|
+
import { getPackageInfo, checkForUpdates } from './utils/package-update.mjs'
|
|
35
35
|
import { installAiHttpLogger } from './ai-http-logger.mjs'
|
|
36
36
|
import { isLoopbackAddress, getLanUrls } from './utils/network.mjs'
|
|
37
37
|
import { parseCookies } from './share-store.mjs'
|
|
@@ -46,6 +46,7 @@ const __dirname = path.dirname(__filename)
|
|
|
46
46
|
const projectRoot = path.resolve(__dirname, '..')
|
|
47
47
|
const serverScript = path.join(__dirname, 'index.mjs')
|
|
48
48
|
const restartSupervisorScript = path.join(__dirname, 'restart-supervisor.mjs')
|
|
49
|
+
const updateSupervisorScript = path.join(__dirname, 'update-supervisor.mjs')
|
|
49
50
|
const bootId = randomUUID()
|
|
50
51
|
const startedAt = new Date().toISOString()
|
|
51
52
|
|
|
@@ -175,6 +176,56 @@ async function requestRestart() {
|
|
|
175
176
|
return { ok: true, restarting: true, bootId }
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
function updateLogFile() {
|
|
180
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').replace('Z', '')
|
|
181
|
+
return path.join(logsDir, `update-${stamp}.log`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function spawnUpdateSupervisor(update) {
|
|
185
|
+
const logFile = updateLogFile()
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const child = spawn(process.execPath, [
|
|
188
|
+
updateSupervisorScript,
|
|
189
|
+
String(process.pid),
|
|
190
|
+
update.name,
|
|
191
|
+
update.latestVersion,
|
|
192
|
+
serverScript,
|
|
193
|
+
projectRoot,
|
|
194
|
+
logFile,
|
|
195
|
+
...process.argv.slice(2),
|
|
196
|
+
], {
|
|
197
|
+
cwd: dataDir,
|
|
198
|
+
detached: true,
|
|
199
|
+
stdio: 'ignore',
|
|
200
|
+
windowsHide: true,
|
|
201
|
+
shell: false,
|
|
202
|
+
env: {
|
|
203
|
+
...process.env,
|
|
204
|
+
QUICKFORGE_NO_OPEN: '1',
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
child.once('error', reject)
|
|
209
|
+
child.once('spawn', () => {
|
|
210
|
+
child.unref()
|
|
211
|
+
resolve({ pid: child.pid, logFile })
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function shutdownForUpdate() {
|
|
217
|
+
logger.info('Shutting down QuickForge for external updater.')
|
|
218
|
+
stopScheduledTaskRunner()
|
|
219
|
+
stopVite()
|
|
220
|
+
await shutdownAgentManager()
|
|
221
|
+
await shutdownMcpConnections()
|
|
222
|
+
await shutdownChannels()
|
|
223
|
+
shutdownTerminalSessions()
|
|
224
|
+
await closeHttpServer()
|
|
225
|
+
flushLogger()
|
|
226
|
+
process.exit(0)
|
|
227
|
+
}
|
|
228
|
+
|
|
178
229
|
async function updateQuickForge() {
|
|
179
230
|
if (updateInProgress) {
|
|
180
231
|
const error = new Error('Update already in progress')
|
|
@@ -186,15 +237,34 @@ async function updateQuickForge() {
|
|
|
186
237
|
try {
|
|
187
238
|
const update = await checkForUpdates(projectRoot)
|
|
188
239
|
if (!update.updateAvailable) {
|
|
240
|
+
updateInProgress = false
|
|
189
241
|
return { ...update, ok: true, updated: false }
|
|
190
242
|
}
|
|
191
243
|
|
|
192
|
-
logger.info(`
|
|
193
|
-
await
|
|
194
|
-
logger.info(
|
|
195
|
-
|
|
196
|
-
|
|
244
|
+
logger.info(`Starting external QuickForge updater from ${update.currentVersion} to ${update.latestVersion}.`)
|
|
245
|
+
const supervisor = await spawnUpdateSupervisor(update)
|
|
246
|
+
logger.info(`Update supervisor started (PID ${supervisor.pid}). Log: ${supervisor.logFile}`)
|
|
247
|
+
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
void shutdownForUpdate().catch((error) => {
|
|
250
|
+
logger.error('Failed to shut down for QuickForge update:', error)
|
|
251
|
+
updateInProgress = false
|
|
252
|
+
})
|
|
253
|
+
}, 100)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
...update,
|
|
257
|
+
ok: true,
|
|
258
|
+
updated: false,
|
|
259
|
+
updateStarted: true,
|
|
260
|
+
restarting: true,
|
|
261
|
+
updaterPid: supervisor.pid,
|
|
262
|
+
logFile: supervisor.logFile,
|
|
263
|
+
bootId,
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
197
266
|
updateInProgress = false
|
|
267
|
+
throw error
|
|
198
268
|
}
|
|
199
269
|
}
|
|
200
270
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
+
const __dirname = path.dirname(__filename)
|
|
7
|
+
const projectRoot = path.resolve(__dirname, '..')
|
|
8
|
+
const serverScript = path.join(__dirname, 'index.mjs')
|
|
9
|
+
const serverScriptUrl = pathToFileURL(serverScript).href
|
|
10
|
+
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeHost(host) {
|
|
16
|
+
return host || '127.0.0.1'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getProbeHost(host) {
|
|
20
|
+
if (host === '0.0.0.0' || host === '::') return '127.0.0.1'
|
|
21
|
+
return host || '127.0.0.1'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getDisplayHost(host) {
|
|
25
|
+
if (host === '0.0.0.0') return '<LAN-IP>'
|
|
26
|
+
if (host === '::') return '<LAN-IP>'
|
|
27
|
+
return host || '127.0.0.1'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getPort(port) {
|
|
31
|
+
return Number(port || 5176)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildEnv(options = {}) {
|
|
35
|
+
const host = normalizeHost(options.host)
|
|
36
|
+
const port = getPort(options.port)
|
|
37
|
+
const shareLan = options.shareLan === true
|
|
38
|
+
const env = {
|
|
39
|
+
...process.env,
|
|
40
|
+
QUICKFORGE_HOST: host,
|
|
41
|
+
QUICKFORGE_PORT: String(port),
|
|
42
|
+
QUICKFORGE_SHARE_LAN: shareLan ? '1' : '0',
|
|
43
|
+
QUICKFORGE_NO_OPEN: options.openBrowser ? '0' : '1',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.dataDir) env.QUICKFORGE_DATA_DIR = path.resolve(options.dataDir)
|
|
47
|
+
if (options.workspaceDir) env.QUICKFORGE_WORKSPACE_DIR = path.resolve(options.workspaceDir)
|
|
48
|
+
if (options.vitePort) env.QUICKFORGE_VITE_PORT = String(options.vitePort)
|
|
49
|
+
if (options.terminal === false) env.QUICKFORGE_TERMINAL = '0'
|
|
50
|
+
if (process.versions.electron) env.ELECTRON_RUN_AS_NODE = '1'
|
|
51
|
+
if (options.allowRemote || shareLan) env.QUICKFORGE_ALLOW_REMOTE = '1'
|
|
52
|
+
|
|
53
|
+
return env
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getQuickForgeUrl(options = {}) {
|
|
57
|
+
const host = getDisplayHost(normalizeHost(options.host))
|
|
58
|
+
const port = getPort(options.port)
|
|
59
|
+
return `http://${host}:${port}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getQuickForgeHealthUrl(options = {}) {
|
|
63
|
+
const host = getProbeHost(normalizeHost(options.host))
|
|
64
|
+
const port = getPort(options.port)
|
|
65
|
+
return `http://${host}:${port}/api/health`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function checkQuickForgeHealth(options = {}) {
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
const timeout = setTimeout(() => controller.abort(), options.requestTimeoutMs || 800)
|
|
71
|
+
timeout.unref?.()
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch(getQuickForgeHealthUrl(options), {
|
|
75
|
+
headers: { accept: 'application/json' },
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
})
|
|
78
|
+
if (!response.ok) return null
|
|
79
|
+
const payload = await response.json()
|
|
80
|
+
if (!payload || payload.ok !== true || !payload.pid) return null
|
|
81
|
+
return payload
|
|
82
|
+
} catch {
|
|
83
|
+
return null
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timeout)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function waitForQuickForge(options = {}) {
|
|
90
|
+
const timeoutMs = options.timeoutMs || 15000
|
|
91
|
+
const deadline = Date.now() + timeoutMs
|
|
92
|
+
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
const health = await checkQuickForgeHealth(options)
|
|
95
|
+
if (health) return health
|
|
96
|
+
await sleep(options.pollIntervalMs || 300)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function startQuickForge(options = {}) {
|
|
103
|
+
const existingHealth = options.reuseExisting === false ? null : await checkQuickForgeHealth(options)
|
|
104
|
+
const url = getQuickForgeUrl(options)
|
|
105
|
+
const healthUrl = getQuickForgeHealthUrl(options)
|
|
106
|
+
|
|
107
|
+
if (existingHealth) {
|
|
108
|
+
return {
|
|
109
|
+
url,
|
|
110
|
+
healthUrl,
|
|
111
|
+
health: existingHealth,
|
|
112
|
+
pid: existingHealth.pid,
|
|
113
|
+
child: null,
|
|
114
|
+
reused: true,
|
|
115
|
+
async stop() {
|
|
116
|
+
return false
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (options.inline === true) {
|
|
122
|
+
await import(serverScriptUrl)
|
|
123
|
+
const health = await waitForQuickForge(options)
|
|
124
|
+
if (!health) throw new Error('QuickForge failed to start: health check timed out')
|
|
125
|
+
return {
|
|
126
|
+
url,
|
|
127
|
+
healthUrl,
|
|
128
|
+
health,
|
|
129
|
+
pid: health.pid || process.pid,
|
|
130
|
+
child: null,
|
|
131
|
+
reused: false,
|
|
132
|
+
inline: true,
|
|
133
|
+
async stop() {
|
|
134
|
+
return false
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const child = spawn(process.execPath, [serverScript], {
|
|
140
|
+
cwd: options.cwd ? path.resolve(options.cwd) : projectRoot,
|
|
141
|
+
detached: options.detached === true,
|
|
142
|
+
stdio: options.stdio || 'ignore',
|
|
143
|
+
windowsHide: true,
|
|
144
|
+
shell: false,
|
|
145
|
+
env: buildEnv(options),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
let exitInfo = null
|
|
149
|
+
child.once('exit', (code, signal) => {
|
|
150
|
+
exitInfo = { code, signal }
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await new Promise((resolve, reject) => {
|
|
154
|
+
child.once('spawn', resolve)
|
|
155
|
+
child.once('error', reject)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const health = await waitForQuickForge(options)
|
|
159
|
+
if (!health) {
|
|
160
|
+
if (!child.killed && exitInfo === null) {
|
|
161
|
+
try {
|
|
162
|
+
child.kill('SIGTERM')
|
|
163
|
+
} catch {
|
|
164
|
+
// ignore best-effort cleanup errors
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const reason = exitInfo
|
|
169
|
+
? `process exited early (code ${exitInfo.code ?? 'null'}, signal ${exitInfo.signal ?? 'null'})`
|
|
170
|
+
: 'health check timed out'
|
|
171
|
+
throw new Error(`QuickForge failed to start: ${reason}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (options.detached === true) child.unref()
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
url,
|
|
178
|
+
healthUrl,
|
|
179
|
+
health,
|
|
180
|
+
pid: health.pid || child.pid,
|
|
181
|
+
child,
|
|
182
|
+
reused: false,
|
|
183
|
+
async stop() {
|
|
184
|
+
if (child.killed) return false
|
|
185
|
+
child.kill('SIGTERM')
|
|
186
|
+
return true
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function stopQuickForge(instance) {
|
|
192
|
+
if (!instance?.child || instance.reused) return false
|
|
193
|
+
if (instance.child.killed) return false
|
|
194
|
+
instance.child.kill('SIGTERM')
|
|
195
|
+
return true
|
|
196
|
+
}
|
package/server/routes/system.mjs
CHANGED
|
@@ -25,7 +25,8 @@ export async function handleSystemApi(req, res, url, context) {
|
|
|
25
25
|
throw error
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const result = await context.updateQuickForge()
|
|
29
|
+
sendJson(res, result.updateStarted ? 202 : 200, result)
|
|
29
30
|
return
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
const [oldPidArg, packageName, latestVersion, serverScript, serverCwd, logFile, ...serverArgs] = process.argv.slice(2)
|
|
7
|
+
const oldPid = Number(oldPidArg)
|
|
8
|
+
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isProcessRunning(pid) {
|
|
14
|
+
if (!pid) return false
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0)
|
|
17
|
+
return true
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ensureLogDir(filePath) {
|
|
24
|
+
try { fs.mkdirSync(path.dirname(filePath), { recursive: true }) } catch { /* ignore */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function timestamp() {
|
|
28
|
+
return new Date().toISOString()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getNpmCommand() {
|
|
32
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ensureLogDir(logFile)
|
|
36
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' })
|
|
37
|
+
|
|
38
|
+
function log(message) {
|
|
39
|
+
logStream.write(`${timestamp()} ${message}\n`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pipeOutput(stream, prefix) {
|
|
43
|
+
stream?.on('data', (chunk) => {
|
|
44
|
+
const text = String(chunk)
|
|
45
|
+
for (const line of text.split(/\r?\n/)) {
|
|
46
|
+
if (line.trim()) log(`${prefix} ${line}`)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function spawnAndWait(command, args, options) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const child = spawn(command, args, options)
|
|
54
|
+
pipeOutput(child.stdout, '[stdout]')
|
|
55
|
+
pipeOutput(child.stderr, '[stderr]')
|
|
56
|
+
child.once('error', reject)
|
|
57
|
+
child.once('exit', (code, signal) => resolve({ code, signal }))
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const target = `${packageName}@latest`
|
|
63
|
+
log(`QuickForge external updater started. target=${target} latest=${latestVersion || 'unknown'} oldPid=${oldPid || 'unknown'}`)
|
|
64
|
+
log(`Updater cwd=${process.cwd()}`)
|
|
65
|
+
log(`Server script=${serverScript}`)
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < 600 && isProcessRunning(oldPid); i += 1) {
|
|
68
|
+
if (i === 0) log(`Waiting for old QuickForge process ${oldPid} to exit...`)
|
|
69
|
+
await sleep(100)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isProcessRunning(oldPid)) {
|
|
73
|
+
log(`Old QuickForge process ${oldPid} is still running after timeout; continuing with npm install.`)
|
|
74
|
+
} else {
|
|
75
|
+
log('Old QuickForge process has exited.')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const npmCommand = getNpmCommand()
|
|
79
|
+
const npmArgs = ['install', '-g', target]
|
|
80
|
+
log(`Running: ${npmCommand} ${npmArgs.join(' ')}`)
|
|
81
|
+
const installResult = await spawnAndWait(npmCommand, npmArgs, {
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
+
shell: process.platform === 'win32',
|
|
85
|
+
windowsHide: true,
|
|
86
|
+
env: process.env,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (installResult.code !== 0) {
|
|
90
|
+
log(`npm install failed. code=${installResult.code} signal=${installResult.signal || ''}`)
|
|
91
|
+
process.exitCode = installResult.code || 1
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
log('npm install completed successfully.')
|
|
96
|
+
log(`Starting QuickForge server: ${process.execPath} ${[serverScript, ...serverArgs].join(' ')}`)
|
|
97
|
+
const child = spawn(process.execPath, [serverScript, ...serverArgs], {
|
|
98
|
+
cwd: serverCwd || undefined,
|
|
99
|
+
detached: true,
|
|
100
|
+
stdio: 'ignore',
|
|
101
|
+
windowsHide: true,
|
|
102
|
+
shell: false,
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
QUICKFORGE_NO_OPEN: '1',
|
|
106
|
+
QUICKFORGE_RESTARTED_FROM_UPDATE: '1',
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
child.unref()
|
|
111
|
+
log(`QuickForge server spawned. pid=${child.pid || 'unknown'}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await main()
|
|
116
|
+
} catch (error) {
|
|
117
|
+
log(`Updater failed: ${error?.stack || error?.message || error}`)
|
|
118
|
+
process.exitCode = 1
|
|
119
|
+
} finally {
|
|
120
|
+
await new Promise((resolve) => logStream.end(resolve))
|
|
121
|
+
}
|