Skip to content

Commit d36091e

Browse files
chore: moved prompts to cli instead of sdk (#24)
Co-authored-by: Solimander <solimander-dev@protonmail.com>
1 parent 03b6d3d commit d36091e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2764
-1905
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"lint": "pnpm -r run lint",
1111
"format": "pnpm -r run format",
1212
"format:fix": "pnpm -r run format:fix",
13+
"typecheck": "pnpm -r run typecheck",
1314
"ecloud": "pnpm -C packages/cli exec -- node ./bin/run.js"
1415
},
1516
"devDependencies": {

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"lint": "eslint .",
3535
"format": "prettier --check .",
3636
"format:fix": "prettier --write .",
37+
"typecheck": "tsc --noEmit",
3738
"ecloud": "node bin/run.js"
3839
},
3940
"oclif": {

packages/cli/src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import {
22
createAppModule,
33
createBillingModule,
4-
getPrivateKeyInteractive,
54
getEnvironmentConfig,
65
requirePrivateKey,
76
getPrivateKeyWithSource,
87
} from "@layr-labs/ecloud-sdk";
98
import { CommonFlags, validateCommonFlags } from "./flags";
9+
import { getPrivateKeyInteractive } from "./utils/prompts";
1010
import { Hex } from "viem";
1111

1212
export async function createAppClient(flags: CommonFlags) {

packages/cli/src/commands/auth/generate.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import {
1010
generateNewPrivateKey,
1111
storePrivateKey,
1212
keyExists,
13-
showPrivateKey,
14-
displayWarning,
1513
} from "@layr-labs/ecloud-sdk";
14+
import { showPrivateKey, displayWarning } from "../../utils/security";
1615

1716
export default class AuthGenerate extends Command {
1817
static description = "Generate a new private key";

packages/cli/src/commands/auth/login.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ import { confirm, select } from "@inquirer/prompts";
99
import {
1010
storePrivateKey,
1111
keyExists,
12-
getHiddenInput,
1312
validatePrivateKey,
1413
getAddressFromPrivateKey,
15-
displayWarning,
1614
getLegacyKeys,
1715
getLegacyPrivateKey,
1816
deleteLegacyPrivateKey,
1917
type LegacyKey,
2018
} from "@layr-labs/ecloud-sdk";
19+
import { getHiddenInput, displayWarning } from "../../utils/security";
2120

2221
export default class AuthLogin extends Command {
2322
static description = "Store your private key in OS keyring";

packages/cli/src/commands/auth/migrate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import {
1010
storePrivateKey,
1111
keyExists,
1212
getAddressFromPrivateKey,
13-
displayWarning,
1413
getLegacyKeys,
1514
getLegacyPrivateKey,
1615
deleteLegacyPrivateKey,
1716
type LegacyKey,
1817
} from "@layr-labs/ecloud-sdk";
18+
import { displayWarning } from "../../utils/security";
1919

2020
export default class AuthMigrate extends Command {
2121
static description = "Migrate a private key from eigenx-cli to ecloud";

packages/cli/src/commands/auth/whoami.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Command } from "@oclif/core";
88
import {
99
getPrivateKeyWithSource,
1010
getAddressFromPrivateKey,
11-
getPrivateKey,
1211
} from "@layr-labs/ecloud-sdk";
1312
import { commonFlags } from "../../flags";
1413

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,64 @@
11
import { Command, Flags } from "@oclif/core";
22
import { createApp } from "@layr-labs/ecloud-sdk";
3+
import {
4+
promptProjectName,
5+
promptLanguage,
6+
selectTemplateInteractive,
7+
} from "../../../utils/prompts";
38

49
export default class AppCreate extends Command {
5-
static description = "Create a new app";
10+
static description = "Create a new app from a template";
611

7-
// CreateApp flags
812
static flags = {
9-
name: Flags.string(),
10-
language: Flags.string(),
11-
template: Flags.string(),
12-
templateVersion: Flags.string(),
13-
verbose: Flags.boolean(),
13+
name: Flags.string({
14+
description: "Project name",
15+
}),
16+
language: Flags.string({
17+
description: "Programming language (typescript, golang, rust, python)",
18+
options: ["typescript", "golang", "rust", "python"],
19+
}),
20+
template: Flags.string({
21+
description: "Template name or custom template URL",
22+
}),
23+
templateVersion: Flags.string({
24+
description: "Template version/ref",
25+
}),
26+
verbose: Flags.boolean({
27+
description: "Verbose output",
28+
default: false,
29+
}),
1430
};
1531

1632
async run() {
1733
const { flags } = await this.parse(AppCreate);
1834

19-
// Skip creating client and call createApp directly
20-
return createApp(flags, {
35+
const logger = {
2136
info: (msg: string, ...args: any[]) => console.log(msg, ...args),
2237
warn: (msg: string, ...args: any[]) => console.warn(msg, ...args),
2338
error: (msg: string, ...args: any[]) => console.error(msg, ...args),
2439
debug: (msg: string, ...args: any[]) =>
2540
flags.verbose && console.debug(msg, ...args),
26-
});
41+
};
42+
43+
// 1. Get project name interactively if not provided
44+
const name = flags.name || await promptProjectName();
45+
46+
// 2. Get language interactively if not provided
47+
const language = flags.language || await promptLanguage();
48+
49+
// 3. Get template interactively if not provided
50+
const template = flags.template || await selectTemplateInteractive(language);
51+
52+
// 4. Call SDK with all gathered parameters
53+
return createApp(
54+
{
55+
name,
56+
language,
57+
template: template || undefined,
58+
templateVersion: flags.templateVersion,
59+
verbose: flags.verbose,
60+
},
61+
logger
62+
);
2763
}
2864
}

packages/cli/src/commands/compute/app/deploy.ts

Lines changed: 152 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import { Command, Flags } from "@oclif/core";
2-
import { logVisibility } from "@layr-labs/ecloud-sdk";
2+
import { getEnvironmentConfig, UserApiClient, formatETH, isMainnet } from "@layr-labs/ecloud-sdk";
3+
import { createPublicClient, http } from "viem";
4+
import { mainnet } from "viem/chains";
35
import { createAppClient } from "../../../client";
46
import { commonFlags } from "../../../flags";
7+
import {
8+
getDockerfileInteractive,
9+
getImageReferenceInteractive,
10+
getOrPromptAppName,
11+
getEnvFileInteractive,
12+
getInstanceTypeInteractive,
13+
getLogSettingsInteractive,
14+
getAppProfileInteractive,
15+
LogVisibility,
16+
confirm,
17+
} from "../../../utils/prompts";
518
import chalk from "chalk";
619

720
export default class AppDeploy extends Command {
@@ -40,28 +53,157 @@ export default class AppDeploy extends Command {
4053
required: false,
4154
description:
4255
"Machine instance type to use e.g. g1-standard-4t, g1-standard-8t",
43-
options: ["g1-standard-4t", "g1-standard-8t"],
4456
env: "ECLOUD_INSTANCE_TYPE",
4557
}),
58+
"skip-profile": Flags.boolean({
59+
required: false,
60+
description: "Skip app profile setup",
61+
default: false,
62+
}),
4663
};
4764

4865
async run() {
4966
const { flags } = await this.parse(AppDeploy);
5067
const app = await createAppClient(flags);
5168

69+
// Get environment config for fetching available instance types
70+
const environment = flags.environment || "sepolia";
71+
const environmentConfig = getEnvironmentConfig(environment);
72+
const rpcUrl = flags["rpc-url"] || environmentConfig.defaultRPCURL;
73+
74+
// 1. Get dockerfile path interactively
75+
const dockerfilePath = await getDockerfileInteractive(flags.dockerfile);
76+
const buildFromDockerfile = dockerfilePath !== "";
77+
78+
// 2. Get image reference interactively (context-aware)
79+
const imageRef = await getImageReferenceInteractive(
80+
flags["image-ref"],
81+
buildFromDockerfile
82+
);
83+
84+
// 3. Get app name interactively
85+
const appName = await getOrPromptAppName(flags.name, environment, imageRef);
86+
87+
// 4. Get env file path interactively
88+
const envFilePath = await getEnvFileInteractive(flags["env-file"]);
89+
90+
// 5. Get instance type interactively
91+
// First, fetch available instance types from backend
92+
const availableTypes = await fetchAvailableInstanceTypes(
93+
environmentConfig,
94+
flags["private-key"],
95+
rpcUrl
96+
);
97+
const instanceType = await getInstanceTypeInteractive(
98+
flags["instance-type"],
99+
"", // No default for new deployments
100+
availableTypes
101+
);
102+
103+
// 6. Get log visibility interactively
104+
const logSettings = await getLogSettingsInteractive(
105+
flags["log-visibility"] as LogVisibility | undefined
106+
);
107+
108+
// 7. Optionally collect app profile
109+
let profile = undefined;
110+
if (!flags["skip-profile"]) {
111+
try {
112+
// Extract suggested name from image reference
113+
const suggestedName = appName;
114+
this.log(
115+
"\nSet up a public profile for your app (you can skip this):"
116+
);
117+
profile = await getAppProfileInteractive(suggestedName, true);
118+
} catch {
119+
// Profile collection cancelled or failed - continue without profile
120+
profile = undefined;
121+
}
122+
}
123+
124+
// 8. Estimate gas cost on mainnet and prompt for confirmation
125+
let gasParams: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint } | undefined;
126+
127+
if (isMainnet(environmentConfig)) {
128+
const chain = mainnet;
129+
const publicClient = createPublicClient({
130+
chain,
131+
transport: http(rpcUrl),
132+
});
133+
134+
// Get current gas prices for estimation
135+
const fees = await publicClient.estimateFeesPerGas();
136+
// Deploy typically has 2-3 executions in the batch
137+
const estimatedGas = BigInt(300000); // Conservative estimate for deploy batch
138+
const maxCostWei = estimatedGas * fees.maxFeePerGas;
139+
const maxCostEth = formatETH(maxCostWei);
140+
141+
const confirmed = await confirm(
142+
`This deployment will cost up to ${maxCostEth} ETH. Continue?`
143+
);
144+
if (!confirmed) {
145+
this.log(`\n${chalk.gray(`Deployment cancelled`)}`);
146+
return;
147+
}
148+
149+
gasParams = {
150+
maxFeePerGas: fees.maxFeePerGas,
151+
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
152+
};
153+
}
154+
155+
// 9. Deploy with all gathered parameters
52156
const res = await app.deploy({
53-
name: flags.name,
54-
dockerfile: flags.dockerfile,
55-
envFile: flags["env-file"],
56-
imageRef: flags["image-ref"],
57-
logVisibility: flags["log-visibility"] as logVisibility,
58-
instanceType: flags["instance-type"],
157+
name: appName,
158+
dockerfile: dockerfilePath,
159+
envFile: envFilePath,
160+
imageRef: imageRef,
161+
logVisibility: logSettings.publicLogs
162+
? "public"
163+
: logSettings.logRedirect
164+
? "private"
165+
: "off",
166+
instanceType,
167+
profile,
168+
gas: gasParams,
59169
});
60170

61171
if (!res.tx || !res.ipAddress) {
62-
this.log(`\n${chalk.gray(`Deploy ${res.ipAddress ? "failed" : "aborted"}`)}`);
172+
this.log(
173+
`\n${chalk.gray(`Deploy ${res.ipAddress ? "failed" : "aborted"}`)}`
174+
);
63175
} else {
64-
this.log(`\n✅ ${chalk.green(`App deployed successfully ${chalk.bold(`(id: ${res.appID}, ip: ${res.ipAddress})`)}`)}`);
176+
this.log(
177+
`\n✅ ${chalk.green(`App deployed successfully ${chalk.bold(`(id: ${res.appID}, ip: ${res.ipAddress})`)}`)}`
178+
);
65179
}
66180
}
67181
}
182+
183+
/**
184+
* Fetch available instance types from backend
185+
*/
186+
async function fetchAvailableInstanceTypes(
187+
environmentConfig: any,
188+
privateKey?: string,
189+
rpcUrl?: string
190+
): Promise<Array<{ sku: string; description: string }>> {
191+
try {
192+
const userApiClient = new UserApiClient(
193+
environmentConfig,
194+
privateKey,
195+
rpcUrl
196+
);
197+
198+
const skuList = await userApiClient.getSKUs();
199+
if (skuList.skus.length === 0) {
200+
throw new Error("No instance types available from server");
201+
}
202+
203+
return skuList.skus;
204+
} catch (err: any) {
205+
console.warn(`Failed to fetch instance types: ${err.message}`);
206+
// Return a default fallback
207+
return [{ sku: "g1-standard-4t", description: "Standard 4-thread instance" }];
208+
}
209+
}

packages/cli/src/commands/compute/app/logs.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Command, Args, Flags } from "@oclif/core";
2+
import { getEnvironmentConfig } from "@layr-labs/ecloud-sdk";
23
import { createAppClient } from "../../../client";
34
import { commonFlags } from "../../../flags";
5+
import { getOrPromptAppID } from "../../../utils/prompts";
46

57
export default class AppLogs extends Command {
68
static description = "View app logs";
@@ -25,10 +27,24 @@ export default class AppLogs extends Command {
2527
const { args, flags } = await this.parse(AppLogs);
2628
const app = await createAppClient(flags);
2729

28-
await app.logs({
30+
// Get environment config
31+
const environment = flags.environment || "sepolia";
32+
const environmentConfig = getEnvironmentConfig(environment);
33+
const rpcUrl = flags["rpc-url"] || environmentConfig.defaultRPCURL;
34+
35+
// Get app ID interactively if not provided
36+
const appID = await getOrPromptAppID({
2937
appID: args["app-id"],
38+
environment,
39+
privateKey: flags["private-key"],
40+
rpcUrl,
41+
action: "view logs for",
42+
});
43+
44+
// Call SDK with the resolved app ID
45+
await app.logs({
46+
appID,
3047
watch: flags.watch,
3148
});
3249
}
3350
}
34-

0 commit comments

Comments
 (0)