@layr-labs/ecloud-cli 0.4.0-dev → 0.4.0-dev.1

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 (66) hide show
  1. package/LICENSE +7 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/auth/generate.js +0 -0
  4. package/dist/commands/auth/login.js +0 -0
  5. package/dist/commands/auth/logout.js +0 -0
  6. package/dist/commands/auth/migrate.js +0 -0
  7. package/dist/commands/auth/whoami.js +0 -0
  8. package/dist/commands/billing/__tests__/status.test.js +552 -0
  9. package/dist/commands/billing/__tests__/status.test.js.map +1 -0
  10. package/dist/commands/billing/__tests__/subscribe.test.js +580 -0
  11. package/dist/commands/billing/__tests__/subscribe.test.js.map +1 -0
  12. package/dist/commands/billing/__tests__/top-up.test.js +729 -0
  13. package/dist/commands/billing/__tests__/top-up.test.js.map +1 -0
  14. package/dist/commands/billing/cancel.js +0 -0
  15. package/dist/commands/billing/status.js +5 -0
  16. package/dist/commands/billing/status.js.map +1 -1
  17. package/dist/commands/billing/subscribe.js +30 -2
  18. package/dist/commands/billing/subscribe.js.map +1 -1
  19. package/dist/commands/billing/top-up.js +491 -0
  20. package/dist/commands/billing/top-up.js.map +1 -0
  21. package/dist/commands/compute/app/configure/tls.js +0 -0
  22. package/dist/commands/compute/app/create.js +0 -0
  23. package/dist/commands/compute/app/deploy.js +13 -3
  24. package/dist/commands/compute/app/deploy.js.map +1 -1
  25. package/dist/commands/compute/app/info.js +1 -1
  26. package/dist/commands/compute/app/info.js.map +1 -1
  27. package/dist/commands/compute/app/list.js +1 -1
  28. package/dist/commands/compute/app/list.js.map +1 -1
  29. package/dist/commands/compute/app/logs.js +1 -1
  30. package/dist/commands/compute/app/logs.js.map +1 -1
  31. package/dist/commands/compute/app/profile/set.js +1 -1
  32. package/dist/commands/compute/app/profile/set.js.map +1 -1
  33. package/dist/commands/compute/app/releases.js +1 -1
  34. package/dist/commands/compute/app/releases.js.map +1 -1
  35. package/dist/commands/compute/app/start.js +1 -1
  36. package/dist/commands/compute/app/start.js.map +1 -1
  37. package/dist/commands/compute/app/stop.js +1 -1
  38. package/dist/commands/compute/app/stop.js.map +1 -1
  39. package/dist/commands/compute/app/terminate.js +1 -1
  40. package/dist/commands/compute/app/terminate.js.map +1 -1
  41. package/dist/commands/compute/app/upgrade.js +1 -1
  42. package/dist/commands/compute/app/upgrade.js.map +1 -1
  43. package/dist/commands/compute/build/info.js +1 -1
  44. package/dist/commands/compute/build/info.js.map +1 -1
  45. package/dist/commands/compute/build/list.js +1 -1
  46. package/dist/commands/compute/build/list.js.map +1 -1
  47. package/dist/commands/compute/build/logs.js +1 -1
  48. package/dist/commands/compute/build/logs.js.map +1 -1
  49. package/dist/commands/compute/build/status.js +1 -1
  50. package/dist/commands/compute/build/status.js.map +1 -1
  51. package/dist/commands/compute/build/submit.js +1 -1
  52. package/dist/commands/compute/build/submit.js.map +1 -1
  53. package/dist/commands/compute/build/verify.js +1 -1
  54. package/dist/commands/compute/build/verify.js.map +1 -1
  55. package/dist/commands/compute/environment/list.js +0 -0
  56. package/dist/commands/compute/environment/set.js +0 -0
  57. package/dist/commands/compute/environment/show.js +0 -0
  58. package/dist/commands/compute/undelegate.js +1 -1
  59. package/dist/commands/compute/undelegate.js.map +1 -1
  60. package/dist/commands/telemetry/disable.js +0 -0
  61. package/dist/commands/telemetry/enable.js +0 -0
  62. package/dist/commands/telemetry/status.js +0 -0
  63. package/dist/commands/upgrade.js +0 -0
  64. package/dist/commands/version.js +0 -0
  65. package/package.json +14 -14
  66. package/VERSION +0 -2
