kadi-deploy 0.19.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/.env.example +6 -0
- package/.prettierrc +6 -0
- package/README.md +589 -0
- package/agent.json +23 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/quick-command.txt +92 -0
- package/scripts/preflight.js +458 -0
- package/scripts/preflight.sh +300 -0
- package/src/cli/bid-selector.ts +222 -0
- package/src/cli/colors.ts +216 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/prompts.ts +190 -0
- package/src/cli/spinners.ts +165 -0
- package/src/commands/deploy-local.ts +475 -0
- package/src/commands/deploy.ts +1342 -0
- package/src/commands/down.ts +679 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/lock.ts +571 -0
- package/src/config/agent-loader.ts +177 -0
- package/src/config/index.ts +9 -0
- package/src/display/deployment-info.ts +220 -0
- package/src/display/pricing.ts +137 -0
- package/src/display/resources.ts +234 -0
- package/src/enhanced-registry-manager.ts +892 -0
- package/src/index.ts +307 -0
- package/src/infrastructure/registry.ts +269 -0
- package/src/schemas/profiles.ts +529 -0
- package/src/secrets/broker-urls.ts +109 -0
- package/src/secrets/handshake.ts +407 -0
- package/src/secrets/index.ts +69 -0
- package/src/secrets/inject-env.ts +171 -0
- package/src/secrets/nonce.ts +31 -0
- package/src/secrets/normalize.ts +204 -0
- package/src/secrets/prepare.ts +152 -0
- package/src/secrets/validate.ts +243 -0
- package/src/secrets/vault.ts +80 -0
- package/src/types/akash.ts +116 -0
- package/src/types/container-registry-ability.d.ts +158 -0
- package/src/types/external.ts +49 -0
- package/src/types.ts +211 -0
- package/src/utils/akt-price.ts +74 -0
- package/tests/agent-loader.test.ts +239 -0
- package/tests/autonomous.test.ts +244 -0
- package/tests/down.test.ts +1143 -0
- package/tests/lock.test.ts +1148 -0
- package/tests/nonce.test.ts +34 -0
- package/tests/normalize.test.ts +270 -0
- package/tests/secrets-schema.test.ts +301 -0
- package/tests/types.test.ts +198 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Command - Akash Network Integration
|
|
3
|
+
*
|
|
4
|
+
* Main deployment command that uses deploy-ability library for Akash deployments.
|
|
5
|
+
* Handles the UX (QR codes, spinners, confirmations) while deploy-ability does the work.
|
|
6
|
+
*
|
|
7
|
+
* @module commands/deploy
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Polyfill localStorage for WalletConnect (requires browser API in Node.js)
|
|
11
|
+
import { LocalStorage } from 'node-localstorage';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const kadiHome = path.join(os.homedir(), '.kadi');
|
|
16
|
+
(globalThis as any).localStorage = new LocalStorage(kadiHome);
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
deployToAkash,
|
|
20
|
+
connectWallet,
|
|
21
|
+
disconnectWallet,
|
|
22
|
+
AkashClient,
|
|
23
|
+
type EnhancedBid,
|
|
24
|
+
type BidFilterCriteria,
|
|
25
|
+
type SecretsProvider,
|
|
26
|
+
} from '@kadi.build/deploy-ability/akash';
|
|
27
|
+
import type { AkashDeploymentData, AkashDryRunData, AutonomousDeploymentConfig } from '@kadi.build/deploy-ability/types';
|
|
28
|
+
|
|
29
|
+
import { loadAgentConfig, getProfile, getFirstProfile } from '../config/agent-loader.js';
|
|
30
|
+
import {
|
|
31
|
+
waitForSecretRequest,
|
|
32
|
+
shareSecrets,
|
|
33
|
+
sendRejection,
|
|
34
|
+
readSecretsFromCli,
|
|
35
|
+
readSecretFromCli,
|
|
36
|
+
prepareSecretsForDeployment,
|
|
37
|
+
performSecretHandshake,
|
|
38
|
+
type WaitForSecretsResult,
|
|
39
|
+
} from '../secrets/index.js';
|
|
40
|
+
import { normalizeSecrets, hasAnySecrets, type NormalizedVaultSource } from '../secrets/normalize.js';
|
|
41
|
+
import { startSpinner, succeedSpinner, failSpinner } from '../cli/spinners.js';
|
|
42
|
+
import { confirmPrompt, selectPrompt, textPrompt } from '../cli/prompts.js';
|
|
43
|
+
import { error as errorColor, warning, success as successColor, dim, bold, highlight } from '../cli/colors.js';
|
|
44
|
+
import { selectBidInteractively, sortBidsByQuality } from '../cli/bid-selector.js';
|
|
45
|
+
import { fetchAktPriceUSD } from '../utils/akt-price.js';
|
|
46
|
+
import fs from 'node:fs/promises';
|
|
47
|
+
import { readFileSync } from 'node:fs';
|
|
48
|
+
import {
|
|
49
|
+
displayDeploymentInfo,
|
|
50
|
+
displayDeploymentSummary,
|
|
51
|
+
displayDryRunInfo,
|
|
52
|
+
displayProfileInfo
|
|
53
|
+
} from '../display/deployment-info.js';
|
|
54
|
+
import { setupRegistryIfNeeded } from '../infrastructure/registry.js';
|
|
55
|
+
import { buildAkashLock, writeLockFile } from './lock.js';
|
|
56
|
+
|
|
57
|
+
import QRCode from 'qrcode-terminal';
|
|
58
|
+
import type { DeploymentContext } from '../types.js';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Executes Akash Network deployment using deploy-ability
|
|
62
|
+
*
|
|
63
|
+
* Flow mirrors old kadi-deploy but uses deploy-ability library:
|
|
64
|
+
* 1. Load profile
|
|
65
|
+
* 2. Connect wallet (WalletConnect QR code)
|
|
66
|
+
* 3. Get signing client
|
|
67
|
+
* 4. Ensure certificate exists
|
|
68
|
+
* 5. Deploy (deploy-ability does the heavy lifting)
|
|
69
|
+
* 6. Display results
|
|
70
|
+
*
|
|
71
|
+
* @param ctx - Deployment context from CLI
|
|
72
|
+
*/
|
|
73
|
+
export async function executeAkashDeployment(ctx: DeploymentContext): Promise<void> {
|
|
74
|
+
const { logger, projectRoot, flags } = ctx;
|
|
75
|
+
|
|
76
|
+
// Track exit code to force process exit after cleanup (0 = success, 1 = failure, null = don't exit)
|
|
77
|
+
// This is necessary because background timers in the registry keep Node.js alive
|
|
78
|
+
let exitCode: number | null = null;
|
|
79
|
+
|
|
80
|
+
// ----------------------------------------------------------------
|
|
81
|
+
// 1. Load agent configuration
|
|
82
|
+
// ----------------------------------------------------------------
|
|
83
|
+
let spinner = startSpinner('Loading agent configuration...');
|
|
84
|
+
|
|
85
|
+
let agentConfig;
|
|
86
|
+
try {
|
|
87
|
+
agentConfig = await loadAgentConfig(projectRoot);
|
|
88
|
+
succeedSpinner(spinner, `Loaded agent: ${agentConfig.name}`);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
failSpinner(spinner, 'Failed to load agent.json');
|
|
91
|
+
logger.error(errorColor((err as Error).message));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ----------------------------------------------------------------
|
|
96
|
+
// 2. Resolve profile
|
|
97
|
+
// ----------------------------------------------------------------
|
|
98
|
+
const profileName = flags.profile || getFirstProfile(agentConfig);
|
|
99
|
+
|
|
100
|
+
if (!profileName) {
|
|
101
|
+
logger.error('No deploy profiles found in agent.json');
|
|
102
|
+
logger.log('Add a deploy profile under the "deploy" field in agent.json');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const profile = getProfile(agentConfig, profileName);
|
|
107
|
+
if (!profile) {
|
|
108
|
+
logger.error(`Profile "${profileName}" not found in agent.json`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate it's an Akash profile
|
|
113
|
+
if (profile.target !== 'akash') {
|
|
114
|
+
logger.error(`Profile "${profileName}" is not an Akash profile (target: ${profile.target})`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// TypeScript type narrowing: profile is now AkashProfile
|
|
119
|
+
type AkashProfileType = typeof profile & { target: 'akash' };
|
|
120
|
+
const akashProfile = profile as AkashProfileType;
|
|
121
|
+
|
|
122
|
+
const network = akashProfile.network || (flags.network as 'mainnet' | 'testnet') || 'testnet';
|
|
123
|
+
logger.log(successColor(`✓ Using profile: ${profileName} (${network})`));
|
|
124
|
+
|
|
125
|
+
// ----------------------------------------------------------------
|
|
126
|
+
// 2.5. SECRETS INJECTION
|
|
127
|
+
// If profile declares secrets, inject KADI_* env vars into all services
|
|
128
|
+
// Supports both legacy single-vault and multi-vault configurations.
|
|
129
|
+
// ----------------------------------------------------------------
|
|
130
|
+
const normalizedSecrets = normalizeSecrets(akashProfile.secrets as any);
|
|
131
|
+
const hasSecrets = hasAnySecrets(normalizedSecrets);
|
|
132
|
+
|
|
133
|
+
let deployNonce: string | undefined;
|
|
134
|
+
let brokerUrl: string | null = null;
|
|
135
|
+
let secretsVault: string | undefined;
|
|
136
|
+
let secretsSources: NormalizedVaultSource[] = [];
|
|
137
|
+
|
|
138
|
+
if (hasSecrets) {
|
|
139
|
+
const result = prepareSecretsForDeployment({
|
|
140
|
+
profile: akashProfile,
|
|
141
|
+
agentConfig,
|
|
142
|
+
projectRoot,
|
|
143
|
+
logger,
|
|
144
|
+
colors: { dim, success: successColor, error: errorColor },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!result) return;
|
|
148
|
+
|
|
149
|
+
deployNonce = result.deployNonce;
|
|
150
|
+
brokerUrl = result.brokerUrl;
|
|
151
|
+
secretsVault = result.secretsVault;
|
|
152
|
+
secretsSources = result.secretsSources;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ----------------------------------------------------------------
|
|
156
|
+
// 2.6. START LISTENING FOR SECRET REQUESTS EARLY
|
|
157
|
+
// We start listening BEFORE deploying to avoid a race condition where
|
|
158
|
+
// the agent requests secrets before we're subscribed to the broker.
|
|
159
|
+
// ----------------------------------------------------------------
|
|
160
|
+
let secretsPromise: Promise<WaitForSecretsResult> | null = null;
|
|
161
|
+
|
|
162
|
+
if (hasSecrets && deployNonce && brokerUrl && !flags.dryRun) {
|
|
163
|
+
logger.log(dim(`\nConnecting to broker to listen for secret requests...`));
|
|
164
|
+
|
|
165
|
+
// Start listening NOW (don't await - let it run in background)
|
|
166
|
+
// This just captures the request silently - we'll prompt after deployment
|
|
167
|
+
secretsPromise = waitForSecretRequest({
|
|
168
|
+
brokerUrl,
|
|
169
|
+
expectedNonce: deployNonce,
|
|
170
|
+
timeout: flags.secretTimeout || 5 * 60 * 1000, // Default 5 minutes
|
|
171
|
+
logger: {
|
|
172
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
173
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Wait for broker subscription before starting deployment
|
|
178
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ----------------------------------------------------------------
|
|
182
|
+
// 3. LOCAL IMAGE HANDLING
|
|
183
|
+
// If profile uses local images (e.g., "my-app" instead of "docker.io/nginx"),
|
|
184
|
+
// we need to make them accessible to Akash providers:
|
|
185
|
+
// - Start temporary registry (localhost:3000)
|
|
186
|
+
// - Push local images to it
|
|
187
|
+
// - Expose publicly via kadi/ngrok/serveo/localtunnel
|
|
188
|
+
// - Rewrite profile to use public URLs
|
|
189
|
+
// ----------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
// Validate tunnel service if provided
|
|
192
|
+
const validTunnelServices = ['kadi', 'ngrok', 'serveo', 'localtunnel'] as const;
|
|
193
|
+
let tunnelService: 'kadi' | 'ngrok' | 'serveo' | 'localtunnel' | undefined = undefined;
|
|
194
|
+
|
|
195
|
+
if (flags.tunnelService) {
|
|
196
|
+
if (!validTunnelServices.includes(flags.tunnelService as 'kadi' | 'ngrok' | 'serveo' | 'localtunnel')) {
|
|
197
|
+
logger.error(`Invalid tunnel service: ${flags.tunnelService}`);
|
|
198
|
+
logger.log(`Valid options: ${validTunnelServices.join(', ')}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
tunnelService = flags.tunnelService as 'kadi' | 'ngrok' | 'serveo' | 'localtunnel';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Type assertion: kadi-deploy's AkashProfileType is runtime-compatible with deploy-ability's AkashDeploymentProfile
|
|
205
|
+
// Both have the same structure (services with image/env/expose), but TypeScript types differ slightly
|
|
206
|
+
const registryContext = await setupRegistryIfNeeded(akashProfile as any, logger, {
|
|
207
|
+
useRemoteRegistry: flags.useRemoteRegistry,
|
|
208
|
+
registryDuration: flags.registryDuration,
|
|
209
|
+
tunnelService,
|
|
210
|
+
containerEngine: (flags.engine || akashProfile.engine) as 'docker' | 'podman' | undefined
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Wrap transformed profile in LoadedProfile structure for deploy-ability
|
|
214
|
+
// Type assertion: Our Zod-validated profile is compatible with deploy-ability's expected type
|
|
215
|
+
// The runtime structure is correct even though TypeScript types differ slightly
|
|
216
|
+
const loadedProfile = {
|
|
217
|
+
name: profileName,
|
|
218
|
+
profile: registryContext.deployableProfile,
|
|
219
|
+
agent: agentConfig,
|
|
220
|
+
projectRoot
|
|
221
|
+
} as any; // TODO: Align types between kadi-deploy and deploy-ability
|
|
222
|
+
|
|
223
|
+
// ----------------------------------------------------------------
|
|
224
|
+
// 3.5. AUTONOMOUS MODE - Fully automated deployment path
|
|
225
|
+
// When --autonomous flag is set, bypass all interactive prompts and use
|
|
226
|
+
// deploy-ability's autonomous mode with secrets vault for wallet credentials.
|
|
227
|
+
// ----------------------------------------------------------------
|
|
228
|
+
if (flags.autonomous) {
|
|
229
|
+
await executeAutonomousDeployment(ctx, {
|
|
230
|
+
akashProfile,
|
|
231
|
+
profileName,
|
|
232
|
+
network,
|
|
233
|
+
loadedProfile,
|
|
234
|
+
registryContext,
|
|
235
|
+
hasSecrets,
|
|
236
|
+
deployNonce,
|
|
237
|
+
brokerUrl,
|
|
238
|
+
secretsVault,
|
|
239
|
+
secretsSources,
|
|
240
|
+
agentConfig,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ----------------------------------------------------------------
|
|
246
|
+
// 4. Dry run check
|
|
247
|
+
// ----------------------------------------------------------------
|
|
248
|
+
if (flags.dryRun) {
|
|
249
|
+
logger.log('\n' + warning('⚠️ DRY RUN MODE - No deployment will be created'));
|
|
250
|
+
logger.log(dim('Skipping wallet connection and certificate checks'));
|
|
251
|
+
logger.log('');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ----------------------------------------------------------------
|
|
255
|
+
// Resource management: wallet, akash client, and registry cleanup
|
|
256
|
+
// All resources are cleaned up in the finally block, ensuring proper
|
|
257
|
+
// cleanup on success, error, and early returns
|
|
258
|
+
// ----------------------------------------------------------------
|
|
259
|
+
let walletResult;
|
|
260
|
+
let akashClient: AkashClient | undefined;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// ----------------------------------------------------------------
|
|
264
|
+
// 5. Connect wallet (WalletConnect with QR code)
|
|
265
|
+
// ----------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
if (!flags.dryRun) {
|
|
268
|
+
spinner = startSpinner('Initializing WalletConnect...');
|
|
269
|
+
|
|
270
|
+
const projectId = akashProfile.walletConnectProjectId || 'ef0fd1c783f5ef70fbb102f5e5dd2c43'; // Default KADI project ID
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// deploy-ability provides URI via callback, we display QR code
|
|
274
|
+
walletResult = await connectWallet(
|
|
275
|
+
projectId,
|
|
276
|
+
network as 'mainnet' | 'testnet',
|
|
277
|
+
{
|
|
278
|
+
onUriGenerated: (uri: string) => {
|
|
279
|
+
succeedSpinner(spinner, 'QR Code generated');
|
|
280
|
+
logger.log('');
|
|
281
|
+
logger.log('📱 Scan this QR code with your Keplr mobile wallet:');
|
|
282
|
+
logger.log('');
|
|
283
|
+
QRCode.generate(uri, { small: true }); // kadi-deploy displays it
|
|
284
|
+
logger.log('');
|
|
285
|
+
spinner = startSpinner('Waiting for wallet approval...');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (!walletResult.success) {
|
|
291
|
+
failSpinner(spinner, 'Wallet connection failed');
|
|
292
|
+
logger.error(errorColor(walletResult.error.message));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
succeedSpinner(spinner, `Wallet connected: ${walletResult.data.address}`);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
failSpinner(spinner, 'Wallet connection failed');
|
|
299
|
+
logger.error(errorColor((err as Error).message));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ----------------------------------------------------------------
|
|
305
|
+
// 6. Create Akash blockchain client
|
|
306
|
+
// ----------------------------------------------------------------
|
|
307
|
+
if (!flags.dryRun && walletResult) {
|
|
308
|
+
spinner = startSpinner('Creating Akash client...');
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
// Create AkashClient with signer from wallet
|
|
312
|
+
akashClient = new AkashClient({
|
|
313
|
+
network: network as 'mainnet' | 'testnet',
|
|
314
|
+
signer: walletResult.data.signer
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
succeedSpinner(spinner, 'Akash client ready');
|
|
318
|
+
} catch (err) {
|
|
319
|
+
failSpinner(spinner, 'Failed to create Akash client');
|
|
320
|
+
logger.error(errorColor((err as Error).message));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ----------------------------------------------------------------
|
|
326
|
+
// 7. Load existing certificate if specified in profile
|
|
327
|
+
// ----------------------------------------------------------------
|
|
328
|
+
let existingCert;
|
|
329
|
+
|
|
330
|
+
if (!flags.dryRun && akashProfile.cert) {
|
|
331
|
+
const certPath = akashProfile.cert;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const certData = await fs.readFile(certPath, 'utf-8');
|
|
335
|
+
existingCert = JSON.parse(certData);
|
|
336
|
+
logger.log(dim(`Loaded certificate from: ${certPath}`));
|
|
337
|
+
} catch (err) {
|
|
338
|
+
logger.error(errorColor(`Failed to load certificate from ${certPath}`));
|
|
339
|
+
logger.error(errorColor((err as Error).message));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ----------------------------------------------------------------
|
|
345
|
+
// 8. Ensure certificate exists
|
|
346
|
+
// ----------------------------------------------------------------
|
|
347
|
+
let certResult;
|
|
348
|
+
const defaultCertPath = path.join(os.homedir(), '.kadi', 'certificate.json');
|
|
349
|
+
|
|
350
|
+
if (!flags.dryRun && walletResult && akashClient) {
|
|
351
|
+
spinner = startSpinner('Checking certificate...');
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
// Get certificate manager from AkashClient
|
|
355
|
+
const certManager = akashClient.getCertificateManager();
|
|
356
|
+
|
|
357
|
+
// Check if certificate exists on-chain (before calling getOrCreate)
|
|
358
|
+
const onChainCert = await certManager.query(walletResult.data.address);
|
|
359
|
+
|
|
360
|
+
// If cert exists on-chain but we don't have it locally, offer interactive options
|
|
361
|
+
if (onChainCert.success && onChainCert.data?.exists && !existingCert) {
|
|
362
|
+
succeedSpinner(spinner, 'Certificate check complete');
|
|
363
|
+
|
|
364
|
+
// Check if default cert file exists
|
|
365
|
+
let defaultCertExists = false;
|
|
366
|
+
try {
|
|
367
|
+
await fs.access(defaultCertPath);
|
|
368
|
+
defaultCertExists = true;
|
|
369
|
+
} catch {
|
|
370
|
+
defaultCertExists = false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
logger.log('');
|
|
374
|
+
|
|
375
|
+
if (defaultCertExists) {
|
|
376
|
+
// Scenario A: Cert file found at default location
|
|
377
|
+
logger.log(bold('This wallet has an active certificate on Akash.'));
|
|
378
|
+
logger.log(successColor(`Found certificate file at ${defaultCertPath}`));
|
|
379
|
+
logger.log('');
|
|
380
|
+
|
|
381
|
+
const useExisting = await confirmPrompt(
|
|
382
|
+
'Use this certificate?',
|
|
383
|
+
true
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (useExisting) {
|
|
387
|
+
// Load the certificate
|
|
388
|
+
try {
|
|
389
|
+
const certData = await fs.readFile(defaultCertPath, 'utf-8');
|
|
390
|
+
existingCert = JSON.parse(certData);
|
|
391
|
+
logger.log(dim(`Loaded certificate from: ${defaultCertPath}`));
|
|
392
|
+
} catch (err) {
|
|
393
|
+
logger.error(errorColor(`Failed to load certificate: ${(err as Error).message}`));
|
|
394
|
+
exitCode = 1;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
// User said no, show the 3-option menu
|
|
399
|
+
const choice = await selectPrompt(
|
|
400
|
+
'How would you like to proceed?',
|
|
401
|
+
[
|
|
402
|
+
'Revoke existing and create new certificate (Recommended)',
|
|
403
|
+
'Enter path to certificate file',
|
|
404
|
+
'Cancel deployment'
|
|
405
|
+
],
|
|
406
|
+
0
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (choice.includes('Revoke')) {
|
|
410
|
+
// Revoke and create new
|
|
411
|
+
spinner = startSpinner('Revoking existing certificate(s)...');
|
|
412
|
+
const revokeResult = await certManager.revoke(walletResult.data.address);
|
|
413
|
+
|
|
414
|
+
if (!revokeResult.success) {
|
|
415
|
+
failSpinner(spinner, 'Failed to revoke certificate');
|
|
416
|
+
logger.error(errorColor(revokeResult.error.message));
|
|
417
|
+
exitCode = 1;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
succeedSpinner(spinner, `Revoked ${revokeResult.data.revokedCount} certificate(s)`);
|
|
422
|
+
// existingCert stays undefined, getOrCreate will make a new one
|
|
423
|
+
|
|
424
|
+
} else if (choice.includes('Enter path')) {
|
|
425
|
+
// Prompt for custom path
|
|
426
|
+
const customPath = await textPrompt('Enter certificate file path:');
|
|
427
|
+
try {
|
|
428
|
+
const certData = await fs.readFile(customPath, 'utf-8');
|
|
429
|
+
existingCert = JSON.parse(certData);
|
|
430
|
+
logger.log(dim(`Loaded certificate from: ${customPath}`));
|
|
431
|
+
} catch (err) {
|
|
432
|
+
logger.error(errorColor(`Failed to load certificate: ${(err as Error).message}`));
|
|
433
|
+
exitCode = 1;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
} else {
|
|
438
|
+
// Cancel
|
|
439
|
+
logger.log('Deployment cancelled');
|
|
440
|
+
exitCode = 0;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
// Scenario B: Cert file NOT found - show 3-option menu directly
|
|
446
|
+
logger.log(warning('This wallet has an active certificate on Akash, but we couldn\'t find'));
|
|
447
|
+
logger.log(warning('the certificate file locally.'));
|
|
448
|
+
logger.log('');
|
|
449
|
+
logger.log(dim(`Checked ${defaultCertPath}...`), dim('not found'));
|
|
450
|
+
logger.log('');
|
|
451
|
+
|
|
452
|
+
const choice = await selectPrompt(
|
|
453
|
+
'How would you like to proceed?',
|
|
454
|
+
[
|
|
455
|
+
'Revoke existing and create new certificate (Recommended)',
|
|
456
|
+
'Enter path to certificate file',
|
|
457
|
+
'Cancel deployment'
|
|
458
|
+
],
|
|
459
|
+
0
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (choice.includes('Revoke')) {
|
|
463
|
+
// Revoke and create new
|
|
464
|
+
spinner = startSpinner('Revoking existing certificate(s)...');
|
|
465
|
+
const revokeResult = await certManager.revoke(walletResult.data.address);
|
|
466
|
+
|
|
467
|
+
if (!revokeResult.success) {
|
|
468
|
+
failSpinner(spinner, 'Failed to revoke certificate');
|
|
469
|
+
logger.error(errorColor(revokeResult.error.message));
|
|
470
|
+
exitCode = 1;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
succeedSpinner(spinner, `Revoked ${revokeResult.data.revokedCount} certificate(s)`);
|
|
475
|
+
// existingCert stays undefined, getOrCreate will make a new one
|
|
476
|
+
|
|
477
|
+
} else if (choice.includes('Enter path')) {
|
|
478
|
+
// Prompt for custom path
|
|
479
|
+
const customPath = await textPrompt('Enter certificate file path:');
|
|
480
|
+
try {
|
|
481
|
+
const certData = await fs.readFile(customPath, 'utf-8');
|
|
482
|
+
existingCert = JSON.parse(certData);
|
|
483
|
+
logger.log(dim(`Loaded certificate from: ${customPath}`));
|
|
484
|
+
} catch (err) {
|
|
485
|
+
logger.error(errorColor(`Failed to load certificate: ${(err as Error).message}`));
|
|
486
|
+
exitCode = 1;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
} else {
|
|
491
|
+
// Cancel
|
|
492
|
+
logger.log('Deployment cancelled');
|
|
493
|
+
exitCode = 0;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Restart spinner for the next step
|
|
499
|
+
spinner = startSpinner('Checking certificate...');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Now proceed with getOrCreate (will create new cert if needed, or validate existing)
|
|
503
|
+
certResult = await certManager.getOrCreate(
|
|
504
|
+
walletResult.data.address,
|
|
505
|
+
existingCert ? { existingCertificate: existingCert } : undefined
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (!certResult.success) {
|
|
509
|
+
failSpinner(spinner, 'Certificate check failed');
|
|
510
|
+
// Extract error message properly (avoid [object Object])
|
|
511
|
+
// TODO: The error message may still show "[object Object]" when wallet rejects
|
|
512
|
+
// a transaction. This needs to be fixed in deploy-ability's broadcast() method
|
|
513
|
+
// where `${error}` should be `${error?.message || String(error)}` instead.
|
|
514
|
+
const errMsg = certResult.error?.message || String(certResult.error) || 'Unknown error';
|
|
515
|
+
logger.error(errorColor(errMsg));
|
|
516
|
+
exitCode = 1;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
succeedSpinner(spinner, 'Certificate ready');
|
|
521
|
+
|
|
522
|
+
// Save certificate for future use if newly generated
|
|
523
|
+
if (!existingCert && !akashProfile.cert) {
|
|
524
|
+
try {
|
|
525
|
+
await fs.mkdir(path.dirname(defaultCertPath), { recursive: true });
|
|
526
|
+
await fs.writeFile(defaultCertPath, JSON.stringify(certResult.data, null, 2), 'utf-8');
|
|
527
|
+
logger.log(successColor(`\nCertificate saved to: ${defaultCertPath}`));
|
|
528
|
+
logger.log(dim(`Tip: Add ${highlight('"cert": "~/.kadi/certificate.json"')} to your profile to skip this step`));
|
|
529
|
+
} catch (saveErr) {
|
|
530
|
+
logger.log(warning('Could not save certificate to disk'));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch (err) {
|
|
534
|
+
failSpinner(spinner, 'Certificate check failed');
|
|
535
|
+
logger.error(errorColor((err as Error).message));
|
|
536
|
+
exitCode = 1;
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ----------------------------------------------------------------
|
|
542
|
+
// 8. Confirm deployment
|
|
543
|
+
// ----------------------------------------------------------------
|
|
544
|
+
if (!flags.dryRun && !flags.yes) {
|
|
545
|
+
const proceed = await confirmPrompt(
|
|
546
|
+
`Deploy to ${network}?`,
|
|
547
|
+
true
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
if (!proceed) {
|
|
551
|
+
logger.log('Deployment cancelled');
|
|
552
|
+
// Finally block will handle cleanup
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ----------------------------------------------------------------
|
|
558
|
+
// 9. Deploy using deploy-ability
|
|
559
|
+
// ----------------------------------------------------------------
|
|
560
|
+
spinner = startSpinner('Deploying to Akash Network...');
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
// For non-dry-run, wallet and certificate are required
|
|
564
|
+
if (!flags.dryRun && (!walletResult || !certResult)) {
|
|
565
|
+
failSpinner(spinner, 'Missing wallet or certificate');
|
|
566
|
+
logger.error('Wallet and certificate are required for deployment');
|
|
567
|
+
// Finally block will handle cleanup
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const result = await deployToAkash({
|
|
572
|
+
projectRoot,
|
|
573
|
+
profile: profileName,
|
|
574
|
+
loadedProfile, // Pass transformed profile with registry URLs
|
|
575
|
+
network: network as 'mainnet' | 'testnet',
|
|
576
|
+
dryRun: flags.dryRun,
|
|
577
|
+
verbose: flags.verbose,
|
|
578
|
+
blacklist: akashProfile.blacklist as readonly string[] | undefined, // Provider blacklist from profile
|
|
579
|
+
|
|
580
|
+
// Wait for containers to be ready before returning
|
|
581
|
+
// This ensures providers have time to pull images from temporary registry
|
|
582
|
+
containerTimeout: 600000, // 10 minutes
|
|
583
|
+
|
|
584
|
+
// Wallet and certificate optional for dry-run, required otherwise
|
|
585
|
+
wallet: walletResult?.data,
|
|
586
|
+
certificate: certResult?.data,
|
|
587
|
+
|
|
588
|
+
// Interactive bid selector - prompts user to choose provider
|
|
589
|
+
bidSelector: async (bids: EnhancedBid[]) => {
|
|
590
|
+
// Sort bids by quality (audited + high uptime first)
|
|
591
|
+
const sortedBids = sortBidsByQuality(bids);
|
|
592
|
+
|
|
593
|
+
// Stop spinner to allow enquirer to take over terminal
|
|
594
|
+
spinner.stop();
|
|
595
|
+
|
|
596
|
+
// Fetch AKT price for USD conversion
|
|
597
|
+
// Fails gracefully - if unavailable, we just show AKT pricing
|
|
598
|
+
const aktPriceUsd = await fetchAktPriceUSD();
|
|
599
|
+
|
|
600
|
+
// Show interactive selection prompt with USD pricing if available
|
|
601
|
+
const selected = await selectBidInteractively(sortedBids, aktPriceUsd);
|
|
602
|
+
|
|
603
|
+
// Restart spinner after selection
|
|
604
|
+
if (selected) {
|
|
605
|
+
spinner = startSpinner('Creating lease with selected provider...');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return selected;
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
// Custom logger that updates spinner text with progress
|
|
612
|
+
logger: {
|
|
613
|
+
log: (msg: string) => {
|
|
614
|
+
// Update spinner text to show current progress
|
|
615
|
+
spinner.text = msg;
|
|
616
|
+
},
|
|
617
|
+
error: (msg: string) => logger.error(msg),
|
|
618
|
+
warn: (msg: string) => {
|
|
619
|
+
spinner.text = msg;
|
|
620
|
+
},
|
|
621
|
+
debug: flags.verbose ? (msg: string) => {
|
|
622
|
+
spinner.text = msg;
|
|
623
|
+
} : () => {}
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
if (!result.success) {
|
|
628
|
+
failSpinner(spinner, 'Deployment failed');
|
|
629
|
+
logger.error(errorColor(result.error.message));
|
|
630
|
+
|
|
631
|
+
if (flags.verbose && result.error.cause) {
|
|
632
|
+
logger.error('\nCause:', result.error.cause);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Set exit code and return (finally block will handle cleanup and exit)
|
|
636
|
+
exitCode = 1;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
succeedSpinner(spinner, 'Deployment complete!');
|
|
641
|
+
|
|
642
|
+
// ----------------------------------------------------------------
|
|
643
|
+
// 9. Display results
|
|
644
|
+
// ----------------------------------------------------------------
|
|
645
|
+
const data = result.data;
|
|
646
|
+
|
|
647
|
+
if ('dryRun' in data && data.dryRun) {
|
|
648
|
+
// Dry run result
|
|
649
|
+
displayDryRunInfo(profileName, data.sdl);
|
|
650
|
+
|
|
651
|
+
// Set exit code (finally block will handle cleanup and exit)
|
|
652
|
+
exitCode = 0;
|
|
653
|
+
} else {
|
|
654
|
+
// Actual deployment result
|
|
655
|
+
const deploymentData = data as AkashDeploymentData;
|
|
656
|
+
|
|
657
|
+
// Fetch AKT price for USD display (fails gracefully)
|
|
658
|
+
const aktPriceUsd = await fetchAktPriceUSD();
|
|
659
|
+
|
|
660
|
+
if (flags.verbose) {
|
|
661
|
+
displayDeploymentInfo(deploymentData, aktPriceUsd);
|
|
662
|
+
} else {
|
|
663
|
+
displayDeploymentSummary(deploymentData, aktPriceUsd);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Show endpoints if available
|
|
667
|
+
if (deploymentData.endpoints && Object.keys(deploymentData.endpoints).length > 0) {
|
|
668
|
+
logger.log(successColor('\nService Endpoints:'));
|
|
669
|
+
for (const [serviceName, endpoint] of Object.entries(deploymentData.endpoints)) {
|
|
670
|
+
logger.log(` ${serviceName}: ${endpoint}`);
|
|
671
|
+
}
|
|
672
|
+
logger.log('');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ----------------------------------------------------------------
|
|
676
|
+
// 9.5. Write deployment lock file for `kadi deploy down`
|
|
677
|
+
// ----------------------------------------------------------------
|
|
678
|
+
try {
|
|
679
|
+
const lock = buildAkashLock(profileName, deploymentData, flags.label);
|
|
680
|
+
const instanceId = await writeLockFile(projectRoot, lock);
|
|
681
|
+
logger.log(dim(` Instance: ${instanceId} (profile: ${profileName}${flags.label ? `, label: ${flags.label}` : ''})`));
|
|
682
|
+
logger.log(dim(` Use \`kadi deploy down --instance ${instanceId}\` to tear down.`));
|
|
683
|
+
if (flags.verbose) {
|
|
684
|
+
logger.log(dim('Lock file written: .kadi-deploy.lock'));
|
|
685
|
+
}
|
|
686
|
+
} catch (lockErr) {
|
|
687
|
+
// Non-fatal: deployment succeeded even if lock write fails
|
|
688
|
+
logger.log(warning(`Could not write lock file: ${(lockErr as Error).message}`));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ----------------------------------------------------------------
|
|
692
|
+
// 10. Wait for secret request result (we started listening earlier)
|
|
693
|
+
// ----------------------------------------------------------------
|
|
694
|
+
if (secretsPromise && brokerUrl && secretsVault) {
|
|
695
|
+
logger.log(dim('\n─────────────────────────────────────────────────────────────────'));
|
|
696
|
+
logger.log(bold('Waiting for agent to request secrets...'));
|
|
697
|
+
logger.log(dim(`Broker: ${brokerUrl}`));
|
|
698
|
+
logger.log(dim('Press Ctrl+C to skip (agent will not receive secrets)'));
|
|
699
|
+
logger.log(dim('─────────────────────────────────────────────────────────────────\n'));
|
|
700
|
+
|
|
701
|
+
// Handle Ctrl+C during secret wait - force exit cleanly
|
|
702
|
+
const sigintHandler = () => {
|
|
703
|
+
logger.log(warning('\n\nSkipping secret sharing (Ctrl+C)'));
|
|
704
|
+
exitCode = 0;
|
|
705
|
+
process.exit(0);
|
|
706
|
+
};
|
|
707
|
+
process.on('SIGINT', sigintHandler);
|
|
708
|
+
|
|
709
|
+
const waitResult = await secretsPromise;
|
|
710
|
+
|
|
711
|
+
// Remove handler after secrets are done
|
|
712
|
+
process.off('SIGINT', sigintHandler);
|
|
713
|
+
|
|
714
|
+
if (waitResult.success && waitResult.request) {
|
|
715
|
+
const request = waitResult.request;
|
|
716
|
+
|
|
717
|
+
// Display request info
|
|
718
|
+
logger.log('');
|
|
719
|
+
logger.log(successColor('Agent connected and requesting secrets!'));
|
|
720
|
+
logger.log('');
|
|
721
|
+
logger.log(` Agent ID: ${request.agentId}`);
|
|
722
|
+
logger.log(` Required: ${request.required.join(', ')}`);
|
|
723
|
+
if (request.optional?.length) {
|
|
724
|
+
logger.log(` Optional: ${request.optional.join(', ')}`);
|
|
725
|
+
}
|
|
726
|
+
logger.log('');
|
|
727
|
+
|
|
728
|
+
// Prompt for approval (auto-approve with --yes or --auto-approve-secrets flag)
|
|
729
|
+
const approved = flags.yes || flags.autoApproveSecrets || await confirmPrompt('Share these secrets with the agent?', true);
|
|
730
|
+
|
|
731
|
+
if (!approved) {
|
|
732
|
+
logger.log(warning('\nSecret sharing declined.'));
|
|
733
|
+
// Notify agent so it can fail immediately instead of timing out
|
|
734
|
+
await sendRejection({
|
|
735
|
+
brokerUrl,
|
|
736
|
+
agentId: request.agentId,
|
|
737
|
+
reason: 'User declined to share secrets',
|
|
738
|
+
logger: {
|
|
739
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
740
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
} else {
|
|
744
|
+
const allRequestedKeys = [
|
|
745
|
+
...request.required,
|
|
746
|
+
...(request.optional || []),
|
|
747
|
+
];
|
|
748
|
+
|
|
749
|
+
// Multi-vault: read from each vault source
|
|
750
|
+
const secrets: Record<string, string> = {};
|
|
751
|
+
for (const source of secretsSources) {
|
|
752
|
+
const sourceKeys = [...source.required, ...source.optional]
|
|
753
|
+
.filter(k => allRequestedKeys.includes(k));
|
|
754
|
+
if (sourceKeys.length === 0) continue;
|
|
755
|
+
|
|
756
|
+
logger.log(dim(`\nReading secrets from vault '${source.vault}'...`));
|
|
757
|
+
const vaultSecrets = readSecretsFromCli({
|
|
758
|
+
keys: sourceKeys,
|
|
759
|
+
vault: source.vault,
|
|
760
|
+
cwd: projectRoot,
|
|
761
|
+
onError: (key, error) => {
|
|
762
|
+
logger.log(dim(` Failed to read '${key}' from vault '${source.vault}': ${error}`));
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
Object.assign(secrets, vaultSecrets);
|
|
766
|
+
}
|
|
767
|
+
const foundKeys = Object.keys(secrets);
|
|
768
|
+
|
|
769
|
+
const missingRequired = request.required.filter(
|
|
770
|
+
(key) => !secrets[key]
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
if (missingRequired.length > 0) {
|
|
774
|
+
const vaultNames = secretsSources.map(s => s.vault).join(', ');
|
|
775
|
+
logger.log(warning(`\nMissing required secrets in vault(s) '${vaultNames}': ${missingRequired.join(', ')}`));
|
|
776
|
+
logger.log(dim(`Use "kadi secret set <key> <value> -v <vault>" to add them.`));
|
|
777
|
+
} else if (foundKeys.length === 0) {
|
|
778
|
+
const vaultNames = secretsSources.map(s => s.vault).join(', ');
|
|
779
|
+
logger.log(warning(`\nNo secrets found in vault(s) '${vaultNames}'.`));
|
|
780
|
+
} else if (!request.publicKey) {
|
|
781
|
+
logger.log(warning('\nAgent did not provide public key for encryption.'));
|
|
782
|
+
} else {
|
|
783
|
+
logger.log(successColor(`\nSharing ${foundKeys.length} secret(s) with agent (encrypted)...`));
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
await shareSecrets({
|
|
787
|
+
brokerUrl,
|
|
788
|
+
agentId: request.agentId,
|
|
789
|
+
agentPublicKey: request.publicKey,
|
|
790
|
+
secrets,
|
|
791
|
+
logger: {
|
|
792
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
793
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
logger.log(successColor('Secrets shared successfully!'));
|
|
798
|
+
logger.log(dim(` Shared: ${foundKeys.join(', ')}`));
|
|
799
|
+
} catch (shareErr) {
|
|
800
|
+
logger.log(warning(`\nFailed to share secrets: ${(shareErr as Error).message}`));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} else if (waitResult.timedOut) {
|
|
805
|
+
logger.log(warning('\nTimeout waiting for agent to request secrets.'));
|
|
806
|
+
} else if (waitResult.error) {
|
|
807
|
+
logger.log(warning(`\nSecret handshake failed: ${waitResult.error}`));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
logger.log('');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Set exit code (finally block will handle cleanup and exit)
|
|
814
|
+
exitCode = 0;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
} catch (err) {
|
|
818
|
+
failSpinner(spinner, 'Deployment failed');
|
|
819
|
+
logger.error(errorColor((err as Error).message));
|
|
820
|
+
|
|
821
|
+
if (flags.verbose) {
|
|
822
|
+
console.error(err);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Set exit code (finally block will handle cleanup and exit)
|
|
826
|
+
exitCode = 1;
|
|
827
|
+
}
|
|
828
|
+
} finally {
|
|
829
|
+
// ----------------------------------------------------------------
|
|
830
|
+
// Guaranteed cleanup: wallet, akash client, and registry
|
|
831
|
+
// ----------------------------------------------------------------
|
|
832
|
+
// This block runs on ALL exit paths: success, error, and early returns
|
|
833
|
+
// Ensures Node.js event loop can exit by closing all open connections
|
|
834
|
+
|
|
835
|
+
// 1. Disconnect Akash client (closes RPC connection and SDK resources)
|
|
836
|
+
if (akashClient) {
|
|
837
|
+
try {
|
|
838
|
+
await akashClient.disconnect();
|
|
839
|
+
} catch (err) {
|
|
840
|
+
// Silently ignore cleanup errors
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// 2. Disconnect wallet (closes WalletConnect session)
|
|
845
|
+
if (walletResult?.success) {
|
|
846
|
+
try {
|
|
847
|
+
await disconnectWallet(walletResult.data);
|
|
848
|
+
} catch (err) {
|
|
849
|
+
// Silently ignore cleanup errors
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// 3. Cleanup registry (if it was started)
|
|
854
|
+
// Note: This is idempotent, safe to call multiple times
|
|
855
|
+
try {
|
|
856
|
+
await registryContext.cleanup();
|
|
857
|
+
} catch (err) {
|
|
858
|
+
// Silently ignore cleanup errors
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Force process exit after cleanup completes
|
|
862
|
+
// This is necessary because phantom handles in Node.js keep the event loop alive
|
|
863
|
+
// Even after all real resources are cleaned up, stale handle references remain
|
|
864
|
+
if (exitCode !== null) {
|
|
865
|
+
process.exit(exitCode);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// =============================================================================
|
|
871
|
+
// Autonomous Deployment
|
|
872
|
+
// =============================================================================
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Options passed to the autonomous deployment function from the main flow
|
|
876
|
+
*/
|
|
877
|
+
interface AutonomousDeploymentParams {
|
|
878
|
+
akashProfile: any;
|
|
879
|
+
profileName: string;
|
|
880
|
+
network: string;
|
|
881
|
+
loadedProfile: any;
|
|
882
|
+
registryContext: any;
|
|
883
|
+
hasSecrets: boolean;
|
|
884
|
+
deployNonce?: string;
|
|
885
|
+
brokerUrl: string | null;
|
|
886
|
+
secretsVault?: string;
|
|
887
|
+
secretsSources: NormalizedVaultSource[];
|
|
888
|
+
agentConfig: any;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Execute a fully autonomous Akash deployment with no human interaction.
|
|
893
|
+
*
|
|
894
|
+
* This function:
|
|
895
|
+
* 1. Creates a SecretsProvider from the local vault (or uses a simple mnemonic provider)
|
|
896
|
+
* 2. Calls deployToAkash() in autonomous mode (wallet from vault, auto-cert, algorithmic bid selection)
|
|
897
|
+
* 3. Auto-approves and performs secret handshake if profile declares secrets
|
|
898
|
+
* 4. Displays results and exits
|
|
899
|
+
*
|
|
900
|
+
* No prompts, no QR codes, no confirmation dialogs. Pure automation.
|
|
901
|
+
*
|
|
902
|
+
* @param ctx - Deployment context from CLI
|
|
903
|
+
* @param params - Pre-resolved profile and config from main flow
|
|
904
|
+
*/
|
|
905
|
+
async function executeAutonomousDeployment(
|
|
906
|
+
ctx: DeploymentContext,
|
|
907
|
+
params: AutonomousDeploymentParams
|
|
908
|
+
): Promise<void> {
|
|
909
|
+
const { logger, projectRoot, flags } = ctx;
|
|
910
|
+
const {
|
|
911
|
+
akashProfile,
|
|
912
|
+
profileName,
|
|
913
|
+
network,
|
|
914
|
+
loadedProfile,
|
|
915
|
+
registryContext,
|
|
916
|
+
hasSecrets,
|
|
917
|
+
deployNonce,
|
|
918
|
+
brokerUrl,
|
|
919
|
+
secretsVault,
|
|
920
|
+
secretsSources,
|
|
921
|
+
agentConfig,
|
|
922
|
+
} = params;
|
|
923
|
+
|
|
924
|
+
let exitCode: number | null = null;
|
|
925
|
+
|
|
926
|
+
logger.log('');
|
|
927
|
+
logger.log(bold('🤖 AUTONOMOUS DEPLOYMENT MODE'));
|
|
928
|
+
logger.log(dim(' No human interaction required — fully automated'));
|
|
929
|
+
logger.log('');
|
|
930
|
+
|
|
931
|
+
// ----------------------------------------------------------------
|
|
932
|
+
// 1. Build SecretsProvider for wallet credentials
|
|
933
|
+
// Uses deploy-ability's createSecretsProvider with secret-ability vault
|
|
934
|
+
// ----------------------------------------------------------------
|
|
935
|
+
const bidStrategy = (flags.bidStrategy as 'cheapest' | 'most-reliable' | 'balanced') || 'cheapest';
|
|
936
|
+
|
|
937
|
+
logger.log(dim(`Bid strategy: ${bidStrategy}`));
|
|
938
|
+
if (flags.bidMaxPrice) {
|
|
939
|
+
logger.log(dim(`Max bid price: ${flags.bidMaxPrice} uAKT/block`));
|
|
940
|
+
}
|
|
941
|
+
if (flags.requireAudited) {
|
|
942
|
+
logger.log(dim(`Requiring audited providers only`));
|
|
943
|
+
}
|
|
944
|
+
logger.log('');
|
|
945
|
+
|
|
946
|
+
// Build bid filter from CLI flags
|
|
947
|
+
const bidFilter: Record<string, unknown> = {};
|
|
948
|
+
if (flags.bidMaxPrice) {
|
|
949
|
+
bidFilter.maxPricePerBlock = flags.bidMaxPrice;
|
|
950
|
+
}
|
|
951
|
+
if (flags.requireAudited) {
|
|
952
|
+
bidFilter.requireAudited = true;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ----------------------------------------------------------------
|
|
956
|
+
// 2. Start listening for secret requests early (same as interactive mode)
|
|
957
|
+
// ----------------------------------------------------------------
|
|
958
|
+
let secretsPromise: Promise<WaitForSecretsResult> | null = null;
|
|
959
|
+
|
|
960
|
+
if (hasSecrets && deployNonce && brokerUrl && !flags.dryRun) {
|
|
961
|
+
logger.log(dim(`Connecting to broker for automated secret sharing...`));
|
|
962
|
+
|
|
963
|
+
secretsPromise = waitForSecretRequest({
|
|
964
|
+
brokerUrl,
|
|
965
|
+
expectedNonce: deployNonce,
|
|
966
|
+
timeout: flags.secretTimeout || 5 * 60 * 1000,
|
|
967
|
+
logger: {
|
|
968
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
969
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// Wait for broker subscription before starting deployment
|
|
974
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ----------------------------------------------------------------
|
|
978
|
+
// 3. Deploy using deploy-ability in autonomous mode
|
|
979
|
+
// ----------------------------------------------------------------
|
|
980
|
+
let spinner = startSpinner('Starting autonomous deployment...');
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
// Dry run path
|
|
984
|
+
if (flags.dryRun) {
|
|
985
|
+
spinner.text = 'Generating deployment preview...';
|
|
986
|
+
|
|
987
|
+
const result = await deployToAkash({
|
|
988
|
+
projectRoot,
|
|
989
|
+
profile: profileName,
|
|
990
|
+
loadedProfile,
|
|
991
|
+
network: network as 'mainnet' | 'testnet',
|
|
992
|
+
dryRun: true,
|
|
993
|
+
verbose: flags.verbose,
|
|
994
|
+
logger: {
|
|
995
|
+
log: (msg: string) => { spinner.text = msg; },
|
|
996
|
+
error: (msg: string) => logger.error(msg),
|
|
997
|
+
warn: (msg: string) => { spinner.text = msg; },
|
|
998
|
+
debug: flags.verbose ? (msg: string) => { spinner.text = msg; } : () => {},
|
|
999
|
+
},
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
if (!result.success) {
|
|
1003
|
+
failSpinner(spinner, 'Dry run failed');
|
|
1004
|
+
logger.error(errorColor(result.error.message));
|
|
1005
|
+
exitCode = 1;
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
succeedSpinner(spinner, 'Dry run complete');
|
|
1010
|
+
const data = result.data as AkashDryRunData;
|
|
1011
|
+
displayDryRunInfo(profileName, data.sdl);
|
|
1012
|
+
exitCode = 0;
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Build the SecretsProvider for wallet credentials (mnemonic + certificate)
|
|
1017
|
+
// The wallet vault defaults to 'global' — it's separate from the deployment
|
|
1018
|
+
// secrets vault (profile.secrets.vault) which holds app secrets like DB_PASSWORD.
|
|
1019
|
+
// --secrets-vault overrides ONLY the wallet vault, not deployment secrets.
|
|
1020
|
+
spinner.text = 'Setting up wallet from secrets vault...';
|
|
1021
|
+
|
|
1022
|
+
const walletVault = flags.secretsVault || 'global';
|
|
1023
|
+
const secretsProvider = createCliSecretsProvider(walletVault, projectRoot);
|
|
1024
|
+
|
|
1025
|
+
// Full autonomous deployment
|
|
1026
|
+
const autonomousConfig: AutonomousDeploymentConfig = {
|
|
1027
|
+
secrets: secretsProvider,
|
|
1028
|
+
bidStrategy,
|
|
1029
|
+
bidFilter: Object.keys(bidFilter).length > 0 ? bidFilter as BidFilterCriteria : undefined,
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
const result = await deployToAkash({
|
|
1033
|
+
projectRoot,
|
|
1034
|
+
profile: profileName,
|
|
1035
|
+
loadedProfile,
|
|
1036
|
+
network: network as 'mainnet' | 'testnet',
|
|
1037
|
+
dryRun: false,
|
|
1038
|
+
verbose: flags.verbose,
|
|
1039
|
+
blacklist: akashProfile.blacklist as readonly string[] | undefined,
|
|
1040
|
+
containerTimeout: 600000, // 10 minutes
|
|
1041
|
+
|
|
1042
|
+
// Autonomous mode config — deploy-ability handles everything
|
|
1043
|
+
autonomous: autonomousConfig,
|
|
1044
|
+
|
|
1045
|
+
// Logger that updates spinner
|
|
1046
|
+
// warn/debug persist-print so diagnostic info isn't lost
|
|
1047
|
+
logger: {
|
|
1048
|
+
log: (msg: string) => { spinner.text = msg; },
|
|
1049
|
+
error: (msg: string) => logger.error(msg),
|
|
1050
|
+
warn: (msg: string) => {
|
|
1051
|
+
const saved = spinner.text;
|
|
1052
|
+
spinner.stop();
|
|
1053
|
+
logger.log(warning(msg));
|
|
1054
|
+
spinner.start(saved);
|
|
1055
|
+
},
|
|
1056
|
+
debug: flags.verbose ? (msg: string) => {
|
|
1057
|
+
const saved = spinner.text;
|
|
1058
|
+
spinner.stop();
|
|
1059
|
+
logger.log(dim(msg));
|
|
1060
|
+
spinner.start(saved);
|
|
1061
|
+
} : () => {},
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
if (!result.success) {
|
|
1066
|
+
failSpinner(spinner, 'Autonomous deployment failed');
|
|
1067
|
+
logger.error(errorColor(result.error.message));
|
|
1068
|
+
|
|
1069
|
+
if (flags.verbose && result.error.cause) {
|
|
1070
|
+
logger.error('\nCause:', result.error.cause);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Provide helpful hints for common autonomous errors
|
|
1074
|
+
const errorCode = (result.error as any).code;
|
|
1075
|
+
if (errorCode === 'SECRETS_ERROR') {
|
|
1076
|
+
logger.log('');
|
|
1077
|
+
logger.log(warning('Hint: Make sure your wallet mnemonic is stored in the vault:'));
|
|
1078
|
+
logger.log(dim(` kadi secrets set --vault ${walletVault} --key AKASH_WALLET --value "your mnemonic"`));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
exitCode = 1;
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
succeedSpinner(spinner, 'Autonomous deployment complete!');
|
|
1086
|
+
|
|
1087
|
+
// ----------------------------------------------------------------
|
|
1088
|
+
// 4. Display results
|
|
1089
|
+
// ----------------------------------------------------------------
|
|
1090
|
+
const data = result.data;
|
|
1091
|
+
|
|
1092
|
+
if ('dryRun' in data && data.dryRun) {
|
|
1093
|
+
displayDryRunInfo(profileName, (data as AkashDryRunData).sdl);
|
|
1094
|
+
exitCode = 0;
|
|
1095
|
+
} else {
|
|
1096
|
+
const deploymentData = data as AkashDeploymentData;
|
|
1097
|
+
|
|
1098
|
+
// Fetch AKT price for USD display
|
|
1099
|
+
const aktPriceUsd = await fetchAktPriceUSD();
|
|
1100
|
+
|
|
1101
|
+
if (flags.verbose) {
|
|
1102
|
+
displayDeploymentInfo(deploymentData, aktPriceUsd);
|
|
1103
|
+
} else {
|
|
1104
|
+
displayDeploymentSummary(deploymentData, aktPriceUsd);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Show endpoints
|
|
1108
|
+
if (deploymentData.endpoints && Object.keys(deploymentData.endpoints).length > 0) {
|
|
1109
|
+
logger.log(successColor('\nService Endpoints:'));
|
|
1110
|
+
for (const [serviceName, endpoint] of Object.entries(deploymentData.endpoints)) {
|
|
1111
|
+
logger.log(` ${serviceName}: ${endpoint}`);
|
|
1112
|
+
}
|
|
1113
|
+
logger.log('');
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ----------------------------------------------------------------
|
|
1117
|
+
// 4.5. Write deployment lock file for `kadi deploy down`
|
|
1118
|
+
// ----------------------------------------------------------------
|
|
1119
|
+
try {
|
|
1120
|
+
const lock = buildAkashLock(profileName, deploymentData, flags.label);
|
|
1121
|
+
const instanceId = await writeLockFile(projectRoot, lock);
|
|
1122
|
+
logger.log(dim(` Instance: ${instanceId} (profile: ${profileName}${flags.label ? `, label: ${flags.label}` : ''})`));
|
|
1123
|
+
logger.log(dim(` Use \`kadi deploy down --instance ${instanceId}\` to tear down.`));
|
|
1124
|
+
if (flags.verbose) {
|
|
1125
|
+
logger.log(dim('Lock file written: .kadi-deploy.lock'));
|
|
1126
|
+
}
|
|
1127
|
+
} catch (lockErr) {
|
|
1128
|
+
// Non-fatal: deployment succeeded even if lock write fails
|
|
1129
|
+
logger.log(warning(`Could not write lock file: ${(lockErr as Error).message}`));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ----------------------------------------------------------------
|
|
1133
|
+
// 5. Automated secret sharing (no prompts)
|
|
1134
|
+
// ----------------------------------------------------------------
|
|
1135
|
+
if (secretsPromise && brokerUrl && secretsVault) {
|
|
1136
|
+
logger.log(dim('\n─────────────────────────────────────────────────────────────────'));
|
|
1137
|
+
logger.log(bold('Automated secret sharing in progress...'));
|
|
1138
|
+
logger.log(dim(`Broker: ${brokerUrl}`));
|
|
1139
|
+
logger.log(dim('─────────────────────────────────────────────────────────────────\n'));
|
|
1140
|
+
|
|
1141
|
+
const waitResult = await secretsPromise;
|
|
1142
|
+
|
|
1143
|
+
if (waitResult.success && waitResult.request) {
|
|
1144
|
+
const request = waitResult.request;
|
|
1145
|
+
|
|
1146
|
+
logger.log(successColor('Agent connected and requesting secrets.'));
|
|
1147
|
+
logger.log(dim(` Agent ID: ${request.agentId}`));
|
|
1148
|
+
logger.log(dim(` Required: ${request.required.join(', ')}`));
|
|
1149
|
+
if (request.optional?.length) {
|
|
1150
|
+
logger.log(dim(` Optional: ${request.optional.join(', ')}`));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Auto-approve: no prompting in autonomous mode
|
|
1154
|
+
logger.log(successColor('\nAuto-approving secret sharing (autonomous mode)...'));
|
|
1155
|
+
|
|
1156
|
+
const allRequestedKeys = [
|
|
1157
|
+
...request.required,
|
|
1158
|
+
...(request.optional || []),
|
|
1159
|
+
];
|
|
1160
|
+
|
|
1161
|
+
// Multi-vault: read from each vault source
|
|
1162
|
+
const secrets: Record<string, string> = {};
|
|
1163
|
+
for (const source of secretsSources) {
|
|
1164
|
+
const sourceKeys = [...source.required, ...source.optional]
|
|
1165
|
+
.filter(k => allRequestedKeys.includes(k));
|
|
1166
|
+
if (sourceKeys.length === 0) continue;
|
|
1167
|
+
|
|
1168
|
+
logger.log(dim(`Reading secrets from vault '${source.vault}'...`));
|
|
1169
|
+
const vaultSecrets = readSecretsFromCli({
|
|
1170
|
+
keys: sourceKeys,
|
|
1171
|
+
vault: source.vault,
|
|
1172
|
+
cwd: projectRoot,
|
|
1173
|
+
onError: (key, error) => {
|
|
1174
|
+
logger.log(dim(` Failed to read '${key}' from vault '${source.vault}': ${error}`));
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
Object.assign(secrets, vaultSecrets);
|
|
1178
|
+
}
|
|
1179
|
+
const foundKeys = Object.keys(secrets);
|
|
1180
|
+
|
|
1181
|
+
const missingRequired = request.required.filter(
|
|
1182
|
+
(key) => !secrets[key]
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
if (missingRequired.length > 0) {
|
|
1186
|
+
const vaultNames = secretsSources.map(s => s.vault).join(', ');
|
|
1187
|
+
logger.log(warning(`Missing required secrets in vault(s) '${vaultNames}': ${missingRequired.join(', ')}`));
|
|
1188
|
+
logger.log(dim(`Use "kadi secret set <key> <value> -v <vault>" to add them.`));
|
|
1189
|
+
} else if (foundKeys.length === 0) {
|
|
1190
|
+
const vaultNames = secretsSources.map(s => s.vault).join(', ');
|
|
1191
|
+
logger.log(warning(`No secrets found in vault(s) '${vaultNames}'.`));
|
|
1192
|
+
} else if (!request.publicKey) {
|
|
1193
|
+
logger.log(warning('Agent did not provide public key for encryption.'));
|
|
1194
|
+
} else {
|
|
1195
|
+
logger.log(successColor(`Sharing ${foundKeys.length} secret(s) with agent (encrypted)...`));
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
await shareSecrets({
|
|
1199
|
+
brokerUrl,
|
|
1200
|
+
agentId: request.agentId,
|
|
1201
|
+
agentPublicKey: request.publicKey,
|
|
1202
|
+
secrets,
|
|
1203
|
+
logger: {
|
|
1204
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
1205
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
logger.log(successColor('✓ Secrets shared successfully!'));
|
|
1210
|
+
logger.log(dim(` Shared: ${foundKeys.join(', ')}`));
|
|
1211
|
+
} catch (shareErr) {
|
|
1212
|
+
logger.log(warning(`Failed to share secrets: ${(shareErr as Error).message}`));
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
} else if (waitResult.timedOut) {
|
|
1216
|
+
logger.log(warning('Timeout waiting for agent to request secrets.'));
|
|
1217
|
+
logger.log(dim('The agent may not have started yet, or may not require secrets.'));
|
|
1218
|
+
} else if (waitResult.error) {
|
|
1219
|
+
logger.log(warning(`Secret handshake failed: ${waitResult.error}`));
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
logger.log('');
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
exitCode = 0;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
failSpinner(spinner, 'Autonomous deployment failed');
|
|
1230
|
+
logger.error(errorColor((err as Error).message));
|
|
1231
|
+
|
|
1232
|
+
if (flags.verbose) {
|
|
1233
|
+
console.error(err);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
exitCode = 1;
|
|
1237
|
+
} finally {
|
|
1238
|
+
// Cleanup registry (if it was started)
|
|
1239
|
+
try {
|
|
1240
|
+
await registryContext.cleanup();
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
// Silently ignore cleanup errors
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (exitCode !== null) {
|
|
1246
|
+
process.exit(exitCode);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// =============================================================================
|
|
1252
|
+
// CLI Secrets Provider Shim
|
|
1253
|
+
// =============================================================================
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Create a SecretsProvider that reads from the local kadi-secret CLI vault.
|
|
1257
|
+
*
|
|
1258
|
+
* This bridges deploy-ability's SecretsProvider interface with kadi-deploy's
|
|
1259
|
+
* existing CLI-based secret vault access. It reads the wallet mnemonic from
|
|
1260
|
+
* the specified vault using `kadi secret get` commands.
|
|
1261
|
+
*
|
|
1262
|
+
* For certificate caching, it stores certificates in the vault alongside the mnemonic.
|
|
1263
|
+
*
|
|
1264
|
+
* @param vaultName - Name of the vault to read from (e.g., 'global')
|
|
1265
|
+
* @param cwd - Working directory for CLI execution
|
|
1266
|
+
* @returns SecretsProvider compatible with deploy-ability's autonomous mode
|
|
1267
|
+
*/
|
|
1268
|
+
function createCliSecretsProvider(vaultName: string, cwd: string): SecretsProvider {
|
|
1269
|
+
return {
|
|
1270
|
+
async getMnemonic(): Promise<string> {
|
|
1271
|
+
// Read the wallet mnemonic from the local kadi-secret vault
|
|
1272
|
+
const mnemonic = readSecretFromCli({
|
|
1273
|
+
key: 'AKASH_WALLET',
|
|
1274
|
+
vault: vaultName,
|
|
1275
|
+
cwd,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
if (!mnemonic) {
|
|
1279
|
+
throw new Error(
|
|
1280
|
+
`Wallet mnemonic not found in vault '${vaultName}' (key: AKASH_WALLET).\n\n` +
|
|
1281
|
+
`Store your mnemonic with:\n` +
|
|
1282
|
+
` kadi secret set AKASH_WALLET "your 12 or 24 word mnemonic" -v ${vaultName}\n\n` +
|
|
1283
|
+
`Or create the vault first:\n` +
|
|
1284
|
+
` kadi secret create ${vaultName}`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return mnemonic;
|
|
1289
|
+
},
|
|
1290
|
+
|
|
1291
|
+
async getCertificate(): Promise<any | null> {
|
|
1292
|
+
// 1. Try the default filesystem path (where interactive mode saves it)
|
|
1293
|
+
const defaultCertPath = path.join(os.homedir(), '.kadi', 'certificate.json');
|
|
1294
|
+
try {
|
|
1295
|
+
const certData = readFileSync(defaultCertPath, 'utf-8');
|
|
1296
|
+
const cert = JSON.parse(certData);
|
|
1297
|
+
if (cert && cert.cert && cert.privateKey && cert.publicKey) {
|
|
1298
|
+
return cert;
|
|
1299
|
+
}
|
|
1300
|
+
} catch {
|
|
1301
|
+
// File doesn't exist or isn't valid JSON — fall through
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// 2. Try to read cached certificate from secrets vault
|
|
1305
|
+
try {
|
|
1306
|
+
const certJson = readSecretFromCli({
|
|
1307
|
+
key: 'akash-tls-certificate',
|
|
1308
|
+
vault: vaultName,
|
|
1309
|
+
cwd,
|
|
1310
|
+
});
|
|
1311
|
+
if (certJson) {
|
|
1312
|
+
const cert = JSON.parse(certJson);
|
|
1313
|
+
if (cert) {
|
|
1314
|
+
return cert;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
} catch {
|
|
1318
|
+
// Vault read failed — fall through
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
return null;
|
|
1322
|
+
},
|
|
1323
|
+
|
|
1324
|
+
async storeCertificate(cert: any): Promise<void> {
|
|
1325
|
+
// Store certificate in vault for future use
|
|
1326
|
+
// Use execSync to call kadi secret set
|
|
1327
|
+
try {
|
|
1328
|
+
const { execSync } = await import('node:child_process');
|
|
1329
|
+
execSync(
|
|
1330
|
+
`kadi secret set akash-tls-certificate '${JSON.stringify(cert).replace(/'/g, "\\'")}' -v "${vaultName}"`,
|
|
1331
|
+
{
|
|
1332
|
+
encoding: 'utf-8',
|
|
1333
|
+
cwd,
|
|
1334
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1335
|
+
}
|
|
1336
|
+
);
|
|
1337
|
+
} catch {
|
|
1338
|
+
// Non-fatal: certificate is already on blockchain
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
}
|