@muyichengshayu/promptx 0.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.
@@ -0,0 +1,389 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import Database from 'better-sqlite3'
4
+ import { ensurePromptxStorageReady } from './appPaths.js'
5
+
6
+ const SCHEMA_VERSION = 1
7
+ const { dataDir } = ensurePromptxStorageReady()
8
+ const dbPath = path.join(dataDir, 'promptx.sqlite')
9
+
10
+ fs.mkdirSync(dataDir, { recursive: true })
11
+
12
+ let db = openDatabaseFromDisk()
13
+ let transactionDepth = 0
14
+
15
+ function setDatabasePragmas(targetDb) {
16
+ targetDb.pragma('foreign_keys = ON')
17
+ targetDb.pragma('journal_mode = WAL')
18
+ targetDb.pragma('synchronous = NORMAL')
19
+ }
20
+
21
+ function validateDatabase(targetDb) {
22
+ targetDb.prepare('SELECT name FROM sqlite_master LIMIT 1').all()
23
+ }
24
+
25
+ function createDatabaseConnection() {
26
+ const connection = new Database(dbPath)
27
+ setDatabasePragmas(connection)
28
+ validateDatabase(connection)
29
+ return connection
30
+ }
31
+
32
+ function closeDatabase(targetDb) {
33
+ if (!targetDb) {
34
+ return
35
+ }
36
+
37
+ try {
38
+ targetDb.close()
39
+ } catch {
40
+ // Ignore close failures during process shutdown or reset.
41
+ }
42
+ }
43
+
44
+ function backupDatabaseFile(reason = 'legacy') {
45
+ if (!fs.existsSync(dbPath)) {
46
+ return ''
47
+ }
48
+
49
+ const backupPath = `${dbPath}.${reason}-${Date.now()}.bak`
50
+ fs.copyFileSync(dbPath, backupPath)
51
+ return backupPath
52
+ }
53
+
54
+ function resetDatabaseFile() {
55
+ closeDatabase(db)
56
+ fs.rmSync(dbPath, { force: true })
57
+ db = createDatabaseConnection()
58
+ }
59
+
60
+ function openDatabaseFromDisk() {
61
+ try {
62
+ return createDatabaseConnection()
63
+ } catch {
64
+ backupDatabaseFile('corrupt')
65
+ fs.rmSync(dbPath, { force: true })
66
+ return createDatabaseConnection()
67
+ }
68
+ }
69
+
70
+ function normalizeIdentifier(value = '') {
71
+ const text = String(value || '').trim()
72
+ if (!/^[A-Za-z0-9_]+$/.test(text)) {
73
+ throw new Error(`非法标识符:${value}`)
74
+ }
75
+ return text
76
+ }
77
+
78
+ function normalizeParams(params = []) {
79
+ if (Array.isArray(params)) {
80
+ return params
81
+ }
82
+
83
+ if (params && typeof params === 'object') {
84
+ return params
85
+ }
86
+
87
+ if (typeof params === 'undefined') {
88
+ return []
89
+ }
90
+
91
+ return [params]
92
+ }
93
+
94
+ function executeStatement(statement, method, params = []) {
95
+ const normalized = normalizeParams(params)
96
+
97
+ if (Array.isArray(normalized)) {
98
+ return statement[method](...normalized)
99
+ }
100
+
101
+ return statement[method](normalized)
102
+ }
103
+
104
+ function tableExists(name) {
105
+ return Boolean(
106
+ get('SELECT name FROM sqlite_master WHERE type = ? AND name = ?', ['table', String(name || '').trim()])
107
+ )
108
+ }
109
+
110
+ function columnExists(tableName, columnName) {
111
+ try {
112
+ const normalizedTableName = normalizeIdentifier(tableName)
113
+ return all(`PRAGMA table_info(${normalizedTableName})`).some((row) => row.name === columnName)
114
+ } catch {
115
+ return false
116
+ }
117
+ }
118
+
119
+ function hasLegacySchema() {
120
+ const hasLegacyDocumentsTable = tableExists('documents')
121
+ const hasLegacyBlockColumn = tableExists('blocks') && !columnExists('blocks', 'task_id')
122
+ const hasLegacyTaskColumns = tableExists('tasks') && !columnExists('tasks', 'auto_title')
123
+
124
+ return hasLegacyDocumentsTable || hasLegacyBlockColumn || hasLegacyTaskColumns
125
+ }
126
+
127
+ function resetLegacyDatabaseIfNeeded() {
128
+ if (!hasLegacySchema()) {
129
+ return false
130
+ }
131
+
132
+ backupDatabaseFile('legacy')
133
+ resetDatabaseFile()
134
+ return true
135
+ }
136
+
137
+ function ensureSchemaMetaTable() {
138
+ db.exec(`
139
+ CREATE TABLE IF NOT EXISTS schema_meta (
140
+ key TEXT PRIMARY KEY,
141
+ value TEXT NOT NULL
142
+ );
143
+ `)
144
+ }
145
+
146
+ function readSchemaVersion() {
147
+ ensureSchemaMetaTable()
148
+ const row = get('SELECT value FROM schema_meta WHERE key = ?', ['schema_version'])
149
+ return Math.max(0, Number(row?.value) || 0)
150
+ }
151
+
152
+ function writeSchemaVersion(version = 0) {
153
+ ensureSchemaMetaTable()
154
+ run(
155
+ `INSERT INTO schema_meta (key, value)
156
+ VALUES ('schema_version', ?)
157
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
158
+ [String(Math.max(0, Number(version) || 0))]
159
+ )
160
+ }
161
+
162
+ function migrateToV1() {
163
+ db.exec(`
164
+ CREATE TABLE IF NOT EXISTS tasks (
165
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
166
+ slug TEXT NOT NULL UNIQUE,
167
+ edit_token TEXT NOT NULL,
168
+ title TEXT NOT NULL DEFAULT '',
169
+ auto_title TEXT NOT NULL DEFAULT '',
170
+ last_prompt_preview TEXT NOT NULL DEFAULT '',
171
+ codex_session_id TEXT NOT NULL DEFAULT '',
172
+ visibility TEXT NOT NULL DEFAULT 'private',
173
+ expires_at TEXT,
174
+ created_at TEXT NOT NULL,
175
+ updated_at TEXT NOT NULL
176
+ );
177
+
178
+ CREATE TABLE IF NOT EXISTS blocks (
179
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
180
+ task_id INTEGER NOT NULL,
181
+ type TEXT NOT NULL,
182
+ content TEXT NOT NULL DEFAULT '',
183
+ sort_order INTEGER NOT NULL DEFAULT 0,
184
+ meta_json TEXT NOT NULL DEFAULT '{}',
185
+ created_at TEXT NOT NULL,
186
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
187
+ );
188
+
189
+ CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at DESC);
190
+ CREATE INDEX IF NOT EXISTS idx_tasks_visibility ON tasks(visibility, created_at DESC);
191
+ CREATE INDEX IF NOT EXISTS idx_blocks_task_sort ON blocks(task_id, sort_order ASC);
192
+
193
+ CREATE TABLE IF NOT EXISTS codex_sessions (
194
+ id TEXT PRIMARY KEY,
195
+ title TEXT NOT NULL,
196
+ cwd TEXT NOT NULL,
197
+ codex_thread_id TEXT,
198
+ created_at TEXT NOT NULL,
199
+ updated_at TEXT NOT NULL
200
+ );
201
+
202
+ CREATE INDEX IF NOT EXISTS idx_codex_sessions_updated_at ON codex_sessions(updated_at DESC);
203
+
204
+ CREATE TABLE IF NOT EXISTS codex_runs (
205
+ id TEXT PRIMARY KEY,
206
+ task_slug TEXT NOT NULL,
207
+ session_id TEXT NOT NULL,
208
+ prompt TEXT NOT NULL DEFAULT '',
209
+ status TEXT NOT NULL,
210
+ response_message TEXT NOT NULL DEFAULT '',
211
+ error_message TEXT NOT NULL DEFAULT '',
212
+ created_at TEXT NOT NULL,
213
+ updated_at TEXT NOT NULL,
214
+ started_at TEXT,
215
+ finished_at TEXT,
216
+ FOREIGN KEY (task_slug) REFERENCES tasks(slug) ON DELETE CASCADE,
217
+ FOREIGN KEY (session_id) REFERENCES codex_sessions(id) ON DELETE CASCADE
218
+ );
219
+
220
+ CREATE INDEX IF NOT EXISTS idx_codex_runs_task_slug_created_at ON codex_runs(task_slug, created_at DESC);
221
+ CREATE INDEX IF NOT EXISTS idx_codex_runs_session_id_status ON codex_runs(session_id, status, created_at DESC);
222
+ CREATE INDEX IF NOT EXISTS idx_codex_runs_status_created_at ON codex_runs(status, created_at DESC);
223
+
224
+ CREATE TABLE IF NOT EXISTS codex_run_events (
225
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
226
+ run_id TEXT NOT NULL,
227
+ seq INTEGER NOT NULL,
228
+ event_type TEXT NOT NULL DEFAULT 'event',
229
+ payload_json TEXT NOT NULL DEFAULT '{}',
230
+ created_at TEXT NOT NULL,
231
+ FOREIGN KEY (run_id) REFERENCES codex_runs(id) ON DELETE CASCADE
232
+ );
233
+
234
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_codex_run_events_run_seq ON codex_run_events(run_id, seq);
235
+ CREATE INDEX IF NOT EXISTS idx_codex_run_events_run_id_id ON codex_run_events(run_id, id ASC);
236
+
237
+ CREATE TABLE IF NOT EXISTS task_git_baselines (
238
+ task_slug TEXT PRIMARY KEY,
239
+ repo_root TEXT NOT NULL,
240
+ head_oid TEXT NOT NULL DEFAULT '',
241
+ branch_label TEXT NOT NULL DEFAULT '',
242
+ created_at TEXT NOT NULL,
243
+ updated_at TEXT NOT NULL,
244
+ FOREIGN KEY (task_slug) REFERENCES tasks(slug) ON DELETE CASCADE
245
+ );
246
+
247
+ CREATE TABLE IF NOT EXISTS task_git_baseline_entries (
248
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
249
+ task_slug TEXT NOT NULL,
250
+ path TEXT NOT NULL,
251
+ state_json TEXT NOT NULL DEFAULT '{}',
252
+ FOREIGN KEY (task_slug) REFERENCES task_git_baselines(task_slug) ON DELETE CASCADE
253
+ );
254
+
255
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_task_git_baseline_entries_scope_path
256
+ ON task_git_baseline_entries(task_slug, path);
257
+
258
+ CREATE TABLE IF NOT EXISTS run_git_baselines (
259
+ run_id TEXT PRIMARY KEY,
260
+ repo_root TEXT NOT NULL,
261
+ head_oid TEXT NOT NULL DEFAULT '',
262
+ branch_label TEXT NOT NULL DEFAULT '',
263
+ created_at TEXT NOT NULL,
264
+ FOREIGN KEY (run_id) REFERENCES codex_runs(id) ON DELETE CASCADE
265
+ );
266
+
267
+ CREATE TABLE IF NOT EXISTS run_git_baseline_entries (
268
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
269
+ run_id TEXT NOT NULL,
270
+ path TEXT NOT NULL,
271
+ state_json TEXT NOT NULL DEFAULT '{}',
272
+ FOREIGN KEY (run_id) REFERENCES run_git_baselines(run_id) ON DELETE CASCADE
273
+ );
274
+
275
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_run_git_baseline_entries_scope_path
276
+ ON run_git_baseline_entries(run_id, path);
277
+
278
+ CREATE TABLE IF NOT EXISTS run_git_final_snapshots (
279
+ run_id TEXT PRIMARY KEY,
280
+ repo_root TEXT NOT NULL,
281
+ head_oid TEXT NOT NULL DEFAULT '',
282
+ branch_label TEXT NOT NULL DEFAULT '',
283
+ created_at TEXT NOT NULL,
284
+ FOREIGN KEY (run_id) REFERENCES codex_runs(id) ON DELETE CASCADE
285
+ );
286
+
287
+ CREATE TABLE IF NOT EXISTS run_git_final_snapshot_entries (
288
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
289
+ run_id TEXT NOT NULL,
290
+ path TEXT NOT NULL,
291
+ state_json TEXT NOT NULL DEFAULT '{}',
292
+ FOREIGN KEY (run_id) REFERENCES run_git_final_snapshots(run_id) ON DELETE CASCADE
293
+ );
294
+
295
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_run_git_final_snapshot_entries_scope_path
296
+ ON run_git_final_snapshot_entries(run_id, path);
297
+ `)
298
+
299
+ const alterStatements = [
300
+ `ALTER TABLE tasks ADD COLUMN auto_title TEXT NOT NULL DEFAULT ''`,
301
+ `ALTER TABLE tasks ADD COLUMN last_prompt_preview TEXT NOT NULL DEFAULT ''`,
302
+ `ALTER TABLE tasks ADD COLUMN codex_session_id TEXT NOT NULL DEFAULT ''`,
303
+ `ALTER TABLE task_git_baselines ADD COLUMN branch_label TEXT NOT NULL DEFAULT ''`,
304
+ `ALTER TABLE run_git_baselines ADD COLUMN branch_label TEXT NOT NULL DEFAULT ''`,
305
+ `ALTER TABLE codex_run_events ADD COLUMN event_type TEXT NOT NULL DEFAULT 'event'`,
306
+ ]
307
+
308
+ alterStatements.forEach((statement) => {
309
+ try {
310
+ db.exec(statement)
311
+ } catch {
312
+ // Column already exists.
313
+ }
314
+ })
315
+
316
+ db.exec('CREATE INDEX IF NOT EXISTS idx_tasks_codex_session_id ON tasks(codex_session_id)')
317
+ db.exec(`
318
+ DELETE FROM blocks
319
+ WHERE task_id NOT IN (SELECT id FROM tasks);
320
+ `)
321
+ }
322
+
323
+ function ensureSchema() {
324
+ resetLegacyDatabaseIfNeeded()
325
+ const currentVersion = readSchemaVersion()
326
+
327
+ if (currentVersion < 1) {
328
+ migrateToV1()
329
+ writeSchemaVersion(1)
330
+ }
331
+
332
+ if (readSchemaVersion() < SCHEMA_VERSION) {
333
+ writeSchemaVersion(SCHEMA_VERSION)
334
+ }
335
+ }
336
+
337
+ ensureSchema()
338
+
339
+ export function persist() {
340
+ return
341
+ }
342
+
343
+ export function all(sql, params = []) {
344
+ const statement = db.prepare(sql)
345
+ return executeStatement(statement, 'all', params)
346
+ }
347
+
348
+ export function get(sql, params = []) {
349
+ const statement = db.prepare(sql)
350
+ return executeStatement(statement, 'get', params) || null
351
+ }
352
+
353
+ export function run(sql, params = []) {
354
+ const statement = db.prepare(sql)
355
+ executeStatement(statement, 'run', params)
356
+ }
357
+
358
+ export function transaction(callback) {
359
+ const isOuterTransaction = transactionDepth === 0
360
+
361
+ if (isOuterTransaction) {
362
+ db.exec('BEGIN')
363
+ }
364
+
365
+ transactionDepth += 1
366
+
367
+ try {
368
+ const result = callback()
369
+ transactionDepth -= 1
370
+
371
+ if (isOuterTransaction) {
372
+ db.exec('COMMIT')
373
+ }
374
+
375
+ return result
376
+ } catch (error) {
377
+ transactionDepth -= 1
378
+
379
+ if (isOuterTransaction) {
380
+ db.exec('ROLLBACK')
381
+ }
382
+
383
+ throw error
384
+ }
385
+ }
386
+
387
+ process.once('exit', () => {
388
+ closeDatabase(db)
389
+ })