run402 1.6.0 → 1.8.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 +16 -4
- package/lib/init.mjs +105 -0
- package/lib/projects.mjs +1 -17
- package/lib/tier.mjs +84 -0
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -13,8 +13,10 @@ Usage:
|
|
|
13
13
|
run402 <command> [subcommand] [options]
|
|
14
14
|
|
|
15
15
|
Commands:
|
|
16
|
+
init Set up wallet, funding, and check tier status
|
|
16
17
|
wallet Manage your x402 wallet (create, fund, balance, status)
|
|
17
|
-
|
|
18
|
+
tier Manage tier subscription (status, set)
|
|
19
|
+
projects Manage projects (provision, list, query, inspect, delete)
|
|
18
20
|
deploy Deploy a full-stack app or static site (Postgres + hosting)
|
|
19
21
|
functions Manage serverless functions (deploy, invoke, logs, list, delete)
|
|
20
22
|
secrets Manage project secrets (set, list, delete)
|
|
@@ -39,9 +41,9 @@ Examples:
|
|
|
39
41
|
run402 image generate "a startup mascot, pixel art" --output logo.png
|
|
40
42
|
|
|
41
43
|
Getting started:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
run402 init Set up everything in one command
|
|
45
|
+
run402 tier set prototype Subscribe to a tier
|
|
46
|
+
run402 deploy ... Deploy your app
|
|
45
47
|
`;
|
|
46
48
|
|
|
47
49
|
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
@@ -50,11 +52,21 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
switch (cmd) {
|
|
55
|
+
case "init": {
|
|
56
|
+
const { run } = await import("./lib/init.mjs");
|
|
57
|
+
await run();
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
53
60
|
case "wallet": {
|
|
54
61
|
const { run } = await import("./lib/wallet.mjs");
|
|
55
62
|
await run(sub, rest);
|
|
56
63
|
break;
|
|
57
64
|
}
|
|
65
|
+
case "tier": {
|
|
66
|
+
const { run } = await import("./lib/tier.mjs");
|
|
67
|
+
await run(sub, rest);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
58
70
|
case "projects": {
|
|
59
71
|
const { run } = await import("./lib/projects.mjs");
|
|
60
72
|
await run(sub, rest);
|
package/lib/init.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readWallet, saveWallet, loadProjects, CONFIG_DIR, WALLET_FILE, API } from "./config.mjs";
|
|
2
|
+
import { mkdirSync } from "fs";
|
|
3
|
+
|
|
4
|
+
const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
|
|
5
|
+
const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
6
|
+
|
|
7
|
+
function short(addr) { return addr.slice(0, 6) + "..." + addr.slice(-4); }
|
|
8
|
+
function line(label, value) { console.log(` ${label.padEnd(10)} ${value}`); }
|
|
9
|
+
|
|
10
|
+
export async function run() {
|
|
11
|
+
console.log();
|
|
12
|
+
|
|
13
|
+
// 1. Config directory
|
|
14
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
line("Config", CONFIG_DIR);
|
|
16
|
+
|
|
17
|
+
// 2. Wallet
|
|
18
|
+
let wallet = readWallet();
|
|
19
|
+
if (!wallet) {
|
|
20
|
+
const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
|
|
21
|
+
const privateKey = generatePrivateKey();
|
|
22
|
+
const account = privateKeyToAccount(privateKey);
|
|
23
|
+
wallet = { address: account.address, privateKey, created: new Date().toISOString(), funded: false };
|
|
24
|
+
saveWallet(wallet);
|
|
25
|
+
line("Wallet", `${short(wallet.address)} (created)`);
|
|
26
|
+
} else {
|
|
27
|
+
line("Wallet", short(wallet.address));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Balance — check on-chain, faucet if zero
|
|
31
|
+
const { createPublicClient, http } = await import("viem");
|
|
32
|
+
const { baseSepolia } = await import("viem/chains");
|
|
33
|
+
const client = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
34
|
+
|
|
35
|
+
let balance = 0;
|
|
36
|
+
try {
|
|
37
|
+
const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [wallet.address] });
|
|
38
|
+
balance = Number(raw);
|
|
39
|
+
} catch {}
|
|
40
|
+
|
|
41
|
+
if (balance === 0) {
|
|
42
|
+
line("Balance", "0 USDC — requesting faucet...");
|
|
43
|
+
const res = await fetch(`${API}/faucet/v1`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ address: wallet.address }),
|
|
47
|
+
});
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
// Poll for up to 30s
|
|
50
|
+
for (let i = 0; i < 30; i++) {
|
|
51
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
52
|
+
try {
|
|
53
|
+
const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [wallet.address] });
|
|
54
|
+
balance = Number(raw);
|
|
55
|
+
if (balance > 0) break;
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
saveWallet({ ...wallet, funded: true, lastFaucet: new Date().toISOString() });
|
|
59
|
+
if (balance > 0) {
|
|
60
|
+
line("Balance", `${(balance / 1e6).toFixed(2)} USDC (funded)`);
|
|
61
|
+
} else {
|
|
62
|
+
line("Balance", "faucet sent — not yet confirmed on-chain");
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
const data = await res.json().catch(() => ({}));
|
|
66
|
+
const msg = data.error || data.message || `HTTP ${res.status}`;
|
|
67
|
+
line("Balance", `faucet failed: ${msg}`);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Tier status
|
|
74
|
+
let tierInfo = null;
|
|
75
|
+
try {
|
|
76
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
77
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
78
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
79
|
+
const signature = await account.signMessage({ message: `run402:${timestamp}` });
|
|
80
|
+
const res = await fetch(`${API}/tiers/v1/status`, {
|
|
81
|
+
headers: { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp },
|
|
82
|
+
});
|
|
83
|
+
if (res.ok) tierInfo = await res.json();
|
|
84
|
+
} catch {}
|
|
85
|
+
|
|
86
|
+
if (tierInfo && tierInfo.tier && tierInfo.status === "active") {
|
|
87
|
+
const expiry = tierInfo.lease_expires_at ? tierInfo.lease_expires_at.split("T")[0] : "unknown";
|
|
88
|
+
line("Tier", `${tierInfo.tier} (expires ${expiry})`);
|
|
89
|
+
} else {
|
|
90
|
+
line("Tier", "(none)");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Projects
|
|
94
|
+
const projects = loadProjects();
|
|
95
|
+
line("Projects", `${projects.length} active`);
|
|
96
|
+
|
|
97
|
+
// 6. Next step
|
|
98
|
+
console.log();
|
|
99
|
+
if (!tierInfo || !tierInfo.tier || tierInfo.status !== "active") {
|
|
100
|
+
console.log(" Next: run402 tier set prototype");
|
|
101
|
+
} else {
|
|
102
|
+
console.log(" Ready to deploy. Run: run402 deploy --tier prototype --manifest app.json");
|
|
103
|
+
}
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
package/lib/projects.mjs
CHANGED
|
@@ -15,7 +15,6 @@ Subcommands:
|
|
|
15
15
|
usage <id> Show compute/storage usage for a project
|
|
16
16
|
schema <id> Inspect the database schema
|
|
17
17
|
rls <id> <template> <tables_json> Apply Row-Level Security policies
|
|
18
|
-
renew <id> Extend the project lease (pays via x402)
|
|
19
18
|
delete <id> Delete a project and remove it from local state
|
|
20
19
|
|
|
21
20
|
Examples:
|
|
@@ -28,13 +27,12 @@ Examples:
|
|
|
28
27
|
run402 projects usage abc123
|
|
29
28
|
run402 projects schema abc123
|
|
30
29
|
run402 projects rls abc123 public_read '[{"table":"posts"}]'
|
|
31
|
-
run402 projects renew abc123
|
|
32
30
|
run402 projects delete abc123
|
|
33
31
|
|
|
34
32
|
Notes:
|
|
35
33
|
- <id> is the project_id shown in 'run402 projects list'
|
|
36
34
|
- 'rest' uses PostgREST query syntax (table name + optional query string)
|
|
37
|
-
- '
|
|
35
|
+
- 'provision' requires a funded wallet — payment is automatic via x402
|
|
38
36
|
- RLS templates: user_owns_rows, public_read, public_read_write
|
|
39
37
|
`;
|
|
40
38
|
|
|
@@ -142,19 +140,6 @@ async function schema(projectId) {
|
|
|
142
140
|
console.log(JSON.stringify(data, null, 2));
|
|
143
141
|
}
|
|
144
142
|
|
|
145
|
-
async function renew(projectId) {
|
|
146
|
-
const p = findProject(projectId);
|
|
147
|
-
const tier = p.tier || "prototype";
|
|
148
|
-
const fetchPaid = await setupPaidFetch();
|
|
149
|
-
const res = await fetchPaid(`${API}/tiers/v1/renew/${tier}`, { method: "POST", headers: { "Content-Type": "application/json" } });
|
|
150
|
-
const data = await res.json();
|
|
151
|
-
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
152
|
-
const projects = loadProjects();
|
|
153
|
-
const idx = projects.findIndex(pr => pr.project_id === projectId);
|
|
154
|
-
if (idx >= 0 && data.lease_expires_at) { projects[idx].lease_expires_at = data.lease_expires_at; saveProjects(projects); }
|
|
155
|
-
console.log(JSON.stringify(data, null, 2));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
143
|
async function deleteProject(projectId) {
|
|
159
144
|
const p = findProject(projectId);
|
|
160
145
|
const res = await fetch(`${API}/projects/v1/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
@@ -181,7 +166,6 @@ export async function run(sub, args) {
|
|
|
181
166
|
case "usage": await usage(args[0]); break;
|
|
182
167
|
case "schema": await schema(args[0]); break;
|
|
183
168
|
case "rls": await rls(args[0], args[1], args[2]); break;
|
|
184
|
-
case "renew": await renew(args[0]); break;
|
|
185
169
|
case "delete": await deleteProject(args[0]); break;
|
|
186
170
|
default:
|
|
187
171
|
console.error(`Unknown subcommand: ${sub}\n`);
|
package/lib/tier.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readWallet, WALLET_FILE, API } from "./config.mjs";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 tier — Manage your Run402 tier subscription
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 tier <subcommand> [args...]
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
status Show current tier (tier name, status, expiry)
|
|
11
|
+
set <tier> Subscribe, renew, or upgrade (pays via x402)
|
|
12
|
+
|
|
13
|
+
Tiers: prototype ($0.10/7d), hobby ($5/30d), team ($20/30d)
|
|
14
|
+
|
|
15
|
+
The server auto-detects the action based on your wallet state:
|
|
16
|
+
- No tier or expired → subscribe
|
|
17
|
+
- Same tier, active → renew (extends from expiry)
|
|
18
|
+
- Higher tier → upgrade (prorated refund to allowance)
|
|
19
|
+
- Lower tier, active → rejected (wait for expiry)
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
run402 tier status
|
|
23
|
+
run402 tier set prototype
|
|
24
|
+
run402 tier set hobby
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
async function setupPaidFetch() {
|
|
28
|
+
if (!existsSync(WALLET_FILE)) {
|
|
29
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const wallet = readWallet();
|
|
33
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
34
|
+
const { createPublicClient, http } = await import("viem");
|
|
35
|
+
const { baseSepolia } = await import("viem/chains");
|
|
36
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
37
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
38
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
39
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
40
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
41
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
42
|
+
const client = new x402Client();
|
|
43
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
44
|
+
return wrapFetchWithPayment(fetch, client);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function status() {
|
|
48
|
+
const w = readWallet();
|
|
49
|
+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
|
|
50
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
51
|
+
const account = privateKeyToAccount(w.privateKey);
|
|
52
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
53
|
+
const signature = await account.signMessage({ message: `run402:${timestamp}` });
|
|
54
|
+
const res = await fetch(`${API}/tiers/v1/status`, {
|
|
55
|
+
headers: { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp },
|
|
56
|
+
});
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
59
|
+
console.log(JSON.stringify(data, null, 2));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function set(tierName) {
|
|
63
|
+
if (!tierName) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 tier set <prototype|hobby|team>" })); process.exit(1); }
|
|
64
|
+
const fetchPaid = await setupPaidFetch();
|
|
65
|
+
const res = await fetchPaid(`${API}/tiers/v1/${tierName}`, { method: "POST", headers: { "Content-Type": "application/json" } });
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
68
|
+
console.log(JSON.stringify(data, null, 2));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function run(sub, args) {
|
|
72
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
73
|
+
console.log(HELP);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
switch (sub) {
|
|
77
|
+
case "status": await status(); break;
|
|
78
|
+
case "set": await set(args[0]); break;
|
|
79
|
+
default:
|
|
80
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
81
|
+
console.log(HELP);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/package.json
CHANGED