@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,142 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { convertSecretsToJinja, isAnsibleTemplate } from './ansible-resolver';
3
+ import { buildContextFromData } from './context';
4
+
5
+ // Mock database for tests
6
+ const mockDb = {
7
+ $client: {
8
+ prepare: () => ({
9
+ get: () => null,
10
+ all: () => [],
11
+ run: () => ({ changes: 0 }),
12
+ }),
13
+ },
14
+ select: () => ({
15
+ from: () => ({
16
+ where: () => ({
17
+ get: () => null,
18
+ all: () => [],
19
+ }),
20
+ }),
21
+ }),
22
+ // biome-ignore lint/suspicious/noExplicitAny: mock database for testing, full type not needed
23
+ } as any;
24
+
25
+ describe('Ansible Resolver', () => {
26
+ describe('isAnsibleTemplate', () => {
27
+ test('should identify .j2 files', async () => {
28
+ expect(isAnsibleTemplate('config.json.j2')).toBe(true);
29
+ expect(isAnsibleTemplate('template.j2')).toBe(true);
30
+ });
31
+
32
+ test('should identify ansible/ directory files', async () => {
33
+ expect(isAnsibleTemplate('ansible/playbook.yml')).toBe(true);
34
+ expect(isAnsibleTemplate('ansible/roles/main.yml')).toBe(true);
35
+ });
36
+
37
+ test('should reject non-ansible files', async () => {
38
+ expect(isAnsibleTemplate('terraform/main.tf')).toBe(false);
39
+ expect(isAnsibleTemplate('config.json')).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe('convertSecretsToJinja', () => {
44
+ test('should convert secret variables to Jinja2 format', async () => {
45
+ const context = buildContextFromData('test', {
46
+ secrets: {
47
+ api_key: 'secret123',
48
+ db_password: 'pass456',
49
+ },
50
+ });
51
+
52
+ const template = 'username: $secret:api_key\npassword: $secret:db_password';
53
+ const result = await convertSecretsToJinja(template, context, mockDb);
54
+
55
+ expect(result.success).toBe(true);
56
+ if (result.success) {
57
+ expect(result.content).toBe('username: {{ api_key }}\npassword: {{ db_password }}');
58
+ }
59
+ });
60
+
61
+ test('should resolve non-secret variables normally', async () => {
62
+ const context = buildContextFromData('test', {
63
+ selfConfig: {
64
+ hostname: 'myhost',
65
+ port: '8080',
66
+ },
67
+ systemConfig: {
68
+ 'dns.primary': '192.168.0.1',
69
+ },
70
+ });
71
+
72
+ const template = 'host: $self:hostname\nport: $self:port\ndns: $system:dns.primary';
73
+ const result = await convertSecretsToJinja(template, context, mockDb);
74
+
75
+ expect(result.success).toBe(true);
76
+ if (result.success) {
77
+ expect(result.content).toBe('host: myhost\nport: 8080\ndns: 192.168.0.1');
78
+ }
79
+ });
80
+
81
+ test('should mix secret conversion and normal resolution', async () => {
82
+ const context = buildContextFromData('test', {
83
+ selfConfig: {
84
+ username: 'admin',
85
+ },
86
+ secrets: {
87
+ password: 'secret123',
88
+ },
89
+ });
90
+
91
+ const template = 'user: $self:username\npass: $secret:password';
92
+ const result = await convertSecretsToJinja(template, context, mockDb);
93
+
94
+ expect(result.success).toBe(true);
95
+ if (result.success) {
96
+ expect(result.content).toBe('user: admin\npass: {{ password }}');
97
+ }
98
+ });
99
+
100
+ test('should return errors for missing non-secret variables', async () => {
101
+ const context = buildContextFromData('test', {});
102
+
103
+ const template = 'host: $self:missing_hostname';
104
+ const result = await convertSecretsToJinja(template, context, mockDb);
105
+
106
+ expect(result.success).toBe(false);
107
+ if (!result.success) {
108
+ expect(result.errors).toHaveLength(1);
109
+ expect(result.errors[0]?.variable).toBe('$self:missing_hostname');
110
+ }
111
+ });
112
+
113
+ test('should convert secrets even if not in context', async () => {
114
+ const context = buildContextFromData('test', {});
115
+
116
+ const template = 'pass: $secret:api_key';
117
+ const result = await convertSecretsToJinja(template, context, mockDb);
118
+
119
+ // Secrets are always converted to Jinja2, regardless of presence
120
+ expect(result.success).toBe(true);
121
+ if (result.success) {
122
+ expect(result.content).toBe('pass: {{ api_key }}');
123
+ }
124
+ });
125
+
126
+ test('should handle multiple occurrences of same variable', async () => {
127
+ const context = buildContextFromData('test', {
128
+ secrets: {
129
+ token: 'xyz',
130
+ },
131
+ });
132
+
133
+ const template = 'auth: $secret:token\nbackup: $secret:token';
134
+ const result = await convertSecretsToJinja(template, context, mockDb);
135
+
136
+ expect(result.success).toBe(true);
137
+ if (result.success) {
138
+ expect(result.content).toBe('auth: {{ token }}\nbackup: {{ token }}');
139
+ }
140
+ });
141
+ });
142
+ });
@@ -0,0 +1,69 @@
1
+ import { parseVariables } from './parser';
2
+ import { resolveVariable } from './resolver';
3
+ import type { ResolutionContext, TemplateResolveResult } from './types';
4
+
5
+ /**
6
+ * Convert secret variables to Jinja2 format for Ansible
7
+ *
8
+ * Execution function (Rule 10.1) - may perform database access
9
+ *
10
+ * Converts $secret:name to {{ name }} for Ansible templates
11
+ * Resolves non-secret variables normally ($self:, $system:, $capability:)
12
+ *
13
+ * @param content - Template content with variables
14
+ * @param context - Resolution context
15
+ * @param db - Database connection (required for capability variables)
16
+ * @returns Resolved template with Jinja2 variables for secrets
17
+ */
18
+ export async function convertSecretsToJinja(
19
+ content: string,
20
+ context: ResolutionContext,
21
+ db: ReturnType<typeof import('../db/client').getDb>,
22
+ ): Promise<TemplateResolveResult> {
23
+ // Parse all variables
24
+ const variables = parseVariables(content);
25
+
26
+ if (variables.length === 0) {
27
+ return { success: true, content };
28
+ }
29
+
30
+ const errors: Array<{ variable: string; error: string }> = [];
31
+ let resolvedContent = content;
32
+
33
+ // Process each variable
34
+ for (const variable of variables) {
35
+ if (variable.type === 'secret') {
36
+ // Convert secret variables to Jinja2 format
37
+ // $secret:vesync_username -> {{ vesync_username }}
38
+ const jinjaVar = `{{ ${variable.path} }}`;
39
+ resolvedContent = resolvedContent.replaceAll(variable.raw, jinjaVar);
40
+ } else {
41
+ // Resolve non-secret variables normally
42
+ const result = await resolveVariable(variable, context, db);
43
+
44
+ if (!result.success) {
45
+ errors.push({ variable: result.variable, error: result.error });
46
+ } else {
47
+ resolvedContent = resolvedContent.replaceAll(variable.raw, result.value);
48
+ }
49
+ }
50
+ }
51
+
52
+ if (errors.length > 0) {
53
+ return { success: false, errors };
54
+ }
55
+
56
+ return { success: true, content: resolvedContent };
57
+ }
58
+
59
+ /**
60
+ * Check if file is an Ansible template
61
+ *
62
+ * Policy function - validation only
63
+ *
64
+ * @param path - File path
65
+ * @returns True if file is Ansible template (.j2 extension or in ansible/ directory)
66
+ */
67
+ export function isAnsibleTemplate(path: string): boolean {
68
+ return path.endsWith('.j2') || path.includes('/ansible/') || path.startsWith('ansible/');
69
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Tests for capability data $self: variable resolution (lazy resolution)
3
+ * Verifies that capability data containing unresolved $self: variables
4
+ * gets resolved from the provider module's config when requested
5
+ */
6
+
7
+ import { afterEach, describe, expect, test } from 'bun:test';
8
+ import { mkdtempSync, rmSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { createDbClient } from '@/db/client';
12
+ import { resolveVariable } from './resolver';
13
+ import type { ResolutionContext } from './types';
14
+
15
+ let testDirs: string[] = [];
16
+
17
+ afterEach(() => {
18
+ for (const dir of testDirs) {
19
+ try {
20
+ rmSync(dir, { recursive: true, force: true });
21
+ } catch {
22
+ // Ignore
23
+ }
24
+ }
25
+ testDirs = [];
26
+ });
27
+
28
+ describe('Capability data $self: variable resolution', () => {
29
+ test('should lazily resolve $self: variables in capability data', async () => {
30
+ const testDir = mkdtempSync(join(tmpdir(), 'test-cap-self-'));
31
+ testDirs.push(testDir);
32
+ const testDbPath = join(testDir, 'test.db');
33
+ const db = createDbClient({ path: testDbPath });
34
+
35
+ // Create provider module (dns-external)
36
+ db.$client
37
+ .prepare(
38
+ `INSERT INTO modules (id, name, version, source_path, state, manifest_data)
39
+ VALUES (?, ?, ?, ?, ?, ?)`,
40
+ )
41
+ .run(
42
+ 'dns-external',
43
+ 'DNS External',
44
+ '1.0.0',
45
+ '/tmp/modules/dns-external',
46
+ 'CONFIGURED',
47
+ JSON.stringify({ id: 'dns-external', name: 'DNS External', version: '1.0.0' }),
48
+ );
49
+
50
+ // Register capability with unresolved $self: variables
51
+ db.$client
52
+ .prepare(
53
+ `INSERT INTO capabilities (module_id, capability_name, version, data)
54
+ VALUES (?, ?, ?, ?)`,
55
+ )
56
+ .run(
57
+ 'dns-external',
58
+ 'dns_external',
59
+ '1.0.0',
60
+ JSON.stringify({
61
+ server: {
62
+ ip: {
63
+ primary: '$self:ip.primary', // Unresolved!
64
+ },
65
+ },
66
+ tsig: {
67
+ key_name: '$self:tsig_key_name', // Unresolved!
68
+ },
69
+ }),
70
+ );
71
+
72
+ // Store provider module's config
73
+ db.$client
74
+ .prepare(
75
+ `INSERT INTO module_configs (module_id, key, value)
76
+ VALUES (?, ?, ?), (?, ?, ?)`,
77
+ )
78
+ .run(
79
+ 'dns-external',
80
+ 'ip.primary',
81
+ '203.0.113.42',
82
+ 'dns-external',
83
+ 'tsig_key_name',
84
+ 'celilo-ddns',
85
+ );
86
+
87
+ // Create consumer module (caddy)
88
+ db.$client
89
+ .prepare(
90
+ `INSERT INTO modules (id, name, version, source_path, state, manifest_data)
91
+ VALUES (?, ?, ?, ?, ?, ?)`,
92
+ )
93
+ .run(
94
+ 'caddy',
95
+ 'Caddy',
96
+ '1.0.0',
97
+ '/tmp/modules/caddy',
98
+ 'CONFIGURED',
99
+ JSON.stringify({ id: 'caddy', name: 'Caddy', version: '1.0.0' }),
100
+ );
101
+
102
+ // Test: Resolve capability variable with lazy $self: resolution
103
+ const context: ResolutionContext = {
104
+ moduleId: 'caddy',
105
+ selfConfig: {},
106
+ systemConfig: {},
107
+ secrets: {},
108
+ systemSecrets: {},
109
+ capabilities: {
110
+ dns_external: {
111
+ server: {
112
+ ip: {
113
+ primary: '$self:ip.primary', // Will be lazily resolved
114
+ },
115
+ },
116
+ tsig: {
117
+ key_name: '$self:tsig_key_name', // Will be lazily resolved
118
+ },
119
+ },
120
+ },
121
+ };
122
+
123
+ const result1 = await resolveVariable(
124
+ {
125
+ type: 'capability',
126
+ path: 'dns_external.server.ip.primary',
127
+ raw: '$capability:dns_external.server.ip.primary',
128
+ },
129
+ context,
130
+ db,
131
+ );
132
+
133
+ expect(result1.success).toBe(true);
134
+ if (result1.success) {
135
+ expect(result1.value).toBe('203.0.113.42');
136
+ }
137
+
138
+ const result2 = await resolveVariable(
139
+ {
140
+ type: 'capability',
141
+ path: 'dns_external.tsig.key_name',
142
+ raw: '$capability:dns_external.tsig.key_name',
143
+ },
144
+ context,
145
+ db,
146
+ );
147
+
148
+ expect(result2.success).toBe(true);
149
+ if (result2.success) {
150
+ expect(result2.value).toBe('celilo-ddns');
151
+ }
152
+ });
153
+
154
+ test('should error when provider module has not configured the required value', async () => {
155
+ const testDir = mkdtempSync(join(tmpdir(), 'test-cap-self-'));
156
+ testDirs.push(testDir);
157
+ const testDbPath = join(testDir, 'test.db');
158
+ const db = createDbClient({ path: testDbPath });
159
+
160
+ // Create provider module
161
+ db.$client
162
+ .prepare(
163
+ `INSERT INTO modules (id, name, version, source_path, state, manifest_data)
164
+ VALUES (?, ?, ?, ?, ?, ?)`,
165
+ )
166
+ .run(
167
+ 'dns-external',
168
+ 'DNS External',
169
+ '1.0.0',
170
+ '/tmp/modules/dns-external',
171
+ 'CONFIGURED',
172
+ JSON.stringify({ id: 'dns-external', name: 'DNS External', version: '1.0.0' }),
173
+ );
174
+
175
+ // Register capability with unresolved variable
176
+ db.$client
177
+ .prepare(
178
+ `INSERT INTO capabilities (module_id, capability_name, version, data)
179
+ VALUES (?, ?, ?, ?)`,
180
+ )
181
+ .run(
182
+ 'dns-external',
183
+ 'dns_external',
184
+ '1.0.0',
185
+ JSON.stringify({
186
+ server: { ip: { primary: '$self:ip.primary' } },
187
+ }),
188
+ );
189
+
190
+ // Provider module config is MISSING the required value
191
+
192
+ const context: ResolutionContext = {
193
+ moduleId: 'caddy',
194
+ selfConfig: {},
195
+ systemConfig: {},
196
+ secrets: {},
197
+ systemSecrets: {},
198
+ capabilities: {
199
+ dns_external: {
200
+ server: { ip: { primary: '$self:ip.primary' } },
201
+ },
202
+ },
203
+ };
204
+
205
+ const result = await resolveVariable(
206
+ {
207
+ type: 'capability',
208
+ path: 'dns_external.server.ip.primary',
209
+ raw: '$capability:dns_external.server.ip.primary',
210
+ },
211
+ context,
212
+ db,
213
+ );
214
+
215
+ expect(result.success).toBe(false);
216
+ if (!result.success) {
217
+ expect(result.error).toContain("has not configured 'ip.primary'");
218
+ }
219
+ });
220
+ });