@celilo/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. package/src/variables/types.ts +59 -0
@@ -0,0 +1,30 @@
1
+ import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
2
+ import { closeDb, createDbClient, findMigrationsFolder } from './client';
3
+
4
+ /**
5
+ * Run database migrations
6
+ */
7
+ export async function runMigrations(dbPath?: string) {
8
+ console.log('Running database migrations...');
9
+
10
+ const db = createDbClient(dbPath ? { path: dbPath } : undefined);
11
+
12
+ try {
13
+ const migrationsFolder = findMigrationsFolder();
14
+ await migrate(db, { migrationsFolder });
15
+ console.log('Migrations completed successfully');
16
+ } catch (error) {
17
+ console.error('Migration failed:', error);
18
+ throw error;
19
+ } finally {
20
+ closeDb();
21
+ }
22
+ }
23
+
24
+ // Run migrations if executed directly
25
+ if (import.meta.main) {
26
+ runMigrations().catch((error) => {
27
+ console.error('Failed to run migrations:', error);
28
+ process.exit(1);
29
+ });
30
+ }
@@ -0,0 +1,221 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { existsSync } from 'node:fs';
3
+ import { unlink } from 'node:fs/promises';
4
+ import { eq } from 'drizzle-orm';
5
+ import { type DbClient, createDbClient } from './client';
6
+ import { capabilities, moduleConfigs, modules, secrets } from './schema';
7
+ import type { NewCapability, NewModule, NewModuleConfig, NewSecret } from './schema';
8
+
9
+ describe('Database Schema', () => {
10
+ let db: DbClient;
11
+ let testDbPath: string;
12
+
13
+ beforeEach(async () => {
14
+ // Create unique database path for each test
15
+ testDbPath = `./test-celilo-${Date.now()}-${Math.random()}.db`;
16
+
17
+ // Create fresh database
18
+ db = createDbClient({ path: testDbPath });
19
+
20
+ // Create tables manually for testing (no migrations yet)
21
+ db.$client.run(`
22
+ CREATE TABLE IF NOT EXISTS modules (
23
+ id TEXT PRIMARY KEY,
24
+ name TEXT NOT NULL,
25
+ version TEXT NOT NULL,
26
+ description TEXT,
27
+ state TEXT NOT NULL DEFAULT 'IMPORTED',
28
+ manifest_data TEXT NOT NULL,
29
+ source_path TEXT NOT NULL,
30
+ imported_at INTEGER NOT NULL DEFAULT (unixepoch()),
31
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
32
+ error_message TEXT
33
+ )
34
+ `);
35
+
36
+ db.$client.run(`
37
+ CREATE TABLE IF NOT EXISTS module_configs (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ module_id TEXT NOT NULL,
40
+ key TEXT NOT NULL,
41
+ value TEXT NOT NULL,
42
+ value_json TEXT,
43
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
44
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
45
+ FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
46
+ )
47
+ `);
48
+
49
+ db.$client.run(`
50
+ CREATE TABLE IF NOT EXISTS capabilities (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ module_id TEXT NOT NULL,
53
+ capability_name TEXT NOT NULL,
54
+ version TEXT NOT NULL,
55
+ data TEXT NOT NULL,
56
+ registered_at INTEGER NOT NULL DEFAULT (unixepoch()),
57
+ FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
58
+ )
59
+ `);
60
+
61
+ db.$client.run(`
62
+ CREATE TABLE IF NOT EXISTS secrets (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ module_id TEXT NOT NULL,
65
+ name TEXT NOT NULL,
66
+ encrypted_value TEXT NOT NULL,
67
+ iv TEXT NOT NULL,
68
+ auth_tag TEXT NOT NULL,
69
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
70
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
71
+ FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
72
+ )
73
+ `);
74
+ });
75
+
76
+ afterEach(async () => {
77
+ db.$client.close();
78
+ if (existsSync(testDbPath)) {
79
+ await unlink(testDbPath);
80
+ }
81
+ // Clean up WAL and SHM files
82
+ const walPath = `${testDbPath}-wal`;
83
+ const shmPath = `${testDbPath}-shm`;
84
+ if (existsSync(walPath)) {
85
+ await unlink(walPath);
86
+ }
87
+ if (existsSync(shmPath)) {
88
+ await unlink(shmPath);
89
+ }
90
+ });
91
+
92
+ test('should create a module', () => {
93
+ const newModule: NewModule = {
94
+ id: 'homebridge',
95
+ name: 'Homebridge',
96
+ version: '1.0.0',
97
+ description: 'HomeKit bridge',
98
+ sourcePath: '/data/modules/homebridge',
99
+ manifestData: { id: 'homebridge', requires: {} },
100
+ };
101
+
102
+ const result = db.insert(modules).values(newModule).returning().get();
103
+
104
+ expect(result).toBeDefined();
105
+ expect(result.id).toBe('homebridge');
106
+ expect(result.name).toBe('Homebridge');
107
+ expect(result.state).toBe('IMPORTED');
108
+ });
109
+
110
+ test('should create module config', () => {
111
+ // Insert module first
112
+ const newModule: NewModule = {
113
+ id: 'homebridge',
114
+ name: 'Homebridge',
115
+ version: '1.0.0',
116
+ sourcePath: '/data/modules/homebridge',
117
+ manifestData: {},
118
+ };
119
+ db.insert(modules).values(newModule).run();
120
+
121
+ // Insert config
122
+ const newConfig: NewModuleConfig = {
123
+ moduleId: 'homebridge',
124
+ key: 'container_ip',
125
+ value: '192.168.0.50',
126
+ };
127
+
128
+ const result = db.insert(moduleConfigs).values(newConfig).returning().get();
129
+
130
+ expect(result).toBeDefined();
131
+ expect(result.moduleId).toBe('homebridge');
132
+ expect(result.key).toBe('container_ip');
133
+ expect(result.value).toBe('192.168.0.50');
134
+ });
135
+
136
+ test('should create capability', () => {
137
+ // Insert module first
138
+ const newModule: NewModule = {
139
+ id: 'dns-external',
140
+ name: 'DNS External',
141
+ version: '1.0.0',
142
+ sourcePath: '/data/modules/dns-external',
143
+ manifestData: {},
144
+ };
145
+ db.insert(modules).values(newModule).run();
146
+
147
+ // Insert capability
148
+ const newCapability: NewCapability = {
149
+ moduleId: 'dns-external',
150
+ capabilityName: 'dns_external',
151
+ version: '1.0.0',
152
+ data: {
153
+ nameserver: 'ns1.example.com',
154
+ zone: 'example.com',
155
+ },
156
+ };
157
+
158
+ const result = db.insert(capabilities).values(newCapability).returning().get();
159
+
160
+ expect(result).toBeDefined();
161
+ expect(result.capabilityName).toBe('dns_external');
162
+ expect(result.data).toEqual({
163
+ nameserver: 'ns1.example.com',
164
+ zone: 'example.com',
165
+ });
166
+ });
167
+
168
+ test('should create secret', () => {
169
+ // Insert module first
170
+ const newModule: NewModule = {
171
+ id: 'dns-external',
172
+ name: 'DNS External',
173
+ version: '1.0.0',
174
+ sourcePath: '/data/modules/dns-external',
175
+ manifestData: {},
176
+ };
177
+ db.insert(modules).values(newModule).run();
178
+
179
+ // Insert secret
180
+ const newSecret: NewSecret = {
181
+ moduleId: 'dns-external',
182
+ name: 'tsig_key',
183
+ encryptedValue: 'encrypted_data_here',
184
+ iv: 'initialization_vector',
185
+ authTag: 'authentication_tag',
186
+ };
187
+
188
+ const result = db.insert(secrets).values(newSecret).returning().get();
189
+
190
+ expect(result).toBeDefined();
191
+ expect(result.name).toBe('tsig_key');
192
+ expect(result.encryptedValue).toBe('encrypted_data_here');
193
+ });
194
+
195
+ test('should cascade delete module configs when module is deleted', () => {
196
+ // Insert module
197
+ const newModule: NewModule = {
198
+ id: 'homebridge',
199
+ name: 'Homebridge',
200
+ version: '1.0.0',
201
+ sourcePath: '/data/modules/homebridge',
202
+ manifestData: {},
203
+ };
204
+ db.insert(modules).values(newModule).run();
205
+
206
+ // Insert config
207
+ const newConfig: NewModuleConfig = {
208
+ moduleId: 'homebridge',
209
+ key: 'container_ip',
210
+ value: '192.168.0.50',
211
+ };
212
+ db.insert(moduleConfigs).values(newConfig).run();
213
+
214
+ // Delete module
215
+ db.delete(modules).where(eq(modules.id, 'homebridge')).run();
216
+
217
+ // Config should be deleted
218
+ const configs = db.select().from(moduleConfigs).all();
219
+ expect(configs).toHaveLength(0);
220
+ });
221
+ });
@@ -0,0 +1,434 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { integer, sqliteTable, text, unique } from 'drizzle-orm/sqlite-core';
3
+
4
+ /**
5
+ * Module lifecycle states
6
+ * IMPORTED, VALIDATED, CONFIGURED, GENERATING, ERROR, DEPLOYING, INSTALLED, VERIFIED, UNINSTALLING
7
+ */
8
+ export type ModuleState =
9
+ | 'IMPORTED'
10
+ | 'VALIDATED'
11
+ | 'CONFIGURED'
12
+ | 'GENERATING'
13
+ | 'DEPLOYING'
14
+ | 'INSTALLED'
15
+ | 'VERIFIED'
16
+ | 'ERROR'
17
+ | 'UNINSTALLING';
18
+
19
+ /**
20
+ * Modules table - stores module metadata and manifest data
21
+ */
22
+ export const modules = sqliteTable('modules', {
23
+ id: text('id').primaryKey(),
24
+ name: text('name').notNull(),
25
+ version: text('version').notNull(),
26
+ description: text('description'),
27
+ state: text('state').$type<ModuleState>().notNull().default('IMPORTED'),
28
+ manifestData: text('manifest_data', { mode: 'json' }).$type<Record<string, unknown>>().notNull(),
29
+ sourcePath: text('source_path').notNull(),
30
+ importedAt: integer('imported_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
31
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
32
+ errorMessage: text('error_message'),
33
+ });
34
+
35
+ /**
36
+ * Module configuration - user-provided key-value pairs
37
+ * Stores configuration like container_ip, zone, etc.
38
+ *
39
+ * For primitive types (string, number, boolean):
40
+ * - value: contains the value (as string)
41
+ * - valueJson: NULL
42
+ *
43
+ * For complex types (arrays, objects):
44
+ * - value: empty string
45
+ * - valueJson: JSON-stringified complex value
46
+ */
47
+ export const moduleConfigs = sqliteTable(
48
+ 'module_configs',
49
+ {
50
+ id: integer('id').primaryKey({ autoIncrement: true }),
51
+ moduleId: text('module_id')
52
+ .notNull()
53
+ .references(() => modules.id, { onDelete: 'cascade' }),
54
+ key: text('key').notNull(),
55
+ value: text('value').notNull(),
56
+ valueJson: text('value_json'), // JSON for complex types (arrays, objects)
57
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
58
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
59
+ },
60
+ (table) => ({
61
+ uniqueModuleKey: unique().on(table.moduleId, table.key),
62
+ }),
63
+ );
64
+
65
+ /**
66
+ * Capabilities table - stores registered capabilities provided by modules
67
+ * Example: namecheap module provides dns_registrar capability
68
+ */
69
+ export const capabilities = sqliteTable('capabilities', {
70
+ id: integer('id').primaryKey({ autoIncrement: true }),
71
+ moduleId: text('module_id')
72
+ .notNull()
73
+ .references(() => modules.id, { onDelete: 'cascade' }),
74
+ capabilityName: text('capability_name').notNull(),
75
+ version: text('version').notNull(),
76
+ data: text('data', { mode: 'json' }).$type<Record<string, unknown>>().notNull(),
77
+ /** JSON array of zone names this capability applies to, or null for zone-agnostic */
78
+ zones: text('zones', { mode: 'json' }).$type<string[] | null>(),
79
+ registeredAt: integer('registered_at', { mode: 'timestamp' })
80
+ .notNull()
81
+ .default(sql`(unixepoch())`),
82
+ });
83
+
84
+ /**
85
+ * Capability secrets table - stores encrypted secrets owned by capabilities
86
+ * Values are encrypted with AES-256-GCM using master key
87
+ * Example: dns-external capability owns TSIG secret
88
+ */
89
+ export const capabilitySecrets = sqliteTable(
90
+ 'capability_secrets',
91
+ {
92
+ id: integer('id').primaryKey({ autoIncrement: true }),
93
+ capabilityId: integer('capability_id')
94
+ .notNull()
95
+ .references(() => capabilities.id, { onDelete: 'cascade' }),
96
+ name: text('name').notNull(),
97
+ description: text('description'), // Added to store secret description
98
+ encryptedValue: text('encrypted_value'), // Nullable - allows metadata-only storage during import
99
+ iv: text('iv'), // Nullable - populated when secret value is set
100
+ authTag: text('auth_tag'), // Nullable - populated when secret value is set
101
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
102
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
103
+ },
104
+ (table) => ({
105
+ uniqueCapabilitySecret: unique().on(table.capabilityId, table.name),
106
+ }),
107
+ );
108
+
109
+ /**
110
+ * Secrets table - stores encrypted secrets per module
111
+ * Values are encrypted with AES-256-GCM using master key
112
+ */
113
+ export const secrets = sqliteTable('secrets', {
114
+ id: integer('id').primaryKey({ autoIncrement: true }),
115
+ moduleId: text('module_id')
116
+ .notNull()
117
+ .references(() => modules.id, { onDelete: 'cascade' }),
118
+ name: text('name').notNull(),
119
+ encryptedValue: text('encrypted_value').notNull(),
120
+ iv: text('iv').notNull(),
121
+ authTag: text('auth_tag').notNull(),
122
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
123
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
124
+ });
125
+
126
+ /**
127
+ * System configuration - system-wide settings
128
+ * Used for $system: variables in templates
129
+ * Examples: DNS servers, network settings, domain names
130
+ */
131
+ export const systemConfig = sqliteTable('system_config', {
132
+ id: integer('id').primaryKey({ autoIncrement: true }),
133
+ key: text('key').notNull().unique(),
134
+ value: text('value').notNull(),
135
+ description: text('description'),
136
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
137
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
138
+ });
139
+
140
+ /**
141
+ * System secrets table - stores encrypted system-level secrets
142
+ * Values are encrypted with AES-256-GCM using master key
143
+ * Examples: Proxmox root password, API tokens, SSH keys
144
+ */
145
+ export const systemSecrets = sqliteTable('system_secrets', {
146
+ id: integer('id').primaryKey({ autoIncrement: true }),
147
+ key: text('key').notNull().unique(),
148
+ encryptedValue: text('encrypted_value').notNull(),
149
+ iv: text('iv').notNull(),
150
+ authTag: text('auth_tag').notNull(),
151
+ description: text('description'),
152
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
153
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
154
+ });
155
+
156
+ /**
157
+ * Module integrity table - stores checksums and signature for package verification
158
+ * Used for runtime auditing to detect tampering, missing files, or extra files
159
+ */
160
+ export const moduleIntegrity = sqliteTable('module_integrity', {
161
+ id: integer('id').primaryKey({ autoIncrement: true }),
162
+ moduleId: text('module_id')
163
+ .notNull()
164
+ .unique()
165
+ .references(() => modules.id, { onDelete: 'cascade' }),
166
+ checksums: text('checksums', { mode: 'json' }).$type<Record<string, string>>().notNull(), // { "path/to/file": "xxhash", ... }
167
+ signature: text('signature'), // Nullable - null for directory imports, populated for .netapp packages
168
+ importedAt: integer('imported_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
169
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
170
+ });
171
+
172
+ /**
173
+ * IPAM (IP Address Management) allocations table
174
+ * Tracks VMID and IP address assignments per module
175
+ * Prevents conflicts and enables automatic allocation from zone subnets
176
+ */
177
+ export const ipAllocations = sqliteTable('ip_allocations', {
178
+ id: integer('id').primaryKey({ autoIncrement: true }),
179
+ moduleId: text('module_id')
180
+ .notNull()
181
+ .references(() => modules.id, { onDelete: 'cascade' }),
182
+ vmid: integer('vmid').notNull().unique(),
183
+ containerIp: text('container_ip').notNull().unique(), // CIDR format (e.g., "10.0.10.10/24")
184
+ zone: text('zone').$type<'dmz' | 'app' | 'secure' | 'internal'>().notNull(),
185
+ allocatedAt: integer('allocated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
186
+ });
187
+
188
+ /**
189
+ * IP reservations table
190
+ * Allows users to reserve IPs for infrastructure or external services
191
+ * IPAM allocator skips reserved IPs
192
+ */
193
+ export const ipReservations = sqliteTable('ip_reservations', {
194
+ id: integer('id').primaryKey({ autoIncrement: true }),
195
+ ipStart: text('ip_start').notNull(), // Single IP or range start
196
+ ipEnd: text('ip_end'), // NULL for single IP, end IP for range
197
+ zone: text('zone').$type<'dmz' | 'app' | 'secure' | 'internal'>().notNull(),
198
+ reason: text('reason').notNull(),
199
+ reservedAt: integer('reserved_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
200
+ });
201
+
202
+ /**
203
+ * VMID reservations table
204
+ * Allows users to reserve VMIDs for existing VMs or external systems
205
+ * IPAM allocator skips reserved VMIDs
206
+ */
207
+ export const vmidReservations = sqliteTable('vmid_reservations', {
208
+ id: integer('id').primaryKey({ autoIncrement: true }),
209
+ vmid: integer('vmid').notNull().unique(),
210
+ reason: text('reason').notNull(),
211
+ reservedAt: integer('reserved_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
212
+ });
213
+
214
+ /**
215
+ * Module builds table
216
+ * Tracks build metadata for modules with custom compilation requirements
217
+ * Example: Caddy with RFC2136 DNS provider, custom Go binaries
218
+ */
219
+ export type BuildStatus = 'success' | 'failed' | 'in_progress';
220
+
221
+ export const moduleBuilds = sqliteTable('module_builds', {
222
+ id: integer('id').primaryKey({ autoIncrement: true }),
223
+ moduleId: text('module_id')
224
+ .notNull()
225
+ .references(() => modules.id, { onDelete: 'cascade' }),
226
+ version: text('version').notNull(),
227
+ builtAt: integer('built_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
228
+ artifacts: text('artifacts', { mode: 'json' }).$type<string[]>().notNull(), // Array of artifact paths
229
+ environment: text('environment').$type<'nix' | 'system'>(),
230
+ status: text('status').$type<BuildStatus>().notNull(),
231
+ buildLog: text('build_log'), // Build output for debugging
232
+ });
233
+
234
+ /**
235
+ * Network zones type
236
+ * - internal: Home network (192.168.0.0/24)
237
+ * - dmz: Public-facing services in home lab (10.0.10.0/24)
238
+ * - app: Internal application services (10.0.20.0/24)
239
+ * - secure: Authentication and database services (10.0.30.0/24)
240
+ * - external: Internet-hosted services (outside home network)
241
+ */
242
+ export type NetworkZone = 'internal' | 'dmz' | 'app' | 'secure' | 'external';
243
+
244
+ /**
245
+ * Container services table
246
+ * Stores container service providers (Proxmox, Digital Ocean, etc.)
247
+ * that can provision new containers/VMs on demand
248
+ */
249
+ export const containerServices = sqliteTable('container_services', {
250
+ id: text('id').primaryKey(), // UUID
251
+ serviceId: text('service_id').notNull().unique(), // User-facing kebab-case ID
252
+ name: text('name').notNull(),
253
+ providerName: text('provider_name')
254
+ .$type<'proxmox' | 'digitalocean' | 'aws' | 'gcp' | 'azure'>()
255
+ .notNull(),
256
+ zones: text('zones', { mode: 'json' }).$type<NetworkZone[]>().notNull(),
257
+ apiCredentialsEncrypted: text('api_credentials_encrypted').notNull(),
258
+ providerConfig: text('provider_config', { mode: 'json' })
259
+ .$type<Record<string, unknown>>()
260
+ .notNull(),
261
+ verified: integer('verified', { mode: 'boolean' }).notNull().default(false),
262
+ verifiedAt: integer('verified_at', { mode: 'timestamp' }),
263
+ verificationError: text('verification_error'),
264
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
265
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
266
+ });
267
+
268
+ /**
269
+ * Machines table
270
+ * Stores pre-existing machines (Raspberry Pi, VPS, bare metal)
271
+ * that users have added to the pool for hosting modules
272
+ */
273
+ export const machines = sqliteTable('machines', {
274
+ id: text('id').primaryKey(), // UUID
275
+ hostname: text('hostname').notNull(),
276
+ zone: text('zone').$type<NetworkZone>().notNull(),
277
+ ipAddress: text('ip_address').notNull(),
278
+ sshUser: text('ssh_user').notNull(),
279
+ sshKeyEncrypted: text('ssh_key_encrypted').notNull(),
280
+ hardware: text('hardware', { mode: 'json' })
281
+ .$type<{
282
+ cpu_cores: number;
283
+ memory_mb: number;
284
+ disk_gb: number;
285
+ arch?: string;
286
+ }>()
287
+ .notNull(),
288
+ /** Machine classification: 'host' (single interface) or 'router' (multi-interface) */
289
+ role: text('role').$type<'host' | 'router'>().notNull().default('host'),
290
+ /** Detected network interfaces with IPs and zones */
291
+ interfaces: text('interfaces', { mode: 'json' })
292
+ .$type<Array<{ name: string; ipAddress: string; zone: string }>>()
293
+ .notNull()
294
+ .default(sql`'[]'`),
295
+ assignedModuleIds: text('assigned_module_ids', { mode: 'json' })
296
+ .$type<string[]>()
297
+ .notNull()
298
+ .default(sql`'[]'`),
299
+ /** Module ID this machine is earmarked for. If set, only this module can use this machine. */
300
+ earmarkedModule: text('earmarked_module'),
301
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
302
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
303
+ });
304
+
305
+ /**
306
+ * Module infrastructure table
307
+ * Tracks which infrastructure (machine or container service) is used for each module
308
+ */
309
+ export const moduleInfrastructure = sqliteTable('module_infrastructure', {
310
+ id: text('id').primaryKey(), // UUID
311
+ moduleId: text('module_id')
312
+ .notNull()
313
+ .references(() => modules.id, { onDelete: 'cascade' }),
314
+ infrastructureType: text('infrastructure_type')
315
+ .$type<'machine' | 'container_service'>()
316
+ .notNull(),
317
+ machineId: text('machine_id').references(() => machines.id),
318
+ serviceId: text('service_id').references(() => containerServices.id),
319
+ containerMetadata: text('container_metadata', { mode: 'json' }).$type<Record<string, unknown>>(), // VMID, droplet ID, etc.
320
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
321
+ });
322
+
323
+ /**
324
+ * Web routes table
325
+ * Tracks routes registered by modules via public_web capability functions.
326
+ * Routes are registered during on_install hooks and removed during on_uninstall.
327
+ * The public_web provider (Caddy) uses these to generate its configuration.
328
+ */
329
+ export const webRoutes = sqliteTable('web_routes', {
330
+ id: integer('id').primaryKey({ autoIncrement: true }),
331
+ slug: text('slug').notNull(),
332
+ moduleId: text('module_id')
333
+ .notNull()
334
+ .references(() => modules.id, { onDelete: 'cascade' }),
335
+ type: text('type').$type<'static' | 'reverse_proxy'>().notNull(),
336
+ path: text('path').notNull().unique(),
337
+ targetHost: text('target_host'),
338
+ targetPort: integer('target_port'),
339
+ subdomain: text('subdomain'),
340
+ websocket: integer('websocket', { mode: 'boolean' }).notNull().default(false),
341
+ contentHash: text('content_hash'),
342
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
343
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
344
+ });
345
+
346
+ /**
347
+ * Backup storage providers - destinations for backup archives
348
+ * Supports local filesystem and S3-compatible storage (AWS S3, MinIO, Backblaze B2, Wasabi)
349
+ */
350
+ export type BackupStorageProvider = 'local' | 's3';
351
+
352
+ export const backupStorages = sqliteTable('backup_storages', {
353
+ id: text('id').primaryKey(), // UUID
354
+ storageId: text('storage_id').notNull().unique(), // User-facing kebab-case ID
355
+ name: text('name').notNull(),
356
+ providerName: text('provider_name').$type<BackupStorageProvider>().notNull(),
357
+ credentialsEncrypted: text('credentials_encrypted').notNull(), // Encrypted JSON blob
358
+ providerConfig: text('provider_config', { mode: 'json' })
359
+ .$type<Record<string, unknown>>()
360
+ .notNull(),
361
+ verified: integer('verified', { mode: 'boolean' }).notNull().default(false),
362
+ verifiedAt: integer('verified_at', { mode: 'timestamp' }),
363
+ verificationError: text('verification_error'),
364
+ isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
365
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
366
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
367
+ });
368
+
369
+ /**
370
+ * Backup records - metadata for each backup taken
371
+ * Tracks both system state backups and module data backups
372
+ */
373
+ export type BackupType = 'module_data' | 'system_state';
374
+ export type BackupStatus = 'in_progress' | 'completed' | 'failed';
375
+
376
+ export const backups = sqliteTable('backups', {
377
+ id: text('id').primaryKey(), // UUID
378
+ moduleId: text('module_id').references(() => modules.id, { onDelete: 'set null' }), // null = system state backup
379
+ storageId: text('storage_id')
380
+ .notNull()
381
+ .references(() => backupStorages.id),
382
+ storagePath: text('storage_path').notNull(), // path/key within storage
383
+ backupType: text('backup_type').$type<BackupType>().notNull(),
384
+ moduleVersion: text('module_version'), // module version at backup time
385
+ schemaVersion: text('schema_version'), // optional data schema version from on_backup hook
386
+ sizeBytes: integer('size_bytes'),
387
+ metadata: text('metadata', { mode: 'json' })
388
+ .$type<Record<string, unknown>>()
389
+ .notNull()
390
+ .default(sql`'{}'`),
391
+ status: text('status').$type<BackupStatus>().notNull().default('in_progress'),
392
+ errorMessage: text('error_message'),
393
+ name: text('name'), // optional human-readable name/annotation
394
+ startedAt: integer('started_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
395
+ completedAt: integer('completed_at', { mode: 'timestamp' }),
396
+ });
397
+
398
+ /**
399
+ * Type exports for use in application code
400
+ */
401
+ export type Module = typeof modules.$inferSelect;
402
+ export type NewModule = typeof modules.$inferInsert;
403
+ export type ModuleConfig = typeof moduleConfigs.$inferSelect;
404
+ export type NewModuleConfig = typeof moduleConfigs.$inferInsert;
405
+ export type Capability = typeof capabilities.$inferSelect;
406
+ export type NewCapability = typeof capabilities.$inferInsert;
407
+ export type Secret = typeof secrets.$inferSelect;
408
+ export type NewSecret = typeof secrets.$inferInsert;
409
+ export type SystemConfig = typeof systemConfig.$inferSelect;
410
+ export type NewSystemConfig = typeof systemConfig.$inferInsert;
411
+ export type SystemSecret = typeof systemSecrets.$inferSelect;
412
+ export type NewSystemSecret = typeof systemSecrets.$inferInsert;
413
+ export type ModuleIntegrity = typeof moduleIntegrity.$inferSelect;
414
+ export type NewModuleIntegrity = typeof moduleIntegrity.$inferInsert;
415
+ export type IpAllocation = typeof ipAllocations.$inferSelect;
416
+ export type NewIpAllocation = typeof ipAllocations.$inferInsert;
417
+ export type IpReservation = typeof ipReservations.$inferSelect;
418
+ export type NewIpReservation = typeof ipReservations.$inferInsert;
419
+ export type VmidReservation = typeof vmidReservations.$inferSelect;
420
+ export type NewVmidReservation = typeof vmidReservations.$inferInsert;
421
+ export type ModuleBuild = typeof moduleBuilds.$inferSelect;
422
+ export type NewModuleBuild = typeof moduleBuilds.$inferInsert;
423
+ export type ContainerService = typeof containerServices.$inferSelect;
424
+ export type NewContainerService = typeof containerServices.$inferInsert;
425
+ export type Machine = typeof machines.$inferSelect;
426
+ export type NewMachine = typeof machines.$inferInsert;
427
+ export type ModuleInfrastructure = typeof moduleInfrastructure.$inferSelect;
428
+ export type NewModuleInfrastructure = typeof moduleInfrastructure.$inferInsert;
429
+ export type WebRoute = typeof webRoutes.$inferSelect;
430
+ export type NewWebRoute = typeof webRoutes.$inferInsert;
431
+ export type BackupStorage = typeof backupStorages.$inferSelect;
432
+ export type NewBackupStorage = typeof backupStorages.$inferInsert;
433
+ export type Backup = typeof backups.$inferSelect;
434
+ export type NewBackup = typeof backups.$inferInsert;