@@ -0,0 +1,580 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/utils/viemClients.ts
13
+ import {
14
+ createPublicClient,
15
+ http
16
+ } from "viem";
17
+ import { privateKeyToAccount } from "viem/accounts";
18
+ import {
19
+ getEnvironmentConfig,
20
+ addHexPrefix,
21
+ createViemClients as sdkCreateViemClients,
22
+ getChainFromID
23
+ } from "@layr-labs/ecloud-sdk";
24
+ var init_viemClients = __esm({
25
+ "src/utils/viemClients.ts"() {
26
+ "use strict";
27
+ }
28
+ });
29
+
30
+ // src/utils/globalConfig.ts
31
+ import * as fs from "fs";
32
+ import * as path from "path";
33
+ import * as os from "os";
34
+ import { load as loadYaml, dump as dumpYaml } from "js-yaml";
35
+ import { getBuildType } from "@layr-labs/ecloud-sdk";
36
+ import * as crypto from "crypto";
37
+ function getGlobalConfigDir() {
38
+ const configHome = process.env.XDG_CONFIG_HOME;
39
+ let baseDir;
40
+ if (configHome && path.isAbsolute(configHome)) {
41
+ baseDir = configHome;
42
+ } else {
43
+ baseDir = path.join(os.homedir(), ".config");
44
+ }
45
+ const buildType = getBuildType();
46
+ const buildSuffix = buildType === "dev" ? "-dev" : "";
47
+ const configDirName = `ecloud${buildSuffix}`;
48
+ return path.join(baseDir, configDirName);
49
+ }
50
+ function getGlobalConfigPath() {
51
+ return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
52
+ }
53
+ function loadGlobalConfig() {
54
+ const configPath = getGlobalConfigPath();
55
+ if (!fs.existsSync(configPath)) {
56
+ return {
57
+ first_run: true
58
+ };
59
+ }
60
+ try {
61
+ const content = fs.readFileSync(configPath, "utf-8");
62
+ const config = loadYaml(content);
63
+ return config || { first_run: true };
64
+ } catch {
65
+ return {
66
+ first_run: true
67
+ };
68
+ }
69
+ }
70
+ function saveGlobalConfig(config) {
71
+ const configPath = getGlobalConfigPath();
72
+ const configDir = path.dirname(configPath);
73
+ fs.mkdirSync(configDir, { recursive: true, mode: 493 });
74
+ const content = dumpYaml(config, { lineWidth: -1 });
75
+ fs.writeFileSync(configPath, content, { mode: 420 });
76
+ }
77
+ function getDefaultEnvironment() {
78
+ const config = loadGlobalConfig();
79
+ return config.default_environment;
80
+ }
81
+ function getGlobalTelemetryPreference() {
82
+ const config = loadGlobalConfig();
83
+ return config.telemetry_enabled;
84
+ }
85
+ function getOrCreateUserUUID() {
86
+ const config = loadGlobalConfig();
87
+ if (config.user_uuid) {
88
+ return config.user_uuid;
89
+ }
90
+ const uuid = generateUUID();
91
+ config.user_uuid = uuid;
92
+ config.first_run = false;
93
+ saveGlobalConfig(config);
94
+ return uuid;
95
+ }
96
+ function generateUUID() {
97
+ const bytes = crypto.randomBytes(16);
98
+ bytes[6] = bytes[6] & 15 | 64;
99
+ bytes[8] = bytes[8] & 63 | 128;
100
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
101
+ return hex.slice(0, 4).join("") + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10, 12).join("") + "-" + hex.slice(12, 16).join("");
102
+ }
103
+ var GLOBAL_CONFIG_FILE, PROFILE_CACHE_TTL_MS;
104
+ var init_globalConfig = __esm({
105
+ "src/utils/globalConfig.ts"() {
106
+ "use strict";
107
+ GLOBAL_CONFIG_FILE = "config.yaml";
108
+ PROFILE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
109
+ }
110
+ });
111
+
112
+ // src/utils/appNames.ts
113
+ import * as fs2 from "fs";
114
+ import * as path2 from "path";
115
+ import * as os2 from "os";
116
+ import { load as loadYaml2, dump as dumpYaml2 } from "js-yaml";
117
+ var CONFIG_DIR, APPS_DIR;
118
+ var init_appNames = __esm({
119
+ "src/utils/appNames.ts"() {
120
+ "use strict";
121
+ CONFIG_DIR = path2.join(os2.homedir(), ".eigenx");
122
+ APPS_DIR = path2.join(CONFIG_DIR, "apps");
123
+ }
124
+ });
125
+
126
+ // src/utils/version.ts
127
+ var init_version = __esm({
128
+ "src/utils/version.ts"() {
129
+ "use strict";
130
+ }
131
+ });
132
+
133
+ // src/utils/appResolver.ts
134
+ import { isAddress } from "viem";
135
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
136
+ import {
137
+ UserApiClient,
138
+ getAllAppsByDeveloper
139
+ } from "@layr-labs/ecloud-sdk";
140
+ var init_appResolver = __esm({
141
+ "src/utils/appResolver.ts"() {
142
+ "use strict";
143
+ init_viemClients();
144
+ init_globalConfig();
145
+ init_appNames();
146
+ init_version();
147
+ }
148
+ });
149
+
150
+ // src/utils/prompts.ts
151
+ import { input, select, password, confirm as inquirerConfirm } from "@inquirer/prompts";
152
+ import chalk from "chalk";
153
+ import fs3 from "fs";
154
+ import path3 from "path";
155
+ import os3 from "os";
156
+ import { isAddress as isAddress2 } from "viem";
157
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
158
+ import {
159
+ getEnvironmentConfig as getEnvironmentConfig2,
160
+ getAvailableEnvironments,
161
+ isEnvironmentAvailable,
162
+ getAllAppsByDeveloper as getAllAppsByDeveloper2,
163
+ getCategoryDescriptions,
164
+ fetchTemplateCatalog,
165
+ PRIMARY_LANGUAGES,
166
+ validateAppName,
167
+ validateImageReference,
168
+ validateFilePath,
169
+ validatePrivateKeyFormat,
170
+ extractAppNameFromImage,
171
+ UserApiClient as UserApiClient2
172
+ } from "@layr-labs/ecloud-sdk";
173
+ async function getPrivateKeyInteractive(privateKey) {
174
+ if (privateKey) {
175
+ if (!validatePrivateKeyFormat(privateKey)) {
176
+ throw new Error("Invalid private key format");
177
+ }
178
+ return privateKey;
179
+ }
180
+ const { getPrivateKeyWithSource: getPrivateKeyWithSource2 } = await import("@layr-labs/ecloud-sdk");
181
+ const result = await getPrivateKeyWithSource2({ privateKey: void 0 });
182
+ if (result) {
183
+ return result.key;
184
+ }
185
+ const key = await password({
186
+ message: "Enter private key:",
187
+ mask: true,
188
+ validate: (value) => {
189
+ if (!value.trim()) {
190
+ return "Private key is required";
191
+ }
192
+ if (!validatePrivateKeyFormat(value)) {
193
+ return "Invalid private key format (must be 64 hex characters, optionally prefixed with 0x)";
194
+ }
195
+ return true;
196
+ }
197
+ });
198
+ return key.trim();
199
+ }
200
+ var MAX_IMAGE_SIZE;
201
+ var init_prompts = __esm({
202
+ "src/utils/prompts.ts"() {
203
+ "use strict";
204
+ init_appResolver();
205
+ init_viemClients();
206
+ init_globalConfig();
207
+ init_appNames();
208
+ init_version();
209
+ MAX_IMAGE_SIZE = 4 * 1024 * 1024;
210
+ }
211
+ });
212
+
213
+ // src/flags.ts
214
+ import { Flags } from "@oclif/core";
215
+ import { getBuildType as getBuildType2 } from "@layr-labs/ecloud-sdk";
216
+ var commonFlags;
217
+ var init_flags = __esm({
218
+ "src/flags.ts"() {
219
+ "use strict";
220
+ init_prompts();
221
+ init_globalConfig();
222
+ commonFlags = {
223
+ environment: Flags.string({
224
+ required: false,
225
+ description: "Deployment environment to use",
226
+ env: "ECLOUD_ENV",
227
+ default: async () => getDefaultEnvironment() || (getBuildType2() === "dev" ? "sepolia-dev" : "sepolia")
228
+ }),
229
+ "private-key": Flags.string({
230
+ required: false,
231
+ description: "Private key for signing transactions",
232
+ env: "ECLOUD_PRIVATE_KEY"
233
+ }),
234
+ "rpc-url": Flags.string({
235
+ required: false,
236
+ description: "RPC URL to connect to blockchain",
237
+ env: "ECLOUD_RPC_URL"
238
+ }),
239
+ verbose: Flags.boolean({
240
+ required: false,
241
+ description: "Enable verbose logging (default: false)",
242
+ default: false
243
+ })
244
+ };
245
+ }
246
+ });
247
+
248
+ // src/client.ts
249
+ import {
250
+ createComputeModule,
251
+ createBillingModule,
252
+ createBuildModule,
253
+ getEnvironmentConfig as getEnvironmentConfig3,
254
+ requirePrivateKey,
255
+ getPrivateKeyWithSource,
256
+ addHexPrefix as addHexPrefix2
257
+ } from "@layr-labs/ecloud-sdk";
258
+ import { createWalletClient, custom } from "viem";
259
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
260
+ async function createBillingClient(flags) {
261
+ const result = await getPrivateKeyWithSource({
262
+ privateKey: flags["private-key"]
263
+ });
264
+ const privateKey = await getPrivateKeyInteractive(result?.key);
265
+ const account = privateKeyToAccount4(addHexPrefix2(privateKey));
266
+ const walletClient = createWalletClient({
267
+ account,
268
+ transport: custom({
269
+ async request() {
270
+ throw new Error("RPC not available - billing uses local signing only");
271
+ }
272
+ })
273
+ });
274
+ return createBillingModule({
275
+ verbose: flags.verbose ?? false,
276
+ walletClient,
277
+ skipTelemetry: true
278
+ // CLI already has telemetry, skip SDK telemetry
279
+ });
280
+ }
281
+ var init_client = __esm({
282
+ "src/client.ts"() {
283
+ "use strict";
284
+ init_flags();
285
+ init_prompts();
286
+ init_version();
287
+ init_viemClients();
288
+ }
289
+ });
290
+
291
+ // src/telemetry.ts
292
+ import {
293
+ createTelemetryClient,
294
+ createAppEnvironment,
295
+ createMetricsContext,
296
+ addMetric,
297
+ addMetricWithDimensions,
298
+ emitMetrics,
299
+ getBuildType as getBuildType3
300
+ } from "@layr-labs/ecloud-sdk";
301
+ function createCLITelemetryClient() {
302
+ const userUUID = getOrCreateUserUUID();
303
+ const environment = createAppEnvironment(userUUID);
304
+ const telemetryEnabled = getGlobalTelemetryPreference();
305
+ return createTelemetryClient(environment, "ecloud-cli", {
306
+ telemetryEnabled: telemetryEnabled !== false
307
+ // Enabled by default, disabled only if explicitly set to false
308
+ });
309
+ }
310
+ async function withTelemetry(command, action) {
311
+ const client = createCLITelemetryClient();
312
+ const metrics = createMetricsContext();
313
+ metrics.properties["source"] = "ecloud-cli";
314
+ metrics.properties["command"] = command.id || command.constructor.name;
315
+ const environment = getDefaultEnvironment() || "sepolia";
316
+ metrics.properties["environment"] = environment;
317
+ const buildType = getBuildType3() || "prod";
318
+ metrics.properties["build_type"] = buildType;
319
+ const cliVersion = command.config.version;
320
+ if (cliVersion) {
321
+ metrics.properties["cli_version"] = cliVersion;
322
+ }
323
+ addMetric(metrics, "Count", 1);
324
+ let actionError;
325
+ let result;
326
+ try {
327
+ result = await action();
328
+ return result;
329
+ } catch (err) {
330
+ actionError = err instanceof Error ? err : new Error(String(err));
331
+ throw err;
332
+ } finally {
333
+ const resultValue = actionError ? "Failure" : "Success";
334
+ const dimensions = {};
335
+ if (actionError) {
336
+ dimensions["error"] = actionError.message;
337
+ }
338
+ addMetricWithDimensions(metrics, resultValue, 1, dimensions);
339
+ const duration = Date.now() - metrics.startTime.getTime();
340
+ addMetric(metrics, "DurationMilliseconds", duration);
341
+ try {
342
+ await emitMetrics(client, metrics);
343
+ await client.close();
344
+ } catch {
345
+ }
346
+ }
347
+ }
348
+ var init_telemetry = __esm({
349
+ "src/telemetry.ts"() {
350
+ "use strict";
351
+ init_globalConfig();
352
+ }
353
+ });
354
+
355
+ // src/commands/billing/subscribe.ts
356
+ var subscribe_exports = {};
357
+ __export(subscribe_exports, {
358
+ default: () => BillingSubscribe
359
+ });
360
+ import { Command, Flags as Flags2 } from "@oclif/core";
361
+ import { isSubscriptionActive } from "@layr-labs/ecloud-sdk";
362
+ import chalk2 from "chalk";
363
+ import open from "open";
364
+ import { select as select2 } from "@inquirer/prompts";
365
+ var PAYMENT_TIMEOUT_MS, POLL_INTERVAL_MS, BillingSubscribe;
366
+ var init_subscribe = __esm({
367
+ "src/commands/billing/subscribe.ts"() {
368
+ "use strict";
369
+ init_client();
370
+ init_flags();
371
+ init_telemetry();
372
+ PAYMENT_TIMEOUT_MS = 5 * 60 * 1e3;
373
+ POLL_INTERVAL_MS = 3e3;
374
+ BillingSubscribe = class _BillingSubscribe extends Command {
375
+ static description = "Create subscription to start deploying apps";
376
+ static flags = {
377
+ ...commonFlags,
378
+ product: Flags2.string({
379
+ required: false,
380
+ description: "Product ID",
381
+ default: "compute",
382
+ options: ["compute"],
383
+ env: "ECLOUD_PRODUCT_ID"
384
+ })
385
+ };
386
+ async run() {
387
+ return withTelemetry(this, async () => {
388
+ const { flags } = await this.parse(_BillingSubscribe);
389
+ const billing = await createBillingClient(flags);
390
+ this.debug(`
391
+ Checking subscription status for ${flags.product}...`);
392
+ const result = await billing.subscribe({
393
+ productId: flags.product
394
+ });
395
+ if (result.type === "already_active") {
396
+ this.log(
397
+ `
398
+ ${chalk2.green("\u2713")} Wallet ${chalk2.bold(billing.address)} is already subscribed to ${flags.product}.`
399
+ );
400
+ this.log(chalk2.gray("Run 'ecloud billing status' for details."));
401
+ return;
402
+ }
403
+ if (result.type === "payment_issue") {
404
+ this.log(
405
+ `
406
+ ${chalk2.yellow("\u26A0")} You already have a subscription on ${flags.product}, but it has a payment issue.`
407
+ );
408
+ this.log("Please update your payment method to restore access.");
409
+ if (result.portalUrl) {
410
+ this.log(`
411
+ ${chalk2.bold("Update payment method:")}`);
412
+ this.log(` ${result.portalUrl}`);
413
+ }
414
+ return;
415
+ }
416
+ const paymentMethod = await select2({
417
+ message: "How would you like to pay for EigenCompute?",
418
+ choices: [
419
+ {
420
+ value: "card",
421
+ name: "Credit card",
422
+ description: "Pay via Stripe checkout (opens browser)"
423
+ },
424
+ {
425
+ value: "usdc",
426
+ name: "Purchase credits with USDC",
427
+ description: "Pay on-chain \u2014 no credit card needed"
428
+ }
429
+ ]
430
+ });
431
+ if (paymentMethod === "usdc") {
432
+ await this.config.runCommand("billing:top-up", [
433
+ ...flags["private-key"] ? ["--private-key", flags["private-key"]] : [],
434
+ ...flags.verbose ? ["--verbose"] : [],
435
+ ...flags.environment ? ["--environment", flags.environment] : [],
436
+ ...flags["rpc-url"] ? ["--rpc-url", flags["rpc-url"]] : [],
437
+ "--product",
438
+ flags.product
439
+ ]);
440
+ return;
441
+ }
442
+ this.log(`
443
+ Opening checkout for wallet ${chalk2.bold(billing.address)}...`);
444
+ this.log(chalk2.gray(`
445
+ URL: ${result.checkoutUrl}`));
446
+ this.log(chalk2.gray(`
447
+ Prefer to pay with USDC? Run: ecloud billing top-up`));
448
+ await open(result.checkoutUrl);
449
+ this.log(`
450
+ ${chalk2.gray("Waiting for payment confirmation...")}`);
451
+ const startTime = Date.now();
452
+ while (Date.now() - startTime < PAYMENT_TIMEOUT_MS) {
453
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
454
+ try {
455
+ const status = await billing.getStatus({
456
+ productId: flags.product
457
+ });
458
+ if (isSubscriptionActive(status.subscriptionStatus)) {
459
+ this.log(
460
+ `
461
+ ${chalk2.green("\u2713")} Subscription activated successfully for ${flags.product}!`
462
+ );
463
+ this.log(`
464
+ ${chalk2.gray("Start deploying with:")} ecloud compute app deploy`);
465
+ return;
466
+ }
467
+ } catch (error) {
468
+ this.debug(`Error polling for subscription status: ${error}`);
469
+ }
470
+ }
471
+ this.log(`
472
+ ${chalk2.yellow("\u26A0")} Payment confirmation timed out after 5 minutes.`);
473
+ this.log(
474
+ chalk2.gray(`If you completed payment, run 'ecloud billing status' to check status.`)
475
+ );
476
+ });
477
+ }
478
+ };
479
+ }
480
+ });
481
+
482
+ // src/commands/billing/__tests__/subscribe.test.ts
483
+ init_client();
484
+ import { describe, it, expect, vi, beforeEach } from "vitest";
485
+ import { select as select3 } from "@inquirer/prompts";
486
+ vi.mock("../../../client", () => ({
487
+ createBillingClient: vi.fn()
488
+ }));
489
+ vi.mock("../../../telemetry", () => ({
490
+ withTelemetry: vi.fn((_cmd, fn) => fn())
491
+ }));
492
+ vi.mock("@inquirer/prompts", () => ({
493
+ select: vi.fn(),
494
+ confirm: vi.fn()
495
+ }));
496
+ vi.mock("open", () => ({
497
+ default: vi.fn()
498
+ }));
499
+ describe("ecloud billing subscribe \u2014 payment method selection", () => {
500
+ let logOutput;
501
+ let mockBilling;
502
+ let runCommandCalls;
503
+ beforeEach(() => {
504
+ logOutput = [];
505
+ runCommandCalls = [];
506
+ mockBilling = {
507
+ address: "0xabcdef1234567890abcdef1234567890abcdef12",
508
+ subscribe: vi.fn(),
509
+ getStatus: vi.fn()
510
+ };
511
+ createBillingClient.mockResolvedValue(mockBilling);
512
+ });
513
+ async function runCommand(selectResponse, subscribeResult = { type: "checkout_created", checkoutUrl: "https://checkout.stripe.com/test" }) {
514
+ const { default: BillingSubscribe2 } = await Promise.resolve().then(() => (init_subscribe(), subscribe_exports));
515
+ select3.mockResolvedValue(selectResponse);
516
+ mockBilling.subscribe.mockResolvedValue(subscribeResult);
517
+ mockBilling.getStatus.mockResolvedValue({
518
+ subscriptionStatus: "active"
519
+ });
520
+ const cmd = new BillingSubscribe2([], {});
521
+ cmd.parse = vi.fn().mockResolvedValue({
522
+ flags: { product: "compute", verbose: false }
523
+ });
524
+ cmd.log = vi.fn((...args) => logOutput.push(args.join(" ")));
525
+ cmd.debug = vi.fn();
526
+ cmd.config = {
527
+ runCommand: vi.fn((...args) => {
528
+ runCommandCalls.push(args);
529
+ return Promise.resolve();
530
+ })
531
+ };
532
+ await cmd.run();
533
+ return logOutput;
534
+ }
535
+ it("presents CC and USDC as payment options", async () => {
536
+ await runCommand("card");
537
+ expect(select3).toHaveBeenCalledWith(
538
+ expect.objectContaining({
539
+ message: expect.stringContaining("pay"),
540
+ choices: expect.arrayContaining([
541
+ expect.objectContaining({ value: "card" }),
542
+ expect.objectContaining({ value: "usdc" })
543
+ ])
544
+ })
545
+ );
546
+ });
547
+ it("delegates to ecloud billing top-up when USDC is selected", async () => {
548
+ await runCommand("usdc");
549
+ expect(runCommandCalls.length).toBe(1);
550
+ expect(runCommandCalls[0][0]).toBe("billing:top-up");
551
+ });
552
+ it("opens Stripe checkout when credit card is selected", async () => {
553
+ const open2 = (await import("open")).default;
554
+ await runCommand("card");
555
+ expect(open2).toHaveBeenCalledWith("https://checkout.stripe.com/test");
556
+ });
557
+ it("shows top-up hint during credit card checkout", async () => {
558
+ await runCommand("card");
559
+ const fullOutput = logOutput.join("\n");
560
+ expect(fullOutput).toContain("ecloud billing top-up");
561
+ });
562
+ it("skips payment selection when subscription is already active", async () => {
563
+ mockBilling.subscribe.mockResolvedValue({
564
+ type: "already_active",
565
+ status: "active"
566
+ });
567
+ const { default: BillingSubscribe2 } = await Promise.resolve().then(() => (init_subscribe(), subscribe_exports));
568
+ const cmd = new BillingSubscribe2([], {});
569
+ cmd.parse = vi.fn().mockResolvedValue({
570
+ flags: { product: "compute", verbose: false }
571
+ });
572
+ cmd.log = vi.fn((...args) => logOutput.push(args.join(" ")));
573
+ cmd.debug = vi.fn();
574
+ await cmd.run();
575
+ expect(select3).not.toHaveBeenCalled();
576
+ const fullOutput = logOutput.join("\n");
577
+ expect(fullOutput).toContain("already subscribed");
578
+ });
579
+ });
580
+ //# sourceMappingURL=subscribe.test.js.map