@mainlayer/cli 0.1.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/LICENSE +11 -0
- package/README.md +167 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2330 -0
- package/dist/postinstall/index.d.ts +1 -0
- package/dist/postinstall/index.js +30 -0
- package/dist/skills-template-CBLbA5-E.js +716 -0
- package/package.json +60 -0
|
@@ -0,0 +1,2330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as configurePlatforms, n as generateSkillsMd, r as PLATFORMS, t as SKILLS_FILENAME } from "../skills-template-CBLbA5-E.js";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import * as clack from "@clack/prompts";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import ky, { HTTPError } from "ky";
|
|
11
|
+
import Conf from "conf";
|
|
12
|
+
import { createCipheriv, createDecipheriv, pbkdf2, randomBytes } from "node:crypto";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { address, createKeyPairFromPrivateKeyBytes, createSolanaRpc, getAddressDecoder, getAddressEncoder, getAddressFromPublicKey } from "@solana/kit";
|
|
15
|
+
import { mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
|
|
16
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
17
|
+
import { findAssociatedTokenPda } from "@solana-program/token";
|
|
18
|
+
import latestVersion from "latest-version";
|
|
19
|
+
//#region src/services/config-service.ts
|
|
20
|
+
var ConfigService = class {
|
|
21
|
+
conf;
|
|
22
|
+
confCwd;
|
|
23
|
+
defaultConf;
|
|
24
|
+
constructor(cwd) {
|
|
25
|
+
this.confCwd = cwd ?? join(homedir(), ".mainlayer");
|
|
26
|
+
this.conf = new Conf({
|
|
27
|
+
cwd: this.confCwd,
|
|
28
|
+
configName: "config"
|
|
29
|
+
});
|
|
30
|
+
this.defaultConf = new Conf({
|
|
31
|
+
cwd: this.confCwd,
|
|
32
|
+
configName: "config"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
get(key) {
|
|
36
|
+
return this.conf.get(key);
|
|
37
|
+
}
|
|
38
|
+
set(key, value) {
|
|
39
|
+
this.conf.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
delete(key) {
|
|
42
|
+
this.conf.delete(key);
|
|
43
|
+
}
|
|
44
|
+
getAll() {
|
|
45
|
+
return this.conf.store;
|
|
46
|
+
}
|
|
47
|
+
getApiUrl() {
|
|
48
|
+
return this.get("apiUrl") ?? process.env["MAINLAYER_API_URL"] ?? "https://api.mainlayer.fr";
|
|
49
|
+
}
|
|
50
|
+
clear() {
|
|
51
|
+
this.conf.clear();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Switch the active profile. Reinitializes this.conf to point at
|
|
55
|
+
* config.<name>.json (or config.json for 'default').
|
|
56
|
+
*/
|
|
57
|
+
setProfile(name) {
|
|
58
|
+
const configName = name === "default" ? "config" : `config.${name}`;
|
|
59
|
+
this.conf = new Conf({
|
|
60
|
+
cwd: this.confCwd,
|
|
61
|
+
configName
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Persist the active profile name to the default config.json.
|
|
66
|
+
* Always writes to the default conf, regardless of what profile is currently active.
|
|
67
|
+
*/
|
|
68
|
+
setActiveProfile(name) {
|
|
69
|
+
this.defaultConf.set("activeProfile", name);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns the active profile name. Reads from MAINLAYER_PROFILE env var first,
|
|
73
|
+
* then from the default config.json, then defaults to 'default'.
|
|
74
|
+
*/
|
|
75
|
+
getActiveProfile() {
|
|
76
|
+
return process.env["MAINLAYER_PROFILE"] ?? this.defaultConf.get("activeProfile") ?? "default";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const configService = new ConfigService();
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/utils/errors.ts
|
|
82
|
+
const EXIT_CODES = {
|
|
83
|
+
SUCCESS: 0,
|
|
84
|
+
GENERAL: 1,
|
|
85
|
+
AUTH_ERROR: 2,
|
|
86
|
+
NOT_FOUND: 3,
|
|
87
|
+
VALIDATION_ERROR: 4,
|
|
88
|
+
ALREADY_EXISTS: 5
|
|
89
|
+
};
|
|
90
|
+
var AppError = class extends Error {
|
|
91
|
+
constructor(message, exitCode = EXIT_CODES.GENERAL, meta) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.exitCode = exitCode;
|
|
94
|
+
this.meta = meta;
|
|
95
|
+
this.name = "AppError";
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/services/api-client.ts
|
|
100
|
+
var ApiClient = class {
|
|
101
|
+
apiKeyOverride;
|
|
102
|
+
setApiKeyOverride(key) {
|
|
103
|
+
this.apiKeyOverride = key;
|
|
104
|
+
}
|
|
105
|
+
createHttp() {
|
|
106
|
+
return ky.extend({
|
|
107
|
+
prefixUrl: configService.getApiUrl(),
|
|
108
|
+
hooks: { beforeRequest: [(request) => {
|
|
109
|
+
const apiKey = this.apiKeyOverride ?? process.env["MAINLAYER_API_KEY"];
|
|
110
|
+
const jwt = configService.get("jwt");
|
|
111
|
+
if (apiKey) request.headers.set("Authorization", `Bearer ${apiKey}`);
|
|
112
|
+
else if (jwt) request.headers.set("Authorization", `Bearer ${jwt}`);
|
|
113
|
+
}] }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async handleError(err) {
|
|
117
|
+
if (err instanceof HTTPError) {
|
|
118
|
+
const body = await err.response.json().catch(() => ({ error: "Unknown error" }));
|
|
119
|
+
const detail = body.detail;
|
|
120
|
+
let msg;
|
|
121
|
+
if (body.message) msg = body.message;
|
|
122
|
+
else if (body.error) msg = body.error;
|
|
123
|
+
else if (Array.isArray(detail)) msg = detail.map((d) => d.msg).join("; ");
|
|
124
|
+
else msg = `HTTP ${err.response.status}`;
|
|
125
|
+
const status = err.response.status;
|
|
126
|
+
if (status === 401) throw new AppError(msg, EXIT_CODES.AUTH_ERROR, {
|
|
127
|
+
type: "auth_error",
|
|
128
|
+
hint: "Run: mainlayer auth login"
|
|
129
|
+
});
|
|
130
|
+
if (status === 404) throw new AppError(msg, EXIT_CODES.NOT_FOUND, { type: "not_found" });
|
|
131
|
+
if (status === 409) throw new AppError(msg, EXIT_CODES.ALREADY_EXISTS, { type: "already_exists" });
|
|
132
|
+
if (status === 422) throw new AppError(msg, EXIT_CODES.VALIDATION_ERROR, { type: "validation_error" });
|
|
133
|
+
throw new AppError(msg, EXIT_CODES.GENERAL, { type: "api_error" });
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
async post(path, body) {
|
|
138
|
+
try {
|
|
139
|
+
return await this.createHttp().post(path, { json: body }).json();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return this.handleError(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async put(path, body) {
|
|
145
|
+
try {
|
|
146
|
+
return await this.createHttp().put(path, { json: body }).json();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return this.handleError(err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async patch(path, body) {
|
|
152
|
+
try {
|
|
153
|
+
return await this.createHttp().patch(path, { json: body }).json();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return this.handleError(err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async get(path, searchParams) {
|
|
159
|
+
try {
|
|
160
|
+
return await this.createHttp().get(path, searchParams ? { searchParams } : void 0).json();
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return this.handleError(err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async delete(path) {
|
|
166
|
+
try {
|
|
167
|
+
return await this.createHttp().delete(path).json();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return this.handleError(err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const apiClient = new ApiClient();
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/utils/output.ts
|
|
176
|
+
function formatOutput(data, opts) {
|
|
177
|
+
if (opts.json || !process.stdout.isTTY) console.log(JSON.stringify(data));
|
|
178
|
+
else for (const [key, value] of Object.entries(data)) console.log(`${chalk.cyan(key + ":")} ${value}`);
|
|
179
|
+
}
|
|
180
|
+
function printError(message) {
|
|
181
|
+
console.error(chalk.red(message));
|
|
182
|
+
}
|
|
183
|
+
function printSuccess(message) {
|
|
184
|
+
console.error(chalk.green(message));
|
|
185
|
+
}
|
|
186
|
+
function printTable(headers, rows) {
|
|
187
|
+
if (rows.length === 0) return;
|
|
188
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
189
|
+
const fmt = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
190
|
+
console.log(chalk.bold(fmt(headers)));
|
|
191
|
+
console.log(widths.map((w) => "-".repeat(w)).join(" "));
|
|
192
|
+
for (const row of rows) console.log(fmt(row));
|
|
193
|
+
}
|
|
194
|
+
function truncate(str, maxLen) {
|
|
195
|
+
return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/utils/prompt.ts
|
|
199
|
+
async function getCredentials(opts) {
|
|
200
|
+
const email = opts.email ?? process.env["MAINLAYER_EMAIL"];
|
|
201
|
+
const password = opts.password ?? process.env["MAINLAYER_PASSWORD"];
|
|
202
|
+
if (email && password) return {
|
|
203
|
+
email,
|
|
204
|
+
password
|
|
205
|
+
};
|
|
206
|
+
if (!process.stdin.isTTY) {
|
|
207
|
+
printError("Set MAINLAYER_EMAIL / MAINLAYER_PASSWORD or pass --email / --password flags");
|
|
208
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
209
|
+
process.exit();
|
|
210
|
+
}
|
|
211
|
+
clack.intro("Mainlayer");
|
|
212
|
+
const resolvedEmail = email ?? await clack.text({
|
|
213
|
+
message: "Email address",
|
|
214
|
+
validate: (v) => v.includes("@") ? void 0 : "Enter a valid email"
|
|
215
|
+
});
|
|
216
|
+
const resolvedPassword = password ?? await clack.password({ message: "Password" });
|
|
217
|
+
if (clack.isCancel(resolvedEmail) || clack.isCancel(resolvedPassword)) {
|
|
218
|
+
clack.cancel("Cancelled.");
|
|
219
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
220
|
+
process.exit();
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
email: resolvedEmail,
|
|
224
|
+
password: resolvedPassword
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function getPassphrase(opts) {
|
|
228
|
+
const envPassphrase = process.env["MAINLAYER_WALLET_PASSPHRASE"];
|
|
229
|
+
if (envPassphrase) return envPassphrase;
|
|
230
|
+
if (!process.stdin.isTTY) {
|
|
231
|
+
printError("Set MAINLAYER_WALLET_PASSPHRASE env var or run in an interactive terminal");
|
|
232
|
+
process.exitCode = EXIT_CODES.AUTH_ERROR;
|
|
233
|
+
process.exit();
|
|
234
|
+
}
|
|
235
|
+
const passphrase = await clack.password({ message: "Enter wallet passphrase" });
|
|
236
|
+
if (clack.isCancel(passphrase)) {
|
|
237
|
+
clack.cancel("Cancelled.");
|
|
238
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
239
|
+
process.exit();
|
|
240
|
+
}
|
|
241
|
+
if (opts?.confirm) {
|
|
242
|
+
const confirmation = await clack.password({ message: "Confirm wallet passphrase" });
|
|
243
|
+
if (clack.isCancel(confirmation)) {
|
|
244
|
+
clack.cancel("Cancelled.");
|
|
245
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
246
|
+
process.exit();
|
|
247
|
+
}
|
|
248
|
+
if (passphrase !== confirmation) {
|
|
249
|
+
printError("Passphrases do not match");
|
|
250
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
251
|
+
process.exit();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return passphrase;
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/cli/auth.ts
|
|
258
|
+
function authCommand() {
|
|
259
|
+
const auth = new Command("auth").description("Authentication commands");
|
|
260
|
+
auth.command("register").description("Create a new Mainlayer account").option("--email <email>", "Email address").option("--password <password>", "Password").option("--json", "Output as JSON").action(async (opts) => {
|
|
261
|
+
try {
|
|
262
|
+
const { email, password } = await getCredentials(opts);
|
|
263
|
+
const spinner = ora({
|
|
264
|
+
text: "Creating account...",
|
|
265
|
+
stream: process.stderr
|
|
266
|
+
}).start();
|
|
267
|
+
let response;
|
|
268
|
+
try {
|
|
269
|
+
response = await apiClient.post("auth/register", {
|
|
270
|
+
email,
|
|
271
|
+
password
|
|
272
|
+
});
|
|
273
|
+
} finally {
|
|
274
|
+
spinner.stop();
|
|
275
|
+
}
|
|
276
|
+
configService.set("jwt", response.token);
|
|
277
|
+
configService.set("jwtExpiresAt", response.expiresAt);
|
|
278
|
+
configService.set("userId", response.userId);
|
|
279
|
+
configService.set("email", response.email);
|
|
280
|
+
const json = opts.json ?? false;
|
|
281
|
+
formatOutput({
|
|
282
|
+
email: response.email,
|
|
283
|
+
userId: response.userId
|
|
284
|
+
}, { json });
|
|
285
|
+
if (process.stdout.isTTY && !json) clack.outro("Account created");
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err instanceof AppError) {
|
|
288
|
+
printError(err.message);
|
|
289
|
+
process.exitCode = err.exitCode;
|
|
290
|
+
} else throw err;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
auth.command("login").description("Log in to your Mainlayer account").option("--email <email>", "Email address").option("--password <password>", "Password").option("--json", "Output as JSON").action(async (opts) => {
|
|
294
|
+
try {
|
|
295
|
+
const { email, password } = await getCredentials(opts);
|
|
296
|
+
const spinner = ora({
|
|
297
|
+
text: "Logging in...",
|
|
298
|
+
stream: process.stderr
|
|
299
|
+
}).start();
|
|
300
|
+
let response;
|
|
301
|
+
try {
|
|
302
|
+
response = await apiClient.post("auth/login", {
|
|
303
|
+
email,
|
|
304
|
+
password
|
|
305
|
+
});
|
|
306
|
+
} finally {
|
|
307
|
+
spinner.stop();
|
|
308
|
+
}
|
|
309
|
+
configService.set("jwt", response.token);
|
|
310
|
+
configService.set("jwtExpiresAt", response.expiresAt);
|
|
311
|
+
configService.set("userId", response.userId);
|
|
312
|
+
configService.set("email", response.email);
|
|
313
|
+
const json = opts.json ?? false;
|
|
314
|
+
formatOutput({
|
|
315
|
+
email: response.email,
|
|
316
|
+
authenticated: true
|
|
317
|
+
}, { json });
|
|
318
|
+
if (process.stdout.isTTY && !json) clack.outro("Logged in");
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err instanceof AppError) {
|
|
321
|
+
printError(err.message);
|
|
322
|
+
process.exitCode = err.exitCode;
|
|
323
|
+
} else throw err;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
auth.command("logout").description("Log out of your Mainlayer account").option("--json", "Output as JSON").action((opts) => {
|
|
327
|
+
configService.delete("jwt");
|
|
328
|
+
configService.delete("jwtExpiresAt");
|
|
329
|
+
configService.delete("userId");
|
|
330
|
+
configService.delete("email");
|
|
331
|
+
const json = opts.json ?? false;
|
|
332
|
+
formatOutput({ loggedOut: true }, { json });
|
|
333
|
+
if (process.stdout.isTTY && !json) clack.outro("Logged out");
|
|
334
|
+
});
|
|
335
|
+
auth.command("status").description("Show current authentication status").option("--json", "Output as JSON").action((opts) => {
|
|
336
|
+
const email = configService.get("email");
|
|
337
|
+
const jwt = configService.get("jwt");
|
|
338
|
+
const jwtExpiresAt = configService.get("jwtExpiresAt");
|
|
339
|
+
const authenticated = !!jwt;
|
|
340
|
+
const expired = jwtExpiresAt ? new Date(jwtExpiresAt) < /* @__PURE__ */ new Date() : false;
|
|
341
|
+
const json = opts.json ?? false;
|
|
342
|
+
formatOutput({
|
|
343
|
+
email: email ?? "",
|
|
344
|
+
authenticated,
|
|
345
|
+
expired
|
|
346
|
+
}, { json });
|
|
347
|
+
});
|
|
348
|
+
const apiKey = new Command("api-key").description("Manage API keys");
|
|
349
|
+
apiKey.command("create").description("Create a new API key").option("--label <label>", "Label for the API key").option("--json", "Output as JSON").action(async (opts) => {
|
|
350
|
+
if (!opts.label) {
|
|
351
|
+
printError("--label is required");
|
|
352
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const response = await apiClient.post("auth/api-keys", { label: opts.label });
|
|
357
|
+
const json = opts.json ?? false;
|
|
358
|
+
if (!json && process.stdout.isTTY) printError("This is the only time the API key value will be shown. Save it now.");
|
|
359
|
+
formatOutput({
|
|
360
|
+
id: response.id,
|
|
361
|
+
label: response.label,
|
|
362
|
+
key: response.key ?? ""
|
|
363
|
+
}, { json });
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (err instanceof AppError) {
|
|
366
|
+
printError(err.message);
|
|
367
|
+
process.exitCode = err.exitCode;
|
|
368
|
+
} else throw err;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
apiKey.command("list").description("List all active API keys").option("--json", "Output as JSON").action(async (opts) => {
|
|
372
|
+
try {
|
|
373
|
+
const keys = await apiClient.get("auth/api-keys");
|
|
374
|
+
if ((opts.json ?? false) || !process.stdout.isTTY) console.log(JSON.stringify(keys));
|
|
375
|
+
else for (const k of keys) console.log(`id: ${k.id} label: ${k.label} created: ${k.createdAt}`);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (err instanceof AppError) {
|
|
378
|
+
printError(err.message);
|
|
379
|
+
process.exitCode = err.exitCode;
|
|
380
|
+
} else throw err;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
apiKey.command("revoke <id>").description("Revoke an API key by ID").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
384
|
+
try {
|
|
385
|
+
await apiClient.delete(`auth/api-keys/${id}`);
|
|
386
|
+
const json = opts.json ?? false;
|
|
387
|
+
formatOutput({
|
|
388
|
+
revoked: true,
|
|
389
|
+
id
|
|
390
|
+
}, { json });
|
|
391
|
+
if (process.stdout.isTTY && !json) clack.outro("API key revoked");
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err instanceof AppError) {
|
|
394
|
+
printError(err.message);
|
|
395
|
+
process.exitCode = err.exitCode;
|
|
396
|
+
} else throw err;
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
auth.addCommand(apiKey);
|
|
400
|
+
auth.command("switch <profile>").description("Switch to a named profile").option("--json", "Output as JSON").action((profile, opts) => {
|
|
401
|
+
configService.setActiveProfile(profile);
|
|
402
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
403
|
+
formatOutput({ activeProfile: profile }, { json });
|
|
404
|
+
if (process.stdout.isTTY && !json) clack.outro(`Switched to profile: ${profile}`);
|
|
405
|
+
});
|
|
406
|
+
auth.command("list").description("List all profiles").option("--json", "Output as JSON").action((opts) => {
|
|
407
|
+
let profiles = ["default"];
|
|
408
|
+
try {
|
|
409
|
+
profiles = readdirSync(join(homedir(), ".mainlayer")).filter((f) => f === "config.json" || /^config\..+\.json$/.test(f)).map((f) => f === "config.json" ? "default" : f.replace(/^config\./, "").replace(/\.json$/, ""));
|
|
410
|
+
if (!profiles.includes("default")) profiles.unshift("default");
|
|
411
|
+
} catch {}
|
|
412
|
+
const active = configService.getActiveProfile();
|
|
413
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
414
|
+
formatOutput({
|
|
415
|
+
profiles,
|
|
416
|
+
active
|
|
417
|
+
}, { json });
|
|
418
|
+
});
|
|
419
|
+
return auth;
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/services/wallet-service.ts
|
|
423
|
+
const pbkdf2Async = promisify(pbkdf2);
|
|
424
|
+
const DEFAULT_WALLET_PATH = join(homedir(), ".mainlayer", "wallet.json");
|
|
425
|
+
const PBKDF2_ITERATIONS = 2e5;
|
|
426
|
+
const USDC_MINT$1 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
427
|
+
var WalletService = class {
|
|
428
|
+
walletPath;
|
|
429
|
+
constructor(walletPath) {
|
|
430
|
+
this.walletPath = walletPath ?? DEFAULT_WALLET_PATH;
|
|
431
|
+
}
|
|
432
|
+
async encryptKeystore(privateKeyBytes, pubkey, passphrase) {
|
|
433
|
+
const salt = randomBytes(16);
|
|
434
|
+
const iv = randomBytes(12);
|
|
435
|
+
const cipher = createCipheriv("aes-256-gcm", await pbkdf2Async(passphrase, salt, PBKDF2_ITERATIONS, 32, "sha256"), iv);
|
|
436
|
+
const encrypted = Buffer.concat([cipher.update(privateKeyBytes), cipher.final()]);
|
|
437
|
+
const authTag = cipher.getAuthTag();
|
|
438
|
+
return {
|
|
439
|
+
version: 1,
|
|
440
|
+
pubkey,
|
|
441
|
+
salt: salt.toString("hex"),
|
|
442
|
+
iv: iv.toString("hex"),
|
|
443
|
+
ciphertext: encrypted.toString("hex"),
|
|
444
|
+
authTag: authTag.toString("hex")
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async decryptKeystore(keystore, passphrase) {
|
|
448
|
+
const salt = Buffer.from(keystore.salt, "hex");
|
|
449
|
+
const iv = Buffer.from(keystore.iv, "hex");
|
|
450
|
+
const ciphertext = Buffer.from(keystore.ciphertext, "hex");
|
|
451
|
+
const authTag = Buffer.from(keystore.authTag, "hex");
|
|
452
|
+
const decipher = createDecipheriv("aes-256-gcm", await pbkdf2Async(passphrase, salt, PBKDF2_ITERATIONS, 32, "sha256"), iv);
|
|
453
|
+
decipher.setAuthTag(authTag);
|
|
454
|
+
try {
|
|
455
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
456
|
+
return new Uint8Array(decrypted);
|
|
457
|
+
} catch {
|
|
458
|
+
throw new AppError("Invalid passphrase", EXIT_CODES.AUTH_ERROR);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
readKeystore() {
|
|
462
|
+
if (!existsSync(this.walletPath)) throw new AppError("No wallet found. Run: mainlayer wallet create", EXIT_CODES.NOT_FOUND);
|
|
463
|
+
const raw = readFileSync(this.walletPath, "utf8");
|
|
464
|
+
const keystore = JSON.parse(raw);
|
|
465
|
+
if (keystore.version !== 1) throw new AppError("Unsupported wallet format version", EXIT_CODES.GENERAL);
|
|
466
|
+
return keystore;
|
|
467
|
+
}
|
|
468
|
+
writeKeystore(keystore) {
|
|
469
|
+
mkdirSync(dirname(this.walletPath), { recursive: true });
|
|
470
|
+
writeFileSync(this.walletPath, JSON.stringify(keystore, null, 2), { mode: 384 });
|
|
471
|
+
}
|
|
472
|
+
async encryptKeystorePublic(privateKeyBytes, pubkey, passphrase) {
|
|
473
|
+
return this.encryptKeystore(privateKeyBytes, pubkey, passphrase);
|
|
474
|
+
}
|
|
475
|
+
async decryptKeystorePublic(keystore, passphrase) {
|
|
476
|
+
return this.decryptKeystore(keystore, passphrase);
|
|
477
|
+
}
|
|
478
|
+
async create(passphrase) {
|
|
479
|
+
if (existsSync(this.walletPath)) throw new AppError("Wallet already exists", EXIT_CODES.ALREADY_EXISTS);
|
|
480
|
+
const keyPair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
|
|
481
|
+
const privateKeyBytes = new Uint8Array(await crypto.subtle.exportKey("pkcs8", keyPair.privateKey)).slice(16);
|
|
482
|
+
const addr = await getAddressFromPublicKey(keyPair.publicKey);
|
|
483
|
+
const keystore = await this.encryptKeystore(privateKeyBytes, addr, passphrase);
|
|
484
|
+
this.writeKeystore(keystore);
|
|
485
|
+
return addr;
|
|
486
|
+
}
|
|
487
|
+
async importFromBase58(base58Key, passphrase) {
|
|
488
|
+
const keyBytes = getAddressEncoder().encode(base58Key);
|
|
489
|
+
const addr = await getAddressFromPublicKey((await createKeyPairFromPrivateKeyBytes(keyBytes)).publicKey);
|
|
490
|
+
const keystore = await this.encryptKeystore(keyBytes, addr, passphrase);
|
|
491
|
+
this.writeKeystore(keystore);
|
|
492
|
+
return addr;
|
|
493
|
+
}
|
|
494
|
+
async importFromMnemonic(mnemonic, passphrase) {
|
|
495
|
+
if (!validateMnemonic(mnemonic, wordlist)) throw new AppError("Invalid mnemonic phrase", EXIT_CODES.VALIDATION_ERROR);
|
|
496
|
+
const privateKeyBytes = mnemonicToSeedSync(mnemonic).slice(0, 32);
|
|
497
|
+
const addr = await getAddressFromPublicKey((await createKeyPairFromPrivateKeyBytes(privateKeyBytes)).publicKey);
|
|
498
|
+
const keystore = await this.encryptKeystore(privateKeyBytes, addr, passphrase);
|
|
499
|
+
this.writeKeystore(keystore);
|
|
500
|
+
return addr;
|
|
501
|
+
}
|
|
502
|
+
async getAddress() {
|
|
503
|
+
return this.readKeystore().pubkey;
|
|
504
|
+
}
|
|
505
|
+
async exportPrivateKey(passphrase) {
|
|
506
|
+
const keystore = this.readKeystore();
|
|
507
|
+
const privateKeyBytes = await this.decryptKeystore(keystore, passphrase);
|
|
508
|
+
return getAddressDecoder().decode(privateKeyBytes);
|
|
509
|
+
}
|
|
510
|
+
async getBalance() {
|
|
511
|
+
const addr = await this.getAddress();
|
|
512
|
+
const rpc = createSolanaRpc(configService.get("solanaRpcUrl") ?? process.env["MAINLAYER_SOLANA_RPC_URL"] ?? "https://api.mainnet-beta.solana.com");
|
|
513
|
+
const balanceResult = await rpc.getBalance(address(addr)).send();
|
|
514
|
+
const sol = Number(balanceResult.value) / 1e9;
|
|
515
|
+
let usdc = 0;
|
|
516
|
+
try {
|
|
517
|
+
const tokenAccounts = await rpc.getTokenAccountsByOwner(address(addr), { mint: address(USDC_MINT$1) }, { encoding: "jsonParsed" }).send();
|
|
518
|
+
for (const account of tokenAccounts.value) {
|
|
519
|
+
const parsed = account.account.data;
|
|
520
|
+
if (parsed && typeof parsed === "object" && "parsed" in parsed && parsed.parsed && typeof parsed.parsed === "object" && "info" in parsed.parsed) {
|
|
521
|
+
const info = parsed.parsed.info;
|
|
522
|
+
usdc += info.tokenAmount?.uiAmount ?? 0;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch {}
|
|
526
|
+
return {
|
|
527
|
+
sol,
|
|
528
|
+
usdc
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async signTransaction(passphrase, unsignedTxB64) {
|
|
532
|
+
const keystore = this.readKeystore();
|
|
533
|
+
const keyPair = await createKeyPairFromPrivateKeyBytes(await this.decryptKeystore(keystore, passphrase));
|
|
534
|
+
const txBytes = Buffer.from(unsignedTxB64, "base64");
|
|
535
|
+
const messageBytes = txBytes.subarray(65);
|
|
536
|
+
const sigBytes = new Uint8Array(await crypto.subtle.sign("Ed25519", keyPair.privateKey, messageBytes));
|
|
537
|
+
const signedTx = Buffer.from(txBytes);
|
|
538
|
+
for (let i = 0; i < 64; i++) signedTx[1 + i] = sigBytes[i];
|
|
539
|
+
return signedTx.toString("base64");
|
|
540
|
+
}
|
|
541
|
+
async signMessage(passphrase, messageText) {
|
|
542
|
+
const keystore = this.readKeystore();
|
|
543
|
+
const keyPair = await createKeyPairFromPrivateKeyBytes(await this.decryptKeystore(keystore, passphrase));
|
|
544
|
+
const msgBytes = new TextEncoder().encode(messageText);
|
|
545
|
+
const sigBytes = new Uint8Array(await crypto.subtle.sign("Ed25519", keyPair.privateKey, msgBytes));
|
|
546
|
+
const { getBase58Codec } = await import("@solana/kit");
|
|
547
|
+
return getBase58Codec().encode(sigBytes);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const walletService = new WalletService();
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/cli/wallet.ts
|
|
553
|
+
function walletCommand() {
|
|
554
|
+
const wallet = new Command("wallet").description("Manage your Solana wallet");
|
|
555
|
+
wallet.command("create").description("Generate a new Solana keypair and store it encrypted").option("--json", "Output as JSON").action(async (opts) => {
|
|
556
|
+
try {
|
|
557
|
+
const passphrase = await getPassphrase({ confirm: true });
|
|
558
|
+
formatOutput({ address: await walletService.create(passphrase) }, { json: opts.json ?? !process.stdout.isTTY });
|
|
559
|
+
if (process.stdout.isTTY && !opts.json) clack.outro("Wallet created");
|
|
560
|
+
} catch (err) {
|
|
561
|
+
if (err instanceof AppError) {
|
|
562
|
+
printError(err.message);
|
|
563
|
+
process.exitCode = err.exitCode;
|
|
564
|
+
} else {
|
|
565
|
+
printError(String(err));
|
|
566
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
wallet.command("import").description("Import an existing Solana keypair (base58 or mnemonic)").option("--base58 <key>", "Import from base58-encoded private key").option("--mnemonic <words>", "Import from BIP39 mnemonic phrase").option("--json", "Output as JSON").action(async (opts) => {
|
|
571
|
+
if (!opts.base58 && !opts.mnemonic || opts.base58 && opts.mnemonic) {
|
|
572
|
+
printError("Provide exactly one of --base58 <key> or --mnemonic <words>");
|
|
573
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const passphrase = await getPassphrase({ confirm: true });
|
|
578
|
+
let address;
|
|
579
|
+
if (opts.base58) address = await walletService.importFromBase58(opts.base58, passphrase);
|
|
580
|
+
else address = await walletService.importFromMnemonic(opts.mnemonic, passphrase);
|
|
581
|
+
formatOutput({ address }, { json: opts.json ?? !process.stdout.isTTY });
|
|
582
|
+
if (process.stdout.isTTY && !opts.json) clack.outro("Wallet imported");
|
|
583
|
+
} catch (err) {
|
|
584
|
+
if (err instanceof AppError) {
|
|
585
|
+
printError(err.message);
|
|
586
|
+
process.exitCode = err.exitCode;
|
|
587
|
+
} else {
|
|
588
|
+
printError(String(err));
|
|
589
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
wallet.command("address").description("Print the wallet public key (no passphrase required)").option("--json", "Output as JSON").action(async (opts) => {
|
|
594
|
+
try {
|
|
595
|
+
const address = await walletService.getAddress();
|
|
596
|
+
if (opts.json || !process.stdout.isTTY) formatOutput({ address }, { json: true });
|
|
597
|
+
else console.log(address);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
if (err instanceof AppError) {
|
|
600
|
+
printError(err.message);
|
|
601
|
+
process.exitCode = err.exitCode;
|
|
602
|
+
} else {
|
|
603
|
+
printError(String(err));
|
|
604
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
wallet.command("balance").description("Show SOL and USDC balances").option("--json", "Output as JSON").action(async (opts) => {
|
|
609
|
+
const spinner = process.stderr.isTTY ? ora({
|
|
610
|
+
text: "Checking balance...",
|
|
611
|
+
stream: process.stderr
|
|
612
|
+
}).start() : null;
|
|
613
|
+
try {
|
|
614
|
+
const { sol, usdc } = await walletService.getBalance();
|
|
615
|
+
spinner?.succeed("Balance loaded");
|
|
616
|
+
formatOutput({
|
|
617
|
+
sol,
|
|
618
|
+
usdc
|
|
619
|
+
}, { json: opts.json ?? !process.stdout.isTTY });
|
|
620
|
+
} catch (err) {
|
|
621
|
+
spinner?.fail("Failed to fetch balance");
|
|
622
|
+
if (err instanceof AppError) {
|
|
623
|
+
printError(err.message);
|
|
624
|
+
process.exitCode = err.exitCode;
|
|
625
|
+
} else {
|
|
626
|
+
printError(String(err));
|
|
627
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
wallet.command("export").description("Export the private key (always requires interactive passphrase entry)").option("--json", "Output as JSON").action(async (opts) => {
|
|
632
|
+
if (!process.stdin.isTTY) {
|
|
633
|
+
printError("wallet export requires interactive passphrase entry");
|
|
634
|
+
process.exitCode = EXIT_CODES.AUTH_ERROR;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const passphrase = await clack.password({ message: "Enter wallet passphrase to export private key" });
|
|
639
|
+
if (clack.isCancel(passphrase)) {
|
|
640
|
+
clack.cancel("Cancelled.");
|
|
641
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const privateKey = await walletService.exportPrivateKey(passphrase);
|
|
645
|
+
if (opts.json || !process.stdout.isTTY) formatOutput({ privateKey }, { json: true });
|
|
646
|
+
else console.log(privateKey);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
if (err instanceof AppError) {
|
|
649
|
+
printError(err.message);
|
|
650
|
+
process.exitCode = err.exitCode;
|
|
651
|
+
} else {
|
|
652
|
+
printError(String(err));
|
|
653
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
return wallet;
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/types/config.ts
|
|
661
|
+
const KNOWN_CONFIG_KEYS = [
|
|
662
|
+
"apiUrl",
|
|
663
|
+
"jwt",
|
|
664
|
+
"jwtExpiresAt",
|
|
665
|
+
"userId",
|
|
666
|
+
"email",
|
|
667
|
+
"lastUpdateCheck",
|
|
668
|
+
"activeProfile"
|
|
669
|
+
];
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region src/cli/config.ts
|
|
672
|
+
function configCommand() {
|
|
673
|
+
const config = new Command("config").description("Read and write CLI configuration");
|
|
674
|
+
config.option("--list", "List all configuration values").option("--json", "Output as JSON").action((opts) => {
|
|
675
|
+
if (opts.list) {
|
|
676
|
+
const display = { ...configService.getAll() };
|
|
677
|
+
if (display.jwt) display.jwt = "***";
|
|
678
|
+
formatOutput(display, { json: opts.json ?? !process.stdout.isTTY });
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
config.command("get <key>").description("Get a configuration value").option("--json", "Output as JSON").action((key, opts) => {
|
|
682
|
+
if (!KNOWN_CONFIG_KEYS.includes(key)) {
|
|
683
|
+
printError(`Unknown config key: ${key}. Known keys: ${KNOWN_CONFIG_KEYS.join(", ")}`);
|
|
684
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const value = configService.get(key);
|
|
688
|
+
if (value === void 0) {
|
|
689
|
+
printError(`Key '${key}' is not set`);
|
|
690
|
+
process.exitCode = EXIT_CODES.NOT_FOUND;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (opts.json ?? !process.stdout.isTTY) formatOutput({ [key]: value }, { json: true });
|
|
694
|
+
else console.log(value);
|
|
695
|
+
});
|
|
696
|
+
config.command("set <key> <value>").description("Set a configuration value").option("--json", "Output as JSON").action((key, value, opts) => {
|
|
697
|
+
if (!KNOWN_CONFIG_KEYS.includes(key)) {
|
|
698
|
+
printError(`Unknown config key: ${key}. Known keys: ${KNOWN_CONFIG_KEYS.join(", ")}`);
|
|
699
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
configService.set(key, value);
|
|
703
|
+
if (opts.json ?? !process.stdout.isTTY) formatOutput({ [key]: value }, { json: true });
|
|
704
|
+
else printSuccess(`Set ${key}`);
|
|
705
|
+
});
|
|
706
|
+
config.command("unset <key>").description("Remove a configuration value").option("--json", "Output as JSON").action((key, opts) => {
|
|
707
|
+
if (!KNOWN_CONFIG_KEYS.includes(key)) {
|
|
708
|
+
printError(`Unknown config key: ${key}. Known keys: ${KNOWN_CONFIG_KEYS.join(", ")}`);
|
|
709
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
configService.delete(key);
|
|
713
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
714
|
+
formatOutput({ unset: key }, { json });
|
|
715
|
+
});
|
|
716
|
+
return config;
|
|
717
|
+
}
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/cli/webhook.ts
|
|
720
|
+
function webhookCommand() {
|
|
721
|
+
const webhook = new Command("webhook").description("Webhook management commands");
|
|
722
|
+
webhook.command("update").description("Update the webhook callback URL for a resource").requiredOption("--resource-id <id>", "Resource ID").requiredOption("--callback-url <url>", "New callback URL").option("--json", "Output as JSON").action(async (opts) => {
|
|
723
|
+
const resourceId = opts.resourceId;
|
|
724
|
+
const callbackUrl = opts.callbackUrl;
|
|
725
|
+
const json = opts.json ?? false;
|
|
726
|
+
if (!resourceId || !callbackUrl) {
|
|
727
|
+
printError("--resource-id and --callback-url are required");
|
|
728
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const spinner = ora({
|
|
732
|
+
text: "Updating webhook...",
|
|
733
|
+
stream: process.stderr
|
|
734
|
+
}).start();
|
|
735
|
+
try {
|
|
736
|
+
await apiClient.put("webhooks/" + resourceId, { callback_url: callbackUrl });
|
|
737
|
+
spinner.stop();
|
|
738
|
+
formatOutput({
|
|
739
|
+
resource_id: resourceId,
|
|
740
|
+
callback_url: callbackUrl,
|
|
741
|
+
updated: true
|
|
742
|
+
}, { json });
|
|
743
|
+
} catch (err) {
|
|
744
|
+
spinner.stop();
|
|
745
|
+
if (err instanceof AppError) {
|
|
746
|
+
printError(err.message);
|
|
747
|
+
process.exitCode = err.exitCode;
|
|
748
|
+
} else throw err;
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
webhook.command("logs").description("View webhook delivery logs").option("--status <status>", "Filter by status").option("--limit <n>", "Max results to return", "20").option("--json", "Output as JSON").action(async (opts) => {
|
|
752
|
+
const json = opts.json ?? false;
|
|
753
|
+
const params = {};
|
|
754
|
+
if (opts.status) params["status"] = opts.status;
|
|
755
|
+
if (opts.limit) params["limit"] = opts.limit;
|
|
756
|
+
const spinner = ora({
|
|
757
|
+
text: "Loading webhook logs...",
|
|
758
|
+
stream: process.stderr
|
|
759
|
+
}).start();
|
|
760
|
+
try {
|
|
761
|
+
const logs = await apiClient.get("webhooks/logs", params);
|
|
762
|
+
spinner.stop();
|
|
763
|
+
if (json) {
|
|
764
|
+
console.log(JSON.stringify(logs));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (logs.length === 0) {
|
|
768
|
+
console.log("No webhook logs found.");
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
printTable([
|
|
772
|
+
"LOG_ID",
|
|
773
|
+
"PAYMENT_ID",
|
|
774
|
+
"STATUS",
|
|
775
|
+
"HTTP",
|
|
776
|
+
"ATTEMPTS"
|
|
777
|
+
], logs.map((l) => [
|
|
778
|
+
truncate(l.id, 12),
|
|
779
|
+
truncate(l.payment_id, 12),
|
|
780
|
+
l.status,
|
|
781
|
+
String(l.http_status ?? "-"),
|
|
782
|
+
String(l.attempts)
|
|
783
|
+
]));
|
|
784
|
+
} catch (err) {
|
|
785
|
+
spinner.stop();
|
|
786
|
+
if (err instanceof AppError) {
|
|
787
|
+
printError(err.message);
|
|
788
|
+
process.exitCode = err.exitCode;
|
|
789
|
+
} else throw err;
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
webhook.command("retry <log-id>").description("Retry a failed webhook delivery").option("--json", "Output as JSON").action(async (logId, opts) => {
|
|
793
|
+
const json = opts.json ?? false;
|
|
794
|
+
const spinner = ora({
|
|
795
|
+
text: "Retrying webhook delivery...",
|
|
796
|
+
stream: process.stderr
|
|
797
|
+
}).start();
|
|
798
|
+
try {
|
|
799
|
+
await apiClient.post("webhooks/logs/" + logId + "/retry");
|
|
800
|
+
spinner.stop();
|
|
801
|
+
formatOutput({
|
|
802
|
+
retried: true,
|
|
803
|
+
log_id: logId
|
|
804
|
+
}, { json });
|
|
805
|
+
} catch (err) {
|
|
806
|
+
spinner.stop();
|
|
807
|
+
if (err instanceof AppError) {
|
|
808
|
+
printError(err.message);
|
|
809
|
+
process.exitCode = err.exitCode;
|
|
810
|
+
} else throw err;
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
webhook.command("rotate-secret").description("Rotate the webhook secret for a resource").requiredOption("--resource-id <id>", "Resource ID").option("--force", "Confirm secret rotation").option("--json", "Output as JSON").action(async (opts) => {
|
|
814
|
+
const resourceId = opts.resourceId;
|
|
815
|
+
const json = opts.json ?? false;
|
|
816
|
+
if (!resourceId) {
|
|
817
|
+
printError("--resource-id is required");
|
|
818
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!opts.force) {
|
|
822
|
+
printError("Error: This will rotate the webhook secret for resource '" + resourceId + "'. The old secret remains valid for 24 hours. Run with --force to confirm.");
|
|
823
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const spinner = ora({
|
|
827
|
+
text: "Rotating webhook secret...",
|
|
828
|
+
stream: process.stderr
|
|
829
|
+
}).start();
|
|
830
|
+
try {
|
|
831
|
+
const response = await apiClient.post("webhooks/" + resourceId + "/rotate-secret");
|
|
832
|
+
spinner.stop();
|
|
833
|
+
if (json) {
|
|
834
|
+
console.log(JSON.stringify(response));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
printSuccess("New webhook secret:");
|
|
838
|
+
console.log(response.webhook_secret);
|
|
839
|
+
console.error(chalk.yellow("Save this secret now. It will not be shown again in this form."));
|
|
840
|
+
} catch (err) {
|
|
841
|
+
spinner.stop();
|
|
842
|
+
if (err instanceof AppError) {
|
|
843
|
+
printError(err.message);
|
|
844
|
+
process.exitCode = err.exitCode;
|
|
845
|
+
} else throw err;
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
return webhook;
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region src/utils/price.ts
|
|
852
|
+
/**
|
|
853
|
+
* Parse a price string to micro-units (integer).
|
|
854
|
+
* Decimal presence (contains '.') means USDC dollars -> multiply by 1_000_000.
|
|
855
|
+
* No decimal means raw micro-units.
|
|
856
|
+
* Per D-06 from 02-CONTEXT.md.
|
|
857
|
+
*/
|
|
858
|
+
function parsePrice(raw) {
|
|
859
|
+
if (raw.includes(".")) {
|
|
860
|
+
const parsed = parseFloat(raw);
|
|
861
|
+
if (Number.isNaN(parsed) || parsed <= 0) throw new Error(`Invalid price: "${raw}". Must be a positive number.`);
|
|
862
|
+
return Math.round(parsed * 1e6);
|
|
863
|
+
}
|
|
864
|
+
const parsed = parseInt(raw, 10);
|
|
865
|
+
if (Number.isNaN(parsed) || parsed <= 0) throw new Error(`Invalid price: "${raw}". Must be a positive integer (micro-units).`);
|
|
866
|
+
return parsed;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Format micro-units to human-readable USDC string.
|
|
870
|
+
* Example: 1000000 -> "$1.00 USDC"
|
|
871
|
+
*/
|
|
872
|
+
function formatPrice(microUnits) {
|
|
873
|
+
return `$${(microUnits / 1e6).toFixed(2)} USDC`;
|
|
874
|
+
}
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/cli/resource-plans.ts
|
|
877
|
+
function buildPlanCommand() {
|
|
878
|
+
const plan = new Command("plan").description("Manage pricing plans for a resource");
|
|
879
|
+
plan.command("create").description("Create a pricing plan for a resource").option("--resource-id <id>", "Resource ID (required)").option("--name <name>", "Plan name (required)").option("--price <price>", "Price in USDC or micro-units (required)").option("--fee-model <model>", "Fee model: pay_per_call | subscription | one_time (required)").option("--credits-per-payment <n>", "Credits included per payment").option("--duration-seconds <n>", "Subscription duration in seconds").option("--max-calls-per-day <n>", "Max calls per day").option("--json", "Output as JSON").action(async (opts) => {
|
|
880
|
+
try {
|
|
881
|
+
const json = opts.json ?? false;
|
|
882
|
+
const missing = [];
|
|
883
|
+
if (!opts.resourceId) missing.push("--resource-id");
|
|
884
|
+
if (!opts.name) missing.push("--name");
|
|
885
|
+
if (!opts.price) missing.push("--price");
|
|
886
|
+
if (!opts.feeModel) missing.push("--fee-model");
|
|
887
|
+
if (missing.length > 0) {
|
|
888
|
+
printError(`Missing required flags: ${missing.join(", ")}`);
|
|
889
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const resourceId = opts.resourceId;
|
|
893
|
+
const parsedPrice = parsePrice(opts.price);
|
|
894
|
+
const body = {
|
|
895
|
+
name: opts.name,
|
|
896
|
+
price_usdc: parsedPrice,
|
|
897
|
+
fee_model: opts.feeModel,
|
|
898
|
+
...opts.creditsPerPayment !== void 0 && { credits_per_payment: Number(opts.creditsPerPayment) },
|
|
899
|
+
...opts.durationSeconds !== void 0 && { duration_seconds: Number(opts.durationSeconds) },
|
|
900
|
+
...opts.maxCallsPerDay !== void 0 && { max_calls_per_day: Number(opts.maxCallsPerDay) }
|
|
901
|
+
};
|
|
902
|
+
const spinner = ora({
|
|
903
|
+
text: "Creating plan...",
|
|
904
|
+
stream: process.stderr
|
|
905
|
+
}).start();
|
|
906
|
+
let response;
|
|
907
|
+
try {
|
|
908
|
+
response = await apiClient.post(`resources/${resourceId}/plans`, body);
|
|
909
|
+
} finally {
|
|
910
|
+
spinner.stop();
|
|
911
|
+
}
|
|
912
|
+
formatOutput({
|
|
913
|
+
id: response.id,
|
|
914
|
+
name: response.name,
|
|
915
|
+
price: formatPrice(response.price_usdc),
|
|
916
|
+
fee_model: response.fee_model
|
|
917
|
+
}, { json });
|
|
918
|
+
} catch (err) {
|
|
919
|
+
if (err instanceof AppError) {
|
|
920
|
+
printError(err.message);
|
|
921
|
+
process.exitCode = err.exitCode;
|
|
922
|
+
} else throw err;
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
plan.command("list").description("List pricing plans for a resource").option("--resource-id <id>", "Resource ID (required)").option("--json", "Output as JSON").action(async (opts) => {
|
|
926
|
+
try {
|
|
927
|
+
const json = opts.json ?? false;
|
|
928
|
+
if (!opts.resourceId) {
|
|
929
|
+
printError("Missing required flag: --resource-id");
|
|
930
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const resourceId = opts.resourceId;
|
|
934
|
+
const spinner = ora({
|
|
935
|
+
text: "Loading plans...",
|
|
936
|
+
stream: process.stderr
|
|
937
|
+
}).start();
|
|
938
|
+
let plans;
|
|
939
|
+
try {
|
|
940
|
+
plans = await apiClient.get(`resources/${resourceId}/plans`);
|
|
941
|
+
} finally {
|
|
942
|
+
spinner.stop();
|
|
943
|
+
}
|
|
944
|
+
if (json || !process.stdout.isTTY) {
|
|
945
|
+
console.log(JSON.stringify(plans));
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (plans.length === 0) {
|
|
949
|
+
console.log("No plans for this resource. Run `mainlayer resource plan create` to add one.");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
printTable([
|
|
953
|
+
"NAME",
|
|
954
|
+
"PRICE",
|
|
955
|
+
"FEE MODEL",
|
|
956
|
+
"CREDITS",
|
|
957
|
+
"DURATION"
|
|
958
|
+
], plans.map((p) => [
|
|
959
|
+
p.name,
|
|
960
|
+
formatPrice(p.price_usdc),
|
|
961
|
+
p.fee_model,
|
|
962
|
+
String(p.credits_per_payment ?? "-"),
|
|
963
|
+
String(p.duration_seconds ?? "-")
|
|
964
|
+
]));
|
|
965
|
+
} catch (err) {
|
|
966
|
+
if (err instanceof AppError) {
|
|
967
|
+
printError(err.message);
|
|
968
|
+
process.exitCode = err.exitCode;
|
|
969
|
+
} else throw err;
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
plan.command("update <plan-name>").description("Update a pricing plan by name").option("--resource-id <id>", "Resource ID (required)").option("--price <price>", "New price in USDC or micro-units").option("--fee-model <model>", "New fee model: pay_per_call | subscription | one_time").option("--credits-per-payment <n>", "New credits per payment").option("--duration-seconds <n>", "New duration in seconds").option("--max-calls-per-day <n>", "New max calls per day").option("--json", "Output as JSON").action(async (planName, opts) => {
|
|
973
|
+
try {
|
|
974
|
+
const json = opts.json ?? false;
|
|
975
|
+
if (!opts.resourceId) {
|
|
976
|
+
printError("Missing required flag: --resource-id");
|
|
977
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const resourceId = opts.resourceId;
|
|
981
|
+
const body = {
|
|
982
|
+
...opts.price !== void 0 && { price_usdc: parsePrice(opts.price) },
|
|
983
|
+
...opts.feeModel !== void 0 && { fee_model: opts.feeModel },
|
|
984
|
+
...opts.creditsPerPayment !== void 0 && { credits_per_payment: Number(opts.creditsPerPayment) },
|
|
985
|
+
...opts.durationSeconds !== void 0 && { duration_seconds: Number(opts.durationSeconds) },
|
|
986
|
+
...opts.maxCallsPerDay !== void 0 && { max_calls_per_day: Number(opts.maxCallsPerDay) }
|
|
987
|
+
};
|
|
988
|
+
const spinner = ora({
|
|
989
|
+
text: "Updating plan...",
|
|
990
|
+
stream: process.stderr
|
|
991
|
+
}).start();
|
|
992
|
+
let response;
|
|
993
|
+
try {
|
|
994
|
+
response = await apiClient.put(`resources/${resourceId}/plans/${planName}`, body);
|
|
995
|
+
} finally {
|
|
996
|
+
spinner.stop();
|
|
997
|
+
}
|
|
998
|
+
formatOutput({
|
|
999
|
+
id: response.id,
|
|
1000
|
+
name: response.name,
|
|
1001
|
+
price: formatPrice(response.price_usdc),
|
|
1002
|
+
fee_model: response.fee_model
|
|
1003
|
+
}, { json });
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
if (err instanceof AppError) {
|
|
1006
|
+
printError(err.message);
|
|
1007
|
+
process.exitCode = err.exitCode;
|
|
1008
|
+
} else throw err;
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
plan.command("delete <plan-name>").description("Delete a pricing plan by name").option("--resource-id <id>", "Resource ID (required)").option("--force", "Confirm deletion without prompt").option("--json", "Output as JSON").action(async (planName, opts) => {
|
|
1012
|
+
try {
|
|
1013
|
+
const json = opts.json ?? false;
|
|
1014
|
+
if (!opts.resourceId) {
|
|
1015
|
+
printError("Missing required flag: --resource-id");
|
|
1016
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const resourceId = opts.resourceId;
|
|
1020
|
+
if (!opts.force) {
|
|
1021
|
+
printError(`Error: This will delete plan '${planName}'. Run with --force to confirm.`);
|
|
1022
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const spinner = ora({
|
|
1026
|
+
text: "Deleting plan...",
|
|
1027
|
+
stream: process.stderr
|
|
1028
|
+
}).start();
|
|
1029
|
+
try {
|
|
1030
|
+
await apiClient.delete(`resources/${resourceId}/plans/${planName}`);
|
|
1031
|
+
} finally {
|
|
1032
|
+
spinner.stop();
|
|
1033
|
+
}
|
|
1034
|
+
formatOutput({
|
|
1035
|
+
deleted: true,
|
|
1036
|
+
plan_name: planName
|
|
1037
|
+
}, { json });
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
if (err instanceof AppError) {
|
|
1040
|
+
printError(err.message);
|
|
1041
|
+
process.exitCode = err.exitCode;
|
|
1042
|
+
} else throw err;
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
return plan;
|
|
1046
|
+
}
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/cli/resource-quota.ts
|
|
1049
|
+
function buildQuotaCommand() {
|
|
1050
|
+
const quota = new Command("quota").description("Manage per-wallet quota limits");
|
|
1051
|
+
quota.command("set").description("Set per-wallet quota limits for a resource").option("--resource-id <id>", "Resource ID (required)").option("--max-purchases <n>", "Max purchases per wallet").option("--max-calls-per-day <n>", "Max calls per day per wallet").option("--json", "Output as JSON").action(async (opts) => {
|
|
1052
|
+
try {
|
|
1053
|
+
const json = opts.json ?? false;
|
|
1054
|
+
if (!opts.resourceId) {
|
|
1055
|
+
printError("Missing required flag: --resource-id");
|
|
1056
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (!opts.maxPurchases && !opts.maxCallsPerDay) {
|
|
1060
|
+
printError("At least one of --max-purchases or --max-calls-per-day must be provided");
|
|
1061
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const resourceId = opts.resourceId;
|
|
1065
|
+
const body = {
|
|
1066
|
+
...opts.maxPurchases !== void 0 && { max_purchases_per_wallet: Number(opts.maxPurchases) },
|
|
1067
|
+
...opts.maxCallsPerDay !== void 0 && { max_calls_per_day_per_wallet: Number(opts.maxCallsPerDay) }
|
|
1068
|
+
};
|
|
1069
|
+
const spinner = ora({
|
|
1070
|
+
text: "Setting quota...",
|
|
1071
|
+
stream: process.stderr
|
|
1072
|
+
}).start();
|
|
1073
|
+
let response;
|
|
1074
|
+
try {
|
|
1075
|
+
response = await apiClient.put(`resources/${resourceId}/quota`, body);
|
|
1076
|
+
} finally {
|
|
1077
|
+
spinner.stop();
|
|
1078
|
+
}
|
|
1079
|
+
formatOutput({
|
|
1080
|
+
resource_id: response.resource_id,
|
|
1081
|
+
max_purchases_per_wallet: response.max_purchases_per_wallet ?? "unlimited",
|
|
1082
|
+
max_calls_per_day_per_wallet: response.max_calls_per_day_per_wallet ?? "unlimited"
|
|
1083
|
+
}, { json });
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
if (err instanceof AppError) {
|
|
1086
|
+
printError(err.message);
|
|
1087
|
+
process.exitCode = err.exitCode;
|
|
1088
|
+
} else throw err;
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
quota.command("get").description("Get quota limits for a resource").option("--resource-id <id>", "Resource ID (required)").option("--json", "Output as JSON").action(async (opts) => {
|
|
1092
|
+
try {
|
|
1093
|
+
const json = opts.json ?? false;
|
|
1094
|
+
if (!opts.resourceId) {
|
|
1095
|
+
printError("Missing required flag: --resource-id");
|
|
1096
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const resourceId = opts.resourceId;
|
|
1100
|
+
const spinner = ora({
|
|
1101
|
+
text: "Loading quota...",
|
|
1102
|
+
stream: process.stderr
|
|
1103
|
+
}).start();
|
|
1104
|
+
let response;
|
|
1105
|
+
try {
|
|
1106
|
+
response = await apiClient.get(`resources/${resourceId}/quota`);
|
|
1107
|
+
} finally {
|
|
1108
|
+
spinner.stop();
|
|
1109
|
+
}
|
|
1110
|
+
formatOutput({
|
|
1111
|
+
resource_id: response.resource_id,
|
|
1112
|
+
max_purchases_per_wallet: response.max_purchases_per_wallet ?? "unlimited",
|
|
1113
|
+
max_calls_per_day_per_wallet: response.max_calls_per_day_per_wallet ?? "unlimited"
|
|
1114
|
+
}, { json });
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
if (err instanceof AppError) {
|
|
1117
|
+
printError(err.message);
|
|
1118
|
+
process.exitCode = err.exitCode;
|
|
1119
|
+
} else throw err;
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
quota.command("delete").description("Remove quota limits for a resource").option("--resource-id <id>", "Resource ID (required)").option("--json", "Output as JSON").action(async (opts) => {
|
|
1123
|
+
try {
|
|
1124
|
+
const json = opts.json ?? false;
|
|
1125
|
+
if (!opts.resourceId) {
|
|
1126
|
+
printError("Missing required flag: --resource-id");
|
|
1127
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const resourceId = opts.resourceId;
|
|
1131
|
+
const spinner = ora({
|
|
1132
|
+
text: "Removing quota...",
|
|
1133
|
+
stream: process.stderr
|
|
1134
|
+
}).start();
|
|
1135
|
+
try {
|
|
1136
|
+
await apiClient.delete(`resources/${resourceId}/quota`);
|
|
1137
|
+
} finally {
|
|
1138
|
+
spinner.stop();
|
|
1139
|
+
}
|
|
1140
|
+
formatOutput({
|
|
1141
|
+
removed: true,
|
|
1142
|
+
resource_id: resourceId
|
|
1143
|
+
}, { json });
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
if (err instanceof AppError) {
|
|
1146
|
+
printError(err.message);
|
|
1147
|
+
process.exitCode = err.exitCode;
|
|
1148
|
+
} else throw err;
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
return quota;
|
|
1152
|
+
}
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/cli/resource.ts
|
|
1155
|
+
const VALID_TYPES = [
|
|
1156
|
+
"api",
|
|
1157
|
+
"file",
|
|
1158
|
+
"endpoint",
|
|
1159
|
+
"page"
|
|
1160
|
+
];
|
|
1161
|
+
const VALID_FEE_MODELS = [
|
|
1162
|
+
"one_time",
|
|
1163
|
+
"subscription",
|
|
1164
|
+
"pay_per_call",
|
|
1165
|
+
"hybrid"
|
|
1166
|
+
];
|
|
1167
|
+
async function resolveVendorWallet(vendorWallet, json) {
|
|
1168
|
+
if (vendorWallet) return vendorWallet;
|
|
1169
|
+
try {
|
|
1170
|
+
const addr = await walletService.getAddress();
|
|
1171
|
+
if (process.stdout.isTTY && !json) process.stderr.write(`Using wallet: ${addr}\n`);
|
|
1172
|
+
return addr;
|
|
1173
|
+
} catch {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async function fetchResourceById(id) {
|
|
1178
|
+
const resource = (await apiClient.get("resources")).find((r) => r.id === id);
|
|
1179
|
+
if (!resource) throw new AppError("Resource not found", EXIT_CODES.NOT_FOUND);
|
|
1180
|
+
return resource;
|
|
1181
|
+
}
|
|
1182
|
+
function formatResourceDetail(r) {
|
|
1183
|
+
return {
|
|
1184
|
+
id: r.id,
|
|
1185
|
+
slug: r.slug,
|
|
1186
|
+
type: r.type,
|
|
1187
|
+
price: formatPrice(r.price_usdc),
|
|
1188
|
+
fee_model: r.fee_model,
|
|
1189
|
+
vendor_wallet: r.vendor_wallet,
|
|
1190
|
+
description: r.description ?? "",
|
|
1191
|
+
discoverable: r.discoverable,
|
|
1192
|
+
active: r.active,
|
|
1193
|
+
created_at: r.created_at
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function resourceCommand() {
|
|
1197
|
+
const resource = new Command("resource").description("Manage vendor resources");
|
|
1198
|
+
resource.command("create").description("Create a new resource").option("--slug <slug>", "Resource slug (URL-friendly identifier)").option("--type <type>", "Resource type: api | file | endpoint | page").option("--price <price>", "Price in USDC (e.g. 1.00) or micro-units (e.g. 1000000)").option("--fee-model <model>", "Fee model: one_time | subscription | pay_per_call | hybrid").option("--vendor-wallet <address>", "Vendor wallet address (auto-fills from local wallet)").option("--description <text>", "Resource description").option("--discoverable", "Make resource discoverable in marketplace").option("--callback-url <url>", "Webhook callback URL").option("--json", "Output as JSON").action(async (opts) => {
|
|
1199
|
+
try {
|
|
1200
|
+
const json = opts.json ?? false;
|
|
1201
|
+
let slug = opts.slug;
|
|
1202
|
+
let type = opts.type;
|
|
1203
|
+
let price = opts.price;
|
|
1204
|
+
let feeModel = opts.feeModel;
|
|
1205
|
+
const isTTY = process.stdin.isTTY;
|
|
1206
|
+
if ((!slug || !type || !price || !feeModel) && isTTY && !json) {
|
|
1207
|
+
clack.intro("Create resource");
|
|
1208
|
+
if (!slug) {
|
|
1209
|
+
const result = await clack.text({
|
|
1210
|
+
message: "Slug",
|
|
1211
|
+
placeholder: "my-api"
|
|
1212
|
+
});
|
|
1213
|
+
if (clack.isCancel(result)) {
|
|
1214
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
slug = result;
|
|
1218
|
+
}
|
|
1219
|
+
if (!type) {
|
|
1220
|
+
const result = await clack.select({
|
|
1221
|
+
message: "Type",
|
|
1222
|
+
options: VALID_TYPES.map((t) => ({
|
|
1223
|
+
value: t,
|
|
1224
|
+
label: t
|
|
1225
|
+
}))
|
|
1226
|
+
});
|
|
1227
|
+
if (clack.isCancel(result)) {
|
|
1228
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
type = result;
|
|
1232
|
+
}
|
|
1233
|
+
if (!price) {
|
|
1234
|
+
const result = await clack.text({
|
|
1235
|
+
message: "Price (USDC)",
|
|
1236
|
+
placeholder: "1.00"
|
|
1237
|
+
});
|
|
1238
|
+
if (clack.isCancel(result)) {
|
|
1239
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
price = result;
|
|
1243
|
+
}
|
|
1244
|
+
if (!feeModel) {
|
|
1245
|
+
const result = await clack.select({
|
|
1246
|
+
message: "Fee model",
|
|
1247
|
+
options: VALID_FEE_MODELS.map((m) => ({
|
|
1248
|
+
value: m,
|
|
1249
|
+
label: m
|
|
1250
|
+
}))
|
|
1251
|
+
});
|
|
1252
|
+
if (clack.isCancel(result)) {
|
|
1253
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
feeModel = result;
|
|
1257
|
+
}
|
|
1258
|
+
} else {
|
|
1259
|
+
const missing = [];
|
|
1260
|
+
if (!slug) missing.push("--slug");
|
|
1261
|
+
if (!type) missing.push("--type");
|
|
1262
|
+
if (!price) missing.push("--price");
|
|
1263
|
+
if (!feeModel) missing.push("--fee-model");
|
|
1264
|
+
if (missing.length > 0) {
|
|
1265
|
+
printError(`Missing required flags: ${missing.join(", ")}`);
|
|
1266
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const parsedPrice = parsePrice(price);
|
|
1271
|
+
const vendorWallet = await resolveVendorWallet(opts.vendorWallet, json);
|
|
1272
|
+
const body = {
|
|
1273
|
+
slug,
|
|
1274
|
+
type,
|
|
1275
|
+
price_usdc: parsedPrice,
|
|
1276
|
+
fee_model: feeModel,
|
|
1277
|
+
vendor_wallet: vendorWallet ?? "",
|
|
1278
|
+
...opts.description !== void 0 && { description: opts.description },
|
|
1279
|
+
...opts.discoverable !== void 0 && { discoverable: opts.discoverable },
|
|
1280
|
+
...opts.callbackUrl !== void 0 && { callback_url: opts.callbackUrl }
|
|
1281
|
+
};
|
|
1282
|
+
const spinner = ora({
|
|
1283
|
+
text: "Creating resource...",
|
|
1284
|
+
stream: process.stderr
|
|
1285
|
+
}).start();
|
|
1286
|
+
let response;
|
|
1287
|
+
try {
|
|
1288
|
+
response = await apiClient.post("resources", body);
|
|
1289
|
+
} finally {
|
|
1290
|
+
spinner.stop();
|
|
1291
|
+
}
|
|
1292
|
+
formatOutput({
|
|
1293
|
+
id: response.id,
|
|
1294
|
+
slug: response.slug,
|
|
1295
|
+
type: response.type,
|
|
1296
|
+
price: formatPrice(response.price_usdc),
|
|
1297
|
+
fee_model: response.fee_model,
|
|
1298
|
+
active: response.active
|
|
1299
|
+
}, { json });
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
if (err instanceof AppError) {
|
|
1302
|
+
printError(err.message);
|
|
1303
|
+
process.exitCode = err.exitCode;
|
|
1304
|
+
} else throw err;
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
resource.command("list").description("List all your resources").option("--json", "Output as JSON").action(async (opts) => {
|
|
1308
|
+
try {
|
|
1309
|
+
const json = opts.json ?? false;
|
|
1310
|
+
const spinner = ora({
|
|
1311
|
+
text: "Loading resources...",
|
|
1312
|
+
stream: process.stderr
|
|
1313
|
+
}).start();
|
|
1314
|
+
let resources;
|
|
1315
|
+
try {
|
|
1316
|
+
resources = await apiClient.get("resources");
|
|
1317
|
+
} finally {
|
|
1318
|
+
spinner.stop();
|
|
1319
|
+
}
|
|
1320
|
+
if (json || !process.stdout.isTTY) {
|
|
1321
|
+
console.log(JSON.stringify(resources));
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (resources.length === 0) {
|
|
1325
|
+
console.log("No resources yet. Run `mainlayer resource create` to register your first one.");
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
printTable([
|
|
1329
|
+
"ID",
|
|
1330
|
+
"SLUG",
|
|
1331
|
+
"TYPE",
|
|
1332
|
+
"PRICE",
|
|
1333
|
+
"FEE MODEL",
|
|
1334
|
+
"ACTIVE"
|
|
1335
|
+
], resources.map((r) => [
|
|
1336
|
+
truncate(r.id, 12),
|
|
1337
|
+
truncate(r.slug, 20),
|
|
1338
|
+
r.type,
|
|
1339
|
+
formatPrice(r.price_usdc),
|
|
1340
|
+
r.fee_model,
|
|
1341
|
+
r.active ? "yes" : "no"
|
|
1342
|
+
]));
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
if (err instanceof AppError) {
|
|
1345
|
+
printError(err.message);
|
|
1346
|
+
process.exitCode = err.exitCode;
|
|
1347
|
+
} else throw err;
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
resource.command("get <id>").description("Get details of a resource by ID").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
1351
|
+
try {
|
|
1352
|
+
const json = opts.json ?? false;
|
|
1353
|
+
const spinner = ora({
|
|
1354
|
+
text: "Loading resource...",
|
|
1355
|
+
stream: process.stderr
|
|
1356
|
+
}).start();
|
|
1357
|
+
let res;
|
|
1358
|
+
try {
|
|
1359
|
+
res = await fetchResourceById(id);
|
|
1360
|
+
} finally {
|
|
1361
|
+
spinner.stop();
|
|
1362
|
+
}
|
|
1363
|
+
formatOutput(formatResourceDetail(res), { json });
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
if (err instanceof AppError) {
|
|
1366
|
+
printError(err.message);
|
|
1367
|
+
process.exitCode = err.exitCode;
|
|
1368
|
+
} else throw err;
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
resource.command("update <id>").description("Update a resource").option("--slug <slug>", "New slug").option("--type <type>", "New type: api | file | endpoint | page").option("--price <price>", "New price in USDC or micro-units").option("--fee-model <model>", "New fee model").option("--vendor-wallet <address>", "New vendor wallet address").option("--description <text>", "New description").option("--discoverable", "Set discoverable to true").option("--no-discoverable", "Set discoverable to false").option("--callback-url <url>", "New callback URL").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
1372
|
+
try {
|
|
1373
|
+
const json = opts.json ?? false;
|
|
1374
|
+
const spinner = ora({
|
|
1375
|
+
text: "Loading resource...",
|
|
1376
|
+
stream: process.stderr
|
|
1377
|
+
}).start();
|
|
1378
|
+
let existing;
|
|
1379
|
+
try {
|
|
1380
|
+
existing = await fetchResourceById(id);
|
|
1381
|
+
} finally {
|
|
1382
|
+
spinner.stop();
|
|
1383
|
+
}
|
|
1384
|
+
const parsedPrice = opts.price !== void 0 ? parsePrice(opts.price) : existing.price_usdc;
|
|
1385
|
+
const body = {
|
|
1386
|
+
slug: opts.slug ?? existing.slug,
|
|
1387
|
+
type: opts.type ?? existing.type,
|
|
1388
|
+
price_usdc: parsedPrice,
|
|
1389
|
+
fee_model: opts.feeModel ?? existing.fee_model,
|
|
1390
|
+
vendor_wallet: opts.vendorWallet ?? existing.vendor_wallet,
|
|
1391
|
+
description: opts.description ?? existing.description ?? void 0,
|
|
1392
|
+
discoverable: opts.discoverable ?? existing.discoverable,
|
|
1393
|
+
callback_url: opts.callbackUrl ?? existing.callback_url ?? void 0
|
|
1394
|
+
};
|
|
1395
|
+
const updateSpinner = ora({
|
|
1396
|
+
text: "Updating resource...",
|
|
1397
|
+
stream: process.stderr
|
|
1398
|
+
}).start();
|
|
1399
|
+
let updated;
|
|
1400
|
+
try {
|
|
1401
|
+
updated = await apiClient.put(`resources/${id}`, body);
|
|
1402
|
+
} finally {
|
|
1403
|
+
updateSpinner.stop();
|
|
1404
|
+
}
|
|
1405
|
+
formatOutput(formatResourceDetail(updated), { json });
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
if (err instanceof AppError) {
|
|
1408
|
+
printError(err.message);
|
|
1409
|
+
process.exitCode = err.exitCode;
|
|
1410
|
+
} else throw err;
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
resource.command("delete <id>").description("Delete a resource").option("--force", "Confirm deletion without prompt").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
1414
|
+
try {
|
|
1415
|
+
const json = opts.json ?? false;
|
|
1416
|
+
if (!opts.force) {
|
|
1417
|
+
const spinner = ora({
|
|
1418
|
+
text: "Loading resource...",
|
|
1419
|
+
stream: process.stderr
|
|
1420
|
+
}).start();
|
|
1421
|
+
let res;
|
|
1422
|
+
try {
|
|
1423
|
+
res = await fetchResourceById(id);
|
|
1424
|
+
} finally {
|
|
1425
|
+
spinner.stop();
|
|
1426
|
+
}
|
|
1427
|
+
printError(`Error: This will permanently delete resource '${res.slug}'. Run with --force to confirm.`);
|
|
1428
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const spinner = ora({
|
|
1432
|
+
text: "Deleting resource...",
|
|
1433
|
+
stream: process.stderr
|
|
1434
|
+
}).start();
|
|
1435
|
+
try {
|
|
1436
|
+
await apiClient.delete(`resources/${id}`);
|
|
1437
|
+
} finally {
|
|
1438
|
+
spinner.stop();
|
|
1439
|
+
}
|
|
1440
|
+
formatOutput({
|
|
1441
|
+
deleted: true,
|
|
1442
|
+
id
|
|
1443
|
+
}, { json });
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
if (err instanceof AppError) {
|
|
1446
|
+
printError(err.message);
|
|
1447
|
+
process.exitCode = err.exitCode;
|
|
1448
|
+
} else throw err;
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
resource.addCommand(buildPlanCommand());
|
|
1452
|
+
resource.addCommand(buildQuotaCommand());
|
|
1453
|
+
return resource;
|
|
1454
|
+
}
|
|
1455
|
+
//#endregion
|
|
1456
|
+
//#region src/cli/coupon.ts
|
|
1457
|
+
function couponCommand() {
|
|
1458
|
+
const coupon = new Command("coupon").description("Manage discount coupons for a resource");
|
|
1459
|
+
coupon.command("create").description("Create a discount coupon for a resource").option("--resource-id <id>", "Resource ID (required)").option("--code <code>", "Coupon code (required, auto-uppercased)").option("--discount-type <type>", "Discount type: percent | fixed (required)").option("--discount-value <n>", "Discount amount (required)").option("--max-uses <n>", "Maximum number of uses").option("--expires-at <iso-date>", "Expiry date (ISO 8601)").option("--json", "Output as JSON").action(async (opts) => {
|
|
1460
|
+
try {
|
|
1461
|
+
const json = opts.json ?? false;
|
|
1462
|
+
const missing = [];
|
|
1463
|
+
if (!opts.resourceId) missing.push("--resource-id");
|
|
1464
|
+
if (!opts.code) missing.push("--code");
|
|
1465
|
+
if (!opts.discountType) missing.push("--discount-type");
|
|
1466
|
+
if (!opts.discountValue) missing.push("--discount-value");
|
|
1467
|
+
if (missing.length > 0) {
|
|
1468
|
+
printError(`Missing required flags: ${missing.join(", ")}`);
|
|
1469
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const resourceId = opts.resourceId;
|
|
1473
|
+
const body = {
|
|
1474
|
+
code: opts.code.toUpperCase(),
|
|
1475
|
+
discount_type: opts.discountType,
|
|
1476
|
+
discount_value: Number(opts.discountValue),
|
|
1477
|
+
...opts.maxUses !== void 0 && { max_uses: Number(opts.maxUses) },
|
|
1478
|
+
...opts.expiresAt !== void 0 && { expires_at: opts.expiresAt }
|
|
1479
|
+
};
|
|
1480
|
+
const spinner = ora({
|
|
1481
|
+
text: "Creating coupon...",
|
|
1482
|
+
stream: process.stderr
|
|
1483
|
+
}).start();
|
|
1484
|
+
let response;
|
|
1485
|
+
try {
|
|
1486
|
+
response = await apiClient.post(`resources/${resourceId}/coupons`, body);
|
|
1487
|
+
} finally {
|
|
1488
|
+
spinner.stop();
|
|
1489
|
+
}
|
|
1490
|
+
formatOutput({
|
|
1491
|
+
id: response.id,
|
|
1492
|
+
code: response.code,
|
|
1493
|
+
discount_type: response.discount_type,
|
|
1494
|
+
discount_value: response.discount_value,
|
|
1495
|
+
active: response.active
|
|
1496
|
+
}, { json });
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
if (err instanceof AppError) {
|
|
1499
|
+
printError(err.message);
|
|
1500
|
+
process.exitCode = err.exitCode;
|
|
1501
|
+
} else throw err;
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
coupon.command("list").description("List coupons for a resource").option("--resource-id <id>", "Resource ID (required)").option("--json", "Output as JSON").action(async (opts) => {
|
|
1505
|
+
try {
|
|
1506
|
+
const json = opts.json ?? false;
|
|
1507
|
+
if (!opts.resourceId) {
|
|
1508
|
+
printError("Missing required flag: --resource-id");
|
|
1509
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const resourceId = opts.resourceId;
|
|
1513
|
+
const spinner = ora({
|
|
1514
|
+
text: "Loading coupons...",
|
|
1515
|
+
stream: process.stderr
|
|
1516
|
+
}).start();
|
|
1517
|
+
let coupons;
|
|
1518
|
+
try {
|
|
1519
|
+
coupons = await apiClient.get(`resources/${resourceId}/coupons`);
|
|
1520
|
+
} finally {
|
|
1521
|
+
spinner.stop();
|
|
1522
|
+
}
|
|
1523
|
+
if (json || !process.stdout.isTTY) {
|
|
1524
|
+
console.log(JSON.stringify(coupons));
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (coupons.length === 0) {
|
|
1528
|
+
console.log("No coupons for this resource. Run `mainlayer coupon create` to add one.");
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
printTable([
|
|
1532
|
+
"CODE",
|
|
1533
|
+
"TYPE",
|
|
1534
|
+
"VALUE",
|
|
1535
|
+
"USES",
|
|
1536
|
+
"MAX",
|
|
1537
|
+
"EXPIRES"
|
|
1538
|
+
], coupons.map((c) => [
|
|
1539
|
+
c.code,
|
|
1540
|
+
c.discount_type,
|
|
1541
|
+
String(c.discount_value),
|
|
1542
|
+
String(c.uses_count),
|
|
1543
|
+
String(c.max_uses ?? "unlimited"),
|
|
1544
|
+
c.expires_at ?? "-"
|
|
1545
|
+
]));
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
if (err instanceof AppError) {
|
|
1548
|
+
printError(err.message);
|
|
1549
|
+
process.exitCode = err.exitCode;
|
|
1550
|
+
} else throw err;
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
coupon.command("delete <code>").description("Delete a coupon by code").option("--resource-id <id>", "Resource ID (required)").option("--force", "Confirm deletion without prompt").option("--json", "Output as JSON").action(async (rawCode, opts) => {
|
|
1554
|
+
try {
|
|
1555
|
+
const json = opts.json ?? false;
|
|
1556
|
+
if (!opts.resourceId) {
|
|
1557
|
+
printError("Missing required flag: --resource-id");
|
|
1558
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const resourceId = opts.resourceId;
|
|
1562
|
+
const code = rawCode.toUpperCase();
|
|
1563
|
+
if (!opts.force) {
|
|
1564
|
+
printError(`Error: This will delete coupon '${code}'. Run with --force to confirm.`);
|
|
1565
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const spinner = ora({
|
|
1569
|
+
text: "Deleting coupon...",
|
|
1570
|
+
stream: process.stderr
|
|
1571
|
+
}).start();
|
|
1572
|
+
try {
|
|
1573
|
+
await apiClient.delete(`resources/${resourceId}/coupons/${code}`);
|
|
1574
|
+
} finally {
|
|
1575
|
+
spinner.stop();
|
|
1576
|
+
}
|
|
1577
|
+
formatOutput({
|
|
1578
|
+
deleted: true,
|
|
1579
|
+
code
|
|
1580
|
+
}, { json });
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
if (err instanceof AppError) {
|
|
1583
|
+
printError(err.message);
|
|
1584
|
+
process.exitCode = err.exitCode;
|
|
1585
|
+
} else throw err;
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
return coupon;
|
|
1589
|
+
}
|
|
1590
|
+
//#endregion
|
|
1591
|
+
//#region src/cli/earnings.ts
|
|
1592
|
+
function expandPeriod(period) {
|
|
1593
|
+
const to = /* @__PURE__ */ new Date();
|
|
1594
|
+
const days = {
|
|
1595
|
+
"7d": 7,
|
|
1596
|
+
"30d": 30,
|
|
1597
|
+
"90d": 90
|
|
1598
|
+
}[period];
|
|
1599
|
+
return {
|
|
1600
|
+
from: (/* @__PURE__ */ new Date(to.getTime() - days * 864e5)).toISOString().slice(0, 10),
|
|
1601
|
+
to: to.toISOString().slice(0, 10)
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
function earningsCommand() {
|
|
1605
|
+
return new Command("earnings").description("View earnings summary").option("--period <period>", "Time period (7d, 30d, 90d)").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--resource-id <id>", "Filter by resource ID").option("--json", "Output as JSON").action(async (opts) => {
|
|
1606
|
+
let fromDate;
|
|
1607
|
+
let toDate;
|
|
1608
|
+
if (opts.period) {
|
|
1609
|
+
if (![
|
|
1610
|
+
"7d",
|
|
1611
|
+
"30d",
|
|
1612
|
+
"90d"
|
|
1613
|
+
].includes(opts.period)) {
|
|
1614
|
+
printError("--period must be one of: 7d, 30d, 90d");
|
|
1615
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const expanded = expandPeriod(opts.period);
|
|
1619
|
+
fromDate = expanded.from;
|
|
1620
|
+
toDate = expanded.to;
|
|
1621
|
+
} else if (opts.from || opts.to) {
|
|
1622
|
+
if (!opts.from || !opts.to) {
|
|
1623
|
+
printError("Both --from and --to are required when using date range");
|
|
1624
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
fromDate = opts.from;
|
|
1628
|
+
toDate = opts.to;
|
|
1629
|
+
} else {
|
|
1630
|
+
const expanded = expandPeriod("30d");
|
|
1631
|
+
fromDate = expanded.from;
|
|
1632
|
+
toDate = expanded.to;
|
|
1633
|
+
}
|
|
1634
|
+
const params = {
|
|
1635
|
+
from: fromDate,
|
|
1636
|
+
to: toDate
|
|
1637
|
+
};
|
|
1638
|
+
if (opts.resourceId) params["resource_id"] = opts.resourceId;
|
|
1639
|
+
const spinner = ora({
|
|
1640
|
+
text: "Loading earnings...",
|
|
1641
|
+
stream: process.stderr
|
|
1642
|
+
}).start();
|
|
1643
|
+
let data;
|
|
1644
|
+
try {
|
|
1645
|
+
data = await apiClient.get("vendor/earnings", params);
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
spinner.stop();
|
|
1648
|
+
if (err instanceof AppError) {
|
|
1649
|
+
printError(err.message);
|
|
1650
|
+
process.exitCode = err.exitCode;
|
|
1651
|
+
} else throw err;
|
|
1652
|
+
return;
|
|
1653
|
+
} finally {
|
|
1654
|
+
spinner.stop();
|
|
1655
|
+
}
|
|
1656
|
+
if ((opts.json ?? false) || !process.stdout.isTTY) {
|
|
1657
|
+
console.log(JSON.stringify(data));
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
formatOutput({
|
|
1661
|
+
period: `${fromDate} to ${toDate}`,
|
|
1662
|
+
total_calls: data.total_calls,
|
|
1663
|
+
total_revenue: formatPrice(data.total_revenue_usdc),
|
|
1664
|
+
unique_buyers: data.unique_buyers
|
|
1665
|
+
}, { json: false });
|
|
1666
|
+
if (data.daily.length > 0) {
|
|
1667
|
+
console.log("");
|
|
1668
|
+
printTable([
|
|
1669
|
+
"DATE",
|
|
1670
|
+
"CALLS",
|
|
1671
|
+
"REVENUE"
|
|
1672
|
+
], data.daily.map((d) => [
|
|
1673
|
+
d.date,
|
|
1674
|
+
String(d.calls),
|
|
1675
|
+
formatPrice(d.revenue_usdc)
|
|
1676
|
+
]));
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
//#endregion
|
|
1681
|
+
//#region src/cli/metrics.ts
|
|
1682
|
+
function metricsCommand() {
|
|
1683
|
+
return new Command("metrics").description("View resource metrics").option("--resource-id <id>", "Resource ID (required)").option("--json", "Output as JSON").action(async (opts) => {
|
|
1684
|
+
if (!opts.resourceId) {
|
|
1685
|
+
printError("--resource-id is required");
|
|
1686
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const spinner = ora({
|
|
1690
|
+
text: "Loading metrics...",
|
|
1691
|
+
stream: process.stderr
|
|
1692
|
+
}).start();
|
|
1693
|
+
let data;
|
|
1694
|
+
try {
|
|
1695
|
+
data = await apiClient.get("vendor/metrics", { resource_id: opts.resourceId });
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
spinner.stop();
|
|
1698
|
+
if (err instanceof AppError) {
|
|
1699
|
+
printError(err.message);
|
|
1700
|
+
process.exitCode = err.exitCode;
|
|
1701
|
+
} else throw err;
|
|
1702
|
+
return;
|
|
1703
|
+
} finally {
|
|
1704
|
+
spinner.stop();
|
|
1705
|
+
}
|
|
1706
|
+
if ((opts.json ?? false) || !process.stdout.isTTY) {
|
|
1707
|
+
console.log(JSON.stringify(data));
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
formatOutput({
|
|
1711
|
+
resource_id: data.resource_id,
|
|
1712
|
+
total_calls: data.total_calls,
|
|
1713
|
+
total_revenue: formatPrice(data.total_revenue_usdc),
|
|
1714
|
+
unique_buyers: data.unique_buyers,
|
|
1715
|
+
quota_calls: data.quota_calls ?? "unlimited",
|
|
1716
|
+
buyers_over_quota: data.buyers_over_quota ?? 0
|
|
1717
|
+
}, { json: false });
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/cli/discover.ts
|
|
1722
|
+
function discoverCommand() {
|
|
1723
|
+
return new Command("discover").description("Search discoverable resources on Mainlayer").option("--json", "Output as JSON").option("--query <text>", "Text search on slug and description").option("--fee-model <model>", "Filter by fee model (one_time, subscription, pay_per_call, hybrid)").option("--max-price <usdc>", "Maximum price in USDC (decimal or micro-units)").option("--type <type>", "Filter by resource type (api, file, endpoint, page)").option("--limit <n>", "Number of results (default: 20, max: 100)", "20").action(async (opts) => {
|
|
1724
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
1725
|
+
const params = {};
|
|
1726
|
+
const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 20, 1), 100);
|
|
1727
|
+
params["limit"] = String(limit);
|
|
1728
|
+
if (opts.query) params["q"] = opts.query;
|
|
1729
|
+
if (opts.feeModel) params["fee_model"] = opts.feeModel;
|
|
1730
|
+
if (opts.maxPrice) params["max_price"] = String(parsePrice(opts.maxPrice));
|
|
1731
|
+
if (opts.type) params["type"] = opts.type;
|
|
1732
|
+
const spinner = ora({
|
|
1733
|
+
text: "Searching resources...",
|
|
1734
|
+
stream: process.stderr
|
|
1735
|
+
}).start();
|
|
1736
|
+
let items;
|
|
1737
|
+
try {
|
|
1738
|
+
items = await apiClient.get("resources/discover", params);
|
|
1739
|
+
} finally {
|
|
1740
|
+
spinner.stop();
|
|
1741
|
+
}
|
|
1742
|
+
if (json) {
|
|
1743
|
+
console.log(JSON.stringify(items));
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
if (items.length === 0) {
|
|
1747
|
+
printSuccess("No resources found.");
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
printTable([
|
|
1751
|
+
"ID",
|
|
1752
|
+
"SLUG",
|
|
1753
|
+
"TYPE",
|
|
1754
|
+
"PRICE",
|
|
1755
|
+
"FEE MODEL"
|
|
1756
|
+
], items.map((r) => [
|
|
1757
|
+
truncate(r.id, 12),
|
|
1758
|
+
truncate(r.slug, 24),
|
|
1759
|
+
r.type,
|
|
1760
|
+
formatPrice(r.price_usdc),
|
|
1761
|
+
r.fee_model
|
|
1762
|
+
]));
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/cli/buy.ts
|
|
1767
|
+
function buyCommand() {
|
|
1768
|
+
return new Command("buy").description("Purchase a resource via X402 Solana USDC payment").argument("<resource-id>", "Resource ID to purchase").option("--json", "Output as JSON").option("--plan <name>", "Select pricing plan").option("--coupon <code>", "Apply discount coupon code").option("--chain <chain>", "Blockchain to use (solana, base, polygon)", "solana").action(async (resourceId, opts) => {
|
|
1769
|
+
try {
|
|
1770
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
1771
|
+
const passphrase = await getPassphrase();
|
|
1772
|
+
const walletAddr = await walletService.getAddress();
|
|
1773
|
+
const spinner = ora({
|
|
1774
|
+
text: "Preparing transaction...",
|
|
1775
|
+
stream: process.stderr
|
|
1776
|
+
}).start();
|
|
1777
|
+
let prepare;
|
|
1778
|
+
try {
|
|
1779
|
+
prepare = await apiClient.post("pay/prepare", {
|
|
1780
|
+
resource_id: resourceId,
|
|
1781
|
+
payer_wallet: walletAddr,
|
|
1782
|
+
chain: opts.chain,
|
|
1783
|
+
plan: opts.plan,
|
|
1784
|
+
coupon_code: opts.coupon
|
|
1785
|
+
});
|
|
1786
|
+
} catch (err) {
|
|
1787
|
+
spinner.stop();
|
|
1788
|
+
if (err instanceof AppError && err.message.includes("plan_required")) throw new AppError("This resource requires a pricing plan. Re-run with --plan <name>. Use \"mainlayer discover\" to see available plans.", EXIT_CODES.VALIDATION_ERROR);
|
|
1789
|
+
throw err;
|
|
1790
|
+
}
|
|
1791
|
+
spinner.stop();
|
|
1792
|
+
if (process.stdout.isTTY && !json) {
|
|
1793
|
+
const confirmed = await clack.confirm({ message: `Pay ${formatPrice(prepare.amount_usdc)} for resource ${resourceId}${opts.plan ? ` (plan: ${opts.plan})` : ""}?` });
|
|
1794
|
+
if (!confirmed || clack.isCancel(confirmed)) {
|
|
1795
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const signSpinner = ora({
|
|
1800
|
+
text: "Signing transaction...",
|
|
1801
|
+
stream: process.stderr
|
|
1802
|
+
}).start();
|
|
1803
|
+
const signedTxB64 = await walletService.signTransaction(passphrase, prepare.unsigned_transaction);
|
|
1804
|
+
signSpinner.stop();
|
|
1805
|
+
const submitSpinner = ora({
|
|
1806
|
+
text: "Submitting payment...",
|
|
1807
|
+
stream: process.stderr
|
|
1808
|
+
}).start();
|
|
1809
|
+
let result;
|
|
1810
|
+
try {
|
|
1811
|
+
result = await apiClient.post("pay", {
|
|
1812
|
+
resource_id: resourceId,
|
|
1813
|
+
payer_wallet: walletAddr,
|
|
1814
|
+
signed_transaction: signedTxB64,
|
|
1815
|
+
chain: opts.chain,
|
|
1816
|
+
plan: opts.plan,
|
|
1817
|
+
coupon_code: opts.coupon
|
|
1818
|
+
});
|
|
1819
|
+
} catch (err) {
|
|
1820
|
+
submitSpinner.stop();
|
|
1821
|
+
if (err instanceof AppError && err.message.includes("tx_expired")) throw new AppError("Transaction expired (blockhash too old). Please try again.", EXIT_CODES.GENERAL);
|
|
1822
|
+
throw err;
|
|
1823
|
+
}
|
|
1824
|
+
submitSpinner.stop();
|
|
1825
|
+
formatOutput({
|
|
1826
|
+
resource: result.resource_id,
|
|
1827
|
+
amount: formatPrice(result.amount_usdc),
|
|
1828
|
+
tx_hash: result.tx_hash,
|
|
1829
|
+
entitlement_id: result.entitlement_id
|
|
1830
|
+
}, { json });
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
if (err instanceof AppError) {
|
|
1833
|
+
printError(err.message);
|
|
1834
|
+
process.exitCode = err.exitCode;
|
|
1835
|
+
} else throw err;
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
//#endregion
|
|
1840
|
+
//#region src/cli/entitlements.ts
|
|
1841
|
+
function entitlementsCommand() {
|
|
1842
|
+
return new Command("entitlements").description("List your active entitlements (access rights)").option("--json", "Output as JSON").option("--resource-id <id>", "Filter by resource ID").option("--limit <n>", "Number of results (default: 20)", "20").action(async (opts) => {
|
|
1843
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
1844
|
+
const params = { limit: opts.limit };
|
|
1845
|
+
const spinner = ora({
|
|
1846
|
+
text: "Fetching entitlements...",
|
|
1847
|
+
stream: process.stderr
|
|
1848
|
+
}).start();
|
|
1849
|
+
let items;
|
|
1850
|
+
try {
|
|
1851
|
+
items = await apiClient.get("entitlements/my", params);
|
|
1852
|
+
} finally {
|
|
1853
|
+
spinner.stop();
|
|
1854
|
+
}
|
|
1855
|
+
if (opts.resourceId) items = items.filter((e) => e.resource_id === opts.resourceId);
|
|
1856
|
+
if (json) {
|
|
1857
|
+
console.log(JSON.stringify(items));
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (items.length === 0) {
|
|
1861
|
+
printSuccess("No entitlements found.");
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
printTable([
|
|
1865
|
+
"RESOURCE ID",
|
|
1866
|
+
"SLUG",
|
|
1867
|
+
"STATUS",
|
|
1868
|
+
"EXPIRES",
|
|
1869
|
+
"CREDITS"
|
|
1870
|
+
], items.map((e) => [
|
|
1871
|
+
truncate(e.resource_id, 12),
|
|
1872
|
+
truncate(e.resource_slug, 24),
|
|
1873
|
+
e.status,
|
|
1874
|
+
e.expires_at ?? "never",
|
|
1875
|
+
e.remaining_credits !== null ? String(e.remaining_credits) : "unlimited"
|
|
1876
|
+
]));
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
//#endregion
|
|
1880
|
+
//#region src/cli/subscribe.ts
|
|
1881
|
+
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
1882
|
+
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
1883
|
+
const DEFAULT_SOLANA_NETWORK = "solana:mainnet";
|
|
1884
|
+
function approveSubcommand() {
|
|
1885
|
+
return new Command("approve").description("Approve auto-renewal for a subscription resource").option("--json", "Output as JSON").requiredOption("--resource-id <id>", "Resource ID to subscribe to").option("--max-renewals <n>", "Max renewal cycles (1-12, default: 1)", "1").option("--plan <name>", "Select pricing plan").option("--chain <chain>", "Blockchain (solana, base, polygon)", "solana").option("--trial-days <n>", "Trial period in days (0-365)").action(async (opts) => {
|
|
1886
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
1887
|
+
const maxRenewals = Math.min(Math.max(parseInt(opts.maxRenewals, 10) || 1, 1), 12);
|
|
1888
|
+
const passphrase = await getPassphrase();
|
|
1889
|
+
const walletAddr = await walletService.getAddress();
|
|
1890
|
+
const [ata] = await findAssociatedTokenPda({
|
|
1891
|
+
owner: address(walletAddr),
|
|
1892
|
+
mint: address(USDC_MINT),
|
|
1893
|
+
tokenProgram: address(TOKEN_PROGRAM)
|
|
1894
|
+
});
|
|
1895
|
+
const delegateTokenAccount = ata.toString();
|
|
1896
|
+
const signedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1897
|
+
const network = process.env["MAINLAYER_SOLANA_NETWORK"] ?? DEFAULT_SOLANA_NETWORK;
|
|
1898
|
+
const message = `approve:${opts.resourceId}:${maxRenewals}:${delegateTokenAccount}:${signedAt}:${network}`;
|
|
1899
|
+
const spinner = ora({
|
|
1900
|
+
text: "Signing approval...",
|
|
1901
|
+
stream: process.stderr
|
|
1902
|
+
}).start();
|
|
1903
|
+
const signedApproval = await walletService.signMessage(passphrase, message);
|
|
1904
|
+
spinner.stop();
|
|
1905
|
+
const submitSpinner = ora({
|
|
1906
|
+
text: "Submitting approval...",
|
|
1907
|
+
stream: process.stderr
|
|
1908
|
+
}).start();
|
|
1909
|
+
const body = {
|
|
1910
|
+
resource_id: opts.resourceId,
|
|
1911
|
+
payer_wallet: walletAddr,
|
|
1912
|
+
max_renewals: maxRenewals,
|
|
1913
|
+
chain: opts.chain,
|
|
1914
|
+
signed_approval: signedApproval,
|
|
1915
|
+
delegate_token_account: delegateTokenAccount,
|
|
1916
|
+
signed_at: signedAt
|
|
1917
|
+
};
|
|
1918
|
+
if (opts.plan) body["plan"] = opts.plan;
|
|
1919
|
+
if (opts.trialDays !== void 0) body["trial_days"] = parseInt(opts.trialDays, 10);
|
|
1920
|
+
let result;
|
|
1921
|
+
try {
|
|
1922
|
+
result = await apiClient.post("subscriptions/approve", body);
|
|
1923
|
+
} finally {
|
|
1924
|
+
submitSpinner.stop();
|
|
1925
|
+
}
|
|
1926
|
+
formatOutput({
|
|
1927
|
+
approval_id: result.approval_id,
|
|
1928
|
+
status: result.status,
|
|
1929
|
+
max_renewals: result.max_renewals,
|
|
1930
|
+
renewals_used: result.renewals_used
|
|
1931
|
+
}, { json });
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
function pauseSubcommand() {
|
|
1935
|
+
return new Command("pause").description("Pause a subscription").option("--json", "Output as JSON").requiredOption("--approval-id <id>", "Subscription approval ID").action(async (opts) => {
|
|
1936
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
1937
|
+
const passphrase = await getPassphrase();
|
|
1938
|
+
const walletAddr = await walletService.getAddress();
|
|
1939
|
+
const message = `pause:${opts.approvalId}`;
|
|
1940
|
+
const signedMessage = await walletService.signMessage(passphrase, message);
|
|
1941
|
+
const spinner = ora({
|
|
1942
|
+
text: "Pausing subscription...",
|
|
1943
|
+
stream: process.stderr
|
|
1944
|
+
}).start();
|
|
1945
|
+
let result;
|
|
1946
|
+
try {
|
|
1947
|
+
result = await apiClient.post(`subscriptions/${opts.approvalId}/pause`, {
|
|
1948
|
+
payer_wallet: walletAddr,
|
|
1949
|
+
signed_message: signedMessage
|
|
1950
|
+
});
|
|
1951
|
+
} finally {
|
|
1952
|
+
spinner.stop();
|
|
1953
|
+
}
|
|
1954
|
+
formatOutput({
|
|
1955
|
+
approval_id: opts.approvalId,
|
|
1956
|
+
status: result.status
|
|
1957
|
+
}, { json });
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
function resumeSubcommand() {
|
|
1961
|
+
return new Command("resume").description("Resume a paused subscription").option("--json", "Output as JSON").requiredOption("--approval-id <id>", "Subscription approval ID").action(async (opts) => {
|
|
1962
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
1963
|
+
const passphrase = await getPassphrase();
|
|
1964
|
+
const walletAddr = await walletService.getAddress();
|
|
1965
|
+
const message = `resume:${opts.approvalId}`;
|
|
1966
|
+
const signedMessage = await walletService.signMessage(passphrase, message);
|
|
1967
|
+
const spinner = ora({
|
|
1968
|
+
text: "Resuming subscription...",
|
|
1969
|
+
stream: process.stderr
|
|
1970
|
+
}).start();
|
|
1971
|
+
let result;
|
|
1972
|
+
try {
|
|
1973
|
+
result = await apiClient.post(`subscriptions/${opts.approvalId}/resume`, {
|
|
1974
|
+
payer_wallet: walletAddr,
|
|
1975
|
+
signed_message: signedMessage
|
|
1976
|
+
});
|
|
1977
|
+
} finally {
|
|
1978
|
+
spinner.stop();
|
|
1979
|
+
}
|
|
1980
|
+
formatOutput({
|
|
1981
|
+
approval_id: opts.approvalId,
|
|
1982
|
+
status: result.status
|
|
1983
|
+
}, { json });
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
function cancelSubcommand() {
|
|
1987
|
+
return new Command("cancel").description("Cancel a subscription").option("--json", "Output as JSON").requiredOption("--resource-id <id>", "Resource ID to cancel subscription for").action(async (opts) => {
|
|
1988
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
1989
|
+
const passphrase = await getPassphrase();
|
|
1990
|
+
const walletAddr = await walletService.getAddress();
|
|
1991
|
+
const message = `cancel:${opts.resourceId}`;
|
|
1992
|
+
const signedMessage = await walletService.signMessage(passphrase, message);
|
|
1993
|
+
const spinner = ora({
|
|
1994
|
+
text: "Cancelling subscription...",
|
|
1995
|
+
stream: process.stderr
|
|
1996
|
+
}).start();
|
|
1997
|
+
let result;
|
|
1998
|
+
try {
|
|
1999
|
+
result = await apiClient.post("subscriptions/cancel", {
|
|
2000
|
+
resource_id: opts.resourceId,
|
|
2001
|
+
payer_wallet: walletAddr,
|
|
2002
|
+
signed_message: signedMessage
|
|
2003
|
+
});
|
|
2004
|
+
} finally {
|
|
2005
|
+
spinner.stop();
|
|
2006
|
+
}
|
|
2007
|
+
formatOutput({
|
|
2008
|
+
resource_id: opts.resourceId,
|
|
2009
|
+
status: result.status
|
|
2010
|
+
}, { json });
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
function listSubcommand$2() {
|
|
2014
|
+
return new Command("list").description("List your subscriptions (active only)").option("--json", "Output as JSON").action(async (opts) => {
|
|
2015
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2016
|
+
const walletAddr = await walletService.getAddress();
|
|
2017
|
+
const spinner = ora({
|
|
2018
|
+
text: "Fetching subscriptions...",
|
|
2019
|
+
stream: process.stderr
|
|
2020
|
+
}).start();
|
|
2021
|
+
let items;
|
|
2022
|
+
try {
|
|
2023
|
+
items = await apiClient.get("subscriptions/my", { payer_wallet: walletAddr });
|
|
2024
|
+
} finally {
|
|
2025
|
+
spinner.stop();
|
|
2026
|
+
}
|
|
2027
|
+
if (json) {
|
|
2028
|
+
console.log(JSON.stringify(items));
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (items.length === 0) {
|
|
2032
|
+
printSuccess("No active subscriptions found.");
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
printTable([
|
|
2036
|
+
"APPROVAL ID",
|
|
2037
|
+
"RESOURCE",
|
|
2038
|
+
"PLAN",
|
|
2039
|
+
"RENEWALS",
|
|
2040
|
+
"STATUS"
|
|
2041
|
+
], items.map((s) => [
|
|
2042
|
+
truncate(s.approval_id, 12),
|
|
2043
|
+
truncate(s.resource_id, 12),
|
|
2044
|
+
s.plan_name ?? "-",
|
|
2045
|
+
`${s.renewals_used}/${s.max_renewals}`,
|
|
2046
|
+
s.status
|
|
2047
|
+
]));
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
function getSubcommand$1() {
|
|
2051
|
+
return new Command("get").description("Get details of a single subscription").argument("<approval-id>", "Subscription approval ID").option("--json", "Output as JSON").action(async (approvalId, opts) => {
|
|
2052
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2053
|
+
const walletAddr = await walletService.getAddress();
|
|
2054
|
+
const spinner = ora({
|
|
2055
|
+
text: "Fetching subscription...",
|
|
2056
|
+
stream: process.stderr
|
|
2057
|
+
}).start();
|
|
2058
|
+
let items;
|
|
2059
|
+
try {
|
|
2060
|
+
items = await apiClient.get("subscriptions/my", { payer_wallet: walletAddr });
|
|
2061
|
+
} finally {
|
|
2062
|
+
spinner.stop();
|
|
2063
|
+
}
|
|
2064
|
+
const sub = items.find((s) => s.approval_id === approvalId);
|
|
2065
|
+
if (!sub) throw new AppError(`Subscription ${approvalId} not found`, EXIT_CODES.NOT_FOUND);
|
|
2066
|
+
formatOutput(sub, { json });
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
function subscribeCommand() {
|
|
2070
|
+
return new Command("subscribe").description("Manage subscriptions").addCommand(approveSubcommand()).addCommand(pauseSubcommand()).addCommand(resumeSubcommand()).addCommand(cancelSubcommand()).addCommand(listSubcommand$2()).addCommand(getSubcommand$1());
|
|
2071
|
+
}
|
|
2072
|
+
//#endregion
|
|
2073
|
+
//#region src/cli/invoices.ts
|
|
2074
|
+
function listSubcommand$1() {
|
|
2075
|
+
return new Command("list").description("List invoices for resources you own").option("--json", "Output as JSON").option("--limit <n>", "Number of results (default: 20)", "20").action(async (opts) => {
|
|
2076
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2077
|
+
const params = { limit: opts.limit };
|
|
2078
|
+
const spinner = ora({
|
|
2079
|
+
text: "Fetching invoices...",
|
|
2080
|
+
stream: process.stderr
|
|
2081
|
+
}).start();
|
|
2082
|
+
let items;
|
|
2083
|
+
try {
|
|
2084
|
+
items = await apiClient.get("invoices", params);
|
|
2085
|
+
} finally {
|
|
2086
|
+
spinner.stop();
|
|
2087
|
+
}
|
|
2088
|
+
if (json) {
|
|
2089
|
+
console.log(JSON.stringify(items));
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if (items.length === 0) {
|
|
2093
|
+
printSuccess("No invoices found.");
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
printTable([
|
|
2097
|
+
"ID",
|
|
2098
|
+
"RESOURCE",
|
|
2099
|
+
"AMOUNT",
|
|
2100
|
+
"STATUS",
|
|
2101
|
+
"DATE"
|
|
2102
|
+
], items.map((inv) => [
|
|
2103
|
+
truncate(inv.id, 12),
|
|
2104
|
+
truncate(inv.resource_id, 12),
|
|
2105
|
+
formatPrice(inv.amount_usdc),
|
|
2106
|
+
inv.status,
|
|
2107
|
+
inv.created_at.split("T")[0] ?? inv.created_at
|
|
2108
|
+
]));
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
function getSubcommand() {
|
|
2112
|
+
return new Command("get").description("Get details of a single invoice").argument("<invoice-id>", "Invoice ID").option("--json", "Output as JSON").action(async (invoiceId, opts) => {
|
|
2113
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2114
|
+
const spinner = ora({
|
|
2115
|
+
text: "Fetching invoice...",
|
|
2116
|
+
stream: process.stderr
|
|
2117
|
+
}).start();
|
|
2118
|
+
let invoice;
|
|
2119
|
+
try {
|
|
2120
|
+
invoice = await apiClient.get(`invoices/${invoiceId}`);
|
|
2121
|
+
} finally {
|
|
2122
|
+
spinner.stop();
|
|
2123
|
+
}
|
|
2124
|
+
formatOutput({
|
|
2125
|
+
id: invoice.id,
|
|
2126
|
+
resource_id: invoice.resource_id,
|
|
2127
|
+
amount: formatPrice(invoice.amount_usdc),
|
|
2128
|
+
status: invoice.status,
|
|
2129
|
+
created_at: invoice.created_at
|
|
2130
|
+
}, { json });
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
function invoicesCommand() {
|
|
2134
|
+
return new Command("invoices").description("View invoices for resources you own").addCommand(listSubcommand$1()).addCommand(getSubcommand());
|
|
2135
|
+
}
|
|
2136
|
+
//#endregion
|
|
2137
|
+
//#region src/cli/refund.ts
|
|
2138
|
+
function requestSubcommand() {
|
|
2139
|
+
return new Command("request").description("Request a refund for a payment (requires ownership of the associated resource)").option("--json", "Output as JSON").requiredOption("--payment-id <id>", "Payment ID to refund").requiredOption("--reason <text>", "Reason for refund").option("--amount <usdc>", "Refund amount in USDC (defaults to full payment amount)").action(async (opts) => {
|
|
2140
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2141
|
+
const body = {
|
|
2142
|
+
payment_id: opts.paymentId,
|
|
2143
|
+
reason: opts.reason
|
|
2144
|
+
};
|
|
2145
|
+
if (opts.amount) body["amount_usdc"] = parsePrice(opts.amount);
|
|
2146
|
+
const spinner = ora({
|
|
2147
|
+
text: "Submitting refund request...",
|
|
2148
|
+
stream: process.stderr
|
|
2149
|
+
}).start();
|
|
2150
|
+
let result;
|
|
2151
|
+
try {
|
|
2152
|
+
result = await apiClient.post("refunds", body);
|
|
2153
|
+
} finally {
|
|
2154
|
+
spinner.stop();
|
|
2155
|
+
}
|
|
2156
|
+
formatOutput({
|
|
2157
|
+
id: result.id,
|
|
2158
|
+
payment_id: result.payment_id,
|
|
2159
|
+
status: result.status
|
|
2160
|
+
}, { json });
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
function refundCommand() {
|
|
2164
|
+
return new Command("refund").description("Manage refunds").addCommand(requestSubcommand());
|
|
2165
|
+
}
|
|
2166
|
+
//#endregion
|
|
2167
|
+
//#region src/cli/dispute.ts
|
|
2168
|
+
function createSubcommand() {
|
|
2169
|
+
return new Command("create").description("Create a dispute for a payment").option("--json", "Output as JSON").requiredOption("--payment-id <id>", "Payment ID to dispute").requiredOption("--reason <text>", "Reason for dispute").action(async (opts) => {
|
|
2170
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2171
|
+
const walletAddr = await walletService.getAddress();
|
|
2172
|
+
const spinner = ora({
|
|
2173
|
+
text: "Creating dispute...",
|
|
2174
|
+
stream: process.stderr
|
|
2175
|
+
}).start();
|
|
2176
|
+
let result;
|
|
2177
|
+
try {
|
|
2178
|
+
result = await apiClient.post("disputes", {
|
|
2179
|
+
payment_id: opts.paymentId,
|
|
2180
|
+
reason: opts.reason,
|
|
2181
|
+
payer_wallet: walletAddr
|
|
2182
|
+
});
|
|
2183
|
+
} finally {
|
|
2184
|
+
spinner.stop();
|
|
2185
|
+
}
|
|
2186
|
+
formatOutput({
|
|
2187
|
+
dispute_id: result.dispute_id,
|
|
2188
|
+
payment_id: result.payment_id,
|
|
2189
|
+
status: result.status,
|
|
2190
|
+
reason: result.reason
|
|
2191
|
+
}, { json });
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
function listSubcommand() {
|
|
2195
|
+
return new Command("list").description("List disputes (for resources you own)").option("--json", "Output as JSON").option("--limit <n>", "Number of results (default: 20)", "20").action(async (opts) => {
|
|
2196
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2197
|
+
const params = { limit: opts.limit };
|
|
2198
|
+
const spinner = ora({
|
|
2199
|
+
text: "Fetching disputes...",
|
|
2200
|
+
stream: process.stderr
|
|
2201
|
+
}).start();
|
|
2202
|
+
let items;
|
|
2203
|
+
try {
|
|
2204
|
+
items = await apiClient.get("disputes", params);
|
|
2205
|
+
} finally {
|
|
2206
|
+
spinner.stop();
|
|
2207
|
+
}
|
|
2208
|
+
if (json) {
|
|
2209
|
+
console.log(JSON.stringify(items));
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (items.length === 0) {
|
|
2213
|
+
printSuccess("No disputes found.");
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
printTable([
|
|
2217
|
+
"DISPUTE ID",
|
|
2218
|
+
"PAYMENT ID",
|
|
2219
|
+
"STATUS",
|
|
2220
|
+
"REASON"
|
|
2221
|
+
], items.map((d) => [
|
|
2222
|
+
truncate(d.dispute_id, 12),
|
|
2223
|
+
truncate(d.payment_id, 12),
|
|
2224
|
+
d.status,
|
|
2225
|
+
truncate(d.reason, 30)
|
|
2226
|
+
]));
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
function disputeCommand() {
|
|
2230
|
+
return new Command("dispute").description("Manage disputes").addCommand(createSubcommand()).addCommand(listSubcommand());
|
|
2231
|
+
}
|
|
2232
|
+
//#endregion
|
|
2233
|
+
//#region src/cli/setup.ts
|
|
2234
|
+
function setupCommand() {
|
|
2235
|
+
return new Command("setup").description("Detect AI platforms and configure Mainlayer MCP server").option("--force", "Re-write MCP entries even if already configured").option("--json", "Output machine-readable JSON").action(async (opts) => {
|
|
2236
|
+
const json = opts.json ?? !process.stdout.isTTY;
|
|
2237
|
+
let spinner;
|
|
2238
|
+
if (!json) spinner = ora({
|
|
2239
|
+
text: "Detecting AI platforms...",
|
|
2240
|
+
stream: process.stderr
|
|
2241
|
+
}).start();
|
|
2242
|
+
let results;
|
|
2243
|
+
try {
|
|
2244
|
+
results = await configurePlatforms({ force: opts.force ?? false });
|
|
2245
|
+
} catch (err) {
|
|
2246
|
+
if (spinner) spinner.fail("Platform detection failed");
|
|
2247
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
2248
|
+
process.exitCode = 1;
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
for (const result of results) {
|
|
2252
|
+
if (!result.configured) continue;
|
|
2253
|
+
const desc = PLATFORMS.find((p) => p.name === result.name);
|
|
2254
|
+
if (!desc) continue;
|
|
2255
|
+
try {
|
|
2256
|
+
const dir = desc.skillsDir(homedir());
|
|
2257
|
+
mkdirSync(dir, { recursive: true });
|
|
2258
|
+
writeFileSync(join(dir, SKILLS_FILENAME), generateSkillsMd(), "utf8");
|
|
2259
|
+
result.skillsDropped = true;
|
|
2260
|
+
} catch {}
|
|
2261
|
+
}
|
|
2262
|
+
if (!json) {
|
|
2263
|
+
if (spinner) spinner.stop();
|
|
2264
|
+
for (const r of results) if (r.configured) process.stderr.write(chalk.green(` \u2714 ${r.name}\n`));
|
|
2265
|
+
else if (r.skipped) process.stderr.write(chalk.yellow(` \u2139 ${r.name}: ${r.error ?? "skipped"}\n`));
|
|
2266
|
+
else if (r.error) process.stderr.write(chalk.red(` \u2718 ${r.name}: ${r.error}\n`));
|
|
2267
|
+
else process.stderr.write(chalk.gray(` \u2013 ${r.name} (not detected)\n`));
|
|
2268
|
+
const configuredNames = results.filter((r) => r.configured).map((r) => r.name);
|
|
2269
|
+
if (configuredNames.length > 0) printSuccess(`Configured MCP for: ${configuredNames.join(", ")}`);
|
|
2270
|
+
else process.stderr.write("No AI platforms detected\n");
|
|
2271
|
+
} else formatOutput({ platforms: results.map((r) => ({
|
|
2272
|
+
name: r.name,
|
|
2273
|
+
configured: r.configured,
|
|
2274
|
+
skills_dropped: r.skillsDropped,
|
|
2275
|
+
skipped: r.skipped,
|
|
2276
|
+
error: r.error ?? null
|
|
2277
|
+
})) }, { json: true });
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
//#endregion
|
|
2281
|
+
//#region src/utils/update-check.ts
|
|
2282
|
+
const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
2283
|
+
async function checkForUpdates() {
|
|
2284
|
+
if (process.env["NO_UPDATE_NOTIFIER"]) return;
|
|
2285
|
+
if (!process.stdout.isTTY) return;
|
|
2286
|
+
try {
|
|
2287
|
+
const lastCheck = configService.get("lastUpdateCheck");
|
|
2288
|
+
if (lastCheck && Date.now() - parseInt(lastCheck, 10) < CHECK_INTERVAL_MS) return;
|
|
2289
|
+
const current = process.env["npm_package_version"] ?? "0.0.0";
|
|
2290
|
+
const latest = await latestVersion("@mainlayer/cli");
|
|
2291
|
+
configService.set("lastUpdateCheck", String(Date.now()));
|
|
2292
|
+
if (latest !== current) process.stderr.write(`\nUpdate available: ${current} -> ${latest}\nRun: npm i -g @mainlayer/cli\n\n`);
|
|
2293
|
+
} catch {}
|
|
2294
|
+
}
|
|
2295
|
+
//#endregion
|
|
2296
|
+
//#region src/cli/index.ts
|
|
2297
|
+
const program = new Command("mainlayer").description("Mainlayer CLI — AI-native payment infrastructure").showSuggestionAfterError(true).version("0.1.0", "-v, --version").option("--json", "Output machine-readable JSON").option("--api-key <key>", "API key override (also: MAINLAYER_API_KEY env)", process.env["MAINLAYER_API_KEY"]).option("--profile <name>", "Use named profile for config/auth isolation", process.env["MAINLAYER_PROFILE"]).addCommand(authCommand()).addCommand(walletCommand()).addCommand(configCommand()).addCommand(webhookCommand()).addCommand(resourceCommand()).addCommand(couponCommand()).addCommand(earningsCommand()).addCommand(metricsCommand()).addCommand(discoverCommand()).addCommand(buyCommand()).addCommand(entitlementsCommand()).addCommand(subscribeCommand()).addCommand(invoicesCommand()).addCommand(refundCommand()).addCommand(disputeCommand()).addCommand(setupCommand());
|
|
2298
|
+
program.hook("preAction", () => {
|
|
2299
|
+
const opts = program.opts();
|
|
2300
|
+
if (opts.apiKey) apiClient.setApiKeyOverride(opts.apiKey);
|
|
2301
|
+
const profile = opts.profile ?? configService.getActiveProfile();
|
|
2302
|
+
if (profile !== "default") configService.setProfile(profile);
|
|
2303
|
+
checkForUpdates().catch(() => {});
|
|
2304
|
+
});
|
|
2305
|
+
program.exitOverride();
|
|
2306
|
+
(async () => {
|
|
2307
|
+
try {
|
|
2308
|
+
await program.parseAsync(process.argv);
|
|
2309
|
+
} catch (err) {
|
|
2310
|
+
if (err instanceof AppError) {
|
|
2311
|
+
if (process.argv.includes("--json") || !process.stdout.isTTY) console.log(JSON.stringify({
|
|
2312
|
+
error: true,
|
|
2313
|
+
message: err.message,
|
|
2314
|
+
code: err.exitCode,
|
|
2315
|
+
type: err.meta?.type ?? "error",
|
|
2316
|
+
hint: err.meta?.hint ?? null
|
|
2317
|
+
}));
|
|
2318
|
+
else {
|
|
2319
|
+
printError(err.message);
|
|
2320
|
+
if (err.meta?.hint) process.stderr.write(chalk.yellow(err.meta.hint) + "\n");
|
|
2321
|
+
}
|
|
2322
|
+
process.exitCode = err.exitCode;
|
|
2323
|
+
} else if (err instanceof Error && err.name === "CommanderError") {} else {
|
|
2324
|
+
printError(String(err));
|
|
2325
|
+
process.exitCode = 1;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
})();
|
|
2329
|
+
//#endregion
|
|
2330
|
+
export {};
|