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
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Akash Network Profile Types
3
+ *
4
+ * Type definitions for Akash deployment profiles and service configurations.
5
+ * These types represent the profile structure AFTER loading from agent.json
6
+ * but BEFORE passing to deploy-ability.
7
+ */
8
+
9
+ import type { ResourceConfig } from '../types.js';
10
+
11
+ /**
12
+ * Port exposure configuration for Akash SDL
13
+ */
14
+ export interface AkashPortExpose {
15
+ port: number;
16
+ as?: number;
17
+ to?: string[];
18
+ proto?: 'tcp' | 'udp';
19
+ global?: boolean;
20
+ accept?: string[];
21
+ }
22
+
23
+ /**
24
+ * Container registry credentials for private images
25
+ */
26
+ export interface AkashRegistryCredentials {
27
+ host: string;
28
+ username: string;
29
+ password: string;
30
+ }
31
+
32
+ /**
33
+ * Pricing configuration for Akash bidding
34
+ */
35
+ export interface AkashPricing {
36
+ denom: string; // e.g., "uakt"
37
+ amount: number | string;
38
+ }
39
+
40
+ /**
41
+ * Placement requirements for Akash providers
42
+ */
43
+ export interface AkashPlacement {
44
+ signedBy?: {
45
+ allOf?: string[];
46
+ anyOf?: string[];
47
+ };
48
+ attributes?: Record<string, any>;
49
+ }
50
+
51
+ /**
52
+ * Service definition for Akash deployment
53
+ *
54
+ * Represents a single service in the deployment profile.
55
+ * Includes container image, runtime config, resources, and pricing.
56
+ */
57
+ export interface AkashServiceDefinition {
58
+ /** Container image (e.g., "nginx:latest" or "my-app:1.0") */
59
+ image: string;
60
+
61
+ /** Optional command override */
62
+ command?: string[];
63
+
64
+ /** Environment variables (array or object format) */
65
+ env?: string[] | Record<string, string>;
66
+
67
+ /** Port exposures */
68
+ expose?: AkashPortExpose[];
69
+
70
+ /** Registry credentials for private images */
71
+ credentials?: AkashRegistryCredentials;
72
+
73
+ /** Resource requirements (CPU, memory, storage, GPU) */
74
+ resources?: ResourceConfig;
75
+
76
+ /** Pricing configuration for bidding */
77
+ pricing?: AkashPricing;
78
+
79
+ /** Number of instances to deploy */
80
+ count?: number;
81
+ }
82
+
83
+ /**
84
+ * Loaded Akash deployment profile
85
+ *
86
+ * Complete Akash profile structure after loading from agent.json.
87
+ * Contains all services and global placement/pricing settings.
88
+ */
89
+ export interface LoadedAkashProfile {
90
+ /** Profile name */
91
+ selectedProfileName: string;
92
+
93
+ /** Deployment target (always 'akash') */
94
+ target: 'akash';
95
+
96
+ /** Network to deploy to */
97
+ network: 'mainnet' | 'testnet';
98
+
99
+ /** Container engine to use for local operations */
100
+ engine?: 'docker' | 'podman';
101
+
102
+ /** Service definitions */
103
+ services: Record<string, AkashServiceDefinition>;
104
+
105
+ /** Placement requirements (auditors, attributes) */
106
+ placement?: AkashPlacement;
107
+
108
+ /** WalletConnect project ID for mobile wallet connection */
109
+ walletConnectProjectId?: string;
110
+
111
+ /** Other profile options */
112
+ verbose?: boolean;
113
+ showCommands?: boolean;
114
+ yes?: boolean;
115
+ dryRun?: boolean;
116
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Ambient type declarations for @kadi.build/container-registry-ability
3
+ *
4
+ * The package ships with types at src/types/index.d.ts but doesn't have a
5
+ * "types" field in package.json or a .d.ts for the main entry point.
6
+ * This declaration bridges that gap so we get proper type checking.
7
+ *
8
+ * Based on container-registry-ability v0.0.6 API surface.
9
+ */
10
+ declare module '@kadi.build/container-registry-ability' {
11
+ export interface TunnelOptions {
12
+ subdomain?: string;
13
+ region?: string;
14
+ authtoken?: string;
15
+ authToken?: string;
16
+ host?: string;
17
+ port?: number;
18
+ protocol?: string;
19
+ /** Pass-through options for TunnelManager (e.g. fallbackServices) */
20
+ managerOptions?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface RegistryUrls {
24
+ localUrl: string;
25
+ localDomain: string;
26
+ tunnelUrl: string | null;
27
+ tunnelDomain: string | null;
28
+ preferredUrl: string;
29
+ preferredDomain: string;
30
+ }
31
+
32
+ export interface Credentials {
33
+ accessKey: string;
34
+ secretKey: string;
35
+ expiry: Date;
36
+ }
37
+
38
+ export interface Container {
39
+ id: string;
40
+ name: string;
41
+ alias?: string;
42
+ tags: string[];
43
+ size: number;
44
+ addedAt: Date;
45
+ }
46
+
47
+ export interface ContainerSpec {
48
+ source?: string;
49
+ type?: 'docker' | 'podman' | 'tar' | 'mock';
50
+ name: string;
51
+ image?: string;
52
+ path?: string;
53
+ alias?: string;
54
+ tags?: string[];
55
+ }
56
+
57
+ export interface ContainerInfo {
58
+ id: string;
59
+ name: string;
60
+ alias: string;
61
+ tags: string[];
62
+ size: number;
63
+ addedAt: Date;
64
+ source: string;
65
+ }
66
+
67
+ export interface RegistryInfo {
68
+ status: string;
69
+ serverId: string;
70
+ localUrl: string;
71
+ tunnelUrl: string | null;
72
+ credentials: Credentials;
73
+ startTime: Date;
74
+ containers: Container[];
75
+ }
76
+
77
+ export interface TunneledContainerRegistryOptions {
78
+ port?: number;
79
+ serverName?: string;
80
+ tunnelService?: string;
81
+ tunnelOptions?: TunnelOptions;
82
+ credentials?: {
83
+ expiry?: number;
84
+ permissions?: string[];
85
+ customKey?: string;
86
+ customSecret?: string;
87
+ };
88
+ autoShutdown?: boolean;
89
+ shutdownOptions?: {
90
+ onCompletion?: boolean;
91
+ completionDelay?: number;
92
+ maxIdleTime?: number;
93
+ maxTotalTime?: number;
94
+ };
95
+ enableMonitoring?: boolean;
96
+ monitoringOptions?: {
97
+ updateInterval?: number;
98
+ enableDashboard?: boolean;
99
+ enableLogging?: boolean;
100
+ };
101
+ preferredEngine?: string;
102
+ containerType?: string;
103
+ engineOptions?: {
104
+ dockerSocket?: string;
105
+ podmanSocket?: string;
106
+ };
107
+ registryOptions?: {
108
+ enableCatalog?: boolean;
109
+ enableHealthCheck?: boolean;
110
+ customHeaders?: object;
111
+ };
112
+ enableLogging?: boolean;
113
+ logLevel?: string;
114
+ duration?: number;
115
+ }
116
+
117
+ export class TunneledContainerRegistry {
118
+ constructor(options?: TunneledContainerRegistryOptions);
119
+
120
+ // Core lifecycle
121
+ start(): Promise<RegistryInfo>;
122
+ stop(): Promise<void>;
123
+
124
+ // Container management
125
+ addContainer(containerSpec: ContainerSpec): Promise<ContainerInfo>;
126
+ addContainers(containerSpecs: ContainerSpec[]): Promise<ContainerInfo[]>;
127
+ removeContainer(nameOrId: string): Promise<void>;
128
+ listContainers(): Container[];
129
+
130
+ // Information
131
+ getRegistryInfo(): RegistryInfo;
132
+ getRegistryUrls(): Promise<RegistryUrls>;
133
+ getAccessCredentials(): Promise<Credentials>;
134
+ getDockerCommands(containerName?: string): Promise<object>;
135
+ getPodmanCommands(containerName?: string): Promise<object>;
136
+ getContainerCommands(containerName?: string): Promise<object>;
137
+ generateCommandHelp(options?: object): Promise<any>;
138
+
139
+ // Statistics
140
+ getStats(): object;
141
+ getRegistryStats(): object;
142
+ getContainerStats(nameOrId: string): object | null;
143
+
144
+ // Configuration
145
+ updateConfig(newConfig: Partial<TunneledContainerRegistryOptions>): Promise<void>;
146
+ refreshCredentials(options?: { expiry?: number }): Promise<Credentials>;
147
+
148
+ // Utility
149
+ healthCheck(): Promise<object>;
150
+ autoDiscoverContainers(options?: object): Promise<ContainerInfo[]>;
151
+ exportRegistryConfig(): Promise<object>;
152
+
153
+ // Event emitter
154
+ on(event: string, listener: (...args: any[]) => void): this;
155
+ once(event: string, listener: (...args: any[]) => void): this;
156
+ emit(event: string, ...args: any[]): boolean;
157
+ }
158
+ }
@@ -0,0 +1,49 @@
1
+ // src/types/external.ts
2
+
3
+ // Commander.js types
4
+ export interface ICommander {
5
+ command(name: string): ICommand;
6
+ parse(argv?: string[]): void;
7
+ opts(): Record<string, unknown>;
8
+ }
9
+
10
+ export interface ICommand {
11
+ alias(alias: string): ICommand;
12
+ description(desc: string): ICommand;
13
+ // Register a subcommand on this command
14
+ command(name: string, opts?: { isDefault?: boolean }): ICommand;
15
+ // Overload for option without default value
16
+ option(flags: string, description: string): ICommand;
17
+ // Overload for option with default value but no parser function
18
+ option(flags: string, description: string, defaultValue: string | number | boolean): ICommand;
19
+ // Overload for option with parser function and optional default
20
+ option(
21
+ flags: string,
22
+ description: string,
23
+ fn: ((value: string, previous: unknown) => unknown) | RegExp,
24
+ defaultValue?: unknown
25
+ ): ICommand;
26
+ action(fn: (...args: unknown[]) => void | Promise<void>): ICommand;
27
+ addHelpText(position: string, text: string): ICommand;
28
+ }
29
+
30
+ // Logger types
31
+ export interface IKadiLogger {
32
+ log(message: string, ...args: unknown[]): void;
33
+ error(message: string, ...args: unknown[]): void;
34
+ warn(message: string, ...args: unknown[]): void;
35
+ info(message: string, ...args: unknown[]): void;
36
+ debug(message: string, ...args: unknown[]): void;
37
+ verbose(message: string, ...args: unknown[]): void;
38
+ }
39
+
40
+ // Core types
41
+ export interface IKadiCore {
42
+ version: string;
43
+ config: Record<string, unknown>;
44
+ utils: {
45
+ readFile(path: string): Promise<string>;
46
+ writeFile(path: string, content: string): Promise<void>;
47
+ exists(path: string): Promise<boolean>;
48
+ };
49
+ }
package/src/types.ts ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Core TypeScript interfaces for the KADI deploy profile system
3
+ *
4
+ * This file defines the type system for the profile-based deployment configuration
5
+ * that replaces the previous flag-based CLI approach. The system follows a three-tier
6
+ * precedence model: CLI flags > Profile settings > System defaults
7
+ */
8
+
9
+ import type { ICommander, IKadiLogger, IKadiCore } from './types/external.js';
10
+
11
+ /**
12
+ * DeployOptions represents raw CLI input before profile resolution
13
+ *
14
+ * This captures what the user provided via command-line flags. All fields are
15
+ * optional because users might rely on profile defaults or system defaults.
16
+ * The profile field indicates which profile to load from agent.json.
17
+ */
18
+ export interface DeployOptions {
19
+ // Profile selection
20
+ profile?: string; // Name of the profile to use from agent.json
21
+
22
+ // CLI overrides - these take precedence over profile settings
23
+ target?: string;
24
+ network?: string;
25
+ engine?: string;
26
+ cert?: string;
27
+ useRemoteRegistry?: boolean;
28
+ registryDuration?: number;
29
+ tunnelService?: string; // Tunnel service (validated at runtime: kadi, ngrok, serveo, localtunnel)
30
+ interactive?: boolean;
31
+ dryRun?: boolean;
32
+ verbose?: boolean;
33
+ yes?: boolean;
34
+ showCommands?: boolean;
35
+ project?: string; // Path to project directory (defaults to cwd)
36
+
37
+ // Secrets injection options
38
+ secretTimeout?: number; // Timeout (ms) for waiting for agent to request secrets
39
+ autoApproveSecrets?: boolean; // Auto-approve secret requests without prompting
40
+
41
+ // Autonomous deployment options
42
+ autonomous?: boolean; // Enable fully autonomous deployment (no human interaction)
43
+ bidStrategy?: 'cheapest' | 'most-reliable' | 'balanced'; // Bid selection strategy for autonomous mode
44
+ bidMaxPrice?: number; // Maximum price per block in uAKT for bid filtering
45
+ requireAudited?: boolean; // Only accept bids from audited providers
46
+ secretsVault?: string; // Override vault name for secrets (default: from profile)
47
+
48
+ // Multi-instance deployment options
49
+ label?: string; // Human-readable label for this deployment instance (e.g. "broker-us-east")
50
+ instance?: string; // Target a specific deployment instance by ID
51
+ }
52
+
53
+ /**
54
+ * ResolvedDeployOptions represents the final configuration after merging
55
+ * CLI flags, profile settings, and system defaults
56
+ *
57
+ * This is what gets passed to deployment targets. All required fields have
58
+ * been resolved to concrete values, eliminating the need for target modules
59
+ * to handle undefined values or complex fallback logic.
60
+ */
61
+ export interface ResolvedDeployOptions {
62
+ // Required fields - always have concrete values after resolution
63
+ target: 'local' | 'akash';
64
+ network: 'mainnet' | 'testnet';
65
+ engine: 'docker' | 'podman';
66
+ useRemoteRegistry: boolean;
67
+ registryDuration: number;
68
+ tunnelService: string;
69
+ interactive: boolean;
70
+ dryRun: boolean;
71
+ verbose: boolean;
72
+ yes: boolean;
73
+ showCommands: boolean;
74
+ project: string;
75
+
76
+ // Optional fields - may remain undefined if not specified anywhere
77
+ cert?: string; // Only defined if user provided cert path
78
+ profile?: string; // Profile name that was used (if any)
79
+
80
+ // Secrets injection options
81
+ secretTimeout?: number; // Timeout (ms) for waiting for agent to request secrets
82
+ autoApproveSecrets?: boolean; // Auto-approve secret requests without prompting
83
+
84
+ // Autonomous deployment options
85
+ autonomous?: boolean; // Enable fully autonomous deployment (no human interaction)
86
+ bidStrategy?: 'cheapest' | 'most-reliable' | 'balanced'; // Bid selection strategy for autonomous mode
87
+ bidMaxPrice?: number; // Maximum price per block in uAKT for bid filtering
88
+ requireAudited?: boolean; // Only accept bids from audited providers
89
+ secretsVault?: string; // Override vault name for secrets (default: from profile)
90
+
91
+ // Multi-instance deployment options
92
+ label?: string; // Human-readable label for this deployment instance
93
+ instance?: string; // Target a specific deployment instance by ID
94
+ }
95
+
96
+ /**
97
+ * Resource configuration for Akash deployments
98
+ * // DO I neeD THIS?
99
+ *
100
+ * Defines compute resources needed for the deployment. These get translated
101
+ * into Akash SDL (Stack Definition Language) resource requirements.
102
+ */
103
+ /**
104
+ * Persistent volume specification for Akash Network
105
+ */
106
+ export interface PersistentVolumeSpec {
107
+ /** Volume name (must be unique within service) */
108
+ name: string;
109
+ /** Volume size (e.g., "10Gi") */
110
+ size: string;
111
+ /** Container mount path (e.g., "/data") */
112
+ mount: string;
113
+ /** Storage class: beta1 (HDD), beta2 (SSD), beta3 (NVMe), ram */
114
+ class?: 'beta1' | 'beta2' | 'beta3' | 'ram';
115
+ }
116
+
117
+ /**
118
+ * Storage specification - supports both legacy and persistent formats
119
+ *
120
+ * **Legacy format (string):**
121
+ * Simple ephemeral storage that's wiped on restart
122
+ * Example: "2Gi"
123
+ *
124
+ * **Array format (persistent volumes):**
125
+ * First element: ephemeral storage size (required)
126
+ * Additional elements: named persistent volumes (optional)
127
+ * Example: ["512Mi", { name: "data", size: "10Gi", mount: "/data", class: "beta2" }]
128
+ */
129
+ export type StorageSpec = string | [string, ...PersistentVolumeSpec[]];
130
+
131
+ export interface ResourceConfig {
132
+ cpu?: number; // CPU units (e.g., 0.5 for half a CPU)
133
+ memory?: string; // Memory with units (e.g., "512Mi", "2Gi")
134
+ storage?: StorageSpec; // Storage: simple string or array with persistent volumes
135
+ gpu?: {
136
+ units: number; // Number of GPU units
137
+ attributes: {
138
+ vendor?: string; // GPU vendor (e.g., "nvidia", "amd")
139
+ model?: string; // GPU model (e.g., "rtx3060", "a100")
140
+ };
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Complete agent configuration structure (agent.json)
146
+ *
147
+ * This represents the full agent.json file structure with support for both
148
+ * traditional service-based configuration and the new profile-based system.
149
+ * Variable interpolation is supported for ${name} and ${version} placeholders.
150
+ */
151
+ export interface AgentConfig {
152
+ // Basic agent metadata
153
+ name: string; // Agent name (used for ${name} interpolation)
154
+ version: string; // Agent version (used for ${version} interpolation)
155
+ license?: string; // License identifier (e.g., "MIT")
156
+ description?: string; // Human-readable description
157
+
158
+ // Abilities configuration - defines what capabilities this agent has
159
+ abilities?: Record<string, string>; // Map of ability name to version (e.g., "data-processor": "1.2.0")
160
+
161
+ // Scripts configuration - lifecycle hooks for the agent
162
+ scripts?: Record<string, string>; // Map of script name to command (e.g., "setup": "npm install")
163
+
164
+ // Build configuration - compatibility with kadi-build profiles
165
+ build?: Record<string, any>; // Build profiles from kadi-build system
166
+
167
+ // Deploy configuration - the heart of the deploy profile system
168
+ // Raw JSON profiles - each target loads and validates its own profile format
169
+ deploy?: Record<string, any>;
170
+
171
+ // Broker configuration - WebSocket URLs for KADI broker connections
172
+ // Used for secrets injection to share secrets with deployed agents
173
+ brokers?: Record<string, string>; // Map of broker name to WebSocket URL (e.g., "default": "ws://localhost:8080/kadi")
174
+ }
175
+
176
+ /**
177
+ * KADI CLI context interface
178
+ *
179
+ * Provides access to core KADI CLI functionality like command registration,
180
+ * logging, and core utilities. Passed to all KADI plugins and modules.
181
+ */
182
+ export interface IKadiContext {
183
+ commander: ICommander; // Commander.js instance for CLI registration
184
+ logger: IKadiLogger; // KADI logging utilities
185
+ core: IKadiCore; // Core KADI functionality and utilities
186
+ }
187
+
188
+ /**
189
+ * Deployment context passed to target modules
190
+ *
191
+ * This is the complete context that deployment targets (local.js, akash.js)
192
+ * receive. It includes the original KADI context plus deployment-specific
193
+ * information like the resolved configuration and agent metadata.
194
+ */
195
+ export interface DeploymentContext extends IKadiContext {
196
+ projectRoot: string; // Absolute path to the project being deployed
197
+ agent: AgentConfig; // Parsed agent.json configuration
198
+ flags: ResolvedDeployOptions; // Final deployment configuration after resolution
199
+ }
200
+
201
+ /**
202
+ * Target module interface
203
+ *
204
+ * All deployment targets must implement this interface. The deploy method
205
+ * receives a complete DeploymentContext and is responsible for executing
206
+ * the deployment according to the resolved configuration.
207
+ */
208
+ export interface DeployTarget {
209
+ description: string;
210
+ deploy(ctx: DeploymentContext): Promise<void>;
211
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * AKT Price Fetcher
3
+ *
4
+ * Utility for fetching current AKT token price in USD from CoinGecko API.
5
+ * Used for displaying USD pricing in bid selection and deployment summaries.
6
+ *
7
+ * @module utils/akt-price
8
+ */
9
+
10
+ /**
11
+ * Fetch current AKT price in USD from CoinGecko
12
+ *
13
+ * Makes a request to CoinGecko's public API to get the current market price
14
+ * of Akash Network (AKT) token in USD.
15
+ *
16
+ * **API Details:**
17
+ * - Endpoint: https://api.coingecko.com/api/v3/simple/price
18
+ * - No API key required for basic usage
19
+ * - Rate limit: ~10-50 requests/minute (free tier)
20
+ * - Response format: { "akash-network": { "usd": 0.45 } }
21
+ *
22
+ * **Error Handling:**
23
+ * Returns undefined if the fetch fails for any reason (network error, API down, etc.)
24
+ * This allows the caller to gracefully degrade to showing only AKT pricing.
25
+ *
26
+ * @returns Promise resolving to USD price per AKT, or undefined if unavailable
27
+ *
28
+ * @example Basic usage
29
+ * ```typescript
30
+ * const aktPrice = await fetchAktPriceUSD();
31
+ * if (aktPrice) {
32
+ * console.log(`1 AKT = $${aktPrice.toFixed(2)}`);
33
+ * } else {
34
+ * console.log('AKT price unavailable');
35
+ * }
36
+ * ```
37
+ *
38
+ * @example With bid selector
39
+ * ```typescript
40
+ * const aktPrice = await fetchAktPriceUSD();
41
+ * const selected = await selectBidInteractively(bids, aktPrice);
42
+ * ```
43
+ */
44
+ export async function fetchAktPriceUSD(): Promise<number | undefined> {
45
+ try {
46
+ const response = await fetch(
47
+ 'https://api.coingecko.com/api/v3/simple/price?ids=akash-network&vs_currencies=usd',
48
+ {
49
+ // 5 second timeout
50
+ signal: AbortSignal.timeout(5000),
51
+ headers: {
52
+ 'Accept': 'application/json',
53
+ }
54
+ }
55
+ );
56
+
57
+ if (!response.ok) {
58
+ return undefined;
59
+ }
60
+
61
+ const data = await response.json();
62
+ const price = data?.['akash-network']?.['usd'];
63
+
64
+ // Validate price is a positive number
65
+ if (typeof price === 'number' && price > 0 && Number.isFinite(price)) {
66
+ return price;
67
+ }
68
+
69
+ return undefined;
70
+ } catch (error) {
71
+ // Silent failure - caller can handle missing price gracefully
72
+ return undefined;
73
+ }
74
+ }