@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,37 @@
1
+ /**
2
+ * Tests for shell completion
3
+ */
4
+
5
+ import { describe, expect, it } from 'bun:test';
6
+ import { getCompletions } from './completion';
7
+
8
+ describe('getCompletions', () => {
9
+ it('completes top-level commands', async () => {
10
+ const suggestions = await getCompletions(['m'], 0);
11
+ expect(suggestions).toContain('module');
12
+ expect(suggestions).toContain('machine');
13
+ expect(suggestions).not.toContain('service');
14
+ });
15
+
16
+ it('completes service subcommands', async () => {
17
+ const suggestions = await getCompletions(['service', 'v'], 1);
18
+ expect(suggestions).toContain('verify');
19
+ expect(suggestions).not.toContain('add');
20
+ });
21
+
22
+ it('completes service add providers', async () => {
23
+ const suggestions = await getCompletions(['service', 'add', 'p'], 2);
24
+ expect(suggestions).toContain('proxmox');
25
+ expect(suggestions).not.toContain('digitalocean');
26
+ });
27
+
28
+ it('returns empty array for unknown context', async () => {
29
+ const suggestions = await getCompletions(['unknown', 'command'], 2);
30
+ expect(suggestions).toEqual([]);
31
+ });
32
+
33
+ it('handles completion at different positions', async () => {
34
+ const suggestions = await getCompletions(['module', 'li'], 1);
35
+ expect(suggestions).toContain('list');
36
+ });
37
+ });
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Shell Completion Support
3
+ * Provides tab completion suggestions for bash/zsh
4
+ */
5
+
6
+ import { eq } from 'drizzle-orm';
7
+ import { getDb } from '../db/client';
8
+ import { capabilities, modules } from '../db/schema';
9
+ import type { ModuleManifest } from '../manifest/schema';
10
+ import { listBackups } from '../services/backup-metadata';
11
+ import { listBackupStorages } from '../services/backup-storage';
12
+ import { listContainerServices } from '../services/container-service';
13
+ import { listMachines } from '../services/machine-pool';
14
+
15
+ /**
16
+ * Get completion suggestions based on current command context
17
+ *
18
+ * @param words - Array of words typed so far
19
+ * @param current - Index of word being completed
20
+ * @returns Array of completion suggestions
21
+ */
22
+ export async function getCompletions(words: string[], current: number): Promise<string[]> {
23
+ // Remove 'celilo' from words if present
24
+ const args = words[0] === 'celilo' ? words.slice(1) : words;
25
+ const currentIndex = words[0] === 'celilo' ? current - 1 : current;
26
+
27
+ // Complete top-level commands
28
+ // currentIndex === 0 means we're completing the first word (the command)
29
+ if (currentIndex === 0) {
30
+ const commands = [
31
+ 'backup',
32
+ 'capability',
33
+ 'help',
34
+ 'hook',
35
+ 'package',
36
+ 'module',
37
+ 'service',
38
+ 'storage',
39
+ 'machine',
40
+ 'system',
41
+ 'ipam',
42
+ 'completion',
43
+ 'status',
44
+ 'version',
45
+ ];
46
+ return filterSuggestions(commands, args[0] || '');
47
+ }
48
+
49
+ const command = args[0];
50
+
51
+ // Capability subcommands
52
+ if (command === 'capability' && currentIndex === 1) {
53
+ const subcommands = ['list', 'info'];
54
+ return filterSuggestions(subcommands, args[1] || '');
55
+ }
56
+
57
+ // Capability info - complete with capability names
58
+ if (command === 'capability' && args[1] === 'info' && currentIndex === 2) {
59
+ const db = getDb();
60
+ const capabilityRows = db
61
+ .select({ name: capabilities.capabilityName })
62
+ .from(capabilities)
63
+ .all();
64
+ const capabilityNames = capabilityRows.map((c) => c.name);
65
+ return filterSuggestions(capabilityNames, args[2] || '');
66
+ }
67
+
68
+ // Hook subcommands
69
+ if (command === 'hook' && currentIndex === 1) {
70
+ const subcommands = ['run'];
71
+ return filterSuggestions(subcommands, args[1] || '');
72
+ }
73
+
74
+ // Hook run - complete with module IDs
75
+ if (command === 'hook' && args[1] === 'run' && currentIndex === 2) {
76
+ const db = getDb();
77
+ const moduleRows = db.select({ id: modules.id }).from(modules).all();
78
+ const moduleIds = moduleRows.map((p) => p.id);
79
+ return filterSuggestions(moduleIds, args[2] || '');
80
+ }
81
+
82
+ // Hook run <module-id> - complete with hook names from manifest
83
+ if (command === 'hook' && args[1] === 'run' && currentIndex === 3) {
84
+ const db = getDb();
85
+ const module = db
86
+ .select()
87
+ .from(modules)
88
+ .where(eq(modules.id, args[2] || ''))
89
+ .get();
90
+ if (module?.manifestData) {
91
+ const manifest = module.manifestData as ModuleManifest;
92
+ const hookNames = Object.keys(manifest.hooks || {});
93
+ return filterSuggestions(hookNames, args[3] || '');
94
+ }
95
+ }
96
+
97
+ // Module subcommands
98
+ if (command === 'module' && currentIndex === 1) {
99
+ const subcommands = [
100
+ 'import',
101
+ 'list',
102
+ 'remove',
103
+ 'update',
104
+ 'audit',
105
+ 'config',
106
+ 'show-config',
107
+ 'show-zone',
108
+ 'build',
109
+ 'generate',
110
+ 'deploy',
111
+ 'destroy',
112
+ 'health',
113
+ 'logs',
114
+ 'run-hook',
115
+ 'secret',
116
+ 'status',
117
+ 'types',
118
+ 'validate',
119
+ ];
120
+ return filterSuggestions(subcommands, args[1] || '');
121
+ }
122
+
123
+ // Module config subcommands (celilo module config set/get)
124
+ if (command === 'module' && args[1] === 'config' && currentIndex === 2) {
125
+ const subcommands = ['set', 'get'];
126
+ return filterSuggestions(subcommands, args[2] || '');
127
+ }
128
+
129
+ // Module types subcommands (celilo module types generate/check)
130
+ if (command === 'module' && args[1] === 'types' && currentIndex === 2) {
131
+ const subcommands = ['generate', 'check'];
132
+ return filterSuggestions(subcommands, args[2] || '');
133
+ }
134
+
135
+ // Module config set/get - complete with module IDs
136
+ if (
137
+ command === 'module' &&
138
+ args[1] === 'config' &&
139
+ (args[2] === 'set' || args[2] === 'get') &&
140
+ currentIndex === 3
141
+ ) {
142
+ const db = getDb();
143
+ const modulesList = db.select({ id: modules.id }).from(modules).all();
144
+ const moduleIds = modulesList.map((p) => p.id);
145
+ return filterSuggestions(moduleIds, args[3] || '');
146
+ }
147
+
148
+ // Module config set/get <module-id> - complete with config variable names
149
+ if (
150
+ command === 'module' &&
151
+ args[1] === 'config' &&
152
+ (args[2] === 'set' || args[2] === 'get') &&
153
+ currentIndex === 4
154
+ ) {
155
+ const db = getDb();
156
+ const module = db
157
+ .select()
158
+ .from(modules)
159
+ .where(eq(modules.id, args[3] || ''))
160
+ .get();
161
+ if (module?.manifestData) {
162
+ const manifest = module.manifestData as ModuleManifest;
163
+ const varNames = (manifest.variables?.owns || [])
164
+ .filter((v) => v.source === 'user' || !v.source)
165
+ .map((v) => v.name);
166
+ return filterSuggestions(varNames, args[4] || '');
167
+ }
168
+ }
169
+
170
+ // Module secret subcommands (celilo module secret set/list)
171
+ if (command === 'module' && args[1] === 'secret' && currentIndex === 2) {
172
+ const subcommands = ['set', 'list'];
173
+ return filterSuggestions(subcommands, args[2] || '');
174
+ }
175
+
176
+ // Module secret set/list - complete with module IDs
177
+ if (
178
+ command === 'module' &&
179
+ args[1] === 'secret' &&
180
+ (args[2] === 'set' || args[2] === 'list') &&
181
+ currentIndex === 3
182
+ ) {
183
+ const db = getDb();
184
+ const modulesList = db.select({ id: modules.id }).from(modules).all();
185
+ const moduleIds = modulesList.map((p) => p.id);
186
+ return filterSuggestions(moduleIds, args[3] || '');
187
+ }
188
+
189
+ // Module secret set <module-id> - complete with secret names
190
+ if (command === 'module' && args[1] === 'secret' && args[2] === 'set' && currentIndex === 4) {
191
+ const db = getDb();
192
+ const module = db
193
+ .select()
194
+ .from(modules)
195
+ .where(eq(modules.id, args[3] || ''))
196
+ .get();
197
+ if (module?.manifestData) {
198
+ const manifest = module.manifestData as ModuleManifest;
199
+ const secretNames = (manifest.secrets?.declares || []).map((s) => s.name);
200
+ return filterSuggestions(secretNames, args[4] || '');
201
+ }
202
+ }
203
+
204
+ // Service subcommands
205
+ if (command === 'service' && currentIndex === 1) {
206
+ const subcommands = ['add', 'list', 'verify', 'reconfigure', 'remove', 'config'];
207
+ return filterSuggestions(subcommands, args[1] || '');
208
+ }
209
+
210
+ // Service add providers
211
+ if (command === 'service' && args[1] === 'add' && currentIndex === 2) {
212
+ const providers = ['proxmox', 'digitalocean'];
213
+ return filterSuggestions(providers, args[2] || '');
214
+ }
215
+
216
+ // Service verify - complete with service IDs
217
+ if (command === 'service' && args[1] === 'verify' && currentIndex === 2) {
218
+ const services = await listContainerServices();
219
+ const serviceIds = services.map((s) => s.serviceId);
220
+ return filterSuggestions(serviceIds, args[2] || '');
221
+ }
222
+
223
+ // Service reconfigure - complete with service IDs
224
+ if (command === 'service' && args[1] === 'reconfigure' && currentIndex === 2) {
225
+ const services = await listContainerServices();
226
+ const serviceIds = services.map((s) => s.serviceId);
227
+ return filterSuggestions(serviceIds, args[2] || '');
228
+ }
229
+
230
+ // Service remove - complete with service IDs
231
+ if (command === 'service' && args[1] === 'remove' && currentIndex === 2) {
232
+ const services = await listContainerServices();
233
+ const serviceIds = services.map((s) => s.serviceId);
234
+ return filterSuggestions(serviceIds, args[2] || '');
235
+ }
236
+
237
+ // Service config operations
238
+ if (command === 'service' && args[1] === 'config' && currentIndex === 2) {
239
+ const operations = ['get', 'set'];
240
+ return filterSuggestions(operations, args[2] || '');
241
+ }
242
+
243
+ // Service config get - complete with service IDs
244
+ if (command === 'service' && args[1] === 'config' && args[2] === 'get' && currentIndex === 3) {
245
+ const services = await listContainerServices();
246
+ const serviceIds = services.map((s) => s.serviceId);
247
+ return filterSuggestions(serviceIds, args[3] || '');
248
+ }
249
+
250
+ // Service config get - complete with config keys
251
+ if (command === 'service' && args[1] === 'config' && args[2] === 'get' && currentIndex === 4) {
252
+ const configKeys = ['name', 'zones', 'providerConfig'];
253
+ return filterSuggestions(configKeys, args[4] || '');
254
+ }
255
+
256
+ // Service config set - complete with service IDs
257
+ if (command === 'service' && args[1] === 'config' && args[2] === 'set' && currentIndex === 3) {
258
+ const services = await listContainerServices();
259
+ const serviceIds = services.map((s) => s.serviceId);
260
+ return filterSuggestions(serviceIds, args[3] || '');
261
+ }
262
+
263
+ // Service config set - complete with config keys
264
+ if (command === 'service' && args[1] === 'config' && args[2] === 'set' && currentIndex === 4) {
265
+ const configKeys = ['name', 'zones', 'providerConfig'];
266
+ return filterSuggestions(configKeys, args[4] || '');
267
+ }
268
+
269
+ // Machine subcommands
270
+ if (command === 'machine' && currentIndex === 1) {
271
+ const subcommands = ['add', 'list', 'status', 'remove', 'earmark', 'detect'];
272
+ return filterSuggestions(subcommands, args[1] || '');
273
+ }
274
+
275
+ // Machine remove/earmark - complete with machine hostnames and IPs
276
+ if (
277
+ command === 'machine' &&
278
+ (args[1] === 'remove' || args[1] === 'earmark') &&
279
+ currentIndex === 2
280
+ ) {
281
+ const machineList = await listMachines();
282
+ const identifiers = [
283
+ ...machineList.map((m) => m.hostname),
284
+ ...machineList.map((m) => m.ipAddress),
285
+ ...machineList.flatMap((m) => m.interfaces.map((i) => i.ipAddress)),
286
+ ];
287
+ return filterSuggestions(identifiers, args[2] || '');
288
+ }
289
+
290
+ // Machine earmark - complete with module IDs at position 3
291
+ if (command === 'machine' && args[1] === 'earmark' && currentIndex === 3) {
292
+ const db = getDb();
293
+ const moduleRows = db.select().from(modules).all();
294
+ const moduleIds = moduleRows.map((p: { id: string }) => p.id);
295
+ return filterSuggestions(moduleIds, args[3] || '');
296
+ }
297
+
298
+ // Module commands that take module ID directly at position 2
299
+ if (command === 'module' && currentIndex === 2) {
300
+ const moduleCommands = [
301
+ 'generate',
302
+ 'deploy',
303
+ 'destroy',
304
+ 'logs',
305
+ 'remove',
306
+ 'build',
307
+ 'run-hook',
308
+ 'status',
309
+ ];
310
+ if (moduleCommands.includes(args[1] || '')) {
311
+ const db = getDb();
312
+ const moduleRows = db.select().from(modules).all();
313
+ const moduleIds = moduleRows.map((p: { id: string }) => p.id);
314
+ return filterSuggestions(moduleIds, args[2] || '');
315
+ }
316
+ }
317
+
318
+ // Module run-hook <module-id> - complete with hook names from manifest
319
+ if (command === 'module' && args[1] === 'run-hook' && currentIndex === 3) {
320
+ const db = getDb();
321
+ const module = db
322
+ .select()
323
+ .from(modules)
324
+ .where(eq(modules.id, args[2] || ''))
325
+ .get();
326
+ if (module?.manifestData) {
327
+ const manifest = module.manifestData as ModuleManifest;
328
+ const hookNames = Object.keys(manifest.hooks || {});
329
+ return filterSuggestions(hookNames, args[3] || '');
330
+ }
331
+ }
332
+
333
+ // Storage subcommands
334
+ if (command === 'storage' && currentIndex === 1) {
335
+ const subcommands = ['add', 'list', 'remove', 'verify', 'set-default'];
336
+ return filterSuggestions(subcommands, args[1] || '');
337
+ }
338
+
339
+ // Storage add providers
340
+ if (command === 'storage' && args[1] === 'add' && currentIndex === 2) {
341
+ const providers = ['local', 's3'];
342
+ return filterSuggestions(providers, args[2] || '');
343
+ }
344
+
345
+ // Storage remove/verify/set-default - complete with storage IDs
346
+ if (
347
+ command === 'storage' &&
348
+ (args[1] === 'remove' || args[1] === 'verify' || args[1] === 'set-default') &&
349
+ currentIndex === 2
350
+ ) {
351
+ const storages = listBackupStorages();
352
+ const storageIds = storages.map((s) => s.storageId);
353
+ return filterSuggestions(storageIds, args[2] || '');
354
+ }
355
+
356
+ // Backup subcommands
357
+ if (command === 'backup' && currentIndex === 1) {
358
+ const subcommands = ['create', 'list', 'restore', 'delete', 'prune', 'name', 'import'];
359
+ return filterSuggestions(subcommands, args[1] || '');
360
+ }
361
+
362
+ // Backup create/list/prune - complete with module IDs
363
+ if (
364
+ command === 'backup' &&
365
+ (args[1] === 'create' || args[1] === 'list' || args[1] === 'prune') &&
366
+ currentIndex === 2
367
+ ) {
368
+ const db = getDb();
369
+ const moduleRows = db.select({ id: modules.id }).from(modules).all();
370
+ const moduleIds = moduleRows.map((p: { id: string }) => p.id);
371
+ return filterSuggestions(moduleIds, args[2] || '');
372
+ }
373
+
374
+ // Backup restore/delete/name - complete with short backup IDs and names
375
+ if (
376
+ command === 'backup' &&
377
+ (args[1] === 'restore' || args[1] === 'delete' || args[1] === 'name') &&
378
+ currentIndex === 2
379
+ ) {
380
+ const backupList = listBackups({ limit: 50 });
381
+ const suggestions = backupList.map((b) => b.id.substring(0, 8));
382
+ for (const b of backupList) {
383
+ if (b.name) suggestions.push(b.name);
384
+ }
385
+ return filterSuggestions(suggestions, args[2] || '');
386
+ }
387
+
388
+ // IPAM subcommands
389
+ if (command === 'ipam' && currentIndex === 1) {
390
+ const subcommands = ['show', 'list-allocations', 'vmid', 'ip'];
391
+ return filterSuggestions(subcommands, args[1] || '');
392
+ }
393
+
394
+ // Completion subcommands
395
+ if (command === 'completion' && currentIndex === 1) {
396
+ const subcommands = ['bash', 'zsh'];
397
+ return filterSuggestions(subcommands, args[1] || '');
398
+ }
399
+
400
+ // System subcommands
401
+ if (command === 'system' && currentIndex === 1) {
402
+ const subcommands = ['init', 'config', 'secret', 'vault-password'];
403
+ return filterSuggestions(subcommands, args[1] || '');
404
+ }
405
+
406
+ // System config operations
407
+ if (command === 'system' && args[1] === 'config' && currentIndex === 2) {
408
+ const operations = ['get', 'set'];
409
+ return filterSuggestions(operations, args[2] || '');
410
+ }
411
+
412
+ // System secret operations
413
+ if (command === 'system' && args[1] === 'secret' && currentIndex === 2) {
414
+ const operations = ['get', 'set'];
415
+ return filterSuggestions(operations, args[2] || '');
416
+ }
417
+
418
+ return [];
419
+ }
420
+
421
+ /**
422
+ * Filter suggestions based on current input
423
+ */
424
+ function filterSuggestions(options: string[], current: string): string[] {
425
+ if (!current) return options;
426
+ return options.filter((opt) => opt.startsWith(current));
427
+ }
428
+
429
+ /**
430
+ * Generate bash completion script
431
+ */
432
+ export function generateBashCompletion(): string {
433
+ return `# Celilo bash completion
434
+ _celilo_completion() {
435
+ local cur prev words cword
436
+ _init_completion || return
437
+
438
+ # Get completions from celilo
439
+ local completions=$(celilo --get-completions "\${COMP_WORDS[@]}" "\${COMP_CWORD}")
440
+
441
+ # Apply completions
442
+ COMPREPLY=( $(compgen -W "$completions" -- "$cur") )
443
+ return 0
444
+ }
445
+
446
+ complete -F _celilo_completion celilo
447
+ `;
448
+ }
449
+
450
+ /**
451
+ * Generate zsh completion script
452
+ */
453
+ export function generateZshCompletion(): string {
454
+ return `#compdef celilo
455
+
456
+ _celilo() {
457
+ local -a completions
458
+ local -a words_array
459
+ local current_word
460
+
461
+ # Get current word index (CURRENT is 1-based)
462
+ current_word=\${CURRENT}
463
+
464
+ # Get words from command line
465
+ words_array=("\${words[@]}")
466
+
467
+ # Call celilo to get completions (returns newline-separated list)
468
+ local result
469
+ result=$(celilo --get-completions "\${words_array[@]}" "$current_word" 2>/dev/null)
470
+
471
+ # Split result into array by newlines
472
+ completions=("\${(@f)result}")
473
+
474
+ # Provide completions
475
+ if (( \${#completions} > 0 )); then
476
+ compadd -a completions
477
+ fi
478
+ }
479
+
480
+ compdef _celilo celilo
481
+ `;
482
+ }