@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,235 @@
1
+ /**
2
+ * Machine Add Command
3
+ * Add a machine to the machine pool with auto-detection
4
+ */
5
+
6
+ import { existsSync } from 'node:fs';
7
+ import { readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { getDb } from '../../db/client';
10
+ import {
11
+ detectMachineInfo,
12
+ detectNetworkInterfaces,
13
+ testSshConnection,
14
+ } from '../../services/machine-detector';
15
+ import { addMachine, getMachineByIp } from '../../services/machine-pool';
16
+ import { loadExistingConfiguration } from '../../services/system-init';
17
+ import { detectZoneFromIp } from '../../services/zone-detector';
18
+ import { celiloIntro, celiloOutro, promptText } from '../prompts';
19
+ import type { CommandResult } from '../types';
20
+ import { validateIpAddress, validateRequired } from '../validators';
21
+
22
+ /**
23
+ * Auto-detect SSH private key from system configuration
24
+ *
25
+ * Reads ssh.public_key from system config and finds matching private key in ~/.ssh/
26
+ *
27
+ * @returns Path to SSH private key, or null if not found
28
+ */
29
+ function findSshPrivateKey(): string | null {
30
+ try {
31
+ const db = getDb();
32
+ const config = loadExistingConfiguration(db);
33
+ const publicKey = config['ssh.public_key'];
34
+
35
+ if (!publicKey) {
36
+ return null;
37
+ }
38
+
39
+ // Extract key type from public key (e.g., "ssh-ed25519", "ssh-rsa")
40
+ const keyType = publicKey.split(' ')[0];
41
+
42
+ const sshDir = join(process.env.HOME || '~', '.ssh');
43
+ if (!existsSync(sshDir)) {
44
+ return null;
45
+ }
46
+
47
+ // Common private key filenames based on type
48
+ const keyFileMap: Record<string, string[]> = {
49
+ 'ssh-ed25519': ['id_ed25519'],
50
+ 'ssh-rsa': ['id_rsa'],
51
+ 'ecdsa-sha2-nistp256': ['id_ecdsa'],
52
+ 'ecdsa-sha2-nistp384': ['id_ecdsa'],
53
+ 'ecdsa-sha2-nistp521': ['id_ecdsa'],
54
+ };
55
+
56
+ const candidateFiles = keyFileMap[keyType] || ['id_rsa', 'id_ed25519', 'id_ecdsa'];
57
+
58
+ // Try common key files
59
+ for (const keyFile of candidateFiles) {
60
+ const keyPath = join(sshDir, keyFile);
61
+ if (existsSync(keyPath)) {
62
+ // Verify this is the matching private key by checking if public key exists
63
+ const pubKeyPath = `${keyPath}.pub`;
64
+ if (existsSync(pubKeyPath)) {
65
+ const pubKeyContent = readFileSync(pubKeyPath, 'utf8').trim();
66
+ // Check if public keys match (compare key type and key data, ignore comment)
67
+ const [sysType, sysKey] = publicKey.split(' ');
68
+ const [fileType, fileKey] = pubKeyContent.split(' ');
69
+ if (sysType === fileType && sysKey === fileKey) {
70
+ return keyPath;
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ return null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Handle machine add command
84
+ *
85
+ * @param args - Command arguments (unused for interactive mode)
86
+ * @param flags - Command flags (--ip, --ssh-user, --ssh-key-file for non-interactive)
87
+ */
88
+ export async function handleMachineAdd(
89
+ args: string[],
90
+ flags: Record<string, boolean | string> = {},
91
+ ): Promise<CommandResult> {
92
+ try {
93
+ celiloIntro('Add Machine to Pool');
94
+
95
+ // Hybrid mode: use flags for what's provided, prompt for what's missing
96
+ let ipAddress: string;
97
+ let sshUser: string;
98
+ let sshKeyPath: string;
99
+
100
+ // IP address: from positional arg, --ip flag, or prompt
101
+ if (args[0] && /^\d+\.\d+\.\d+\.\d+$/.test(args[0])) {
102
+ ipAddress = args[0];
103
+ } else if (typeof flags.ip === 'string') {
104
+ ipAddress = flags.ip;
105
+ } else {
106
+ ipAddress = await promptText({
107
+ message: 'Machine IP address:',
108
+ validate: validateIpAddress,
109
+ });
110
+ }
111
+
112
+ // Check for duplicate IP
113
+ const existing = await getMachineByIp(ipAddress);
114
+ if (existing) {
115
+ return {
116
+ success: false,
117
+ error: `Machine with IP ${ipAddress} already exists (hostname: ${existing.hostname}, zone: ${existing.zone})`,
118
+ };
119
+ }
120
+
121
+ // SSH user: from flag, default to 'root', or prompt
122
+ if (typeof flags['ssh-user'] === 'string') {
123
+ sshUser = flags['ssh-user'];
124
+ } else {
125
+ sshUser = await promptText({
126
+ message: 'SSH username:',
127
+ defaultValue: 'root',
128
+ placeholder: 'root',
129
+ validate: validateRequired('SSH username'),
130
+ });
131
+ }
132
+
133
+ // SSH key: from flag, auto-detect, or error
134
+ if (typeof flags['ssh-key-file'] === 'string') {
135
+ sshKeyPath = flags['ssh-key-file'];
136
+ const expandedPath = sshKeyPath.replace(/^~/, process.env.HOME || '~');
137
+ if (!existsSync(expandedPath)) {
138
+ return { success: false, error: `SSH key file not found: ${expandedPath}` };
139
+ }
140
+ } else {
141
+ // Auto-detect SSH key from system config
142
+ const detectedKeyPath = findSshPrivateKey();
143
+ if (!detectedKeyPath) {
144
+ return {
145
+ success: false,
146
+ error:
147
+ 'Cannot find SSH private key.\n\n' +
148
+ 'The ssh.public_key system config is set, but no matching private key was found in ~/.ssh/\n\n' +
149
+ 'Specify manually with: --ssh-key-file ~/.ssh/id_ed25519',
150
+ };
151
+ }
152
+ sshKeyPath = detectedKeyPath;
153
+ console.log(`Using SSH key: ${sshKeyPath}`);
154
+ }
155
+
156
+ // Expand tilde in path
157
+ const expandedKeyPath = sshKeyPath.replace(/^~/, process.env.HOME || '~');
158
+
159
+ // Read SSH key content
160
+ const sshKey = readFileSync(expandedKeyPath, 'utf8');
161
+
162
+ console.log('\nTesting SSH connection...');
163
+
164
+ // Test SSH connectivity
165
+ const canConnect = await testSshConnection(ipAddress, sshUser, expandedKeyPath);
166
+ if (!canConnect) {
167
+ return {
168
+ success: false,
169
+ error: `Cannot connect to ${sshUser}@${ipAddress} with provided SSH key`,
170
+ };
171
+ }
172
+
173
+ console.log('✓ SSH connection successful\n');
174
+
175
+ console.log('Detecting machine information...');
176
+
177
+ // Auto-detect machine info
178
+ const detectedInfo = await detectMachineInfo(ipAddress, sshUser, expandedKeyPath);
179
+
180
+ console.log('✓ Machine detected:');
181
+ console.log(` Hostname: ${detectedInfo.hostname}`);
182
+ console.log(` OS: ${detectedInfo.osInfo}`);
183
+ console.log(
184
+ ` CPU: ${detectedInfo.hardware.cpu_cores} cores (${detectedInfo.hardware.arch || 'unknown'})`,
185
+ );
186
+ console.log(` Memory: ${detectedInfo.hardware.memory_mb} MB`);
187
+ console.log(` Disk: ${detectedInfo.hardware.disk_gb} GB\n`);
188
+
189
+ // Auto-detect zone from IP
190
+ console.log('Detecting network zone...');
191
+ const zone = await detectZoneFromIp(ipAddress);
192
+ console.log(`✓ Zone: ${zone}\n`);
193
+
194
+ // Detect network interfaces and classify machine
195
+ console.log('Detecting network interfaces...');
196
+ const { interfaces, role } = await detectNetworkInterfaces(ipAddress, sshUser, expandedKeyPath);
197
+
198
+ console.log(`✓ Role: ${role}`);
199
+ for (const iface of interfaces) {
200
+ console.log(` ${iface.name}: ${iface.ipAddress} (${iface.zone})`);
201
+ }
202
+ console.log('');
203
+
204
+ // Add machine to pool
205
+ const earmark = typeof flags.earmark === 'string' ? flags.earmark : undefined;
206
+ const machine = await addMachine({
207
+ hostname: detectedInfo.hostname,
208
+ zone,
209
+ ipAddress,
210
+ sshUser,
211
+ sshKey,
212
+ hardware: detectedInfo.hardware,
213
+ role,
214
+ interfaces,
215
+ assignedModuleIds: [],
216
+ earmarkedModule: earmark || null,
217
+ });
218
+
219
+ const earmarkNote = earmark ? `\n Earmarked for: ${earmark}` : '';
220
+ const roleNote = role === 'router' ? ` (router - ${interfaces.length} interfaces)` : '';
221
+ celiloOutro(
222
+ `Machine '${detectedInfo.hostname}' added successfully!\n\nDetails:\n Zone: ${zone}${roleNote}\n IP: ${ipAddress}\n Hardware: ${detectedInfo.hardware.cpu_cores} cores, ${detectedInfo.hardware.memory_mb} MB RAM, ${detectedInfo.hardware.disk_gb} GB disk${earmarkNote}\n\nNext steps:\n - List machines: celilo machine list\n - Check status: celilo machine status ${detectedInfo.hostname}`,
223
+ );
224
+
225
+ return {
226
+ success: true,
227
+ message: `Added machine: ${machine.id}`,
228
+ };
229
+ } catch (error) {
230
+ return {
231
+ success: false,
232
+ error: `Failed to add machine: ${error instanceof Error ? error.message : String(error)}`,
233
+ };
234
+ }
235
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Machine Earmark Command
3
+ * Earmark a machine for a specific module, or clear an earmark
4
+ */
5
+
6
+ import {
7
+ getMachineByHostname,
8
+ getMachineByIp,
9
+ updateMachineEarmark,
10
+ } from '../../services/machine-pool';
11
+ import { celiloIntro, celiloOutro } from '../prompts';
12
+ import type { CommandResult } from '../types';
13
+
14
+ /**
15
+ * Handle machine earmark command
16
+ *
17
+ * @param args - [hostname|ip, module-id] or [hostname|ip, --clear]
18
+ * @param flags - --clear to remove earmark
19
+ */
20
+ export async function handleMachineEarmark(
21
+ args: string[],
22
+ flags: Record<string, boolean | string> = {},
23
+ ): Promise<CommandResult> {
24
+ try {
25
+ celiloIntro('Earmark Machine');
26
+
27
+ const identifier = args[0];
28
+ if (!identifier) {
29
+ return {
30
+ success: false,
31
+ error:
32
+ 'Machine hostname or IP is required\n\nUsage:\n celilo machine earmark <hostname|ip> <module-id>\n celilo machine earmark <hostname|ip> --clear',
33
+ };
34
+ }
35
+
36
+ // Look up by IP first, then by hostname
37
+ const isIp = /^\d+\.\d+\.\d+\.\d+$/.test(identifier);
38
+ const machine = isIp
39
+ ? await getMachineByIp(identifier)
40
+ : await getMachineByHostname(identifier);
41
+
42
+ if (!machine) {
43
+ return {
44
+ success: false,
45
+ error: `Machine not found: ${identifier}`,
46
+ };
47
+ }
48
+
49
+ // Clear earmark
50
+ if (flags.clear) {
51
+ await updateMachineEarmark(machine.id, null);
52
+ celiloOutro(`Cleared earmark on '${machine.hostname}' (${machine.ipAddress})`);
53
+ return {
54
+ success: true,
55
+ message: `Cleared earmark on ${machine.hostname}`,
56
+ };
57
+ }
58
+
59
+ // Set earmark
60
+ const moduleId = args[1];
61
+ if (!moduleId) {
62
+ return {
63
+ success: false,
64
+ error:
65
+ 'Module ID is required\n\nUsage:\n celilo machine earmark <hostname|ip> <module-id>\n celilo machine earmark <hostname|ip> --clear',
66
+ };
67
+ }
68
+
69
+ await updateMachineEarmark(machine.id, moduleId);
70
+ celiloOutro(`Earmarked '${machine.hostname}' (${machine.ipAddress}) for module '${moduleId}'`);
71
+
72
+ return {
73
+ success: true,
74
+ message: `Earmarked ${machine.hostname} for ${moduleId}`,
75
+ };
76
+ } catch (error) {
77
+ return {
78
+ success: false,
79
+ error: `Failed to earmark machine: ${error instanceof Error ? error.message : String(error)}`,
80
+ };
81
+ }
82
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Machine List Command
3
+ * List all machines in the machine pool
4
+ */
5
+
6
+ import type { NetworkZone } from '../../db/schema';
7
+ import { type MachineFilters, listMachines } from '../../services/machine-pool';
8
+ import { celiloIntro } from '../prompts';
9
+ import type { CommandResult } from '../types';
10
+
11
+ /**
12
+ * Handle machine list command
13
+ *
14
+ * @param args - Command arguments (unused)
15
+ * @param flags - Command flags (--zone filter)
16
+ */
17
+ export async function handleMachineList(
18
+ _args: string[],
19
+ flags: Record<string, boolean | string> = {},
20
+ ): Promise<CommandResult> {
21
+ try {
22
+ celiloIntro('Machine Pool');
23
+
24
+ // Apply zone filter if provided
25
+ const filters: MachineFilters = {};
26
+ if (flags.zone && typeof flags.zone === 'string') {
27
+ filters.zone = flags.zone as NetworkZone;
28
+ }
29
+
30
+ const machines = await listMachines(filters);
31
+
32
+ if (machines.length === 0) {
33
+ console.log('No machines in pool.\n');
34
+ console.log('Add a machine:');
35
+ console.log(' celilo machine add');
36
+ return { success: true, message: 'No machines found' };
37
+ }
38
+
39
+ console.log('');
40
+ for (const machine of machines) {
41
+ const assignedCount = machine.assignedModuleIds.length;
42
+ const assignedText =
43
+ assignedCount === 0 ? 'None (available)' : machine.assignedModuleIds.join(', ');
44
+
45
+ const roleLabel = machine.role === 'router' ? ' [router]' : '';
46
+ console.log(`${machine.hostname} (${machine.zone})${roleLabel}`);
47
+ console.log(` IP: ${machine.ipAddress}`);
48
+ console.log(` SSH: ${machine.sshUser}@${machine.ipAddress}`);
49
+ if (machine.role === 'router' && machine.interfaces.length > 1) {
50
+ console.log(' Interfaces:');
51
+ for (const iface of machine.interfaces) {
52
+ console.log(` ${iface.name}: ${iface.ipAddress} (${iface.zone})`);
53
+ }
54
+ }
55
+ console.log(
56
+ ` Hardware: ${machine.hardware.cpu_cores} cores, ${machine.hardware.memory_mb} MB RAM, ${machine.hardware.disk_gb} GB disk`,
57
+ );
58
+ console.log(` Assigned: ${assignedText}`);
59
+ if (machine.earmarkedModule) {
60
+ console.log(` Earmarked: ${machine.earmarkedModule}`);
61
+ }
62
+ console.log('');
63
+ }
64
+
65
+ console.log(`Total: ${machines.length} machine${machines.length === 1 ? '' : 's'}\n`);
66
+
67
+ return {
68
+ success: true,
69
+ message: `Found ${machines.length} machine(s)`,
70
+ };
71
+ } catch (error) {
72
+ return {
73
+ success: false,
74
+ error: `Failed to list machines: ${error instanceof Error ? error.message : String(error)}`,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Machine Remove Command
3
+ * Remove a machine from the machine pool
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import { getMachineByHostname, getMachineByIp, removeMachine } from '../../services/machine-pool';
8
+ import { celiloIntro, celiloOutro } from '../prompts';
9
+ import type { CommandResult } from '../types';
10
+
11
+ /**
12
+ * Handle machine remove command
13
+ *
14
+ * @param args - Command arguments [hostname]
15
+ * @param flags - Command flags (--force)
16
+ */
17
+ export async function handleMachineRemove(
18
+ args: string[],
19
+ flags: Record<string, boolean | string> = {},
20
+ ): Promise<CommandResult> {
21
+ try {
22
+ celiloIntro('Remove Machine');
23
+
24
+ // Get identifier from args (hostname or IP)
25
+ const identifier = args[0];
26
+ if (!identifier) {
27
+ return {
28
+ success: false,
29
+ error: 'Hostname or IP address is required\n\nUsage: celilo machine remove <hostname|ip>',
30
+ };
31
+ }
32
+
33
+ // Look up by IP first, then by hostname
34
+ const isIp = /^\d+\.\d+\.\d+\.\d+$/.test(identifier);
35
+ const machine = isIp
36
+ ? await getMachineByIp(identifier)
37
+ : await getMachineByHostname(identifier);
38
+ if (!machine) {
39
+ return {
40
+ success: false,
41
+ error: `Machine not found: ${identifier}`,
42
+ };
43
+ }
44
+ const hostname = machine.hostname;
45
+
46
+ // Check for assigned modules
47
+ if (machine.assignedModuleIds.length > 0) {
48
+ console.log(
49
+ `\nError: Machine '${hostname}' has ${machine.assignedModuleIds.length} assigned module(s):`,
50
+ );
51
+ for (const moduleId of machine.assignedModuleIds) {
52
+ console.log(` - ${moduleId}`);
53
+ }
54
+ console.log('\nModules must be unassigned or shut down before removing the machine.\n');
55
+
56
+ return {
57
+ success: false,
58
+ error: 'Cannot remove machine with assigned modules',
59
+ };
60
+ }
61
+
62
+ // Confirm deletion
63
+ if (!flags.force) {
64
+ const confirmed = await p.confirm({
65
+ message: `Remove machine '${hostname}' (${machine.ipAddress})?`,
66
+ initialValue: false,
67
+ });
68
+
69
+ if (p.isCancel(confirmed) || !confirmed) {
70
+ p.cancel('Operation cancelled');
71
+ return { success: false, error: 'Cancelled by user' };
72
+ }
73
+ }
74
+
75
+ // Remove the machine
76
+ await removeMachine(machine.id);
77
+
78
+ celiloOutro(`Machine '${hostname}' removed successfully!`);
79
+
80
+ return {
81
+ success: true,
82
+ message: `Removed machine: ${hostname}`,
83
+ };
84
+ } catch (error) {
85
+ return {
86
+ success: false,
87
+ error: `Failed to remove machine: ${error instanceof Error ? error.message : String(error)}`,
88
+ };
89
+ }
90
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Machine Status Command
3
+ * Show detailed machine status with live connectivity and resource usage
4
+ */
5
+
6
+ import { detectMachineInfo, testSshConnection } from '../../services/machine-detector';
7
+ import { getMachineByHostname, getModuleResourcesOnMachine } from '../../services/machine-pool';
8
+ import { ManagedSshKey } from '../../services/ssh-key-manager';
9
+ import { celiloIntro } from '../prompts';
10
+ import type { CommandResult } from '../types';
11
+
12
+ /**
13
+ * Handle machine status command
14
+ *
15
+ * @param args - Command arguments [hostname]
16
+ * @param flags - Command flags
17
+ */
18
+ export async function handleMachineStatus(
19
+ args: string[],
20
+ _flags: Record<string, boolean | string> = {},
21
+ ): Promise<CommandResult> {
22
+ try {
23
+ celiloIntro('Machine Status');
24
+
25
+ // Get hostname from args
26
+ const hostname = args[0];
27
+ if (!hostname) {
28
+ return {
29
+ success: false,
30
+ error: 'Hostname is required\n\nUsage: celilo machine status <hostname>',
31
+ };
32
+ }
33
+
34
+ // Verify machine exists
35
+ const machine = await getMachineByHostname(hostname);
36
+ if (!machine) {
37
+ return {
38
+ success: false,
39
+ error: `Machine not found: ${hostname}`,
40
+ };
41
+ }
42
+
43
+ console.log('\nMachine Information');
44
+ console.log('─────────────────');
45
+ console.log(`Hostname: ${machine.hostname}`);
46
+ console.log(`Zone: ${machine.zone}`);
47
+ console.log(`IP: ${machine.ipAddress}`);
48
+ console.log(`SSH User: ${machine.sshUser}`);
49
+ console.log('');
50
+
51
+ console.log('Hardware Specifications');
52
+ console.log('──────────────────────');
53
+ console.log(`CPU: ${machine.hardware.cpu_cores} cores`);
54
+ console.log(`Memory: ${machine.hardware.memory_mb} MB`);
55
+ console.log(`Disk: ${machine.hardware.disk_gb} GB`);
56
+ console.log('');
57
+
58
+ console.log('Assigned Modules');
59
+ console.log('───────────────');
60
+ if (machine.assignedModuleIds.length === 0) {
61
+ console.log('None (available)');
62
+ } else {
63
+ for (const moduleId of machine.assignedModuleIds) {
64
+ console.log(` - ${moduleId}`);
65
+ }
66
+
67
+ // Show resource allocation
68
+ const allocated = await getModuleResourcesOnMachine(machine.id);
69
+ console.log('');
70
+ console.log('Resource Allocation');
71
+ console.log('──────────────────');
72
+ console.log(`CPU: ${allocated.cpu} / ${machine.hardware.cpu_cores} cores`);
73
+ console.log(`Memory: ${allocated.memory} / ${machine.hardware.memory_mb} MB`);
74
+ console.log(`Disk: ${allocated.disk} / ${machine.hardware.disk_gb} GB`);
75
+ }
76
+ console.log('');
77
+
78
+ console.log('Connectivity Status');
79
+ console.log('──────────────────');
80
+ console.log('Testing SSH connection...');
81
+
82
+ // Get SSH key and test connectivity
83
+ const managedKey = new ManagedSshKey(machine.id);
84
+ try {
85
+ await managedKey.use(async (keyPath) => {
86
+ const canConnect = await testSshConnection(machine.ipAddress, machine.sshUser, keyPath);
87
+
88
+ if (canConnect) {
89
+ console.log('✓ SSH connection: OK');
90
+
91
+ // Try to get live hardware info
92
+ console.log('\nQuerying current resource usage...');
93
+ try {
94
+ const liveInfo = await detectMachineInfo(machine.ipAddress, machine.sshUser, keyPath);
95
+ console.log('✓ Live hardware info retrieved:');
96
+ console.log(` CPU: ${liveInfo.hardware.cpu_cores} cores`);
97
+ console.log(` Memory: ${liveInfo.hardware.memory_mb} MB`);
98
+ console.log(` Disk: ${liveInfo.hardware.disk_gb} GB`);
99
+ console.log(` OS: ${liveInfo.osInfo}`);
100
+ } catch (error) {
101
+ console.log('✗ Could not retrieve live hardware info');
102
+ if (error instanceof Error) {
103
+ console.log(` Error: ${error.message}`);
104
+ }
105
+ }
106
+ } else {
107
+ console.log('✗ SSH connection: FAILED');
108
+ console.log(' Machine may be offline or SSH key may have changed');
109
+ }
110
+ });
111
+ } catch (error) {
112
+ console.log('✗ SSH connection: ERROR');
113
+ if (error instanceof Error) {
114
+ console.log(` Error: ${error.message}`);
115
+ }
116
+ }
117
+
118
+ console.log('');
119
+ console.log(`Last updated: ${machine.updatedAt.toISOString()}\n`);
120
+
121
+ return {
122
+ success: true,
123
+ message: `Retrieved status for machine: ${hostname}`,
124
+ };
125
+ } catch (error) {
126
+ return {
127
+ success: false,
128
+ error: `Failed to get machine status: ${error instanceof Error ? error.message : String(error)}`,
129
+ };
130
+ }
131
+ }
@@ -0,0 +1,51 @@
1
+ import { auditModule } from '../../module/packaging/audit';
2
+ import type { CommandResult } from '../types';
3
+
4
+ /**
5
+ * Audit module integrity
6
+ *
7
+ * Usage: celilo module audit <module-id>
8
+ *
9
+ * Returns a CommandResult so the dispatcher controls process exit
10
+ * behavior. An earlier implementation called `process.exit()` directly,
11
+ * which killed the persistent CLI process used by `CLIContext` in
12
+ * integration tests.
13
+ */
14
+ export async function moduleAudit(args: string[]): Promise<CommandResult> {
15
+ if (args.length === 0) {
16
+ return {
17
+ success: false,
18
+ error: 'Module ID is required\n\nUsage: celilo module audit <module-id>',
19
+ };
20
+ }
21
+
22
+ const moduleId = args[0];
23
+
24
+ const result = await auditModule(moduleId);
25
+
26
+ if (result.error) {
27
+ return { success: false, error: result.error };
28
+ }
29
+
30
+ if (result.success) {
31
+ return {
32
+ success: true,
33
+ message: `Module '${moduleId}' passed integrity check\n No violations found.`,
34
+ };
35
+ }
36
+
37
+ // Build a multi-line failure message that includes every violation.
38
+ const violationLines = result.violations.map((v) => {
39
+ const icon = v.type === 'missing' ? '⚠' : v.type === 'modified' ? '✗' : '!';
40
+ return ` ${icon} [${v.type.toUpperCase()}] ${v.message}`;
41
+ });
42
+
43
+ return {
44
+ success: false,
45
+ error: [
46
+ `Module '${moduleId}' failed integrity check`,
47
+ ` Found ${result.violations.length} violation(s):`,
48
+ ...violationLines,
49
+ ].join('\n'),
50
+ };
51
+ }