@shawnstack/quickforge 1.5.2 → 1.5.3
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 +10 -10
- package/dist/assets/{AgentProfilesPage-DKyIh3dE.js → AgentProfilesPage-BToo_R3Y.js} +1 -1
- package/dist/assets/{ChatPanelHost-BUZ6scv9.js → ChatPanelHost-BTqhhkWK.js} +1 -1
- package/dist/assets/{PluginsPage-CN-SFQ_s.js → PluginsPage-DwzV2vQ4.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-C0htXZk2.js → ScheduledTasksPage-Cbm6LVk3.js} +1 -1
- package/dist/assets/{SharedConversationPage-CxbAx1fN.js → SharedConversationPage-CHE9qABz.js} +1 -1
- package/dist/assets/{TerminalDock-_voUf7d-.js → TerminalDock-Loi8A4pJ.js} +1 -1
- package/dist/assets/{WorkspaceInspector-Ci4FuaZH.js → WorkspaceInspector-Nf5xELW7.js} +1 -1
- package/dist/assets/{WorkspaceReaderDialog-D75__GFg.js → WorkspaceReaderDialog-Bai7v3V0.js} +1 -1
- package/dist/assets/{diff-line-counts-DHyWKEXk.js → diff-line-counts-CCPYa_e0.js} +1 -1
- package/dist/assets/{index-BzOV50wA.js → index-Bt_dRvdG.js} +39 -5
- package/dist/assets/{index-BI7xZuj-.css → index-BzaZg9Br.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/mcp/config.mjs +20 -20
- package/server/routes/backup.mjs +84 -20
- package/server/storage.mjs +182 -48
package/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="QuickForge" />
|
|
12
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
13
|
<title>速构 QuickForge</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-Bt_dRvdG.js"></script>
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-DWdDZTNf.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/pi-ai-Cx633yhb.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/lit-vendor-Dr3cpBGF.js">
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<link rel="modulepreload" crossorigin href="/assets/icons-DzxBk7tb.js">
|
|
21
21
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-DsAeMFcm.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/logger-B65Akg8A.js">
|
|
23
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BzaZg9Br.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body>
|
|
26
26
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/mcp/config.mjs
CHANGED
|
@@ -163,43 +163,43 @@ export function normalizeMcpServers(value) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export async function readMcpServers() {
|
|
166
|
-
const
|
|
167
|
-
return normalizeMcpServers(
|
|
166
|
+
const mcp = await readStore('mcp')
|
|
167
|
+
return normalizeMcpServers(mcp?.[MCP_CONFIG_KEY])
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
export async function writeMcpServers(servers) {
|
|
171
171
|
const normalized = normalizeMcpServers(servers)
|
|
172
|
-
return atomicUpdate('
|
|
173
|
-
|
|
174
|
-
return
|
|
175
|
-
}).then((
|
|
172
|
+
return atomicUpdate('mcp', (mcp) => {
|
|
173
|
+
mcp[MCP_CONFIG_KEY] = normalized.map((server) => ({ ...server, updatedAt: new Date().toISOString() }))
|
|
174
|
+
return mcp
|
|
175
|
+
}).then((mcp) => normalizeMcpServers(mcp?.[MCP_CONFIG_KEY]))
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
export async function upsertMcpServer(server) {
|
|
179
179
|
const normalized = normalizeMcpServerConfig(server)
|
|
180
|
-
return atomicUpdate('
|
|
181
|
-
const servers = normalizeMcpServers(
|
|
180
|
+
return atomicUpdate('mcp', (mcp) => {
|
|
181
|
+
const servers = normalizeMcpServers(mcp?.[MCP_CONFIG_KEY])
|
|
182
182
|
const index = servers.findIndex((item) => item.name === normalized.name)
|
|
183
183
|
const next = { ...normalized, updatedAt: new Date().toISOString() }
|
|
184
184
|
if (index >= 0) servers[index] = next
|
|
185
185
|
else servers.push(next)
|
|
186
|
-
|
|
187
|
-
return
|
|
188
|
-
}).then((
|
|
186
|
+
mcp[MCP_CONFIG_KEY] = servers
|
|
187
|
+
return mcp
|
|
188
|
+
}).then((mcp) => normalizeMcpServers(mcp?.[MCP_CONFIG_KEY]))
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
export async function deleteMcpServer(name) {
|
|
192
192
|
const normalizedName = normalizeName(name)
|
|
193
|
-
return atomicUpdate('
|
|
194
|
-
|
|
195
|
-
return
|
|
196
|
-
}).then((
|
|
193
|
+
return atomicUpdate('mcp', (mcp) => {
|
|
194
|
+
mcp[MCP_CONFIG_KEY] = normalizeMcpServers(mcp?.[MCP_CONFIG_KEY]).filter((server) => server.name !== normalizedName)
|
|
195
|
+
return mcp
|
|
196
|
+
}).then((mcp) => normalizeMcpServers(mcp?.[MCP_CONFIG_KEY]))
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
export async function setMcpServerEnabled(name, enabled) {
|
|
200
200
|
const normalizedName = normalizeName(name)
|
|
201
|
-
return atomicUpdate('
|
|
202
|
-
const servers = normalizeMcpServers(
|
|
201
|
+
return atomicUpdate('mcp', (mcp) => {
|
|
202
|
+
const servers = normalizeMcpServers(mcp?.[MCP_CONFIG_KEY])
|
|
203
203
|
const index = servers.findIndex((server) => server.name === normalizedName)
|
|
204
204
|
if (index < 0) {
|
|
205
205
|
const error = new Error(`MCP server not found: ${normalizedName}`)
|
|
@@ -211,7 +211,7 @@ export async function setMcpServerEnabled(name, enabled) {
|
|
|
211
211
|
enabled: Boolean(enabled),
|
|
212
212
|
updatedAt: new Date().toISOString(),
|
|
213
213
|
}
|
|
214
|
-
|
|
215
|
-
return
|
|
216
|
-
}).then((
|
|
214
|
+
mcp[MCP_CONFIG_KEY] = servers
|
|
215
|
+
return mcp
|
|
216
|
+
}).then((mcp) => normalizeMcpServers(mcp?.[MCP_CONFIG_KEY]))
|
|
217
217
|
}
|
package/server/routes/backup.mjs
CHANGED
|
@@ -16,7 +16,13 @@ import { getWorkspaceRoot } from '../utils/workspace.mjs'
|
|
|
16
16
|
const BACKUP_VERSION = 1
|
|
17
17
|
const BACKUP_APP = 'quickforge'
|
|
18
18
|
const backupScopes = new Set(['all', 'config', 'sessions'])
|
|
19
|
-
const restoreSectionIds = new Set(['settings', 'providerKeys', 'customProviders', 'projects', 'scheduledTasks', 'conversations'])
|
|
19
|
+
const restoreSectionIds = new Set(['settings', 'mcp', 'providerKeys', 'customProviders', 'projects', 'scheduledTasks', 'conversations'])
|
|
20
|
+
const restoreModes = new Set(['replace', 'merge'])
|
|
21
|
+
|
|
22
|
+
function normalizeMode(value) {
|
|
23
|
+
const mode = String(value || 'replace')
|
|
24
|
+
return restoreModes.has(mode) ? mode : 'replace'
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
function normalizeScope(value) {
|
|
22
28
|
const scope = String(value || 'all')
|
|
@@ -109,8 +115,9 @@ async function buildBackup(scope = 'all', options = {}) {
|
|
|
109
115
|
const data = {}
|
|
110
116
|
|
|
111
117
|
if (includeConfig) {
|
|
112
|
-
const [settings, providerKeys, customProviders, projects, scheduledTasks] = await Promise.all([
|
|
118
|
+
const [settings, mcp, providerKeys, customProviders, projects, scheduledTasks] = await Promise.all([
|
|
113
119
|
readStore('settings'),
|
|
120
|
+
readStore('mcp'),
|
|
114
121
|
includeSecrets ? readStore('provider-keys') : Promise.resolve(undefined),
|
|
115
122
|
readStore('custom-providers'),
|
|
116
123
|
readProjectConfigData(),
|
|
@@ -118,6 +125,7 @@ async function buildBackup(scope = 'all', options = {}) {
|
|
|
118
125
|
])
|
|
119
126
|
Object.assign(data, {
|
|
120
127
|
settings,
|
|
128
|
+
mcp,
|
|
121
129
|
customProviders,
|
|
122
130
|
projects,
|
|
123
131
|
scheduledTasks,
|
|
@@ -160,6 +168,7 @@ function normalizeBackupPayload(payload) {
|
|
|
160
168
|
|
|
161
169
|
const sections = {
|
|
162
170
|
settings: section(data, 'settings'),
|
|
171
|
+
mcp: section(data, 'mcp'),
|
|
163
172
|
providerKeys: section(data, 'providerKeys', 'provider-keys'),
|
|
164
173
|
customProviders: section(data, 'customProviders', 'custom-providers'),
|
|
165
174
|
projects: section(data, 'projects'),
|
|
@@ -168,6 +177,17 @@ function normalizeBackupPayload(payload) {
|
|
|
168
177
|
sessionsMetadata: section(data, 'sessionsMetadata', 'sessions-metadata'),
|
|
169
178
|
}
|
|
170
179
|
|
|
180
|
+
// Backward compat: older backups stored MCP servers inside settings.mcpServers.
|
|
181
|
+
if (
|
|
182
|
+
sections.mcp === undefined &&
|
|
183
|
+
sections.settings && typeof sections.settings === 'object' && !Array.isArray(sections.settings) &&
|
|
184
|
+
Object.prototype.hasOwnProperty.call(sections.settings, 'mcpServers')
|
|
185
|
+
) {
|
|
186
|
+
const { mcpServers, ...restSettings } = sections.settings
|
|
187
|
+
sections.settings = restSettings
|
|
188
|
+
sections.mcp = { mcpServers: Array.isArray(mcpServers) ? mcpServers : [] }
|
|
189
|
+
}
|
|
190
|
+
|
|
171
191
|
if (Object.values(sections).every((value) => value === undefined)) {
|
|
172
192
|
const error = new Error('Backup does not contain any restorable sections')
|
|
173
193
|
error.statusCode = 400
|
|
@@ -197,6 +217,7 @@ function validateBackupPayload(payload) {
|
|
|
197
217
|
...backup,
|
|
198
218
|
sections: {
|
|
199
219
|
settings: assertObjectSection(sections.settings, 'settings'),
|
|
220
|
+
mcp: assertObjectSection(sections.mcp, 'mcp'),
|
|
200
221
|
providerKeys: assertObjectSection(sections.providerKeys, 'providerKeys'),
|
|
201
222
|
customProviders: assertObjectSection(sections.customProviders, 'customProviders'),
|
|
202
223
|
projects: assertProjectConfig(sections.projects),
|
|
@@ -249,6 +270,7 @@ function filterRestoreSections(sections, selected) {
|
|
|
249
270
|
if (!selected) return sections
|
|
250
271
|
return {
|
|
251
272
|
settings: selected.has('settings') ? sections.settings : undefined,
|
|
273
|
+
mcp: selected.has('mcp') ? sections.mcp : undefined,
|
|
252
274
|
providerKeys: selected.has('providerKeys') ? sections.providerKeys : undefined,
|
|
253
275
|
customProviders: selected.has('customProviders') ? sections.customProviders : undefined,
|
|
254
276
|
projects: selected.has('projects') ? sections.projects : undefined,
|
|
@@ -267,7 +289,8 @@ function parseImportPayload(body) {
|
|
|
267
289
|
const backup = validateBackupPayload(payload)
|
|
268
290
|
const requestedSections = body?.backup && typeof body === 'object' ? body.sections : undefined
|
|
269
291
|
const selected = normalizeRestoreSections(requestedSections, backup.sections)
|
|
270
|
-
|
|
292
|
+
const mode = normalizeMode(body?.mode)
|
|
293
|
+
return { backup: backupWithSelectedSections(backup, selected), mode }
|
|
271
294
|
}
|
|
272
295
|
|
|
273
296
|
function countKeys(value) {
|
|
@@ -277,6 +300,7 @@ function countKeys(value) {
|
|
|
277
300
|
function buildSummary(sections) {
|
|
278
301
|
const summary = {}
|
|
279
302
|
if (sections.settings !== undefined) summary.settings = countKeys(sections.settings)
|
|
303
|
+
if (sections.mcp !== undefined) summary.mcp = Array.isArray(sections.mcp?.mcpServers) ? sections.mcp.mcpServers.length : countKeys(sections.mcp)
|
|
280
304
|
if (sections.providerKeys !== undefined) summary.providerKeys = countKeys(sections.providerKeys)
|
|
281
305
|
if (sections.customProviders !== undefined) summary.customProviders = countKeys(sections.customProviders)
|
|
282
306
|
if (sections.projects !== undefined) summary.projects = sections.projects.projects.length
|
|
@@ -318,46 +342,86 @@ async function writeSafetyBackup() {
|
|
|
318
342
|
return file
|
|
319
343
|
}
|
|
320
344
|
|
|
321
|
-
|
|
345
|
+
// Merge two plain-object stores: backup entries override local on key collision,
|
|
346
|
+
// local-only keys are preserved.
|
|
347
|
+
function mergeRecordStore(localValue, backupValue) {
|
|
348
|
+
return { ...(localValue && typeof localValue === 'object' ? localValue : {}), ...backupValue }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Merge projects config: dedupe the projects array by id (backup wins on
|
|
352
|
+
// collision, local-only entries preserved), take activeProjectId / globalSkills
|
|
353
|
+
// from backup.
|
|
354
|
+
function mergeProjectConfig(localConfig, backupConfig) {
|
|
355
|
+
const localProjects = Array.isArray(localConfig?.projects) ? localConfig.projects : []
|
|
356
|
+
const backupProjects = Array.isArray(backupConfig.projects) ? backupConfig.projects : []
|
|
357
|
+
const merged = new Map()
|
|
358
|
+
for (const project of localProjects) {
|
|
359
|
+
if (project && typeof project.id === 'string') merged.set(project.id, project)
|
|
360
|
+
}
|
|
361
|
+
for (const project of backupProjects) {
|
|
362
|
+
if (project && typeof project.id === 'string') merged.set(project.id, project)
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
activeProjectId: typeof backupConfig.activeProjectId === 'string' ? backupConfig.activeProjectId : (localConfig?.activeProjectId ?? null),
|
|
366
|
+
globalSkills: Array.isArray(backupConfig.globalSkills) ? backupConfig.globalSkills : (Array.isArray(localConfig?.globalSkills) ? localConfig.globalSkills : []),
|
|
367
|
+
projects: [...merged.values()],
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function restoreValidatedBackup(backup, mode = 'replace') {
|
|
372
|
+
const merge = mode === 'merge'
|
|
322
373
|
const { sections } = backup
|
|
323
374
|
const summary = {}
|
|
324
375
|
|
|
325
376
|
if (sections.settings !== undefined) {
|
|
326
|
-
await
|
|
327
|
-
|
|
377
|
+
const value = merge ? mergeRecordStore(await readStore('settings'), sections.settings) : sections.settings
|
|
378
|
+
await writeStore('settings', value)
|
|
379
|
+
summary.settings = countKeys(value)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (sections.mcp !== undefined) {
|
|
383
|
+
const value = merge ? mergeRecordStore(await readStore('mcp'), sections.mcp) : sections.mcp
|
|
384
|
+
await writeStore('mcp', value)
|
|
385
|
+
summary.mcp = Array.isArray(value?.mcpServers) ? value.mcpServers.length : countKeys(value)
|
|
328
386
|
}
|
|
329
387
|
|
|
330
388
|
if (sections.providerKeys !== undefined) {
|
|
331
|
-
await
|
|
332
|
-
|
|
389
|
+
const value = merge ? mergeRecordStore(await readStore('provider-keys'), sections.providerKeys) : sections.providerKeys
|
|
390
|
+
await writeStore('provider-keys', value)
|
|
391
|
+
summary.providerKeys = countKeys(value)
|
|
333
392
|
}
|
|
334
393
|
|
|
335
394
|
if (sections.customProviders !== undefined) {
|
|
336
|
-
await
|
|
337
|
-
|
|
395
|
+
const value = merge ? mergeRecordStore(await readStore('custom-providers'), sections.customProviders) : sections.customProviders
|
|
396
|
+
await writeStore('custom-providers', value)
|
|
397
|
+
summary.customProviders = countKeys(value)
|
|
338
398
|
}
|
|
339
399
|
|
|
340
400
|
if (sections.projects !== undefined) {
|
|
341
|
-
await
|
|
401
|
+
const value = merge ? mergeProjectConfig(await readProjectConfigData(), sections.projects) : sections.projects
|
|
402
|
+
await writeProjectConfigData(value)
|
|
342
403
|
await initializeActiveProject()
|
|
343
404
|
setActiveWorkspaceRootForFilesystem(getWorkspaceRoot())
|
|
344
|
-
summary.projects =
|
|
405
|
+
summary.projects = value.projects.length
|
|
345
406
|
}
|
|
346
407
|
|
|
347
408
|
if (sections.scheduledTasks !== undefined) {
|
|
348
|
-
await
|
|
349
|
-
|
|
409
|
+
const value = merge ? mergeRecordStore(await readStore('scheduled-tasks'), sections.scheduledTasks) : sections.scheduledTasks
|
|
410
|
+
await writeStore('scheduled-tasks', value)
|
|
411
|
+
summary.scheduledTasks = countKeys(value)
|
|
350
412
|
}
|
|
351
413
|
|
|
352
414
|
if (sections.sessions !== undefined) {
|
|
353
415
|
const sessions = filterSessionsByMetadata(sections.sessions, sections.sessionsMetadata)
|
|
354
|
-
await
|
|
355
|
-
|
|
416
|
+
const value = merge ? mergeRecordStore(await readStore('sessions'), sessions) : sessions
|
|
417
|
+
await writeStore('sessions', value)
|
|
418
|
+
summary.sessions = countKeys(value)
|
|
356
419
|
}
|
|
357
420
|
|
|
358
421
|
if (sections.sessionsMetadata !== undefined) {
|
|
359
|
-
await
|
|
360
|
-
|
|
422
|
+
const value = merge ? mergeRecordStore(await readStore('sessions-metadata'), sections.sessionsMetadata) : sections.sessionsMetadata
|
|
423
|
+
await writeStore('sessions-metadata', value)
|
|
424
|
+
summary.sessionsMetadata = countKeys(value)
|
|
361
425
|
}
|
|
362
426
|
|
|
363
427
|
return summary
|
|
@@ -382,9 +446,9 @@ export async function handleBackupApi(req, res, url) {
|
|
|
382
446
|
if (req.method === 'POST' && url.pathname === '/api/backup/import') {
|
|
383
447
|
await ensureStorage()
|
|
384
448
|
const body = await readJsonBody(req)
|
|
385
|
-
const backup = parseImportPayload(body)
|
|
449
|
+
const { backup, mode } = parseImportPayload(body)
|
|
386
450
|
const safetyBackupPath = await writeSafetyBackup()
|
|
387
|
-
const summary = await restoreValidatedBackup(backup)
|
|
451
|
+
const summary = await restoreValidatedBackup(backup, mode)
|
|
388
452
|
sendJson(res, 200, { ok: true, safetyBackupPath, summary })
|
|
389
453
|
return
|
|
390
454
|
}
|
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
|
|