@rashidee/co2 1.2.5

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 (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. package/static/index.html +24 -0
@@ -0,0 +1,750 @@
1
+ # Local Database Patterns — better-sqlite3 + Drizzle ORM
2
+
3
+ This reference describes the embedded SQLite persistence layer for CLI applications that
4
+ need structured, queryable, relational local data. Include relevant content in Section 16
5
+ of the generated specification.
6
+
7
+ ---
8
+
9
+ ## When to Use Each Persistence Layer
10
+
11
+ ```
12
+ ┌──────────────────────────────────────────────────────────────────────────┐
13
+ │ Decision guide │
14
+ │ │
15
+ │ conf (key-value) — user preferences, tokens, single values │
16
+ │ e.g. apiToken, defaultEnv, telemetryEnabled │
17
+ │ │
18
+ │ cosmiconfig (file) — project-level config, team conventions │
19
+ │ e.g. .mytoolrc, deploy targets, service list │
20
+ │ │
21
+ │ SQLite (this reference) — structured records, history, relationships │
22
+ │ e.g. run history, cached API results, task queues, audit logs, │
23
+ │ local entity store, sync state, multi-row queryable data │
24
+ └──────────────────────────────────────────────────────────────────────────┘
25
+ ```
26
+
27
+ **Use SQLite when the data has any of these characteristics:**
28
+ - Multiple rows of the same entity type (history entries, cached records, tasks)
29
+ - Queries involving filtering, sorting, or aggregation
30
+ - Relationships between entity types (foreign keys)
31
+ - Data that grows over time and needs pruning
32
+ - Need for ACID transactions across multiple writes
33
+
34
+ **Do NOT use SQLite for:** API tokens, simple boolean flags, last-used values — those
35
+ belong in `conf`.
36
+
37
+ ---
38
+
39
+ ## Technology Stack
40
+
41
+ | Package | Role | Version |
42
+ |------------------|-----------------------------------------------|---------|
43
+ | `better-sqlite3` | Synchronous SQLite driver for Node.js | 11.x |
44
+ | `drizzle-orm` | TypeScript-first ORM (schema + query builder) | 0.36.x |
45
+ | `env-paths` | OS-correct data directory resolution | 3.x |
46
+ | `drizzle-kit` | Migration generator & CLI (devDependency) | 0.28.x |
47
+ | `@types/better-sqlite3` | Type declarations | 7.x |
48
+
49
+ ### Why better-sqlite3 (synchronous) over other options
50
+
51
+ CLI commands are inherently sequential. Every command action is a single invocation that
52
+ runs top-to-bottom. The synchronous API of `better-sqlite3` maps perfectly to this model:
53
+
54
+ ```typescript
55
+ // Synchronous — reads and writes without async/await ceremony
56
+ const runs = db.prepare('SELECT * FROM runs ORDER BY started_at DESC LIMIT 10').all()
57
+
58
+ // vs async alternatives that add complexity with no benefit in a CLI:
59
+ const runs = await db.query.runs.findMany({ limit: 10 })
60
+ ```
61
+
62
+ Using the synchronous driver also means no risk of forgetting to `await` a query inside
63
+ a command action handler.
64
+
65
+ ### Why Drizzle over raw SQL or Kysely
66
+
67
+ - Schema defined in TypeScript → full type inference on query results
68
+ - Migrations generated automatically by `drizzle-kit generate`
69
+ - No runtime schema validation overhead (unlike Prisma's engine model)
70
+ - Works natively with `better-sqlite3`'s synchronous API
71
+
72
+ ---
73
+
74
+ ## Database File Location
75
+
76
+ The database file must live in the OS-appropriate **data directory** — not the config
77
+ directory (`conf` uses) and not the current working directory.
78
+
79
+ ```typescript
80
+ // src/db/path.ts
81
+ import envPaths from 'env-paths'
82
+ import { join } from 'node:path'
83
+ import { mkdirSync } from 'node:fs'
84
+
85
+ /**
86
+ * Returns the absolute path to the application's SQLite database file.
87
+ * Creates the data directory if it does not exist.
88
+ *
89
+ * OS locations:
90
+ * macOS: ~/Library/Application Support/{{BINARY_NAME}}/data.db
91
+ * Linux: ~/.local/share/{{BINARY_NAME}}/data.db
92
+ * Windows: %APPDATA%\{{BINARY_NAME}}\data.db
93
+ */
94
+ export function getDatabasePath(): string {
95
+ const paths = envPaths('{{BINARY_NAME}}', { suffix: '' })
96
+ mkdirSync(paths.data, { recursive: true })
97
+ return join(paths.data, 'data.db')
98
+ }
99
+ ```
100
+
101
+ > **`env-paths` is ESM-only (v3+).** Import it with `.js` extension and ensure
102
+ > `"type": "module"` is set in `package.json`.
103
+
104
+ ---
105
+
106
+ ## Schema Definition
107
+
108
+ Drizzle schemas live in `src/db/schema.ts`. Each table is a typed constant. Derive
109
+ exact table names and columns from the `model/<command>/model.md` context files.
110
+
111
+ ```typescript
112
+ // src/db/schema.ts
113
+ import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'
114
+
115
+ // ── Example: command run history ─────────────────────────────────────────────
116
+
117
+ export const runs = sqliteTable('runs', {
118
+ id: text('id').primaryKey(), // UUIDv4 — generated in service
119
+ command: text('command').notNull(), // e.g. 'deploy'
120
+ status: text('status', {
121
+ enum: ['running', 'success', 'failed', 'cancelled'],
122
+ }).notNull().default('running'),
123
+ exitCode: integer('exit_code'),
124
+ startedAt: integer('started_at', { mode: 'timestamp' }).notNull(),
125
+ finishedAt: integer('finished_at', { mode: 'timestamp' }),
126
+ durationMs: integer('duration_ms'),
127
+ environment: text('environment'),
128
+ notes: text('notes'),
129
+ })
130
+
131
+ // ── Example: cached remote resources ─────────────────────────────────────────
132
+
133
+ export const resourceCache = sqliteTable('resource_cache', {
134
+ id: text('id').primaryKey(),
135
+ resourceType: text('resource_type').notNull(),
136
+ externalId: text('external_id').notNull(),
137
+ payload: text('payload').notNull(), // JSON-serialised
138
+ cachedAt: integer('cached_at', { mode: 'timestamp' }).notNull(),
139
+ expiresAt: integer('expires_at', { mode: 'timestamp' }),
140
+ })
141
+
142
+ // ── Exported schema map (used by drizzle-kit and by the client) ──────────────
143
+
144
+ export const schema = { runs, resourceCache }
145
+ ```
146
+
147
+ **Timestamp columns** use `integer` with `mode: 'timestamp'` — Drizzle maps these to
148
+ JavaScript `Date` objects automatically. SQLite stores them as Unix epoch integers.
149
+
150
+ **JSON columns** are stored as `text` and serialised/deserialised in the repository layer.
151
+ Never store raw objects — always `JSON.stringify` before insert and `JSON.parse` after read.
152
+
153
+ ---
154
+
155
+ ## Database Client (Singleton)
156
+
157
+ ```typescript
158
+ // src/db/client.ts
159
+ import Database from 'better-sqlite3'
160
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
161
+ import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
162
+ import { join, dirname } from 'node:path'
163
+ import { fileURLToPath } from 'node:url'
164
+ import { getDatabasePath } from './path.js'
165
+ import { schema } from './schema.js'
166
+
167
+ const __filename = fileURLToPath(import.meta.url)
168
+ const __dirname = dirname(__filename)
169
+
170
+ export type DbClient = ReturnType<typeof createDbClient>
171
+
172
+ let _db: DbClient | null = null
173
+
174
+ /**
175
+ * Create a Drizzle client over a better-sqlite3 connection.
176
+ * Exposed for testing (pass a custom dbPath to use an in-memory db).
177
+ */
178
+ export function createDbClient(dbPath = getDatabasePath()) {
179
+ const sqlite = new Database(dbPath)
180
+
181
+ // Enable WAL mode for better concurrent read performance
182
+ // (safe for CLI — multiple terminal tabs may read simultaneously)
183
+ sqlite.pragma('journal_mode = WAL')
184
+ sqlite.pragma('foreign_keys = ON')
185
+ sqlite.pragma('synchronous = NORMAL') // safe with WAL; faster than FULL
186
+
187
+ const db = drizzle(sqlite, { schema })
188
+
189
+ // Run pending migrations automatically on first use
190
+ const migrationsFolder = join(__dirname, '../../drizzle')
191
+ migrate(db, { migrationsFolder })
192
+
193
+ return db
194
+ }
195
+
196
+ /**
197
+ * Returns the application-wide singleton DB client.
198
+ * Opens the database and runs migrations on first call.
199
+ */
200
+ export function getDb(): DbClient {
201
+ _db ??= createDbClient()
202
+ return _db
203
+ }
204
+
205
+ /**
206
+ * Close the database connection. Call in tests and before process.exit.
207
+ */
208
+ export function closeDb(): void {
209
+ // Drizzle doesn't expose a close method; access the underlying SQLite instance
210
+ if (_db) {
211
+ // Cast to access the underlying Database instance
212
+ const sqlite = (_db as unknown as { session: { db: InstanceType<typeof Database> } }).session.db
213
+ sqlite.close()
214
+ _db = null
215
+ }
216
+ }
217
+ ```
218
+
219
+ > **Migrations run automatically** every time the CLI starts. Drizzle's migrator is
220
+ > idempotent — it tracks applied migrations in the `__drizzle_migrations` table and
221
+ > only applies new ones. There is no startup penalty once all migrations are applied.
222
+
223
+ ---
224
+
225
+ ## Repository Pattern
226
+
227
+ Each logical entity gets a repository class that encapsulates all queries. Services
228
+ inject the repository through their constructor, enabling clean unit testing with mocks.
229
+
230
+ ```typescript
231
+ // src/db/repositories/runs.repository.ts
232
+ import { eq, desc, and, gte } from 'drizzle-orm'
233
+ import { randomUUID } from 'node:crypto'
234
+ import type { DbClient } from '../client.js'
235
+ import { runs } from '../schema.js'
236
+
237
+ export interface RunRecord {
238
+ id: string
239
+ command: string
240
+ status: 'running' | 'success' | 'failed' | 'cancelled'
241
+ exitCode: number | null
242
+ startedAt: Date
243
+ finishedAt: Date | null
244
+ durationMs: number | null
245
+ environment: string | null
246
+ notes: string | null
247
+ }
248
+
249
+ export interface CreateRunInput {
250
+ command: string
251
+ environment?: string
252
+ }
253
+
254
+ export interface CompleteRunInput {
255
+ id: string
256
+ status: 'success' | 'failed' | 'cancelled'
257
+ exitCode?: number
258
+ notes?: string
259
+ }
260
+
261
+ export class RunsRepository {
262
+ constructor(private readonly db: DbClient) {}
263
+
264
+ create(input: CreateRunInput): RunRecord {
265
+ const id = randomUUID()
266
+ const now = new Date()
267
+
268
+ this.db.insert(runs).values({
269
+ id,
270
+ command: input.command,
271
+ status: 'running',
272
+ startedAt: now,
273
+ environment: input.environment ?? null,
274
+ exitCode: null,
275
+ finishedAt: null,
276
+ durationMs: null,
277
+ notes: null,
278
+ }).run()
279
+
280
+ return this.findById(id)!
281
+ }
282
+
283
+ complete(input: CompleteRunInput): RunRecord {
284
+ const now = new Date()
285
+ const existing = this.findById(input.id)
286
+ if (!existing) throw new Error(`Run not found: ${input.id}`)
287
+
288
+ const durationMs = now.getTime() - existing.startedAt.getTime()
289
+
290
+ this.db.update(runs)
291
+ .set({
292
+ status: input.status,
293
+ exitCode: input.exitCode ?? null,
294
+ finishedAt: now,
295
+ durationMs,
296
+ notes: input.notes ?? null,
297
+ })
298
+ .where(eq(runs.id, input.id))
299
+ .run()
300
+
301
+ return this.findById(input.id)!
302
+ }
303
+
304
+ findById(id: string): RunRecord | undefined {
305
+ return this.db.select().from(runs).where(eq(runs.id, id)).get()
306
+ }
307
+
308
+ listRecent(limit = 20): RunRecord[] {
309
+ return this.db.select()
310
+ .from(runs)
311
+ .orderBy(desc(runs.startedAt))
312
+ .limit(limit)
313
+ .all()
314
+ }
315
+
316
+ listByCommand(command: string, limit = 20): RunRecord[] {
317
+ return this.db.select()
318
+ .from(runs)
319
+ .where(eq(runs.command, command))
320
+ .orderBy(desc(runs.startedAt))
321
+ .limit(limit)
322
+ .all()
323
+ }
324
+
325
+ countSince(since: Date): number {
326
+ const result = this.db.select({ count: runs.id })
327
+ .from(runs)
328
+ .where(gte(runs.startedAt, since))
329
+ .all()
330
+ return result.length
331
+ }
332
+
333
+ deleteOlderThan(before: Date): number {
334
+ const result = this.db.delete(runs)
335
+ .where(and(
336
+ // Keep running entries — only delete terminal states
337
+ eq(runs.status, 'success'),
338
+ ))
339
+ .run()
340
+ return result.changes
341
+ }
342
+ }
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Service Integration
348
+
349
+ Services receive the repository via constructor injection:
350
+
351
+ ```typescript
352
+ // src/services/history.service.ts
353
+ import { getDb } from '../db/client.js'
354
+ import { RunsRepository } from '../db/repositories/runs.repository.js'
355
+ import type { RunRecord } from '../db/repositories/runs.repository.js'
356
+
357
+ export interface HistoryListOptions {
358
+ command?: string
359
+ limit: number
360
+ }
361
+
362
+ export class HistoryService {
363
+ private readonly runsRepo: RunsRepository
364
+
365
+ constructor(db = getDb()) {
366
+ this.runsRepo = new RunsRepository(db)
367
+ }
368
+
369
+ list(options: HistoryListOptions): RunRecord[] {
370
+ if (options.command) {
371
+ return this.runsRepo.listByCommand(options.command, options.limit)
372
+ }
373
+ return this.runsRepo.listRecent(options.limit)
374
+ }
375
+
376
+ recordStart(command: string, environment?: string): RunRecord {
377
+ return this.runsRepo.create({ command, environment })
378
+ }
379
+
380
+ recordComplete(id: string, exitCode: number): RunRecord {
381
+ return this.runsRepo.complete({
382
+ id,
383
+ status: exitCode === 0 ? 'success' : 'failed',
384
+ exitCode,
385
+ })
386
+ }
387
+
388
+ pruneOlderThanDays(days: number): number {
389
+ const cutoff = new Date()
390
+ cutoff.setDate(cutoff.getDate() - days)
391
+ return this.runsRepo.deleteOlderThan(cutoff)
392
+ }
393
+ }
394
+ ```
395
+
396
+ **The key rules:**
397
+ - Services call repositories; they never use raw SQL
398
+ - Services contain all business logic (filtering, transformation, aggregation)
399
+ - Services return typed data — never raw Drizzle result objects in public APIs
400
+
401
+ ---
402
+
403
+ ## Transactions
404
+
405
+ Use `db.transaction()` for operations that must be atomic:
406
+
407
+ ```typescript
408
+ // Inside a repository or service
409
+ import type { DbClient } from '../db/client.js'
410
+
411
+ export class SyncRepository {
412
+ constructor(private readonly db: DbClient) {}
413
+
414
+ applySync(items: SyncItem[]): void {
415
+ // All inserts/updates happen atomically — partial failures roll back
416
+ this.db.transaction((tx) => {
417
+ for (const item of items) {
418
+ tx.insert(resourceCache).values({
419
+ id: randomUUID(),
420
+ resourceType: item.type,
421
+ externalId: item.id,
422
+ payload: JSON.stringify(item.data),
423
+ cachedAt: new Date(),
424
+ expiresAt: item.expiresAt ?? null,
425
+ }).run()
426
+ }
427
+ })
428
+ }
429
+ }
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Migration Workflow
435
+
436
+ ### drizzle.config.ts
437
+
438
+ ```typescript
439
+ import type { Config } from 'drizzle-kit'
440
+
441
+ export default {
442
+ schema: './src/db/schema.ts',
443
+ out: './drizzle',
444
+ dialect: 'sqlite',
445
+ dbCredentials: {
446
+ url: './dev.db', // local dev database only — production uses OS data dir
447
+ },
448
+ } satisfies Config
449
+ ```
450
+
451
+ ### Migration scripts in package.json
452
+
453
+ ```json
454
+ {
455
+ "scripts": {
456
+ "db:generate": "drizzle-kit generate",
457
+ "db:migrate": "drizzle-kit migrate",
458
+ "db:studio": "drizzle-kit studio",
459
+ "db:push": "drizzle-kit push"
460
+ }
461
+ }
462
+ ```
463
+
464
+ ### Workflow for schema changes
465
+
466
+ ```bash
467
+ # 1. Edit src/db/schema.ts — add/modify tables or columns
468
+ # 2. Generate a new migration SQL file
469
+ npm run db:generate
470
+ # → creates drizzle/0001_add_duration_column.sql
471
+
472
+ # 3. Commit the migration file alongside the schema change
473
+ git add drizzle/ src/db/schema.ts
474
+
475
+ # 4. On next CLI run, the migrator applies it automatically
476
+ ```
477
+
478
+ **Migration files are committed to source control.** Never edit generated `.sql` files
479
+ after commit — always generate new ones with `db:generate`.
480
+
481
+ ### The `drizzle/` folder structure
482
+
483
+ ```
484
+ drizzle/
485
+ ├── 0000_initial_schema.sql
486
+ ├── 0001_add_duration_column.sql
487
+ ├── 0002_add_resource_cache.sql
488
+ └── meta/
489
+ ├── _journal.json ← drizzle-kit tracking — commit this
490
+ ├── 0000_snapshot.json
491
+ └── 0001_snapshot.json
492
+ ```
493
+
494
+ ---
495
+
496
+ ## Directory Structure Additions
497
+
498
+ When Local Database = yes, add these paths to the project structure:
499
+
500
+ ```
501
+ src/
502
+ └── db/
503
+ ├── client.ts ← Database singleton + migration runner
504
+ ├── path.ts ← OS data directory resolution
505
+ ├── schema.ts ← Drizzle table definitions (all tables)
506
+ └── repositories/
507
+ ├── runs.repository.ts
508
+ └── cache.repository.ts
509
+
510
+ drizzle/ ← Generated SQL migrations (committed to git)
511
+ ├── 0000_initial_schema.sql
512
+ └── meta/
513
+ └── _journal.json
514
+
515
+ dev.db ← Local dev database (gitignored)
516
+ ```
517
+
518
+ Add to `.gitignore`:
519
+ ```gitignore
520
+ # Local development database
521
+ dev.db
522
+ dev.db-shm
523
+ dev.db-wal
524
+
525
+ # Drizzle studio temp
526
+ .drizzle/
527
+ ```
528
+
529
+ ---
530
+
531
+ ## pkg / Binary Packaging Compatibility
532
+
533
+ When `Binary Packaging = yes`, the migration files must be available at runtime inside
534
+ the packaged binary. Add them to the `pkg.assets` array:
535
+
536
+ ```json
537
+ {
538
+ "pkg": {
539
+ "scripts": "dist/cjs/cli.cjs",
540
+ "assets": [
541
+ "drizzle/**/*"
542
+ ]
543
+ }
544
+ }
545
+ ```
546
+
547
+ The `getDatabasePath()` function uses `env-paths` which resolves to a **writable**
548
+ OS directory — the database file itself is never bundled, only the migration SQL files.
549
+
550
+ For the migration runner inside the binary, resolve the migrations path from
551
+ `import.meta.url` using a snapshot-safe path:
552
+
553
+ ```typescript
554
+ // src/db/client.ts — pkg-safe migration path resolution
555
+ import { fileURLToPath } from 'node:url'
556
+ import { dirname, join } from 'node:path'
557
+
558
+ const __filename = fileURLToPath(import.meta.url)
559
+ const __dirname = dirname(__filename)
560
+
561
+ // Works in both development (src/) and packaged binary (dist/cjs/)
562
+ // because drizzle/ is bundled as an asset relative to the compiled output
563
+ const migrationsFolder = join(__dirname, '../../drizzle')
564
+ ```
565
+
566
+ ---
567
+
568
+ ## Testing
569
+
570
+ Use an **in-memory SQLite database** for all tests. Pass a custom path to `createDbClient`:
571
+
572
+ ```typescript
573
+ // test/helpers/db.ts
574
+ import { createDbClient } from '../../src/db/client.js'
575
+ import type { DbClient } from '../../src/db/client.js'
576
+
577
+ /**
578
+ * Create an isolated in-memory database for a single test.
579
+ * Migrations are applied automatically.
580
+ * ':memory:' means the database is discarded when the connection closes.
581
+ */
582
+ export function createTestDb(): DbClient {
583
+ return createDbClient(':memory:')
584
+ }
585
+ ```
586
+
587
+ ### Repository unit test
588
+
589
+ ```typescript
590
+ // test/unit/db/runs.repository.test.ts
591
+ import { describe, it, expect, beforeEach } from 'vitest'
592
+ import { createTestDb } from '../../helpers/db.js'
593
+ import { RunsRepository } from '../../../src/db/repositories/runs.repository.js'
594
+ import type { DbClient } from '../../../src/db/client.js'
595
+
596
+ describe('RunsRepository', () => {
597
+ let db: DbClient
598
+ let repo: RunsRepository
599
+
600
+ beforeEach(() => {
601
+ db = createTestDb()
602
+ repo = new RunsRepository(db)
603
+ })
604
+
605
+ it('creates a run with status "running"', () => {
606
+ const run = repo.create({ command: 'deploy', environment: 'staging' })
607
+
608
+ expect(run.id).toMatch(/^[0-9a-f-]{36}$/) // UUID format
609
+ expect(run.status).toBe('running')
610
+ expect(run.command).toBe('deploy')
611
+ expect(run.environment).toBe('staging')
612
+ expect(run.finishedAt).toBeNull()
613
+ })
614
+
615
+ it('completes a run and sets durationMs', () => {
616
+ const run = repo.create({ command: 'deploy' })
617
+
618
+ // Small delay so duration is non-zero
619
+ const completed = repo.complete({ id: run.id, status: 'success', exitCode: 0 })
620
+
621
+ expect(completed.status).toBe('success')
622
+ expect(completed.exitCode).toBe(0)
623
+ expect(completed.finishedAt).toBeInstanceOf(Date)
624
+ expect(completed.durationMs).toBeGreaterThanOrEqual(0)
625
+ })
626
+
627
+ it('listRecent returns entries newest-first', () => {
628
+ repo.create({ command: 'init' })
629
+ repo.create({ command: 'deploy' })
630
+
631
+ const results = repo.listRecent(10)
632
+
633
+ expect(results[0]?.command).toBe('deploy') // newest first
634
+ expect(results[1]?.command).toBe('init')
635
+ })
636
+
637
+ it('listByCommand filters by command name', () => {
638
+ repo.create({ command: 'init' })
639
+ repo.create({ command: 'deploy' })
640
+ repo.create({ command: 'deploy' })
641
+
642
+ const deploys = repo.listByCommand('deploy')
643
+
644
+ expect(deploys).toHaveLength(2)
645
+ expect(deploys.every(r => r.command === 'deploy')).toBe(true)
646
+ })
647
+
648
+ it('throws when completing a non-existent run', () => {
649
+ expect(() => repo.complete({ id: 'nonexistent', status: 'success' }))
650
+ .toThrow('Run not found')
651
+ })
652
+ })
653
+ ```
654
+
655
+ ### Service unit test with injected test DB
656
+
657
+ ```typescript
658
+ // test/unit/services/history.service.test.ts
659
+ import { describe, it, expect, beforeEach } from 'vitest'
660
+ import { createTestDb } from '../../helpers/db.js'
661
+ import { HistoryService } from '../../../src/services/history.service.js'
662
+
663
+ describe('HistoryService', () => {
664
+ let service: HistoryService
665
+
666
+ beforeEach(() => {
667
+ const db = createTestDb()
668
+ service = new HistoryService(db) // inject test DB
669
+ })
670
+
671
+ it('lists no runs on empty database', () => {
672
+ expect(service.list({ limit: 10 })).toHaveLength(0)
673
+ })
674
+
675
+ it('records a run start and completion lifecycle', () => {
676
+ const run = service.recordStart('deploy', 'staging')
677
+ expect(run.status).toBe('running')
678
+
679
+ const completed = service.recordComplete(run.id, 0)
680
+ expect(completed.status).toBe('success')
681
+ expect(completed.exitCode).toBe(0)
682
+ })
683
+
684
+ it('pruneOlderThanDays removes old successful runs', () => {
685
+ // Manufacture a run that finished 31 days ago
686
+ // (use repo directly to backdate, or test via count)
687
+ service.recordStart('deploy')
688
+ // In a real test, you'd use a repository directly with a backdated timestamp
689
+ const deleted = service.pruneOlderThanDays(30)
690
+ expect(typeof deleted).toBe('number')
691
+ })
692
+ })
693
+ ```
694
+
695
+ ---
696
+
697
+ ## Operational Notes
698
+
699
+ ### WAL mode explanation
700
+
701
+ ```
702
+ sqlite.pragma('journal_mode = WAL')
703
+ ```
704
+
705
+ WAL (Write-Ahead Log) allows concurrent readers while a write is in progress. Without it,
706
+ a write locks the entire database. For a CLI this matters when:
707
+ - Two terminal tabs run commands simultaneously
708
+ - A long-running background daemon holds a connection while the user runs a query
709
+
710
+ ### Database inspection during development
711
+
712
+ ```bash
713
+ # Drizzle Studio — web UI to browse your SQLite data
714
+ npm run db:studio
715
+
716
+ # SQLite CLI (if installed)
717
+ sqlite3 ~/Library/Application\ Support/my-tool/data.db
718
+ .tables
719
+ .schema runs
720
+ SELECT * FROM runs ORDER BY started_at DESC LIMIT 5;
721
+ ```
722
+
723
+ ### Database size management
724
+
725
+ For long-lived CLIs (e.g. CI tools), add an automatic pruning step:
726
+
727
+ ```typescript
728
+ // src/cli.ts — call once at startup, after update check
729
+ import { HistoryService } from './services/history.service.js'
730
+
731
+ const retention = userConfig.get('historyRetentionDays') ?? 90
732
+ new HistoryService().pruneOlderThanDays(retention)
733
+ ```
734
+
735
+ ### Common mistake — forgetting `.run()` on write statements
736
+
737
+ Drizzle for `better-sqlite3` returns a prepared statement object. You must call
738
+ `.run()` to execute inserts/updates/deletes:
739
+
740
+ ```typescript
741
+ // WRONG — nothing happens; the statement is prepared but not executed
742
+ db.insert(runs).values({ id: '...', ... })
743
+
744
+ // CORRECT
745
+ db.insert(runs).values({ id: '...', ... }).run()
746
+
747
+ // Queries (SELECT) return results directly — no .run() needed
748
+ const result = db.select().from(runs).all() // ✓ array
749
+ const row = db.select().from(runs).get() // ✓ first row or undefined
750
+ ```