@layr-labs/ecloud-cli 0.0.1-dev → 0.0.1-rfc.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 (75) hide show
  1. package/README.md +2 -6
  2. package/dist/commands/app/create.js +29 -0
  3. package/dist/commands/app/create.js.map +1 -0
  4. package/dist/commands/app/deploy.js +142 -0
  5. package/dist/commands/app/deploy.js.map +1 -0
  6. package/dist/commands/app/logs.js +108 -0
  7. package/dist/commands/app/logs.js.map +1 -0
  8. package/dist/commands/app/start.js +121 -0
  9. package/dist/commands/app/start.js.map +1 -0
  10. package/dist/commands/app/stop.js +121 -0
  11. package/dist/commands/app/stop.js.map +1 -0
  12. package/dist/commands/app/terminate.js +128 -0
  13. package/dist/commands/app/terminate.js.map +1 -0
  14. package/dist/commands/app/upgrade.js +142 -0
  15. package/dist/commands/app/upgrade.js.map +1 -0
  16. package/dist/commands/auth/generate.js +10 -116
  17. package/dist/commands/auth/generate.js.map +1 -1
  18. package/dist/commands/auth/login.js +35 -37
  19. package/dist/commands/auth/login.js.map +1 -1
  20. package/dist/commands/auth/logout.js +8 -2
  21. package/dist/commands/auth/logout.js.map +1 -1
  22. package/dist/commands/auth/migrate.js +37 -32
  23. package/dist/commands/auth/migrate.js.map +1 -1
  24. package/dist/commands/auth/whoami.js +21 -53
  25. package/dist/commands/auth/whoami.js.map +1 -1
  26. package/dist/commands/billing/cancel.js +22 -83
  27. package/dist/commands/billing/cancel.js.map +1 -1
  28. package/dist/commands/billing/status.js +29 -92
  29. package/dist/commands/billing/status.js.map +1 -1
  30. package/dist/commands/billing/subscribe.js +31 -86
  31. package/dist/commands/billing/subscribe.js.map +1 -1
  32. package/dist/keys/mainnet-alpha/prod/kms-encryption-public-key.pem +14 -0
  33. package/dist/keys/mainnet-alpha/prod/kms-signing-public-key.pem +4 -0
  34. package/dist/keys/sepolia/dev/kms-encryption-public-key.pem +14 -0
  35. package/dist/keys/sepolia/dev/kms-signing-public-key.pem +4 -0
  36. package/dist/keys/sepolia/prod/kms-encryption-public-key.pem +14 -0
  37. package/dist/keys/sepolia/prod/kms-signing-public-key.pem +4 -0
  38. package/dist/templates/Dockerfile.layered.tmpl +58 -0
  39. package/dist/templates/compute-source-env.sh.tmpl +110 -0
  40. package/package.json +4 -29
  41. package/VERSION +0 -2
  42. package/dist/commands/compute/app/configure/tls.js +0 -150
  43. package/dist/commands/compute/app/configure/tls.js.map +0 -1
  44. package/dist/commands/compute/app/create.js +0 -134
  45. package/dist/commands/compute/app/create.js.map +0 -1
  46. package/dist/commands/compute/app/deploy.js +0 -1081
  47. package/dist/commands/compute/app/deploy.js.map +0 -1
  48. package/dist/commands/compute/app/info.js +0 -809
  49. package/dist/commands/compute/app/info.js.map +0 -1
  50. package/dist/commands/compute/app/list.js +0 -570
  51. package/dist/commands/compute/app/list.js.map +0 -1
  52. package/dist/commands/compute/app/logs.js +0 -629
  53. package/dist/commands/compute/app/logs.js.map +0 -1
  54. package/dist/commands/compute/app/profile/set.js +0 -1072
  55. package/dist/commands/compute/app/profile/set.js.map +0 -1
  56. package/dist/commands/compute/app/start.js +0 -665
  57. package/dist/commands/compute/app/start.js.map +0 -1
  58. package/dist/commands/compute/app/stop.js +0 -665
  59. package/dist/commands/compute/app/stop.js.map +0 -1
  60. package/dist/commands/compute/app/terminate.js +0 -671
  61. package/dist/commands/compute/app/terminate.js.map +0 -1
  62. package/dist/commands/compute/app/upgrade.js +0 -1063
  63. package/dist/commands/compute/app/upgrade.js.map +0 -1
  64. package/dist/commands/compute/environment/list.js +0 -89
  65. package/dist/commands/compute/environment/list.js.map +0 -1
  66. package/dist/commands/compute/environment/set.js +0 -215
  67. package/dist/commands/compute/environment/set.js.map +0 -1
  68. package/dist/commands/compute/environment/show.js +0 -96
  69. package/dist/commands/compute/environment/show.js.map +0 -1
  70. package/dist/commands/compute/undelegate.js +0 -250
  71. package/dist/commands/compute/undelegate.js.map +0 -1
  72. package/dist/commands/upgrade.js +0 -91
  73. package/dist/commands/upgrade.js.map +0 -1
  74. package/dist/commands/version.js +0 -65
  75. package/dist/commands/version.js.map +0 -1
