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.
Files changed (52) hide show
  1. package/.env.example +6 -0
  2. package/.prettierrc +6 -0
  3. package/README.md +589 -0
  4. package/agent.json +23 -0
  5. package/index.js +11 -0
  6. package/package.json +42 -0
  7. package/quick-command.txt +92 -0
  8. package/scripts/preflight.js +458 -0
  9. package/scripts/preflight.sh +300 -0
  10. package/src/cli/bid-selector.ts +222 -0
  11. package/src/cli/colors.ts +216 -0
  12. package/src/cli/index.ts +11 -0
  13. package/src/cli/prompts.ts +190 -0
  14. package/src/cli/spinners.ts +165 -0
  15. package/src/commands/deploy-local.ts +475 -0
  16. package/src/commands/deploy.ts +1342 -0
  17. package/src/commands/down.ts +679 -0
  18. package/src/commands/index.ts +10 -0
  19. package/src/commands/lock.ts +571 -0
  20. package/src/config/agent-loader.ts +177 -0
  21. package/src/config/index.ts +9 -0
  22. package/src/display/deployment-info.ts +220 -0
  23. package/src/display/pricing.ts +137 -0
  24. package/src/display/resources.ts +234 -0
  25. package/src/enhanced-registry-manager.ts +892 -0
  26. package/src/index.ts +307 -0
  27. package/src/infrastructure/registry.ts +269 -0
  28. package/src/schemas/profiles.ts +529 -0
  29. package/src/secrets/broker-urls.ts +109 -0
  30. package/src/secrets/handshake.ts +407 -0
  31. package/src/secrets/index.ts +69 -0
  32. package/src/secrets/inject-env.ts +171 -0
  33. package/src/secrets/nonce.ts +31 -0
  34. package/src/secrets/normalize.ts +204 -0
  35. package/src/secrets/prepare.ts +152 -0
  36. package/src/secrets/validate.ts +243 -0
  37. package/src/secrets/vault.ts +80 -0
  38. package/src/types/akash.ts +116 -0
  39. package/src/types/container-registry-ability.d.ts +158 -0
  40. package/src/types/external.ts +49 -0
  41. package/src/types.ts +211 -0
  42. package/src/utils/akt-price.ts +74 -0
  43. package/tests/agent-loader.test.ts +239 -0
  44. package/tests/autonomous.test.ts +244 -0
  45. package/tests/down.test.ts +1143 -0
  46. package/tests/lock.test.ts +1148 -0
  47. package/tests/nonce.test.ts +34 -0
  48. package/tests/normalize.test.ts +270 -0
  49. package/tests/secrets-schema.test.ts +301 -0
  50. package/tests/types.test.ts +198 -0
  51. package/tsconfig.json +18 -0
  52. package/vitest.config.ts +9 -0
