@celilo/cli 0.5.0-alpha.7 → 0.5.0-alpha.9
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/package.json +2 -2
- package/src/api-clients/proxmox.test.ts +78 -0
- package/src/api-clients/proxmox.ts +96 -1
- package/src/cli/command-registry.ts +32 -3
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/events.ts +8 -3
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.ts +10 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +275 -260
- package/src/cli/commands/service-reconfigure.ts +171 -153
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/completion.ts +15 -0
- package/src/cli/index.ts +25 -0
- package/src/cli/service-credential.ts +54 -0
- package/src/services/bus-interview.ts +232 -0
- package/src/services/deploy-validation.test.ts +52 -2
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/fleet-checks.test.ts +13 -0
- package/src/services/fleet-checks.ts +15 -0
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +30 -0
- package/src/templates/generator.ts +86 -31
|
@@ -13,7 +13,6 @@ import { cpSync, existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
13
13
|
import { unlink } from 'node:fs/promises';
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join, resolve } from 'node:path';
|
|
16
|
-
import * as p from '@clack/prompts';
|
|
17
16
|
import { eq } from 'drizzle-orm';
|
|
18
17
|
import { parse as parseYaml } from 'yaml';
|
|
19
18
|
import { registerModuleCapabilities } from '../../capabilities/registration';
|
|
@@ -23,6 +22,7 @@ import { ModuleManifestSchema } from '../../manifest/schema';
|
|
|
23
22
|
import type { ModuleManifest } from '../../manifest/schema';
|
|
24
23
|
import { cleanupTempDir, extractPackage } from '../../module/packaging/extract';
|
|
25
24
|
import { RegistryClient } from '../../registry/client';
|
|
25
|
+
import { askConfirm, withInterviewSession } from '../../services/bus-interview';
|
|
26
26
|
import { getFlag } from '../parser';
|
|
27
27
|
import { log } from '../prompts';
|
|
28
28
|
import type { CommandResult } from '../types';
|
|
@@ -508,11 +508,15 @@ async function runRegistrySweep(
|
|
|
508
508
|
log.message('Each breaking update will be applied only on explicit confirmation.\n');
|
|
509
509
|
|
|
510
510
|
for (const plan of breaking) {
|
|
511
|
-
const proceed = await
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
511
|
+
const proceed = await withInterviewSession(() =>
|
|
512
|
+
askConfirm({
|
|
513
|
+
scope: `module-upgrade:${plan.moduleId}`,
|
|
514
|
+
key: 'apply_breaking',
|
|
515
|
+
message: `Apply breaking update for ${plan.moduleId} (${plan.installedVersion} → ${plan.targetVersion})?`,
|
|
516
|
+
defaultValue: false,
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
if (!proceed) {
|
|
516
520
|
skippedBreaking++;
|
|
517
521
|
continue;
|
|
518
522
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celilo proxmox node list` — live per-node capacity from the Proxmox cluster
|
|
3
|
+
* (ISS-0060, Phase 1 of v2/PROXMOX_CAPACITY_AND_LIFECYCLE.md).
|
|
4
|
+
*
|
|
5
|
+
* Reads reality from the Proxmox API via `ProxmoxClient`, never a cached DB
|
|
6
|
+
* value — the foundation for capacity-aware placement (ISS-0061) and
|
|
7
|
+
* reconcile-on-read (the rest of ISS-0060 / ISS-0090).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ProxmoxClient, type ProxmoxCredentials } from '../../api-clients/proxmox';
|
|
11
|
+
import { getServiceCredentials, listContainerServices } from '../../services/container-service';
|
|
12
|
+
import { celiloIntro } from '../prompts';
|
|
13
|
+
import type { CommandResult } from '../types';
|
|
14
|
+
|
|
15
|
+
function formatUptime(sec: number): string {
|
|
16
|
+
if (sec <= 0) return '—';
|
|
17
|
+
const days = Math.floor(sec / 86400);
|
|
18
|
+
const hours = Math.floor((sec % 86400) / 3600);
|
|
19
|
+
return days > 0 ? `${days}d${hours}h` : `${hours}h`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve which Proxmox service to introspect: an explicit service-id arg, else
|
|
24
|
+
* the sole Proxmox service. Returns an error message when the choice is
|
|
25
|
+
* ambiguous or the named service doesn't exist.
|
|
26
|
+
*/
|
|
27
|
+
function resolveProxmoxService(
|
|
28
|
+
services: Awaited<ReturnType<typeof listContainerServices>>,
|
|
29
|
+
requested: string | undefined,
|
|
30
|
+
): { service: (typeof services)[number] } | { error: string } {
|
|
31
|
+
const proxmox = services.filter((s) => s.providerName === 'proxmox');
|
|
32
|
+
if (proxmox.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
error: 'No Proxmox container service configured. Add one: celilo service add proxmox',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (requested) {
|
|
38
|
+
const match = proxmox.find((s) => s.serviceId === requested);
|
|
39
|
+
if (!match) {
|
|
40
|
+
return {
|
|
41
|
+
error: `No Proxmox service '${requested}'. Known: ${proxmox.map((s) => s.serviceId).join(', ')}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { service: match };
|
|
45
|
+
}
|
|
46
|
+
if (proxmox.length > 1) {
|
|
47
|
+
return {
|
|
48
|
+
error: `Multiple Proxmox services — specify one: celilo proxmox node list <service-id>\n ${proxmox
|
|
49
|
+
.map((s) => s.serviceId)
|
|
50
|
+
.join('\n ')}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { service: proxmox[0] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function handleProxmoxNodeList(
|
|
57
|
+
args: string[],
|
|
58
|
+
_flags: Record<string, boolean | string> = {},
|
|
59
|
+
): Promise<CommandResult> {
|
|
60
|
+
celiloIntro('Proxmox nodes');
|
|
61
|
+
|
|
62
|
+
const resolved = resolveProxmoxService(await listContainerServices(), args[0]);
|
|
63
|
+
if ('error' in resolved) {
|
|
64
|
+
console.log(`✗ ${resolved.error}`);
|
|
65
|
+
return { success: false, error: resolved.error };
|
|
66
|
+
}
|
|
67
|
+
const { service } = resolved;
|
|
68
|
+
|
|
69
|
+
const creds = (await getServiceCredentials(service.id)) as ProxmoxCredentials;
|
|
70
|
+
const result = await new ProxmoxClient(creds).nodeCapacities();
|
|
71
|
+
if (!result.success) {
|
|
72
|
+
console.log(`✗ Could not reach Proxmox (${service.serviceId}): ${result.message}`);
|
|
73
|
+
return { success: false, error: result.message };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (result.data.length === 0) {
|
|
77
|
+
console.log('No nodes reported by the cluster.');
|
|
78
|
+
return { success: true, message: 'No nodes' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`Service: ${service.serviceId} (${service.name})\n`);
|
|
82
|
+
console.log(
|
|
83
|
+
'NODE STATUS RAM free/total CPU DISK free/total UPTIME',
|
|
84
|
+
);
|
|
85
|
+
console.log(
|
|
86
|
+
'─────────────────────────────────────────────────────────────────────────────────────',
|
|
87
|
+
);
|
|
88
|
+
for (const n of result.data) {
|
|
89
|
+
const status = n.online ? 'online' : 'OFFLINE';
|
|
90
|
+
const ram = `${(n.memFreeMb / 1024).toFixed(1)}/${(n.memTotalMb / 1024).toFixed(1)} GB`;
|
|
91
|
+
const cpu = `${n.cpuCores} cores ${n.cpuUsedPct}%`;
|
|
92
|
+
const disk = `${n.diskFreeGb}/${n.diskTotalGb} GB`;
|
|
93
|
+
console.log(
|
|
94
|
+
`${n.node.padEnd(11)} ${status.padEnd(9)} ${ram.padEnd(19)} ${cpu.padEnd(15)} ${disk.padEnd(19)} ${formatUptime(
|
|
95
|
+
n.uptimeSec,
|
|
96
|
+
)}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
console.log('');
|
|
100
|
+
return { success: true, message: `${result.data.length} node(s)` };
|
|
101
|
+
}
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* means the canonical filename always matches what `pveam download` accepts.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import * as p from '@clack/prompts';
|
|
14
13
|
import {
|
|
15
14
|
type ProxmoxAppliance,
|
|
16
15
|
type ProxmoxCredentials,
|
|
@@ -18,9 +17,8 @@ import {
|
|
|
18
17
|
downloadAppliance,
|
|
19
18
|
listAvailableAppliances,
|
|
20
19
|
} from '../../api-clients/proxmox';
|
|
20
|
+
import { askSelect, askText } from '../../services/bus-interview';
|
|
21
21
|
import { FuelGauge } from '../fuel-gauge';
|
|
22
|
-
import { promptText } from '../prompts';
|
|
23
|
-
import { validateRequired } from '../validators';
|
|
24
22
|
|
|
25
23
|
export interface UbuntuTemplateChoice {
|
|
26
24
|
/** Canonical filename, e.g. "ubuntu-24.04-standard_24.04-2_amd64.tar.zst" */
|
|
@@ -103,7 +101,7 @@ export function pickInitialTemplate(
|
|
|
103
101
|
export async function selectUbuntuApplianceFromCatalog(
|
|
104
102
|
credentials: ProxmoxCredentials,
|
|
105
103
|
nodeName: string,
|
|
106
|
-
opts: { currentTemplate?: string }
|
|
104
|
+
opts: { scope: string; currentTemplate?: string },
|
|
107
105
|
): Promise<TemplateSelectionResult> {
|
|
108
106
|
console.log('\nFetching Proxmox template catalog...');
|
|
109
107
|
const result = await listAvailableAppliances(credentials, nodeName);
|
|
@@ -115,13 +113,17 @@ export async function selectUbuntuApplianceFromCatalog(
|
|
|
115
113
|
' After saving, run `pveam download <storage> <template>` on the Proxmox host before deploying.',
|
|
116
114
|
);
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
// Manual fallback over the bus (ISS-0127) — drivable headlessly by
|
|
117
|
+
// answering `interview.required.<scope>.lxc_template_manual`.
|
|
118
|
+
const manual = await askText({
|
|
119
|
+
scope: opts.scope,
|
|
120
|
+
key: 'lxc_template_manual',
|
|
121
|
+
message: 'Template filename',
|
|
120
122
|
placeholder: 'ubuntu-24.04-standard_24.04-2_amd64.tar.zst',
|
|
121
|
-
|
|
123
|
+
required: true,
|
|
122
124
|
});
|
|
123
125
|
|
|
124
|
-
if (
|
|
126
|
+
if (manual.trim() === '') {
|
|
125
127
|
return { kind: 'cancelled' };
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -140,8 +142,11 @@ export async function selectUbuntuApplianceFromCatalog(
|
|
|
140
142
|
|
|
141
143
|
const initialValue = pickInitialTemplate(options, opts.currentTemplate);
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
// Template choice over the bus (ISS-0127), keyed `lxc_template` within scope.
|
|
146
|
+
const selection = await askSelect({
|
|
147
|
+
scope: opts.scope,
|
|
148
|
+
key: 'lxc_template',
|
|
149
|
+
message: 'Default Ubuntu template',
|
|
145
150
|
options: options.map((e) => ({
|
|
146
151
|
value: e.appliance.template,
|
|
147
152
|
label: `Ubuntu ${e.major}.${String(e.minor).padStart(2, '0')}${e.isLts ? ' LTS' : ''}`,
|
|
@@ -149,13 +154,9 @@ export async function selectUbuntuApplianceFromCatalog(
|
|
|
149
154
|
? `revision ${e.appliance.version}`
|
|
150
155
|
: `revision ${e.appliance.version} · non-LTS`,
|
|
151
156
|
})),
|
|
152
|
-
initialValue,
|
|
157
|
+
defaultValue: initialValue,
|
|
153
158
|
});
|
|
154
159
|
|
|
155
|
-
if (p.isCancel(selection)) {
|
|
156
|
-
return { kind: 'cancelled' };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
160
|
return { kind: 'selected', choice: { template: selection, source: 'catalog' } };
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Configure a Digital Ocean VPS service
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
6
|
import type { NetworkZone } from '../../db/schema';
|
|
7
|
+
import { askMultiselect, askText, withInterviewSession } from '../../services/bus-interview';
|
|
8
8
|
import {
|
|
9
9
|
addContainerService,
|
|
10
10
|
testConnection as testServiceConnection,
|
|
11
11
|
updateVerificationStatus,
|
|
12
12
|
} from '../../services/container-service';
|
|
13
|
-
import { celiloIntro, celiloOutro
|
|
13
|
+
import { celiloIntro, celiloOutro } from '../prompts';
|
|
14
|
+
import { resolveServiceCredential } from '../service-credential';
|
|
14
15
|
import type { CommandResult } from '../types';
|
|
15
|
-
import { validateRequired } from '../validators';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Handle service add digitalocean command
|
|
@@ -22,112 +22,123 @@ import { validateRequired } from '../validators';
|
|
|
22
22
|
*/
|
|
23
23
|
export async function handleServiceAddDigitalOcean(
|
|
24
24
|
_args: string[],
|
|
25
|
-
|
|
25
|
+
flags: Record<string, boolean | string> = {},
|
|
26
26
|
): Promise<CommandResult> {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
// Non-secret prompts route through the bus interview (ISS-0127); the DO API
|
|
28
|
+
// token is a service credential that travels by flag/env only (D7).
|
|
29
|
+
const scope = 'service-add:digitalocean';
|
|
30
|
+
|
|
31
|
+
return withInterviewSession(async () => {
|
|
32
|
+
try {
|
|
33
|
+
celiloIntro('Add Digital Ocean VPS Service');
|
|
34
|
+
|
|
35
|
+
const name = await askText({
|
|
36
|
+
scope,
|
|
37
|
+
key: 'name',
|
|
38
|
+
message: 'Human-readable name',
|
|
39
|
+
defaultValue: 'Digital Ocean VPS',
|
|
40
|
+
placeholder: 'Digital Ocean VPS',
|
|
41
|
+
required: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const zones = await askMultiselect({
|
|
45
|
+
scope,
|
|
46
|
+
key: 'zones',
|
|
47
|
+
message: 'Zones this service can provision to',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 'internal', label: 'internal' },
|
|
50
|
+
{ value: 'dmz', label: 'dmz' },
|
|
51
|
+
{ value: 'app', label: 'app' },
|
|
52
|
+
{ value: 'secure', label: 'secure' },
|
|
53
|
+
{ value: 'external', label: 'external' },
|
|
54
|
+
],
|
|
55
|
+
required: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log('\nDigital Ocean Configuration');
|
|
59
|
+
console.log('──────────────────────────');
|
|
60
|
+
|
|
61
|
+
// API token is a service credential — flag/env only (D7).
|
|
62
|
+
const apiToken = await resolveServiceCredential({
|
|
63
|
+
field: 'API token (Personal Access Token)',
|
|
64
|
+
flag: 'api-token',
|
|
65
|
+
envVar: 'DIGITALOCEAN_API_TOKEN',
|
|
66
|
+
flagValue: flags['api-token'],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const defaultRegion = await askText({
|
|
70
|
+
scope,
|
|
71
|
+
key: 'default_region',
|
|
72
|
+
message: 'Default region',
|
|
73
|
+
defaultValue: 'nyc3',
|
|
74
|
+
placeholder: 'nyc3',
|
|
75
|
+
required: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const defaultSize = await askText({
|
|
79
|
+
scope,
|
|
80
|
+
key: 'default_size',
|
|
81
|
+
message: 'Default droplet size',
|
|
82
|
+
defaultValue: 's-1vcpu-1gb',
|
|
83
|
+
placeholder: 's-1vcpu-1gb',
|
|
84
|
+
required: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const defaultImage = await askText({
|
|
88
|
+
scope,
|
|
89
|
+
key: 'default_image',
|
|
90
|
+
message: 'Default image',
|
|
91
|
+
defaultValue: 'ubuntu-22-04-x64',
|
|
92
|
+
placeholder: 'ubuntu-22-04-x64',
|
|
93
|
+
required: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Store the service first so we can test it
|
|
97
|
+
const service = await addContainerService({
|
|
98
|
+
name,
|
|
99
|
+
providerName: 'digitalocean',
|
|
100
|
+
zones: zones as unknown as NetworkZone[],
|
|
101
|
+
apiCredentials: {
|
|
102
|
+
api_token: apiToken,
|
|
103
|
+
},
|
|
104
|
+
providerConfig: {
|
|
105
|
+
default_region: defaultRegion,
|
|
106
|
+
default_size: defaultSize,
|
|
107
|
+
default_image: defaultImage,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Test connection
|
|
112
|
+
console.log('\nTesting connection...');
|
|
113
|
+
const testResult = await testServiceConnection(service);
|
|
114
|
+
await updateVerificationStatus(service.id, testResult);
|
|
115
|
+
|
|
116
|
+
if (!testResult.success) {
|
|
117
|
+
console.log(`✗ Connection test failed: ${testResult.message}`);
|
|
118
|
+
console.log(
|
|
119
|
+
'\nService saved but not verified. The service will not be used until verification succeeds.',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
celiloOutro(
|
|
123
|
+
`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`,
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
console.log(`✓ ${testResult.message}`);
|
|
127
|
+
|
|
128
|
+
celiloOutro(
|
|
129
|
+
`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`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
message: `Added Digital Ocean service: ${service.id}`,
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: `Failed to add Digital Ocean service: ${error instanceof Error ? error.message : String(error)}`,
|
|
141
|
+
};
|
|
55
142
|
}
|
|
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
|
-
}
|
|
143
|
+
});
|
|
133
144
|
}
|