@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,1583 @@
1
+ /**
2
+ * CLI Entry Point
3
+ *
4
+ * Orchestration function (Rule 10.1) - routes commands to handlers
5
+ */
6
+
7
+ import * as p from '@clack/prompts';
8
+ import { CLIServerRequestSchema, parseJsonWithValidation } from '../validation/schemas';
9
+ import { COMMANDS, type CommandDef } from './command-registry';
10
+ import { handleCapabilityInfo } from './commands/capability-info';
11
+ import { handleCapabilityList } from './commands/capability-list';
12
+ import { handleCompletion } from './commands/completion';
13
+ import { handleHookRun } from './commands/hook-run';
14
+ import {
15
+ handleIpamIpListReservations,
16
+ handleIpamIpReserve,
17
+ handleIpamIpUnreserve,
18
+ handleIpamListAllocations,
19
+ handleIpamShow,
20
+ handleIpamVmidListReservations,
21
+ handleIpamVmidReserve,
22
+ handleIpamVmidUnreserve,
23
+ } from './commands/ipam';
24
+ import { handleMachineAdd } from './commands/machine-add';
25
+ import { handleMachineEarmark } from './commands/machine-earmark';
26
+ import { handleMachineList } from './commands/machine-list';
27
+ import { handleMachineRemove } from './commands/machine-remove';
28
+ import { handleMachineStatus } from './commands/machine-status';
29
+ import { moduleAudit } from './commands/module-audit';
30
+ import { handleModuleBuild } from './commands/module-build';
31
+ import { handleModuleConfigGet, handleModuleConfigSet } from './commands/module-config';
32
+ import { handleModuleDeploy } from './commands/module-deploy';
33
+ import { handleModuleGenerate } from './commands/module-generate';
34
+ import { handleModuleHealth } from './commands/module-health';
35
+ import { handleModuleImport } from './commands/module-import';
36
+ import { handleModuleList } from './commands/module-list';
37
+ import { handleModuleLogs } from './commands/module-logs';
38
+ import { handleModuleRemove } from './commands/module-remove';
39
+ import { handleModuleShowConfig, handleModuleShowZone } from './commands/module-show';
40
+ import { handleModuleStatus } from './commands/module-status';
41
+ import { handleModuleTypesCheck, handleModuleTypesGenerate } from './commands/module-types';
42
+ import { handleModuleUpgrade } from './commands/module-upgrade';
43
+ import { handlePackage } from './commands/package';
44
+ import { handleSecretList } from './commands/secret-list';
45
+ import { handleSecretSet } from './commands/secret-set';
46
+ import { handleServiceAddDigitalOcean } from './commands/service-add-digitalocean';
47
+ import { handleServiceAddProxmox } from './commands/service-add-proxmox';
48
+ import { handleServiceConfigGet } from './commands/service-config-get';
49
+ import { handleServiceConfigSet } from './commands/service-config-set';
50
+ import { handleServiceList } from './commands/service-list';
51
+ import { handleServiceReconfigure } from './commands/service-reconfigure';
52
+ import { handleServiceRemove } from './commands/service-remove';
53
+ import { handleServiceVerify } from './commands/service-verify';
54
+ import { handleStatus } from './commands/status';
55
+ import { handleSystemConfigGet, handleSystemConfigSet } from './commands/system-config';
56
+ import { handleSystemInit } from './commands/system-init';
57
+ import { handleSystemSecretGet } from './commands/system-secret-get';
58
+ import { handleSystemSecretSet } from './commands/system-secret-set';
59
+ import { handleSystemVaultPassword } from './commands/system-vault-password';
60
+ import { getCompletions } from './completion';
61
+ import { parseArguments, validateFlags } from './parser';
62
+ import type { CommandResult } from './types';
63
+
64
+ /**
65
+ * Look up a command definition from the registry and validate flags.
66
+ * Walks the full subcommand chain (e.g., ipam -> ip -> reserve) to find
67
+ * the leaf command where flags are defined.
68
+ * Returns an error CommandResult if unknown flags are found, or null if valid.
69
+ */
70
+ function checkFlags(
71
+ command: string,
72
+ subcommand: string | undefined,
73
+ flags: Record<string, string | boolean>,
74
+ args: string[] = [],
75
+ ): CommandResult | null {
76
+ // Skip validation for help requests
77
+ if (flags.help || flags.h) return null;
78
+
79
+ const topDef = COMMANDS.find((c) => c.name === command);
80
+ if (!topDef) return null;
81
+
82
+ let commandDef: CommandDef | undefined = subcommand
83
+ ? topDef.subcommands?.find((s) => s.name === subcommand)
84
+ : topDef;
85
+ if (!commandDef) return null;
86
+
87
+ // Walk deeper into nested subcommands using args
88
+ // e.g., for "ipam ip reserve", subcommand='ip' and args=['reserve', ...]
89
+ // We need to find the 'reserve' sub-subcommand to get its flags
90
+ for (const arg of args) {
91
+ const deeper: CommandDef | undefined = commandDef?.subcommands?.find((s) => s.name === arg);
92
+ if (!deeper) break;
93
+ commandDef = deeper;
94
+ }
95
+
96
+ if (!commandDef) return null;
97
+
98
+ const error = validateFlags(flags, commandDef);
99
+ if (error) {
100
+ return { success: false, error };
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Display general help message
107
+ */
108
+ function displayHelp(): CommandResult {
109
+ const helpText = `
110
+ Celilo - Home Lab Orchestration System
111
+
112
+ Usage:
113
+ celilo <command> [subcommand] [args...] [options]
114
+
115
+ Commands:
116
+ status Show system and module status
117
+ capability View registered module capabilities
118
+ package Create distributable .netapp packages from module source
119
+ module Manage modules (import, list, configure, build, generate)
120
+ service Manage container services (Proxmox, Digital Ocean)
121
+ storage Manage backup storage destinations
122
+ backup Create and manage backups
123
+ machine Manage machine pool (bring-your-own-hardware)
124
+ system Manage system configuration
125
+ ipam Manage IP address and VMID allocations and reservations
126
+ completion Generate shell completion scripts (bash/zsh)
127
+
128
+ help, --help, -h Show this help message
129
+
130
+ For command-specific help:
131
+ celilo package --help
132
+ celilo module --help
133
+ celilo secret --help
134
+ celilo service --help
135
+ celilo machine --help
136
+ celilo system --help
137
+ celilo ipam --help
138
+
139
+ Enable tab completion:
140
+ # Bash
141
+ celilo completion bash >> ~/.bashrc && source ~/.bashrc
142
+ # Zsh
143
+ celilo completion zsh >> ~/.zshrc && source ~/.zshrc
144
+
145
+ Examples:
146
+ celilo package ./modules/homebridge
147
+ celilo module import ./modules/homebridge
148
+ celilo module list
149
+ celilo module build caddy
150
+ celilo module secret set homebridge api_key mykey123
151
+ celilo system config set dns.primary 192.168.0.1
152
+ celilo system vault-password
153
+ `;
154
+
155
+ return {
156
+ success: true,
157
+ message: helpText.trim(),
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Display package command help
163
+ */
164
+ function displayPackageHelp(): CommandResult {
165
+ const helpText = `
166
+ Celilo - Module Packaging
167
+
168
+ Usage:
169
+ celilo package <source-directory> [options]
170
+
171
+ Description:
172
+ Creates a distributable .netapp package from a module source directory.
173
+ The package includes checksums and a signature for integrity verification.
174
+
175
+ Options:
176
+ --output <path> Output path for the package (default: <source-dir>/<module-id>.netapp)
177
+
178
+ Examples:
179
+ celilo package ./modules/homebridge
180
+ celilo package ./my-module --output /tmp/test.netapp
181
+ celilo package ../custom-module
182
+
183
+ Related Commands:
184
+ celilo module import <package.netapp> Import a packaged module
185
+ celilo module audit <module-id> Verify package integrity
186
+ `;
187
+
188
+ return {
189
+ success: true,
190
+ message: helpText.trim(),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Display capability command help
196
+ */
197
+ function displayCapabilityHelp(): CommandResult {
198
+ const helpText = `
199
+ Celilo - Capability Management
200
+
201
+ Usage:
202
+ celilo capability <subcommand> [args...]
203
+
204
+ Subcommands:
205
+ list List all registered capabilities
206
+ info <name> Show detailed capability information
207
+
208
+ Description:
209
+ Capabilities are cross-module data contracts. A module can "provide" a capability
210
+ (e.g., dns_registrar) and other modules can "require" it to access shared data
211
+ and secrets.
212
+
213
+ Capabilities are automatically registered when importing modules that declare
214
+ "provides.capabilities" in their manifest.
215
+
216
+ Examples:
217
+ celilo capability list
218
+ celilo capability info dns_registrar
219
+ celilo capability info public_web
220
+ `;
221
+
222
+ return {
223
+ success: true,
224
+ message: helpText.trim(),
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Display module command help
230
+ */
231
+ function displayModuleHelp(): CommandResult {
232
+ const helpText = `
233
+ Celilo - Module Management
234
+
235
+ Usage:
236
+ celilo module <subcommand> [args...] [options]
237
+
238
+ Subcommands:
239
+ import <path> Import a module from directory or .netapp file
240
+ Options:
241
+ --target <path> Target base directory (default: platform-specific)
242
+ --auto-generate-secrets Auto-generate all secrets without prompting
243
+
244
+ list List all installed modules
245
+
246
+ remove <id> Remove a module and all its data
247
+
248
+ audit <id> Verify module integrity (checksums)
249
+
250
+ config set <id> <key> <value> Set module configuration value
251
+ config get <id> [key] Get module configuration value(s)
252
+
253
+ secret set <id> <key> <value> Set encrypted module secret
254
+ secret list <id> List module secrets
255
+
256
+ show-config <id> Show all config including auto-derived values
257
+ show-zone <id> Show module's network zone and config
258
+
259
+ build <module-id> Execute build scripts (Nix + Ansible)
260
+
261
+ generate <id> Generate infrastructure code for module
262
+ Options:
263
+ --output <path> Output directory (default: <module>/generated)
264
+
265
+ deploy <module-id> Deploy module to infrastructure (auto-generates/builds if needed)
266
+ Options:
267
+ --debug Run hooks with visible browser (Playwright)
268
+ --no-interactive Fail instead of prompting for missing config
269
+
270
+ health [module-id] Run health checks (transitions to VERIFIED on success)
271
+ Options:
272
+ --json Machine-readable JSON output
273
+ --debug Show detailed check output
274
+
275
+ run-hook <id> <hook-name> [k=v...] Run a module hook (e.g., retry on_install after fixing an issue)
276
+ Options:
277
+ --debug Run with visible browser (Playwright hooks)
278
+
279
+ update <path> [path...] Update module code while preserving state (configs, secrets, infra)
280
+
281
+ Examples:
282
+ celilo module import ./modules/homebridge
283
+ celilo module import homebridge.netapp
284
+ celilo module import /abs/path/to/module --target /custom/location
285
+ celilo module list
286
+ celilo module remove homebridge
287
+ celilo module audit homebridge
288
+ celilo module config set homebridge hostname myhost
289
+ celilo module config set homebridge container_ip "192.168.0.110/24"
290
+ celilo module config get homebridge
291
+ celilo module config get homebridge hostname
292
+ celilo module show-config homebridge
293
+ celilo module show-zone homebridge
294
+ celilo module build caddy
295
+ celilo module generate homebridge
296
+ celilo module generate homebridge --output /custom/output
297
+ celilo module deploy homebridge
298
+ celilo module deploy caddy
299
+ celilo module run-hook caddy on_install
300
+ celilo module run-hook namecheap validate_config --debug
301
+ `;
302
+
303
+ return {
304
+ success: true,
305
+ message: helpText.trim(),
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Display service command help
311
+ */
312
+ function displayServiceHelp(): CommandResult {
313
+ const helpText = `
314
+ Celilo - Container Service Management
315
+
316
+ Usage:
317
+ celilo service <subcommand> [args...] [options]
318
+
319
+ Subcommands:
320
+ add proxmox Configure a Proxmox container service
321
+ add digitalocean Configure a Digital Ocean VPS service
322
+ list List all configured container services
323
+ Options:
324
+ --zone <zone> Filter by network zone
325
+ verify <service-id> Re-verify a container service connection
326
+ reconfigure <service-id> Re-run configuration interview (change template, storage, etc.)
327
+ remove <id> Remove a container service
328
+ Options:
329
+ --force Skip confirmation prompts
330
+ config get <service-id> [key] Get service configuration
331
+ config set <service-id> <key> <value> Set service configuration
332
+
333
+ Description:
334
+ Container services are API-driven infrastructure providers that can automatically
335
+ provision containers/VMs for modules. Celilo prefers container services over
336
+ manually-added machines when selecting infrastructure during module generation.
337
+
338
+ Supported providers:
339
+ - Proxmox: LXC containers on home lab hardware
340
+ - Digital Ocean: Cloud VPS instances
341
+
342
+ Examples:
343
+ # Add container services
344
+ celilo service add proxmox
345
+ celilo service add digitalocean
346
+
347
+ # List all services
348
+ celilo service list
349
+
350
+ # List services for specific zone
351
+ celilo service list --zone dmz
352
+
353
+ # Verify a service connection
354
+ celilo service verify proxmox-home-lab
355
+
356
+ # Get service configuration
357
+ celilo service config get proxmox-home-lab
358
+ celilo service config get proxmox-home-lab name
359
+ celilo service config get proxmox-home-lab zones
360
+
361
+ # Set service configuration
362
+ celilo service config set proxmox-home-lab name "New Service Name"
363
+ celilo service config set proxmox-home-lab zones '["dmz", "app"]'
364
+ celilo service config set proxmox-home-lab providerConfig '{"storage": "local-lvm"}'
365
+
366
+ # Remove service
367
+ celilo service remove <service-id>
368
+ celilo service remove <service-id> --force
369
+
370
+ Related Commands:
371
+ celilo module generate <module-id> Generate infrastructure code (uses services)
372
+ celilo module deploy <module-id> Deploy module to infrastructure
373
+ `;
374
+
375
+ return {
376
+ success: true,
377
+ message: helpText.trim(),
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Display backup command help
383
+ */
384
+ function displayBackupHelp(): CommandResult {
385
+ const helpText = `
386
+ Celilo - Backup Management
387
+
388
+ Usage:
389
+ celilo backup <subcommand> [args...] [options]
390
+
391
+ Subcommands:
392
+ create [module-id] Create a backup (system state + modules, or specific module)
393
+ Options:
394
+ --force Ignore schedule, back up now
395
+ --storage <id> Use specific storage destination
396
+ --no-interactive Non-interactive mode (for cron)
397
+
398
+ list [module-id] List available backups
399
+ Options:
400
+ --limit <n> Number of backups to show (default: 20)
401
+
402
+ restore <backup-id> Restore from a backup
403
+ Options:
404
+ --yes Skip confirmation prompt
405
+
406
+ delete <backup-id> Delete a specific backup
407
+ Options:
408
+ --force Skip confirmation prompt
409
+
410
+ prune [module-id] Remove old backups per retention policies
411
+ Options:
412
+ --dry-run Show what would be deleted without deleting
413
+
414
+ name <backup-id> [name] Get or set a human-readable name on a backup
415
+ Options:
416
+ --clear Remove the name
417
+
418
+ import <file> <module> Import a local file as a module backup
419
+ Options:
420
+ --name <name> Human-readable name for the backup
421
+ --schema-version <v> Schema version metadata
422
+ --storage <id> Use specific storage destination
423
+ --yes Skip confirmation prompt
424
+
425
+ Description:
426
+ Backups are encrypted with the master key and uploaded to a configured
427
+ storage destination. System state backups capture the Celilo database.
428
+ Module data backups use on_backup hooks defined in module manifests.
429
+
430
+ Any command that accepts a <backup-id> also accepts a backup name.
431
+ Use "celilo backup name" to assign human-readable names.
432
+
433
+ Examples:
434
+ # Create a full backup (system + all due modules)
435
+ celilo backup create
436
+
437
+ # Back up a specific module
438
+ celilo backup create authentik --force
439
+
440
+ # List recent backups
441
+ celilo backup list
442
+ celilo backup list authentik --limit 5
443
+
444
+ # Restore from a backup
445
+ celilo backup restore <backup-id>
446
+
447
+ # Name a backup
448
+ celilo backup name <backup-id> Data from beso before migration
449
+ celilo backup name <backup-id> --clear
450
+
451
+ # Prune old backups
452
+ celilo backup prune --dry-run
453
+ celilo backup prune
454
+
455
+ # Import a local file as a backup
456
+ celilo backup import ./backups/db.sqlite lunacycle --name "pre-migration"
457
+
458
+ Related Commands:
459
+ celilo storage add local Add backup storage destination
460
+ celilo storage list List storage destinations
461
+ `;
462
+
463
+ return {
464
+ success: true,
465
+ message: helpText.trim(),
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Display storage command help
471
+ */
472
+ function displayStorageHelp(): CommandResult {
473
+ const helpText = `
474
+ Celilo - Backup Storage Management
475
+
476
+ Usage:
477
+ celilo storage <subcommand> [args...] [options]
478
+
479
+ Subcommands:
480
+ add local Add local filesystem storage
481
+ add s3 Add S3-compatible storage (AWS, MinIO, B2, Wasabi, GCS)
482
+ list List all configured storage destinations
483
+ verify <storage-id> Test storage connectivity and permissions
484
+ set-default <id> Set the default backup storage destination
485
+ remove <storage-id> Remove a storage destination
486
+ Options:
487
+ --force Skip confirmation prompts
488
+
489
+ Description:
490
+ Backup storage destinations are where Celilo sends encrypted backup archives.
491
+ You must configure at least one storage destination before creating backups.
492
+
493
+ Examples:
494
+ # Add local storage
495
+ celilo storage add local
496
+
497
+ # List storage destinations
498
+ celilo storage list
499
+
500
+ # Verify storage connectivity
501
+ celilo storage verify local-backups
502
+
503
+ # Set default destination
504
+ celilo storage set-default local-backups
505
+
506
+ # Remove storage
507
+ celilo storage remove local-backups --force
508
+
509
+ Related Commands:
510
+ celilo backup create Create a backup
511
+ celilo backup list List available backups
512
+ celilo backup restore <id> Restore from a backup
513
+ `;
514
+
515
+ return {
516
+ success: true,
517
+ message: helpText.trim(),
518
+ };
519
+ }
520
+
521
+ /**
522
+ * Display machine command help
523
+ */
524
+ function displayMachineHelp(): CommandResult {
525
+ const helpText = `
526
+ Celilo - Machine Pool Management
527
+
528
+ Usage:
529
+ celilo machine <subcommand> [args...] [options]
530
+
531
+ Subcommands:
532
+ add Add a machine to the pool with auto-detection
533
+ Interactive mode (prompts for input):
534
+ celilo machine add
535
+
536
+ Non-interactive mode:
537
+ celilo machine add --ip <ip> --ssh-user <user> --ssh-key-file <path>
538
+ Options:
539
+ --ip <ip> Machine IP address
540
+ --ssh-user <user> SSH username (default: root)
541
+ --ssh-key-file <path> Path to SSH private key
542
+
543
+ list List all machines in the pool
544
+ Options:
545
+ --zone <zone> Filter by network zone
546
+
547
+ status <hostname> Show detailed machine status with live connectivity
548
+
549
+ remove <hostname> Remove a machine from the pool
550
+ Options:
551
+ --force Skip confirmation prompts
552
+
553
+ Description:
554
+ The machine pool contains bring-your-own-hardware: existing machines like
555
+ Raspberry Pis, VPS instances, or bare metal servers that you manually add
556
+ to Celilo.
557
+
558
+ When adding a machine, Celilo automatically:
559
+ - Tests SSH connectivity
560
+ - Detects hostname, CPU, memory, disk
561
+ - Detects network zone from IP address
562
+ - Encrypts and stores SSH key for Ansible deployments
563
+
564
+ Celilo prefers container services over machine pool when selecting
565
+ infrastructure during module generation.
566
+
567
+ Examples:
568
+ # Add machine interactively
569
+ celilo machine add
570
+
571
+ # Add machine non-interactively
572
+ celilo machine add --ip 192.168.1.100 --ssh-user ubuntu --ssh-key-file ~/.ssh/id_ed25519
573
+
574
+ # List all machines
575
+ celilo machine list
576
+
577
+ # List machines in specific zone
578
+ celilo machine list --zone dmz
579
+
580
+ # Show machine status
581
+ celilo machine status pi-01
582
+
583
+ # Earmark machine for a module
584
+ celilo machine earmark 10.0.10.10 caddy
585
+ celilo machine earmark pi-01 --clear
586
+
587
+ # Remove machine (by hostname or IP)
588
+ celilo machine remove pi-01
589
+ celilo machine remove 10.0.10.10 --force
590
+
591
+ Related Commands:
592
+ celilo service list List container services
593
+ celilo module generate <id> Generate infrastructure code (selects infrastructure)
594
+ `;
595
+
596
+ return {
597
+ success: true,
598
+ message: helpText.trim(),
599
+ };
600
+ }
601
+
602
+ /**
603
+ * Display IPAM command help
604
+ */
605
+ function displayIpamHelp(): CommandResult {
606
+ const helpText = `
607
+ Celilo - IPAM (IP Address Management)
608
+
609
+ Usage:
610
+ celilo ipam <resource> <action> [args...] [options]
611
+
612
+ Resources:
613
+ vmid Manage VMID reservations and allocations
614
+ ip Manage IP address reservations
615
+ list-allocations List all IPAM allocations (VMIDs and IPs)
616
+
617
+ VMID Commands:
618
+ celilo ipam vmid reserve <vmid-or-range> --reason <reason>
619
+ celilo ipam vmid unreserve <vmid-or-range>
620
+ celilo ipam vmid list-reservations
621
+
622
+ IP Commands:
623
+ celilo ipam ip exclude <ip-range> --reason <reason> [--zone <zone>]
624
+ celilo ipam ip include <ip-or-range> [--zone <zone>]
625
+ celilo ipam ip list-exclusions
626
+
627
+ Description:
628
+ IPAM automatically allocates VMIDs and IP addresses during module generation.
629
+ Exclusions prevent IPAM from allocating specific IPs (e.g., DHCP ranges, infrastructure).
630
+
631
+ VMIDs start at 2100 and increment sequentially.
632
+ IPs are allocated from zone subnets (network.{zone}.subnet).
633
+ Zone is auto-detected from the IP address unless overridden with --zone.
634
+
635
+ Examples:
636
+ # Reserve VMID for existing VM
637
+ celilo ipam vmid reserve 2100 --reason "Existing Proxmox VM"
638
+ celilo ipam vmid reserve 2100-2110 --reason "Reserved range"
639
+
640
+ # Exclude IPs from IPAM allocation (zone auto-detected from IP)
641
+ celilo ipam ip exclude 10.0.10.1-10.0.10.9 --reason "Infrastructure"
642
+ celilo ipam ip exclude 192.168.0.1-192.168.0.150 --reason "DHCP managed range"
643
+
644
+ # List allocations and reservations
645
+ celilo ipam list-allocations
646
+ celilo ipam vmid list-reservations
647
+ celilo ipam ip list-reservations
648
+
649
+ # Remove reservations
650
+ celilo ipam vmid unreserve 2100
651
+ celilo ipam ip unreserve 10.0.10.1 --zone dmz
652
+ `;
653
+
654
+ return {
655
+ success: true,
656
+ message: helpText.trim(),
657
+ };
658
+ }
659
+
660
+ /**
661
+ * Display system command help
662
+ */
663
+ function displaySystemHelp(): CommandResult {
664
+ const helpText = `
665
+ Celilo - System Configuration
666
+
667
+ Usage:
668
+ celilo system <subcommand> [args...]
669
+
670
+ Subcommands:
671
+ init [--accept-defaults] [key=val...] Initialize system with guided setup or defaults
672
+
673
+ config set <key> <value> Set system-wide configuration value
674
+ config get [key] Get system configuration value(s)
675
+
676
+ secret set <key> <value> Set encrypted system-level secret
677
+ secret get [key] Get decrypted system secret value(s)
678
+
679
+ vault-password Get Ansible Vault password for decrypting secrets.yml
680
+
681
+ Description:
682
+ System initialization (init) guides you through first-time setup with interactive
683
+ prompts or applies sensible defaults for testing.
684
+
685
+ System configuration stores values that are shared across all modules, such as
686
+ DNS servers, network settings, and domain information. These values can be
687
+ referenced in module templates using $system:key.path syntax.
688
+
689
+ System secrets store encrypted sensitive values like passwords and tokens.
690
+ Note: Provider-specific credentials (Proxmox, DigitalOcean, etc.) are managed
691
+ via 'celilo service' commands, not system secrets.
692
+
693
+ Examples:
694
+ # Initialize system (first-time setup)
695
+ celilo system init
696
+ celilo system init --accept-defaults
697
+ celilo system init --accept-defaults primary_domain=example.com dns.primary=1.1.1.1
698
+
699
+ # Set DNS configuration
700
+ celilo system config set dns.primary 192.168.0.1
701
+ celilo system config set dns.fallback "8.8.8.8 1.1.1.1"
702
+
703
+ # Set network configuration
704
+ celilo system config set network.bridge vmbr0
705
+ celilo system config set network.dmz.subnet 10.0.10.0/24
706
+
707
+ # Get specific value
708
+ celilo system config get dns.primary
709
+
710
+ # Get all system config
711
+ celilo system config get
712
+ celilo system secret get
713
+
714
+ # Get vault password for Ansible
715
+ celilo system vault-password
716
+
717
+ Using Vault Password:
718
+ # View encrypted Ansible secrets
719
+ ansible-vault view secrets.yml \\
720
+ --vault-password-file=<(celilo system vault-password)
721
+
722
+ # Run Ansible playbook with encrypted secrets
723
+ ansible-playbook playbook.yml \\
724
+ --vault-password-file=<(celilo system vault-password)
725
+
726
+ # Edit encrypted secrets
727
+ ansible-vault edit secrets.yml \\
728
+ --vault-password-file=<(celilo system vault-password)
729
+ `;
730
+
731
+ return {
732
+ success: true,
733
+ message: helpText.trim(),
734
+ };
735
+ }
736
+
737
+ /**
738
+ * Route command to appropriate handler
739
+ *
740
+ * @param argv - Command-line arguments
741
+ * @returns Command result
742
+ */
743
+ export async function runCli(argv: string[]): Promise<CommandResult> {
744
+ const parsed = parseArguments(argv);
745
+
746
+ // Handle --get-completions for shell completion (must be before other processing)
747
+ if (parsed.flags['get-completions']) {
748
+ try {
749
+ // Arguments: celilo --get-completions word1 word2 ... currentIndex
750
+ // The words array and current index are passed as remaining args
751
+ const words = parsed.args.slice(0, -1); // All args except last
752
+ const currentStr = parsed.args[parsed.args.length - 1]; // Last arg is the index
753
+ const current = Number.parseInt(currentStr || '0', 10);
754
+
755
+ const suggestions = await getCompletions(words, current);
756
+
757
+ // Return suggestions as newline-separated list (for zsh array splitting)
758
+ return {
759
+ success: true,
760
+ message: suggestions.join('\n'),
761
+ };
762
+ } catch (_error) {
763
+ // Silent failure for completion errors
764
+ return { success: true, message: '' };
765
+ }
766
+ }
767
+
768
+ // Handle general help (only if no specific command, or command is 'help')
769
+ if (parsed.command === 'help') {
770
+ return displayHelp();
771
+ }
772
+
773
+ // Handle completion command
774
+ if (parsed.command === 'completion') {
775
+ // Completion is a simple command without subcommands
776
+ // If parser treated first arg as subcommand, prepend it to args
777
+ const completionArgs = parsed.subcommand ? [parsed.subcommand, ...parsed.args] : parsed.args;
778
+ return handleCompletion(completionArgs, parsed.flags);
779
+ }
780
+
781
+ // Handle status command
782
+ if (parsed.command === 'status') {
783
+ return handleStatus();
784
+ }
785
+
786
+ // Route commands
787
+ if (parsed.command === 'package') {
788
+ // Handle package --help
789
+ if (parsed.flags.help || parsed.flags.h) {
790
+ return displayPackageHelp();
791
+ }
792
+
793
+ // Package is a simple command without subcommands
794
+ // If parser treated first arg as subcommand, prepend it to args
795
+ const packageArgs = parsed.subcommand ? [parsed.subcommand, ...parsed.args] : parsed.args;
796
+ return handlePackage(packageArgs, parsed.flags);
797
+ }
798
+
799
+ if (parsed.command === 'hook') {
800
+ if (parsed.flags.help || parsed.flags.h) {
801
+ return {
802
+ success: true,
803
+ message: [
804
+ 'Hook commands:',
805
+ '',
806
+ ' celilo hook run <module-id> <hook-name> [--debug] [key=value...]',
807
+ '',
808
+ 'Options:',
809
+ ' --debug Run with visible browser (Playwright hooks)',
810
+ '',
811
+ 'Examples:',
812
+ ' celilo hook run namecheap validate_config --debug',
813
+ ' celilo hook run namecheap container_created vps_ip=138.68.140.177',
814
+ ].join('\n'),
815
+ };
816
+ }
817
+
818
+ if (parsed.subcommand === 'run') {
819
+ return handleHookRun(parsed.args, parsed.flags);
820
+ }
821
+
822
+ return {
823
+ success: false,
824
+ error: 'Hook subcommand required\n\nRun "celilo hook --help" for usage',
825
+ };
826
+ }
827
+
828
+ if (parsed.command === 'capability') {
829
+ if (parsed.flags.help || parsed.flags.h) {
830
+ return displayCapabilityHelp();
831
+ }
832
+
833
+ if (!parsed.subcommand) {
834
+ return {
835
+ success: false,
836
+ error: 'Capability subcommand required\n\nRun "celilo capability --help" for usage',
837
+ };
838
+ }
839
+
840
+ const capFlagError = checkFlags('capability', parsed.subcommand, parsed.flags, parsed.args);
841
+ if (capFlagError) return capFlagError;
842
+
843
+ if (parsed.subcommand === 'list') {
844
+ return handleCapabilityList();
845
+ }
846
+
847
+ if (parsed.subcommand === 'info') {
848
+ return handleCapabilityInfo(parsed.args);
849
+ }
850
+
851
+ return {
852
+ success: false,
853
+ error: `Unknown capability subcommand: ${parsed.subcommand}\n\nRun "celilo capability --help" for usage`,
854
+ };
855
+ }
856
+
857
+ if (parsed.command === 'module') {
858
+ // Handle module --help
859
+ if (parsed.flags.help || parsed.flags.h) {
860
+ return displayModuleHelp();
861
+ }
862
+
863
+ if (!parsed.subcommand) {
864
+ return {
865
+ success: false,
866
+ error: 'Module subcommand required\n\nRun "celilo module --help" for usage',
867
+ };
868
+ }
869
+
870
+ const flagError = checkFlags('module', parsed.subcommand, parsed.flags, parsed.args);
871
+ if (flagError) return flagError;
872
+
873
+ switch (parsed.subcommand) {
874
+ case 'import':
875
+ return handleModuleImport(parsed.args, parsed.flags);
876
+ case 'list':
877
+ return handleModuleList();
878
+ case 'status':
879
+ return handleModuleStatus(parsed.args);
880
+ case 'logs':
881
+ return handleModuleLogs(parsed.args, parsed.flags);
882
+ case 'health':
883
+ return handleModuleHealth(parsed.args, parsed.flags);
884
+ case 'remove':
885
+ return handleModuleRemove(parsed.args, parsed.flags);
886
+ case 'update':
887
+ return handleModuleUpgrade(parsed.args, parsed.flags);
888
+ case 'audit':
889
+ return moduleAudit(parsed.args);
890
+ case 'config': {
891
+ // Config requires additional subcommand (set/get)
892
+ const configSubcommand = parsed.args[0];
893
+ if (!configSubcommand) {
894
+ return {
895
+ success: false,
896
+ error: 'Config action required (set or get)\n\nRun "celilo help" for usage',
897
+ };
898
+ }
899
+ const configArgs = parsed.args.slice(1);
900
+ if (configSubcommand === 'set') {
901
+ return handleModuleConfigSet(configArgs);
902
+ }
903
+ if (configSubcommand === 'get') {
904
+ return handleModuleConfigGet(configArgs);
905
+ }
906
+ return {
907
+ success: false,
908
+ error: `Unknown config action: ${configSubcommand}`,
909
+ };
910
+ }
911
+ case 'secret': {
912
+ // Module secret requires additional subcommand (set/list)
913
+ const moduleSecretSubcommand = parsed.args[0];
914
+ if (!moduleSecretSubcommand) {
915
+ return {
916
+ success: false,
917
+ error:
918
+ 'Secret action required (set or list)\n\nUsage:\n celilo module secret set <module-id> <key> <value>\n celilo module secret list <module-id>',
919
+ };
920
+ }
921
+ const moduleSecretArgs = parsed.args.slice(1);
922
+ if (moduleSecretSubcommand === 'set') {
923
+ return handleSecretSet(moduleSecretArgs);
924
+ }
925
+ if (moduleSecretSubcommand === 'list') {
926
+ return handleSecretList(moduleSecretArgs);
927
+ }
928
+ return {
929
+ success: false,
930
+ error: `Unknown secret action: ${moduleSecretSubcommand}`,
931
+ };
932
+ }
933
+ case 'types': {
934
+ // Types requires additional subcommand (generate/check)
935
+ const typesSubcommand = parsed.args[0];
936
+ if (!typesSubcommand) {
937
+ return {
938
+ success: false,
939
+ error:
940
+ 'Types action required (generate or check)\n\nUsage:\n celilo module types generate <module-dir>\n celilo module types check <module-dir>',
941
+ };
942
+ }
943
+ const typesArgs = parsed.args.slice(1);
944
+ if (typesSubcommand === 'generate') {
945
+ return handleModuleTypesGenerate(typesArgs);
946
+ }
947
+ if (typesSubcommand === 'check') {
948
+ return handleModuleTypesCheck(typesArgs);
949
+ }
950
+ return {
951
+ success: false,
952
+ error: `Unknown types action: ${typesSubcommand}\n\nAvailable: generate, check`,
953
+ };
954
+ }
955
+ case 'generate':
956
+ return handleModuleGenerate(parsed.args, parsed.flags);
957
+ case 'build':
958
+ return handleModuleBuild(parsed.args, parsed.flags);
959
+ case 'deploy':
960
+ return handleModuleDeploy(parsed.args, parsed.flags);
961
+ case 'validate':
962
+ // Alias for `module deploy --preflight`
963
+ return handleModuleDeploy(parsed.args, { ...parsed.flags, preflight: true });
964
+ case 'run-hook':
965
+ return handleHookRun(parsed.args, parsed.flags);
966
+ case 'show-config':
967
+ return handleModuleShowConfig(parsed.args);
968
+ case 'show-zone':
969
+ return handleModuleShowZone(parsed.args);
970
+ default:
971
+ return {
972
+ success: false,
973
+ error: `Unknown module subcommand: ${parsed.subcommand}\n\nRun "celilo help" for usage`,
974
+ };
975
+ }
976
+ }
977
+
978
+ if (parsed.command === 'secret') {
979
+ return {
980
+ success: false,
981
+ error:
982
+ 'The top-level "secret" command has been moved.\n\nUse:\n celilo module secret set <module-id> <key> <value>\n celilo module secret list <module-id>\n celilo system secret set <key> <value>\n celilo system secret get <key>',
983
+ };
984
+ }
985
+
986
+ if (parsed.command === 'service') {
987
+ // Handle service --help
988
+ if (parsed.flags.help || parsed.flags.h) {
989
+ return displayServiceHelp();
990
+ }
991
+
992
+ if (!parsed.subcommand) {
993
+ return {
994
+ success: false,
995
+ error: 'Service subcommand required\n\nRun "celilo service --help" for usage',
996
+ };
997
+ }
998
+
999
+ const svcFlagError = checkFlags('service', parsed.subcommand, parsed.flags, parsed.args);
1000
+ if (svcFlagError) return svcFlagError;
1001
+
1002
+ if (parsed.subcommand === 'add') {
1003
+ // Add requires provider type
1004
+ const provider = parsed.args[0];
1005
+ if (!provider) {
1006
+ return {
1007
+ success: false,
1008
+ error:
1009
+ 'Provider type required (proxmox or digitalocean)\n\nRun "celilo service --help" for usage',
1010
+ };
1011
+ }
1012
+ const addArgs = parsed.args.slice(1);
1013
+ if (provider === 'proxmox') {
1014
+ return handleServiceAddProxmox(addArgs, parsed.flags);
1015
+ }
1016
+ if (provider === 'digitalocean') {
1017
+ return handleServiceAddDigitalOcean(addArgs, parsed.flags);
1018
+ }
1019
+ return {
1020
+ success: false,
1021
+ error: `Unknown provider: ${provider}\n\nSupported providers: proxmox, digitalocean`,
1022
+ };
1023
+ }
1024
+
1025
+ if (parsed.subcommand === 'list') {
1026
+ return handleServiceList(parsed.args, parsed.flags);
1027
+ }
1028
+
1029
+ if (parsed.subcommand === 'remove') {
1030
+ return handleServiceRemove(parsed.args, parsed.flags);
1031
+ }
1032
+
1033
+ if (parsed.subcommand === 'verify') {
1034
+ return handleServiceVerify(parsed.args, parsed.flags);
1035
+ }
1036
+
1037
+ if (parsed.subcommand === 'reconfigure') {
1038
+ return handleServiceReconfigure(parsed.args, parsed.flags);
1039
+ }
1040
+
1041
+ if (parsed.subcommand === 'config') {
1042
+ const configSubcommand = parsed.args[0];
1043
+ if (!configSubcommand) {
1044
+ return {
1045
+ success: false,
1046
+ error: 'Config action required (set or get)\n\nRun "celilo help" for usage',
1047
+ };
1048
+ }
1049
+ const configArgs = parsed.args.slice(1);
1050
+ if (configSubcommand === 'set') {
1051
+ return handleServiceConfigSet(configArgs);
1052
+ }
1053
+ if (configSubcommand === 'get') {
1054
+ return handleServiceConfigGet(configArgs);
1055
+ }
1056
+ return {
1057
+ success: false,
1058
+ error: `Unknown config action: ${configSubcommand}`,
1059
+ };
1060
+ }
1061
+
1062
+ return {
1063
+ success: false,
1064
+ error: `Unknown service subcommand: ${parsed.subcommand}\n\nRun "celilo service --help" for usage`,
1065
+ };
1066
+ }
1067
+
1068
+ if (parsed.command === 'storage') {
1069
+ if (parsed.flags.help || parsed.flags.h) {
1070
+ return displayStorageHelp();
1071
+ }
1072
+
1073
+ if (!parsed.subcommand) {
1074
+ return {
1075
+ success: false,
1076
+ error: 'Storage subcommand required\n\nRun "celilo storage --help" for usage',
1077
+ };
1078
+ }
1079
+
1080
+ // Skip flag validation for 'add' — flags belong to the sub-subcommand (local/s3)
1081
+ if (parsed.subcommand !== 'add') {
1082
+ const storageFlagError = checkFlags('storage', parsed.subcommand, parsed.flags, parsed.args);
1083
+ if (storageFlagError) return storageFlagError;
1084
+ }
1085
+
1086
+ if (parsed.subcommand === 'add') {
1087
+ const provider = parsed.args[0];
1088
+ if (!provider) {
1089
+ return {
1090
+ success: false,
1091
+ error: 'Provider type required (local or s3)\n\nRun "celilo storage --help" for usage',
1092
+ };
1093
+ }
1094
+ const addArgs = parsed.args.slice(1);
1095
+ if (provider === 'local') {
1096
+ const { handleStorageAddLocal } = await import('./commands/storage-add-local');
1097
+ return handleStorageAddLocal(addArgs, parsed.flags);
1098
+ }
1099
+ if (provider === 's3') {
1100
+ const { handleStorageAddS3 } = await import('./commands/storage-add-s3');
1101
+ return handleStorageAddS3(addArgs, parsed.flags);
1102
+ }
1103
+ return {
1104
+ success: false,
1105
+ error: `Unknown storage provider: ${provider}\n\nSupported providers: local, s3`,
1106
+ };
1107
+ }
1108
+
1109
+ if (parsed.subcommand === 'list') {
1110
+ const { handleStorageList } = await import('./commands/storage-list');
1111
+ return handleStorageList(parsed.args, parsed.flags);
1112
+ }
1113
+
1114
+ if (parsed.subcommand === 'remove') {
1115
+ const { handleStorageRemove } = await import('./commands/storage-remove');
1116
+ return handleStorageRemove(parsed.args, parsed.flags);
1117
+ }
1118
+
1119
+ if (parsed.subcommand === 'verify') {
1120
+ const { handleStorageVerify } = await import('./commands/storage-verify');
1121
+ return handleStorageVerify(parsed.args, parsed.flags);
1122
+ }
1123
+
1124
+ if (parsed.subcommand === 'set-default') {
1125
+ const { handleStorageSetDefault } = await import('./commands/storage-set-default');
1126
+ return handleStorageSetDefault(parsed.args, parsed.flags);
1127
+ }
1128
+
1129
+ return {
1130
+ success: false,
1131
+ error: `Unknown storage subcommand: ${parsed.subcommand}\n\nRun "celilo storage --help" for usage`,
1132
+ };
1133
+ }
1134
+
1135
+ if (parsed.command === 'backup') {
1136
+ if (parsed.flags.help || parsed.flags.h) {
1137
+ return displayBackupHelp();
1138
+ }
1139
+
1140
+ if (!parsed.subcommand) {
1141
+ return {
1142
+ success: false,
1143
+ error: 'Backup subcommand required\n\nRun "celilo backup --help" for usage',
1144
+ };
1145
+ }
1146
+
1147
+ const backupFlagError = checkFlags('backup', parsed.subcommand, parsed.flags, parsed.args);
1148
+ if (backupFlagError) return backupFlagError;
1149
+
1150
+ if (parsed.subcommand === 'create') {
1151
+ const { handleBackupCreate } = await import('./commands/backup-create');
1152
+ return handleBackupCreate(parsed.args, parsed.flags);
1153
+ }
1154
+
1155
+ if (parsed.subcommand === 'list') {
1156
+ const { handleBackupList } = await import('./commands/backup-list');
1157
+ return handleBackupList(parsed.args, parsed.flags);
1158
+ }
1159
+
1160
+ if (parsed.subcommand === 'restore') {
1161
+ const { handleBackupRestore } = await import('./commands/backup-restore');
1162
+ return handleBackupRestore(parsed.args, parsed.flags);
1163
+ }
1164
+
1165
+ if (parsed.subcommand === 'prune') {
1166
+ const { handleBackupPrune } = await import('./commands/backup-prune');
1167
+ return handleBackupPrune(parsed.args, parsed.flags);
1168
+ }
1169
+
1170
+ if (parsed.subcommand === 'delete') {
1171
+ const { handleBackupDelete } = await import('./commands/backup-delete');
1172
+ return handleBackupDelete(parsed.args, parsed.flags);
1173
+ }
1174
+
1175
+ if (parsed.subcommand === 'name') {
1176
+ const { handleBackupName } = await import('./commands/backup-name');
1177
+ return handleBackupName(parsed.args, parsed.flags);
1178
+ }
1179
+
1180
+ if (parsed.subcommand === 'import') {
1181
+ const { handleBackupImport } = await import('./commands/backup-import');
1182
+ return handleBackupImport(parsed.args, parsed.flags);
1183
+ }
1184
+
1185
+ return {
1186
+ success: false,
1187
+ error: `Unknown backup subcommand: ${parsed.subcommand}\n\nRun "celilo backup --help" for usage`,
1188
+ };
1189
+ }
1190
+
1191
+ if (parsed.command === 'machine') {
1192
+ // Handle machine --help
1193
+ if (parsed.flags.help || parsed.flags.h) {
1194
+ return displayMachineHelp();
1195
+ }
1196
+
1197
+ if (!parsed.subcommand) {
1198
+ return {
1199
+ success: false,
1200
+ error: 'Machine subcommand required\n\nRun "celilo machine --help" for usage',
1201
+ };
1202
+ }
1203
+
1204
+ const machFlagError = checkFlags('machine', parsed.subcommand, parsed.flags, parsed.args);
1205
+ if (machFlagError) return machFlagError;
1206
+
1207
+ if (parsed.subcommand === 'add') {
1208
+ return handleMachineAdd(parsed.args, parsed.flags);
1209
+ }
1210
+
1211
+ if (parsed.subcommand === 'list') {
1212
+ return handleMachineList(parsed.args, parsed.flags);
1213
+ }
1214
+
1215
+ if (parsed.subcommand === 'status') {
1216
+ return handleMachineStatus(parsed.args, parsed.flags);
1217
+ }
1218
+
1219
+ if (parsed.subcommand === 'remove') {
1220
+ return handleMachineRemove(parsed.args, parsed.flags);
1221
+ }
1222
+
1223
+ if (parsed.subcommand === 'earmark') {
1224
+ return handleMachineEarmark(parsed.args, parsed.flags);
1225
+ }
1226
+
1227
+ return {
1228
+ success: false,
1229
+ error: `Unknown machine subcommand: ${parsed.subcommand}\n\nRun "celilo machine --help" for usage`,
1230
+ };
1231
+ }
1232
+
1233
+ if (parsed.command === 'system') {
1234
+ // Handle system --help
1235
+ if (parsed.flags.help || parsed.flags.h) {
1236
+ return displaySystemHelp();
1237
+ }
1238
+
1239
+ if (!parsed.subcommand) {
1240
+ return {
1241
+ success: false,
1242
+ error: 'System subcommand required\n\nRun "celilo system --help" for usage',
1243
+ };
1244
+ }
1245
+
1246
+ const sysFlagError = checkFlags('system', parsed.subcommand, parsed.flags, parsed.args);
1247
+ if (sysFlagError) return sysFlagError;
1248
+
1249
+ if (parsed.subcommand === 'init') {
1250
+ return handleSystemInit(parsed.args, parsed.flags);
1251
+ }
1252
+
1253
+ if (parsed.subcommand === 'config') {
1254
+ const configSubcommand = parsed.args[0];
1255
+ if (!configSubcommand) {
1256
+ return {
1257
+ success: false,
1258
+ error: 'Config action required (set or get)\n\nRun "celilo system --help" for usage',
1259
+ };
1260
+ }
1261
+ const configArgs = parsed.args.slice(1);
1262
+ if (configSubcommand === 'set') {
1263
+ return handleSystemConfigSet(configArgs);
1264
+ }
1265
+ if (configSubcommand === 'get') {
1266
+ return handleSystemConfigGet(configArgs);
1267
+ }
1268
+ return {
1269
+ success: false,
1270
+ error: `Unknown config action: ${configSubcommand}`,
1271
+ };
1272
+ }
1273
+
1274
+ if (parsed.subcommand === 'secret') {
1275
+ const secretSubcommand = parsed.args[0];
1276
+ if (!secretSubcommand) {
1277
+ return {
1278
+ success: false,
1279
+ error: 'Secret action required (set or get)\n\nRun "celilo system --help" for usage',
1280
+ };
1281
+ }
1282
+ const secretArgs = parsed.args.slice(1);
1283
+ if (secretSubcommand === 'set') {
1284
+ return handleSystemSecretSet(secretArgs);
1285
+ }
1286
+ if (secretSubcommand === 'get') {
1287
+ return handleSystemSecretGet(secretArgs);
1288
+ }
1289
+ return {
1290
+ success: false,
1291
+ error: `Unknown secret action: ${secretSubcommand}`,
1292
+ };
1293
+ }
1294
+
1295
+ if (parsed.subcommand === 'vault-password') {
1296
+ return handleSystemVaultPassword();
1297
+ }
1298
+
1299
+ return {
1300
+ success: false,
1301
+ error: `Unknown system subcommand: ${parsed.subcommand}\n\nRun "celilo system --help" for usage`,
1302
+ };
1303
+ }
1304
+
1305
+ if (parsed.command === 'ipam') {
1306
+ // Handle ipam --help
1307
+ if (parsed.flags.help || parsed.flags.h) {
1308
+ return displayIpamHelp();
1309
+ }
1310
+
1311
+ if (!parsed.subcommand) {
1312
+ return {
1313
+ success: false,
1314
+ error: 'IPAM subcommand required\n\nRun "celilo ipam --help" for usage',
1315
+ };
1316
+ }
1317
+
1318
+ const ipamFlagError = checkFlags('ipam', parsed.subcommand, parsed.flags, parsed.args);
1319
+ if (ipamFlagError) return ipamFlagError;
1320
+
1321
+ // Handle show command (comprehensive summary)
1322
+ if (parsed.subcommand === 'show') {
1323
+ return handleIpamShow(parsed.args, parsed.flags);
1324
+ }
1325
+
1326
+ // Handle list-allocations (no additional subcommand)
1327
+ if (parsed.subcommand === 'list-allocations') {
1328
+ return handleIpamListAllocations(parsed.args, parsed.flags);
1329
+ }
1330
+
1331
+ // VMID commands
1332
+ if (parsed.subcommand === 'vmid') {
1333
+ const vmidSubcommand = parsed.args[0];
1334
+ if (!vmidSubcommand) {
1335
+ return {
1336
+ success: false,
1337
+ error:
1338
+ 'VMID action required (reserve, unreserve, list-reservations)\n\nRun "celilo ipam --help" for usage',
1339
+ };
1340
+ }
1341
+ const vmidArgs = parsed.args.slice(1);
1342
+ if (vmidSubcommand === 'reserve') {
1343
+ return handleIpamVmidReserve(vmidArgs, parsed.flags);
1344
+ }
1345
+ if (vmidSubcommand === 'unreserve') {
1346
+ return handleIpamVmidUnreserve(vmidArgs, parsed.flags);
1347
+ }
1348
+ if (vmidSubcommand === 'list-reservations') {
1349
+ return handleIpamVmidListReservations(vmidArgs, parsed.flags);
1350
+ }
1351
+ return {
1352
+ success: false,
1353
+ error: `Unknown VMID action: ${vmidSubcommand}`,
1354
+ };
1355
+ }
1356
+
1357
+ // IP commands
1358
+ if (parsed.subcommand === 'ip') {
1359
+ const ipSubcommand = parsed.args[0];
1360
+ if (!ipSubcommand) {
1361
+ return {
1362
+ success: false,
1363
+ error:
1364
+ 'IP action required (exclude, include, list-exclusions)\n\nRun "celilo ipam --help" for usage',
1365
+ };
1366
+ }
1367
+ const ipArgs = parsed.args.slice(1);
1368
+ if (ipSubcommand === 'exclude') {
1369
+ return handleIpamIpReserve(ipArgs, parsed.flags);
1370
+ }
1371
+ if (ipSubcommand === 'include') {
1372
+ return handleIpamIpUnreserve(ipArgs, parsed.flags);
1373
+ }
1374
+ if (ipSubcommand === 'list-exclusions') {
1375
+ return handleIpamIpListReservations(ipArgs, parsed.flags);
1376
+ }
1377
+ return {
1378
+ success: false,
1379
+ error: `Unknown IP action: ${ipSubcommand}`,
1380
+ };
1381
+ }
1382
+
1383
+ return {
1384
+ success: false,
1385
+ error: `Unknown IPAM subcommand: ${parsed.subcommand}\n\nRun "celilo ipam --help" for usage`,
1386
+ };
1387
+ }
1388
+
1389
+ return {
1390
+ success: false,
1391
+ error: `Unknown command: ${parsed.command}\n\nRun "celilo help" for usage`,
1392
+ };
1393
+ }
1394
+
1395
+ /**
1396
+ * Server Mode - Persistent CLI Process
1397
+ *
1398
+ * Accepts commands via stdin, executes them, and returns results via stdout.
1399
+ * Used by test infrastructure for efficient command execution without spawning
1400
+ * fresh processes.
1401
+ *
1402
+ * Protocol:
1403
+ * - Input (stdin): JSON lines with `{ command: string, id: number }`
1404
+ * - Output (stdout): JSON lines with `{ id: number, success: boolean, message?: string, error?: string, exitCode: number, duration: number }`
1405
+ * - Special command "__exit__" terminates server
1406
+ * - Special command "__ping__" returns pong (health check)
1407
+ */
1408
+ /**
1409
+ * Parse command string respecting quotes
1410
+ * Handles: system config set dns.fallback "8.8.8.8 1.1.1.1"
1411
+ * Returns: ['system', 'config', 'set', 'dns.fallback', '8.8.8.8 1.1.1.1']
1412
+ */
1413
+ function parseCommand(command: string): string[] {
1414
+ const args: string[] = [];
1415
+ let current = '';
1416
+ let inQuotes = false;
1417
+ let quoteChar = '';
1418
+
1419
+ for (let i = 0; i < command.length; i++) {
1420
+ const char = command[i];
1421
+
1422
+ if ((char === '"' || char === "'") && !inQuotes) {
1423
+ // Start of quoted string
1424
+ inQuotes = true;
1425
+ quoteChar = char;
1426
+ } else if (char === quoteChar && inQuotes) {
1427
+ // End of quoted string
1428
+ inQuotes = false;
1429
+ quoteChar = '';
1430
+ } else if (char === ' ' && !inQuotes) {
1431
+ // Space outside quotes - word boundary
1432
+ if (current) {
1433
+ args.push(current);
1434
+ current = '';
1435
+ }
1436
+ } else {
1437
+ // Regular character
1438
+ current += char;
1439
+ }
1440
+ }
1441
+
1442
+ // Add final argument
1443
+ if (current) {
1444
+ args.push(current);
1445
+ }
1446
+
1447
+ return args;
1448
+ }
1449
+
1450
+ async function serverMode(): Promise<void> {
1451
+ const readline = await import('node:readline');
1452
+
1453
+ // Signal ready
1454
+ console.log(JSON.stringify({ type: 'ready', pid: process.pid }));
1455
+
1456
+ const rl = readline.createInterface({
1457
+ input: process.stdin,
1458
+ output: process.stdout,
1459
+ terminal: false,
1460
+ });
1461
+
1462
+ // Keep process alive - don't exit when stdin ends
1463
+ rl.on('close', () => {
1464
+ // Stdin closed - exit gracefully
1465
+ process.exit(0);
1466
+ });
1467
+
1468
+ for await (const line of rl) {
1469
+ try {
1470
+ const request = parseJsonWithValidation(line, CLIServerRequestSchema, 'CLI server request');
1471
+
1472
+ // Health check
1473
+ if (request.command === '__ping__') {
1474
+ console.log(
1475
+ JSON.stringify({
1476
+ id: request.id,
1477
+ success: true,
1478
+ message: 'pong',
1479
+ exitCode: 0,
1480
+ duration: 0,
1481
+ }),
1482
+ );
1483
+ continue;
1484
+ }
1485
+
1486
+ // Exit command
1487
+ if (request.command === '__exit__') {
1488
+ console.log(
1489
+ JSON.stringify({
1490
+ id: request.id,
1491
+ success: true,
1492
+ message: 'exiting',
1493
+ exitCode: 0,
1494
+ duration: 0,
1495
+ }),
1496
+ );
1497
+ process.exit(0);
1498
+ }
1499
+
1500
+ // Execute command
1501
+ const startTime = Date.now();
1502
+ const argv = ['bun', 'celilo', ...parseCommand(request.command)];
1503
+ const result = await runCli(argv);
1504
+ const duration = Date.now() - startTime;
1505
+
1506
+ // Send response
1507
+ console.log(
1508
+ JSON.stringify({
1509
+ id: request.id,
1510
+ success: result.success,
1511
+ message: result.success ? result.message : undefined,
1512
+ error: !result.success ? result.error : undefined,
1513
+ details: !result.success ? result.details : undefined,
1514
+ exitCode: result.success ? 0 : 1,
1515
+ duration,
1516
+ }),
1517
+ );
1518
+ } catch (error) {
1519
+ // Parse or execution error
1520
+ console.error(
1521
+ JSON.stringify({
1522
+ id: -1,
1523
+ success: false,
1524
+ error: `Server error: ${error}`,
1525
+ exitCode: 1,
1526
+ duration: 0,
1527
+ }),
1528
+ );
1529
+ }
1530
+ }
1531
+ }
1532
+
1533
+ /**
1534
+ * Main CLI entry point
1535
+ *
1536
+ * Executes CLI and handles output/exit codes.
1537
+ * If CLI_SERVER_MODE=true, runs in persistent server mode for testing.
1538
+ */
1539
+ export async function main(): Promise<void> {
1540
+ // Check for server mode
1541
+ if (process.env.CLI_SERVER_MODE === 'true') {
1542
+ await serverMode();
1543
+ return;
1544
+ }
1545
+
1546
+ // Normal single-command execution
1547
+ try {
1548
+ const result = await runCli(process.argv);
1549
+
1550
+ if (result.success) {
1551
+ // For completion mode, output raw text without formatting
1552
+ if (process.argv.includes('--get-completions') || process.argv.includes('completion')) {
1553
+ console.log(result.message);
1554
+ process.exit(0);
1555
+ }
1556
+
1557
+ // Handle multi-line messages: split on section boundaries (\n\n) so
1558
+ // lines within a section are logged together (avoiding clack's per-call spacing).
1559
+ const sections = result.message.split('\n\n');
1560
+ p.log.success(sections[0]);
1561
+ for (let i = 1; i < sections.length; i++) {
1562
+ if (sections[i].trim()) {
1563
+ p.log.message(sections[i]);
1564
+ }
1565
+ }
1566
+ process.exit(0);
1567
+ }
1568
+
1569
+ p.log.error(`Error: ${result.error}`);
1570
+ if (result.details) {
1571
+ console.error('Details:', result.details);
1572
+ }
1573
+ process.exit(1);
1574
+ } catch (error) {
1575
+ console.error('Fatal error:', error);
1576
+ process.exit(1);
1577
+ }
1578
+ }
1579
+
1580
+ // Execute main only when run directly (not when imported by tests)
1581
+ if (import.meta.main) {
1582
+ await main();
1583
+ }