@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.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- 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
|
+
```
|