@layr-labs/ecloud-cli 0.1.2 → 0.2.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.
- package/VERSION +2 -2
- package/dist/commands/auth/generate.js.map +1 -1
- package/dist/commands/auth/login.js.map +1 -1
- package/dist/commands/auth/logout.js.map +1 -1
- package/dist/commands/auth/migrate.js.map +1 -1
- package/dist/commands/auth/whoami.js.map +1 -1
- package/dist/commands/billing/cancel.js +1 -0
- package/dist/commands/billing/cancel.js.map +1 -1
- package/dist/commands/billing/status.js +1 -0
- package/dist/commands/billing/status.js.map +1 -1
- package/dist/commands/billing/subscribe.js +2 -1
- package/dist/commands/billing/subscribe.js.map +1 -1
- package/dist/commands/compute/app/create.js.map +1 -1
- package/dist/commands/compute/app/deploy.js +477 -16
- package/dist/commands/compute/app/deploy.js.map +1 -1
- package/dist/commands/compute/app/info.js +6 -3
- package/dist/commands/compute/app/info.js.map +1 -1
- package/dist/commands/compute/app/list.js +4 -2
- package/dist/commands/compute/app/list.js.map +1 -1
- package/dist/commands/compute/app/logs.js +7 -3
- package/dist/commands/compute/app/logs.js.map +1 -1
- package/dist/commands/compute/app/profile/set.js +7 -3
- package/dist/commands/compute/app/profile/set.js.map +1 -1
- package/dist/commands/compute/app/releases.js +1111 -0
- package/dist/commands/compute/app/releases.js.map +1 -0
- package/dist/commands/compute/app/start.js +7 -3
- package/dist/commands/compute/app/start.js.map +1 -1
- package/dist/commands/compute/app/stop.js +7 -3
- package/dist/commands/compute/app/stop.js.map +1 -1
- package/dist/commands/compute/app/terminate.js +7 -3
- package/dist/commands/compute/app/terminate.js.map +1 -1
- package/dist/commands/compute/app/upgrade.js +449 -9
- package/dist/commands/compute/app/upgrade.js.map +1 -1
- package/dist/commands/compute/build/info.js +500 -0
- package/dist/commands/compute/build/info.js.map +1 -0
- package/dist/commands/compute/build/list.js +494 -0
- package/dist/commands/compute/build/list.js.map +1 -0
- package/dist/commands/compute/build/logs.js +459 -0
- package/dist/commands/compute/build/logs.js.map +1 -0
- package/dist/commands/compute/build/status.js +481 -0
- package/dist/commands/compute/build/status.js.map +1 -0
- package/dist/commands/compute/build/submit.js +618 -0
- package/dist/commands/compute/build/submit.js.map +1 -0
- package/dist/commands/compute/build/verify.js +439 -0
- package/dist/commands/compute/build/verify.js.map +1 -0
- package/dist/commands/compute/environment/list.js.map +1 -1
- package/dist/commands/compute/environment/set.js.map +1 -1
- package/dist/commands/compute/environment/show.js.map +1 -1
- package/dist/commands/compute/undelegate.js +6 -3
- package/dist/commands/compute/undelegate.js.map +1 -1
- package/dist/commands/telemetry/disable.js.map +1 -1
- package/dist/commands/telemetry/enable.js.map +1 -1
- package/dist/commands/telemetry/status.js.map +1 -1
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/version.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/compute/app/releases.ts
|
|
4
|
+
import { Command, Args, Flags as Flags2 } from "@oclif/core";
|
|
5
|
+
import { getEnvironmentConfig as getEnvironmentConfig2, UserApiClient as UserApiClient3 } from "@layr-labs/ecloud-sdk";
|
|
6
|
+
|
|
7
|
+
// src/flags.ts
|
|
8
|
+
import { Flags } from "@oclif/core";
|
|
9
|
+
import { getBuildType as getBuildType2 } from "@layr-labs/ecloud-sdk";
|
|
10
|
+
|
|
11
|
+
// src/utils/prompts.ts
|
|
12
|
+
import { input, select, password, confirm as inquirerConfirm } from "@inquirer/prompts";
|
|
13
|
+
import fs3 from "fs";
|
|
14
|
+
import path3 from "path";
|
|
15
|
+
import os3 from "os";
|
|
16
|
+
import { isAddress as isAddress2 } from "viem";
|
|
17
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
18
|
+
import {
|
|
19
|
+
getEnvironmentConfig,
|
|
20
|
+
getAvailableEnvironments,
|
|
21
|
+
isEnvironmentAvailable,
|
|
22
|
+
getAllAppsByDeveloper as getAllAppsByDeveloper2,
|
|
23
|
+
getCategoryDescriptions,
|
|
24
|
+
fetchTemplateCatalog,
|
|
25
|
+
PRIMARY_LANGUAGES,
|
|
26
|
+
validateAppName,
|
|
27
|
+
validateImageReference,
|
|
28
|
+
validateFilePath,
|
|
29
|
+
validatePrivateKeyFormat,
|
|
30
|
+
extractAppNameFromImage,
|
|
31
|
+
UserApiClient as UserApiClient2
|
|
32
|
+
} from "@layr-labs/ecloud-sdk";
|
|
33
|
+
|
|
34
|
+
// src/utils/appResolver.ts
|
|
35
|
+
import { isAddress } from "viem";
|
|
36
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
37
|
+
import {
|
|
38
|
+
UserApiClient,
|
|
39
|
+
getAllAppsByDeveloper
|
|
40
|
+
} from "@layr-labs/ecloud-sdk";
|
|
41
|
+
|
|
42
|
+
// src/utils/globalConfig.ts
|
|
43
|
+
import * as fs from "fs";
|
|
44
|
+
import * as path from "path";
|
|
45
|
+
import * as os from "os";
|
|
46
|
+
import { load as loadYaml, dump as dumpYaml } from "js-yaml";
|
|
47
|
+
import { getBuildType } from "@layr-labs/ecloud-sdk";
|
|
48
|
+
import * as crypto from "crypto";
|
|
49
|
+
var GLOBAL_CONFIG_FILE = "config.yaml";
|
|
50
|
+
var PROFILE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
51
|
+
function getGlobalConfigDir() {
|
|
52
|
+
const configHome = process.env.XDG_CONFIG_HOME;
|
|
53
|
+
let baseDir;
|
|
54
|
+
if (configHome && path.isAbsolute(configHome)) {
|
|
55
|
+
baseDir = configHome;
|
|
56
|
+
} else {
|
|
57
|
+
baseDir = path.join(os.homedir(), ".config");
|
|
58
|
+
}
|
|
59
|
+
const buildType = getBuildType();
|
|
60
|
+
const buildSuffix = buildType === "dev" ? "-dev" : "";
|
|
61
|
+
const configDirName = `ecloud${buildSuffix}`;
|
|
62
|
+
return path.join(baseDir, configDirName);
|
|
63
|
+
}
|
|
64
|
+
function getGlobalConfigPath() {
|
|
65
|
+
return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
|
|
66
|
+
}
|
|
67
|
+
function loadGlobalConfig() {
|
|
68
|
+
const configPath = getGlobalConfigPath();
|
|
69
|
+
if (!fs.existsSync(configPath)) {
|
|
70
|
+
return {
|
|
71
|
+
first_run: true
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
76
|
+
const config = loadYaml(content);
|
|
77
|
+
return config || { first_run: true };
|
|
78
|
+
} catch {
|
|
79
|
+
return {
|
|
80
|
+
first_run: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function saveGlobalConfig(config) {
|
|
85
|
+
const configPath = getGlobalConfigPath();
|
|
86
|
+
const configDir = path.dirname(configPath);
|
|
87
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 493 });
|
|
88
|
+
const content = dumpYaml(config, { lineWidth: -1 });
|
|
89
|
+
fs.writeFileSync(configPath, content, { mode: 420 });
|
|
90
|
+
}
|
|
91
|
+
function normalizeDirectoryPath(directoryPath) {
|
|
92
|
+
const resolved = path.resolve(directoryPath);
|
|
93
|
+
try {
|
|
94
|
+
return fs.realpathSync(resolved);
|
|
95
|
+
} catch {
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function getLinkedAppForDirectory(environment, directoryPath) {
|
|
100
|
+
if (!directoryPath) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const config = loadGlobalConfig();
|
|
104
|
+
const links = config.directory_links?.[environment];
|
|
105
|
+
if (!links) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const normalizedPath = normalizeDirectoryPath(directoryPath);
|
|
109
|
+
const appId = links[normalizedPath];
|
|
110
|
+
return appId || null;
|
|
111
|
+
}
|
|
112
|
+
function getDefaultEnvironment() {
|
|
113
|
+
const config = loadGlobalConfig();
|
|
114
|
+
return config.default_environment;
|
|
115
|
+
}
|
|
116
|
+
function getGlobalTelemetryPreference() {
|
|
117
|
+
const config = loadGlobalConfig();
|
|
118
|
+
return config.telemetry_enabled;
|
|
119
|
+
}
|
|
120
|
+
function getProfileCache(environment) {
|
|
121
|
+
const config = loadGlobalConfig();
|
|
122
|
+
const cacheEntry = config.profile_cache?.[environment];
|
|
123
|
+
if (!cacheEntry) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
if (now - cacheEntry.updated_at > PROFILE_CACHE_TTL_MS) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return cacheEntry.profiles;
|
|
131
|
+
}
|
|
132
|
+
function setProfileCache(environment, profiles) {
|
|
133
|
+
const config = loadGlobalConfig();
|
|
134
|
+
if (!config.profile_cache) {
|
|
135
|
+
config.profile_cache = {};
|
|
136
|
+
}
|
|
137
|
+
config.profile_cache[environment] = {
|
|
138
|
+
updated_at: Date.now(),
|
|
139
|
+
profiles
|
|
140
|
+
};
|
|
141
|
+
saveGlobalConfig(config);
|
|
142
|
+
}
|
|
143
|
+
function getOrCreateUserUUID() {
|
|
144
|
+
const config = loadGlobalConfig();
|
|
145
|
+
if (config.user_uuid) {
|
|
146
|
+
return config.user_uuid;
|
|
147
|
+
}
|
|
148
|
+
const uuid = generateUUID();
|
|
149
|
+
config.user_uuid = uuid;
|
|
150
|
+
config.first_run = false;
|
|
151
|
+
saveGlobalConfig(config);
|
|
152
|
+
return uuid;
|
|
153
|
+
}
|
|
154
|
+
function generateUUID() {
|
|
155
|
+
const bytes = crypto.randomBytes(16);
|
|
156
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
157
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
158
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
159
|
+
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("");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/utils/appNames.ts
|
|
163
|
+
import * as fs2 from "fs";
|
|
164
|
+
import * as path2 from "path";
|
|
165
|
+
import * as os2 from "os";
|
|
166
|
+
import { load as loadYaml2, dump as dumpYaml2 } from "js-yaml";
|
|
167
|
+
var CONFIG_DIR = path2.join(os2.homedir(), ".eigenx");
|
|
168
|
+
var APPS_DIR = path2.join(CONFIG_DIR, "apps");
|
|
169
|
+
var APP_REGISTRY_VERSION = "1.0.0";
|
|
170
|
+
function getAppRegistryPath(environment) {
|
|
171
|
+
return path2.join(APPS_DIR, `${environment}.yaml`);
|
|
172
|
+
}
|
|
173
|
+
function loadAppRegistry(environment) {
|
|
174
|
+
const filePath = getAppRegistryPath(environment);
|
|
175
|
+
if (!fs2.existsSync(filePath)) {
|
|
176
|
+
return {
|
|
177
|
+
version: APP_REGISTRY_VERSION,
|
|
178
|
+
apps: {}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
183
|
+
const registry = loadYaml2(content);
|
|
184
|
+
if (!registry.apps) {
|
|
185
|
+
registry.apps = {};
|
|
186
|
+
}
|
|
187
|
+
return registry;
|
|
188
|
+
} catch {
|
|
189
|
+
return {
|
|
190
|
+
version: APP_REGISTRY_VERSION,
|
|
191
|
+
apps: {}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function listApps(environment) {
|
|
196
|
+
const registry = loadAppRegistry(environment);
|
|
197
|
+
const result = {};
|
|
198
|
+
for (const [name, app] of Object.entries(registry.apps)) {
|
|
199
|
+
if (app?.app_id) {
|
|
200
|
+
result[name] = String(app.app_id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/utils/version.ts
|
|
207
|
+
function getCliVersion() {
|
|
208
|
+
return true ? "0.2.0-dev.1" : "0.0.0";
|
|
209
|
+
}
|
|
210
|
+
function getClientId() {
|
|
211
|
+
return `ecloud-cli/v${getCliVersion()}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/utils/appResolver.ts
|
|
215
|
+
var CHUNK_SIZE = 10;
|
|
216
|
+
async function getAppInfosChunked(userApiClient, appIds, addressCount) {
|
|
217
|
+
if (appIds.length === 0) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const chunks = [];
|
|
221
|
+
for (let i = 0; i < appIds.length; i += CHUNK_SIZE) {
|
|
222
|
+
chunks.push(appIds.slice(i, i + CHUNK_SIZE));
|
|
223
|
+
}
|
|
224
|
+
const chunkResults = await Promise.all(
|
|
225
|
+
chunks.map((chunk) => userApiClient.getInfos(chunk, addressCount))
|
|
226
|
+
);
|
|
227
|
+
return chunkResults.flat();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/utils/prompts.ts
|
|
231
|
+
function addHexPrefix(value) {
|
|
232
|
+
if (value.startsWith("0x")) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
return `0x${value}`;
|
|
236
|
+
}
|
|
237
|
+
function getCurrentProjectPath() {
|
|
238
|
+
return process.env.INIT_CWD || process.cwd();
|
|
239
|
+
}
|
|
240
|
+
var ContractAppStatusStarted = 1;
|
|
241
|
+
var ContractAppStatusStopped = 2;
|
|
242
|
+
var ContractAppStatusTerminated = 3;
|
|
243
|
+
var ContractAppStatusSuspended = 4;
|
|
244
|
+
function getContractStatusString(status) {
|
|
245
|
+
switch (status) {
|
|
246
|
+
case ContractAppStatusStarted:
|
|
247
|
+
return "Started";
|
|
248
|
+
case ContractAppStatusStopped:
|
|
249
|
+
return "Stopped";
|
|
250
|
+
case ContractAppStatusTerminated:
|
|
251
|
+
return "Terminated";
|
|
252
|
+
case ContractAppStatusSuspended:
|
|
253
|
+
return "Suspended";
|
|
254
|
+
default:
|
|
255
|
+
return "Unknown";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function getStatusPriority(status, isExited) {
|
|
259
|
+
if (isExited) {
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
switch (status) {
|
|
263
|
+
case ContractAppStatusStarted:
|
|
264
|
+
return 0;
|
|
265
|
+
case ContractAppStatusStopped:
|
|
266
|
+
return 2;
|
|
267
|
+
case ContractAppStatusTerminated:
|
|
268
|
+
return 3;
|
|
269
|
+
default:
|
|
270
|
+
return 4;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function formatAppDisplay(environmentName, appID, profileName) {
|
|
274
|
+
if (profileName) {
|
|
275
|
+
return `${profileName} (${environmentName}:${appID})`;
|
|
276
|
+
}
|
|
277
|
+
return `${environmentName}:${appID}`;
|
|
278
|
+
}
|
|
279
|
+
async function getOrPromptAppID(appIDOrOptions, environment) {
|
|
280
|
+
let options;
|
|
281
|
+
if (environment !== void 0) {
|
|
282
|
+
options = {
|
|
283
|
+
appID: appIDOrOptions,
|
|
284
|
+
environment
|
|
285
|
+
};
|
|
286
|
+
} else if (appIDOrOptions && typeof appIDOrOptions === "object" && "environment" in appIDOrOptions) {
|
|
287
|
+
options = appIDOrOptions;
|
|
288
|
+
} else {
|
|
289
|
+
options = {
|
|
290
|
+
appID: appIDOrOptions,
|
|
291
|
+
environment: "sepolia"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (options.appID) {
|
|
295
|
+
const normalized = typeof options.appID === "string" ? addHexPrefix(options.appID) : options.appID;
|
|
296
|
+
if (isAddress2(normalized)) {
|
|
297
|
+
return normalized;
|
|
298
|
+
}
|
|
299
|
+
const profileCache = getProfileCache(options.environment);
|
|
300
|
+
if (profileCache) {
|
|
301
|
+
const searchName = options.appID.toLowerCase();
|
|
302
|
+
for (const [appId, name] of Object.entries(profileCache)) {
|
|
303
|
+
if (name.toLowerCase() === searchName) {
|
|
304
|
+
return appId;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const apps = listApps(options.environment);
|
|
309
|
+
const foundAppID = apps[options.appID];
|
|
310
|
+
if (foundAppID) {
|
|
311
|
+
return addHexPrefix(foundAppID);
|
|
312
|
+
}
|
|
313
|
+
throw new Error(
|
|
314
|
+
`App name '${options.appID}' not found in environment '${options.environment}'`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return getAppIDInteractive(options);
|
|
318
|
+
}
|
|
319
|
+
async function getAppIDInteractive(options) {
|
|
320
|
+
const action = options.action || "view";
|
|
321
|
+
const environment = options.environment || "sepolia";
|
|
322
|
+
const environmentConfig = getEnvironmentConfig(environment);
|
|
323
|
+
if (!options.privateKey || !options.rpcUrl) {
|
|
324
|
+
return getAppIDInteractiveFromRegistry(environment, action);
|
|
325
|
+
}
|
|
326
|
+
console.log(`
|
|
327
|
+
Select an app to ${action}:
|
|
328
|
+
`);
|
|
329
|
+
const privateKeyHex = addHexPrefix(options.privateKey);
|
|
330
|
+
const account = privateKeyToAccount2(privateKeyHex);
|
|
331
|
+
const developerAddr = account.address;
|
|
332
|
+
const { apps, appConfigs } = await getAllAppsByDeveloper2(
|
|
333
|
+
options.rpcUrl,
|
|
334
|
+
environmentConfig,
|
|
335
|
+
developerAddr
|
|
336
|
+
);
|
|
337
|
+
if (apps.length === 0) {
|
|
338
|
+
throw new Error("no apps found for your address");
|
|
339
|
+
}
|
|
340
|
+
const profileNames = {};
|
|
341
|
+
let cachedProfiles = getProfileCache(environment);
|
|
342
|
+
if (!cachedProfiles) {
|
|
343
|
+
try {
|
|
344
|
+
const userApiClient = new UserApiClient2(
|
|
345
|
+
environmentConfig,
|
|
346
|
+
options.privateKey,
|
|
347
|
+
options.rpcUrl,
|
|
348
|
+
getClientId()
|
|
349
|
+
);
|
|
350
|
+
const appInfos = await getAppInfosChunked(userApiClient, apps);
|
|
351
|
+
const freshProfiles = {};
|
|
352
|
+
for (const info of appInfos) {
|
|
353
|
+
if (info.profile?.name) {
|
|
354
|
+
const normalizedId = String(info.address).toLowerCase();
|
|
355
|
+
freshProfiles[normalizedId] = info.profile.name;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
setProfileCache(environment, freshProfiles);
|
|
359
|
+
cachedProfiles = freshProfiles;
|
|
360
|
+
} catch {
|
|
361
|
+
cachedProfiles = {};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const [appId, name] of Object.entries(cachedProfiles)) {
|
|
365
|
+
profileNames[appId.toLowerCase()] = name;
|
|
366
|
+
}
|
|
367
|
+
const localApps = listApps(environment);
|
|
368
|
+
for (const [name, appID] of Object.entries(localApps)) {
|
|
369
|
+
const normalizedID = String(appID).toLowerCase();
|
|
370
|
+
if (!profileNames[normalizedID]) {
|
|
371
|
+
profileNames[normalizedID] = name;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const isEligible = (status) => {
|
|
375
|
+
switch (action) {
|
|
376
|
+
case "view":
|
|
377
|
+
case "view info for":
|
|
378
|
+
case "view releases for":
|
|
379
|
+
case "set profile for":
|
|
380
|
+
return true;
|
|
381
|
+
case "start":
|
|
382
|
+
return status === ContractAppStatusStopped || status === ContractAppStatusSuspended;
|
|
383
|
+
case "stop":
|
|
384
|
+
return status === ContractAppStatusStarted;
|
|
385
|
+
default:
|
|
386
|
+
return status !== ContractAppStatusTerminated && status !== ContractAppStatusSuspended;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const appItems = [];
|
|
390
|
+
for (let i = 0; i < apps.length; i++) {
|
|
391
|
+
const appAddr = apps[i];
|
|
392
|
+
const config = appConfigs[i];
|
|
393
|
+
const status = config.status;
|
|
394
|
+
if (!isEligible(status)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const statusStr = getContractStatusString(status);
|
|
398
|
+
const profileName = profileNames[String(appAddr).toLowerCase()] || "";
|
|
399
|
+
const displayName = formatAppDisplay(environmentConfig.name, appAddr, profileName);
|
|
400
|
+
appItems.push({
|
|
401
|
+
addr: appAddr,
|
|
402
|
+
display: `${displayName} - ${statusStr}`,
|
|
403
|
+
status,
|
|
404
|
+
index: i
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const linkedAppId = getLinkedAppForDirectory(environment, getCurrentProjectPath());
|
|
408
|
+
const normalizedLinkedAppId = linkedAppId ? linkedAppId.toLowerCase() : "";
|
|
409
|
+
appItems.sort((a, b) => {
|
|
410
|
+
if (normalizedLinkedAppId) {
|
|
411
|
+
const aLinked = String(a.addr).toLowerCase() === normalizedLinkedAppId;
|
|
412
|
+
const bLinked = String(b.addr).toLowerCase() === normalizedLinkedAppId;
|
|
413
|
+
if (aLinked && !bLinked) {
|
|
414
|
+
return -1;
|
|
415
|
+
}
|
|
416
|
+
if (bLinked && !aLinked) {
|
|
417
|
+
return 1;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const aPriority = getStatusPriority(a.status, false);
|
|
421
|
+
const bPriority = getStatusPriority(b.status, false);
|
|
422
|
+
if (aPriority !== bPriority) {
|
|
423
|
+
return aPriority - bPriority;
|
|
424
|
+
}
|
|
425
|
+
return b.index - a.index;
|
|
426
|
+
});
|
|
427
|
+
if (appItems.length === 0) {
|
|
428
|
+
switch (action) {
|
|
429
|
+
case "start":
|
|
430
|
+
throw new Error("no startable apps found - only Stopped apps can be started");
|
|
431
|
+
case "stop":
|
|
432
|
+
throw new Error("no running apps found - only Running apps can be stopped");
|
|
433
|
+
default:
|
|
434
|
+
throw new Error("no active apps found");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const choices = appItems.map((item) => ({
|
|
438
|
+
name: item.display,
|
|
439
|
+
value: item.addr
|
|
440
|
+
}));
|
|
441
|
+
const selected = await select({
|
|
442
|
+
message: "Select app:",
|
|
443
|
+
choices
|
|
444
|
+
});
|
|
445
|
+
return selected;
|
|
446
|
+
}
|
|
447
|
+
async function getAppIDInteractiveFromRegistry(environment, action) {
|
|
448
|
+
const allApps = {};
|
|
449
|
+
const cachedProfiles = getProfileCache(environment);
|
|
450
|
+
if (cachedProfiles) {
|
|
451
|
+
for (const [appId, name] of Object.entries(cachedProfiles)) {
|
|
452
|
+
allApps[name] = appId;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const localApps = listApps(environment);
|
|
456
|
+
for (const [name, appId] of Object.entries(localApps)) {
|
|
457
|
+
if (!allApps[name]) {
|
|
458
|
+
allApps[name] = appId;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (Object.keys(allApps).length === 0) {
|
|
462
|
+
console.log("\nNo apps found in registry.");
|
|
463
|
+
console.log("You can enter an app ID (address) or app name.");
|
|
464
|
+
console.log();
|
|
465
|
+
const appIDInput = await input({
|
|
466
|
+
message: "Enter app ID or name:",
|
|
467
|
+
default: "",
|
|
468
|
+
validate: (value) => {
|
|
469
|
+
if (!value) {
|
|
470
|
+
return "App ID or name cannot be empty";
|
|
471
|
+
}
|
|
472
|
+
const normalized2 = addHexPrefix(value);
|
|
473
|
+
if (isAddress2(normalized2)) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
return "Invalid app ID address";
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
const normalized = addHexPrefix(appIDInput);
|
|
480
|
+
if (isAddress2(normalized)) {
|
|
481
|
+
return normalized;
|
|
482
|
+
}
|
|
483
|
+
throw new Error(`Invalid app ID address: ${appIDInput}`);
|
|
484
|
+
}
|
|
485
|
+
const entries = Object.entries(allApps);
|
|
486
|
+
const linkedAppId = getLinkedAppForDirectory(environment, getCurrentProjectPath());
|
|
487
|
+
if (linkedAppId) {
|
|
488
|
+
const linkedIndex = entries.findIndex(
|
|
489
|
+
([, appId]) => String(appId).toLowerCase() === linkedAppId.toLowerCase()
|
|
490
|
+
);
|
|
491
|
+
if (linkedIndex > 0) {
|
|
492
|
+
const [linkedEntry] = entries.splice(linkedIndex, 1);
|
|
493
|
+
entries.unshift(linkedEntry);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const choices = entries.map(([name, appID]) => {
|
|
497
|
+
const displayName = `${name} (${appID})`;
|
|
498
|
+
return { name: displayName, value: appID };
|
|
499
|
+
});
|
|
500
|
+
choices.push({ name: "Enter custom app ID or name", value: "custom" });
|
|
501
|
+
console.log(`
|
|
502
|
+
Select an app to ${action}:`);
|
|
503
|
+
const selected = await select({
|
|
504
|
+
message: "Choose app:",
|
|
505
|
+
choices
|
|
506
|
+
});
|
|
507
|
+
if (selected === "custom") {
|
|
508
|
+
const appIDInput = await input({
|
|
509
|
+
message: "Enter app ID or name:",
|
|
510
|
+
default: "",
|
|
511
|
+
validate: (value) => {
|
|
512
|
+
if (!value) {
|
|
513
|
+
return "App ID or name cannot be empty";
|
|
514
|
+
}
|
|
515
|
+
const normalized2 = addHexPrefix(value);
|
|
516
|
+
if (isAddress2(normalized2)) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
if (allApps[value]) {
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
return "Invalid app ID or name not found";
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
const normalized = addHexPrefix(appIDInput);
|
|
526
|
+
if (isAddress2(normalized)) {
|
|
527
|
+
return normalized;
|
|
528
|
+
}
|
|
529
|
+
const foundAppID = allApps[appIDInput];
|
|
530
|
+
if (foundAppID) {
|
|
531
|
+
return addHexPrefix(foundAppID);
|
|
532
|
+
}
|
|
533
|
+
throw new Error(`Failed to resolve app ID from input: ${appIDInput}`);
|
|
534
|
+
}
|
|
535
|
+
return addHexPrefix(selected);
|
|
536
|
+
}
|
|
537
|
+
async function getPrivateKeyInteractive(privateKey) {
|
|
538
|
+
if (privateKey) {
|
|
539
|
+
if (!validatePrivateKeyFormat(privateKey)) {
|
|
540
|
+
throw new Error("Invalid private key format");
|
|
541
|
+
}
|
|
542
|
+
return privateKey;
|
|
543
|
+
}
|
|
544
|
+
const { getPrivateKeyWithSource } = await import("@layr-labs/ecloud-sdk");
|
|
545
|
+
const result = await getPrivateKeyWithSource({ privateKey: void 0 });
|
|
546
|
+
if (result) {
|
|
547
|
+
return result.key;
|
|
548
|
+
}
|
|
549
|
+
const key = await password({
|
|
550
|
+
message: "Enter private key:",
|
|
551
|
+
mask: true,
|
|
552
|
+
validate: (value) => {
|
|
553
|
+
if (!value.trim()) {
|
|
554
|
+
return "Private key is required";
|
|
555
|
+
}
|
|
556
|
+
if (!validatePrivateKeyFormat(value)) {
|
|
557
|
+
return "Invalid private key format (must be 64 hex characters, optionally prefixed with 0x)";
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
return key.trim();
|
|
563
|
+
}
|
|
564
|
+
async function getEnvironmentInteractive(environment) {
|
|
565
|
+
if (environment) {
|
|
566
|
+
try {
|
|
567
|
+
getEnvironmentConfig(environment);
|
|
568
|
+
if (!isEnvironmentAvailable(environment)) {
|
|
569
|
+
throw new Error(`Environment ${environment} is not available in this build`);
|
|
570
|
+
}
|
|
571
|
+
return environment;
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const availableEnvs = getAvailableEnvironments();
|
|
576
|
+
let defaultEnv;
|
|
577
|
+
const configDefaultEnv = getDefaultEnvironment();
|
|
578
|
+
if (configDefaultEnv && availableEnvs.includes(configDefaultEnv)) {
|
|
579
|
+
try {
|
|
580
|
+
getEnvironmentConfig(configDefaultEnv);
|
|
581
|
+
defaultEnv = configDefaultEnv;
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const choices = [];
|
|
586
|
+
if (availableEnvs.includes("sepolia")) {
|
|
587
|
+
choices.push({ name: "sepolia - Ethereum Sepolia testnet", value: "sepolia" });
|
|
588
|
+
}
|
|
589
|
+
if (availableEnvs.includes("sepolia-dev")) {
|
|
590
|
+
choices.push({ name: "sepolia-dev - Ethereum Sepolia testnet (dev)", value: "sepolia-dev" });
|
|
591
|
+
}
|
|
592
|
+
if (availableEnvs.includes("mainnet-alpha")) {
|
|
593
|
+
choices.push({
|
|
594
|
+
name: "mainnet-alpha - Ethereum mainnet (\u26A0\uFE0F uses real funds)",
|
|
595
|
+
value: "mainnet-alpha"
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (choices.length === 0) {
|
|
599
|
+
throw new Error("No environments available in this build");
|
|
600
|
+
}
|
|
601
|
+
const env = await select({
|
|
602
|
+
message: "Select environment:",
|
|
603
|
+
choices,
|
|
604
|
+
default: defaultEnv
|
|
605
|
+
});
|
|
606
|
+
return env;
|
|
607
|
+
}
|
|
608
|
+
var MAX_IMAGE_SIZE = 4 * 1024 * 1024;
|
|
609
|
+
|
|
610
|
+
// src/flags.ts
|
|
611
|
+
var commonFlags = {
|
|
612
|
+
environment: Flags.string({
|
|
613
|
+
required: false,
|
|
614
|
+
description: "Deployment environment to use",
|
|
615
|
+
env: "ECLOUD_ENV",
|
|
616
|
+
default: async () => getDefaultEnvironment() || (getBuildType2() === "dev" ? "sepolia-dev" : "sepolia")
|
|
617
|
+
}),
|
|
618
|
+
"private-key": Flags.string({
|
|
619
|
+
required: false,
|
|
620
|
+
description: "Private key for signing transactions",
|
|
621
|
+
env: "ECLOUD_PRIVATE_KEY"
|
|
622
|
+
}),
|
|
623
|
+
"rpc-url": Flags.string({
|
|
624
|
+
required: false,
|
|
625
|
+
description: "RPC URL to connect to blockchain",
|
|
626
|
+
env: "ECLOUD_RPC_URL"
|
|
627
|
+
}),
|
|
628
|
+
verbose: Flags.boolean({
|
|
629
|
+
required: false,
|
|
630
|
+
description: "Enable verbose logging (default: false)",
|
|
631
|
+
default: false
|
|
632
|
+
})
|
|
633
|
+
};
|
|
634
|
+
async function validateCommonFlags(flags, options) {
|
|
635
|
+
flags["environment"] = await getEnvironmentInteractive(flags["environment"]);
|
|
636
|
+
if (options?.requirePrivateKey !== false) {
|
|
637
|
+
flags["private-key"] = await getPrivateKeyInteractive(flags["private-key"]);
|
|
638
|
+
}
|
|
639
|
+
return flags;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/telemetry.ts
|
|
643
|
+
import {
|
|
644
|
+
createTelemetryClient,
|
|
645
|
+
createAppEnvironment,
|
|
646
|
+
createMetricsContext,
|
|
647
|
+
addMetric,
|
|
648
|
+
addMetricWithDimensions,
|
|
649
|
+
emitMetrics,
|
|
650
|
+
getBuildType as getBuildType3
|
|
651
|
+
} from "@layr-labs/ecloud-sdk";
|
|
652
|
+
function createCLITelemetryClient() {
|
|
653
|
+
const userUUID = getOrCreateUserUUID();
|
|
654
|
+
const environment = createAppEnvironment(userUUID);
|
|
655
|
+
const telemetryEnabled = getGlobalTelemetryPreference();
|
|
656
|
+
return createTelemetryClient(environment, "ecloud-cli", {
|
|
657
|
+
telemetryEnabled: telemetryEnabled !== false
|
|
658
|
+
// Enabled by default, disabled only if explicitly set to false
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
async function withTelemetry(command, action) {
|
|
662
|
+
const client = createCLITelemetryClient();
|
|
663
|
+
const metrics = createMetricsContext();
|
|
664
|
+
metrics.properties["source"] = "ecloud-cli";
|
|
665
|
+
metrics.properties["command"] = command.id || command.constructor.name;
|
|
666
|
+
const environment = getDefaultEnvironment() || "sepolia";
|
|
667
|
+
metrics.properties["environment"] = environment;
|
|
668
|
+
const buildType = getBuildType3() || "prod";
|
|
669
|
+
metrics.properties["build_type"] = buildType;
|
|
670
|
+
const cliVersion = command.config.version;
|
|
671
|
+
if (cliVersion) {
|
|
672
|
+
metrics.properties["cli_version"] = cliVersion;
|
|
673
|
+
}
|
|
674
|
+
addMetric(metrics, "Count", 1);
|
|
675
|
+
let actionError;
|
|
676
|
+
let result;
|
|
677
|
+
try {
|
|
678
|
+
result = await action();
|
|
679
|
+
return result;
|
|
680
|
+
} catch (err) {
|
|
681
|
+
actionError = err instanceof Error ? err : new Error(String(err));
|
|
682
|
+
throw err;
|
|
683
|
+
} finally {
|
|
684
|
+
const resultValue = actionError ? "Failure" : "Success";
|
|
685
|
+
const dimensions = {};
|
|
686
|
+
if (actionError) {
|
|
687
|
+
dimensions["error"] = actionError.message;
|
|
688
|
+
}
|
|
689
|
+
addMetricWithDimensions(metrics, resultValue, 1, dimensions);
|
|
690
|
+
const duration = Date.now() - metrics.startTime.getTime();
|
|
691
|
+
addMetric(metrics, "DurationMilliseconds", duration);
|
|
692
|
+
try {
|
|
693
|
+
await emitMetrics(client, metrics);
|
|
694
|
+
await client.close();
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/commands/compute/app/releases.ts
|
|
701
|
+
import chalk2 from "chalk";
|
|
702
|
+
|
|
703
|
+
// src/utils/releases.ts
|
|
704
|
+
import chalk from "chalk";
|
|
705
|
+
|
|
706
|
+
// src/utils/build.ts
|
|
707
|
+
function formatSourceLink(repoUrl, gitRef) {
|
|
708
|
+
const normalizedRepo = repoUrl.replace(/\.git$/, "");
|
|
709
|
+
try {
|
|
710
|
+
const url = new URL(normalizedRepo);
|
|
711
|
+
const host = url.host.toLowerCase();
|
|
712
|
+
if (host === "github.com") {
|
|
713
|
+
const path4 = url.pathname.replace(/\/+$/, "");
|
|
714
|
+
if (path4.split("/").filter(Boolean).length >= 2) {
|
|
715
|
+
return `https://github.com${path4}/tree/${gitRef}`;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
return `${repoUrl}@${gitRef}`;
|
|
721
|
+
}
|
|
722
|
+
function extractRepoName(repoUrl) {
|
|
723
|
+
const normalized = repoUrl.replace(/\.git$/, "");
|
|
724
|
+
const match = normalized.match(/\/([^/]+?)$/);
|
|
725
|
+
return match?.[1];
|
|
726
|
+
}
|
|
727
|
+
function formatDependencyLines(dependencies) {
|
|
728
|
+
if (!dependencies || Object.keys(dependencies).length === 0) return [];
|
|
729
|
+
const lines = [];
|
|
730
|
+
lines.push("Dependencies (resolved builds):");
|
|
731
|
+
for (const [digest, dep] of Object.entries(dependencies)) {
|
|
732
|
+
const name = extractRepoName(dep.repoUrl);
|
|
733
|
+
const depSource = formatSourceLink(dep.repoUrl, dep.gitRef);
|
|
734
|
+
lines.push(` - ${digest} \u2713${name ? ` ${name}` : ""}`);
|
|
735
|
+
lines.push(` ${depSource}`);
|
|
736
|
+
}
|
|
737
|
+
return lines;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/utils/releases.ts
|
|
741
|
+
function isJsonObject(value) {
|
|
742
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
743
|
+
}
|
|
744
|
+
function sortedKeys(obj) {
|
|
745
|
+
return Object.keys(obj).sort((a, b) => a.localeCompare(b));
|
|
746
|
+
}
|
|
747
|
+
function formatPublicEnv(publicEnv) {
|
|
748
|
+
if (!publicEnv) return [];
|
|
749
|
+
if (typeof publicEnv === "string") {
|
|
750
|
+
try {
|
|
751
|
+
const parsed = JSON.parse(publicEnv);
|
|
752
|
+
if (Object.keys(parsed).length === 0) return [];
|
|
753
|
+
const lines = [];
|
|
754
|
+
lines.push(chalk.gray("publicEnv:"));
|
|
755
|
+
for (const k of sortedKeys(parsed)) {
|
|
756
|
+
const v = parsed[k];
|
|
757
|
+
lines.push(` ${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
758
|
+
}
|
|
759
|
+
return lines;
|
|
760
|
+
} catch {
|
|
761
|
+
return [`publicEnv: ${publicEnv}`];
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (isJsonObject(publicEnv) && !("BYTES_PER_ELEMENT" in publicEnv)) {
|
|
765
|
+
if (Object.keys(publicEnv).length === 0) return [];
|
|
766
|
+
const lines = [];
|
|
767
|
+
lines.push(chalk.gray("publicEnv:"));
|
|
768
|
+
for (const k of sortedKeys(publicEnv)) {
|
|
769
|
+
const v = publicEnv[k];
|
|
770
|
+
lines.push(` ${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
771
|
+
}
|
|
772
|
+
return lines;
|
|
773
|
+
}
|
|
774
|
+
return [`publicEnv: ${chalk.gray("<unavailable>")}`];
|
|
775
|
+
}
|
|
776
|
+
function formatMaybe(label, value) {
|
|
777
|
+
if (value === void 0 || value === null) return void 0;
|
|
778
|
+
const s = String(value);
|
|
779
|
+
if (!s) return void 0;
|
|
780
|
+
return `${label}: ${s}`;
|
|
781
|
+
}
|
|
782
|
+
function buildSummaryLines(build) {
|
|
783
|
+
if (!build) return [chalk.gray("Build: -")];
|
|
784
|
+
const lines = [];
|
|
785
|
+
lines.push(chalk.green("Build:"));
|
|
786
|
+
if (build.buildId) lines.push(` build_id: ${build.buildId}`);
|
|
787
|
+
if (build.buildType) lines.push(` build_type: ${build.buildType}`);
|
|
788
|
+
if (build.repoUrl && build.gitRef) {
|
|
789
|
+
lines.push(` repo_url: ${build.repoUrl}`);
|
|
790
|
+
lines.push(` git_ref: ${build.gitRef}`);
|
|
791
|
+
lines.push(` source: ${formatSourceLink(build.repoUrl, build.gitRef)}`);
|
|
792
|
+
} else {
|
|
793
|
+
if (build.repoUrl) lines.push(` repo_url: ${build.repoUrl}`);
|
|
794
|
+
if (build.gitRef) lines.push(` git_ref: ${build.gitRef}`);
|
|
795
|
+
}
|
|
796
|
+
if (build.imageUrl) lines.push(` image_url: ${build.imageUrl}`);
|
|
797
|
+
if (build.imageDigest) lines.push(` image_digest: ${build.imageDigest}`);
|
|
798
|
+
if (build.provenanceSignature) lines.push(` provenance_signature: ${build.provenanceSignature}`);
|
|
799
|
+
if (build.createdAt) lines.push(` created_at: ${build.createdAt}`);
|
|
800
|
+
const deps = build.dependencies;
|
|
801
|
+
if (deps && Object.keys(deps).length > 0) {
|
|
802
|
+
const depMap = {};
|
|
803
|
+
for (const [digest, dep] of Object.entries(deps)) {
|
|
804
|
+
if (dep.repoUrl && dep.gitRef) {
|
|
805
|
+
depMap[digest] = { repoUrl: dep.repoUrl, gitRef: dep.gitRef };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const depLines = formatDependencyLines(depMap);
|
|
809
|
+
if (depLines.length) {
|
|
810
|
+
lines.push("");
|
|
811
|
+
lines.push(...depLines.map((l) => l ? ` ${l}` : l));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return lines;
|
|
815
|
+
}
|
|
816
|
+
function formatAppRelease(release, index) {
|
|
817
|
+
const lines = [];
|
|
818
|
+
const id = release.rmsReleaseId ?? String(index);
|
|
819
|
+
lines.push(chalk.cyan.bold(`Release ${id}`));
|
|
820
|
+
const headerFields = [
|
|
821
|
+
formatMaybe("createdAt", release.createdAt),
|
|
822
|
+
formatMaybe("createdAtBlock", release.createdAtBlock),
|
|
823
|
+
formatMaybe("upgradeByTime", release.upgradeByTime),
|
|
824
|
+
formatMaybe("registryUrl", release.registryUrl),
|
|
825
|
+
formatMaybe("imageDigest", release.imageDigest)
|
|
826
|
+
].filter(Boolean);
|
|
827
|
+
lines.push(...headerFields);
|
|
828
|
+
const pub = formatPublicEnv(release.publicEnv);
|
|
829
|
+
if (pub.length) {
|
|
830
|
+
lines.push("");
|
|
831
|
+
lines.push(...pub);
|
|
832
|
+
}
|
|
833
|
+
lines.push("");
|
|
834
|
+
lines.push(...buildSummaryLines(release.build));
|
|
835
|
+
return lines;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/commands/compute/app/releases.ts
|
|
839
|
+
import { isAddress as isAddress3 } from "viem";
|
|
840
|
+
import Table from "cli-table3";
|
|
841
|
+
|
|
842
|
+
// src/utils/cliFormat.ts
|
|
843
|
+
function terminalWidth(fallback = 120) {
|
|
844
|
+
const cols = typeof process.stdout.columns === "number" ? process.stdout.columns : void 0;
|
|
845
|
+
return cols && cols > 0 ? cols : fallback;
|
|
846
|
+
}
|
|
847
|
+
function formatHumanTime(value) {
|
|
848
|
+
const raw = String(value ?? "").trim();
|
|
849
|
+
if (!raw) return "-";
|
|
850
|
+
if (/^\d+$/.test(raw)) {
|
|
851
|
+
const n = Number(raw);
|
|
852
|
+
if (Number.isFinite(n) && n > 0) {
|
|
853
|
+
const ms = raw.length <= 10 ? n * 1e3 : n;
|
|
854
|
+
const d2 = new Date(ms);
|
|
855
|
+
if (!Number.isNaN(d2.getTime())) return d2.toLocaleString();
|
|
856
|
+
}
|
|
857
|
+
return raw;
|
|
858
|
+
}
|
|
859
|
+
const d = new Date(raw);
|
|
860
|
+
if (Number.isNaN(d.getTime())) return raw;
|
|
861
|
+
return d.toLocaleString();
|
|
862
|
+
}
|
|
863
|
+
function formatRepoDisplay(repoUrl) {
|
|
864
|
+
const normalized = String(repoUrl || "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
865
|
+
try {
|
|
866
|
+
const url = new URL(normalized);
|
|
867
|
+
const host = url.host.toLowerCase();
|
|
868
|
+
if (host === "github.com") {
|
|
869
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
870
|
+
if (parts.length >= 2) return `github.com/${parts[0]}/${parts[1]}`;
|
|
871
|
+
}
|
|
872
|
+
return `${host}${url.pathname}`.replace(/\/+$/, "");
|
|
873
|
+
} catch {
|
|
874
|
+
return normalized;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function extractRepoName2(repoUrl) {
|
|
878
|
+
const normalized = String(repoUrl || "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
879
|
+
try {
|
|
880
|
+
const url = new URL(normalized);
|
|
881
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
882
|
+
return parts.length ? parts[parts.length - 1] : normalized;
|
|
883
|
+
} catch {
|
|
884
|
+
const m = normalized.match(/[:/]+([^/:]+)$/);
|
|
885
|
+
return m?.[1] || normalized || "unknown";
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function formatImageDisplay(imageUrl) {
|
|
889
|
+
const s = String(imageUrl || "");
|
|
890
|
+
return s.replace(/^docker\.io\//i, "");
|
|
891
|
+
}
|
|
892
|
+
function provenanceSummary(options) {
|
|
893
|
+
const parts = [];
|
|
894
|
+
if (options.provenanceJson) parts.push("prov\u2713");
|
|
895
|
+
if (options.provenanceSignature) parts.push("sig\u2713");
|
|
896
|
+
const depCount = options.dependencies ? Object.keys(options.dependencies).length : 0;
|
|
897
|
+
if (depCount > 0) parts.push(`deps:${depCount}`);
|
|
898
|
+
return parts.length ? parts.join(" ") : "-";
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/commands/compute/app/releases.ts
|
|
902
|
+
function sortReleasesOldestFirst(releases) {
|
|
903
|
+
const toNum = (v) => {
|
|
904
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
905
|
+
if (typeof v === "string" && /^\d+$/.test(v)) return Number(v);
|
|
906
|
+
return 0;
|
|
907
|
+
};
|
|
908
|
+
return [...releases].sort((a, b) => {
|
|
909
|
+
const byBlock = toNum(a.createdAtBlock) - toNum(b.createdAtBlock);
|
|
910
|
+
if (byBlock !== 0) return byBlock;
|
|
911
|
+
return toNum(a.createdAt) - toNum(b.createdAt);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
function separator() {
|
|
915
|
+
return chalk2.gray("\u2014".repeat(40));
|
|
916
|
+
}
|
|
917
|
+
function formatDepLines(deps) {
|
|
918
|
+
if (!deps || Object.keys(deps).length === 0) return [];
|
|
919
|
+
const entries = Object.entries(deps).sort(([a], [b]) => a.localeCompare(b));
|
|
920
|
+
const lines = [];
|
|
921
|
+
lines.push(" Dependencies:");
|
|
922
|
+
for (const [digest, dep] of entries) {
|
|
923
|
+
const repoUrl = dep.repoUrl || "";
|
|
924
|
+
const name = extractRepoName2(repoUrl);
|
|
925
|
+
const repo = repoUrl ? formatRepoDisplay(repoUrl) : "-";
|
|
926
|
+
lines.push(` - ${digest}`);
|
|
927
|
+
lines.push(` ${name} (${repo})`);
|
|
928
|
+
}
|
|
929
|
+
return lines;
|
|
930
|
+
}
|
|
931
|
+
function provenanceSummaryFromBuild(build) {
|
|
932
|
+
if (!build) return "-";
|
|
933
|
+
return provenanceSummary({
|
|
934
|
+
provenanceJson: build.provenanceJson,
|
|
935
|
+
provenanceSignature: build.provenanceSignature,
|
|
936
|
+
dependencies: build.dependencies
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
var AppReleases = class _AppReleases extends Command {
|
|
940
|
+
static description = "Show app releases (including verifiable build + dependency builds when available)";
|
|
941
|
+
static args = {
|
|
942
|
+
"app-id": Args.string({
|
|
943
|
+
description: "App ID or name",
|
|
944
|
+
required: false
|
|
945
|
+
})
|
|
946
|
+
};
|
|
947
|
+
static flags = {
|
|
948
|
+
...commonFlags,
|
|
949
|
+
json: Flags2.boolean({
|
|
950
|
+
description: "Output JSON instead of formatted text",
|
|
951
|
+
default: false
|
|
952
|
+
}),
|
|
953
|
+
full: Flags2.boolean({
|
|
954
|
+
description: "Show the full (multi-line) release details instead of a table",
|
|
955
|
+
default: false
|
|
956
|
+
})
|
|
957
|
+
};
|
|
958
|
+
async run() {
|
|
959
|
+
return withTelemetry(this, async () => {
|
|
960
|
+
const { args, flags } = await this.parse(_AppReleases);
|
|
961
|
+
const rawAppId = args["app-id"];
|
|
962
|
+
const needsPrivateKey = !rawAppId || !isAddress3(rawAppId);
|
|
963
|
+
const validatedFlags = await validateCommonFlags(flags, {
|
|
964
|
+
requirePrivateKey: needsPrivateKey
|
|
965
|
+
});
|
|
966
|
+
const environment = validatedFlags.environment || "sepolia";
|
|
967
|
+
const environmentConfig = getEnvironmentConfig2(environment);
|
|
968
|
+
const rpcUrl = validatedFlags["rpc-url"] || environmentConfig.defaultRPCURL;
|
|
969
|
+
const privateKey = validatedFlags["private-key"];
|
|
970
|
+
const appID = await getOrPromptAppID({
|
|
971
|
+
appID: args["app-id"],
|
|
972
|
+
environment,
|
|
973
|
+
privateKey,
|
|
974
|
+
rpcUrl,
|
|
975
|
+
action: "view releases for"
|
|
976
|
+
});
|
|
977
|
+
const userApiClient = new UserApiClient3(environmentConfig, privateKey, rpcUrl, getClientId());
|
|
978
|
+
const data = await userApiClient.getApp(appID);
|
|
979
|
+
const releases = sortReleasesOldestFirst(data.releases);
|
|
980
|
+
if (releases.length === 0) {
|
|
981
|
+
this.log(`
|
|
982
|
+
No releases found for app ${chalk2.gray(appID)}`);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (flags.json) {
|
|
986
|
+
this.log(JSON.stringify({ appID, releases }, null, 2));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
this.log(`
|
|
990
|
+
Releases for app ${chalk2.gray(appID)}:
|
|
991
|
+
`);
|
|
992
|
+
if (flags.full) {
|
|
993
|
+
for (let i = 0; i < releases.length; i++) {
|
|
994
|
+
const lines = formatAppRelease(releases[i], i);
|
|
995
|
+
for (const line of lines) this.log(line);
|
|
996
|
+
if (i !== releases.length - 1) this.log(`
|
|
997
|
+
${separator()}
|
|
998
|
+
`);
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const rows = releases.map((r, i) => {
|
|
1003
|
+
const rel = r.rmsReleaseId ?? String(i);
|
|
1004
|
+
const block = r.createdAtBlock ? String(r.createdAtBlock) : "-";
|
|
1005
|
+
const created = r.createdAt ? formatHumanTime(r.createdAt) : "-";
|
|
1006
|
+
const repo = formatRepoDisplay(r.build?.repoUrl ?? "-");
|
|
1007
|
+
const commit = r.build?.gitRef ?? "-";
|
|
1008
|
+
const digest = r.imageDigest ?? r.build?.imageDigest ?? "-";
|
|
1009
|
+
const image = formatImageDisplay(r.build?.imageUrl ?? r.registryUrl ?? "-");
|
|
1010
|
+
const build = r.build?.buildId ?? "-";
|
|
1011
|
+
const prov = provenanceSummaryFromBuild(r.build);
|
|
1012
|
+
const depCount = r.build?.dependencies ? Object.keys(r.build.dependencies).length : 0;
|
|
1013
|
+
const deps = depCount > 0 ? `deps:${depCount}` : "-";
|
|
1014
|
+
return { rel, block, created, repo, commit, digest, image, build, prov, deps };
|
|
1015
|
+
});
|
|
1016
|
+
const headers = {
|
|
1017
|
+
rel: chalk2.bold("Rel"),
|
|
1018
|
+
block: chalk2.bold("Block"),
|
|
1019
|
+
created: chalk2.bold("Created"),
|
|
1020
|
+
repo: chalk2.bold("Repo"),
|
|
1021
|
+
commit: chalk2.bold("Commit"),
|
|
1022
|
+
digest: chalk2.bold("Digest"),
|
|
1023
|
+
image: chalk2.bold("Image"),
|
|
1024
|
+
build: chalk2.bold("Build"),
|
|
1025
|
+
prov: chalk2.bold("Prov"),
|
|
1026
|
+
deps: chalk2.bold("Deps")
|
|
1027
|
+
};
|
|
1028
|
+
const tw = terminalWidth();
|
|
1029
|
+
const shouldStack = tw < 140;
|
|
1030
|
+
if (shouldStack) {
|
|
1031
|
+
for (const r of rows) {
|
|
1032
|
+
this.log(`${chalk2.cyan(r.rel)} ${r.created} (block ${r.block})`);
|
|
1033
|
+
this.log(` Repo: ${r.repo}`);
|
|
1034
|
+
this.log(` Commit: ${r.commit}`);
|
|
1035
|
+
this.log(` Digest: ${r.digest}`);
|
|
1036
|
+
this.log(` Image: ${r.image}`);
|
|
1037
|
+
this.log(` Build: ${r.build}`);
|
|
1038
|
+
this.log(` Provenance: ${r.prov}`);
|
|
1039
|
+
const relObj = releases.find((x, idx) => (x.rmsReleaseId ?? String(idx)) === r.rel);
|
|
1040
|
+
const depLines = formatDepLines(relObj?.build?.dependencies);
|
|
1041
|
+
if (depLines.length) {
|
|
1042
|
+
for (const l of depLines) this.log(l);
|
|
1043
|
+
}
|
|
1044
|
+
this.log(chalk2.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1045
|
+
}
|
|
1046
|
+
this.log("");
|
|
1047
|
+
this.log(
|
|
1048
|
+
chalk2.gray(
|
|
1049
|
+
`Tip: use ${chalk2.yellow("--full")} for detailed release output, ${chalk2.yellow(
|
|
1050
|
+
"--json"
|
|
1051
|
+
)} to copy/paste, and ${chalk2.yellow(
|
|
1052
|
+
"ecloud compute build info <buildId>"
|
|
1053
|
+
)} for full build/provenance details.`
|
|
1054
|
+
)
|
|
1055
|
+
);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const fixed = 6 + 10 + 20 + 36 + 12 + 8 + 10;
|
|
1059
|
+
const remaining = Math.max(60, tw - fixed);
|
|
1060
|
+
const repoW = Math.max(18, Math.floor(remaining * 0.25));
|
|
1061
|
+
const digestW = Math.max(18, Math.floor(remaining * 0.35));
|
|
1062
|
+
const imageW = Math.max(18, remaining - repoW - digestW);
|
|
1063
|
+
const table = new Table({
|
|
1064
|
+
head: [
|
|
1065
|
+
headers.rel,
|
|
1066
|
+
headers.block,
|
|
1067
|
+
headers.created,
|
|
1068
|
+
headers.repo,
|
|
1069
|
+
headers.commit,
|
|
1070
|
+
headers.digest,
|
|
1071
|
+
headers.image,
|
|
1072
|
+
headers.build,
|
|
1073
|
+
headers.prov,
|
|
1074
|
+
headers.deps
|
|
1075
|
+
],
|
|
1076
|
+
colWidths: [6, 10, 20, repoW, 10, digestW, imageW, 36, 12, 8],
|
|
1077
|
+
wordWrap: true,
|
|
1078
|
+
style: { "padding-left": 0, "padding-right": 1, head: [], border: [] }
|
|
1079
|
+
});
|
|
1080
|
+
for (const r of rows) {
|
|
1081
|
+
table.push([
|
|
1082
|
+
r.rel,
|
|
1083
|
+
r.block,
|
|
1084
|
+
r.created,
|
|
1085
|
+
r.repo,
|
|
1086
|
+
r.commit,
|
|
1087
|
+
r.digest,
|
|
1088
|
+
r.image,
|
|
1089
|
+
r.build,
|
|
1090
|
+
r.prov,
|
|
1091
|
+
r.deps
|
|
1092
|
+
]);
|
|
1093
|
+
}
|
|
1094
|
+
this.log(table.toString());
|
|
1095
|
+
this.log("");
|
|
1096
|
+
this.log(
|
|
1097
|
+
chalk2.gray(
|
|
1098
|
+
`Tip: use ${chalk2.yellow("--full")} for detailed release output, ${chalk2.yellow(
|
|
1099
|
+
"--json"
|
|
1100
|
+
)} to copy/paste, and ${chalk2.yellow(
|
|
1101
|
+
"ecloud compute build info <buildId>"
|
|
1102
|
+
)} for full build/provenance details.`
|
|
1103
|
+
)
|
|
1104
|
+
);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
export {
|
|
1109
|
+
AppReleases as default
|
|
1110
|
+
};
|
|
1111
|
+
//# sourceMappingURL=releases.js.map
|