@@ -1,1081 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/commands/compute/app/deploy.ts
4
- import { Command, Flags as Flags2 } from "@oclif/core";
5
- import {
6
- getEnvironmentConfig as getEnvironmentConfig2,
7
- UserApiClient as UserApiClient3,
8
- isMainnet,
9
- prepareDeploy,
10
- executeDeploy,
11
- watchDeployment
12
- } from "@layr-labs/ecloud-sdk";
13
-
14
- // src/flags.ts
15
- import { Flags } from "@oclif/core";
16
-
17
- // src/utils/prompts.ts
18
- import { input, select, password, confirm as inquirerConfirm } from "@inquirer/prompts";
19
- import fs3 from "fs";
20
- import path3 from "path";
21
- import os3 from "os";
22
- import { isAddress as isAddress2 } from "viem";
23
- import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
24
- import {
25
- getEnvironmentConfig,
26
- getAvailableEnvironments,
27
- isEnvironmentAvailable,
28
- getAllAppsByDeveloper as getAllAppsByDeveloper2,
29
- getCategoryDescriptions,
30
- fetchTemplateCatalog,
31
- PRIMARY_LANGUAGES,
32
- validateAppName,
33
- validateImageReference,
34
- validateFilePath,
35
- validatePrivateKeyFormat,
36
- extractAppNameFromImage,
37
- UserApiClient as UserApiClient2
38
- } from "@layr-labs/ecloud-sdk";
39
-
40
- // src/utils/appResolver.ts
41
- import { isAddress } from "viem";
42
- import { privateKeyToAccount } from "viem/accounts";
43
- import {
44
- UserApiClient,
45
- getAllAppsByDeveloper
46
- } from "@layr-labs/ecloud-sdk";
47
-
48
- // src/utils/globalConfig.ts
49
- import * as fs from "fs";
50
- import * as path from "path";
51
- import * as os from "os";
52
- import { load as loadYaml, dump as dumpYaml } from "js-yaml";
53
- import { getBuildType } from "@layr-labs/ecloud-sdk";
54
- var GLOBAL_CONFIG_FILE = "config.yaml";
55
- var PROFILE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
56
- function getGlobalConfigDir() {
57
- const configHome = process.env.XDG_CONFIG_HOME;
58
- let baseDir;
59
- if (configHome && path.isAbsolute(configHome)) {
60
- baseDir = configHome;
61
- } else {
62
- baseDir = path.join(os.homedir(), ".config");
63
- }
64
- const buildType = getBuildType();
65
- const buildSuffix = buildType === "dev" ? "-dev" : "";
66
- const configDirName = `ecloud${buildSuffix}`;
67
- return path.join(baseDir, configDirName);
68
- }
69
- function getGlobalConfigPath() {
70
- return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
71
- }
72
- function loadGlobalConfig() {
73
- const configPath = getGlobalConfigPath();
74
- if (!fs.existsSync(configPath)) {
75
- return {
76
- first_run: true
77
- };
78
- }
79
- try {
80
- const content = fs.readFileSync(configPath, "utf-8");
81
- const config = loadYaml(content);
82
- return config || { first_run: true };
83
- } catch {
84
- return {
85
- first_run: true
86
- };
87
- }
88
- }
89
- function saveGlobalConfig(config) {
90
- const configPath = getGlobalConfigPath();
91
- const configDir = path.dirname(configPath);
92
- fs.mkdirSync(configDir, { recursive: true, mode: 493 });
93
- const content = dumpYaml(config, { lineWidth: -1 });
94
- fs.writeFileSync(configPath, content, { mode: 420 });
95
- }
96
- function invalidateProfileCache(environment) {
97
- const config = loadGlobalConfig();
98
- if (!config.profile_cache) {
99
- return;
100
- }
101
- if (environment) {
102
- delete config.profile_cache[environment];
103
- } else {
104
- config.profile_cache = {};
105
- }
106
- saveGlobalConfig(config);
107
- }
108
-
109
- // src/utils/appNames.ts
110
- import * as fs2 from "fs";
111
- import * as path2 from "path";
112
- import * as os2 from "os";
113
- import { load as loadYaml2, dump as dumpYaml2 } from "js-yaml";
114
- var CONFIG_DIR = path2.join(os2.homedir(), ".eigenx");
115
- var APPS_DIR = path2.join(CONFIG_DIR, "apps");
116
- var APP_REGISTRY_VERSION = "1.0.0";
117
- function getAppRegistryPath(environment) {
118
- return path2.join(APPS_DIR, `${environment}.yaml`);
119
- }
120
- function loadAppRegistry(environment) {
121
- const filePath = getAppRegistryPath(environment);
122
- if (!fs2.existsSync(filePath)) {
123
- return {
124
- version: APP_REGISTRY_VERSION,
125
- apps: {}
126
- };
127
- }
128
- try {
129
- const content = fs2.readFileSync(filePath, "utf-8");
130
- const registry = loadYaml2(content);
131
- if (!registry.apps) {
132
- registry.apps = {};
133
- }
134
- return registry;
135
- } catch {
136
- return {
137
- version: APP_REGISTRY_VERSION,
138
- apps: {}
139
- };
140
- }
141
- }
142
- function listApps(environment) {
143
- const registry = loadAppRegistry(environment);
144
- const result = {};
145
- for (const [name, app] of Object.entries(registry.apps)) {
146
- if (app?.app_id) {
147
- result[name] = String(app.app_id);
148
- }
149
- }
150
- return result;
151
- }
152
- function isAppNameAvailable(environment, name) {
153
- const apps = listApps(environment);
154
- return !apps[name];
155
- }
156
- function findAvailableName(environment, baseName) {
157
- const apps = listApps(environment);
158
- if (!apps[baseName]) {
159
- return baseName;
160
- }
161
- for (let i = 2; i <= 100; i++) {
162
- const candidate = `${baseName}-${i}`;
163
- if (!apps[candidate]) {
164
- return candidate;
165
- }
166
- }
167
- return `${baseName}-${Date.now()}`;
168
- }
169
-
170
- // src/utils/prompts.ts
171
- async function getDockerfileInteractive(dockerfilePath) {
172
- if (dockerfilePath) {
173
- return dockerfilePath;
174
- }
175
- const cwd = process.env.INIT_CWD || process.cwd();
176
- const dockerfilePath_resolved = path3.join(cwd, "Dockerfile");
177
- if (!fs3.existsSync(dockerfilePath_resolved)) {
178
- return "";
179
- }
180
- console.log(`
181
- Found Dockerfile in ${cwd}`);
182
- const choice = await select({
183
- message: "Choose deployment method:",
184
- choices: [
185
- { name: "Build and deploy from Dockerfile", value: "build" },
186
- { name: "Deploy existing image from registry", value: "existing" }
187
- ]
188
- });
189
- switch (choice) {
190
- case "build":
191
- return dockerfilePath_resolved;
192
- case "existing":
193
- return "";
194
- default:
195
- throw new Error(`Unexpected choice: ${choice}`);
196
- }
197
- }
198
- function extractHostname(registry) {
199
- let hostname = registry.replace(/^https?:\/\//, "");
200
- hostname = hostname.split("/")[0];
201
- hostname = hostname.split(":")[0];
202
- return hostname.toLowerCase();
203
- }
204
- function isDockerHub(registry) {
205
- const hostname = extractHostname(registry);
206
- return hostname === "docker.io" || hostname === "index.docker.io" || hostname === "registry-1.docker.io";
207
- }
208
- function isGHCR(registry) {
209
- const hostname = extractHostname(registry);
210
- return hostname === "ghcr.io";
211
- }
212
- function isGCR(registry) {
213
- const hostname = extractHostname(registry);
214
- return hostname === "gcr.io" || hostname.endsWith(".gcr.io");
215
- }
216
- async function getCredentialsFromHelper(registry) {
217
- const dockerConfigPath = path3.join(os3.homedir(), ".docker", "config.json");
218
- if (!fs3.existsSync(dockerConfigPath)) {
219
- return void 0;
220
- }
221
- try {
222
- const config = JSON.parse(fs3.readFileSync(dockerConfigPath, "utf-8"));
223
- const credsStore = config.credsStore;
224
- if (!credsStore) {
225
- return void 0;
226
- }
227
- const { execSync } = await import("child_process");
228
- const helper = `docker-credential-${credsStore}`;
229
- try {
230
- const registryVariants = [];
231
- if (isDockerHub(registry)) {
232
- registryVariants.push("https://index.docker.io/v1/");
233
- registryVariants.push("https://index.docker.io/v1");
234
- registryVariants.push("index.docker.io");
235
- registryVariants.push("docker.io");
236
- } else {
237
- const baseRegistry = registry.replace(/^https?:\/\//, "").replace(/\/v1\/?$/, "").replace(/\/$/, "");
238
- registryVariants.push(`https://${baseRegistry}`);
239
- registryVariants.push(`https://${baseRegistry}/v1/`);
240
- registryVariants.push(baseRegistry);
241
- }
242
- for (const variant of registryVariants) {
243
- try {
244
- const output = execSync(`echo "${variant}" | ${helper} get`, {
245
- encoding: "utf-8"
246
- });
247
- const creds = JSON.parse(output);
248
- if (creds.Username && creds.Secret) {
249
- return { username: creds.Username, password: creds.Secret };
250
- }
251
- } catch {
252
- continue;
253
- }
254
- }
255
- } catch {
256
- return void 0;
257
- }
258
- } catch {
259
- return void 0;
260
- }
261
- return void 0;
262
- }
263
- async function getAvailableRegistries() {
264
- const dockerConfigPath = path3.join(os3.homedir(), ".docker", "config.json");
265
- if (!fs3.existsSync(dockerConfigPath)) {
266
- return [];
267
- }
268
- try {
269
- const configContent = fs3.readFileSync(dockerConfigPath, "utf-8");
270
- const config = JSON.parse(configContent);
271
- const auths = config.auths || {};
272
- const credsStore = config.credsStore;
273
- const gcrProjects = /* @__PURE__ */ new Map();
274
- const registries = [];
275
- for (const [registry, auth] of Object.entries(auths)) {
276
- const authData = auth;
277
- const hostname = extractHostname(registry);
278
- if (hostname.includes("access-token") || hostname.includes("refresh-token")) {
279
- continue;
280
- }
281
- let username = authData.username;
282
- let registryType = "other";
283
- let normalizedURL = registry;
284
- if (isDockerHub(registry)) {
285
- registryType = "dockerhub";
286
- normalizedURL = "https://index.docker.io/v1/";
287
- } else if (isGHCR(registry)) {
288
- registryType = "ghcr";
289
- normalizedURL = registry.replace(/^https?:\/\//, "").replace(/\/v1\/?$/, "");
290
- } else if (isGCR(registry)) {
291
- registryType = "gcr";
292
- normalizedURL = "gcr.io";
293
- }
294
- if (!username && credsStore) {
295
- const creds = await getCredentialsFromHelper(registry);
296
- if (creds) {
297
- username = creds.username;
298
- }
299
- }
300
- if (!username) {
301
- continue;
302
- }
303
- const info = {
304
- URL: normalizedURL,
305
- Username: username,
306
- Type: registryType
307
- };
308
- if (registryType === "gcr") {
309
- if (!gcrProjects.has(username)) {
310
- gcrProjects.set(username, info);
311
- }
312
- continue;
313
- }
314
- registries.push(info);
315
- }
316
- for (const gcrInfo of Array.from(gcrProjects.values())) {
317
- registries.push(gcrInfo);
318
- }
319
- registries.sort((a, b) => {
320
- if (a.Type === "dockerhub") return -1;
321
- if (b.Type === "dockerhub") return 1;
322
- return a.Type.localeCompare(b.Type);
323
- });
324
- return registries;
325
- } catch {
326
- return [];
327
- }
328
- }
329
- function getDefaultAppName() {
330
- try {
331
- const cwd = process.env.INIT_CWD || process.cwd();
332
- return path3.basename(cwd);
333
- } catch {
334
- return "myapp";
335
- }
336
- }
337
- function suggestImageReference(registry, imageName, tag) {
338
- imageName = imageName.toLowerCase().replace(/_/g, "-");
339
- if (!tag) {
340
- tag = "latest";
341
- }
342
- switch (registry.Type) {
343
- case "dockerhub":
344
- return `${registry.Username}/${imageName}:${tag}`;
345
- case "ghcr":
346
- return `ghcr.io/${registry.Username}/${imageName}:${tag}`;
347
- case "gcr":
348
- return `gcr.io/${registry.Username}/${imageName}:${tag}`;
349
- default:
350
- let host = registry.URL;
351
- if (host.startsWith("https://")) {
352
- host = host.substring(8);
353
- } else if (host.startsWith("http://")) {
354
- host = host.substring(7);
355
- }
356
- host = host.replace(/\/$/, "");
357
- return `${host}/${registry.Username}/${imageName}:${tag}`;
358
- }
359
- }
360
- function displayDetectedRegistries(registries, appName) {
361
- console.log("Detected authenticated registries:");
362
- for (const reg of registries) {
363
- const suggestion = suggestImageReference(reg, appName, "latest");
364
- console.log(` ${reg.Type}: ${suggestion}`);
365
- }
366
- console.log();
367
- }
368
- function displayAuthenticationInstructions() {
369
- console.log("No authenticated registries detected.");
370
- console.log("To authenticate:");
371
- console.log(" docker login <registry-url>");
372
- console.log();
373
- }
374
- function displayRegistryExamples(appName) {
375
- console.log("Examples:");
376
- console.log(` docker.io/${appName.toLowerCase()}:latest`);
377
- console.log(` ghcr.io/username/${appName.toLowerCase()}:latest`);
378
- console.log(` gcr.io/project-id/${appName.toLowerCase()}:latest`);
379
- console.log();
380
- }
381
- async function selectRegistryInteractive(registries, imageName, tag) {
382
- if (registries.length === 1) {
383
- const defaultRef = suggestImageReference(registries[0], imageName, tag);
384
- return input({
385
- message: "Enter image reference:",
386
- default: defaultRef,
387
- validate: (value) => {
388
- const result = validateImageReference(value);
389
- return result === true ? true : result;
390
- }
391
- });
392
- }
393
- const choices = registries.map((reg) => ({
394
- name: suggestImageReference(reg, imageName, tag),
395
- value: suggestImageReference(reg, imageName, tag)
396
- }));
397
- choices.push({ name: "Enter custom image reference", value: "custom" });
398
- const choice = await select({
399
- message: "Select image destination:",
400
- choices
401
- });
402
- if (choice === "custom") {
403
- return input({
404
- message: "Enter image reference:",
405
- default: "",
406
- validate: (value) => {
407
- const result = validateImageReference(value);
408
- return result === true ? true : result;
409
- }
410
- });
411
- }
412
- return choice;
413
- }
414
- async function getImageReferenceInteractive(imageRef, buildFromDockerfile = false) {
415
- if (imageRef) {
416
- return imageRef;
417
- }
418
- const registries = await getAvailableRegistries();
419
- const appName = getDefaultAppName();
420
- if (buildFromDockerfile) {
421
- console.log("\n\u{1F4E6} Build & Push Configuration");
422
- console.log("Your Docker image will be built and pushed to a registry");
423
- console.log("so that EigenX can pull and run it in the TEE.");
424
- console.log();
425
- if (registries.length > 0) {
426
- displayDetectedRegistries(registries, appName);
427
- return selectRegistryInteractive(registries, appName, "latest");
428
- }
429
- displayAuthenticationInstructions();
430
- } else {
431
- console.log("\n\u{1F433} Docker Image Selection");
432
- console.log("Specify an existing Docker image from a registry to run in the TEE.");
433
- console.log();
434
- }
435
- displayRegistryExamples(appName);
436
- const imageRefInput = await input({
437
- message: "Enter Docker image reference:",
438
- default: "",
439
- validate: (value) => {
440
- const result = validateImageReference(value);
441
- return result === true ? true : result;
442
- }
443
- });
444
- return imageRefInput;
445
- }
446
- async function getAvailableAppNameInteractive(environment, imageRef) {
447
- const baseName = extractAppNameFromImage(imageRef);
448
- const suggestedName = findAvailableName(environment, baseName);
449
- while (true) {
450
- console.log("\nApp name selection:");
451
- const name = await input({
452
- message: "Enter app name:",
453
- default: suggestedName,
454
- validate: (value) => {
455
- try {
456
- validateAppName(value);
457
- return true;
458
- } catch (err) {
459
- return err.message;
460
- }
461
- }
462
- });
463
- if (isAppNameAvailable(environment, name)) {
464
- return name;
465
- }
466
- console.log(`App name '${name}' is already taken.`);
467
- const newSuggested = findAvailableName(environment, name);
468
- console.log(`Suggested alternative: ${newSuggested}`);
469
- }
470
- }
471
- async function getOrPromptAppName(appName, environment, imageRef) {
472
- if (appName) {
473
- validateAppName(appName);
474
- if (isAppNameAvailable(environment, appName)) {
475
- return appName;
476
- }
477
- console.log(`Warning: App name '${appName}' is already taken.`);
478
- return getAvailableAppNameInteractive(environment, imageRef);
479
- }
480
- return getAvailableAppNameInteractive(environment, imageRef);
481
- }
482
- async function getEnvFileInteractive(envFilePath) {
483
- if (envFilePath && fs3.existsSync(envFilePath)) {
484
- return envFilePath;
485
- }
486
- if (fs3.existsSync(".env")) {
487
- return ".env";
488
- }
489
- console.log("\nEnvironment file not found.");
490
- console.log("Environment files contain variables like RPC_URL, etc.");
491
- const choice = await select({
492
- message: "Choose an option:",
493
- choices: [
494
- { name: "Enter path to existing env file", value: "enter" },
495
- { name: "Continue without env file", value: "continue" }
496
- ]
497
- });
498
- switch (choice) {
499
- case "enter":
500
- const envFile = await input({
501
- message: "Enter environment file path:",
502
- default: "",
503
- validate: (value) => {
504
- const result = validateFilePath(value);
505
- return result === true ? true : result;
506
- }
507
- });
508
- return envFile;
509
- case "continue":
510
- return "";
511
- default:
512
- throw new Error(`Unexpected choice: ${choice}`);
513
- }
514
- }
515
- async function getInstanceTypeInteractive(instanceType, defaultSKU, availableTypes) {
516
- if (instanceType) {
517
- const valid = availableTypes.find((t) => t.sku === instanceType);
518
- if (valid) {
519
- return instanceType;
520
- }
521
- const validSKUs = availableTypes.map((t) => t.sku).join(", ");
522
- throw new Error(`Invalid instance-type: ${instanceType} (must be one of: ${validSKUs})`);
523
- }
524
- const isCurrentType = defaultSKU !== "";
525
- if (defaultSKU === "" && availableTypes.length > 0) {
526
- defaultSKU = availableTypes[0].sku;
527
- }
528
- if (isCurrentType && defaultSKU) {
529
- console.log(`
530
- Select instance type (current: ${defaultSKU}):`);
531
- } else {
532
- console.log("\nSelect instance type:");
533
- }
534
- const choices = availableTypes.map((it) => {
535
- let name = `${it.sku} - ${it.description}`;
536
- if (it.sku === defaultSKU) {
537
- name += isCurrentType ? " (current)" : " (default)";
538
- }
539
- return { name, value: it.sku };
540
- });
541
- const choice = await select({
542
- message: "Choose instance:",
543
- choices
544
- });
545
- return choice;
546
- }
547
- async function getLogSettingsInteractive(logVisibility) {
548
- if (logVisibility) {
549
- switch (logVisibility) {
550
- case "public":
551
- return { logRedirect: "always", publicLogs: true };
552
- case "private":
553
- return { logRedirect: "always", publicLogs: false };
554
- case "off":
555
- return { logRedirect: "", publicLogs: false };
556
- default:
557
- throw new Error(
558
- `Invalid log-visibility: ${logVisibility} (must be public, private, or off)`
559
- );
560
- }
561
- }
562
- const choice = await select({
563
- message: "Do you want to view your app's logs?",
564
- choices: [
565
- { name: "Yes, but only viewable by app and platform admins", value: "private" },
566
- { name: "Yes, publicly viewable by anyone", value: "public" },
567
- { name: "No, disable logs entirely", value: "off" }
568
- ]
569
- });
570
- switch (choice) {
571
- case "private":
572
- return { logRedirect: "always", publicLogs: false };
573
- case "public":
574
- return { logRedirect: "always", publicLogs: true };
575
- case "off":
576
- return { logRedirect: "", publicLogs: false };
577
- default:
578
- throw new Error(`Unexpected choice: ${choice}`);
579
- }
580
- }
581
- async function getResourceUsageMonitoringInteractive(resourceUsageMonitoring) {
582
- if (resourceUsageMonitoring) {
583
- switch (resourceUsageMonitoring) {
584
- case "enable":
585
- case "disable":
586
- return resourceUsageMonitoring;
587
- default:
588
- throw new Error(
589
- `Invalid resource-usage-monitoring: ${resourceUsageMonitoring} (must be enable or disable)`
590
- );
591
- }
592
- }
593
- const choice = await select({
594
- message: "Show resource usage (CPU/memory) for your app?",
595
- choices: [
596
- { name: "Yes, enable resource usage monitoring", value: "enable" },
597
- { name: "No, disable resource usage monitoring", value: "disable" }
598
- ]
599
- });
600
- return choice;
601
- }
602
- async function confirm(prompt) {
603
- return confirmWithDefault(prompt, false);
604
- }
605
- async function confirmWithDefault(prompt, defaultValue = false) {
606
- return await inquirerConfirm({
607
- message: prompt,
608
- default: defaultValue
609
- });
610
- }
611
- async function getPrivateKeyInteractive(privateKey) {
612
- if (privateKey) {
613
- if (!validatePrivateKeyFormat(privateKey)) {
614
- throw new Error("Invalid private key format");
615
- }
616
- return privateKey;
617
- }
618
- const { getPrivateKeyWithSource } = await import("@layr-labs/ecloud-sdk");
619
- const result = await getPrivateKeyWithSource({ privateKey: void 0 });
620
- if (result) {
621
- return result.key;
622
- }
623
- const key = await password({
624
- message: "Enter private key:",
625
- mask: true,
626
- validate: (value) => {
627
- if (!value.trim()) {
628
- return "Private key is required";
629
- }
630
- if (!validatePrivateKeyFormat(value)) {
631
- return "Invalid private key format (must be 64 hex characters, optionally prefixed with 0x)";
632
- }
633
- return true;
634
- }
635
- });
636
- return key.trim();
637
- }
638
- var MAX_DESCRIPTION_LENGTH = 1e3;
639
- var MAX_IMAGE_SIZE = 4 * 1024 * 1024;
640
- var VALID_IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"];
641
- var VALID_X_HOSTS = ["twitter.com", "www.twitter.com", "x.com", "www.x.com"];
642
- function validateURL(rawURL) {
643
- if (!rawURL.trim()) {
644
- return "URL cannot be empty";
645
- }
646
- try {
647
- const url = new URL(rawURL);
648
- if (url.protocol !== "http:" && url.protocol !== "https:") {
649
- return "URL scheme must be http or https";
650
- }
651
- } catch {
652
- return "Invalid URL format";
653
- }
654
- return void 0;
655
- }
656
- function validateXURL(rawURL) {
657
- const urlErr = validateURL(rawURL);
658
- if (urlErr) {
659
- return urlErr;
660
- }
661
- try {
662
- const url = new URL(rawURL);
663
- const host = url.hostname.toLowerCase();
664
- if (!VALID_X_HOSTS.includes(host)) {
665
- return "URL must be a valid X/Twitter URL (x.com or twitter.com)";
666
- }
667
- if (!url.pathname || url.pathname === "/") {
668
- return "X URL must include a username or profile path";
669
- }
670
- } catch {
671
- return "Invalid X URL format";
672
- }
673
- return void 0;
674
- }
675
- function validateDescription(description) {
676
- if (!description.trim()) {
677
- return "Description cannot be empty";
678
- }
679
- if (description.length > MAX_DESCRIPTION_LENGTH) {
680
- return `Description cannot exceed ${MAX_DESCRIPTION_LENGTH} characters`;
681
- }
682
- return void 0;
683
- }
684
- function validateImagePath(filePath) {
685
- const cleanedPath = filePath.trim().replace(/^["']|["']$/g, "");
686
- if (!cleanedPath) {
687
- return "Image path cannot be empty";
688
- }
689
- if (!fs3.existsSync(cleanedPath)) {
690
- return `Image file not found: ${cleanedPath}`;
691
- }
692
- const stats = fs3.statSync(cleanedPath);
693
- if (stats.isDirectory()) {
694
- return "Path is a directory, not a file";
695
- }
696
- if (stats.size > MAX_IMAGE_SIZE) {
697
- const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
698
- return `Image file size (${sizeMB} MB) exceeds maximum allowed size of 4 MB`;
699
- }
700
- const ext = path3.extname(cleanedPath).toLowerCase();
701
- if (!VALID_IMAGE_EXTENSIONS.includes(ext)) {
702
- return "Image must be JPG or PNG format";
703
- }
704
- return void 0;
705
- }
706
- async function getAppProfileInteractive(defaultName = "", allowRetry = true) {
707
- while (true) {
708
- const name = await getAppNameForProfile(defaultName);
709
- const website = await getAppWebsiteInteractive();
710
- const description = await getAppDescriptionInteractive();
711
- const xURL = await getAppXURLInteractive();
712
- const imagePath = await getAppImageInteractive();
713
- const profile = {
714
- name,
715
- website,
716
- description,
717
- xURL,
718
- imagePath
719
- };
720
- console.log("\n" + formatProfileForDisplay(profile));
721
- const confirmed = await inquirerConfirm({
722
- message: "Continue with this profile?",
723
- default: true
724
- });
725
- if (confirmed) {
726
- return profile;
727
- }
728
- if (!allowRetry) {
729
- throw new Error("Profile confirmation cancelled");
730
- }
731
- const retry = await inquirerConfirm({
732
- message: "Would you like to re-enter the information?",
733
- default: true
734
- });
735
- if (!retry) {
736
- return void 0;
737
- }
738
- defaultName = name;
739
- }
740
- }
741
- async function getAppNameForProfile(defaultName) {
742
- if (defaultName) {
743
- validateAppName(defaultName);
744
- return defaultName;
745
- }
746
- return await input({
747
- message: "App name:",
748
- default: "",
749
- validate: (value) => {
750
- if (!value.trim()) {
751
- return "Name is required";
752
- }
753
- try {
754
- validateAppName(value);
755
- return true;
756
- } catch (err) {
757
- return err.message;
758
- }
759
- }
760
- });
761
- }
762
- async function getAppWebsiteInteractive() {
763
- const website = await input({
764
- message: "Website URL (optional):",
765
- default: "",
766
- validate: (value) => {
767
- if (!value.trim()) {
768
- return true;
769
- }
770
- const err = validateURL(value);
771
- return err ? err : true;
772
- }
773
- });
774
- if (!website.trim()) {
775
- return void 0;
776
- }
777
- return website;
778
- }
779
- async function getAppDescriptionInteractive() {
780
- const description = await input({
781
- message: "Description (optional):",
782
- default: "",
783
- validate: (value) => {
784
- if (!value.trim()) {
785
- return true;
786
- }
787
- const err = validateDescription(value);
788
- return err ? err : true;
789
- }
790
- });
791
- if (!description.trim()) {
792
- return void 0;
793
- }
794
- return description;
795
- }
796
- async function getAppXURLInteractive() {
797
- const xURL = await input({
798
- message: "X (Twitter) URL (optional):",
799
- default: "",
800
- validate: (value) => {
801
- if (!value.trim()) {
802
- return true;
803
- }
804
- const err = validateXURL(value);
805
- return err ? err : true;
806
- }
807
- });
808
- if (!xURL.trim()) {
809
- return void 0;
810
- }
811
- return xURL;
812
- }
813
- async function getAppImageInteractive() {
814
- const wantsImage = await inquirerConfirm({
815
- message: "Would you like to upload an app icon/logo?",
816
- default: false
817
- });
818
- if (!wantsImage) {
819
- return void 0;
820
- }
821
- const imagePath = await input({
822
- message: "Image path (drag & drop image file or enter path - JPG/PNG, max 4MB, square recommended):",
823
- default: "",
824
- validate: (value) => {
825
- if (!value.trim()) {
826
- return true;
827
- }
828
- const err = validateImagePath(value);
829
- return err ? err : true;
830
- }
831
- });
832
- if (!imagePath.trim()) {
833
- return void 0;
834
- }
835
- return imagePath.trim().replace(/^["']|["']$/g, "");
836
- }
837
- function formatProfileForDisplay(profile) {
838
- let output = "\n\u{1F4CB} Profile Summary:\n";
839
- output += ` Name: ${profile.name}
840
- `;
841
- if (profile.website) {
842
- output += ` Website: ${profile.website}
843
- `;
844
- }
845
- if (profile.description) {
846
- output += ` Description: ${profile.description}
847
- `;
848
- }
849
- if (profile.xURL) {
850
- output += ` X URL: ${profile.xURL}
851
- `;
852
- }
853
- if (profile.imagePath) {
854
- output += ` Image: ${profile.imagePath}
855
- `;
856
- }
857
- return output;
858
- }
859
-
860
- // src/flags.ts
861
- var commonFlags = {
862
- environment: Flags.string({
863
- required: false,
864
- description: "Deployment environment to use",
865
- env: "ECLOUD_ENV"
866
- }),
867
- "private-key": Flags.string({
868
- required: false,
869
- description: "Private key for signing transactions",
870
- env: "ECLOUD_PRIVATE_KEY"
871
- }),
872
- "rpc-url": Flags.string({
873
- required: false,
874
- description: "RPC URL to connect to blockchain",
875
- env: "ECLOUD_RPC_URL"
876
- }),
877
- verbose: Flags.boolean({
878
- required: false,
879
- description: "Enable verbose logging (default: false)",
880
- default: false
881
- })
882
- };
883
-
884
- // src/commands/compute/app/deploy.ts
885
- import chalk from "chalk";
886
- var AppDeploy = class _AppDeploy extends Command {
887
- static description = "Deploy new app";
888
- static flags = {
889
- ...commonFlags,
890
- name: Flags2.string({
891
- required: false,
892
- description: "Friendly name for the app",
893
- env: "ECLOUD_NAME"
894
- }),
895
- dockerfile: Flags2.string({
896
- required: false,
897
- description: "Path to Dockerfile",
898
- env: "ECLOUD_DOCKERFILE_PATH"
899
- }),
900
- "image-ref": Flags2.string({
901
- required: false,
902
- description: "Image reference pointing to registry",
903
- env: "ECLOUD_IMAGE_REF"
904
- }),
905
- "env-file": Flags2.string({
906
- required: false,
907
- description: 'Environment file to use (default: ".env")',
908
- default: ".env",
909
- env: "ECLOUD_ENVFILE_PATH"
910
- }),
911
- "log-visibility": Flags2.string({
912
- required: false,
913
- description: "Log visibility setting: public, private, or off",
914
- options: ["public", "private", "off"],
915
- env: "ECLOUD_LOG_VISIBILITY"
916
- }),
917
- "instance-type": Flags2.string({
918
- required: false,
919
- description: "Machine instance type to use e.g. g1-standard-4t, g1-standard-8t",
920
- env: "ECLOUD_INSTANCE_TYPE"
921
- }),
922
- "skip-profile": Flags2.boolean({
923
- required: false,
924
- description: "Skip app profile setup",
925
- default: false
926
- }),
927
- "resource-usage-monitoring": Flags2.string({
928
- required: false,
929
- description: "Resource usage monitoring: enable or disable",
930
- options: ["enable", "disable"],
931
- env: "ECLOUD_RESOURCE_USAGE_MONITORING"
932
- }),
933
- website: Flags2.string({
934
- required: false,
935
- description: "App website URL (optional)"
936
- }),
937
- description: Flags2.string({
938
- required: false,
939
- description: "App description (optional)"
940
- }),
941
- "x-url": Flags2.string({
942
- required: false,
943
- description: "X (Twitter) profile URL (optional)"
944
- }),
945
- image: Flags2.string({
946
- required: false,
947
- description: "Path to app icon/logo image - JPG/PNG, max 4MB, square recommended (optional)"
948
- })
949
- };
950
- async run() {
951
- const { flags } = await this.parse(_AppDeploy);
952
- const logger = {
953
- info: (msg) => this.log(msg),
954
- warn: (msg) => this.warn(msg),
955
- error: (msg) => this.error(msg),
956
- debug: (msg) => flags.verbose && this.log(msg)
957
- };
958
- const environment = flags.environment || "sepolia";
959
- const environmentConfig = getEnvironmentConfig2(environment);
960
- const rpcUrl = flags["rpc-url"] || environmentConfig.defaultRPCURL;
961
- const privateKey = await getPrivateKeyInteractive(flags["private-key"]);
962
- const dockerfilePath = await getDockerfileInteractive(flags.dockerfile);
963
- const buildFromDockerfile = dockerfilePath !== "";
964
- const imageRef = await getImageReferenceInteractive(flags["image-ref"], buildFromDockerfile);
965
- const appName = await getOrPromptAppName(flags.name, environment, imageRef);
966
- const envFilePath = await getEnvFileInteractive(flags["env-file"]);
967
- const availableTypes = await fetchAvailableInstanceTypes(environmentConfig, privateKey, rpcUrl);
968
- const instanceType = await getInstanceTypeInteractive(
969
- flags["instance-type"],
970
- "",
971
- // No default for new deployments
972
- availableTypes
973
- );
974
- const logSettings = await getLogSettingsInteractive(
975
- flags["log-visibility"]
976
- );
977
- const resourceUsageMonitoring = await getResourceUsageMonitoringInteractive(
978
- flags["resource-usage-monitoring"]
979
- );
980
- const logVisibility = logSettings.publicLogs ? "public" : logSettings.logRedirect ? "private" : "off";
981
- const { prepared, gasEstimate } = await prepareDeploy(
982
- {
983
- privateKey,
984
- rpcUrl,
985
- environment,
986
- dockerfilePath,
987
- imageRef,
988
- envFilePath,
989
- appName,
990
- instanceType,
991
- logVisibility,
992
- resourceUsageMonitoring
993
- },
994
- logger
995
- );
996
- this.log(`
997
- Estimated transaction cost: ${chalk.cyan(gasEstimate.maxCostEth)} ETH`);
998
- if (isMainnet(environmentConfig)) {
999
- const confirmed = await confirm(`Continue with deployment?`);
1000
- if (!confirmed) {
1001
- this.log(`
1002
- ${chalk.gray(`Deployment cancelled`)}`);
1003
- return;
1004
- }
1005
- }
1006
- const res = await executeDeploy(
1007
- prepared,
1008
- {
1009
- maxFeePerGas: gasEstimate.maxFeePerGas,
1010
- maxPriorityFeePerGas: gasEstimate.maxPriorityFeePerGas
1011
- },
1012
- logger
1013
- );
1014
- if (!flags["skip-profile"]) {
1015
- const hasProfileFlags = flags.website || flags.description || flags["x-url"] || flags.image;
1016
- let profile = null;
1017
- if (hasProfileFlags) {
1018
- profile = {
1019
- name: appName,
1020
- website: flags.website,
1021
- description: flags.description,
1022
- xURL: flags["x-url"],
1023
- imagePath: flags.image
1024
- };
1025
- } else {
1026
- this.log(
1027
- "\nDeployment confirmed onchain. While your instance provisions, set up a public profile:"
1028
- );
1029
- try {
1030
- profile = await getAppProfileInteractive(appName, true) || null;
1031
- } catch {
1032
- logger.debug("Profile collection skipped or cancelled");
1033
- }
1034
- }
1035
- if (profile) {
1036
- logger.info("Uploading app profile...");
1037
- try {
1038
- const userApiClient = new UserApiClient3(environmentConfig, privateKey, rpcUrl);
1039
- await userApiClient.uploadAppProfile(
1040
- res.appId,
1041
- profile.name,
1042
- profile.website,
1043
- profile.description,
1044
- profile.xURL,
1045
- profile.imagePath
1046
- );
1047
- logger.info("\u2713 Profile uploaded successfully");
1048
- try {
1049
- invalidateProfileCache(environment);
1050
- } catch (cacheErr) {
1051
- logger.debug(`Failed to invalidate profile cache: ${cacheErr.message}`);
1052
- }
1053
- } catch (uploadErr) {
1054
- logger.warn(`Failed to upload profile: ${uploadErr.message}`);
1055
- }
1056
- }
1057
- }
1058
- const ipAddress = await watchDeployment(res.appId, privateKey, rpcUrl, environment, logger);
1059
- this.log(
1060
- `
1061
- \u2705 ${chalk.green(`App deployed successfully ${chalk.bold(`(id: ${res.appId}, ip: ${ipAddress})`)}`)}`
1062
- );
1063
- }
1064
- };
1065
- async function fetchAvailableInstanceTypes(environmentConfig, privateKey, rpcUrl) {
1066
- try {
1067
- const userApiClient = new UserApiClient3(environmentConfig, privateKey, rpcUrl);
1068
- const skuList = await userApiClient.getSKUs();
1069
- if (skuList.skus.length === 0) {
1070
- throw new Error("No instance types available from server");
1071
- }
1072
- return skuList.skus;
1073
- } catch (err) {
1074
- console.warn(`Failed to fetch instance types: ${err.message}`);
1075
- return [{ sku: "g1-standard-4t", description: "Standard 4-thread instance" }];
1076
- }
1077
- }
1078
- export {
1079
- AppDeploy as default
1080
- };
1081
- //# sourceMappingURL=deploy.js.map