@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,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
|
+
}
|