run402 1.7.0 → 1.8.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/cli.mjs CHANGED
@@ -13,9 +13,11 @@ 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
- deploy Deploy a full-stack app or static site (Postgres + hosting)
18
+ tier Manage tier subscription (status, set)
19
+ projects Manage projects (provision, list, query, inspect, delete)
20
+ deploy Deploy a full-stack app or static site (requires active tier)
19
21
  functions Manage serverless functions (deploy, invoke, logs, list, delete)
20
22
  secrets Manage project secrets (set, list, delete)
21
23
  storage Manage file storage (upload, download, list, delete)
@@ -31,7 +33,7 @@ Run 'run402 <command> --help' for detailed usage of each command.
31
33
  Examples:
32
34
  run402 wallet create
33
35
  run402 wallet fund
34
- run402 deploy --tier prototype --manifest app.json
36
+ run402 deploy --manifest app.json
35
37
  run402 projects list
36
38
  run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
37
39
  run402 functions deploy <project_id> my-fn --code handler.ts
@@ -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 --manifest app.json
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/deploy.mjs CHANGED
@@ -8,35 +8,27 @@ Usage:
8
8
  cat manifest.json | run402 deploy [options]
9
9
 
10
10
  Options:
11
- --tier <tier> Deployment tier: prototype | hobby | team (default: prototype)
12
11
  --manifest <file> Path to manifest JSON file (default: read from stdin)
13
12
  --help, -h Show this help message
14
13
 
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
14
  Manifest format (JSON):
21
15
  {
22
16
  "name": "my-app",
23
- "files": {
24
- "index.html": "<html>...</html>",
25
- "style.css": "body { margin: 0; }"
26
- },
27
- "env": {
28
- "MY_VAR": "value"
29
- }
17
+ "migrations": "CREATE TABLE items ...",
18
+ "site": [{ "file": "index.html", "data": "<html>...</html>" }],
19
+ "subdomain": "my-app"
30
20
  }
31
21
 
32
22
  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
23
+ run402 deploy --manifest app.json
24
+ cat app.json | run402 deploy
25
+
26
+ Prerequisites:
27
+ - run402 init Set up wallet and funding
28
+ - run402 tier set prototype Subscribe to a tier
36
29
 
37
30
  Notes:
38
- - Requires a funded wallet (run402 wallet create && run402 wallet fund)
39
- - Payments are processed automatically via x402 micropayments (Base Sepolia USDC)
31
+ - Requires an active tier subscription (run402 tier set <tier>)
40
32
  - Project credentials (project_id, keys, URL) are saved locally after deploy
41
33
  - Use 'run402 projects list' to see all deployed projects
42
34
  `;
@@ -56,10 +48,9 @@ function saveProject(project) {
56
48
  }
57
49
 
58
50
  export async function run(args) {
59
- const opts = { tier: "prototype", manifest: null };
51
+ const opts = { manifest: null };
60
52
  for (let i = 0; i < args.length; i++) {
61
53
  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
54
  if (args[i] === "--manifest" && args[i + 1]) opts.manifest = args[++i];
64
55
  }
65
56
 
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 --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/lib/wallet.mjs CHANGED
@@ -10,7 +10,6 @@ Subcommands:
10
10
  create Generate a new wallet and save it locally
11
11
  fund Request test USDC from the Run402 faucet (Base Sepolia)
12
12
  balance Show on-chain USDC (mainnet + testnet) and Run402 billing balance
13
- tier Show current tier subscription (tier, status, expiry)
14
13
  export Print the wallet address (useful for scripting)
15
14
  checkout Create a billing checkout session (--amount <usd_micros>)
16
15
  history View billing transaction history (--limit <n>)
@@ -127,21 +126,6 @@ async function balance() {
127
126
  }, null, 2));
128
127
  }
129
128
 
130
- async function tier() {
131
- const w = readWallet();
132
- if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
133
- const { privateKeyToAccount } = await loadDeps();
134
- const account = privateKeyToAccount(w.privateKey);
135
- const timestamp = Math.floor(Date.now() / 1000).toString();
136
- const signature = await account.signMessage({ message: `run402:${timestamp}` });
137
- const res = await fetch(`${API}/tiers/v1/status`, {
138
- headers: { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp },
139
- });
140
- const data = await res.json();
141
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
142
- console.log(JSON.stringify(data, null, 2));
143
- }
144
-
145
129
  async function exportAddr() {
146
130
  const w = readWallet();
147
131
  if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet." })); process.exit(1); }
@@ -189,7 +173,6 @@ export async function run(sub, args) {
189
173
  case "create": await create(); break;
190
174
  case "fund": await fund(); break;
191
175
  case "balance": await balance(); break;
192
- case "tier": await tier(); break;
193
176
  case "export": await exportAddr(); break;
194
177
  case "checkout": await checkout(args); break;
195
178
  case "history": await history(args); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
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": {