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 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
- projects Manage projects (provision, list, query, inspect, renew, delete)
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
- 1. run402 wallet create Create a local wallet
43
- 2. run402 wallet fund Fund it with test USDC (Base Sepolia faucet)
44
- 3. run402 deploy ... Deploy your app — payments handled automatically
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
- - 'renew' and 'provision' require a funded wallet — payment is automatic via x402
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.",
5
5
  "type": "module",
6
6
  "bin": {