@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.
Files changed (26) hide show
  1. package/README.md +21 -15
  2. package/bin/quickforge.mjs +11 -1
  3. package/dist/assets/{anthropic-u1nbNXhV.js → anthropic-By-wpU1w.js} +1 -1
  4. package/dist/assets/{azure-openai-responses-DQ6xSOmb.js → azure-openai-responses-C8spS__i.js} +1 -1
  5. package/dist/assets/{confirm-dialog-DSmrqQ60.js → confirm-dialog-4mZt9XEq.js} +1 -1
  6. package/dist/assets/{google-OeyKMN12.js → google-DiIcyajo.js} +1 -1
  7. package/dist/assets/{google-gemini-cli-SnPixyBu.js → google-gemini-cli-BXZFGMXD.js} +1 -1
  8. package/dist/assets/{google-vertex-y0o2eCZV.js → google-vertex-D93MV5Cx.js} +1 -1
  9. package/dist/assets/{index-CK_34smc.js → index-Bq6VHkyY.js} +473 -473
  10. package/dist/assets/index-D7uXa1RT.css +3 -0
  11. package/dist/assets/{mistral-DzE_jn-B.js → mistral-BAJNGYqd.js} +1 -1
  12. package/dist/assets/{openai-codex-responses-MtFRvp_b.js → openai-codex-responses-BHHCy65K.js} +1 -1
  13. package/dist/assets/{openai-completions-C2dhwzO8.js → openai-completions-BtZAvOiJ.js} +1 -1
  14. package/dist/assets/{openai-responses-C4n0VhzY.js → openai-responses-CP9-AyAD.js} +1 -1
  15. package/dist/assets/{openai-responses-shared-D2RkRvTj.js → openai-responses-shared-_z7sua8J.js} +1 -1
  16. package/dist/assets/{prompt-dialog-B4BD09Oc.js → prompt-dialog-BGMKszUz.js} +1 -1
  17. package/dist/index.html +2 -2
  18. package/package.json +1 -1
  19. package/server/index.mjs +8 -6
  20. package/server/project-config.mjs +11 -30
  21. package/server/routes/project.mjs +6 -12
  22. package/server/routes/storage.mjs +9 -3
  23. package/server/storage.mjs +343 -139
  24. package/server/utils/platform.mjs +1 -1
  25. package/server/utils/response.mjs +1 -1
  26. package/dist/assets/index-BQJ8qi1U.css +0 -3
@@ -10,208 +10,412 @@ export const stores = new Set([
10
10
  'sessions-metadata',
11
11
  ])
12
12
 
