@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,497 @@
1
+ /**
2
+ * Rich Zsh Completion Generator
3
+ *
4
+ * Generates a full zsh completion file from the command registry.
5
+ * Produces native zsh completions with descriptions, flags, dynamic
6
+ * database-backed argument completions, and nested command hierarchies.
7
+ *
8
+ * This is the ONLY source for zsh completions. The generated file
9
+ * should never be hand-edited.
10
+ */
11
+
12
+ import type { ArgDef, CommandDef, FlagDef } from './command-registry';
13
+
14
+ /**
15
+ * Escape a string for use in zsh completion descriptions.
16
+ * Colons and backslashes need escaping in _describe arrays.
17
+ */
18
+ function escapeZshDescription(text: string): string {
19
+ return text.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
20
+ }
21
+
22
+ /**
23
+ * Convert a command path to a zsh function name.
24
+ * e.g., ["module", "config"] → "_celilo_module_config"
25
+ */
26
+ function functionName(path: string[]): string {
27
+ return `_celilo${path.length > 0 ? `_${path.join('_')}` : ''}`;
28
+ }
29
+
30
+ /**
31
+ * Generate the _arguments line for flags
32
+ */
33
+ function generateFlagArgs(flags: FlagDef[]): string[] {
34
+ return flags.map((flag) => {
35
+ const desc = escapeZshDescription(flag.description);
36
+ if (flag.takesValue) {
37
+ const hint = flag.valueHint ?? '';
38
+ // If valueHint is a zsh completer function (starts with _), use directly
39
+ // Otherwise treat as space-separated literal values
40
+ if (hint.startsWith('_')) {
41
+ return `'--${flag.name}[${desc}]:${flag.name}:${hint}'`;
42
+ }
43
+ if (hint) {
44
+ return `'--${flag.name}[${desc}]:${flag.name}:(${hint})'`;
45
+ }
46
+ return `'--${flag.name}[${desc}]:${flag.name}:'`;
47
+ }
48
+ return `'--${flag.name}[${desc}]'`;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Get the zsh completion action for a dynamic completion source
54
+ */
55
+ function completionAction(completion: string): string {
56
+ switch (completion) {
57
+ case 'module_ids':
58
+ return '_celilo_module_ids';
59
+ case 'service_ids':
60
+ return '_celilo_service_ids';
61
+ case 'machine_hostnames':
62
+ return '_celilo_machine_hostnames';
63
+ case 'capability_names':
64
+ return '_celilo_capability_names';
65
+ case 'config_keys':
66
+ return '_celilo_config_keys';
67
+ case 'system_config_keys':
68
+ return '_celilo_system_config_keys';
69
+ case 'system_secret_keys':
70
+ return '_celilo_system_secret_keys';
71
+ case 'storage_ids':
72
+ return '_celilo_storage_ids';
73
+ case 'backup_ids':
74
+ return '_celilo_backup_ids';
75
+ case 'files':
76
+ return '_files';
77
+ case 'directories':
78
+ return '_files -/';
79
+ default:
80
+ return '';
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Generate positional argument specs for _arguments
86
+ */
87
+ function generatePositionalArgs(args: ArgDef[]): string[] {
88
+ return args.map((arg, i) => {
89
+ const pos = arg.variadic ? '*' : String(i + 1);
90
+ const desc = escapeZshDescription(arg.description);
91
+ if (arg.completion) {
92
+ const action = completionAction(arg.completion);
93
+ return `'${pos}:${desc}:${action}'`;
94
+ }
95
+ return `'${pos}:${desc}:_files'`;
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Generate a leaf command's completion (args + flags, no subcommands)
101
+ */
102
+ function generateLeafArgs(cmd: CommandDef): string {
103
+ const parts: string[] = [];
104
+
105
+ if (cmd.flags && cmd.flags.length > 0) {
106
+ parts.push(...generateFlagArgs(cmd.flags));
107
+ }
108
+
109
+ if (cmd.args && cmd.args.length > 0) {
110
+ parts.push(...generatePositionalArgs(cmd.args));
111
+ }
112
+
113
+ if (parts.length === 0) {
114
+ return ' # No additional arguments';
115
+ }
116
+
117
+ if (parts.length === 1) {
118
+ return ` _arguments ${parts[0]}`;
119
+ }
120
+
121
+ return ` _arguments \\\n ${parts.join(' \\\n ')}`;
122
+ }
123
+
124
+ /**
125
+ * Generate a command function that has subcommands
126
+ */
127
+ function generateCommandFunction(cmd: CommandDef, path: string[], lines: string[]): void {
128
+ const fnName = functionName(path);
129
+ const subcommands = cmd.subcommands ?? [];
130
+
131
+ lines.push(`${fnName}() {`);
132
+ lines.push(' local curcontext="$curcontext" state line');
133
+ lines.push(' typeset -A opt_args');
134
+ lines.push('');
135
+ lines.push(' _arguments -C \\');
136
+ lines.push(` '1: :${fnName}_commands' \\`);
137
+ lines.push(" '*::arg:->args'");
138
+ lines.push('');
139
+ lines.push(' case $state in');
140
+ lines.push(' args)');
141
+ lines.push(' case $line[1] in');
142
+
143
+ for (const sub of subcommands) {
144
+ if (sub.subcommands && sub.subcommands.length > 0) {
145
+ // Sub has its own subcommands — dispatch to its function
146
+ lines.push(` ${sub.name})`);
147
+ lines.push(` ${functionName([...path, sub.name])}`);
148
+ lines.push(' ;;');
149
+ } else {
150
+ // Leaf subcommand
151
+ lines.push(` ${sub.name})`);
152
+ lines.push(generateLeafArgs(sub));
153
+ lines.push(' ;;');
154
+ }
155
+ }
156
+
157
+ lines.push(' esac');
158
+ lines.push(' ;;');
159
+ lines.push(' esac');
160
+ lines.push('}');
161
+ lines.push('');
162
+
163
+ // Generate the _commands list function
164
+ lines.push(`${fnName}_commands() {`);
165
+ lines.push(' local commands=(');
166
+ for (const sub of subcommands) {
167
+ lines.push(` '${sub.name}:${escapeZshDescription(sub.description)}'`);
168
+ }
169
+ lines.push(' )');
170
+ lines.push(" _describe 'command' commands");
171
+ lines.push('}');
172
+ lines.push('');
173
+
174
+ // Recurse into subcommands that have their own subcommands
175
+ for (const sub of subcommands) {
176
+ if (sub.subcommands && sub.subcommands.length > 0) {
177
+ generateCommandFunction(sub, [...path, sub.name], lines);
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Collect all dynamic completion sources used across the command tree
184
+ */
185
+ function collectCompletionSources(commands: CommandDef[]): Set<string> {
186
+ const sources = new Set<string>();
187
+
188
+ function walk(cmd: CommandDef): void {
189
+ if (cmd.args) {
190
+ for (const arg of cmd.args) {
191
+ if (arg.completion && !['files', 'directories'].includes(arg.completion)) {
192
+ sources.add(arg.completion);
193
+ }
194
+ }
195
+ }
196
+ if (cmd.subcommands) {
197
+ for (const sub of cmd.subcommands) {
198
+ walk(sub);
199
+ }
200
+ }
201
+ }
202
+
203
+ for (const cmd of commands) {
204
+ walk(cmd);
205
+ }
206
+
207
+ return sources;
208
+ }
209
+
210
+ /**
211
+ * Generate the dynamic completion helper functions
212
+ */
213
+ function generateDynamicCompletions(sources: Set<string>): string[] {
214
+ const lines: string[] = [];
215
+
216
+ if (sources.has('module_ids')) {
217
+ lines.push(`# Dynamic completions - module IDs
218
+ _celilo_module_ids() {
219
+ local -a module_ids
220
+ local db_path="\${CELILO_DB_PATH:-$HOME/Library/Application Support/celilo/celilo.db}"
221
+
222
+ if [[ -f "$db_path" ]] && command -v sqlite3 >/dev/null 2>&1; then
223
+ module_ids=(\${(f)"$(sqlite3 "$db_path" "SELECT id FROM modules;" 2>/dev/null)"})
224
+ if [[ \${#module_ids[@]} -gt 0 ]]; then
225
+ _describe 'module id' module_ids
226
+ return
227
+ fi
228
+ fi
229
+
230
+ _message 'module-id'
231
+ }
232
+ `);
233
+ }
234
+
235
+ if (sources.has('service_ids')) {
236
+ lines.push(`# Dynamic completions - service IDs
237
+ _celilo_service_ids() {
238
+ local -a service_ids
239
+ local db_path="\${CELILO_DB_PATH:-$HOME/Library/Application Support/celilo/celilo.db}"
240
+
241
+ if [[ -f "$db_path" ]] && command -v sqlite3 >/dev/null 2>&1; then
242
+ while IFS='|' read -r id name; do
243
+ service_ids+=("$id:$name")
244
+ done < <(sqlite3 "$db_path" "SELECT service_id, name FROM container_services;" 2>/dev/null)
245
+ if [[ \${#service_ids[@]} -gt 0 ]]; then
246
+ _describe 'service id' service_ids
247
+ return
248
+ fi
249
+ fi
250
+
251
+ _message 'service-id'
252
+ }
253
+ `);
254
+ }
255
+
256
+ if (sources.has('machine_hostnames')) {
257
+ lines.push(`# Dynamic completions - machine hostnames
258
+ _celilo_machine_hostnames() {
259
+ local -a hostnames
260
+ local db_path="\${CELILO_DB_PATH:-$HOME/Library/Application Support/celilo/celilo.db}"
261
+
262
+ if [[ -f "$db_path" ]] && command -v sqlite3 >/dev/null 2>&1; then
263
+ hostnames=(\${(f)"$(sqlite3 "$db_path" "SELECT hostname FROM machines;" 2>/dev/null)"})
264
+ if [[ \${#hostnames[@]} -gt 0 ]]; then
265
+ _describe 'hostname' hostnames
266
+ return
267
+ fi
268
+ fi
269
+
270
+ _message 'hostname'
271
+ }
272
+ `);
273
+ }
274
+
275
+ if (sources.has('capability_names')) {
276
+ lines.push(`# Dynamic completions - capability names
277
+ _celilo_capability_names() {
278
+ local -a capability_names
279
+ local db_path="\${CELILO_DB_PATH:-$HOME/Library/Application Support/celilo/celilo.db}"
280
+
281
+ if [[ -f "$db_path" ]] && command -v sqlite3 >/dev/null 2>&1; then
282
+ while IFS='|' read -r name module_id; do
283
+ capability_names+=("$name:provided by $module_id")
284
+ done < <(sqlite3 "$db_path" "SELECT capability_name, module_id FROM capabilities;" 2>/dev/null)
285
+ if [[ \${#capability_names[@]} -gt 0 ]]; then
286
+ _describe 'capability name' capability_names
287
+ return
288
+ fi
289
+ fi
290
+
291
+ _message 'capability-name'
292
+ }
293
+ `);
294
+ }
295
+
296
+ if (sources.has('config_keys')) {
297
+ lines.push(`# Dynamic completions - module config keys (from manifest)
298
+ _celilo_config_keys() {
299
+ local module_id=$line[1]
300
+ local db_path="\${CELILO_DB_PATH:-$HOME/Library/Application Support/celilo/celilo.db}"
301
+
302
+ if [[ -n "$module_id" ]] && [[ -f "$db_path" ]] && command -v sqlite3 >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
303
+ local manifest_json=$(sqlite3 "$db_path" "SELECT manifest_data FROM modules WHERE id = '$module_id';" 2>/dev/null)
304
+ if [[ -n "$manifest_json" ]]; then
305
+ local -a config_keys
306
+ while IFS='|' read -r key desc required; do
307
+ local desc_text="$desc"
308
+ if [[ "$required" == "true" ]]; then
309
+ desc_text="$desc (required)"
310
+ fi
311
+ config_keys+=("$key:$desc_text")
312
+ done < <(echo "$manifest_json" | jq -r '.variables.owns[]? | "\\(.name)|\\(.description // "No description")|\\(.required // false)"' 2>/dev/null)
313
+ if [[ \${#config_keys[@]} -gt 0 ]]; then
314
+ _describe 'config key' config_keys
315
+ return 0
316
+ fi
317
+ fi
318
+ fi
319
+
320
+ _message 'config-key'
321
+ }
322
+ `);
323
+ }
324
+
325
+ if (sources.has('system_config_keys')) {
326
+ lines.push(`# Dynamic completions - system config keys
327
+ _celilo_system_config_keys() {
328
+ local config_keys=(
329
+ 'proxmox.api_url:Proxmox API URL'
330
+ 'proxmox.default_target_node:Default Proxmox node'
331
+ 'proxmox.lxc_template:LXC container template'
332
+ 'network.bridge:Network bridge name'
333
+ 'network.dmz.subnet:DMZ subnet'
334
+ 'network.dmz.vlan:DMZ VLAN ID'
335
+ 'network.dmz.gateway:DMZ gateway IP'
336
+ 'network.app.subnet:App subnet'
337
+ 'network.app.vlan:App VLAN ID'
338
+ 'network.app.gateway:App gateway IP'
339
+ 'network.secure.subnet:Secure subnet'
340
+ 'network.secure.vlan:Secure VLAN ID'
341
+ 'network.secure.gateway:Secure gateway IP'
342
+ 'network.internal.subnet:Internal subnet'
343
+ 'network.internal.gateway:Internal gateway IP'
344
+ 'dns.primary:Primary DNS server'
345
+ 'dns.fallback:Fallback DNS servers'
346
+ 'routing.internal_gateway:Internal gateway IP'
347
+ 'ssh.public_key:SSH public key'
348
+ )
349
+ _describe 'config key' config_keys
350
+ }
351
+ `);
352
+ }
353
+
354
+ if (sources.has('system_secret_keys')) {
355
+ lines.push(`# Dynamic completions - system secret keys
356
+ _celilo_system_secret_keys() {
357
+ local secret_keys=(
358
+ 'proxmox.root_password:Proxmox root password'
359
+ )
360
+ _describe 'secret key' secret_keys
361
+ }
362
+ `);
363
+ }
364
+
365
+ if (sources.has('storage_ids')) {
366
+ lines.push(`# Dynamic completions - backup storage IDs
367
+ _celilo_storage_ids() {
368
+ local -a storage_ids
369
+ local db_path="\${CELILO_DB_PATH:-$HOME/Library/Application Support/celilo/celilo.db}"
370
+
371
+ if [[ -f "$db_path" ]] && command -v sqlite3 >/dev/null 2>&1; then
372
+ while IFS='|' read -r id name; do
373
+ storage_ids+=("$id:$name")
374
+ done < <(sqlite3 "$db_path" "SELECT storage_id, name FROM backup_storages;" 2>/dev/null)
375
+ if [[ \${#storage_ids[@]} -gt 0 ]]; then
376
+ _describe 'storage id' storage_ids
377
+ return
378
+ fi
379
+ fi
380
+
381
+ _message 'storage-id'
382
+ }
383
+ `);
384
+ }
385
+
386
+ if (sources.has('backup_ids')) {
387
+ lines.push(`# Dynamic completions - backup IDs and names
388
+ _celilo_backup_ids() {
389
+ local -a backup_ids
390
+ local db_path="\${CELILO_DB_PATH:-$HOME/Library/Application Support/celilo/celilo.db}"
391
+
392
+ if [[ -f "$db_path" ]] && command -v sqlite3 >/dev/null 2>&1; then
393
+ while IFS='|' read -r id module_id backup_type started_at name; do
394
+ local short_id="\${id:0:8}"
395
+ local label="\${module_id:-system}"
396
+ if [[ "$backup_type" == "system_state" ]]; then label="system"; fi
397
+ local date="\${started_at}"
398
+ backup_ids+=("$short_id:$label $date")
399
+ if [[ -n "$name" ]]; then
400
+ backup_ids+=("$name:$label $date")
401
+ fi
402
+ done < <(sqlite3 "$db_path" "SELECT id, module_id, backup_type, datetime(started_at, 'unixepoch'), name FROM backups ORDER BY started_at DESC LIMIT 50;" 2>/dev/null)
403
+ if [[ \${#backup_ids[@]} -gt 0 ]]; then
404
+ _describe 'backup id' backup_ids
405
+ return
406
+ fi
407
+ fi
408
+
409
+ _message 'backup-id or name'
410
+ }
411
+ `);
412
+ }
413
+
414
+ return lines;
415
+ }
416
+
417
+ /**
418
+ * Generate the complete rich zsh completion script
419
+ *
420
+ * @param commands - Command tree from the registry
421
+ * @returns Complete zsh completion script as a string
422
+ */
423
+ export function generateRichZshCompletion(commands: CommandDef[]): string {
424
+ const lines: string[] = [];
425
+
426
+ // Header
427
+ lines.push('#compdef celilo');
428
+ lines.push('');
429
+ lines.push('# Auto-generated by: celilo completion zsh');
430
+ lines.push('# Do not edit manually. Regenerate with:');
431
+ lines.push('# celilo completion zsh > ~/.zsh/completions/_celilo');
432
+ lines.push('');
433
+
434
+ // Main _celilo function
435
+ lines.push('_celilo() {');
436
+ lines.push(' local curcontext="$curcontext" state line');
437
+ lines.push(' typeset -A opt_args');
438
+ lines.push('');
439
+ lines.push(' _arguments -C \\');
440
+ lines.push(" '1: :_celilo_commands' \\");
441
+ lines.push(" '*::arg:->args'");
442
+ lines.push('');
443
+ lines.push(' case $state in');
444
+ lines.push(' args)');
445
+ lines.push(' case $line[1] in');
446
+
447
+ for (const cmd of commands) {
448
+ if (cmd.subcommands && cmd.subcommands.length > 0) {
449
+ lines.push(` ${cmd.name})`);
450
+ lines.push(` _celilo_${cmd.name}`);
451
+ lines.push(' ;;');
452
+ } else if (cmd.args || cmd.flags) {
453
+ lines.push(` ${cmd.name})`);
454
+ lines.push(generateLeafArgs(cmd));
455
+ lines.push(' ;;');
456
+ } else {
457
+ lines.push(` ${cmd.name})`);
458
+ lines.push(' # No additional arguments');
459
+ lines.push(' ;;');
460
+ }
461
+ }
462
+
463
+ lines.push(' esac');
464
+ lines.push(' ;;');
465
+ lines.push(' esac');
466
+ lines.push('}');
467
+ lines.push('');
468
+
469
+ // Top-level commands list
470
+ lines.push('_celilo_commands() {');
471
+ lines.push(' local commands=(');
472
+ for (const cmd of commands) {
473
+ lines.push(` '${cmd.name}:${escapeZshDescription(cmd.description)}'`);
474
+ }
475
+ lines.push(' )');
476
+ lines.push(" _describe 'command' commands");
477
+ lines.push('}');
478
+ lines.push('');
479
+
480
+ // Generate functions for each command with subcommands
481
+ for (const cmd of commands) {
482
+ if (cmd.subcommands && cmd.subcommands.length > 0) {
483
+ generateCommandFunction(cmd, [cmd.name], lines);
484
+ }
485
+ }
486
+
487
+ // Generate dynamic completion helpers
488
+ const sources = collectCompletionSources(commands);
489
+ const dynamicLines = generateDynamicCompletions(sources);
490
+ lines.push(...dynamicLines);
491
+
492
+ // Footer
493
+ lines.push('_celilo "$@"');
494
+ lines.push('');
495
+
496
+ return lines.join('\n');
497
+ }