@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,133 @@
1
+ /**
2
+ * Service Add Digital Ocean Command
3
+ * Configure a Digital Ocean VPS service
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import type { NetworkZone } from '../../db/schema';
8
+ import {
9
+ addContainerService,
10
+ testConnection as testServiceConnection,
11
+ updateVerificationStatus,
12
+ } from '../../services/container-service';
13
+ import { celiloIntro, celiloOutro, promptPassword, promptText } from '../prompts';
14
+ import type { CommandResult } from '../types';
15
+ import { validateRequired } from '../validators';
16
+
17
+ /**
18
+ * Handle service add digitalocean command
19
+ *
20
+ * @param args - Command arguments (unused for interactive mode)
21
+ * @param flags - Command flags
22
+ */
23
+ export async function handleServiceAddDigitalOcean(
24
+ _args: string[],
25
+ _flags: Record<string, boolean | string> = {},
26
+ ): Promise<CommandResult> {
27
+ try {
28
+ celiloIntro('Add Digital Ocean VPS Service');
29
+
30
+ // Prompt for service configuration
31
+ const name = await promptText({
32
+ message: 'Human-readable name:',
33
+ defaultValue: 'Digital Ocean VPS',
34
+ placeholder: 'Digital Ocean VPS',
35
+ validate: validateRequired('Service name'),
36
+ });
37
+
38
+ // Prompt for zones (multi-select)
39
+ const zones = await p.multiselect<NetworkZone>({
40
+ message: 'Zones this service can provision to:',
41
+ options: [
42
+ { value: 'internal' as NetworkZone, label: 'internal' },
43
+ { value: 'dmz' as NetworkZone, label: 'dmz' },
44
+ { value: 'app' as NetworkZone, label: 'app' },
45
+ { value: 'secure' as NetworkZone, label: 'secure' },
46
+ { value: 'external' as NetworkZone, label: 'external' },
47
+ ],
48
+ required: true,
49
+ initialValues: ['external' as NetworkZone], // Digital Ocean is typically for external-facing services
50
+ });
51
+
52
+ if (p.isCancel(zones)) {
53
+ p.cancel('Operation cancelled');
54
+ return { success: false, error: 'Cancelled by user' };
55
+ }
56
+
57
+ console.log('\nDigital Ocean Configuration');
58
+ console.log('──────────────────────────');
59
+
60
+ const apiToken = await promptPassword({
61
+ message: 'API Token (Personal Access Token):',
62
+ validate: validateRequired('API token'),
63
+ });
64
+
65
+ const defaultRegion = await promptText({
66
+ message: 'Default region:',
67
+ defaultValue: 'nyc3',
68
+ placeholder: 'nyc3',
69
+ validate: validateRequired('Region'),
70
+ });
71
+
72
+ const defaultSize = await promptText({
73
+ message: 'Default droplet size:',
74
+ defaultValue: 's-1vcpu-1gb',
75
+ placeholder: 's-1vcpu-1gb',
76
+ validate: validateRequired('Droplet size'),
77
+ });
78
+
79
+ const defaultImage = await promptText({
80
+ message: 'Default image:',
81
+ defaultValue: 'ubuntu-22-04-x64',
82
+ placeholder: 'ubuntu-22-04-x64',
83
+ validate: validateRequired('Image'),
84
+ });
85
+
86
+ // Store the service first so we can test it
87
+ const service = await addContainerService({
88
+ name,
89
+ providerName: 'digitalocean',
90
+ zones: zones as NetworkZone[],
91
+ apiCredentials: {
92
+ api_token: apiToken,
93
+ },
94
+ providerConfig: {
95
+ default_region: defaultRegion,
96
+ default_size: defaultSize,
97
+ default_image: defaultImage,
98
+ },
99
+ });
100
+
101
+ // Test connection
102
+ console.log('\nTesting connection...');
103
+ const testResult = await testServiceConnection(service);
104
+ await updateVerificationStatus(service.id, testResult);
105
+
106
+ if (!testResult.success) {
107
+ console.log(`✗ Connection test failed: ${testResult.message}`);
108
+ console.log(
109
+ '\nService saved but not verified. The service will not be used until verification succeeds.',
110
+ );
111
+
112
+ celiloOutro(
113
+ `Service '${service.serviceId}' (${name}) added but not verified.\n\nNext steps:\n - Fix the connection issue\n - Re-verify: celilo service verify ${service.serviceId}\n - Check status: celilo service list`,
114
+ );
115
+ } else {
116
+ console.log(`✓ ${testResult.message}`);
117
+
118
+ celiloOutro(
119
+ `Service '${service.serviceId}' (${name}) added and verified successfully!\n\nService ID: ${service.serviceId}\n\nNext steps:\n - Generate module: celilo module generate <module-id>\n - List services: celilo service list`,
120
+ );
121
+ }
122
+
123
+ return {
124
+ success: true,
125
+ message: `Added Digital Ocean service: ${service.id}`,
126
+ };
127
+ } catch (error) {
128
+ return {
129
+ success: false,
130
+ error: `Failed to add Digital Ocean service: ${error instanceof Error ? error.message : String(error)}`,
131
+ };
132
+ }
133
+ }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Service Add Proxmox Command
3
+ * Configure a Proxmox container service
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import {
8
+ buildProxmoxApiUrl,
9
+ buildTemplatePath,
10
+ buildTemplateUrl,
11
+ checkTaskStatus,
12
+ downloadTemplate,
13
+ extractTemplateFilename,
14
+ listAvailableTemplates,
15
+ listNodeStorage,
16
+ } from '../../api-clients/proxmox';
17
+ import type { NetworkZone } from '../../db/schema';
18
+ import {
19
+ addContainerService,
20
+ testConnection as testServiceConnection,
21
+ updateVerificationStatus,
22
+ } from '../../services/container-service';
23
+ import { FuelGauge } from '../fuel-gauge';
24
+ import { celiloIntro, celiloOutro, promptPassword, promptText } from '../prompts';
25
+ import type { CommandResult } from '../types';
26
+ import { validateRequired } from '../validators';
27
+
28
+ /**
29
+ * Handle service add proxmox command
30
+ *
31
+ * @param args - Command arguments (unused for interactive mode)
32
+ * @param flags - Command flags
33
+ */
34
+ export async function handleServiceAddProxmox(
35
+ _args: string[],
36
+ _flags: Record<string, boolean | string> = {},
37
+ ): Promise<CommandResult> {
38
+ try {
39
+ celiloIntro('Add Proxmox Container Service');
40
+
41
+ // Prompt for service configuration
42
+ const name = await promptText({
43
+ message: 'Human-readable name:',
44
+ defaultValue: 'Proxmox Home Lab',
45
+ placeholder: 'Proxmox Home Lab',
46
+ validate: validateRequired('Service name'),
47
+ });
48
+
49
+ // Prompt for zones (multi-select)
50
+ const zones = await p.multiselect<NetworkZone>({
51
+ message: 'Zones this service can provision to:',
52
+ options: [
53
+ { value: 'internal' as NetworkZone, label: 'internal' },
54
+ { value: 'dmz' as NetworkZone, label: 'dmz' },
55
+ { value: 'app' as NetworkZone, label: 'app' },
56
+ { value: 'secure' as NetworkZone, label: 'secure' },
57
+ { value: 'external' as NetworkZone, label: 'external' },
58
+ ],
59
+ required: true,
60
+ initialValues: ['internal', 'dmz', 'app', 'secure'] as NetworkZone[], // Common defaults
61
+ });
62
+
63
+ if (p.isCancel(zones)) {
64
+ p.cancel('Operation cancelled');
65
+ return { success: false, error: 'Cancelled by user' };
66
+ }
67
+
68
+ console.log('\nProxmox Configuration');
69
+ console.log('─────────────────────');
70
+
71
+ const ipAddress = await promptText({
72
+ message: 'Proxmox IP address:',
73
+ placeholder: 'e.g., 192.168.1.100 or proxmox.local',
74
+ validate: validateRequired('IP address'),
75
+ });
76
+
77
+ const port = await promptText({
78
+ message: 'Proxmox API port:',
79
+ defaultValue: '8006',
80
+ placeholder: '8006',
81
+ validate: (value): string | Error | undefined => {
82
+ // validateRequired returns undefined for valid inputs (including undefined with defaults)
83
+ const requiredError = validateRequired('Port')(value);
84
+ if (requiredError !== undefined) return requiredError;
85
+
86
+ // If value is undefined, default will be used - that's valid
87
+ if (value === undefined) return undefined;
88
+
89
+ const portNum = Number(value);
90
+ if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
91
+ return 'Port must be a number between 1 and 65535';
92
+ }
93
+ return undefined;
94
+ },
95
+ });
96
+
97
+ // Build the full API URL
98
+ const apiUrl = buildProxmoxApiUrl(ipAddress, Number(port));
99
+
100
+ const apiTokenId = await promptText({
101
+ message: 'API Token ID:',
102
+ defaultValue: 'root@pam!celilo',
103
+ placeholder: 'root@pam!celilo',
104
+ validate: validateRequired('API token ID'),
105
+ });
106
+
107
+ const apiTokenSecret = await promptPassword({
108
+ message: 'API Token Secret:',
109
+ validate: validateRequired('API token secret'),
110
+ });
111
+
112
+ const targetNode = await promptText({
113
+ message: 'Default target node:',
114
+ defaultValue: 'pve',
115
+ placeholder: 'pve',
116
+ validate: validateRequired('Target node'),
117
+ });
118
+
119
+ const storage = await promptText({
120
+ message: 'Default storage:',
121
+ defaultValue: 'local-lvm',
122
+ placeholder: 'local-lvm',
123
+ validate: validateRequired('Storage'),
124
+ });
125
+
126
+ const ubuntuVersion = await p.select({
127
+ message: 'Ubuntu LTS version for default template:',
128
+ options: [
129
+ { value: '24.04', label: 'Ubuntu 24.04 LTS (Noble Numbat)' },
130
+ { value: '22.04', label: 'Ubuntu 22.04 LTS (Jammy Jellyfish)', hint: 'Recommended' },
131
+ { value: '20.04', label: 'Ubuntu 20.04 LTS (Focal Fossa)' },
132
+ ],
133
+ initialValue: '22.04',
134
+ });
135
+
136
+ if (p.isCancel(ubuntuVersion)) {
137
+ p.cancel('Operation cancelled');
138
+ return { success: false, error: 'Cancelled by user' };
139
+ }
140
+
141
+ // Find storage that supports vztmpl content
142
+ console.log('\nFinding storage for templates...');
143
+ const storageListResult = await listNodeStorage(
144
+ {
145
+ api_url: apiUrl,
146
+ api_token_id: apiTokenId,
147
+ api_token_secret: apiTokenSecret,
148
+ },
149
+ targetNode,
150
+ );
151
+
152
+ let templateStorage = 'local'; // Default fallback
153
+ if (storageListResult.success) {
154
+ // Find first active storage that supports 'vztmpl' content
155
+ const vztmplStorage = storageListResult.data.find(
156
+ (s) => s.active && s.enabled && s.content.includes('vztmpl'),
157
+ );
158
+ if (vztmplStorage) {
159
+ templateStorage = vztmplStorage.storage;
160
+ console.log(`✓ Using storage '${templateStorage}' for templates`);
161
+ } else {
162
+ console.log(`⚠ No storage found with 'vztmpl' support, using '${templateStorage}'`);
163
+ }
164
+ }
165
+
166
+ // Build template path
167
+ const lxcTemplate = buildTemplatePath(templateStorage, ubuntuVersion as string);
168
+ const templateFilename = extractTemplateFilename(lxcTemplate);
169
+
170
+ // Check if template exists
171
+ console.log(`\nChecking if template '${templateFilename}' exists...`);
172
+ const templatesResult = await listAvailableTemplates(
173
+ {
174
+ api_url: apiUrl,
175
+ api_token_id: apiTokenId,
176
+ api_token_secret: apiTokenSecret,
177
+ },
178
+ targetNode,
179
+ templateStorage,
180
+ );
181
+
182
+ let templateExists = false;
183
+ if (templatesResult.success) {
184
+ templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
185
+ }
186
+
187
+ // Save the service FIRST so credentials aren't lost if template download fails
188
+ const service = await addContainerService({
189
+ name,
190
+ providerName: 'proxmox',
191
+ zones: zones as unknown as NetworkZone[],
192
+ apiCredentials: {
193
+ api_url: apiUrl,
194
+ api_token_id: apiTokenId,
195
+ api_token_secret: apiTokenSecret,
196
+ },
197
+ providerConfig: {
198
+ default_target_node: targetNode,
199
+ lxc_template: lxcTemplate,
200
+ storage,
201
+ },
202
+ });
203
+
204
+ console.log(`✓ Service '${service.serviceId}' saved\n`);
205
+
206
+ // Now attempt template download (service is already saved)
207
+ let templateReady = templateExists;
208
+
209
+ if (!templateExists) {
210
+ console.log(`✗ Template '${templateFilename}' not found in storage '${templateStorage}'`);
211
+
212
+ const shouldDownload = await p.confirm({
213
+ message: 'Download template now?',
214
+ initialValue: true,
215
+ });
216
+
217
+ if (p.isCancel(shouldDownload)) {
218
+ p.cancel('Operation cancelled');
219
+ return { success: false, error: 'Cancelled by user' };
220
+ }
221
+
222
+ if (shouldDownload) {
223
+ const templateUrl = buildTemplateUrl(ubuntuVersion as string);
224
+
225
+ const downloadResult = await downloadTemplate(
226
+ {
227
+ api_url: apiUrl,
228
+ api_token_id: apiTokenId,
229
+ api_token_secret: apiTokenSecret,
230
+ },
231
+ targetNode,
232
+ templateStorage,
233
+ templateUrl,
234
+ );
235
+
236
+ if (!downloadResult.success) {
237
+ console.log(`\n✗ Failed to start download: ${downloadResult.message}`);
238
+ } else {
239
+ // Wait for download to complete with fuel-gauge progress
240
+ const upid = downloadResult.data;
241
+ const gauge = new FuelGauge(`Downloading ${templateFilename}`);
242
+ gauge.start();
243
+
244
+ let downloadComplete = false;
245
+ let attempts = 0;
246
+ const maxAttempts = 60;
247
+
248
+ while (!downloadComplete && attempts < maxAttempts) {
249
+ await new Promise((resolve) => setTimeout(resolve, 5000));
250
+
251
+ const statusResult = await checkTaskStatus(
252
+ {
253
+ api_url: apiUrl,
254
+ api_token_id: apiTokenId,
255
+ api_token_secret: apiTokenSecret,
256
+ },
257
+ targetNode,
258
+ upid,
259
+ );
260
+
261
+ if (statusResult.success) {
262
+ if (statusResult.data.status === 'stopped') {
263
+ if (statusResult.data.exitstatus === 'OK') {
264
+ downloadComplete = true;
265
+ templateReady = true;
266
+ gauge.stop(true);
267
+ } else {
268
+ gauge.stop(false);
269
+ console.log(`\n✗ Template download failed: ${statusResult.data.exitstatus}`);
270
+ break;
271
+ }
272
+ } else {
273
+ gauge.addOutput(`Status: ${statusResult.data.status} (${attempts * 5}s elapsed)`);
274
+ }
275
+ } else {
276
+ gauge.addOutput(`Waiting for status... (${attempts * 5}s elapsed)`);
277
+ }
278
+
279
+ attempts++;
280
+ }
281
+
282
+ if (!downloadComplete && !templateReady) {
283
+ gauge.stop(false);
284
+ console.log('\n✗ Template download timed out after 5 minutes');
285
+ }
286
+ }
287
+
288
+ if (!templateReady) {
289
+ console.log(
290
+ '\nTemplate is not available. The service has been saved but needs a template.\n',
291
+ );
292
+ console.log('Next steps:');
293
+ console.log(
294
+ ` 1. Try a different version: celilo service reconfigure ${service.serviceId}`,
295
+ );
296
+ console.log(
297
+ ` 2. Download manually: ssh root@${ipAddress} pveam download ${templateStorage} ${templateFilename}`,
298
+ );
299
+ console.log(` 3. Then verify: celilo service verify ${service.serviceId}`);
300
+ }
301
+ } else {
302
+ console.log(
303
+ '\nTemplate not downloaded. Service saved but will need a template before deployment.',
304
+ );
305
+ }
306
+ } else {
307
+ console.log(`✓ Template '${templateFilename}' found`);
308
+ }
309
+
310
+ // Test connection
311
+ console.log('\nTesting connection...');
312
+ const testResult = await testServiceConnection(service);
313
+ await updateVerificationStatus(service.id, testResult);
314
+
315
+ if (!testResult.success) {
316
+ console.log(`✗ Connection test failed: ${testResult.message}`);
317
+ console.log(
318
+ '\nService saved but not verified. The service will not be used until verification succeeds.',
319
+ );
320
+
321
+ celiloOutro(
322
+ `Service '${service.serviceId}' (${name}) added but not verified.\n\nNext steps:\n - Fix the connection issue\n - Re-verify: celilo service verify ${service.serviceId}\n - Check status: celilo service list`,
323
+ );
324
+ } else {
325
+ console.log(`✓ ${testResult.message}`);
326
+
327
+ celiloOutro(
328
+ `Service '${service.serviceId}' (${name}) added and verified successfully!\n\nService ID: ${service.serviceId}\n\nNext steps:\n - Generate module: celilo module generate <module-id>\n - List services: celilo service list`,
329
+ );
330
+ }
331
+
332
+ return {
333
+ success: true,
334
+ message: `Added Proxmox service: ${service.id}`,
335
+ };
336
+ } catch (error) {
337
+ return {
338
+ success: false,
339
+ error: `Failed to add Proxmox service: ${error instanceof Error ? error.message : String(error)}`,
340
+ };
341
+ }
342
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Service config get command
3
+ */
4
+
5
+ import { getContainerServiceByServiceId } from '../../services/container-service';
6
+ import { getArg, validateRequiredArgs } from '../parser';
7
+ import type { CommandResult } from '../types';
8
+
9
+ /**
10
+ * Handle service config get command
11
+ *
12
+ * Usage: celilo service config get <service-id> [key]
13
+ *
14
+ * @param args - Command arguments
15
+ * @returns Command result
16
+ */
17
+ export async function handleServiceConfigGet(args: string[]): Promise<CommandResult> {
18
+ const error = validateRequiredArgs(args, 1);
19
+ if (error) {
20
+ return {
21
+ success: false,
22
+ error: `${error}\n\nUsage: celilo service config get <service-id> [key]`,
23
+ };
24
+ }
25
+
26
+ const serviceId = getArg(args, 0);
27
+ const key = getArg(args, 1);
28
+
29
+ if (!serviceId) {
30
+ return {
31
+ success: false,
32
+ error: 'Service ID is required',
33
+ };
34
+ }
35
+
36
+ const service = await getContainerServiceByServiceId(serviceId);
37
+ if (!service) {
38
+ return {
39
+ success: false,
40
+ error: `Service not found: ${serviceId}`,
41
+ };
42
+ }
43
+
44
+ // Configurable fields
45
+ const configurableFields: Record<string, unknown> = {
46
+ name: service.name,
47
+ zones: service.zones,
48
+ providerConfig: service.providerConfig,
49
+ };
50
+
51
+ if (key) {
52
+ // Get specific config value
53
+ if (!(key in configurableFields)) {
54
+ const validKeys = Object.keys(configurableFields).join(', ');
55
+ return {
56
+ success: false,
57
+ error: `Invalid config key '${key}' for service.\n\nValid keys: ${validKeys}`,
58
+ };
59
+ }
60
+
61
+ const value = configurableFields[key];
62
+ const formatted = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
63
+
64
+ return {
65
+ success: true,
66
+ message: `${key} = ${formatted}`,
67
+ data: { key, value },
68
+ };
69
+ }
70
+
71
+ // Get all config for service
72
+ const lines = [`Configuration for ${serviceId}:`, ''];
73
+ for (const [configKey, value] of Object.entries(configurableFields)) {
74
+ const formatted = typeof value === 'object' ? JSON.stringify(value) : String(value);
75
+ lines.push(`${configKey} = ${formatted}`);
76
+ }
77
+
78
+ return {
79
+ success: true,
80
+ message: lines.join('\n'),
81
+ data: configurableFields,
82
+ };
83
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Service config set command
3
+ */
4
+
5
+ import { eq } from 'drizzle-orm';
6
+ import { z } from 'zod';
7
+ import { getDb } from '../../db/client';
8
+ import { type NetworkZone, containerServices } from '../../db/schema';
9
+ import { getContainerServiceByServiceId } from '../../services/container-service';
10
+ import { getArg, validateRequiredArgs } from '../parser';
11
+ import type { CommandResult } from '../types';
12
+
13
+ /**
14
+ * Handle service config set command
15
+ *
16
+ * Usage: celilo service config set <service-id> <key> <value>
17
+ *
18
+ * @param args - Command arguments
19
+ * @returns Command result
20
+ */
21
+ export async function handleServiceConfigSet(args: string[]): Promise<CommandResult> {
22
+ const error = validateRequiredArgs(args, 3);
23
+ if (error) {
24
+ return {
25
+ success: false,
26
+ error: `${error}\n\nUsage: celilo service config set <service-id> <key> <value>`,
27
+ };
28
+ }
29
+
30
+ const serviceId = getArg(args, 0);
31
+ const key = getArg(args, 1);
32
+ const valueRaw = getArg(args, 2);
33
+
34
+ if (!serviceId || !key || !valueRaw) {
35
+ return {
36
+ success: false,
37
+ error: 'Service ID, key, and value are required',
38
+ };
39
+ }
40
+
41
+ const service = await getContainerServiceByServiceId(serviceId);
42
+ if (!service) {
43
+ return {
44
+ success: false,
45
+ error: `Service not found: ${serviceId}`,
46
+ };
47
+ }
48
+
49
+ // Validate key
50
+ const validKeys = ['name', 'zones', 'providerConfig'];
51
+ if (!validKeys.includes(key)) {
52
+ return {
53
+ success: false,
54
+ error: `Invalid config key '${key}'.\n\nValid keys: ${validKeys.join(', ')}`,
55
+ };
56
+ }
57
+
58
+ const db = getDb();
59
+ const now = new Date();
60
+
61
+ try {
62
+ // Handle different key types
63
+ if (key === 'name') {
64
+ // Simple string value
65
+ await db
66
+ .update(containerServices)
67
+ .set({
68
+ name: valueRaw,
69
+ updatedAt: now,
70
+ })
71
+ .where(eq(containerServices.id, service.id));
72
+
73
+ return {
74
+ success: true,
75
+ message: `Updated service ${serviceId}: ${key} = ${valueRaw}`,
76
+ };
77
+ }
78
+
79
+ if (key === 'zones') {
80
+ // Parse as JSON array of NetworkZone
81
+ const ZonesSchema = z.array(z.enum(['internal', 'dmz', 'app', 'secure', 'external']));
82
+ const parsed = JSON.parse(valueRaw);
83
+ const zones = ZonesSchema.parse(parsed) as NetworkZone[];
84
+
85
+ await db
86
+ .update(containerServices)
87
+ .set({
88
+ zones, // Drizzle auto-stringifies with mode: 'json'
89
+ updatedAt: now,
90
+ })
91
+ .where(eq(containerServices.id, service.id));
92
+
93
+ return {
94
+ success: true,
95
+ message: `Updated service ${serviceId}: ${key} = ${JSON.stringify(zones)}`,
96
+ };
97
+ }
98
+
99
+ if (key === 'providerConfig') {
100
+ // Parse as JSON object
101
+ const parsed = JSON.parse(valueRaw);
102
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
103
+ return {
104
+ success: false,
105
+ error: 'providerConfig must be a JSON object',
106
+ };
107
+ }
108
+
109
+ await db
110
+ .update(containerServices)
111
+ .set({
112
+ providerConfig: parsed, // Drizzle auto-stringifies with mode: 'json'
113
+ updatedAt: now,
114
+ })
115
+ .where(eq(containerServices.id, service.id));
116
+
117
+ return {
118
+ success: true,
119
+ message: `Updated service ${serviceId}: ${key} = ${JSON.stringify(parsed)}`,
120
+ };
121
+ }
122
+
123
+ return {
124
+ success: false,
125
+ error: `Config key '${key}' is not implemented`,
126
+ };
127
+ } catch (error) {
128
+ if (error instanceof SyntaxError) {
129
+ return {
130
+ success: false,
131
+ error: `Invalid JSON value: ${error.message}`,
132
+ };
133
+ }
134
+ if (error instanceof z.ZodError) {
135
+ return {
136
+ success: false,
137
+ error: `Validation failed: ${error.errors.map((e) => e.message).join(', ')}`,
138
+ };
139
+ }
140
+ return {
141
+ success: false,
142
+ error: `Failed to set config: ${error instanceof Error ? error.message : String(error)}`,
143
+ };
144
+ }
145
+ }