@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,463 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { existsSync } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
4
+ import { type DbClient, createDbClient } from '../db/client';
5
+ import { moduleConfigs, systemConfig } from '../db/schema';
6
+ import {
7
+ buildHostVars,
8
+ buildSystemVars,
9
+ extractInventoryHost,
10
+ generateGroupVarsYaml,
11
+ generateHostVarsYaml,
12
+ generateHostsIni,
13
+ parseConfigValue,
14
+ } from './inventory';
15
+ import type { InventoryHost } from './inventory';
16
+
17
+ const TEST_DB_PATH = './test-inventory.db';
18
+
19
+ describe('generateHostsIni', () => {
20
+ test('generates INI format for single host', () => {
21
+ const hosts: InventoryHost[] = [
22
+ {
23
+ hostname: 'iot',
24
+ ansibleHost: '192.168.0.110',
25
+ ansibleUser: 'root',
26
+ groups: ['homebridge'],
27
+ },
28
+ ];
29
+
30
+ const result = generateHostsIni(hosts);
31
+
32
+ expect(result).toContain('[homebridge]');
33
+ expect(result).toContain('iot ansible_host=192.168.0.110 ansible_user=root');
34
+ });
35
+
36
+ test('generates INI format for host in multiple groups', () => {
37
+ const hosts: InventoryHost[] = [
38
+ {
39
+ hostname: 'web',
40
+ ansibleHost: '10.0.10.10',
41
+ ansibleUser: 'root',
42
+ groups: ['web_server', 'production'],
43
+ },
44
+ ];
45
+
46
+ const result = generateHostsIni(hosts);
47
+
48
+ expect(result).toContain('[web_server]');
49
+ expect(result).toContain('[production]');
50
+ expect(result).toContain('web ansible_host=10.0.10.10 ansible_user=root');
51
+ });
52
+
53
+ test('generates INI format for multiple hosts', () => {
54
+ const hosts: InventoryHost[] = [
55
+ {
56
+ hostname: 'iot',
57
+ ansibleHost: '192.168.0.110',
58
+ ansibleUser: 'root',
59
+ groups: ['homebridge'],
60
+ },
61
+ {
62
+ hostname: 'dns-ext',
63
+ ansibleHost: '188.166.157.2',
64
+ ansibleUser: 'root',
65
+ groups: ['dns_external'],
66
+ },
67
+ ];
68
+
69
+ const result = generateHostsIni(hosts);
70
+
71
+ expect(result).toContain('[homebridge]');
72
+ expect(result).toContain('[dns_external]');
73
+ expect(result).toContain('iot ansible_host=192.168.0.110 ansible_user=root');
74
+ expect(result).toContain('dns-ext ansible_host=188.166.157.2 ansible_user=root');
75
+ });
76
+
77
+ test('includes SSH private key file path when provided', () => {
78
+ const hosts: InventoryHost[] = [
79
+ {
80
+ hostname: 'machine-host',
81
+ ansibleHost: '192.168.1.100',
82
+ ansibleUser: 'ubuntu',
83
+ groups: ['machines'],
84
+ ansibleSshPrivateKeyFile: '/var/lib/celilo/tmp/ansible-keys/machine-machine-1.key',
85
+ },
86
+ ];
87
+
88
+ const result = generateHostsIni(hosts);
89
+
90
+ expect(result).toContain('[machines]');
91
+ expect(result).toContain(
92
+ 'machine-host ansible_host=192.168.1.100 ansible_user=ubuntu ansible_ssh_private_key_file=/var/lib/celilo/tmp/ansible-keys/machine-machine-1.key',
93
+ );
94
+ });
95
+ });
96
+
97
+ describe('generateHostVarsYaml', () => {
98
+ test('generates YAML for simple variables', () => {
99
+ const vars = {
100
+ vmid: 2110,
101
+ hostname: 'iot',
102
+ container_ip: '192.168.0.110/24',
103
+ };
104
+
105
+ const result = generateHostVarsYaml(vars);
106
+
107
+ expect(result).toContain('vmid: 2110');
108
+ expect(result).toContain('hostname: iot');
109
+ expect(result).toContain('container_ip: 192.168.0.110/24');
110
+ });
111
+
112
+ test('generates YAML for arrays', () => {
113
+ const vars = {
114
+ zone_records: [
115
+ { name: 'ns1', type: 'A', value: '188.166.157.2' },
116
+ { name: 'www', type: 'A', value: '71.36.99.96' },
117
+ ],
118
+ };
119
+
120
+ const result = generateHostVarsYaml(vars);
121
+
122
+ expect(result).toContain('zone_records:');
123
+ expect(result).toContain('- name: ns1');
124
+ expect(result).toContain('type: A');
125
+ expect(result).toContain('value: 188.166.157.2');
126
+ expect(result).toContain('- name: www');
127
+ expect(result).toContain('value: 71.36.99.96');
128
+ });
129
+
130
+ test('generates YAML for nested objects', () => {
131
+ const vars = {
132
+ network: {
133
+ interface: 'eth0',
134
+ ip: '192.168.0.110',
135
+ gateway: '192.168.0.254',
136
+ },
137
+ };
138
+
139
+ const result = generateHostVarsYaml(vars);
140
+
141
+ expect(result).toContain('network:');
142
+ expect(result).toContain('interface: eth0');
143
+ expect(result).toContain('ip: 192.168.0.110');
144
+ expect(result).toContain('gateway: 192.168.0.254');
145
+ });
146
+
147
+ test('sorts keys for deterministic output', () => {
148
+ const vars = {
149
+ zebra: 'last',
150
+ apple: 'first',
151
+ middle: 'second',
152
+ };
153
+
154
+ const result = generateHostVarsYaml(vars);
155
+
156
+ const lines = result.split('\n');
157
+ const appleIndex = lines.findIndex((l) => l.includes('apple'));
158
+ const middleIndex = lines.findIndex((l) => l.includes('middle'));
159
+ const zebraIndex = lines.findIndex((l) => l.includes('zebra'));
160
+
161
+ expect(appleIndex).toBeLessThan(middleIndex);
162
+ expect(middleIndex).toBeLessThan(zebraIndex);
163
+ });
164
+ });
165
+
166
+ describe('generateGroupVarsYaml', () => {
167
+ test('generates YAML for system config', () => {
168
+ const vars = {
169
+ dns_primary: '192.168.0.1',
170
+ dns_fallback: '8.8.8.8 1.1.1.1',
171
+ routing_internal_gateway: '192.168.0.254',
172
+ };
173
+
174
+ const result = generateGroupVarsYaml(vars);
175
+
176
+ expect(result).toContain('dns_primary: 192.168.0.1');
177
+ expect(result).toContain('dns_fallback: 8.8.8.8 1.1.1.1');
178
+ expect(result).toContain('routing_internal_gateway: 192.168.0.254');
179
+ });
180
+ });
181
+
182
+ describe('parseConfigValue', () => {
183
+ test('parses JSON numbers', () => {
184
+ expect(parseConfigValue('2110')).toBe(2110);
185
+ expect(parseConfigValue('42.5')).toBe(42.5);
186
+ });
187
+
188
+ test('parses JSON booleans', () => {
189
+ expect(parseConfigValue('true')).toBe(true);
190
+ expect(parseConfigValue('false')).toBe(false);
191
+ });
192
+
193
+ test('parses JSON arrays', () => {
194
+ const result = parseConfigValue('["homebridge", "production"]');
195
+ expect(Array.isArray(result)).toBe(true);
196
+ expect(result).toEqual(['homebridge', 'production']);
197
+ });
198
+
199
+ test('parses JSON objects', () => {
200
+ const result = parseConfigValue('{"name":"test","value":123}');
201
+ expect(typeof result).toBe('object');
202
+ expect(result).toEqual({ name: 'test', value: 123 });
203
+ });
204
+
205
+ test('returns string for non-JSON values', () => {
206
+ expect(parseConfigValue('iot')).toBe('iot');
207
+ expect(parseConfigValue('192.168.0.110/24')).toBe('192.168.0.110/24');
208
+ expect(parseConfigValue('Home Bridge')).toBe('Home Bridge');
209
+ });
210
+ });
211
+
212
+ describe('Database integration', () => {
213
+ let db: DbClient;
214
+
215
+ beforeEach(() => {
216
+ // Set environment variable for test database
217
+ process.env.CELILO_DB_PATH = TEST_DB_PATH;
218
+
219
+ db = createDbClient({ path: TEST_DB_PATH });
220
+
221
+ // Create tables
222
+ db.$client.run(`
223
+ CREATE TABLE IF NOT EXISTS modules (
224
+ id TEXT PRIMARY KEY,
225
+ name TEXT NOT NULL,
226
+ version TEXT NOT NULL,
227
+ description TEXT,
228
+ state TEXT NOT NULL DEFAULT 'IMPORTED',
229
+ manifest_data TEXT NOT NULL,
230
+ source_path TEXT NOT NULL,
231
+ imported_at INTEGER NOT NULL DEFAULT (unixepoch()),
232
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
233
+ error_message TEXT
234
+ )
235
+ `);
236
+
237
+ db.$client.run(`
238
+ CREATE TABLE IF NOT EXISTS module_configs (
239
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
240
+ module_id TEXT NOT NULL,
241
+ key TEXT NOT NULL,
242
+ value TEXT NOT NULL,
243
+ value_json TEXT,
244
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
245
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
246
+ FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
247
+ )
248
+ `);
249
+
250
+ db.$client.run(`
251
+ CREATE TABLE IF NOT EXISTS system_config (
252
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
253
+ key TEXT NOT NULL UNIQUE,
254
+ value TEXT NOT NULL,
255
+ description TEXT,
256
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
257
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
258
+ )
259
+ `);
260
+ });
261
+
262
+ afterEach(async () => {
263
+ db.$client.close();
264
+ // Clean up environment variable
265
+ process.env.CELILO_DB_PATH = undefined;
266
+ if (existsSync(TEST_DB_PATH)) {
267
+ await rm(TEST_DB_PATH);
268
+ }
269
+ // Clean up WAL files
270
+ if (existsSync(`${TEST_DB_PATH}-wal`)) {
271
+ await rm(`${TEST_DB_PATH}-wal`);
272
+ }
273
+ if (existsSync(`${TEST_DB_PATH}-shm`)) {
274
+ await rm(`${TEST_DB_PATH}-shm`);
275
+ }
276
+ });
277
+
278
+ describe('buildHostVars', () => {
279
+ test('builds host vars from module config', () => {
280
+ // Insert module first (for foreign key)
281
+ db.$client.run(
282
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('homebridge', 'Homebridge', '1.0.0', '/path', '{}')`,
283
+ );
284
+
285
+ // Insert module config with proper schema
286
+ db.insert(moduleConfigs)
287
+ .values([
288
+ { moduleId: 'homebridge', key: 'vmid', value: '2110', valueJson: null },
289
+ { moduleId: 'homebridge', key: 'hostname', value: 'iot', valueJson: null },
290
+ {
291
+ moduleId: 'homebridge',
292
+ key: 'container_ip',
293
+ value: '192.168.0.110/24',
294
+ valueJson: null,
295
+ },
296
+ { moduleId: 'homebridge', key: 'cores', value: '1', valueJson: null },
297
+ ])
298
+ .run();
299
+
300
+ const vars = buildHostVars('homebridge', db);
301
+
302
+ expect(vars.vmid).toBe(2110);
303
+ expect(vars.hostname).toBe('iot');
304
+ expect(vars.container_ip).toBe('192.168.0.110/24');
305
+ expect(vars.cores).toBe(1);
306
+ });
307
+
308
+ test('converts dot notation to underscores', () => {
309
+ db.$client.run(
310
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('test', 'Test', '1.0.0', '/path', '{}')`,
311
+ );
312
+
313
+ db.insert(moduleConfigs)
314
+ .values([
315
+ { moduleId: 'test', key: 'network.interface', value: 'eth0', valueJson: null },
316
+ { moduleId: 'test', key: 'network.ip', value: '192.168.0.1', valueJson: null },
317
+ ])
318
+ .run();
319
+
320
+ const vars = buildHostVars('test', db);
321
+
322
+ expect(vars.network_interface).toBe('eth0');
323
+ expect(vars.network_ip).toBe('192.168.0.1');
324
+ });
325
+
326
+ test('skips inventory.* keys', () => {
327
+ db.$client.run(
328
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('test', 'Test', '1.0.0', '/path', '{}')`,
329
+ );
330
+
331
+ db.insert(moduleConfigs)
332
+ .values([
333
+ { moduleId: 'test', key: 'hostname', value: 'iot', valueJson: null },
334
+ { moduleId: 'test', key: 'inventory.hostname', value: 'iot', valueJson: null },
335
+ {
336
+ moduleId: 'test',
337
+ key: 'inventory.ansible_host',
338
+ value: '192.168.0.110',
339
+ valueJson: null,
340
+ },
341
+ ])
342
+ .run();
343
+
344
+ const vars = buildHostVars('test', db);
345
+
346
+ expect(vars.hostname).toBe('iot');
347
+ expect(vars.inventory_hostname).toBeUndefined();
348
+ expect(vars.inventory_ansible_host).toBeUndefined();
349
+ });
350
+
351
+ test('parses array values', () => {
352
+ db.$client.run(
353
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('dns', 'DNS', '1.0.0', '/path', '{}')`,
354
+ );
355
+
356
+ // Complex array type - store in valueJson column
357
+ db.insert(moduleConfigs)
358
+ .values([
359
+ {
360
+ moduleId: 'dns',
361
+ key: 'zone_records',
362
+ value: '', // Empty for complex types
363
+ valueJson: '[{"name":"ns1","type":"A","value":"188.166.157.2"}]',
364
+ },
365
+ ])
366
+ .run();
367
+
368
+ const vars = buildHostVars('dns', db);
369
+
370
+ expect(Array.isArray(vars.zone_records)).toBe(true);
371
+ expect(vars.zone_records).toEqual([{ name: 'ns1', type: 'A', value: '188.166.157.2' }]);
372
+ });
373
+ });
374
+
375
+ describe('buildSystemVars', () => {
376
+ test('builds system vars from system config', () => {
377
+ db.insert(systemConfig)
378
+ .values([
379
+ { key: 'dns.primary', value: '192.168.0.1' },
380
+ { key: 'dns.fallback', value: '8.8.8.8 1.1.1.1' },
381
+ { key: 'network.bridge', value: 'vmbr0' },
382
+ ])
383
+ .run();
384
+
385
+ const vars = buildSystemVars(db);
386
+
387
+ expect(vars.dns_primary).toBe('192.168.0.1');
388
+ expect(vars.dns_fallback).toBe('8.8.8.8 1.1.1.1');
389
+ expect(vars.network_bridge).toBe('vmbr0');
390
+ });
391
+
392
+ test('returns empty object when no system config', () => {
393
+ const vars = buildSystemVars(db);
394
+ expect(vars).toEqual({});
395
+ });
396
+ });
397
+
398
+ describe('extractInventoryHost', () => {
399
+ test('extracts inventory host from config with auto-derivation', () => {
400
+ db.$client.run(
401
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('homebridge', 'Homebridge', '1.0.0', '/path', '{}')`,
402
+ );
403
+
404
+ db.insert(moduleConfigs)
405
+ .values([
406
+ { moduleId: 'homebridge', key: 'hostname', value: 'iot' },
407
+ { moduleId: 'homebridge', key: 'container_ip', value: '192.168.0.110/24' },
408
+ ])
409
+ .run();
410
+
411
+ const host = extractInventoryHost('homebridge', db);
412
+
413
+ expect(host).not.toBeNull();
414
+ expect(host?.hostname).toBe('iot');
415
+ expect(host?.ansibleHost).toBe('192.168.0.110'); // CIDR stripped
416
+ expect(host?.ansibleUser).toBe('root'); // Default
417
+ expect(host?.groups).toEqual(['homebridge']); // Module ID
418
+ });
419
+
420
+ test('handles VPS-based modules', () => {
421
+ db.$client.run(
422
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('dns-external', 'DNS External', '1.0.0', '/path', '{}')`,
423
+ );
424
+
425
+ db.insert(moduleConfigs)
426
+ .values([
427
+ { moduleId: 'dns-external', key: 'hostname', value: 'dns-ext' },
428
+ { moduleId: 'dns-external', key: 'vps_ip', value: '188.166.157.2' },
429
+ ])
430
+ .run();
431
+
432
+ const host = extractInventoryHost('dns-external', db);
433
+
434
+ expect(host).not.toBeNull();
435
+ expect(host?.hostname).toBe('dns-ext');
436
+ expect(host?.ansibleHost).toBe('188.166.157.2'); // VPS IP used directly
437
+ expect(host?.ansibleUser).toBe('root');
438
+ expect(host?.groups).toEqual(['dns-external']);
439
+ });
440
+
441
+ test('returns null when required fields missing', () => {
442
+ db.$client.run(
443
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('test', 'Test', '1.0.0', '/path', '{}')`,
444
+ );
445
+
446
+ db.insert(moduleConfigs)
447
+ .values([
448
+ { moduleId: 'test', key: 'hostname', value: 'iot' },
449
+ // Missing container_ip or vps_ip for ansible_host
450
+ ])
451
+ .run();
452
+
453
+ const host = extractInventoryHost('test', db);
454
+
455
+ expect(host).toBeNull();
456
+ });
457
+
458
+ test('returns null when no inventory config', () => {
459
+ const host = extractInventoryHost('nonexistent', db);
460
+ expect(host).toBeNull();
461
+ });
462
+ });
463
+ });