@shawnstack/quickforge 1.5.2 → 1.5.4
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-DKyIh3dE.js → AgentProfilesPage-CGm7ZzRM.js} +1 -1
- package/dist/assets/ChatPanelHost-nzOC6Tbg.js +242 -0
- package/dist/assets/{PluginsPage-CN-SFQ_s.js → PluginsPage-K3o4AB2E.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-C0htXZk2.js → ScheduledTasksPage-BVjejep8.js} +1 -1
- package/dist/assets/{SharedConversationPage-CxbAx1fN.js → SharedConversationPage-BwmUajLr.js} +1 -1
- package/dist/assets/{TerminalDock-_voUf7d-.js → TerminalDock-D-GWlS7P.js} +1 -1
- package/dist/assets/{WorkspaceInspector-Ci4FuaZH.js → WorkspaceInspector-CEU6nnM-.js} +2 -2
- package/dist/assets/{WorkspaceReaderDialog-D75__GFg.js → WorkspaceReaderDialog-D9Qy6LUm.js} +1 -1
- package/dist/assets/{diff-line-counts-DHyWKEXk.js → diff-line-counts-DCot_pZu.js} +1 -1
- package/dist/assets/index-CkQWeO9c.css +3 -0
- package/dist/assets/index-DTiIspXQ.js +1482 -0
- package/dist/favicon.svg +16 -16
- package/dist/index.html +2 -2
- 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/package.json +2 -1
- package/server/index.mjs +76 -6
- package/server/mcp/config.mjs +20 -20
- package/server/public-api.mjs +196 -0
- package/server/routes/backup.mjs +84 -20
- package/server/routes/system.mjs +2 -1
- package/server/storage.mjs +182 -48
- package/server/update-supervisor.mjs +121 -0
- package/dist/assets/ChatPanelHost-BUZ6scv9.js +0 -242
- package/dist/assets/index-BI7xZuj-.css +0 -3
- package/dist/assets/index-BzOV50wA.js +0 -1442
package/server/storage.mjs
CHANGED
|
@@ -46,6 +46,7 @@ export async function cleanOldLogs() {
|
|
|
46
46
|
|
|
47
47
|
export const stores = new Set([
|
|
48
48
|
'settings',
|
|
49
|
+
'mcp',
|
|
49
50
|
'provider-keys',
|
|
50
51
|
'custom-providers',
|
|
51
52
|
'plugins',
|
|
@@ -74,13 +75,102 @@ export function getStoreRevision(storeName) {
|
|
|
74
75
|
return storeRevisions.get(storeName) || 0
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
// Each configuration store is persisted to its own file under config/.
|
|
79
|
+
// "Solo" stores own a file whose root object *is* the store data.
|
|
80
|
+
// "Shared" stores share one file (providers.json) keyed by section, so that
|
|
81
|
+
// strongly-coupled provider definitions and their API keys stay in sync under
|
|
82
|
+
// a single write queue.
|
|
83
|
+
const soloConfigStores = {
|
|
84
|
+
settings: 'settings.json',
|
|
85
|
+
mcp: 'mcp-servers.json',
|
|
86
|
+
plugins: 'plugins.json',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sharedConfigGroups = {
|
|
90
|
+
'providers.json': {
|
|
91
|
+
queue: 'providers',
|
|
92
|
+
sections: {
|
|
93
|
+
'provider-keys': 'providerKeys',
|
|
94
|
+
'custom-providers': 'customProviders',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Reverse index: storeName -> { file, queue, sectionKey|null }
|
|
100
|
+
const configStoreLocations = (() => {
|
|
101
|
+
const map = {}
|
|
102
|
+
for (const [store, file] of Object.entries(soloConfigStores)) {
|
|
103
|
+
map[store] = { file, queue: store, sectionKey: null }
|
|
104
|
+
}
|
|
105
|
+
for (const [file, group] of Object.entries(sharedConfigGroups)) {
|
|
106
|
+
for (const [store, sectionKey] of Object.entries(group.sections)) {
|
|
107
|
+
map[store] = { file, queue: group.queue, sectionKey }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return map
|
|
111
|
+
})()
|
|
112
|
+
|
|
113
|
+
function isConfigStore(storeName) {
|
|
114
|
+
return Boolean(configStoreLocations[storeName])
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function configStoreFilePath(storeName) {
|
|
118
|
+
return path.join(configDir, configStoreLocations[storeName].file)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Coerce a value into a plain object record (the shape every config store holds).
|
|
122
|
+
function asRecord(value) {
|
|
123
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
124
|
+
}
|
|
78
125
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
// Where each config store lived in the legacy unified config.json, used only as
|
|
127
|
+
// a read fallback (D6 read-side safety net). Note: `mcp` was lifted out of
|
|
128
|
+
// `settings.mcpServers` — its legacy source is nested, not a direct section.
|
|
129
|
+
const legacyConfigSectionReaders = {
|
|
130
|
+
settings: (config) => asRecord(config.app?.settings),
|
|
131
|
+
mcp: (config) => ({ mcpServers: Array.isArray(config.app?.settings?.mcpServers) ? config.app.settings.mcpServers : [] }),
|
|
132
|
+
plugins: (config) => asRecord(config.extensions?.plugins),
|
|
133
|
+
'provider-keys': (config) => asRecord(config.credentials?.providerKeys),
|
|
134
|
+
'custom-providers': (config) => asRecord(config.providers?.customProviders),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Read a single config store from its (possibly shared) file, with a fallback
|
|
138
|
+
// to the legacy unified config.json section when the split file is absent and
|
|
139
|
+
// the split migration has not completed yet (D6 read-side safety net, so an
|
|
140
|
+
// interrupted or partially-completed migration can never surface empty data).
|
|
141
|
+
async function readConfigStore(storeName) {
|
|
142
|
+
const loc = configStoreLocations[storeName]
|
|
143
|
+
const file = configStoreFilePath(storeName)
|
|
144
|
+
|
|
145
|
+
if (existsSync(file)) {
|
|
146
|
+
const content = await readJsonFile(file, {})
|
|
147
|
+
if (loc.sectionKey) return asRecord(content?.[loc.sectionKey])
|
|
148
|
+
return asRecord(content)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Split file missing: only fall back to the legacy section while the split
|
|
152
|
+
// migration is still pending. Once `.split-migrated` exists the split files
|
|
153
|
+
// are authoritative, so a genuinely missing file means an empty store.
|
|
154
|
+
if (!existsSync(splitMigrationMarkerFile)) {
|
|
155
|
+
const reader = legacyConfigSectionReaders[storeName]
|
|
156
|
+
if (reader) return reader(await readConfigFile())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Write a single config store back to its (possibly shared) file. For shared
|
|
163
|
+
// files the sibling sections are preserved. Must run inside the store's queue.
|
|
164
|
+
async function writeConfigStore(storeName, data) {
|
|
165
|
+
const loc = configStoreLocations[storeName]
|
|
166
|
+
const file = configStoreFilePath(storeName)
|
|
167
|
+
if (loc.sectionKey) {
|
|
168
|
+
const content = await readJsonFile(file, {})
|
|
169
|
+
content[loc.sectionKey] = data && typeof data === 'object' && !Array.isArray(data) ? data : {}
|
|
170
|
+
await writeJsonAtomic(file, content)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
await writeJsonAtomic(file, data && typeof data === 'object' && !Array.isArray(data) ? data : {})
|
|
84
174
|
}
|
|
85
175
|
|
|
86
176
|
export function getDataDir() {
|
|
@@ -97,18 +187,16 @@ export const userCommandsDir = path.join(dataDir, 'commands')
|
|
|
97
187
|
|
|
98
188
|
const quickForgeConfigFile = path.join(configDir, 'config.json')
|
|
99
189
|
const configMigrationMarkerFile = path.join(configDir, '.layout-migrated')
|
|
190
|
+
const splitMigrationMarkerFile = path.join(configDir, '.split-migrated')
|
|
191
|
+
const projectsConfigFile = path.join(configDir, 'projects.json')
|
|
100
192
|
const legacyStorageMigrationMarkerFile = path.join(storageDir, '.layout-migrated')
|
|
101
193
|
|
|
102
194
|
export function storeFile(storeName) {
|
|
103
195
|
assertStore(storeName)
|
|
104
|
-
if (
|
|
196
|
+
if (isConfigStore(storeName)) return configStoreFilePath(storeName)
|
|
105
197
|
return sessionStoreFile(storeName, { scope: 'global' })
|
|
106
198
|
}
|
|
107
199
|
|
|
108
|
-
export function configFile() {
|
|
109
|
-
return quickForgeConfigFile
|
|
110
|
-
}
|
|
111
|
-
|
|
112
200
|
function legacyFlatStoreFile(storeName) {
|
|
113
201
|
return path.join(storageDir, `${storeName}.json`)
|
|
114
202
|
}
|
|
@@ -228,17 +316,6 @@ function normalizeConfig(value) {
|
|
|
228
316
|
}
|
|
229
317
|
}
|
|
230
318
|
|
|
231
|
-
function configSection(config, storeName) {
|
|
232
|
-
const [section, key] = configStoreSections[storeName]
|
|
233
|
-
return config?.[section]?.[key] && typeof config[section][key] === 'object' ? config[section][key] : {}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function setConfigSection(config, storeName, data) {
|
|
237
|
-
const [section, key] = configStoreSections[storeName]
|
|
238
|
-
config[section] = config[section] && typeof config[section] === 'object' ? config[section] : {}
|
|
239
|
-
config[section][key] = data && typeof data === 'object' ? data : {}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
319
|
function sessionBucket(value) {
|
|
243
320
|
if (value?.scope === 'project' && value?.projectId) {
|
|
244
321
|
return { scope: 'project', projectId: String(value.projectId) }
|
|
@@ -322,6 +399,10 @@ async function readConfigFile() {
|
|
|
322
399
|
return normalizeConfig(await readJsonFile(quickForgeConfigFile, defaultConfig()))
|
|
323
400
|
}
|
|
324
401
|
|
|
402
|
+
// Used only within the migration chain (migrateUnifiedConfig). It persists the
|
|
403
|
+
// unified (pre-split) layout at layoutVersion 1 as an intermediate step; the
|
|
404
|
+
// subsequent migrateSplitConfig() then demotes config.json to metadata-only
|
|
405
|
+
// (layoutVersion 2). Do not use for normal runtime config writes.
|
|
325
406
|
async function writeConfigFile(config) {
|
|
326
407
|
const next = normalizeConfig(config)
|
|
327
408
|
next.layoutVersion = 1
|
|
@@ -651,6 +732,68 @@ async function migrateUnifiedConfig() {
|
|
|
651
732
|
await fs.writeFile(configMigrationMarkerFile, `${new Date().toISOString()}\n`, 'utf8')
|
|
652
733
|
}
|
|
653
734
|
|
|
735
|
+
// Split the legacy unified config.json into per-store files under config/.
|
|
736
|
+
// Each target file is only written when it does not already exist, so a
|
|
737
|
+
// partially-completed migration can safely resume. config.json is demoted to
|
|
738
|
+
// metadata only after the split succeeds.
|
|
739
|
+
async function migrateSplitConfig() {
|
|
740
|
+
if (existsSync(splitMigrationMarkerFile)) return
|
|
741
|
+
|
|
742
|
+
// Fresh installs have no unified config.json to split — just record marker.
|
|
743
|
+
if (!existsSync(quickForgeConfigFile)) {
|
|
744
|
+
await fs.writeFile(splitMigrationMarkerFile, `${new Date().toISOString()}\n`, 'utf8')
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const config = await readConfigFile()
|
|
749
|
+
|
|
750
|
+
// settings.json — mcpServers is stripped out (moved to its own store below)
|
|
751
|
+
if (!existsSync(path.join(configDir, 'settings.json'))) {
|
|
752
|
+
const oldSettings = config.app?.settings && typeof config.app.settings === 'object'
|
|
753
|
+
? { ...config.app.settings }
|
|
754
|
+
: {}
|
|
755
|
+
delete oldSettings.mcpServers
|
|
756
|
+
await writeJsonAtomic(path.join(configDir, 'settings.json'), oldSettings)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// mcp-servers.json — lifted out of settings.mcpServers
|
|
760
|
+
if (!existsSync(path.join(configDir, 'mcp-servers.json'))) {
|
|
761
|
+
const mcpServers = config.app?.settings?.mcpServers
|
|
762
|
+
await writeJsonAtomic(path.join(configDir, 'mcp-servers.json'), {
|
|
763
|
+
mcpServers: Array.isArray(mcpServers) ? mcpServers : [],
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// providers.json — customProviders + providerKeys kept together (coupled)
|
|
768
|
+
if (!existsSync(path.join(configDir, 'providers.json'))) {
|
|
769
|
+
const customProviders = config.providers?.customProviders && typeof config.providers.customProviders === 'object'
|
|
770
|
+
? config.providers.customProviders
|
|
771
|
+
: {}
|
|
772
|
+
const providerKeys = config.credentials?.providerKeys && typeof config.credentials.providerKeys === 'object'
|
|
773
|
+
? config.credentials.providerKeys
|
|
774
|
+
: {}
|
|
775
|
+
await writeJsonAtomic(path.join(configDir, 'providers.json'), { customProviders, providerKeys })
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// plugins.json
|
|
779
|
+
if (!existsSync(path.join(configDir, 'plugins.json'))) {
|
|
780
|
+
const plugins = config.extensions?.plugins && typeof config.extensions.plugins === 'object'
|
|
781
|
+
? config.extensions.plugins
|
|
782
|
+
: {}
|
|
783
|
+
await writeJsonAtomic(path.join(configDir, 'plugins.json'), plugins)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// projects.json
|
|
787
|
+
if (!existsSync(projectsConfigFile)) {
|
|
788
|
+
await writeJsonAtomic(projectsConfigFile, normalizeProjectConfig(config.projects))
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Demote config.json to metadata only.
|
|
792
|
+
await writeJsonAtomic(quickForgeConfigFile, { layoutVersion: 2, migratedAt: new Date().toISOString() })
|
|
793
|
+
|
|
794
|
+
await fs.writeFile(splitMigrationMarkerFile, `${new Date().toISOString()}\n`, 'utf8')
|
|
795
|
+
}
|
|
796
|
+
|
|
654
797
|
const writeQueues = new Map()
|
|
655
798
|
|
|
656
799
|
function enqueueWrite(queueName, operation) {
|
|
@@ -673,15 +816,13 @@ function enqueueWrite(queueName, operation) {
|
|
|
673
816
|
*/
|
|
674
817
|
export async function atomicUpdate(storeName, updateFn) {
|
|
675
818
|
assertStore(storeName)
|
|
676
|
-
const queueName =
|
|
819
|
+
const queueName = isConfigStore(storeName) ? configStoreLocations[storeName].queue : storeName
|
|
677
820
|
return enqueueWrite(queueName, async () => {
|
|
678
821
|
await ensureStorage()
|
|
679
|
-
if (
|
|
680
|
-
const
|
|
681
|
-
const data = configSection(config, storeName)
|
|
822
|
+
if (isConfigStore(storeName)) {
|
|
823
|
+
const data = await readConfigStore(storeName)
|
|
682
824
|
const updated = updateFn(data)
|
|
683
|
-
|
|
684
|
-
await writeConfigFile(config)
|
|
825
|
+
await writeConfigStore(storeName, updated)
|
|
685
826
|
return updated
|
|
686
827
|
}
|
|
687
828
|
const data = await readSessionStore(storeName)
|
|
@@ -718,13 +859,11 @@ export async function atomicSessionMetadataUpdate(scope, projectId, updateFn) {
|
|
|
718
859
|
* Atomically read-modify-write the project config within the config queue.
|
|
719
860
|
*/
|
|
720
861
|
export async function atomicProjectConfigUpdate(updateFn) {
|
|
721
|
-
return enqueueWrite('
|
|
862
|
+
return enqueueWrite('projects', async () => {
|
|
722
863
|
await ensureStorage()
|
|
723
|
-
const
|
|
724
|
-
const projectConfig = normalizeProjectConfig(config.projects)
|
|
864
|
+
const projectConfig = normalizeProjectConfig(await readJsonFile(projectsConfigFile, defaultProjectConfig()))
|
|
725
865
|
const updated = updateFn(projectConfig)
|
|
726
|
-
|
|
727
|
-
await writeConfigFile(config)
|
|
866
|
+
await writeJsonAtomic(projectsConfigFile, normalizeProjectConfig(updated))
|
|
728
867
|
return updated
|
|
729
868
|
})
|
|
730
869
|
}
|
|
@@ -755,9 +894,10 @@ export function ensureStorage() {
|
|
|
755
894
|
])
|
|
756
895
|
|
|
757
896
|
await migrateUnifiedConfig()
|
|
897
|
+
await migrateSplitConfig()
|
|
758
898
|
|
|
759
899
|
await Promise.all([
|
|
760
|
-
ensureJsonFile(quickForgeConfigFile,
|
|
900
|
+
ensureJsonFile(quickForgeConfigFile, { layoutVersion: 2 }),
|
|
761
901
|
ensureJsonFile(sessionStoreFile('sessions-metadata', { scope: 'global' })),
|
|
762
902
|
])
|
|
763
903
|
})()
|
|
@@ -770,9 +910,8 @@ export async function readStore(storeName) {
|
|
|
770
910
|
assertStore(storeName)
|
|
771
911
|
await ensureStorage()
|
|
772
912
|
|
|
773
|
-
if (
|
|
774
|
-
|
|
775
|
-
return configSection(config, storeName)
|
|
913
|
+
if (isConfigStore(storeName)) {
|
|
914
|
+
return readConfigStore(storeName)
|
|
776
915
|
}
|
|
777
916
|
|
|
778
917
|
return readSessionStore(storeName)
|
|
@@ -780,15 +919,13 @@ export async function readStore(storeName) {
|
|
|
780
919
|
|
|
781
920
|
export async function writeStore(storeName, data) {
|
|
782
921
|
assertStore(storeName)
|
|
783
|
-
const queueName =
|
|
922
|
+
const queueName = isConfigStore(storeName) ? configStoreLocations[storeName].queue : storeName
|
|
784
923
|
|
|
785
924
|
return enqueueWrite(queueName, async () => {
|
|
786
925
|
await ensureStorage()
|
|
787
926
|
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
setConfigSection(config, storeName, data)
|
|
791
|
-
await writeConfigFile(config)
|
|
927
|
+
if (isConfigStore(storeName)) {
|
|
928
|
+
await writeConfigStore(storeName, data)
|
|
792
929
|
return
|
|
793
930
|
}
|
|
794
931
|
|
|
@@ -798,16 +935,13 @@ export async function writeStore(storeName, data) {
|
|
|
798
935
|
|
|
799
936
|
export async function readProjectConfigData() {
|
|
800
937
|
await ensureStorage()
|
|
801
|
-
|
|
802
|
-
return normalizeProjectConfig(config.projects)
|
|
938
|
+
return normalizeProjectConfig(await readJsonFile(projectsConfigFile, defaultProjectConfig()))
|
|
803
939
|
}
|
|
804
940
|
|
|
805
941
|
export async function writeProjectConfigData(projectConfig) {
|
|
806
|
-
return enqueueWrite('
|
|
942
|
+
return enqueueWrite('projects', async () => {
|
|
807
943
|
await ensureStorage()
|
|
808
|
-
|
|
809
|
-
config.projects = normalizeProjectConfig(projectConfig)
|
|
810
|
-
await writeConfigFile(config)
|
|
944
|
+
await writeJsonAtomic(projectsConfigFile, normalizeProjectConfig(projectConfig))
|
|
811
945
|
})
|
|
812
946
|
}
|
|
813
947
|
|
|
@@ -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
|
+
}
|