package/src/index.ts ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * KADI Deploy - Main Entry Point
3
+ *
4
+ * Profile-based deployment system for KADI agents using deploy-ability library.
5
+ * Supports deployment to Akash Network and local Docker/Podman.
6
+ *
7
+ * This implementation uses deploy-ability as a library for all deployment operations.
8
+ *
9
+ * @module index
10
+ */
11
+
12
+ import path from 'node:path';
13
+ import type { IKadiContext, DeployOptions, ResolvedDeployOptions, DeploymentContext } from './types.js';
14
+ import { loadAgentConfig, getFirstProfile, hasProfile, getProfile } from './config/agent-loader.js';
15
+ import { executeAkashDeployment } from './commands/deploy.js';
16
+ import { executeLocalDeployment } from './commands/deploy-local.js';
17
+ import { executeDown } from './commands/down.js';
18
+ import { validateSecretsOrFail } from './secrets/index.js';
19
+
20
+ /**
21
+ * Register the "deploy" command with the Kadi CLI
22
+ *
23
+ * Uses Commander subcommands:
24
+ * kadi deploy [up] — Deploy agents (default, backward compatible)
25
+ * kadi deploy down — Tear down an active deployment
26
+ *
27
+ * @param ctx - Plugin context injected by the Kadi CLI
28
+ */
29
+ export default function registerDeploy(ctx: IKadiContext) {
30
+ const { commander, logger } = ctx;
31
+
32
+ // Parent command — `kadi deploy`
33
+ const deploy = commander
34
+ .command('deploy')
35
+ .alias('d')
36
+ .description(
37
+ 'Deploy agents to Akash Network or locally, or tear them down'
38
+ );
39
+
40
+ // ─────────────────────────────────────────────────────────────────
41
+ // Subcommand: deploy up (default — runs when bare `kadi deploy` is used)
42
+ // ─────────────────────────────────────────────────────────────────
43
+ deploy
44
+ .command('up', { isDefault: true })
45
+ .description(
46
+ 'Deploy agents to Akash Network or locally using profiles from agent.json'
47
+ )
48
+
49
+ // Profile selection
50
+ .option(
51
+ '--profile <profile>',
52
+ 'Deploy profile to use from agent.json (if not specified, uses first available profile)'
53
+ )
54
+
55
+ // Project configuration
56
+ .option(
57
+ '-p, --project <path>',
58
+ 'Path to folder containing agent.json'
59
+ )
60
+
61
+ // Deployment options
62
+ .option('--dry-run', "Preview deployment without executing")
63
+ .option('--yes, -y', 'Skip confirmation prompts')
64
+ .option('--verbose, -v', 'Verbose logging')
65
+
66
+ // Network/engine overrides
67
+ .option('--network <network>', 'Akash network (mainnet|testnet)')
68
+ .option('--engine <engine>', 'Container engine (docker|podman)')
69
+
70
+ // Certificate (Akash only)
71
+ .option('--cert <path>', 'Path to existing TLS certificate')
72
+
73
+ // Autonomous deployment (agent-controlled, no human interaction)
74
+ .option('--autonomous', 'Fully autonomous deployment with no human interaction (uses secrets vault for wallet)')
75
+ .option('--bid-strategy <strategy>', 'Bid selection strategy for autonomous mode (cheapest|most-reliable|balanced)', 'cheapest')
76
+ .option('--bid-max-price <uakt>', 'Maximum bid price per block in uAKT (autonomous mode)')
77
+ .option('--require-audited', 'Only accept bids from audited providers (autonomous mode)')
78
+ .option('--secrets-vault <vault>', 'Vault for wallet mnemonic (default: "global"). Deployment secrets always use the profile\'s secrets.vault')
79
+ .option('--auto-approve-secrets', 'Auto-approve secret sharing without prompting')
80
+ .option('--secret-timeout <ms>', 'Timeout (ms) for waiting for agent to request secrets')
81
+ .option('--label <label>', 'Human-readable label for this deployment instance (e.g. "broker-east")')
82
+
83
+ // Help text
84
+ .addHelpText(
85
+ 'after',
86
+ `
87
+ Profile Examples:
88
+ kadi deploy # Use first available profile
89
+ kadi deploy --profile production # Use production profile
90
+ kadi deploy --profile akash-mainnet # Deploy to Akash mainnet
91
+ kadi deploy --profile local-dev # Deploy locally
92
+
93
+ Autonomous Deployment (no human interaction):
94
+ kadi deploy --autonomous # Auto-deploy using secrets vault
95
+ kadi deploy --autonomous --bid-strategy most-reliable
96
+ kadi deploy --autonomous --bid-max-price 1000 --require-audited
97
+ kadi deploy --autonomous --auto-approve-secrets
98
+
99
+ Teardown:
100
+ kadi deploy down # Tear down the active deployment
101
+ kadi deploy down --autonomous # Fully non-interactive (skips confirmation + QR)
102
+ kadi deploy down --autonomous --profile prod # Required when multiple deployments active
103
+ kadi deploy down --yes # Skip confirmation (interactive mode)
104
+
105
+ Configuration:
106
+ Profiles are defined in agent.json under the "deploy" field.
107
+ CLI flags override profile settings.
108
+
109
+ For autonomous mode, store your wallet mnemonic in the secrets vault:
110
+ kadi secrets set --vault global --key AKASH_WALLET --value "your mnemonic"
111
+
112
+ Supported Targets:
113
+ • akash - Deploy to Akash Network (decentralized cloud)
114
+ • local - Deploy to local Docker/Podman
115
+ `
116
+ )
117
+
118
+ .action(async (...args: unknown[]) => {
119
+ // Commander passes flags as first argument
120
+ const flags = args[0] as Record<string, unknown>;
121
+
122
+ // Set default project path if not provided
123
+ const deployFlags = {
124
+ ...flags,
125
+ project: (flags.project as string) || process.cwd(),
126
+ bidMaxPrice: flags.bidMaxPrice ? parseInt(flags.bidMaxPrice as string, 10) : undefined,
127
+ secretTimeout: flags.secretTimeout ? parseInt(flags.secretTimeout as string, 10) : undefined,
128
+ } as DeployOptions;
129
+
130
+ try {
131
+ await deployWithDeployAbility(ctx, deployFlags);
132
+ } catch (error) {
133
+ const errorMessage =
134
+ error instanceof Error ? error.message : String(error);
135
+ logger.error(`Deploy failed: ${errorMessage}`);
136
+
137
+ if (deployFlags.verbose) {
138
+ logger.error('Full error details:');
139
+ console.error(error);
140
+ }
141
+
142
+ process.exitCode = 1;
143
+ }
144
+ });
145
+
146
+ // ─────────────────────────────────────────────────────────────────
147
+ // Subcommand: deploy down — tear down an active deployment
148
+ // ─────────────────────────────────────────────────────────────────
149
+ deploy
150
+ .command('down')
151
+ .description(
152
+ 'Tear down an active deployment (reads .kadi-deploy.lock)'
153
+ )
154
+ .option(
155
+ '-p, --project <path>',
156
+ 'Path to project with agent.json / .kadi-deploy.lock'
157
+ )
158
+ .option('--profile <profile>', 'Profile name to tear down (prompts if multiple active)')
159
+ .option('--instance <id>', 'Instance ID to tear down (4-char hex from deploy output)')
160
+ .option('--all', 'Tear down all active deployments')
161
+ .option('--engine <engine>', 'Override container engine (docker|podman)')
162
+ .option('-y, --yes', 'Skip confirmation prompt')
163
+ .option('--verbose, -v', 'Verbose output')
164
+ .option('--autonomous', 'Autonomous Akash teardown (wallet from secrets vault, no QR)')
165
+ .option('--secrets-vault <vault>', 'Vault name for wallet mnemonic (default: "global")')
166
+ .option('--network <network>', 'Override Akash network (mainnet|testnet)')
167
+ .action(async (...args: unknown[]) => {
168
+ const flags = args[0] as Record<string, unknown>;
169
+
170
+ try {
171
+ await executeDown(ctx, {
172
+ project: (flags.project as string) || process.cwd(),
173
+ profile: flags.profile as string | undefined,
174
+ instance: flags.instance as string | undefined,
175
+ all: flags.all as boolean | undefined,
176
+ engine: flags.engine as string | undefined,
177
+ yes: flags.yes as boolean | undefined,
178
+ verbose: flags.verbose as boolean | undefined,
179
+ autonomous: flags.autonomous as boolean | undefined,
180
+ secretsVault: flags.secretsVault as string | undefined,
181
+ network: flags.network as string | undefined,
182
+ });
183
+ } catch (error) {
184
+ const errorMessage =
185
+ error instanceof Error ? error.message : String(error);
186
+ logger.error(`Teardown failed: ${errorMessage}`);
187
+
188
+ if (flags.verbose) {
189
+ logger.error('Full error details:');
190
+ console.error(error);
191
+ }
192
+
193
+ process.exitCode = 1;
194
+ }
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Main deployment logic using deploy-ability library
200
+ *
201
+ * This function:
202
+ * 1. Loads agent.json
203
+ * 2. Resolves profile
204
+ * 3. Routes to appropriate command (Akash or local)
205
+ *
206
+ * @param ctx - KADI CLI context
207
+ * @param options - Raw CLI options
208
+ */
209
+ async function deployWithDeployAbility(ctx: IKadiContext, options: DeployOptions) {
210
+ const { logger } = ctx;
211
+ const projectRoot = path.resolve(options.project || process.cwd());
212
+
213
+ // ----------------------------------------------------------------
214
+ // 1. Load agent.json
215
+ // ----------------------------------------------------------------
216
+ let agentConfig;
217
+ try {
218
+ agentConfig = await loadAgentConfig(projectRoot);
219
+ } catch (error) {
220
+ const errorMessage = error instanceof Error ? error.message : String(error);
221
+ logger.error(
222
+ `Unable to load agent.json from ${projectRoot}: ${errorMessage}`
223
+ );
224
+ return;
225
+ }
226
+
227
+ // ----------------------------------------------------------------
228
+ // 2. Resolve profile
229
+ // ----------------------------------------------------------------
230
+ let selectedProfile = options.profile;
231
+
232
+ if (options.profile) {
233
+ // User explicitly requested a profile - validate it exists
234
+ if (!hasProfile(agentConfig, options.profile)) {
235
+ logger.error(
236
+ `Deploy profile '${options.profile}' not found in agent.json`
237
+ );
238
+ return;
239
+ }
240
+ } else {
241
+ // No profile specified - use first one
242
+ selectedProfile = getFirstProfile(agentConfig);
243
+
244
+ if (!selectedProfile) {
245
+ logger.error('No deploy profiles found in agent.json');
246
+ logger.log('Add a deploy profile under the "deploy" field in agent.json');
247
+ return;
248
+ }
249
+ }
250
+
251
+ // Get profile configuration
252
+ if (!selectedProfile) {
253
+ logger.error('No deploy profiles available');
254
+ return;
255
+ }
256
+
257
+ // Get and validate profile structure (Zod validation)
258
+ const profile = getProfile(agentConfig, selectedProfile);
259
+ if (!profile) {
260
+ logger.error(`Profile "${selectedProfile}" not found in agent.json`);
261
+ return;
262
+ }
263
+
264
+ // ----------------------------------------------------------------
265
+ // 2.5. Validate secrets exist before deployment
266
+ // ----------------------------------------------------------------
267
+ // Check that all required secrets are available in the local vault.
268
+ // This shells out to `kadi secret list --json` and compares against
269
+ // the profile's secrets.required array. Fails fast with helpful error
270
+ // if any required secrets are missing.
271
+ try {
272
+ validateSecretsOrFail(profile, projectRoot, logger);
273
+ } catch (err) {
274
+ const errorMessage = err instanceof Error ? err.message : String(err);
275
+ logger.error(errorMessage);
276
+ return;
277
+ }
278
+
279
+ const target = profile.target;
280
+
281
+ // ----------------------------------------------------------------
282
+ // 3. Build deployment context
283
+ // ----------------------------------------------------------------
284
+ const deployCtx: DeploymentContext = {
285
+ ...ctx,
286
+ projectRoot,
287
+ agent: agentConfig,
288
+ flags: {
289
+ ...options,
290
+ profile: selectedProfile,
291
+ target,
292
+ project: projectRoot
293
+ } as ResolvedDeployOptions
294
+ };
295
+
296
+ // ----------------------------------------------------------------
297
+ // 4. Route to appropriate deployment command
298
+ // ----------------------------------------------------------------
299
+ if (target === 'akash') {
300
+ await executeAkashDeployment(deployCtx);
301
+ } else if (target === 'local') {
302
+ await executeLocalDeployment(deployCtx);
303
+ } else {
304
+ logger.error(`Unknown deployment target: ${target}`);
305
+ logger.log('Supported targets: akash, local');
306
+ }
307
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Registry Infrastructure Module
3
+ *
4
+ * Handles temporary container registry for local images.
5
+ *
6
+ * **Problem**: Akash providers can't pull local images like "my-app"
7
+ * **Solution**:
8
+ * 1. Start temporary registry on localhost:3000
9
+ * 2. Push local images to it
10
+ * 3. Expose publicly via tunnel (kadi/ngrok/serveo/localtunnel)
11
+ * 4. Rewrite profile to use public registry URLs
12
+ * 5. Providers can now pull images during deployment
13
+ *
14
+ * @module infrastructure/registry
15
+ */
16
+
17
+ import { TemporaryContainerRegistryManager } from '../enhanced-registry-manager.js';
18
+ import type { IKadiLogger } from '../types/external.js';
19
+ import type { AkashDeploymentData } from '@kadi.build/deploy-ability/types';
20
+ import type { AkashProfile } from '../schemas/profiles.js';
21
+ import { execSync } from 'node:child_process';
22
+
23
+ /**
24
+ * Registry context returned by setupRegistryIfNeeded
25
+ */
26
+ export interface RegistryContext {
27
+ /**
28
+ * Profile ready for deployment to Akash
29
+ *
30
+ * Local images have been replaced with public registry URLs.
31
+ * For example:
32
+ * - Original: { image: "my-app" }
33
+ * - Deployable: { image: "abc123.serveo.net/my-app:latest", credentials: {...} }
34
+ */
35
+ deployableProfile: AkashProfile;
36
+
37
+ /**
38
+ * Cleanup function to call after deployment
39
+ *
40
+ * Waits for containers to be running on provider, then shuts down
41
+ * the temporary registry (no longer needed once containers pulled images).
42
+ */
43
+ cleanup: () => Promise<void>;
44
+ }
45
+
46
+ /**
47
+ * Sets up temporary registry infrastructure if profile uses local images
48
+ *
49
+ * @param profile - Original profile from agent.json
50
+ * @param logger - Logger instance
51
+ * @param options - Registry configuration options
52
+ * @returns Registry context with deployable profile and cleanup function
53
+ *
54
+ * @example No local images
55
+ * ```typescript
56
+ * // Profile uses remote images only (docker.io/nginx)
57
+ * const ctx = await setupRegistryIfNeeded(profile, logger)
58
+ * // ctx.deployableProfile === profile (unchanged)
59
+ * // ctx.cleanup() is a no-op
60
+ * ```
61
+ *
62
+ * @example Local images present
63
+ * ```typescript
64
+ * // Profile uses local images (my-app, my-api)
65
+ * const ctx = await setupRegistryIfNeeded(profile, logger)
66
+ * // ctx.deployableProfile has images rewritten to public URLs
67
+ * // ctx.cleanup() waits for containers and shuts down registry
68
+ * ```
69
+ */
70
+ export async function setupRegistryIfNeeded(
71
+ profile: AkashProfile,
72
+ logger: IKadiLogger,
73
+ options: {
74
+ useRemoteRegistry?: boolean;
75
+ registryDuration?: number;
76
+ tunnelService?: 'kadi' | 'ngrok' | 'serveo' | 'localtunnel';
77
+ containerEngine?: 'docker' | 'podman';
78
+ } = {}
79
+ ): Promise<RegistryContext> {
80
+ // User explicitly wants to use remote registry
81
+ if (options.useRemoteRegistry) {
82
+ logger.log('📡 Using remote registry (local registry disabled)');
83
+ logger.log('⚠️ Ensure all images are pushed to a remote registry!');
84
+
85
+ return {
86
+ deployableProfile: profile,
87
+ cleanup: async () => {} // No cleanup needed
88
+ };
89
+ }
90
+
91
+ // Check if any services use local images
92
+ if (!hasLocalImages(profile)) {
93
+ // All images are remote (docker.io/nginx, ghcr.io/owner/repo, etc.)
94
+ // No registry needed - providers can pull directly
95
+ return {
96
+ deployableProfile: profile,
97
+ cleanup: async () => {} // No cleanup needed
98
+ };
99
+ }
100
+
101
+ // Profile has local images - start registry infrastructure
102
+ const resolvedTunnel = options.tunnelService || 'kadi';
103
+ const resolvedEngine = options.containerEngine || 'docker';
104
+ logger.log(`🔧 Local images detected — setting up temporary registry (${resolvedTunnel} tunnel)...`);
105
+
106
+ const manager = new TemporaryContainerRegistryManager(logger);
107
+
108
+ try {
109
+ await manager.startTemporaryRegistry({
110
+ port: 3000,
111
+ tunnelService: resolvedTunnel,
112
+ containerEngine: resolvedEngine,
113
+ durationMs: options.registryDuration || 600000, // 10 minutes default
114
+ autoShutdown: false // We'll shutdown manually after containers are running
115
+ });
116
+ } catch (err) {
117
+ const msg = (err as Error).message || String(err);
118
+ logger.error(`❌ Registry startup failed: ${msg}`);
119
+ if (resolvedTunnel === 'kadi' && (msg.includes('token') || msg.includes('auth') || msg.includes('KADI_TUNNEL'))) {
120
+ logger.error(``);
121
+ logger.error(` The kadi tunnel (frpc) requires a token.`);
122
+ logger.error(` Ensure your .env file contains KADI_TUNNEL_TOKEN and is`);
123
+ logger.error(` placed in the kadi-deploy plugin root (next to package.json).`);
124
+ }
125
+ throw err;
126
+ }
127
+
128
+ // Push local images to registry
129
+ logger.log(`📦 Pushing local images to temporary registry...`);
130
+ await manager.addLocalImagesToTemporaryRegistry(
131
+ profile,
132
+ resolvedEngine
133
+ );
134
+
135
+ // Transform profile: "my-app" → "abc123.serveo.net/my-app:latest"
136
+ const deployableProfile = transformProfileWithRegistry(profile, manager);
137
+
138
+ return {
139
+ deployableProfile,
140
+ cleanup: async () => {
141
+ if (manager.isRunning()) {
142
+ logger.log('🛑 Shutting down temporary registry...');
143
+ await manager.stopTemporaryRegistry();
144
+ logger.log('✅ Temporary registry shut down');
145
+ }
146
+ }
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Checks if profile contains any local container images
152
+ *
153
+ * **Reality-Based Detection:**
154
+ * Instead of guessing based on image name patterns, we check if images
155
+ * actually exist locally using `docker images -q <image>`.
156
+ *
157
+ * This is more accurate because:
158
+ * - "nginx" might look local but exists only remotely (Docker Hub)
159
+ * - "my-app" might be on local machine and needs our temporary registry
160
+ * - We avoid false positives from name heuristics
161
+ *
162
+ * **Why this matters:**
163
+ * If this returns true, we start the temporary registry infrastructure
164
+ * (tunnel, S3 server). We should only do this when there are ACTUAL local
165
+ * images that need to be made publicly accessible.
166
+ *
167
+ * @param profile - Profile to check
168
+ * @param engine - Container engine to use for checking (default: 'docker')
169
+ * @returns True if profile has any images that exist locally
170
+ */
171
+ export function hasLocalImages(profile: AkashProfile, engine: 'docker' | 'podman' = 'docker'): boolean {
172
+ if (!profile.services) return false;
173
+
174
+ return Object.values(profile.services).some((service) => {
175
+ const image = service.image;
176
+ if (!image) return false;
177
+
178
+ try {
179
+ // Check if image exists locally
180
+ const result = execSync(`${engine} images -q ${image}`, {
181
+ encoding: 'utf8',
182
+ stdio: ['pipe', 'pipe', 'pipe'] // Suppress stderr
183
+ });
184
+ return result.trim().length > 0;
185
+ } catch (error) {
186
+ // Image doesn't exist locally or engine unavailable
187
+ return false;
188
+ }
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Transforms profile to use registry URLs for local images
194
+ *
195
+ * Creates a new profile object with:
196
+ * - Local images replaced with public registry URLs
197
+ * - Registry credentials added to each service
198
+ *
199
+ * @param profile - Original profile
200
+ * @param manager - Running registry manager
201
+ * @returns Transformed profile ready for deployment
202
+ */
203
+ function transformProfileWithRegistry(
204
+ profile: AkashProfile,
205
+ manager: TemporaryContainerRegistryManager
206
+ ): AkashProfile {
207
+ // Deep clone to avoid mutating original
208
+ const transformed = JSON.parse(JSON.stringify(profile)) as AkashProfile;
209
+
210
+ // Get registry credentials
211
+ const credentials = manager.getRegistryCredentials();
212
+
213
+ // Transform each service
214
+ for (const [serviceName, service] of Object.entries(transformed.services)) {
215
+ // Service is typed from Zod schema
216
+ const originalImage = service.image;
217
+
218
+ // Check if this is a local image
219
+ const isLocal =
220
+ !originalImage.includes('/') ||
221
+ originalImage.startsWith('localhost/') ||
222
+ originalImage.startsWith('127.0.0.1/');
223
+
224
+ if (isLocal) {
225
+ // Get public registry URL for this image
226
+ const registryUrl = manager.getPublicImageUrl(serviceName, originalImage);
227
+
228
+ if (registryUrl) {
229
+ // Replace image with registry URL
230
+ service.image = registryUrl;
231
+
232
+ // Add registry credentials
233
+ if (credentials) {
234
+ service.credentials = {
235
+ host: credentials.host,
236
+ username: credentials.username,
237
+ password: credentials.password
238
+ };
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ return transformed;
245
+ }
246
+
247
+ /**
248
+ * Wait for containers to be running on Akash provider before shutting down registry
249
+ *
250
+ * This is called by the cleanup function to ensure providers have pulled images
251
+ * before we shut down the temporary registry.
252
+ *
253
+ * @param deploymentData - Deployment result from deploy-ability
254
+ * @param logger - Logger instance
255
+ * @param timeoutMs - How long to wait (default: 5 minutes)
256
+ */
257
+ export async function waitForContainersRunning(
258
+ deploymentData: AkashDeploymentData,
259
+ logger: IKadiLogger,
260
+ timeoutMs: number = 300000
261
+ ): Promise<void> {
262
+ logger.log('⏳ Waiting for containers to be running on provider...');
263
+
264
+ // TODO: Use deploy-ability's waitForContainersRunning if available
265
+ // For now, just wait a bit to let providers pull images
266
+ await new Promise(resolve => setTimeout(resolve, 30000)); // 30 seconds
267
+
268
+ logger.log('✅ Containers should be running (or pulling images)');
269
+ }