@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.
- package/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- 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
|
+
}
|