run402 1.0.0
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/cli.mjs +67 -0
- package/lib/config.mjs +40 -0
- package/lib/deploy.mjs +92 -0
- package/lib/image.mjs +80 -0
- package/lib/projects.mjs +128 -0
- package/lib/wallet.mjs +89 -0
- package/package.json +35 -0
package/cli.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* run402 — CLI for Run402
|
|
4
|
+
* https://run402.com
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const [,, cmd, sub, ...rest] = process.argv;
|
|
8
|
+
|
|
9
|
+
const HELP = `run402 v1.0.0 — Full-stack backend infra for AI agents
|
|
10
|
+
https://run402.com
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
run402 <command> [subcommand] [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
wallet Manage your x402 wallet (create, fund, check status)
|
|
17
|
+
projects Manage deployed projects (list, query, inspect, renew, delete)
|
|
18
|
+
deploy Deploy a full-stack app or static site (Postgres + hosting)
|
|
19
|
+
image Generate AI images via x402 micropayments
|
|
20
|
+
|
|
21
|
+
Run 'run402 <command> --help' for detailed usage of each command.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
run402 wallet create
|
|
25
|
+
run402 wallet fund
|
|
26
|
+
run402 deploy --tier prototype --manifest app.json
|
|
27
|
+
run402 projects list
|
|
28
|
+
run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
|
|
29
|
+
run402 image generate "a startup mascot, pixel art" --output logo.png
|
|
30
|
+
|
|
31
|
+
Getting started:
|
|
32
|
+
1. run402 wallet create Create a local wallet
|
|
33
|
+
2. run402 wallet fund Fund it with test USDC (Base Sepolia faucet)
|
|
34
|
+
3. run402 deploy ... Deploy your app — payments handled automatically
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
38
|
+
console.log(HELP);
|
|
39
|
+
process.exit(cmd ? 0 : 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (cmd) {
|
|
43
|
+
case "wallet": {
|
|
44
|
+
const { run } = await import("./lib/wallet.mjs");
|
|
45
|
+
await run(sub, rest);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "projects": {
|
|
49
|
+
const { run } = await import("./lib/projects.mjs");
|
|
50
|
+
await run(sub, rest);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "deploy": {
|
|
54
|
+
const { run } = await import("./lib/deploy.mjs");
|
|
55
|
+
await run([sub, ...rest].filter(Boolean));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "image": {
|
|
59
|
+
const { run } = await import("./lib/image.mjs");
|
|
60
|
+
await run(sub, rest);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
65
|
+
console.log(HELP);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run402 config loader — reads local project and wallet state.
|
|
3
|
+
* Kept in a separate module so credential reads stay isolated.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
|
|
10
|
+
export const CONFIG_DIR = join(homedir(), ".config", "run402");
|
|
11
|
+
export const WALLET_FILE = join(CONFIG_DIR, "wallet.json");
|
|
12
|
+
export const PROJECTS_FILE = join(CONFIG_DIR, "projects.json");
|
|
13
|
+
export const API = "https://api.run402.com";
|
|
14
|
+
|
|
15
|
+
export function readWallet() {
|
|
16
|
+
if (!existsSync(WALLET_FILE)) return null;
|
|
17
|
+
return JSON.parse(readFileSync(WALLET_FILE, "utf-8"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function saveWallet(data) {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
writeFileSync(WALLET_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
23
|
+
try { chmodSync(WALLET_FILE, 0o600); } catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadProjects() {
|
|
27
|
+
if (!existsSync(PROJECTS_FILE)) return [];
|
|
28
|
+
return JSON.parse(readFileSync(PROJECTS_FILE, "utf-8"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveProjects(projects) {
|
|
32
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function findProject(id) {
|
|
37
|
+
const p = loadProjects().find(p => p.project_id === id);
|
|
38
|
+
if (!p) { console.error(`Project ${id} not found in local registry.`); process.exit(1); }
|
|
39
|
+
return p;
|
|
40
|
+
}
|
package/lib/deploy.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { readWallet, loadProjects, API, WALLET_FILE, PROJECTS_FILE } from "./config.mjs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 deploy — Deploy a full-stack app or static site on Run402
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 deploy [options]
|
|
8
|
+
cat manifest.json | run402 deploy [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--tier <tier> Deployment tier: prototype | hobby | team (default: prototype)
|
|
12
|
+
--manifest <file> Path to manifest JSON file (default: read from stdin)
|
|
13
|
+
--help, -h Show this help message
|
|
14
|
+
|
|
15
|
+
Tiers:
|
|
16
|
+
prototype Smallest, cheapest — great for demos and experiments
|
|
17
|
+
hobby Mid-tier — personal projects and side hustles
|
|
18
|
+
team Full power — production-ready, shared team access
|
|
19
|
+
|
|
20
|
+
Manifest format (JSON):
|
|
21
|
+
{
|
|
22
|
+
"name": "my-app",
|
|
23
|
+
"files": {
|
|
24
|
+
"index.html": "<html>...</html>",
|
|
25
|
+
"style.css": "body { margin: 0; }"
|
|
26
|
+
},
|
|
27
|
+
"env": {
|
|
28
|
+
"MY_VAR": "value"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
run402 deploy --tier prototype --manifest app.json
|
|
34
|
+
run402 deploy --tier hobby --manifest app.json
|
|
35
|
+
cat app.json | run402 deploy --tier team
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
- Requires a funded wallet (run402 wallet create && run402 wallet fund)
|
|
39
|
+
- Payments are processed automatically via x402 micropayments (Base Sepolia USDC)
|
|
40
|
+
- Project credentials (project_id, keys, URL) are saved locally after deploy
|
|
41
|
+
- Use 'run402 projects list' to see all deployed projects
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
async function readStdin() {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
47
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveProject(project) {
|
|
51
|
+
const projects = loadProjects();
|
|
52
|
+
projects.push({ project_id: project.project_id, anon_key: project.anon_key, service_key: project.service_key, tier: project.tier, lease_expires_at: project.lease_expires_at, site_url: project.site_url || project.subdomain_url, deployed_at: new Date().toISOString() });
|
|
53
|
+
const dir = PROJECTS_FILE.replace(/\/[^/]+$/, "");
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2), { mode: 0o600 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function run(args) {
|
|
59
|
+
const opts = { tier: "prototype", manifest: null };
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
|
|
62
|
+
if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
|
|
63
|
+
if (args[i] === "--manifest" && args[i + 1]) opts.manifest = args[++i];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!existsSync(WALLET_FILE)) {
|
|
67
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const wallet = readWallet();
|
|
71
|
+
const manifest = opts.manifest ? JSON.parse(readFileSync(opts.manifest, "utf-8")) : JSON.parse(await readStdin());
|
|
72
|
+
|
|
73
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
74
|
+
const { createPublicClient, http } = await import("viem");
|
|
75
|
+
const { baseSepolia } = await import("viem/chains");
|
|
76
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
77
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
78
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
79
|
+
|
|
80
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
81
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
82
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
83
|
+
const client = new x402Client();
|
|
84
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
85
|
+
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
86
|
+
|
|
87
|
+
const res = await fetchPaid(`${API}/v1/deploy/${opts.tier}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(manifest) });
|
|
88
|
+
const result = await res.json();
|
|
89
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...result })); process.exit(1); }
|
|
90
|
+
saveProject(result);
|
|
91
|
+
console.log(JSON.stringify(result, null, 2));
|
|
92
|
+
}
|
package/lib/image.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { writeFileSync, existsSync } from "fs";
|
|
2
|
+
import { readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 image — Generate AI images via x402 micropayments
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 image generate "<prompt>" [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--aspect <ratio> Image aspect ratio: square | landscape | portrait (default: square)
|
|
11
|
+
--output <file> Save image to file (e.g. output.png)
|
|
12
|
+
If omitted, returns base64 JSON to stdout
|
|
13
|
+
--help, -h Show this help message
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
run402 image generate "a startup mascot, pixel art"
|
|
17
|
+
run402 image generate "futuristic city at night" --aspect landscape
|
|
18
|
+
run402 image generate "portrait of a cat CEO" --aspect portrait --output cat.png
|
|
19
|
+
|
|
20
|
+
Output (without --output):
|
|
21
|
+
{ "status": "ok", "aspect": "square", "content_type": "image/png", "image": "<base64>" }
|
|
22
|
+
|
|
23
|
+
Notes:
|
|
24
|
+
- Requires a funded wallet (run402 wallet create && run402 wallet fund)
|
|
25
|
+
- Payments are processed automatically via x402 micropayments (Base Sepolia USDC)
|
|
26
|
+
- Use --output to save directly to a file instead of printing base64
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
export async function run(sub, args) {
|
|
30
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
31
|
+
console.log(HELP);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (sub !== "generate") {
|
|
36
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
37
|
+
console.log(HELP);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const opts = { prompt: null, aspect: "square", output: null };
|
|
42
|
+
let i = 0;
|
|
43
|
+
if (i < args.length && !args[i].startsWith("--")) opts.prompt = args[i++];
|
|
44
|
+
while (i < args.length) {
|
|
45
|
+
if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
|
|
46
|
+
else if (args[i] === "--aspect" && args[i + 1]) { opts.aspect = args[++i]; }
|
|
47
|
+
else if (args[i] === "--output" && args[i + 1]) { opts.output = args[++i]; }
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!opts.prompt) { console.error(JSON.stringify({ status: "error", message: "Prompt required. Usage: run402 image generate \"your prompt\"" })); process.exit(1); }
|
|
52
|
+
if (!existsSync(WALLET_FILE)) { console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" })); process.exit(1); }
|
|
53
|
+
|
|
54
|
+
const wallet = readWallet();
|
|
55
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
56
|
+
const { createPublicClient, http } = await import("viem");
|
|
57
|
+
const { baseSepolia } = await import("viem/chains");
|
|
58
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
59
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
60
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
61
|
+
|
|
62
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
63
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
64
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
65
|
+
const client = new x402Client();
|
|
66
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
67
|
+
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
68
|
+
|
|
69
|
+
const res = await fetchPaid(`${API}/v1/generate-image`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: opts.prompt, aspect: opts.aspect }) });
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
72
|
+
|
|
73
|
+
if (opts.output) {
|
|
74
|
+
const buf = Buffer.from(data.image, "base64");
|
|
75
|
+
writeFileSync(opts.output, buf);
|
|
76
|
+
console.log(JSON.stringify({ status: "ok", file: opts.output, size: buf.length, aspect: data.aspect }));
|
|
77
|
+
} else {
|
|
78
|
+
console.log(JSON.stringify({ status: "ok", aspect: data.aspect, content_type: data.content_type, image: data.image }));
|
|
79
|
+
}
|
|
80
|
+
}
|
package/lib/projects.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { findProject, loadProjects, saveProjects, readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 projects — Manage your deployed Run402 projects
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 projects <subcommand> [args...]
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
list List all your projects (IDs, tiers, URLs, expiry)
|
|
11
|
+
sql <id> "<query>" Run a SQL query against a project's Postgres DB
|
|
12
|
+
rest <id> <table> [params] Query a table via the REST API (PostgREST)
|
|
13
|
+
usage <id> Show compute/storage usage for a project
|
|
14
|
+
schema <id> Inspect the database schema
|
|
15
|
+
renew <id> Extend the project lease (pays via x402)
|
|
16
|
+
delete <id> Delete a project and remove it from local state
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
run402 projects list
|
|
20
|
+
run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
|
|
21
|
+
run402 projects rest abc123 users "limit=10&select=id,name"
|
|
22
|
+
run402 projects usage abc123
|
|
23
|
+
run402 projects schema abc123
|
|
24
|
+
run402 projects renew abc123
|
|
25
|
+
run402 projects delete abc123
|
|
26
|
+
|
|
27
|
+
Notes:
|
|
28
|
+
- <id> is the project_id shown in 'run402 projects list'
|
|
29
|
+
- 'rest' uses PostgREST query syntax (table name + optional query string)
|
|
30
|
+
- 'renew' requires a funded wallet — payment is automatic via x402
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
async function setupPaidFetch() {
|
|
34
|
+
if (!existsSync(WALLET_FILE)) {
|
|
35
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const wallet = readWallet();
|
|
39
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
40
|
+
const { createPublicClient, http } = await import("viem");
|
|
41
|
+
const { baseSepolia } = await import("viem/chains");
|
|
42
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
43
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
44
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
45
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
46
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
47
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
48
|
+
const client = new x402Client();
|
|
49
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
50
|
+
return wrapFetchWithPayment(fetch, client);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function list() {
|
|
54
|
+
const projects = loadProjects();
|
|
55
|
+
if (projects.length === 0) { console.log(JSON.stringify({ status: "ok", projects: [], message: "No projects yet." })); return; }
|
|
56
|
+
console.log(JSON.stringify(projects.map(p => ({ project_id: p.project_id, tier: p.tier, site_url: p.site_url, lease_expires_at: p.lease_expires_at, deployed_at: p.deployed_at })), null, 2));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function sqlCmd(projectId, query) {
|
|
60
|
+
const p = findProject(projectId);
|
|
61
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/sql`, { method: "POST", headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "text/plain" }, body: query });
|
|
62
|
+
console.log(JSON.stringify(await res.json(), null, 2));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function rest(projectId, table, queryParams) {
|
|
66
|
+
const p = findProject(projectId);
|
|
67
|
+
const res = await fetch(`${API}/rest/v1/${table}${queryParams ? '?' + queryParams : ''}`, { headers: { "apikey": p.anon_key } });
|
|
68
|
+
console.log(JSON.stringify(await res.json(), null, 2));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function usage(projectId) {
|
|
72
|
+
const p = findProject(projectId);
|
|
73
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/usage`, { headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
76
|
+
console.log(JSON.stringify(data, null, 2));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function schema(projectId) {
|
|
80
|
+
const p = findProject(projectId);
|
|
81
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/schema`, { headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
84
|
+
console.log(JSON.stringify(data, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function renew(projectId) {
|
|
88
|
+
const fetchPaid = await setupPaidFetch();
|
|
89
|
+
const res = await fetchPaid(`${API}/v1/projects/${projectId}/renew`, { method: "POST", headers: { "Content-Type": "application/json" } });
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
92
|
+
const projects = loadProjects();
|
|
93
|
+
const idx = projects.findIndex(pr => pr.project_id === projectId);
|
|
94
|
+
if (idx >= 0 && data.lease_expires_at) { projects[idx].lease_expires_at = data.lease_expires_at; saveProjects(projects); }
|
|
95
|
+
console.log(JSON.stringify(data, null, 2));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function deleteProject(projectId) {
|
|
99
|
+
const p = findProject(projectId);
|
|
100
|
+
const res = await fetch(`${API}/v1/projects/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
101
|
+
if (res.status === 204 || res.ok) {
|
|
102
|
+
saveProjects(loadProjects().filter(pr => pr.project_id !== projectId));
|
|
103
|
+
console.log(JSON.stringify({ status: "ok", message: `Project ${projectId} deleted.` }));
|
|
104
|
+
} else {
|
|
105
|
+
const data = await res.json().catch(() => ({}));
|
|
106
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function run(sub, args) {
|
|
111
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
112
|
+
console.log(HELP);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
switch (sub) {
|
|
116
|
+
case "list": await list(); break;
|
|
117
|
+
case "sql": await sqlCmd(args[0], args[1]); break;
|
|
118
|
+
case "rest": await rest(args[0], args[1], args[2]); break;
|
|
119
|
+
case "usage": await usage(args[0]); break;
|
|
120
|
+
case "schema": await schema(args[0]); break;
|
|
121
|
+
case "renew": await renew(args[0]); break;
|
|
122
|
+
case "delete": await deleteProject(args[0]); break;
|
|
123
|
+
default:
|
|
124
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
125
|
+
console.log(HELP);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
package/lib/wallet.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readWallet, saveWallet, API } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
const HELP = `run402 wallet — Manage your x402 wallet
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
run402 wallet <subcommand>
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
status Show wallet address, network, and funding status
|
|
10
|
+
create Generate a new wallet and save it locally
|
|
11
|
+
fund Request test USDC from the Run402 faucet (Base Sepolia)
|
|
12
|
+
export Print the wallet address (useful for scripting)
|
|
13
|
+
|
|
14
|
+
Notes:
|
|
15
|
+
- Wallet is stored locally at ~/.run402/wallet.json
|
|
16
|
+
- Network: Base Sepolia (testnet) — uses USDC for x402 payments
|
|
17
|
+
- You need to create and fund a wallet before deploying or generating images
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
run402 wallet create
|
|
21
|
+
run402 wallet status
|
|
22
|
+
run402 wallet fund
|
|
23
|
+
run402 wallet export
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
async function loadDeps() {
|
|
27
|
+
const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
|
|
28
|
+
const { createPublicClient, http } = await import("viem");
|
|
29
|
+
const { baseSepolia } = await import("viem/chains");
|
|
30
|
+
return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, baseSepolia };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function status() {
|
|
34
|
+
const w = readWallet();
|
|
35
|
+
if (!w) {
|
|
36
|
+
console.log(JSON.stringify({ status: "no_wallet", message: "No wallet found. Run: run402 wallet create" }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log(JSON.stringify({ status: "ok", address: w.address, network: w.network || "base-sepolia", created: w.created, funded: w.funded || false }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function create() {
|
|
43
|
+
if (readWallet()) {
|
|
44
|
+
console.log(JSON.stringify({ status: "error", message: "Wallet already exists. Use 'status' to check it." }));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const { generatePrivateKey, privateKeyToAccount } = await loadDeps();
|
|
48
|
+
const privateKey = generatePrivateKey();
|
|
49
|
+
const account = privateKeyToAccount(privateKey);
|
|
50
|
+
saveWallet({ address: account.address, privateKey, network: "base-sepolia", created: new Date().toISOString(), funded: false });
|
|
51
|
+
console.log(JSON.stringify({ status: "ok", address: account.address, message: "Wallet created and saved." }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fund() {
|
|
55
|
+
const w = readWallet();
|
|
56
|
+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
|
|
57
|
+
const res = await fetch(`${API}/v1/faucet`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: w.address }) });
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
saveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
|
|
61
|
+
console.log(JSON.stringify({ status: "ok", ...data }));
|
|
62
|
+
} else {
|
|
63
|
+
console.log(JSON.stringify({ status: "error", ...data }));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function exportAddr() {
|
|
69
|
+
const w = readWallet();
|
|
70
|
+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet." })); process.exit(1); }
|
|
71
|
+
console.log(w.address);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function run(sub, args) {
|
|
75
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
76
|
+
console.log(HELP);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
switch (sub) {
|
|
80
|
+
case "status": await status(); break;
|
|
81
|
+
case "create": await create(); break;
|
|
82
|
+
case "fund": await fund(); break;
|
|
83
|
+
case "export": await exportAddr(); break;
|
|
84
|
+
default:
|
|
85
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
86
|
+
console.log(HELP);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "run402",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"run402": "cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli.mjs",
|
|
11
|
+
"lib/"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@x402/evm": "^2.6.0",
|
|
15
|
+
"@x402/fetch": "^2.6.0",
|
|
16
|
+
"viem": "^2.47.1"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"homepage": "https://run402.com",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/kychee-com/run402-mcp.git",
|
|
26
|
+
"directory": "cli"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"run402",
|
|
30
|
+
"x402",
|
|
31
|
+
"postgres",
|
|
32
|
+
"ai-agent",
|
|
33
|
+
"openclaw"
|
|
34
|
+
]
|
|
35
|
+
}
|