@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,322 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { parseVariables } from '../variables/parser';
4
+ import type { VariableReference } from '../variables/types';
5
+ import type { ModuleManifest } from './schema';
6
+
7
+ /**
8
+ * Template validation error
9
+ */
10
+ export interface TemplateValidationError {
11
+ file: string;
12
+ variable: string;
13
+ error: string;
14
+ }
15
+
16
+ /**
17
+ * Template validation result
18
+ */
19
+ export interface TemplateValidationResult {
20
+ success: boolean;
21
+ errors: TemplateValidationError[];
22
+ }
23
+
24
+ /**
25
+ * Get nested value from object using dot notation
26
+ * Returns undefined if path doesn't exist
27
+ *
28
+ * @param obj - Object to traverse
29
+ * @param path - Dot-separated path (e.g., 'requires.machine.cpu')
30
+ * @returns Value if found, undefined otherwise
31
+ */
32
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
33
+ const parts = path.split('.');
34
+ let current: unknown = obj;
35
+
36
+ for (const part of parts) {
37
+ if (current === null || current === undefined) {
38
+ return undefined;
39
+ }
40
+ if (typeof current !== 'object') {
41
+ return undefined;
42
+ }
43
+ current = (current as Record<string, unknown>)[part];
44
+ }
45
+
46
+ return current;
47
+ }
48
+
49
+ /**
50
+ * Check if a path exists in the manifest
51
+ *
52
+ * @param manifest - Module manifest
53
+ * @param path - Dot-separated path (e.g., 'requires.machine.cpu')
54
+ * @returns True if path exists in manifest
55
+ */
56
+ function pathExistsInManifest(manifest: ModuleManifest, path: string): boolean {
57
+ // Convert manifest to a plain object we can traverse
58
+ const manifestObj = manifest as unknown as Record<string, unknown>;
59
+ const value = getNestedValue(manifestObj, path);
60
+ return value !== undefined;
61
+ }
62
+
63
+ /**
64
+ * Auto-allocated variables that are injected during module generation
65
+ * These don't need to be declared in the manifest
66
+ *
67
+ * IPAM auto-allocation and zone-based derivation
68
+ */
69
+ const AUTO_ALLOCATED_VARIABLES = new Set([
70
+ 'vmid', // Auto-allocated by IPAM (container-based modules)
71
+ 'container_ip', // Auto-allocated by IPAM (container-based modules)
72
+ 'vlan', // Auto-derived from zone configuration
73
+ 'gateway', // Auto-derived from zone configuration
74
+ 'target_node', // Can be auto-derived from system config
75
+ ]);
76
+
77
+ /**
78
+ * Check if a variable is declared in the manifest or auto-allocated
79
+ *
80
+ * @param manifest - Module manifest
81
+ * @param variableName - Variable name to check
82
+ * @returns True if variable is declared or auto-allocated
83
+ */
84
+ function isVariableDeclared(manifest: ModuleManifest, variableName: string): boolean {
85
+ // Check if it's an auto-allocated variable
86
+ if (AUTO_ALLOCATED_VARIABLES.has(variableName)) {
87
+ return true;
88
+ }
89
+
90
+ // Check in variables.owns
91
+ const declared = manifest.variables?.owns?.some((v) => v.name === variableName);
92
+ if (declared) return true;
93
+
94
+ // Check in variables.imports
95
+ const imported = manifest.variables?.imports?.some((v) => v.name === variableName);
96
+ if (imported) return true;
97
+
98
+ return false;
99
+ }
100
+
101
+ /**
102
+ * Validate a $self: variable reference
103
+ *
104
+ * Policy function - validates variable path against manifest structure
105
+ *
106
+ * @param variable - Variable reference to validate
107
+ * @param manifest - Module manifest
108
+ * @returns Error message if invalid, null if valid
109
+ */
110
+ function validateSelfVariable(
111
+ variable: VariableReference,
112
+ manifest: ModuleManifest,
113
+ ): string | null {
114
+ const path = variable.path;
115
+
116
+ // Check if it's a direct variable reference (e.g., $self:hostname)
117
+ if (!path.includes('.')) {
118
+ if (isVariableDeclared(manifest, path)) {
119
+ return null; // Valid - variable is declared
120
+ }
121
+ // Check if it exists as a top-level manifest field
122
+ if (pathExistsInManifest(manifest, path)) {
123
+ return null; // Valid - exists in manifest
124
+ }
125
+ return `Self variable '${path}' not found in module configuration`;
126
+ }
127
+
128
+ // Check if it's a nested path (e.g., $self:requires.machine.cpu)
129
+ if (pathExistsInManifest(manifest, path)) {
130
+ return null; // Valid
131
+ }
132
+
133
+ return `Self variable '${path}' not found in module configuration`;
134
+ }
135
+
136
+ /**
137
+ * Validate a $capability: variable reference
138
+ *
139
+ * Policy function - validates capability references against manifest requirements
140
+ *
141
+ * @param variable - Variable reference to validate
142
+ * @param manifest - Module manifest
143
+ * @returns Error message if invalid, null if valid
144
+ */
145
+ function validateCapabilityVariable(
146
+ variable: VariableReference,
147
+ manifest: ModuleManifest,
148
+ ): string | null {
149
+ const [capabilityName, ...pathParts] = variable.path.split('.');
150
+
151
+ if (!capabilityName) {
152
+ return 'Capability variable must specify capability name (e.g., dns_registrar.primary_domain)';
153
+ }
154
+
155
+ if (pathParts.length === 0) {
156
+ return `Capability variable must specify data path (e.g., ${capabilityName}.nameserver)`;
157
+ }
158
+
159
+ // Check if module requires this capability
160
+ const requiresCapability = manifest.requires?.capabilities?.some(
161
+ (cap) => cap.name === capabilityName,
162
+ );
163
+
164
+ if (!requiresCapability) {
165
+ return `Module references capability '${capabilityName}' but does not require it in manifest`;
166
+ }
167
+
168
+ return null; // Valid - capability is required
169
+ }
170
+
171
+ /**
172
+ * Validate variable references in a template
173
+ *
174
+ * Policy function - validates template variables against manifest
175
+ *
176
+ * @param content - Template content
177
+ * @param manifest - Module manifest
178
+ * @param relativePath - Relative file path (for error reporting)
179
+ * @returns Array of validation errors
180
+ */
181
+ function validateTemplateContent(
182
+ content: string,
183
+ manifest: ModuleManifest,
184
+ relativePath: string,
185
+ ): TemplateValidationError[] {
186
+ const errors: TemplateValidationError[] = [];
187
+ const variables = parseVariables(content);
188
+
189
+ for (const variable of variables) {
190
+ let error: string | null = null;
191
+
192
+ switch (variable.type) {
193
+ case 'self':
194
+ error = validateSelfVariable(variable, manifest);
195
+ break;
196
+
197
+ case 'capability':
198
+ error = validateCapabilityVariable(variable, manifest);
199
+ break;
200
+
201
+ case 'system':
202
+ case 'system_secret':
203
+ case 'secret':
204
+ // These are validated at runtime, not import time
205
+ // System config and secrets might not exist yet during import
206
+ break;
207
+ }
208
+
209
+ if (error) {
210
+ errors.push({
211
+ file: relativePath,
212
+ variable: variable.raw,
213
+ error,
214
+ });
215
+ }
216
+ }
217
+
218
+ return errors;
219
+ }
220
+
221
+ /**
222
+ * Find all template files in a directory recursively
223
+ *
224
+ * @param dirPath - Directory to search
225
+ * @param basePath - Base path for relative path calculation
226
+ * @returns Array of template file paths (relative to basePath)
227
+ */
228
+ async function findTemplateFiles(dirPath: string, basePath: string): Promise<string[]> {
229
+ const templateFiles: string[] = [];
230
+
231
+ async function walkDir(currentPath: string) {
232
+ const entries = await readdir(currentPath, { withFileTypes: true });
233
+
234
+ for (const entry of entries) {
235
+ const fullPath = join(currentPath, entry.name);
236
+
237
+ if (entry.isDirectory()) {
238
+ await walkDir(fullPath);
239
+ } else if (entry.isFile() && entry.name.endsWith('.tpl')) {
240
+ // Store relative path from basePath
241
+ const relativePath = fullPath.substring(basePath.length + 1);
242
+ templateFiles.push(relativePath);
243
+ }
244
+ }
245
+ }
246
+
247
+ await walkDir(dirPath);
248
+ return templateFiles;
249
+ }
250
+
251
+ /**
252
+ * Validate all template files in a module directory
253
+ *
254
+ * Policy function - validates template variable references against manifest
255
+ *
256
+ * @param modulePath - Path to module directory
257
+ * @param manifest - Validated module manifest
258
+ * @returns Validation result with errors if any
259
+ */
260
+ export async function validateModuleTemplates(
261
+ modulePath: string,
262
+ manifest: ModuleManifest,
263
+ ): Promise<TemplateValidationResult> {
264
+ const allErrors: TemplateValidationError[] = [];
265
+
266
+ try {
267
+ // Find all .tpl files
268
+ const templateFiles = await findTemplateFiles(modulePath, modulePath);
269
+
270
+ // Validate each template file
271
+ for (const relativePath of templateFiles) {
272
+ const fullPath = join(modulePath, relativePath);
273
+ const content = await readFile(fullPath, 'utf-8');
274
+ const errors = validateTemplateContent(content, manifest, relativePath);
275
+ allErrors.push(...errors);
276
+ }
277
+
278
+ return {
279
+ success: allErrors.length === 0,
280
+ errors: allErrors,
281
+ };
282
+ } catch (error) {
283
+ return {
284
+ success: false,
285
+ errors: [
286
+ {
287
+ file: '<directory>',
288
+ variable: '',
289
+ error: `Failed to validate templates: ${error instanceof Error ? error.message : 'Unknown error'}`,
290
+ },
291
+ ],
292
+ };
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Format template validation errors into a readable error message
298
+ *
299
+ * @param errors - Array of validation errors
300
+ * @returns Formatted error message
301
+ */
302
+ export function formatTemplateValidationErrors(errors: TemplateValidationError[]): string {
303
+ const lines: string[] = ['Failed to validate template variables:'];
304
+
305
+ // Group errors by file
306
+ const errorsByFile = new Map<string, TemplateValidationError[]>();
307
+ for (const error of errors) {
308
+ const fileErrors = errorsByFile.get(error.file) || [];
309
+ fileErrors.push(error);
310
+ errorsByFile.set(error.file, fileErrors);
311
+ }
312
+
313
+ // Format errors by file
314
+ for (const [file, fileErrors] of errorsByFile) {
315
+ lines.push(` ${file}:`);
316
+ for (const error of fileErrors) {
317
+ lines.push(` ${error.variable}: ${error.error}`);
318
+ }
319
+ }
320
+
321
+ return lines.join('\n');
322
+ }