@cliangdev/flux-plugin 0.2.0 → 0.3.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 (108) hide show
  1. package/README.md +11 -7
  2. package/agents/coder.md +150 -25
  3. package/bin/install.cjs +171 -16
  4. package/commands/breakdown.md +47 -10
  5. package/commands/dashboard.md +29 -0
  6. package/commands/flux.md +92 -12
  7. package/commands/implement.md +166 -17
  8. package/commands/linear.md +6 -5
  9. package/commands/prd.md +996 -82
  10. package/manifest.json +2 -1
  11. package/package.json +9 -11
  12. package/skills/flux-orchestrator/SKILL.md +11 -3
  13. package/skills/prd-writer/SKILL.md +761 -0
  14. package/skills/ux-ui-design/SKILL.md +346 -0
  15. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  16. package/src/__tests__/version.test.ts +37 -0
  17. package/src/adapters/local/.gitkeep +0 -0
  18. package/src/dashboard/__tests__/api.test.ts +211 -0
  19. package/src/dashboard/browser.ts +35 -0
  20. package/src/dashboard/public/app.js +869 -0
  21. package/src/dashboard/public/index.html +90 -0
  22. package/src/dashboard/public/styles.css +807 -0
  23. package/src/dashboard/public/vendor/highlight.css +10 -0
  24. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  25. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  26. package/src/dashboard/server.ts +296 -0
  27. package/src/dashboard/watchers.ts +83 -0
  28. package/src/server/__tests__/config.test.ts +163 -0
  29. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  30. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  31. package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
  32. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  33. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  34. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  35. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  36. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  37. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  38. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  39. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  40. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  41. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  42. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  43. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  44. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  45. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  46. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  47. package/src/server/adapters/factory.ts +90 -0
  48. package/src/server/adapters/index.ts +9 -0
  49. package/src/server/adapters/linear/adapter.ts +1141 -0
  50. package/src/server/adapters/linear/client.ts +169 -0
  51. package/src/server/adapters/linear/config.ts +152 -0
  52. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  53. package/src/server/adapters/linear/helpers/index.ts +7 -0
  54. package/src/server/adapters/linear/index.ts +16 -0
  55. package/src/server/adapters/linear/mappers/description.ts +136 -0
  56. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  57. package/src/server/adapters/linear/mappers/index.ts +27 -0
  58. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  59. package/src/server/adapters/linear/mappers/task.ts +82 -0
  60. package/src/server/adapters/linear/types.ts +264 -0
  61. package/src/server/adapters/local-adapter.ts +1009 -0
  62. package/src/server/adapters/types.ts +293 -0
  63. package/src/server/config.ts +73 -0
  64. package/src/server/db/__tests__/queries.test.ts +473 -0
  65. package/src/server/db/ids.ts +17 -0
  66. package/src/server/db/index.ts +69 -0
  67. package/src/server/db/queries.ts +142 -0
  68. package/src/server/db/refs.ts +60 -0
  69. package/src/server/db/schema.ts +97 -0
  70. package/src/server/db/sqlite.ts +10 -0
  71. package/src/server/index.ts +81 -0
  72. package/src/server/tools/__tests__/crud.test.ts +411 -0
  73. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  74. package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
  75. package/src/server/tools/__tests__/query.test.ts +405 -0
  76. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  77. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  78. package/src/server/tools/configure-linear.ts +373 -0
  79. package/src/server/tools/create-epic.ts +44 -0
  80. package/src/server/tools/create-prd.ts +40 -0
  81. package/src/server/tools/create-task.ts +47 -0
  82. package/src/server/tools/criteria.ts +50 -0
  83. package/src/server/tools/delete-entity.ts +76 -0
  84. package/src/server/tools/dependencies.ts +55 -0
  85. package/src/server/tools/get-entity.ts +240 -0
  86. package/src/server/tools/get-linear-url.ts +28 -0
  87. package/src/server/tools/get-stats.ts +52 -0
  88. package/src/server/tools/get-version.ts +20 -0
  89. package/src/server/tools/index.ts +158 -0
  90. package/src/server/tools/init-project.ts +108 -0
  91. package/src/server/tools/query-entities.ts +167 -0
  92. package/src/server/tools/render-status.ts +219 -0
  93. package/src/server/tools/update-entity.ts +140 -0
  94. package/src/server/tools/update-status.ts +166 -0
  95. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  96. package/src/server/utils/logger.ts +9 -0
  97. package/src/server/utils/mcp-response.ts +254 -0
  98. package/src/server/utils/status-transitions.ts +160 -0
  99. package/src/status-line/__tests__/status-line.test.ts +215 -0
  100. package/src/status-line/index.ts +147 -0
  101. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  102. package/src/utils/__tests__/display.test.ts +97 -0
  103. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  104. package/src/utils/display.ts +62 -0
  105. package/src/utils/status-renderer.ts +214 -0
  106. package/src/version.ts +5 -0
  107. package/dist/server/index.js +0 -87063
  108. package/skills/prd-template/SKILL.md +0 -242
