@shawnstack/quickforge 1.0.0 → 1.1.0
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 +21 -15
- package/bin/quickforge.mjs +11 -1
- package/dist/assets/{anthropic-u1nbNXhV.js → anthropic-By-wpU1w.js} +1 -1
- package/dist/assets/{azure-openai-responses-DQ6xSOmb.js → azure-openai-responses-C8spS__i.js} +1 -1
- package/dist/assets/{confirm-dialog-DSmrqQ60.js → confirm-dialog-4mZt9XEq.js} +1 -1
- package/dist/assets/{google-OeyKMN12.js → google-DiIcyajo.js} +1 -1
- package/dist/assets/{google-gemini-cli-SnPixyBu.js → google-gemini-cli-BXZFGMXD.js} +1 -1
- package/dist/assets/{google-vertex-y0o2eCZV.js → google-vertex-D93MV5Cx.js} +1 -1
- package/dist/assets/{index-CK_34smc.js → index-Bq6VHkyY.js} +473 -473
- package/dist/assets/index-D7uXa1RT.css +3 -0
- package/dist/assets/{mistral-DzE_jn-B.js → mistral-BAJNGYqd.js} +1 -1
- package/dist/assets/{openai-codex-responses-MtFRvp_b.js → openai-codex-responses-BHHCy65K.js} +1 -1
- package/dist/assets/{openai-completions-C2dhwzO8.js → openai-completions-BtZAvOiJ.js} +1 -1
- package/dist/assets/{openai-responses-C4n0VhzY.js → openai-responses-CP9-AyAD.js} +1 -1
- package/dist/assets/{openai-responses-shared-D2RkRvTj.js → openai-responses-shared-_z7sua8J.js} +1 -1
- package/dist/assets/{prompt-dialog-B4BD09Oc.js → prompt-dialog-BGMKszUz.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/index.mjs +8 -6
- package/server/project-config.mjs +11 -30
- package/server/routes/project.mjs +6 -12
- package/server/routes/storage.mjs +9 -3
- package/server/storage.mjs +343 -139
- package/server/utils/platform.mjs +1 -1
- package/server/utils/response.mjs +1 -1
- package/dist/assets/index-BQJ8qi1U.css +0 -3
package/server/storage.mjs
CHANGED
|
@@ -10,208 +10,412 @@ export const stores = new Set([
|
|
|
10
10
|
'sessions-metadata',
|
|
11
11
|
])
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), appName)
|
|
13
|
+
const configStores = new Set(['settings', 'provider-keys', 'custom-providers'])
|
|
14
|
+
const sessionStores = new Set(['sessions', 'sessions-metadata'])
|
|
15
|
+
|
|
16
|
+
const configStoreSections = {
|
|
17
|
+
settings: ['app', 'settings'],
|
|
18
|
+
'provider-keys': ['credentials', 'providerKeys'],
|
|
19
|
+
'custom-providers': ['providers', 'customProviders'],
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export function getDataDir() {
|
|
24
23
|
if (process.env.QUICKFORGE_DATA_DIR) return path.resolve(process.env.QUICKFORGE_DATA_DIR)
|
|
25
|
-
if (process.env.FASTCODE_DATA_DIR) return path.resolve(process.env.FASTCODE_DATA_DIR)
|
|
26
24
|
return path.join(os.homedir(), '.quickforge')
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
export const dataDir = getDataDir()
|
|
28
|
+
export const configDir = path.join(dataDir, 'config')
|
|
30
29
|
export const storageDir = path.join(dataDir, 'storage')
|
|
30
|
+
export const cacheDir = path.join(dataDir, 'cache')
|
|
31
|
+
export const logsDir = path.join(dataDir, 'logs')
|
|
32
|
+
|
|
33
|
+
const quickForgeConfigFile = path.join(configDir, 'config.json')
|
|
34
|
+
const configMigrationMarkerFile = path.join(configDir, '.layout-migrated')
|
|
35
|
+
const legacyStorageMigrationMarkerFile = path.join(storageDir, '.layout-migrated')
|
|
31
36
|
|
|
32
37
|
export function storeFile(storeName) {
|
|
33
|
-
|
|
38
|
+
assertStore(storeName)
|
|
39
|
+
if (configStores.has(storeName)) return quickForgeConfigFile
|
|
40
|
+
return sessionStoreFile(storeName, { scope: 'global' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function configFile() {
|
|
44
|
+
return quickForgeConfigFile
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
// Compatibility export for older modules/imports. Project config now lives inside config/config.json -> projects.
|
|
36
48
|
export function projectConfigFile() {
|
|
49
|
+
return quickForgeConfigFile
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function legacyFlatStoreFile(storeName) {
|
|
53
|
+
return path.join(storageDir, `${storeName}.json`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function legacyNestedStoreFile(storeName) {
|
|
57
|
+
const paths = {
|
|
58
|
+
settings: ['config', 'settings.json'],
|
|
59
|
+
'provider-keys': ['credentials', 'provider-keys.json'],
|
|
60
|
+
'custom-providers': ['providers', 'custom-providers.json'],
|
|
61
|
+
}
|
|
62
|
+
return path.join(storageDir, ...paths[storeName])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function legacyFlatProjectConfigFile() {
|
|
37
66
|
return path.join(storageDir, 'project.json')
|
|
38
67
|
}
|
|
39
68
|
|
|
40
|
-
|
|
69
|
+
function legacyNestedProjectConfigFile() {
|
|
70
|
+
return path.join(storageDir, 'projects', 'project.json')
|
|
71
|
+
}
|
|
41
72
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
[
|
|
46
|
-
|
|
47
|
-
if (!existsSync(file)) await fs.writeFile(file, '{}\n', 'utf8')
|
|
48
|
-
}),
|
|
49
|
-
)
|
|
73
|
+
function defaultProjectConfig() {
|
|
74
|
+
return {
|
|
75
|
+
activeProjectId: null,
|
|
76
|
+
projects: [],
|
|
77
|
+
}
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
function defaultConfig() {
|
|
81
|
+
return {
|
|
82
|
+
layoutVersion: 1,
|
|
83
|
+
updatedAt: new Date().toISOString(),
|
|
84
|
+
app: {
|
|
85
|
+
settings: {},
|
|
86
|
+
},
|
|
87
|
+
providers: {
|
|
88
|
+
customProviders: {},
|
|
89
|
+
},
|
|
90
|
+
credentials: {
|
|
91
|
+
providerKeys: {},
|
|
92
|
+
},
|
|
93
|
+
projects: defaultProjectConfig(),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeProjectConfig(value) {
|
|
98
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.projects)) return defaultProjectConfig()
|
|
99
|
+
return {
|
|
100
|
+
activeProjectId: typeof value.activeProjectId === 'string' ? value.activeProjectId : null,
|
|
101
|
+
projects: value.projects,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeConfig(value) {
|
|
106
|
+
const base = defaultConfig()
|
|
107
|
+
const input = value && typeof value === 'object' ? value : {}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...input,
|
|
111
|
+
layoutVersion: Number(input.layoutVersion || base.layoutVersion),
|
|
112
|
+
updatedAt: typeof input.updatedAt === 'string' ? input.updatedAt : base.updatedAt,
|
|
113
|
+
app: {
|
|
114
|
+
...(input.app && typeof input.app === 'object' ? input.app : {}),
|
|
115
|
+
settings:
|
|
116
|
+
input.app?.settings && typeof input.app.settings === 'object'
|
|
117
|
+
? input.app.settings
|
|
118
|
+
: base.app.settings,
|
|
119
|
+
},
|
|
120
|
+
providers: {
|
|
121
|
+
...(input.providers && typeof input.providers === 'object' ? input.providers : {}),
|
|
122
|
+
customProviders:
|
|
123
|
+
input.providers?.customProviders && typeof input.providers.customProviders === 'object'
|
|
124
|
+
? input.providers.customProviders
|
|
125
|
+
: base.providers.customProviders,
|
|
126
|
+
},
|
|
127
|
+
credentials: {
|
|
128
|
+
...(input.credentials && typeof input.credentials === 'object' ? input.credentials : {}),
|
|
129
|
+
providerKeys:
|
|
130
|
+
input.credentials?.providerKeys && typeof input.credentials.providerKeys === 'object'
|
|
131
|
+
? input.credentials.providerKeys
|
|
132
|
+
: base.credentials.providerKeys,
|
|
133
|
+
},
|
|
134
|
+
projects: normalizeProjectConfig(input.projects),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function configSection(config, storeName) {
|
|
139
|
+
const [section, key] = configStoreSections[storeName]
|
|
140
|
+
return config?.[section]?.[key] && typeof config[section][key] === 'object' ? config[section][key] : {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setConfigSection(config, storeName, data) {
|
|
144
|
+
const [section, key] = configStoreSections[storeName]
|
|
145
|
+
config[section] = config[section] && typeof config[section] === 'object' ? config[section] : {}
|
|
146
|
+
config[section][key] = data && typeof data === 'object' ? data : {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function sessionBucket(value) {
|
|
150
|
+
if (value?.scope === 'project' && value?.projectId) {
|
|
151
|
+
return { scope: 'project', projectId: String(value.projectId) }
|
|
152
|
+
}
|
|
153
|
+
return { scope: 'global' }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertSafePathSegment(segment) {
|
|
157
|
+
if (!segment || segment === '.' || segment === '..' || /[\\/]/.test(segment)) {
|
|
158
|
+
const error = new Error(`Invalid path segment: ${segment}`)
|
|
159
|
+
error.statusCode = 400
|
|
160
|
+
throw error
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sessionStoreFile(storeName, bucket) {
|
|
165
|
+
if (bucket.scope === 'project') {
|
|
166
|
+
assertSafePathSegment(bucket.projectId)
|
|
167
|
+
return path.join(storageDir, 'conversations', 'projects', bucket.projectId, `${storeName}.json`)
|
|
168
|
+
}
|
|
169
|
+
return path.join(storageDir, 'conversations', 'global', `${storeName}.json`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function ensureProjectCache(projectId) {
|
|
173
|
+
const safeProjectId = String(projectId || '')
|
|
174
|
+
assertSafePathSegment(safeProjectId)
|
|
175
|
+
const projectCacheDir = path.join(cacheDir, 'projects', safeProjectId)
|
|
176
|
+
|
|
177
|
+
await Promise.all([
|
|
178
|
+
fs.mkdir(path.join(projectCacheDir, 'workspace', 'file-index'), { recursive: true }),
|
|
179
|
+
fs.mkdir(path.join(projectCacheDir, 'workspace', 'grep'), { recursive: true }),
|
|
180
|
+
fs.mkdir(path.join(projectCacheDir, 'llm', 'responses'), { recursive: true }),
|
|
181
|
+
fs.mkdir(path.join(projectCacheDir, 'llm', 'reasoning'), { recursive: true }),
|
|
182
|
+
fs.mkdir(path.join(projectCacheDir, 'assets'), { recursive: true }),
|
|
183
|
+
fs.mkdir(path.join(projectCacheDir, 'tmp'), { recursive: true }),
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
return projectCacheDir
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function ensureJsonFile(file, defaultValue = {}) {
|
|
190
|
+
await fs.mkdir(path.dirname(file), { recursive: true })
|
|
191
|
+
if (!existsSync(file)) await fs.writeFile(file, `${JSON.stringify(defaultValue, null, 2)}\n`, 'utf8')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function readJsonFile(file, defaultValue = {}) {
|
|
56
195
|
try {
|
|
57
196
|
const text = await fs.readFile(file, 'utf8')
|
|
58
|
-
return text.trim() ? JSON.parse(text) :
|
|
197
|
+
return text.trim() ? JSON.parse(text) : defaultValue
|
|
59
198
|
} catch (error) {
|
|
60
|
-
if (error?.code === 'ENOENT') return
|
|
199
|
+
if (error?.code === 'ENOENT') return defaultValue
|
|
61
200
|
throw error
|
|
62
201
|
}
|
|
63
202
|
}
|
|
64
203
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.then(async () => {
|
|
71
|
-
await ensureStorage()
|
|
72
|
-
const file = storeFile(storeName)
|
|
73
|
-
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
74
|
-
await fs.writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
75
|
-
await fs.rename(tmp, file)
|
|
76
|
-
})
|
|
77
|
-
writeQueues.set(storeName, next)
|
|
78
|
-
return next
|
|
204
|
+
async function writeJsonAtomic(file, data) {
|
|
205
|
+
await fs.mkdir(path.dirname(file), { recursive: true })
|
|
206
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
207
|
+
await fs.writeFile(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
208
|
+
await fs.rename(tmp, file)
|
|
79
209
|
}
|
|
80
210
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
const error = new Error(`Unknown storage store: ${storeName}`)
|
|
84
|
-
error.statusCode = 404
|
|
85
|
-
throw error
|
|
86
|
-
}
|
|
211
|
+
async function readConfigFile() {
|
|
212
|
+
return normalizeConfig(await readJsonFile(quickForgeConfigFile, defaultConfig()))
|
|
87
213
|
}
|
|
88
214
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}, value)
|
|
215
|
+
async function writeConfigFile(config) {
|
|
216
|
+
const next = normalizeConfig(config)
|
|
217
|
+
next.layoutVersion = 1
|
|
218
|
+
next.updatedAt = new Date().toISOString()
|
|
219
|
+
await writeJsonAtomic(quickForgeConfigFile, next)
|
|
95
220
|
}
|
|
96
221
|
|
|
97
|
-
|
|
222
|
+
async function listProjectSessionFiles(storeName) {
|
|
223
|
+
const projectsDir = path.join(storageDir, 'conversations', 'projects')
|
|
224
|
+
let entries = []
|
|
98
225
|
try {
|
|
99
|
-
await fs.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return false
|
|
226
|
+
entries = await fs.readdir(projectsDir, { withFileTypes: true })
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error?.code !== 'ENOENT') throw error
|
|
103
229
|
}
|
|
230
|
+
|
|
231
|
+
return entries
|
|
232
|
+
.filter((entry) => entry.isDirectory())
|
|
233
|
+
.map((entry) => path.join(projectsDir, entry.name, `${storeName}.json`))
|
|
104
234
|
}
|
|
105
235
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
236
|
+
async function listSessionStoreFiles(storeName) {
|
|
237
|
+
return [
|
|
238
|
+
sessionStoreFile(storeName, { scope: 'global' }),
|
|
239
|
+
...(await listProjectSessionFiles(storeName)),
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function readSessionStore(storeName) {
|
|
244
|
+
const files = await listSessionStoreFiles(storeName)
|
|
245
|
+
const result = {}
|
|
246
|
+
for (const file of files) {
|
|
247
|
+
Object.assign(result, await readJsonFile(file, {}))
|
|
113
248
|
}
|
|
249
|
+
return result
|
|
114
250
|
}
|
|
115
251
|
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
if (!source) return false
|
|
252
|
+
async function writeSessionStore(storeName, data) {
|
|
253
|
+
const buckets = new Map()
|
|
119
254
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
changed = true
|
|
255
|
+
for (const [key, value] of Object.entries(data || {})) {
|
|
256
|
+
const bucket = sessionBucket(value)
|
|
257
|
+
const bucketKey = bucket.scope === 'project' ? `project:${bucket.projectId}` : 'global'
|
|
258
|
+
if (!buckets.has(bucketKey)) buckets.set(bucketKey, { bucket, data: {} })
|
|
259
|
+
buckets.get(bucketKey).data[key] = value
|
|
126
260
|
}
|
|
127
261
|
|
|
128
|
-
|
|
262
|
+
const filesToWrite = new Set(await listSessionStoreFiles(storeName))
|
|
263
|
+
for (const { bucket } of buckets.values()) {
|
|
264
|
+
filesToWrite.add(sessionStoreFile(storeName, bucket))
|
|
265
|
+
}
|
|
129
266
|
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
267
|
+
await Promise.all(
|
|
268
|
+
[...filesToWrite].map(async (file) => {
|
|
269
|
+
const bucketEntry = [...buckets.values()].find((entry) => sessionStoreFile(storeName, entry.bucket) === file)
|
|
270
|
+
await writeJsonAtomic(file, bucketEntry?.data ?? {})
|
|
271
|
+
}),
|
|
272
|
+
)
|
|
135
273
|
}
|
|
136
274
|
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
if (!
|
|
275
|
+
async function migrateLegacySessionStore(storeName) {
|
|
276
|
+
const file = legacyFlatStoreFile(storeName)
|
|
277
|
+
if (!existsSync(file)) return
|
|
140
278
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
279
|
+
const legacy = await readJsonFile(file, {})
|
|
280
|
+
const current = await readSessionStore(storeName)
|
|
281
|
+
const merged = { ...legacy, ...current }
|
|
282
|
+
await writeSessionStore(storeName, merged)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function migrateUnifiedConfig() {
|
|
286
|
+
if (existsSync(configMigrationMarkerFile)) return
|
|
287
|
+
|
|
288
|
+
const current = await readConfigFile()
|
|
289
|
+
const flatSettings = await readJsonFile(legacyFlatStoreFile('settings'), {})
|
|
290
|
+
const nestedSettings = await readJsonFile(legacyNestedStoreFile('settings'), {})
|
|
291
|
+
const flatProviderKeys = await readJsonFile(legacyFlatStoreFile('provider-keys'), {})
|
|
292
|
+
const nestedProviderKeys = await readJsonFile(legacyNestedStoreFile('provider-keys'), {})
|
|
293
|
+
const flatCustomProviders = await readJsonFile(legacyFlatStoreFile('custom-providers'), {})
|
|
294
|
+
const nestedCustomProviders = await readJsonFile(legacyNestedStoreFile('custom-providers'), {})
|
|
295
|
+
const flatProjects = normalizeProjectConfig(await readJsonFile(legacyFlatProjectConfigFile(), defaultProjectConfig()))
|
|
296
|
+
const nestedProjects = normalizeProjectConfig(await readJsonFile(legacyNestedProjectConfigFile(), defaultProjectConfig()))
|
|
297
|
+
|
|
298
|
+
current.app.settings = {
|
|
299
|
+
...flatSettings,
|
|
300
|
+
...nestedSettings,
|
|
301
|
+
...current.app.settings,
|
|
302
|
+
}
|
|
303
|
+
current.credentials.providerKeys = {
|
|
304
|
+
...flatProviderKeys,
|
|
305
|
+
...nestedProviderKeys,
|
|
306
|
+
...current.credentials.providerKeys,
|
|
307
|
+
}
|
|
308
|
+
current.providers.customProviders = {
|
|
309
|
+
...flatCustomProviders,
|
|
310
|
+
...nestedCustomProviders,
|
|
311
|
+
...current.providers.customProviders,
|
|
149
312
|
}
|
|
150
313
|
|
|
151
|
-
if (
|
|
314
|
+
if (current.projects.projects.length === 0) {
|
|
315
|
+
current.projects = nestedProjects.projects.length > 0 ? nestedProjects : flatProjects
|
|
316
|
+
}
|
|
152
317
|
|
|
153
|
-
await
|
|
154
|
-
await fs.copyFile(source, target)
|
|
155
|
-
return true
|
|
156
|
-
}
|
|
318
|
+
await writeConfigFile(current)
|
|
157
319
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
320
|
+
if (!existsSync(legacyStorageMigrationMarkerFile)) {
|
|
321
|
+
await migrateLegacySessionStore('sessions')
|
|
322
|
+
await migrateLegacySessionStore('sessions-metadata')
|
|
323
|
+
await fs.mkdir(path.dirname(legacyStorageMigrationMarkerFile), { recursive: true })
|
|
324
|
+
await fs.writeFile(legacyStorageMigrationMarkerFile, `${new Date().toISOString()}\n`, 'utf8')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await fs.mkdir(path.dirname(configMigrationMarkerFile), { recursive: true })
|
|
328
|
+
await fs.writeFile(configMigrationMarkerFile, `${new Date().toISOString()}\n`, 'utf8')
|
|
167
329
|
}
|
|
168
330
|
|
|
169
|
-
|
|
170
|
-
const resolvedSource = path.resolve(sourceDir)
|
|
171
|
-
const resolvedTarget = path.resolve(dataDir)
|
|
172
|
-
const sourceKey = process.platform === 'win32' ? resolvedSource.toLowerCase() : resolvedSource
|
|
173
|
-
const targetKey = process.platform === 'win32' ? resolvedTarget.toLowerCase() : resolvedTarget
|
|
174
|
-
if (sourceKey === targetKey) return false
|
|
331
|
+
const writeQueues = new Map()
|
|
175
332
|
|
|
176
|
-
|
|
177
|
-
|
|
333
|
+
function enqueueWrite(queueName, operation) {
|
|
334
|
+
const previous = writeQueues.get(queueName) || Promise.resolve()
|
|
335
|
+
const next = previous
|
|
336
|
+
.catch(() => undefined)
|
|
337
|
+
.then(operation)
|
|
338
|
+
writeQueues.set(queueName, next)
|
|
339
|
+
return next
|
|
340
|
+
}
|
|
178
341
|
|
|
342
|
+
export async function ensureStorage() {
|
|
343
|
+
await fs.mkdir(configDir, { recursive: true })
|
|
179
344
|
await fs.mkdir(storageDir, { recursive: true })
|
|
345
|
+
await fs.mkdir(cacheDir, { recursive: true })
|
|
346
|
+
await fs.mkdir(logsDir, { recursive: true })
|
|
347
|
+
await Promise.all([
|
|
348
|
+
fs.mkdir(path.join(cacheDir, 'global', 'llm'), { recursive: true }),
|
|
349
|
+
fs.mkdir(path.join(cacheDir, 'global', 'tmp'), { recursive: true }),
|
|
350
|
+
fs.mkdir(path.join(cacheDir, 'projects'), { recursive: true }),
|
|
351
|
+
fs.mkdir(path.join(storageDir, 'conversations', 'projects'), { recursive: true }),
|
|
352
|
+
])
|
|
180
353
|
|
|
181
|
-
|
|
182
|
-
for (const store of stores) {
|
|
183
|
-
const sourceFile = path.join(sourceStorageDir, `${store}.json`)
|
|
184
|
-
const targetFile = storeFile(store)
|
|
185
|
-
if (!(await directoryExists(sourceFile))) continue
|
|
186
|
-
if (await directoryExists(targetFile)) {
|
|
187
|
-
migrated = (await mergeJsonObjectFile(sourceFile, targetFile)) || migrated
|
|
188
|
-
} else {
|
|
189
|
-
await fs.copyFile(sourceFile, targetFile)
|
|
190
|
-
migrated = true
|
|
191
|
-
}
|
|
192
|
-
}
|
|
354
|
+
await migrateUnifiedConfig()
|
|
193
355
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
356
|
+
await Promise.all([
|
|
357
|
+
ensureJsonFile(quickForgeConfigFile, defaultConfig()),
|
|
358
|
+
...[...sessionStores].map((store) => ensureJsonFile(sessionStoreFile(store, { scope: 'global' }))),
|
|
359
|
+
])
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function readStore(storeName) {
|
|
363
|
+
assertStore(storeName)
|
|
364
|
+
await ensureStorage()
|
|
365
|
+
|
|
366
|
+
if (configStores.has(storeName)) {
|
|
367
|
+
const config = await readConfigFile()
|
|
368
|
+
return configSection(config, storeName)
|
|
198
369
|
}
|
|
199
370
|
|
|
200
|
-
|
|
201
|
-
if (migrated) console.log(`Migrated legacy QuickForge data from ${resolvedSource} to ${resolvedTarget}`)
|
|
202
|
-
return migrated
|
|
371
|
+
return readSessionStore(storeName)
|
|
203
372
|
}
|
|
204
373
|
|
|
205
|
-
export async function
|
|
206
|
-
|
|
374
|
+
export async function writeStore(storeName, data) {
|
|
375
|
+
assertStore(storeName)
|
|
376
|
+
const queueName = configStores.has(storeName) ? 'config' : storeName
|
|
207
377
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
378
|
+
return enqueueWrite(queueName, async () => {
|
|
379
|
+
await ensureStorage()
|
|
380
|
+
|
|
381
|
+
if (configStores.has(storeName)) {
|
|
382
|
+
const config = await readConfigFile()
|
|
383
|
+
setConfigSection(config, storeName, data)
|
|
384
|
+
await writeConfigFile(config)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
await writeSessionStore(storeName, data)
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function readProjectConfigData() {
|
|
393
|
+
await ensureStorage()
|
|
394
|
+
const config = await readConfigFile()
|
|
395
|
+
return normalizeProjectConfig(config.projects)
|
|
396
|
+
}
|
|
213
397
|
|
|
214
|
-
|
|
215
|
-
|
|
398
|
+
export async function writeProjectConfigData(projectConfig) {
|
|
399
|
+
return enqueueWrite('config', async () => {
|
|
400
|
+
await ensureStorage()
|
|
401
|
+
const config = await readConfigFile()
|
|
402
|
+
config.projects = normalizeProjectConfig(projectConfig)
|
|
403
|
+
await writeConfigFile(config)
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function assertStore(storeName) {
|
|
408
|
+
if (!stores.has(storeName)) {
|
|
409
|
+
const error = new Error(`Unknown storage store: ${storeName}`)
|
|
410
|
+
error.statusCode = 404
|
|
411
|
+
throw error
|
|
216
412
|
}
|
|
217
413
|
}
|
|
414
|
+
|
|
415
|
+
export function getComparable(value, key) {
|
|
416
|
+
if (!value || typeof value !== 'object') return undefined
|
|
417
|
+
return key.split('.').reduce((current, part) => {
|
|
418
|
+
if (!current || typeof current !== 'object') return undefined
|
|
419
|
+
return current[part]
|
|
420
|
+
}, value)
|
|
421
|
+
}
|
|
@@ -122,7 +122,7 @@ try {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
export function openBrowser(url) {
|
|
125
|
-
if (process.env.QUICKFORGE_NO_OPEN === '1'
|
|
125
|
+
if (process.env.QUICKFORGE_NO_OPEN === '1') return
|
|
126
126
|
|
|
127
127
|
const command = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
128
128
|
const args = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const DEFAULT_MAX_BODY_BYTES = Number(process.env.QUICKFORGE_MAX_BODY_BYTES ||
|
|
1
|
+
const DEFAULT_MAX_BODY_BYTES = Number(process.env.QUICKFORGE_MAX_BODY_BYTES || 50 * 1024 * 1024)
|
|
2
2
|
|
|
3
3
|
export function sendJson(res, status, value) {
|
|
4
4
|
const body = JSON.stringify(value)
|