@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,80 @@
1
+ /**
2
+ * Module import command
3
+ */
4
+
5
+ import { resolve } from 'node:path';
6
+ import { getModuleStoragePath, shortenPath } from '../../config/paths';
7
+ import { getDb } from '../../db/client';
8
+ import { importModule } from '../../module/import';
9
+ import { getArg, getFlag, validateRequiredArgs } from '../parser';
10
+ import type { CommandResult } from '../types';
11
+ import { generateTypesForImportedModule } from './module-types';
12
+
13
+ /**
14
+ * Handle module import command
15
+ *
16
+ * Usage: celilo module import <path> [--target <path>]
17
+ *
18
+ * @param args - Command arguments
19
+ * @param flags - Command flags
20
+ * @returns Command result
21
+ */
22
+ export async function handleModuleImport(
23
+ args: string[],
24
+ flags: Record<string, string | boolean>,
25
+ ): Promise<CommandResult> {
26
+ // Validate arguments
27
+ const error = validateRequiredArgs(args, 1);
28
+ if (error) {
29
+ return {
30
+ success: false,
31
+ error: `${error}\n\nUsage: celilo module import <path> [--target <path>]`,
32
+ };
33
+ }
34
+
35
+ const sourcePath = getArg(args, 0);
36
+ if (!sourcePath) {
37
+ return {
38
+ success: false,
39
+ error: 'Source path is required',
40
+ };
41
+ }
42
+
43
+ // Resolve paths
44
+ const resolvedSourcePath = resolve(sourcePath);
45
+ const targetBasePath = getFlag(flags, 'target', getModuleStoragePath());
46
+ const resolvedTargetBasePath = resolve(targetBasePath);
47
+
48
+ // Import module
49
+ const db = getDb();
50
+ const result = await importModule({
51
+ sourcePath: resolvedSourcePath,
52
+ targetBasePath: resolvedTargetBasePath,
53
+ db,
54
+ flags,
55
+ });
56
+
57
+ if (!result.success) {
58
+ return {
59
+ success: false,
60
+ error: result.error,
61
+ details: result.details,
62
+ };
63
+ }
64
+
65
+ // Belt-and-suspenders: regenerate the module's celilo/types.d.ts so
66
+ // the imported module can never have stale types. Non-fatal on error —
67
+ // a codegen failure should not abort a successful import.
68
+ const typesPath = await generateTypesForImportedModule(result.targetPath);
69
+ const typesMessage = typesPath ? `\nGenerated types: ${shortenPath(typesPath)}` : '';
70
+
71
+ return {
72
+ success: true,
73
+ message: `Successfully imported module: ${result.moduleId}\nFiles copied to: ${shortenPath(result.targetPath)}${typesMessage}`,
74
+ data: {
75
+ moduleId: result.moduleId,
76
+ targetPath: result.targetPath,
77
+ typesPath: typesPath ?? undefined,
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,43 @@
1
+ import { getDb } from '../../db/client';
2
+ import { modules } from '../../db/schema';
3
+ import type { CommandResult } from '../types';
4
+
5
+ /**
6
+ * Handle module list command
7
+ *
8
+ * Usage: celilo module list
9
+ *
10
+ * @returns Command result
11
+ */
12
+ export async function handleModuleList(): Promise<CommandResult> {
13
+ const db = getDb();
14
+
15
+ // Query all modules
16
+ const moduleRows = db.select().from(modules).all();
17
+
18
+ if (moduleRows.length === 0) {
19
+ return {
20
+ success: true,
21
+ message: 'No modules installed',
22
+ };
23
+ }
24
+
25
+ // Format module list
26
+ const lines = ['Installed modules:', ''];
27
+ for (const module of moduleRows) {
28
+ lines.push(`${module.id} (v${module.version}) - ${module.state}`);
29
+ if (module.description) {
30
+ lines.push(` ${module.description}`);
31
+ }
32
+ if (module.errorMessage) {
33
+ lines.push(` Error: ${module.errorMessage}`);
34
+ }
35
+ lines.push('');
36
+ }
37
+
38
+ return {
39
+ success: true,
40
+ message: lines.join('\n'),
41
+ data: moduleRows,
42
+ };
43
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Module logs command - show deploy logs for a module
3
+ */
4
+
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { eq } from 'drizzle-orm';
8
+ import { getModuleStoragePath } from '../../config/paths';
9
+ import { getDb } from '../../db/client';
10
+ import { modules } from '../../db/schema';
11
+ import { getArg, hasFlag, validateRequiredArgs } from '../parser';
12
+ import type { CommandResult } from '../types';
13
+
14
+ /**
15
+ * Handle module logs command
16
+ *
17
+ * Usage: celilo module logs <id> [--tail <n>]
18
+ */
19
+ export async function handleModuleLogs(
20
+ args: string[],
21
+ flags: Record<string, string | boolean>,
22
+ ): Promise<CommandResult> {
23
+ const error = validateRequiredArgs(args, 1);
24
+ if (error) {
25
+ return {
26
+ success: false,
27
+ error: `${error}\n\nUsage: celilo module logs <id> [--tail <n>]`,
28
+ };
29
+ }
30
+
31
+ const moduleId = getArg(args, 0);
32
+ if (!moduleId) {
33
+ return { success: false, error: 'Module ID is required' };
34
+ }
35
+
36
+ const db = getDb();
37
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
38
+ if (!module) {
39
+ return { success: false, error: `Module not found: ${moduleId}` };
40
+ }
41
+
42
+ const logPath = join(getModuleStoragePath(), moduleId, 'generated', 'deploy.log');
43
+
44
+ if (!existsSync(logPath)) {
45
+ return {
46
+ success: true,
47
+ message: `No deploy logs found for ${moduleId}.\nLogs are created on the next deployment.`,
48
+ };
49
+ }
50
+
51
+ const content = readFileSync(logPath, 'utf-8');
52
+
53
+ // --tail N: show only the last N lines
54
+ const tailValue = flags.tail;
55
+ if (typeof tailValue === 'string') {
56
+ const n = Number.parseInt(tailValue, 10);
57
+ if (Number.isNaN(n) || n < 1) {
58
+ return { success: false, error: '--tail requires a positive number' };
59
+ }
60
+ const lines = content.trimEnd().split('\n');
61
+ const tail = lines.slice(-n);
62
+ return { success: true, message: tail.join('\n') };
63
+ }
64
+
65
+ // --last: show only the most recent deploy run
66
+ if (hasFlag(flags, 'last')) {
67
+ const runs = content.split(/(?=\n--- Ansible deploy )/);
68
+ const lastRun = runs[runs.length - 1];
69
+ return { success: true, message: (lastRun || content).trim() };
70
+ }
71
+
72
+ return { success: true, message: content.trim() };
73
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Module remove command
3
+ *
4
+ * Removes a module, destroying infrastructure if present.
5
+ * Requires confirmation for infrastructure modules unless --force is passed.
6
+ */
7
+
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { eq } from 'drizzle-orm';
11
+ import { getDb } from '../../db/client';
12
+ import { moduleInfrastructure, modules } from '../../db/schema';
13
+ import { deallocateForModule } from '../../ipam/auto-allocator';
14
+ import { executeBuildWithProgress } from '../../services/build-stream';
15
+ import { getContainerService, getServiceCredentials } from '../../services/container-service';
16
+ import { getArg, hasFlag, validateRequiredArgs } from '../parser';
17
+ import { log, promptConfirm } from '../prompts';
18
+ import type { CommandResult } from '../types';
19
+
20
+ /**
21
+ * Handle module remove command
22
+ *
23
+ * Usage: celilo module remove <id> [--force]
24
+ *
25
+ * @param args - Command arguments
26
+ * @param flags - Command flags
27
+ * @returns Command result
28
+ */
29
+ export async function handleModuleRemove(
30
+ args: string[],
31
+ flags: Record<string, string | boolean> = {},
32
+ ): Promise<CommandResult> {
33
+ // Validate arguments
34
+ const error = validateRequiredArgs(args, 1);
35
+ if (error) {
36
+ return {
37
+ success: false,
38
+ error: `${error}\n\nUsage: celilo module remove <id> [--force]`,
39
+ };
40
+ }
41
+
42
+ const moduleId = getArg(args, 0);
43
+
44
+ if (!moduleId) {
45
+ return {
46
+ success: false,
47
+ error: 'Module ID is required',
48
+ };
49
+ }
50
+
51
+ const force = hasFlag(flags, 'force');
52
+ const db = getDb();
53
+
54
+ // Check if module exists
55
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
56
+
57
+ if (!module) {
58
+ return {
59
+ success: false,
60
+ error: `Module not found: ${moduleId}`,
61
+ };
62
+ }
63
+
64
+ // Check if module has infrastructure that needs to be destroyed
65
+ const infra = db
66
+ .select()
67
+ .from(moduleInfrastructure)
68
+ .where(eq(moduleInfrastructure.moduleId, moduleId))
69
+ .get();
70
+
71
+ const terraformDir = join(module.sourcePath, 'generated', 'terraform');
72
+ const hasTerraformState = existsSync(join(terraformDir, 'terraform.tfstate'));
73
+
74
+ if (infra && hasTerraformState) {
75
+ // Module has infrastructure — need to destroy it
76
+ if (!force) {
77
+ const confirmed = await promptConfirm({
78
+ message: `Module '${moduleId}' has deployed infrastructure. This will run terraform destroy to remove it. Continue?`,
79
+ });
80
+
81
+ if (!confirmed) {
82
+ return {
83
+ success: false,
84
+ error: 'Removal cancelled',
85
+ };
86
+ }
87
+ }
88
+
89
+ // Build Terraform env vars (need service credentials for destroy)
90
+ const terraformEnvVars: Record<string, string> = {
91
+ TF_IN_AUTOMATION: '1',
92
+ };
93
+
94
+ if (infra.infrastructureType === 'container_service' && infra.serviceId) {
95
+ const service = await getContainerService(infra.serviceId);
96
+ if (service) {
97
+ const credentials = await getServiceCredentials(infra.serviceId);
98
+ if (service.providerName === 'digitalocean' && 'api_token' in credentials) {
99
+ terraformEnvVars.TF_VAR_digitalocean_token = credentials.api_token;
100
+ } else if (service.providerName === 'proxmox' && 'api_url' in credentials) {
101
+ terraformEnvVars.TF_VAR_proxmox_api_url = credentials.api_url;
102
+ terraformEnvVars.TF_VAR_proxmox_token_id = credentials.api_token_id;
103
+ terraformEnvVars.TF_VAR_proxmox_token_secret = credentials.api_token_secret;
104
+ }
105
+ }
106
+ }
107
+
108
+ // Run terraform destroy
109
+ log.info(`Destroying infrastructure for ${moduleId}...`);
110
+
111
+ const initResult = await executeBuildWithProgress({
112
+ command: 'terraform',
113
+ args: ['init', '-upgrade'],
114
+ cwd: terraformDir,
115
+ title: 'Initializing Terraform',
116
+ env: terraformEnvVars,
117
+ });
118
+
119
+ if (!initResult.success) {
120
+ return {
121
+ success: false,
122
+ error: `Terraform init failed: ${initResult.error}`,
123
+ };
124
+ }
125
+
126
+ const destroyResult = await executeBuildWithProgress({
127
+ command: 'terraform',
128
+ args: ['destroy', '-auto-approve', '-no-color'],
129
+ cwd: terraformDir,
130
+ title: 'Destroying infrastructure',
131
+ env: terraformEnvVars,
132
+ });
133
+
134
+ if (!destroyResult.success) {
135
+ return {
136
+ success: false,
137
+ error: `Terraform destroy failed: ${destroyResult.error}\n\nInfrastructure may still exist. Check your provider console.`,
138
+ };
139
+ }
140
+
141
+ log.success('Infrastructure destroyed');
142
+ }
143
+
144
+ // Remove DNS record (if dns_internal is available)
145
+ try {
146
+ const { autoDeregisterDns } = await import('../../services/dns-auto-register');
147
+ await autoDeregisterDns(moduleId, db, log);
148
+ } catch {
149
+ // Non-fatal -- continue with removal
150
+ }
151
+
152
+ // Deallocate IPAM resources (if any)
153
+ await deallocateForModule(moduleId, db);
154
+
155
+ // Delete module (cascade will remove configs, secrets, capabilities, infrastructure records)
156
+ db.delete(modules).where(eq(modules.id, moduleId)).run();
157
+
158
+ return {
159
+ success: true,
160
+ message: `Successfully removed module: ${moduleId}`,
161
+ };
162
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Module debugging commands
3
+ * Show detailed module information including auto-derived values
4
+ */
5
+
6
+ import { eq } from 'drizzle-orm';
7
+ import { getDb } from '../../db/client';
8
+ import { modules } from '../../db/schema';
9
+ import type { ModuleManifest } from '../../manifest/schema';
10
+ import { buildResolutionContext } from '../../variables/context';
11
+ import { getArg, validateRequiredArgs } from '../parser';
12
+ import type { CommandResult } from '../types';
13
+
14
+ /**
15
+ * Handle module show-config command
16
+ * Shows ALL configuration including auto-derived values
17
+ *
18
+ * Usage: celilo module show-config <module-id>
19
+ *
20
+ * @param args - Command arguments
21
+ * @returns Command result
22
+ */
23
+ export async function handleModuleShowConfig(args: string[]): Promise<CommandResult> {
24
+ const error = validateRequiredArgs(args, 1);
25
+ if (error) {
26
+ return {
27
+ success: false,
28
+ error: `${error}\n\nUsage: celilo module show-config <module-id>`,
29
+ };
30
+ }
31
+
32
+ const moduleId = getArg(args, 0);
33
+ if (!moduleId) {
34
+ return {
35
+ success: false,
36
+ error: 'Module ID is required',
37
+ };
38
+ }
39
+
40
+ const db = getDb();
41
+
42
+ // Check if module exists
43
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
44
+ if (!module) {
45
+ return {
46
+ success: false,
47
+ error: `Module not found: ${moduleId}`,
48
+ };
49
+ }
50
+
51
+ try {
52
+ // Build resolution context to get ALL config (including auto-derived)
53
+ const context = await buildResolutionContext(moduleId, db);
54
+
55
+ const lines = [`Complete configuration for ${moduleId}:`, ''];
56
+
57
+ // Group config by source
58
+ const userConfig: string[] = [];
59
+ const autoConfig: string[] = [];
60
+
61
+ for (const [key, value] of Object.entries(context.selfConfig)) {
62
+ // Skip internal/derived keys that aren't useful for display
63
+ if (key.startsWith('requires.machine.')) continue;
64
+
65
+ const line = ` ${key} = ${value}`;
66
+
67
+ // Categorize: inventory.* and some others are auto-derived
68
+ if (
69
+ key.startsWith('inventory.') ||
70
+ key === 'vmid' ||
71
+ key === 'container_ip' ||
72
+ key === 'gateway' ||
73
+ key === 'vlan' ||
74
+ key === 'subnet' ||
75
+ key === 'bridge'
76
+ ) {
77
+ autoConfig.push(line);
78
+ } else {
79
+ userConfig.push(line);
80
+ }
81
+ }
82
+
83
+ if (userConfig.length > 0) {
84
+ lines.push('User Configuration:');
85
+ lines.push(...userConfig);
86
+ lines.push('');
87
+ }
88
+
89
+ if (autoConfig.length > 0) {
90
+ lines.push('Auto-Derived Configuration:');
91
+ lines.push(...autoConfig);
92
+ lines.push('');
93
+ }
94
+
95
+ // Show zone if set
96
+ if (context.selfConfig.zone) {
97
+ lines.push(`Network Zone: ${context.selfConfig.zone}`);
98
+ }
99
+
100
+ return {
101
+ success: true,
102
+ message: lines.join('\n'),
103
+ data: context.selfConfig,
104
+ };
105
+ } catch (error) {
106
+ return {
107
+ success: false,
108
+ error: `Failed to get module configuration: ${error instanceof Error ? error.message : String(error)}`,
109
+ };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Handle module show-zone command
115
+ * Shows which zone the module is in
116
+ *
117
+ * Usage: celilo module show-zone <module-id>
118
+ *
119
+ * @param args - Command arguments
120
+ * @returns Command result
121
+ */
122
+ export async function handleModuleShowZone(args: string[]): Promise<CommandResult> {
123
+ const error = validateRequiredArgs(args, 1);
124
+ if (error) {
125
+ return {
126
+ success: false,
127
+ error: `${error}\n\nUsage: celilo module show-zone <module-id>`,
128
+ };
129
+ }
130
+
131
+ const moduleId = getArg(args, 0);
132
+ if (!moduleId) {
133
+ return {
134
+ success: false,
135
+ error: 'Module ID is required',
136
+ };
137
+ }
138
+
139
+ const db = getDb();
140
+
141
+ // Check if module exists
142
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
143
+ if (!module) {
144
+ return {
145
+ success: false,
146
+ error: `Module not found: ${moduleId}`,
147
+ };
148
+ }
149
+
150
+ try {
151
+ // Build resolution context to get zone
152
+ const context = await buildResolutionContext(moduleId, db);
153
+ const manifest = module.manifestData as ModuleManifest;
154
+
155
+ const zone = context.selfConfig.zone || manifest.requires?.machine?.zone;
156
+
157
+ if (!zone) {
158
+ return {
159
+ success: true,
160
+ message: `Module ${moduleId} has no zone configured`,
161
+ };
162
+ }
163
+
164
+ const zoneDescriptions: Record<string, string> = {
165
+ dmz: 'DMZ (Public-facing services)',
166
+ app: 'Application (Internal services)',
167
+ secure: 'Secure (Authentication/Database)',
168
+ external: 'External (VPS/Cloud)',
169
+ };
170
+
171
+ const description = zoneDescriptions[zone] || 'Unknown zone';
172
+
173
+ const lines = [`Module: ${moduleId}`, `Zone: ${zone} - ${description}`, ''];
174
+
175
+ // Show network config if available
176
+ if (zone !== 'external') {
177
+ lines.push('Network Configuration:');
178
+ if (context.selfConfig.gateway) lines.push(` Gateway: ${context.selfConfig.gateway}`);
179
+ if (context.selfConfig.vlan) lines.push(` VLAN: ${context.selfConfig.vlan}`);
180
+ if (context.selfConfig.subnet) lines.push(` Subnet: ${context.selfConfig.subnet}`);
181
+ if (context.selfConfig.bridge) lines.push(` Bridge: ${context.selfConfig.bridge}`);
182
+ } else {
183
+ lines.push('External zone (VPS/Cloud) - no local network config');
184
+ }
185
+
186
+ return {
187
+ success: true,
188
+ message: lines.join('\n'),
189
+ data: {
190
+ zone,
191
+ network:
192
+ zone !== 'external'
193
+ ? {
194
+ gateway: context.selfConfig.gateway,
195
+ vlan: context.selfConfig.vlan,
196
+ subnet: context.selfConfig.subnet,
197
+ bridge: context.selfConfig.bridge,
198
+ }
199
+ : null,
200
+ },
201
+ };
202
+ } catch (error) {
203
+ return {
204
+ success: false,
205
+ error: `Failed to get module zone: ${error instanceof Error ? error.message : String(error)}`,
206
+ };
207
+ }
208
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Module status command - show detailed information about a module
3
+ */
4
+
5
+ import { eq } from 'drizzle-orm';
6
+ import { getDb } from '../../db/client';
7
+ import { capabilities, moduleConfigs, modules, secrets } from '../../db/schema';
8
+ import { getArg, validateRequiredArgs } from '../parser';
9
+ import type { CommandResult } from '../types';
10
+
11
+ /**
12
+ * Handle module status command
13
+ *
14
+ * Usage: celilo module status <id>
15
+ *
16
+ * @param args - Command arguments
17
+ * @returns Command result
18
+ */
19
+ export async function handleModuleStatus(args: string[]): Promise<CommandResult> {
20
+ // Validate arguments
21
+ const error = validateRequiredArgs(args, 1);
22
+ if (error) {
23
+ return {
24
+ success: false,
25
+ error: `${error}\n\nUsage: celilo module status <id>`,
26
+ };
27
+ }
28
+
29
+ const moduleId = getArg(args, 0);
30
+
31
+ if (!moduleId) {
32
+ return {
33
+ success: false,
34
+ error: 'Module ID is required',
35
+ };
36
+ }
37
+
38
+ const db = getDb();
39
+
40
+ // Check if module exists
41
+ const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
42
+
43
+ if (!module) {
44
+ return {
45
+ success: false,
46
+ error: `Module not found: ${moduleId}`,
47
+ };
48
+ }
49
+
50
+ // Get configuration values
51
+ const configs = db.select().from(moduleConfigs).where(eq(moduleConfigs.moduleId, moduleId)).all();
52
+
53
+ // Get secrets (names only, not values)
54
+ const moduleSecrets = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
55
+
56
+ // Get capabilities
57
+ const moduleCapabilities = db
58
+ .select()
59
+ .from(capabilities)
60
+ .where(eq(capabilities.moduleId, moduleId))
61
+ .all();
62
+
63
+ // Build sections as multi-line blocks joined by \n\n.
64
+ // The display code in index.ts splits on \n\n and passes each section
65
+ // as a single multi-line string to p.log.message(), avoiding clack's
66
+ // per-line extra spacing.
67
+ const sections: string[] = [];
68
+
69
+ // Section 1: Module metadata
70
+ const metadataLines = [
71
+ `Module: ${module.id}`,
72
+ `Name: ${module.name}`,
73
+ `Version: ${module.version}`,
74
+ `State: ${module.state}`,
75
+ ];
76
+ if (module.description) {
77
+ metadataLines.push(`Description: ${module.description}`);
78
+ }
79
+ metadataLines.push(`Source: ${module.sourcePath}`);
80
+ metadataLines.push(`Imported: ${module.importedAt.toISOString()}`);
81
+ metadataLines.push(`Updated: ${module.updatedAt.toISOString()}`);
82
+ if (module.errorMessage) {
83
+ metadataLines.push(`Error: ${module.errorMessage}`);
84
+ }
85
+ sections.push(metadataLines.join('\n'));
86
+
87
+ // Section 2: Configuration
88
+ if (configs.length > 0) {
89
+ const configLines = ['Configuration:'];
90
+ for (const config of configs) {
91
+ const value = config.valueJson ? JSON.stringify(JSON.parse(config.valueJson)) : config.value;
92
+ configLines.push(` ${config.key}: ${value}`);
93
+ }
94
+ sections.push(configLines.join('\n'));
95
+ } else {
96
+ sections.push('Configuration: (none)');
97
+ }
98
+
99
+ // Section 3: Secrets
100
+ if (moduleSecrets.length > 0) {
101
+ const secretLines = ['Secrets:'];
102
+ for (const secret of moduleSecrets) {
103
+ secretLines.push(` ${secret.name}: ********`);
104
+ }
105
+ sections.push(secretLines.join('\n'));
106
+ } else {
107
+ sections.push('Secrets: (none)');
108
+ }
109
+
110
+ // Section 4: Capabilities
111
+ if (moduleCapabilities.length > 0) {
112
+ const capabilityLines = ['Provides Capabilities:'];
113
+ for (const capability of moduleCapabilities) {
114
+ capabilityLines.push(` ${capability.capabilityName} (v${capability.version})`);
115
+ }
116
+ sections.push(capabilityLines.join('\n'));
117
+ } else {
118
+ sections.push('Provides Capabilities: (none)');
119
+ }
120
+
121
+ return {
122
+ success: true,
123
+ message: sections.join('\n\n'),
124
+ data: {
125
+ module,
126
+ configs,
127
+ secrets: moduleSecrets.map((s) => ({ name: s.name })),
128
+ capabilities: moduleCapabilities,
129
+ },
130
+ };
131
+ }