@@ -0,0 +1,473 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ // Set up test environment BEFORE any imports
6
+ const TEST_DIR = `/tmp/flux-test-db-${Date.now()}-${Math.random().toString(36).slice(2)}`;
7
+ const FLUX_DIR = join(TEST_DIR, ".flux");
8
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
9
+
10
+ // Now import modules
11
+ import { config } from "../../config.js";
12
+ import {
13
+ closeDb,
14
+ count,
15
+ findAll,
16
+ findById,
17
+ findByRef,
18
+ generateId,
19
+ generateRef,
20
+ getDb,
21
+ initDb,
22
+ insert,
23
+ remove,
24
+ update,
25
+ } from "../index.js";
26
+
27
+ describe("Database Queries", () => {
28
+ let projectId: string;
29
+
30
+ beforeEach(() => {
31
+ closeDb();
32
+ config.clearCache();
33
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
34
+
35
+ mkdirSync(FLUX_DIR, { recursive: true });
36
+ writeFileSync(
37
+ join(FLUX_DIR, "project.json"),
38
+ JSON.stringify({ name: "test-project", ref_prefix: "TEST" }),
39
+ );
40
+ initDb();
41
+
42
+ // Create a project for foreign key constraints
43
+ projectId = generateId("proj");
44
+ const db = getDb();
45
+ insert(db, "projects", {
46
+ id: projectId,
47
+ name: "test-project",
48
+ ref_prefix: "TEST",
49
+ });
50
+ });
51
+
52
+ afterEach(() => {
53
+ closeDb();
54
+ config.clearCache();
55
+ if (existsSync(TEST_DIR)) {
56
+ rmSync(TEST_DIR, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ describe("generateId", () => {
61
+ test("generates unique IDs with prefix", () => {
62
+ const id1 = generateId("test");
63
+ const id2 = generateId("test");
64
+
65
+ expect(id1).toMatch(/^test_/);
66
+ expect(id2).toMatch(/^test_/);
67
+ expect(id1).not.toBe(id2);
68
+ });
69
+
70
+ test("generates IDs with different prefixes", () => {
71
+ const prdId = generateId("prd");
72
+ const epicId = generateId("epic");
73
+ const taskId = generateId("task");
74
+
75
+ expect(prdId).toMatch(/^prd_/);
76
+ expect(epicId).toMatch(/^epic_/);
77
+ expect(taskId).toMatch(/^task_/);
78
+ });
79
+ });
80
+
81
+ describe("insert and findById", () => {
82
+ test("inserts and retrieves record", () => {
83
+ const db = getDb();
84
+ const id = generateId("prd");
85
+
86
+ insert(db, "prds", {
87
+ id,
88
+ project_id: projectId,
89
+ ref: "TEST-P1",
90
+ title: "Test PRD",
91
+ status: "DRAFT",
92
+ });
93
+
94
+ const result = findById<{ id: string; title: string }>(db, "prds", id);
95
+ expect(result).toBeDefined();
96
+ expect(result?.id).toBe(id);
97
+ expect(result?.title).toBe("Test PRD");
98
+ });
99
+
100
+ test("findById returns null for non-existent record", () => {
101
+ const db = getDb();
102
+ const result = findById(db, "prds", "nonexistent_id");
103
+ expect(result).toBeNull();
104
+ });
105
+ });
106
+
107
+ describe("findByRef", () => {
108
+ test("finds record by ref", () => {
109
+ const db = getDb();
110
+ const id = generateId("prd");
111
+
112
+ insert(db, "prds", {
113
+ id,
114
+ project_id: projectId,
115
+ ref: "TEST-P99",
116
+ title: "Test PRD",
117
+ status: "DRAFT",
118
+ });
119
+
120
+ const result = findByRef<{ ref: string; title: string }>(
121
+ db,
122
+ "prds",
123
+ "TEST-P99",
124
+ );
125
+ expect(result).toBeDefined();
126
+ expect(result?.ref).toBe("TEST-P99");
127
+ });
128
+
129
+ test("findByRef returns null for non-existent ref", () => {
130
+ const db = getDb();
131
+ const result = findByRef(db, "prds", "NONEXISTENT-P999");
132
+ expect(result).toBeNull();
133
+ });
134
+ });
135
+
136
+ describe("findAll", () => {
137
+ test("finds all records without filter", () => {
138
+ const db = getDb();
139
+
140
+ insert(db, "prds", {
141
+ id: generateId("prd"),
142
+ project_id: projectId,
143
+ ref: "TEST-P1",
144
+ title: "PRD 1",
145
+ status: "DRAFT",
146
+ });
147
+ insert(db, "prds", {
148
+ id: generateId("prd"),
149
+ project_id: projectId,
150
+ ref: "TEST-P2",
151
+ title: "PRD 2",
152
+ status: "APPROVED",
153
+ });
154
+
155
+ const results = findAll<{ title: string }>(db, "prds");
156
+ expect(results.length).toBe(2);
157
+ });
158
+
159
+ test("finds records with where filter", () => {
160
+ const db = getDb();
161
+
162
+ insert(db, "prds", {
163
+ id: generateId("prd"),
164
+ project_id: projectId,
165
+ ref: "TEST-P1",
166
+ title: "PRD 1",
167
+ status: "DRAFT",
168
+ });
169
+ insert(db, "prds", {
170
+ id: generateId("prd"),
171
+ project_id: projectId,
172
+ ref: "TEST-P2",
173
+ title: "PRD 2",
174
+ status: "APPROVED",
175
+ });
176
+
177
+ const results = findAll<{ status: string }>(db, "prds", {
178
+ status: "DRAFT",
179
+ });
180
+ expect(results.length).toBe(1);
181
+ expect(results[0].status).toBe("DRAFT");
182
+ });
183
+
184
+ test("supports pagination with limit", () => {
185
+ const db = getDb();
186
+
187
+ for (let i = 1; i <= 5; i++) {
188
+ insert(db, "prds", {
189
+ id: generateId("prd"),
190
+ project_id: projectId,
191
+ ref: `TEST-P${i}`,
192
+ title: `PRD ${i}`,
193
+ status: "DRAFT",
194
+ });
195
+ }
196
+
197
+ const results = findAll<{ title: string }>(db, "prds", { limit: 3 });
198
+ expect(results.length).toBe(3);
199
+ });
200
+
201
+ test("supports pagination with limit and offset", () => {
202
+ const db = getDb();
203
+
204
+ for (let i = 1; i <= 5; i++) {
205
+ insert(db, "prds", {
206
+ id: generateId("prd"),
207
+ project_id: projectId,
208
+ ref: `TEST-P${i}`,
209
+ title: `PRD ${i}`,
210
+ status: "DRAFT",
211
+ });
212
+ }
213
+
214
+ const page1 = findAll<{ title: string }>(db, "prds", {
215
+ limit: 2,
216
+ offset: 0,
217
+ });
218
+ expect(page1.length).toBe(2);
219
+
220
+ const page2 = findAll<{ title: string }>(db, "prds", {
221
+ limit: 2,
222
+ offset: 2,
223
+ });
224
+ expect(page2.length).toBe(2);
225
+
226
+ const page3 = findAll<{ title: string }>(db, "prds", {
227
+ limit: 2,
228
+ offset: 4,
229
+ });
230
+ expect(page3.length).toBe(1);
231
+ });
232
+
233
+ test("supports where filter with pagination", () => {
234
+ const db = getDb();
235
+
236
+ for (let i = 1; i <= 3; i++) {
237
+ insert(db, "prds", {
238
+ id: generateId("prd"),
239
+ project_id: projectId,
240
+ ref: `TEST-P${i}`,
241
+ title: `Draft PRD ${i}`,
242
+ status: "DRAFT",
243
+ });
244
+ }
245
+ for (let i = 4; i <= 5; i++) {
246
+ insert(db, "prds", {
247
+ id: generateId("prd"),
248
+ project_id: projectId,
249
+ ref: `TEST-P${i}`,
250
+ title: `Approved PRD ${i}`,
251
+ status: "APPROVED",
252
+ });
253
+ }
254
+
255
+ const results = findAll<{ status: string }>(db, "prds", {
256
+ where: { status: "DRAFT" },
257
+ limit: 2,
258
+ });
259
+ expect(results.length).toBe(2);
260
+ expect(results.every((r) => r.status === "DRAFT")).toBe(true);
261
+ });
262
+ });
263
+
264
+ describe("update", () => {
265
+ test("updates record fields", () => {
266
+ const db = getDb();
267
+ const id = generateId("prd");
268
+
269
+ insert(db, "prds", {
270
+ id,
271
+ project_id: projectId,
272
+ ref: "TEST-P1",
273
+ title: "Original Title",
274
+ status: "DRAFT",
275
+ });
276
+
277
+ update(db, "prds", id, { title: "Updated Title", status: "APPROVED" });
278
+
279
+ const result = findById<{ title: string; status: string }>(
280
+ db,
281
+ "prds",
282
+ id,
283
+ );
284
+ expect(result?.title).toBe("Updated Title");
285
+ expect(result?.status).toBe("APPROVED");
286
+ });
287
+
288
+ test("update sets updated_at timestamp", () => {
289
+ const db = getDb();
290
+ const id = generateId("prd");
291
+
292
+ insert(db, "prds", {
293
+ id,
294
+ project_id: projectId,
295
+ ref: "TEST-P1",
296
+ title: "Test",
297
+ status: "DRAFT",
298
+ });
299
+
300
+ const before = findById<{ updated_at: string }>(db, "prds", id);
301
+ const _beforeTime = before?.updated_at;
302
+
303
+ // Small delay to ensure timestamp changes
304
+ update(db, "prds", id, { title: "Updated" });
305
+
306
+ const after = findById<{ updated_at: string }>(db, "prds", id);
307
+ expect(after?.updated_at).toBeDefined();
308
+ // updated_at should be set (may or may not be different from before due to speed)
309
+ });
310
+ });
311
+
312
+ describe("remove", () => {
313
+ test("deletes record by id", () => {
314
+ const db = getDb();
315
+ const id = generateId("prd");
316
+
317
+ insert(db, "prds", {
318
+ id,
319
+ project_id: projectId,
320
+ ref: "TEST-P1",
321
+ title: "Test PRD",
322
+ status: "DRAFT",
323
+ });
324
+
325
+ expect(findById(db, "prds", id)).toBeDefined();
326
+
327
+ remove(db, "prds", id);
328
+
329
+ expect(findById(db, "prds", id)).toBeNull();
330
+ });
331
+ });
332
+
333
+ describe("count", () => {
334
+ test("counts all records without filter", () => {
335
+ const db = getDb();
336
+
337
+ insert(db, "prds", {
338
+ id: generateId("prd"),
339
+ project_id: projectId,
340
+ ref: "TEST-P1",
341
+ title: "PRD 1",
342
+ status: "DRAFT",
343
+ });
344
+ insert(db, "prds", {
345
+ id: generateId("prd"),
346
+ project_id: projectId,
347
+ ref: "TEST-P2",
348
+ title: "PRD 2",
349
+ status: "APPROVED",
350
+ });
351
+
352
+ expect(count(db, "prds")).toBe(2);
353
+ });
354
+
355
+ test("counts records with filter", () => {
356
+ const db = getDb();
357
+
358
+ insert(db, "prds", {
359
+ id: generateId("prd"),
360
+ project_id: projectId,
361
+ ref: "TEST-P1",
362
+ title: "PRD 1",
363
+ status: "DRAFT",
364
+ });
365
+ insert(db, "prds", {
366
+ id: generateId("prd"),
367
+ project_id: projectId,
368
+ ref: "TEST-P2",
369
+ title: "PRD 2",
370
+ status: "APPROVED",
371
+ });
372
+
373
+ expect(count(db, "prds", { status: "DRAFT" })).toBe(1);
374
+ expect(count(db, "prds", { status: "APPROVED" })).toBe(1);
375
+ expect(count(db, "prds", { status: "COMPLETED" })).toBe(0);
376
+ });
377
+ });
378
+
379
+ describe("generateRef", () => {
380
+ test("generates sequential refs starting from 1", () => {
381
+ const db = getDb();
382
+ const ref = generateRef(db, "P", "TEST");
383
+ expect(ref).toBe("TEST-P1");
384
+ });
385
+
386
+ test("generates next sequential ref based on existing records", () => {
387
+ const db = getDb();
388
+
389
+ insert(db, "prds", {
390
+ id: generateId("prd"),
391
+ project_id: projectId,
392
+ ref: "TEST-P1",
393
+ title: "PRD 1",
394
+ status: "DRAFT",
395
+ });
396
+
397
+ const ref = generateRef(db, "P", "TEST");
398
+ expect(ref).toBe("TEST-P2");
399
+ });
400
+
401
+ test("handles gaps in ref sequence by using max ref number", () => {
402
+ const db = getDb();
403
+
404
+ const prdId = generateId("prd");
405
+ insert(db, "prds", {
406
+ id: prdId,
407
+ project_id: projectId,
408
+ ref: "TEST-P1",
409
+ title: "PRD",
410
+ status: "DRAFT",
411
+ });
412
+ const epicId = generateId("epic");
413
+ insert(db, "epics", {
414
+ id: epicId,
415
+ prd_id: prdId,
416
+ ref: "TEST-E1",
417
+ title: "Epic",
418
+ status: "PENDING",
419
+ });
420
+
421
+ insert(db, "tasks", {
422
+ id: generateId("task"),
423
+ epic_id: epicId,
424
+ ref: "TEST-T1",
425
+ title: "Task 1",
426
+ status: "PENDING",
427
+ priority: "MEDIUM",
428
+ });
429
+ insert(db, "tasks", {
430
+ id: generateId("task"),
431
+ epic_id: epicId,
432
+ ref: "TEST-T5",
433
+ title: "Task 5",
434
+ status: "PENDING",
435
+ priority: "MEDIUM",
436
+ });
437
+ insert(db, "tasks", {
438
+ id: generateId("task"),
439
+ epic_id: epicId,
440
+ ref: "TEST-T10",
441
+ title: "Task 10",
442
+ status: "PENDING",
443
+ priority: "MEDIUM",
444
+ });
445
+
446
+ const ref = generateRef(db, "T", "TEST");
447
+ expect(ref).toBe("TEST-T11");
448
+ });
449
+
450
+ test("handles different prefixes independently", () => {
451
+ const db = getDb();
452
+
453
+ insert(db, "prds", {
454
+ id: generateId("prd"),
455
+ project_id: projectId,
456
+ ref: "ABC-P5",
457
+ title: "PRD",
458
+ status: "DRAFT",
459
+ });
460
+ insert(db, "prds", {
461
+ id: generateId("prd"),
462
+ project_id: projectId,
463
+ ref: "XYZ-P10",
464
+ title: "PRD",
465
+ status: "DRAFT",
466
+ });
467
+
468
+ expect(generateRef(db, "P", "ABC")).toBe("ABC-P6");
469
+ expect(generateRef(db, "P", "XYZ")).toBe("XYZ-P11");
470
+ expect(generateRef(db, "P", "NEW")).toBe("NEW-P1");
471
+ });
472
+ });
473
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Generate a unique ID with optional type prefix.
3
+ * IDs are sortable by creation time and include randomness for uniqueness.
4
+ *
5
+ * @param prefix - Optional type prefix (e.g., "prd", "epic", "task")
6
+ * @returns Unique ID string
7
+ *
8
+ * @example
9
+ * generateId("prd") // -> "prd_m1abc123xyz"
10
+ * generateId("epic") // -> "epic_m1abc456def"
11
+ * generateId("task") // -> "task_m1abc789ghi"
12
+ */
13
+ export function generateId(prefix: string = ""): string {
14
+ const timestamp = Date.now().toString(36);
15
+ const random = Math.random().toString(36).substring(2, 10);
16
+ return prefix ? `${prefix}_${timestamp}${random}` : `${timestamp}${random}`;
17
+ }
@@ -0,0 +1,69 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { config } from "../config.js";
3
+ import { logger } from "../utils/logger.js";
4
+ import { SCHEMA } from "./schema.js";
5
+ import { Database } from "./sqlite.js";
6
+
7
+ let db: Database | null = null;
8
+
9
+ /**
10
+ * Initialize the database, creating the .flux directory and schema if needed.
11
+ * Returns a singleton database instance.
12
+ */
13
+ export function initDb(): Database {
14
+ if (db) {
15
+ return db;
16
+ }
17
+
18
+ // Create .flux directory if it doesn't exist
19
+ if (!existsSync(config.fluxPath)) {
20
+ logger.info(`Creating flux directory: ${config.fluxPath}`);
21
+ mkdirSync(config.fluxPath, { recursive: true });
22
+ }
23
+
24
+ // Open or create database
25
+ logger.info(`Opening database: ${config.dbPath}`);
26
+ db = new Database(config.dbPath);
27
+
28
+ // Enable foreign keys
29
+ db.run("PRAGMA foreign_keys = ON");
30
+
31
+ // Initialize schema
32
+ db.exec(SCHEMA);
33
+
34
+ logger.info("Database initialized successfully");
35
+ return db;
36
+ }
37
+
38
+ /**
39
+ * Get the database instance. Throws if not initialized.
40
+ */
41
+ export function getDb(): Database {
42
+ if (!db) {
43
+ throw new Error("Database not initialized. Call initDb() first.");
44
+ }
45
+ return db;
46
+ }
47
+
48
+ /**
49
+ * Close the database connection.
50
+ */
51
+ export function closeDb(): void {
52
+ if (db) {
53
+ db.close();
54
+ db = null;
55
+ logger.info("Database connection closed");
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Check if a Flux project exists in the current directory.
61
+ */
62
+ export function fluxProjectExists(): boolean {
63
+ return existsSync(config.fluxPath) && existsSync(config.dbPath);
64
+ }
65
+
66
+ // Re-export utilities for convenience
67
+ export { generateId } from "./ids.js";
68
+ export * from "./queries.js";
69
+ export { generatePrefix, generateRef } from "./refs.js";
@@ -0,0 +1,142 @@
1
+ import type { Database, SQLQueryBindings } from "./sqlite.js";
2
+
3
+ /**
4
+ * Find a record by its ref field.
5
+ */
6
+ export function findByRef<T>(
7
+ db: Database,
8
+ table: string,
9
+ ref: string,
10
+ ): T | null {
11
+ return db.query(`SELECT * FROM ${table} WHERE ref = ?`).get(ref) as T | null;
12
+ }
13
+
14
+ /**
15
+ * Find a record by its id field.
16
+ */
17
+ export function findById<T>(db: Database, table: string, id: string): T | null {
18
+ return db.query(`SELECT * FROM ${table} WHERE id = ?`).get(id) as T | null;
19
+ }
20
+
21
+ interface FindAllOptions {
22
+ where?: Record<string, unknown>;
23
+ limit?: number;
24
+ offset?: number;
25
+ }
26
+
27
+ /**
28
+ * Find all records in a table, optionally filtered with pagination.
29
+ */
30
+ export function findAll<T>(
31
+ db: Database,
32
+ table: string,
33
+ whereOrOptions?: Record<string, unknown> | FindAllOptions,
34
+ ): T[] {
35
+ // Handle both old signature (where object) and new signature (options object)
36
+ let where: Record<string, unknown> | undefined;
37
+ let limit: number | undefined;
38
+ let offset: number | undefined;
39
+
40
+ if (whereOrOptions) {
41
+ if (
42
+ "where" in whereOrOptions ||
43
+ "limit" in whereOrOptions ||
44
+ "offset" in whereOrOptions
45
+ ) {
46
+ const opts = whereOrOptions as FindAllOptions;
47
+ where = opts.where;
48
+ limit = opts.limit;
49
+ offset = opts.offset;
50
+ } else {
51
+ where = whereOrOptions as Record<string, unknown>;
52
+ }
53
+ }
54
+
55
+ let sql = `SELECT * FROM ${table}`;
56
+ const values: SQLQueryBindings[] = [];
57
+
58
+ if (where && Object.keys(where).length > 0) {
59
+ const conditions = Object.keys(where)
60
+ .map((k) => `${k} = ?`)
61
+ .join(" AND ");
62
+ sql += ` WHERE ${conditions}`;
63
+ values.push(...(Object.values(where) as SQLQueryBindings[]));
64
+ }
65
+
66
+ if (limit !== undefined) {
67
+ sql += ` LIMIT ?`;
68
+ values.push(limit);
69
+ }
70
+
71
+ if (offset !== undefined && offset > 0) {
72
+ sql += ` OFFSET ?`;
73
+ values.push(offset);
74
+ }
75
+
76
+ return db.query(sql).all(...values) as T[];
77
+ }
78
+
79
+ /**
80
+ * Insert a new record into a table.
81
+ */
82
+ export function insert(
83
+ db: Database,
84
+ table: string,
85
+ data: Record<string, unknown>,
86
+ ): void {
87
+ const keys = Object.keys(data);
88
+ const placeholders = keys.map(() => "?").join(", ");
89
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
90
+ const values = Object.values(data) as SQLQueryBindings[];
91
+ db.query(sql).run(...values);
92
+ }
93
+
94
+ /**
95
+ * Update an existing record by id.
96
+ */
97
+ export function update(
98
+ db: Database,
99
+ table: string,
100
+ id: string,
101
+ data: Record<string, unknown>,
102
+ ): void {
103
+ const sets = Object.keys(data)
104
+ .map((k) => `${k} = ?`)
105
+ .join(", ");
106
+ const sql = `UPDATE ${table} SET ${sets}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
107
+ const values = [...Object.values(data), id] as SQLQueryBindings[];
108
+ db.query(sql).run(...values);
109
+ }
110
+
111
+ /**
112
+ * Delete a record by id.
113
+ */
114
+ export function remove(db: Database, table: string, id: string): void {
115
+ db.query(`DELETE FROM ${table} WHERE id = ?`).run(id);
116
+ }
117
+
118
+ /**
119
+ * Count records in a table, optionally filtered.
120
+ */
121
+ export function count(
122
+ db: Database,
123
+ table: string,
124
+ where?: Record<string, unknown>,
125
+ ): number {
126
+ if (!where || Object.keys(where).length === 0) {
127
+ const result = db.query(`SELECT COUNT(*) as count FROM ${table}`).get() as {
128
+ count: number;
129
+ };
130
+ return result.count;
131
+ }
132
+
133
+ const conditions = Object.keys(where)
134
+ .map((k) => `${k} = ?`)
135
+ .join(" AND ");
136
+ const sql = `SELECT COUNT(*) as count FROM ${table} WHERE ${conditions}`;
137
+ const values = Object.values(where) as SQLQueryBindings[];
138
+ const result = db.query(sql).get(...values) as {
139
+ count: number;
140
+ };
141
+ return result.count;
142
+ }