13
- function platformDataDir(appName) {
14
- if (process.platform === 'win32') {
15
- return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), appName)
16
- }
17
- if (process.platform === 'darwin') {
18
- return path.join(os.homedir(), 'Library', 'Application Support', appName)
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
- return path.join(storageDir, `${storeName}.json`)
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
- const writeQueues = new Map()
69
+ function legacyNestedProjectConfigFile() {
70
+ return path.join(storageDir, 'projects', 'project.json')
71
+ }
41
72
 
42
- export async function ensureStorage() {
43
- await fs.mkdir(storageDir, { recursive: true })
44
- await Promise.all(
45
- [...stores].map(async (store) => {
46
- const file = storeFile(store)
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
- export async function readStore(storeName) {
53
- assertStore(storeName)
54
- await ensureStorage()
55
- const file = storeFile(storeName)
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
- export async function writeStore(storeName, data) {
66
- assertStore(storeName)
67
- const previous = writeQueues.get(storeName) || Promise.resolve()
68
- const next = previous
69
- .catch(() => undefined)
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 assertStore(storeName) {
82
- if (!stores.has(storeName)) {
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
- export function getComparable(value, key) {
90
- if (!value || typeof value !== 'object') return undefined
91
- return key.split('.').reduce((current, part) => {
92
- if (!current || typeof current !== 'object') return undefined
93
- return current[part]
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
- export async function directoryExists(file) {
222
+ async function listProjectSessionFiles(storeName) {
223
+ const projectsDir = path.join(storageDir, 'conversations', 'projects')
224
+ let entries = []
98
225
  try {
99
- await fs.access(file)
100
- return true
101
- } catch {
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
- export async function readJsonObject(file) {
107
- try {
108
- const text = await fs.readFile(file, 'utf8')
109
- const parsed = text.trim() ? JSON.parse(text) : {}
110
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
111
- } catch {
112
- return null
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
- export async function mergeJsonObjectFile(sourceFile, targetFile) {
117
- const source = await readJsonObject(sourceFile)
118
- if (!source) return false
252
+ async function writeSessionStore(storeName, data) {
253
+ const buckets = new Map()
119
254
 
120
- const target = (await readJsonObject(targetFile)) ?? {}
121
- let changed = false
122
- for (const [key, value] of Object.entries(source)) {
123
- if (Object.hasOwn(target, key)) continue
124
- target[key] = value
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
- if (!changed) return false
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 fs.mkdir(path.dirname(targetFile), { recursive: true })
131
- const tmp = `${targetFile}.${process.pid}.${Date.now()}.tmp`
132
- await fs.writeFile(tmp, `${JSON.stringify(target, null, 2)}\n`, 'utf8')
133
- await fs.rename(tmp, targetFile)
134
- return true
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
- export async function copyMissingRecursive(source, target) {
138
- const stat = await fs.stat(source).catch(() => null)
139
- if (!stat) return false
275
+ async function migrateLegacySessionStore(storeName) {
276
+ const file = legacyFlatStoreFile(storeName)
277
+ if (!existsSync(file)) return
140
278
 
141
- if (stat.isDirectory()) {
142
- await fs.mkdir(target, { recursive: true })
143
- let copied = false
144
- const entries = await fs.readdir(source, { withFileTypes: true })
145
- for (const entry of entries) {
146
- copied = (await copyMissingRecursive(path.join(source, entry.name), path.join(target, entry.name))) || copied
147
- }
148
- return copied
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 (!stat.isFile() || (await directoryExists(target))) return false
314
+ if (current.projects.projects.length === 0) {
315
+ current.projects = nestedProjects.projects.length > 0 ? nestedProjects : flatProjects
316
+ }
152
317
 
153
- await fs.mkdir(path.dirname(target), { recursive: true })
154
- await fs.copyFile(source, target)
155
- return true
156
- }
318
+ await writeConfigFile(current)
157
319
 
158
- export function uniquePaths(paths) {
159
- const seen = new Set()
160
- return paths.filter((item) => {
161
- const resolved = path.resolve(item)
162
- const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved
163
- if (seen.has(key)) return false
164
- seen.add(key)
165
- return true
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
- export async function migrateLegacyDataDir(sourceDir) {
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
- const sourceStorageDir = path.join(resolvedSource, 'storage')
177
- if (!(await directoryExists(sourceStorageDir))) return false
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
- let migrated = false
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
- const sourceProjectFile = path.join(sourceStorageDir, 'project.json')
195
- if ((await directoryExists(sourceProjectFile)) && !(await directoryExists(projectConfigFile()))) {
196
- await fs.copyFile(sourceProjectFile, projectConfigFile())
197
- migrated = true
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
- migrated = (await copyMissingRecursive(sourceStorageDir, storageDir)) || migrated
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 migrateLegacyDataDirs() {
206
- if (process.env.QUICKFORGE_DATA_DIR || process.env.FASTCODE_DATA_DIR) return
374
+ export async function writeStore(storeName, data) {
375
+ assertStore(storeName)
376
+ const queueName = configStores.has(storeName) ? 'config' : storeName
207
377
 
208
- const legacyDirs = uniquePaths([
209
- platformDataDir('QuickForge'),
210
- platformDataDir('FastCode'),
211
- path.join(os.homedir(), '.fastcode'),
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
- for (const dir of legacyDirs) {
215
- await migrateLegacyDataDir(dir)
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' || process.env.FASTCODE_NO_OPEN === '1') return
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 || process.env.FASTCODE_MAX_BODY_BYTES || 50 * 1024 * 1024)
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)