@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,237 @@
1
+ /**
2
+ * Tests for module configuration service
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
6
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { closeDb, createDbClient } from '../db/client';
10
+ import { runMigrations } from '../db/migrate';
11
+ import { modules } from '../db/schema';
12
+ import {
13
+ formatConfigValue,
14
+ getAllModuleConfigValues,
15
+ getModuleConfigValue,
16
+ isComplexValue,
17
+ parseConfigValue,
18
+ setModuleConfigValue,
19
+ } from './module-config';
20
+
21
+ describe('module-config service', () => {
22
+ let testDbPath: string;
23
+ let testDir: string;
24
+
25
+ beforeEach(async () => {
26
+ // Create temp directory for test database
27
+ testDir = mkdtempSync(join(tmpdir(), 'celilo-test-'));
28
+ testDbPath = join(testDir, 'test.db');
29
+
30
+ // Set environment variable for database path
31
+ process.env.CELILO_DB_PATH = testDbPath;
32
+
33
+ // Initialize database and run migrations
34
+ await runMigrations(testDbPath);
35
+
36
+ // Insert test module
37
+ const db = createDbClient({ path: testDbPath });
38
+ db.insert(modules)
39
+ .values({
40
+ id: 'test-module',
41
+ name: 'Test Module',
42
+ version: '1.0.0',
43
+ manifestData: {
44
+ variables: {
45
+ owns: [
46
+ { name: 'string_var', type: 'string' },
47
+ { name: 'number_var', type: 'number' },
48
+ { name: 'bool_var', type: 'boolean' },
49
+ { name: 'array_var', type: 'array' },
50
+ { name: 'object_var', type: 'object' },
51
+ ],
52
+ },
53
+ },
54
+ sourcePath: '/test/path',
55
+ })
56
+ .run();
57
+ });
58
+
59
+ afterEach(() => {
60
+ closeDb();
61
+ // Clean up environment variable
62
+ process.env.CELILO_DB_PATH = undefined;
63
+ // Clean up test database
64
+ rmSync(testDir, { recursive: true, force: true });
65
+ });
66
+
67
+ describe('isComplexValue', () => {
68
+ it('should return false for primitives', () => {
69
+ expect(isComplexValue('string')).toBe(false);
70
+ expect(isComplexValue(123)).toBe(false);
71
+ expect(isComplexValue(true)).toBe(false);
72
+ expect(isComplexValue(null)).toBe(false);
73
+ expect(isComplexValue(undefined)).toBe(false);
74
+ });
75
+
76
+ it('should return true for arrays', () => {
77
+ expect(isComplexValue([])).toBe(true);
78
+ expect(isComplexValue([1, 2, 3])).toBe(true);
79
+ expect(isComplexValue(['a', 'b'])).toBe(true);
80
+ });
81
+
82
+ it('should return true for objects', () => {
83
+ expect(isComplexValue({})).toBe(false); // Empty object
84
+ expect(isComplexValue({ key: 'value' })).toBe(true);
85
+ expect(isComplexValue({ a: 1, b: 2 })).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('parseConfigValue', () => {
90
+ it('should parse primitives from strings', async () => {
91
+ expect(await parseConfigValue('hello')).toBe('hello');
92
+ expect(await parseConfigValue('123')).toBe(123);
93
+ expect(await parseConfigValue('true')).toBe(true);
94
+ expect(await parseConfigValue('false')).toBe(false);
95
+ });
96
+
97
+ it('should parse JSON strings', async () => {
98
+ expect(await parseConfigValue('{"key":"value"}')).toEqual({ key: 'value' });
99
+ expect(await parseConfigValue('[1,2,3]')).toEqual([1, 2, 3]);
100
+ });
101
+
102
+ it('should read from @file syntax', async () => {
103
+ const filePath = join(testDir, 'config.json');
104
+ writeFileSync(filePath, JSON.stringify({ key: 'value' }));
105
+
106
+ const result = await parseConfigValue(`@${filePath}`);
107
+ expect(result).toEqual({ key: 'value' });
108
+ });
109
+ });
110
+
111
+ describe('setModuleConfigValue and getModuleConfigValue', () => {
112
+ it('should store and retrieve primitive string', async () => {
113
+ await setModuleConfigValue('test-module', 'string_var', 'hello');
114
+
115
+ const result = getModuleConfigValue('test-module', 'string_var');
116
+ expect(result).not.toBeNull();
117
+ expect(result?.value).toBe('hello');
118
+ expect(result?.isPrimitive).toBe(true);
119
+ });
120
+
121
+ it('should store and retrieve primitive number', async () => {
122
+ await setModuleConfigValue('test-module', 'number_var', '42');
123
+
124
+ const result = getModuleConfigValue('test-module', 'number_var');
125
+ expect(result).not.toBeNull();
126
+ expect(result?.value).toBe(42);
127
+ expect(result?.isPrimitive).toBe(true);
128
+ });
129
+
130
+ it('should store and retrieve primitive boolean', async () => {
131
+ await setModuleConfigValue('test-module', 'bool_var', 'true');
132
+
133
+ const result = getModuleConfigValue('test-module', 'bool_var');
134
+ expect(result).not.toBeNull();
135
+ expect(result?.value).toBe(true);
136
+ expect(result?.isPrimitive).toBe(true);
137
+ });
138
+
139
+ it('should store and retrieve complex array', async () => {
140
+ const arrayValue = JSON.stringify([1, 2, 3]);
141
+ await setModuleConfigValue('test-module', 'array_var', arrayValue);
142
+
143
+ const result = getModuleConfigValue('test-module', 'array_var');
144
+ expect(result).not.toBeNull();
145
+ expect(result?.value).toEqual([1, 2, 3]);
146
+ expect(result?.isPrimitive).toBe(false);
147
+ });
148
+
149
+ it('should store and retrieve complex object', async () => {
150
+ const objectValue = JSON.stringify({ key: 'value', nested: { foo: 'bar' } });
151
+ await setModuleConfigValue('test-module', 'object_var', objectValue);
152
+
153
+ const result = getModuleConfigValue('test-module', 'object_var');
154
+ expect(result).not.toBeNull();
155
+ expect(result?.value).toEqual({ key: 'value', nested: { foo: 'bar' } });
156
+ expect(result?.isPrimitive).toBe(false);
157
+ });
158
+
159
+ it('should update existing config', async () => {
160
+ await setModuleConfigValue('test-module', 'string_var', 'first');
161
+ await setModuleConfigValue('test-module', 'string_var', 'second');
162
+
163
+ const result = getModuleConfigValue('test-module', 'string_var');
164
+ expect(result?.value).toBe('second');
165
+ });
166
+
167
+ it('should reject type mismatch based on manifest (primitive to complex)', async () => {
168
+ // array_var is declared as type: array in manifest
169
+ // Setting it to a string should fail manifest validation
170
+ await expect(
171
+ setModuleConfigValue('test-module', 'array_var', 'simple string'),
172
+ ).rejects.toThrow('Expected array');
173
+ });
174
+
175
+ it('should reject type mismatch based on manifest (complex to primitive)', async () => {
176
+ // string_var is declared as type: string in manifest
177
+ // Setting it to an object (via JSON) should fail manifest validation
178
+ await expect(
179
+ setModuleConfigValue('test-module', 'string_var', '{"key":"value"}'),
180
+ ).rejects.toThrow('Expected string');
181
+ });
182
+ });
183
+
184
+ describe('getAllModuleConfigValues', () => {
185
+ it('should return empty array when no config exists', () => {
186
+ const result = getAllModuleConfigValues('test-module');
187
+ expect(result).toEqual([]);
188
+ });
189
+
190
+ it('should return all config values', async () => {
191
+ await setModuleConfigValue('test-module', 'string_var', 'hello');
192
+ await setModuleConfigValue('test-module', 'number_var', '42');
193
+ await setModuleConfigValue('test-module', 'array_var', '[1,2,3]');
194
+
195
+ const result = getAllModuleConfigValues('test-module');
196
+ expect(result).toHaveLength(3);
197
+
198
+ const stringVar = result.find((c) => c.key === 'string_var');
199
+ expect(stringVar?.value).toBe('hello');
200
+ expect(stringVar?.isPrimitive).toBe(true);
201
+
202
+ const numberVar = result.find((c) => c.key === 'number_var');
203
+ expect(numberVar?.value).toBe(42);
204
+ expect(numberVar?.isPrimitive).toBe(true);
205
+
206
+ const arrayVar = result.find((c) => c.key === 'array_var');
207
+ expect(arrayVar?.value).toEqual([1, 2, 3]);
208
+ expect(arrayVar?.isPrimitive).toBe(false);
209
+ });
210
+ });
211
+
212
+ describe('formatConfigValue', () => {
213
+ it('should format primitive values', () => {
214
+ expect(formatConfigValue({ key: 'test', value: 'hello', isPrimitive: true })).toBe('hello');
215
+ expect(formatConfigValue({ key: 'test', value: 42, isPrimitive: true })).toBe('42');
216
+ expect(formatConfigValue({ key: 'test', value: true, isPrimitive: true })).toBe('true');
217
+ });
218
+
219
+ it('should format complex values as JSON', () => {
220
+ const result = formatConfigValue({
221
+ key: 'test',
222
+ value: [1, 2, 3],
223
+ isPrimitive: false,
224
+ });
225
+ expect(result).toBe('[\n 1,\n 2,\n 3\n]');
226
+ });
227
+
228
+ it('should format object values as JSON', () => {
229
+ const result = formatConfigValue({
230
+ key: 'test',
231
+ value: { key: 'value' },
232
+ isPrimitive: false,
233
+ });
234
+ expect(result).toBe('{\n "key": "value"\n}');
235
+ });
236
+ });
237
+ });
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Module configuration service
3
+ * Handles storage and retrieval of module configuration values
4
+ * Supports both primitive types (string, number, boolean) and complex types (arrays, objects)
5
+ */
6
+
7
+ import { readFile } from 'node:fs/promises';
8
+ import { and, eq } from 'drizzle-orm';
9
+ import { type DbClient, getDb } from '../db/client';
10
+ import { moduleConfigs, modules } from '../db/schema';
11
+ import { formatManifestValidationErrors, validateAgainstManifest } from './manifest-validation';
12
+ import { formatValidationErrors, validateConfigValue } from './schema-validation';
13
+
14
+ export interface ConfigValue {
15
+ key: string;
16
+ value: string | number | boolean | unknown[] | Record<string, unknown>;
17
+ isPrimitive: boolean;
18
+ }
19
+
20
+ /**
21
+ * Check if a value is a complex type (array or object)
22
+ */
23
+ export function isComplexValue(value: unknown): boolean {
24
+ if (value === null || typeof value !== 'object') {
25
+ return false;
26
+ }
27
+
28
+ // Arrays are complex
29
+ if (Array.isArray(value)) {
30
+ return true;
31
+ }
32
+
33
+ // Non-empty objects are complex
34
+ return Object.keys(value).length > 0;
35
+ }
36
+
37
+ /**
38
+ * Parse a configuration value
39
+ * Handles @file syntax for reading from files
40
+ * Handles JSON strings for complex types
41
+ */
42
+ export async function parseConfigValue(
43
+ valueStr: string,
44
+ ): Promise<string | number | boolean | unknown[] | Record<string, unknown>> {
45
+ // Handle @file syntax
46
+ if (valueStr.startsWith('@')) {
47
+ const filePath = valueStr.slice(1);
48
+ const fileContent = await readFile(filePath, 'utf-8');
49
+ return JSON.parse(fileContent);
50
+ }
51
+
52
+ // Try to parse as JSON (for complex types or JSON strings)
53
+ try {
54
+ const parsed = JSON.parse(valueStr);
55
+ return parsed;
56
+ } catch {
57
+ // Not valid JSON, treat as string
58
+ // Check for boolean values
59
+ if (valueStr === 'true') return true;
60
+ if (valueStr === 'false') return false;
61
+
62
+ // Check for number values
63
+ const num = Number(valueStr);
64
+ if (!Number.isNaN(num) && valueStr.trim() !== '') {
65
+ return num;
66
+ }
67
+
68
+ // Return as string
69
+ return valueStr;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get module configuration value
75
+ * Returns parsed value (primitive or complex type)
76
+ */
77
+ export function getModuleConfigValue(
78
+ moduleId: string,
79
+ key: string,
80
+ db: DbClient = getDb(),
81
+ ): ConfigValue | null {
82
+ const config = db
83
+ .select()
84
+ .from(moduleConfigs)
85
+ .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
86
+ .get();
87
+
88
+ if (!config) {
89
+ return null;
90
+ }
91
+
92
+ // Check if complex type (stored in valueJson)
93
+ if (config.valueJson) {
94
+ try {
95
+ const parsed = JSON.parse(config.valueJson);
96
+ return {
97
+ key: config.key,
98
+ value: parsed,
99
+ isPrimitive: false,
100
+ };
101
+ } catch (error) {
102
+ throw new Error(
103
+ `Failed to parse config value for ${moduleId}.${config.key}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
104
+ );
105
+ }
106
+ }
107
+
108
+ // Primitive type (stored in value)
109
+ // Parse the string value to correct type
110
+ let parsedValue: string | number | boolean = config.value;
111
+
112
+ // Boolean
113
+ if (config.value === 'true') parsedValue = true;
114
+ else if (config.value === 'false') parsedValue = false;
115
+ // Number
116
+ else {
117
+ const num = Number(config.value);
118
+ if (!Number.isNaN(num)) {
119
+ parsedValue = num;
120
+ }
121
+ }
122
+
123
+ return {
124
+ key: config.key,
125
+ value: parsedValue,
126
+ isPrimitive: true,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Get all module configuration values
132
+ */
133
+ export function getAllModuleConfigValues(moduleId: string, db: DbClient = getDb()): ConfigValue[] {
134
+ const configs = db.select().from(moduleConfigs).where(eq(moduleConfigs.moduleId, moduleId)).all();
135
+
136
+ return configs.map((config: typeof moduleConfigs.$inferSelect) => {
137
+ // Check if complex type
138
+ if (config.valueJson) {
139
+ try {
140
+ const parsed = JSON.parse(config.valueJson);
141
+ return {
142
+ key: config.key,
143
+ value: parsed,
144
+ isPrimitive: false,
145
+ };
146
+ } catch (error) {
147
+ throw new Error(
148
+ `Failed to parse config value for ${moduleId}.${config.key}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
149
+ );
150
+ }
151
+ }
152
+
153
+ // Primitive type
154
+ let parsedValue: string | number | boolean = config.value;
155
+
156
+ // Boolean
157
+ if (config.value === 'true') parsedValue = true;
158
+ else if (config.value === 'false') parsedValue = false;
159
+ // Number
160
+ else {
161
+ const num = Number(config.value);
162
+ if (!Number.isNaN(num)) {
163
+ parsedValue = num;
164
+ }
165
+ }
166
+
167
+ return {
168
+ key: config.key,
169
+ value: parsedValue,
170
+ isPrimitive: true,
171
+ };
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Set module configuration value
177
+ * Automatically detects primitive vs complex types and stores appropriately
178
+ */
179
+ export async function setModuleConfigValue(
180
+ moduleId: string,
181
+ key: string,
182
+ valueStr: string,
183
+ db: DbClient = getDb(),
184
+ ): Promise<void> {
185
+ // Parse the value
186
+ const parsedValue = await parseConfigValue(valueStr);
187
+
188
+ // Get module manifest for validation
189
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
190
+ if (!module) {
191
+ throw new Error(`Module not found: ${moduleId}`);
192
+ }
193
+
194
+ const manifest = module.manifestData as Record<string, unknown>;
195
+ const variables = manifest.variables as
196
+ | {
197
+ owns?: Array<{
198
+ name: string;
199
+ type?: string;
200
+ minimum?: number;
201
+ maximum?: number;
202
+ pattern?: string;
203
+ }>;
204
+ }
205
+ | undefined;
206
+ const declaredVars = variables?.owns || [];
207
+ const variable = declaredVars.find((v) => v.name === key);
208
+
209
+ // Validate against manifest type (if type is declared)
210
+ if (variable?.type) {
211
+ const manifestValidation = validateAgainstManifest(parsedValue, {
212
+ name: variable.name,
213
+ type: variable.type as 'string' | 'integer' | 'number' | 'boolean' | 'array' | 'object',
214
+ minimum: variable.minimum,
215
+ maximum: variable.maximum,
216
+ pattern: variable.pattern,
217
+ });
218
+
219
+ if (!manifestValidation.valid) {
220
+ throw new Error(formatManifestValidationErrors(manifestValidation.errors || []));
221
+ }
222
+ }
223
+
224
+ // Validate against JSON Schema (if schema exists)
225
+ // This provides deeper validation for complex types
226
+ const validation = await validateConfigValue(moduleId, key, parsedValue, db);
227
+ if (!validation.valid) {
228
+ throw new Error(formatValidationErrors(validation.errors || []));
229
+ }
230
+
231
+ // Determine if complex type
232
+ const isComplex = isComplexValue(parsedValue);
233
+
234
+ // Check if config already exists
235
+ const existingConfig = db
236
+ .select()
237
+ .from(moduleConfigs)
238
+ .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
239
+ .get();
240
+
241
+ if (existingConfig) {
242
+ // Update existing config
243
+ if (isComplex) {
244
+ // Store in valueJson column
245
+ db.update(moduleConfigs)
246
+ .set({
247
+ value: '', // Empty string for complex types
248
+ valueJson: JSON.stringify(parsedValue),
249
+ updatedAt: new Date(),
250
+ })
251
+ .where(eq(moduleConfigs.id, existingConfig.id))
252
+ .run();
253
+ } else {
254
+ // Store in value column
255
+ db.update(moduleConfigs)
256
+ .set({
257
+ value: String(parsedValue),
258
+ valueJson: null,
259
+ updatedAt: new Date(),
260
+ })
261
+ .where(eq(moduleConfigs.id, existingConfig.id))
262
+ .run();
263
+ }
264
+ } else {
265
+ // Insert new config
266
+ if (isComplex) {
267
+ db.insert(moduleConfigs)
268
+ .values({
269
+ moduleId,
270
+ key,
271
+ value: '', // Empty string for complex types
272
+ valueJson: JSON.stringify(parsedValue),
273
+ })
274
+ .run();
275
+ } else {
276
+ db.insert(moduleConfigs)
277
+ .values({
278
+ moduleId,
279
+ key,
280
+ value: String(parsedValue),
281
+ valueJson: null,
282
+ })
283
+ .run();
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Format a config value for display
290
+ */
291
+ export function formatConfigValue(configValue: ConfigValue): string {
292
+ if (configValue.isPrimitive) {
293
+ return String(configValue.value);
294
+ }
295
+
296
+ // Complex type - format as JSON
297
+ return JSON.stringify(configValue.value, null, 2);
298
+ }