@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,676 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, statSync, writeFileSync } from 'node:fs';
3
+ import { copyFile, mkdir, readFile, readdir } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { eq } from 'drizzle-orm';
6
+ import { z } from 'zod';
7
+ import { getWellKnownCapability, isWellKnown } from '../capabilities/well-known';
8
+ import { log } from '../cli/prompts';
9
+ import { getModuleStoragePath } from '../config/paths';
10
+ import { type DbClient, getDb } from '../db/client';
11
+ import { capabilities, moduleIntegrity, modules } from '../db/schema';
12
+ import type { NewModule, NewModuleIntegrity } from '../db/schema';
13
+ import type { ModuleManifest } from '../manifest/schema';
14
+ import {
15
+ validateCapabilityNames,
16
+ validateDeriveFromSources,
17
+ validateHookContract,
18
+ validateManifest,
19
+ validateProvidesNoCrossCapabilityRefs,
20
+ validateVariableSources,
21
+ validateZoneRequirements,
22
+ } from '../manifest/validate';
23
+ import { parseJsonWithValidation } from '../validation/schemas';
24
+ import { cleanupTempDir, extractPackage, verifyPackageIntegrity } from './packaging/extract';
25
+
26
+ /**
27
+ * Module import options
28
+ */
29
+ export interface ModuleImportOptions {
30
+ sourcePath: string;
31
+ targetBasePath?: string;
32
+ db?: DbClient;
33
+ flags?: Record<string, string | boolean>;
34
+ }
35
+
36
+ /**
37
+ * Module import result
38
+ */
39
+ export interface ModuleImportSuccess {
40
+ success: true;
41
+ moduleId: string;
42
+ targetPath: string;
43
+ }
44
+
45
+ export interface ModuleImportError {
46
+ success: false;
47
+ error: string;
48
+ details?: unknown;
49
+ }
50
+
51
+ export type ModuleImportResult = ModuleImportSuccess | ModuleImportError;
52
+
53
+ /**
54
+ * Get default target base path for module storage
55
+ * Uses platform-specific defaults with environment variable overrides
56
+ */
57
+ function getDefaultTargetBase(): string {
58
+ return getModuleStoragePath();
59
+ }
60
+
61
+ /**
62
+ * Validate module directory structure
63
+ *
64
+ * Policy function (Rule 10.1) - validates input only, no side effects
65
+ *
66
+ * @param sourcePath - Path to module directory
67
+ * @returns Error message if invalid, null if valid
68
+ */
69
+ export function validateModuleDirectory(sourcePath: string): string | null {
70
+ // Check directory exists
71
+ if (!existsSync(sourcePath)) {
72
+ return `Module directory does not exist: ${sourcePath}`;
73
+ }
74
+
75
+ // Check it's a directory
76
+ const stats = statSync(sourcePath);
77
+ if (!stats.isDirectory()) {
78
+ return `Path is not a directory: ${sourcePath}`;
79
+ }
80
+
81
+ // Check manifest.yml exists
82
+ const manifestPath = join(sourcePath, 'manifest.yml');
83
+ if (!existsSync(manifestPath)) {
84
+ return 'manifest.yml not found in module directory';
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Read and validate manifest from module directory
92
+ *
93
+ * Policy function - reads and validates, no database access
94
+ *
95
+ * @param sourcePath - Path to module directory
96
+ * @returns Validated manifest or error
97
+ */
98
+ export async function readModuleManifest(
99
+ sourcePath: string,
100
+ ): Promise<{ success: true; manifest: ModuleManifest } | { success: false; error: string }> {
101
+ const manifestPath = join(sourcePath, 'manifest.yml');
102
+
103
+ let yamlContent: string;
104
+ try {
105
+ yamlContent = await readFile(manifestPath, 'utf-8');
106
+ } catch (error) {
107
+ return {
108
+ success: false,
109
+ error: `Failed to read manifest.yml: ${error instanceof Error ? error.message : 'Unknown error'}`,
110
+ };
111
+ }
112
+
113
+ const validationResult = validateManifest(yamlContent);
114
+
115
+ if (!validationResult.success) {
116
+ const errorMessages = validationResult.errors.map((e) => `${e.path}: ${e.message}`).join(', ');
117
+ return {
118
+ success: false,
119
+ error: `Manifest validation failed: ${errorMessages}`,
120
+ };
121
+ }
122
+
123
+ const zoneValidation = validateZoneRequirements(validationResult.data);
124
+ if (zoneValidation) {
125
+ const errorMessages = zoneValidation.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
126
+ return {
127
+ success: false,
128
+ error: `Zone validation failed:\n${errorMessages}`,
129
+ };
130
+ }
131
+
132
+ const deriveCheck = validateDeriveFromSources(validationResult.data);
133
+ if (deriveCheck) {
134
+ const errorMessages = deriveCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
135
+ return {
136
+ success: false,
137
+ error: `Variable derive_from validation failed:\n${errorMessages}`,
138
+ };
139
+ }
140
+
141
+ const hookCheck = validateHookContract(validationResult.data);
142
+ if (hookCheck) {
143
+ const errorMessages = hookCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
144
+ return {
145
+ success: false,
146
+ error: `Hook contract validation failed:\n${errorMessages}`,
147
+ };
148
+ }
149
+
150
+ const crossCapCheck = validateProvidesNoCrossCapabilityRefs(validationResult.data);
151
+ if (crossCapCheck) {
152
+ const errorMessages = crossCapCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
153
+ return {
154
+ success: false,
155
+ error: `Capability data validation failed:\n${errorMessages}`,
156
+ };
157
+ }
158
+
159
+ const capNameCheck = validateCapabilityNames(validationResult.data);
160
+ if (capNameCheck) {
161
+ const errorMessages = capNameCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
162
+ return {
163
+ success: false,
164
+ error: `Capability name validation failed:\n${errorMessages}`,
165
+ };
166
+ }
167
+
168
+ const variableSourceCheck = validateVariableSources(validationResult.data);
169
+ if (variableSourceCheck) {
170
+ const errorMessages = variableSourceCheck.errors
171
+ .map((e) => `${e.path}: ${e.message}`)
172
+ .join('\n');
173
+ return {
174
+ success: false,
175
+ error: `Variable source validation failed:\n${errorMessages}`,
176
+ };
177
+ }
178
+
179
+ return {
180
+ success: true,
181
+ manifest: validationResult.data,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Copy module files to target directory
187
+ *
188
+ * Execution function (Rule 10.1) - performs file I/O
189
+ *
190
+ * @param sourcePath - Source module directory
191
+ * @param targetPath - Target directory to copy to
192
+ */
193
+ export async function copyModuleFiles(sourcePath: string, targetPath: string): Promise<void> {
194
+ // Create target directory
195
+ await mkdir(targetPath, { recursive: true });
196
+
197
+ // Get all files in source directory
198
+ async function copyRecursive(src: string, dest: string) {
199
+ const entries = await readdir(src, { withFileTypes: true });
200
+
201
+ for (const entry of entries) {
202
+ const srcPath = join(src, entry.name);
203
+ const destPath = join(dest, entry.name);
204
+
205
+ // Skip metadata files
206
+ if (entry.name === 'checksums.json' || entry.name === 'signature.sig') {
207
+ continue;
208
+ }
209
+
210
+ // Skip directories that shouldn't be copied into celilo's data store.
211
+ // node_modules inside scripts/ ARE kept — they contain the module's
212
+ // runtime deps (@celilo/capabilities etc.). Top-level node_modules
213
+ // (monorepo workspace deps, build tools) are skipped.
214
+ if (
215
+ entry.isDirectory() &&
216
+ (entry.name === '.git' || entry.name === '.next' || entry.name === '.cache')
217
+ ) {
218
+ continue;
219
+ }
220
+
221
+ if (
222
+ entry.isDirectory() &&
223
+ entry.name === 'node_modules' &&
224
+ !src.endsWith('/scripts') &&
225
+ !src.endsWith('\\scripts')
226
+ ) {
227
+ continue;
228
+ }
229
+
230
+ if (entry.isDirectory()) {
231
+ await mkdir(destPath, { recursive: true });
232
+ await copyRecursive(srcPath, destPath);
233
+ } else if (entry.isFile()) {
234
+ await copyFile(srcPath, destPath);
235
+ }
236
+ }
237
+ }
238
+
239
+ await copyRecursive(sourcePath, targetPath);
240
+ }
241
+
242
+ /**
243
+ * Insert module into database
244
+ *
245
+ * Execution function - performs database write
246
+ *
247
+ * @param manifest - Validated manifest
248
+ * @param targetPath - Target path where files are stored
249
+ * @param db - Database client (optional, for testing)
250
+ * @returns Inserted module record
251
+ */
252
+ export async function insertModuleToDb(
253
+ manifest: ModuleManifest,
254
+ targetPath: string,
255
+ db = getDb(),
256
+ ): Promise<void> {
257
+ const newModule: NewModule = {
258
+ id: manifest.id,
259
+ name: manifest.name,
260
+ version: manifest.version,
261
+ description: manifest.description,
262
+ sourcePath: targetPath,
263
+ manifestData: manifest as Record<string, unknown>,
264
+ state: 'IMPORTED',
265
+ };
266
+
267
+ db.insert(modules).values(newModule).run();
268
+ }
269
+
270
+ /**
271
+ * Check if module already exists in database
272
+ *
273
+ * @param moduleId - Module ID to check
274
+ * @param db - Database client (optional, for testing)
275
+ * @returns True if module exists
276
+ */
277
+ export function moduleExists(moduleId: string, db = getDb()): boolean {
278
+ const existing = db.select().from(modules).where(eq(modules.id, moduleId)).get();
279
+ return existing !== undefined;
280
+ }
281
+
282
+ /**
283
+ * Validate well-known capabilities
284
+ *
285
+ * Policy function - checks if module's well-known capabilities are valid:
286
+ * 1. No other module provides the same well-known capability (uniqueness)
287
+ * 2. Module's zone matches capability's required zone (zone enforcement)
288
+ *
289
+ * @param manifest - Module manifest
290
+ * @param db - Database client
291
+ * @returns Error message if invalid, null if valid
292
+ */
293
+ export async function validateWellKnownCapabilities(
294
+ manifest: ModuleManifest,
295
+ db = getDb(),
296
+ ): Promise<string | null> {
297
+ const providedCapabilities = manifest.provides?.capabilities ?? [];
298
+
299
+ for (const capability of providedCapabilities) {
300
+ // Skip non-well-known capabilities
301
+ if (!isWellKnown(capability.name)) {
302
+ continue;
303
+ }
304
+
305
+ const wellKnown = getWellKnownCapability(capability.name);
306
+
307
+ // Check 1: Capability uniqueness - only one module can provide this capability
308
+ const existingCapability = await db
309
+ .select()
310
+ .from(capabilities)
311
+ .where(eq(capabilities.capabilityName, capability.name))
312
+ .all();
313
+
314
+ if (existingCapability.length > 0) {
315
+ const conflictingModule = existingCapability[0];
316
+ return `Well-known capability '${capability.name}' is already provided by module '${conflictingModule.moduleId}'. A home lab can only have one module providing this capability. Remove '${conflictingModule.moduleId}' before importing this module.`;
317
+ }
318
+
319
+ // Check 2: Zone enforcement - module must be in the correct zone
320
+ const moduleZone = manifest.requires?.machine?.zone;
321
+
322
+ if (wellKnown.zone_enforced && moduleZone) {
323
+ if (moduleZone !== wellKnown.required_zone) {
324
+ return `Capability '${capability.name}' requires zone='${wellKnown.required_zone}' (security requirement). Module manifest specifies zone='${moduleZone}'. Update the module manifest to use the correct zone.`;
325
+ }
326
+ }
327
+ }
328
+
329
+ return null;
330
+ }
331
+
332
+ /**
333
+ * Import a module from a directory or .netapp package
334
+ *
335
+ * Orchestration function (Rule 10.1) - coordinates policy, planning, and execution
336
+ * This is the main entry point for module import
337
+ *
338
+ * @param options - Import options
339
+ * @returns Import result
340
+ */
341
+ /**
342
+ * The minimum package.json auto-generated for modules that have hook
343
+ * scripts but no package.json. Contains only the framework dep.
344
+ */
345
+ const DEFAULT_SCRIPTS_PACKAGE_JSON = JSON.stringify(
346
+ {
347
+ private: true,
348
+ dependencies: {
349
+ '@celilo/capabilities': '^0.1.0',
350
+ },
351
+ },
352
+ null,
353
+ 2,
354
+ );
355
+
356
+ /**
357
+ * Install npm dependencies for a module's hook scripts.
358
+ *
359
+ * Per NPM_PACKAGE_RESOLUTION design doc (Option B): each module's
360
+ * scripts/ directory has its own package.json + node_modules for
361
+ * fully isolated dependency resolution. This function:
362
+ *
363
+ * 1. Finds the scripts/ directory (derived from manifest hook paths).
364
+ * 2. If package.json exists and node_modules/ is missing, runs
365
+ * `bun install`.
366
+ * 3. If no package.json exists but hook scripts do, auto-generates a
367
+ * default one with `@celilo/capabilities` and then installs.
368
+ * 4. If no hooks are declared, does nothing.
369
+ */
370
+ async function installScriptDependencies(
371
+ targetPath: string,
372
+ manifest: ModuleManifest,
373
+ ): Promise<void> {
374
+ // Determine where scripts live by looking at hook declarations.
375
+ // All hooks use paths like `./scripts/setup-admin.ts`, so the
376
+ // scripts directory is the common parent.
377
+ if (!manifest.hooks || Object.keys(manifest.hooks).length === 0) {
378
+ return; // No hooks → no scripts → nothing to install
379
+ }
380
+
381
+ const scriptsDir = join(targetPath, 'scripts');
382
+ if (!existsSync(scriptsDir)) {
383
+ return; // No scripts directory on disk
384
+ }
385
+
386
+ const pkgJsonPath = join(scriptsDir, 'package.json');
387
+ const nodeModulesPath = join(scriptsDir, 'node_modules');
388
+
389
+ // Auto-generate package.json if missing but hooks exist
390
+ if (!existsSync(pkgJsonPath)) {
391
+ log.info('Auto-generating scripts/package.json with @celilo/capabilities');
392
+ writeFileSync(pkgJsonPath, DEFAULT_SCRIPTS_PACKAGE_JSON, 'utf-8');
393
+ }
394
+
395
+ // Skip install if node_modules already exists (pre-bundled via
396
+ // future --bundle-deps, or a re-import of an already-installed module)
397
+ if (existsSync(nodeModulesPath)) {
398
+ return;
399
+ }
400
+
401
+ // Run bun install in the scripts directory
402
+ log.info('Installing script dependencies...');
403
+ execSync('bun install', {
404
+ cwd: scriptsDir,
405
+ timeout: 120_000,
406
+ stdio: 'pipe',
407
+ });
408
+ }
409
+
410
+ export async function importModule(options: ModuleImportOptions): Promise<ModuleImportResult> {
411
+ const { sourcePath, targetBasePath = getDefaultTargetBase(), db = getDb(), flags = {} } = options;
412
+
413
+ let actualSourcePath = sourcePath;
414
+ let tempDir: string | null = null;
415
+ let checksums: Record<string, string> | null = null;
416
+ let signature: string | null = null;
417
+
418
+ try {
419
+ // Check if source is a .netapp package
420
+ if (sourcePath.endsWith('.netapp')) {
421
+ // Extract and verify package
422
+ const extractResult = await extractPackage(sourcePath);
423
+ if (!extractResult.success || !extractResult.tempDir) {
424
+ return { success: false, error: extractResult.error || 'Failed to extract package' };
425
+ }
426
+
427
+ tempDir = extractResult.tempDir;
428
+
429
+ // Verify package integrity (unless --skip-verify)
430
+ const skipVerify = flags['skip-verify'] === true;
431
+ if (skipVerify) {
432
+ log.warn('Skipping package signature verification (--skip-verify)');
433
+ } else {
434
+ const verifyResult = await verifyPackageIntegrity(tempDir);
435
+ if (!verifyResult.success) {
436
+ await cleanupTempDir(tempDir);
437
+ return {
438
+ success: false,
439
+ error: verifyResult.error || 'Package integrity verification failed',
440
+ };
441
+ }
442
+ }
443
+
444
+ // Read checksums and signature for database storage
445
+ const checksumsJson = await readFile(join(tempDir, 'checksums.json'), 'utf-8');
446
+ const ChecksumsFileSchema = z.object({
447
+ files: z.record(z.string(), z.string()),
448
+ });
449
+ const checksumsData = parseJsonWithValidation(
450
+ checksumsJson,
451
+ ChecksumsFileSchema,
452
+ 'package checksums.json',
453
+ );
454
+ checksums = checksumsData.files;
455
+ signature = await readFile(join(tempDir, 'signature.sig'), 'utf-8');
456
+
457
+ // Use extracted directory as source
458
+ actualSourcePath = tempDir;
459
+ }
460
+
461
+ // Policy: Validate directory structure
462
+ const dirError = validateModuleDirectory(actualSourcePath);
463
+ if (dirError) {
464
+ if (tempDir) await cleanupTempDir(tempDir);
465
+ return { success: false, error: dirError };
466
+ }
467
+
468
+ // Policy: Read and validate manifest
469
+ const manifestResult = await readModuleManifest(actualSourcePath);
470
+ if (!manifestResult.success) {
471
+ if (tempDir) await cleanupTempDir(tempDir);
472
+ return { success: false, error: manifestResult.error };
473
+ }
474
+
475
+ const manifest = manifestResult.manifest;
476
+
477
+ // Policy: Check if module already exists
478
+ if (moduleExists(manifest.id, db)) {
479
+ if (tempDir) await cleanupTempDir(tempDir);
480
+ return {
481
+ success: false,
482
+ error: `Module '${manifest.id}' already exists. Use update or remove it first.`,
483
+ };
484
+ }
485
+
486
+ // Policy: Validate well-known capabilities (import-time check)
487
+ const capabilityError = await validateWellKnownCapabilities(manifest, db);
488
+ if (capabilityError) {
489
+ if (tempDir) await cleanupTempDir(tempDir);
490
+ return {
491
+ success: false,
492
+ error: capabilityError,
493
+ };
494
+ }
495
+
496
+ // Policy: Validate template variable references (import-time check)
497
+ const { validateModuleTemplates, formatTemplateValidationErrors } = await import(
498
+ '../manifest/template-validator'
499
+ );
500
+ const templateValidation = await validateModuleTemplates(actualSourcePath, manifest);
501
+ if (!templateValidation.success) {
502
+ if (tempDir) await cleanupTempDir(tempDir);
503
+ return {
504
+ success: false,
505
+ error: formatTemplateValidationErrors(templateValidation.errors),
506
+ };
507
+ }
508
+
509
+ // Policy: Check for Ansible dependency conflicts and install collections
510
+ const { installCollectionsForModule } = await import('../ansible/dependencies');
511
+ const installResult = await installCollectionsForModule(manifest, db);
512
+
513
+ if (!installResult.success) {
514
+ if (tempDir) await cleanupTempDir(tempDir);
515
+ return {
516
+ success: false,
517
+ error: installResult.error || 'Failed to install Ansible collections',
518
+ details: installResult.details,
519
+ };
520
+ }
521
+
522
+ // Execution: Validate capability access if module requires capabilities
523
+ if (manifest.requires?.capabilities && manifest.requires.capabilities.length > 0) {
524
+ const { validateCapabilityAccess } = await import('../capabilities/validation');
525
+ const accessResult = await validateCapabilityAccess(manifest, db.$client);
526
+
527
+ if (!accessResult.success) {
528
+ if (tempDir) await cleanupTempDir(tempDir);
529
+ return {
530
+ success: false,
531
+ error: accessResult.error || 'Capability access denied',
532
+ details: accessResult.details,
533
+ };
534
+ }
535
+ }
536
+
537
+ // Planning: Determine target path
538
+ const targetPath = join(targetBasePath, manifest.id);
539
+
540
+ // Execution: Copy files
541
+ try {
542
+ await copyModuleFiles(actualSourcePath, targetPath);
543
+ } catch (error) {
544
+ if (tempDir) await cleanupTempDir(tempDir);
545
+ return {
546
+ success: false,
547
+ error: 'Failed to copy module files',
548
+ details: error,
549
+ };
550
+ }
551
+
552
+ // Execution: Install hook script dependencies (NPM_PACKAGE_RESOLUTION Option B).
553
+ // If the module's scripts/ directory has a package.json, run `bun install`
554
+ // so the hook scripts can resolve their npm imports (e.g. @celilo/capabilities).
555
+ // If no package.json exists but hook scripts do, auto-generate one with
556
+ // just the framework dep — smooths migration for existing modules.
557
+ try {
558
+ await installScriptDependencies(targetPath, manifest);
559
+ } catch (error) {
560
+ // Non-fatal: the module is importable without deps, but hooks
561
+ // will fail at runtime. Warn and continue.
562
+ const msg = error instanceof Error ? error.message : String(error);
563
+ log.warn(`Failed to install script dependencies: ${msg}`);
564
+ log.warn('Hook scripts may fail to resolve imports until this is fixed.');
565
+ }
566
+
567
+ // Execution: Insert to database
568
+ try {
569
+ await insertModuleToDb(manifest, targetPath, db);
570
+ } catch (error) {
571
+ if (tempDir) await cleanupTempDir(tempDir);
572
+ return {
573
+ success: false,
574
+ error: 'Failed to insert module to database',
575
+ details: error,
576
+ };
577
+ }
578
+
579
+ // Execution: Register capabilities if module provides them
580
+ if (manifest.provides?.capabilities && manifest.provides.capabilities.length > 0) {
581
+ const { registerModuleCapabilities } = await import('../capabilities/registration');
582
+ const capResult = await registerModuleCapabilities(manifest.id, manifest, db.$client, flags);
583
+
584
+ if (!capResult.success) {
585
+ if (tempDir) await cleanupTempDir(tempDir);
586
+ return {
587
+ success: false,
588
+ error: capResult.error || 'Failed to register module capabilities',
589
+ details: capResult.details,
590
+ };
591
+ }
592
+ }
593
+
594
+ // Execution: Store integrity data
595
+ // For .netapp packages: store checksums + signature
596
+ // For directory imports: calculate and store checksums (no signature)
597
+ try {
598
+ let finalChecksums: Record<string, string>;
599
+ let finalSignature: string | null = null;
600
+
601
+ if (checksums && signature) {
602
+ // From .netapp package - already verified
603
+ finalChecksums = checksums;
604
+ finalSignature = signature.trim();
605
+ } else {
606
+ // From directory - calculate checksums now
607
+ const { computeChecksums } = await import('./packaging/build');
608
+ const checksumsData = await computeChecksums(targetPath);
609
+ finalChecksums = checksumsData.files;
610
+ // No signature for directory imports
611
+ }
612
+
613
+ const integrityData: NewModuleIntegrity = {
614
+ moduleId: manifest.id,
615
+ checksums: finalChecksums,
616
+ signature: finalSignature,
617
+ };
618
+ db.insert(moduleIntegrity).values(integrityData).run();
619
+ } catch (error) {
620
+ // Non-fatal - module is already imported
621
+ console.warn('Warning: Failed to store module integrity data', error);
622
+ }
623
+
624
+ // For .netapp packages with build artifacts: record a successful build
625
+ // so that `module generate` doesn't require a rebuild
626
+ if (sourcePath.endsWith('.netapp') && manifest.build?.artifacts) {
627
+ const { existsSync: fileExists } = await import('node:fs');
628
+ const artifactPaths = manifest.build.artifacts
629
+ .map((a: string) => join(targetPath, a))
630
+ .filter((p: string) => fileExists(p));
631
+
632
+ if (artifactPaths.length > 0) {
633
+ const { moduleBuilds } = await import('../db/schema');
634
+ db.insert(moduleBuilds)
635
+ .values({
636
+ moduleId: manifest.id,
637
+ version: manifest.version,
638
+ artifacts: artifactPaths,
639
+ status: 'success',
640
+ buildLog: 'Pre-built artifacts from .netapp package',
641
+ })
642
+ .run();
643
+ }
644
+ }
645
+
646
+ // Update dependency cache
647
+ try {
648
+ const { updateDependencyCache } = await import('../ansible/dependencies');
649
+ await updateDependencyCache(db);
650
+ } catch (error) {
651
+ // Non-fatal - cache is optional
652
+ console.warn('Warning: Failed to update dependency cache', error);
653
+ }
654
+
655
+ // Cleanup temp directory if used
656
+ if (tempDir) {
657
+ await cleanupTempDir(tempDir);
658
+ }
659
+
660
+ return {
661
+ success: true,
662
+ moduleId: manifest.id,
663
+ targetPath,
664
+ };
665
+ } catch (error) {
666
+ // Cleanup on unexpected error
667
+ if (tempDir) {
668
+ await cleanupTempDir(tempDir);
669
+ }
670
+ return {
671
+ success: false,
672
+ error: 'Unexpected error during import',
673
+ details: error,
674
+ };
675
+ }
676
+ }