run402 1.0.0 → 1.3.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/README.md +116 -0
- package/cli.mjs +42 -4
- package/lib/apps.mjs +137 -0
- package/lib/functions.mjs +150 -0
- package/lib/projects.mjs +78 -17
- package/lib/secrets.mjs +70 -0
- package/lib/sites.mjs +92 -0
- package/lib/storage.mjs +101 -0
- package/lib/subdomains.mjs +87 -0
- package/lib/wallet.mjs +15 -4
- package/package.json +4 -10
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# run402 CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for [Run402](https://run402.com) — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g run402
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx run402 <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Create a local wallet
|
|
21
|
+
run402 wallet create
|
|
22
|
+
|
|
23
|
+
# 2. Fund it with test USDC (Base Sepolia faucet)
|
|
24
|
+
run402 wallet fund
|
|
25
|
+
|
|
26
|
+
# 3. Deploy your app
|
|
27
|
+
run402 deploy --tier prototype --manifest app.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### `run402 wallet`
|
|
33
|
+
|
|
34
|
+
Manage your local x402 wallet.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
run402 wallet create # Generate a new wallet
|
|
38
|
+
run402 wallet status # Show address, network, funding status
|
|
39
|
+
run402 wallet fund # Request test USDC from the faucet
|
|
40
|
+
run402 wallet export # Print wallet address (for scripting)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `run402 deploy`
|
|
44
|
+
|
|
45
|
+
Deploy a full-stack app or static site.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
run402 deploy --tier prototype --manifest app.json
|
|
49
|
+
run402 deploy --tier hobby --manifest app.json
|
|
50
|
+
cat app.json | run402 deploy --tier team
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Tiers:** `prototype` | `hobby` | `team`
|
|
54
|
+
|
|
55
|
+
**Manifest format:**
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"name": "my-app",
|
|
59
|
+
"files": {
|
|
60
|
+
"index.html": "<html>...</html>",
|
|
61
|
+
"style.css": "body { margin: 0; }"
|
|
62
|
+
},
|
|
63
|
+
"env": {
|
|
64
|
+
"MY_VAR": "value"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `run402 projects`
|
|
70
|
+
|
|
71
|
+
Manage your deployed projects.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
run402 projects list # List all projects
|
|
75
|
+
run402 projects sql <id> "SELECT * FROM users" # Run SQL query
|
|
76
|
+
run402 projects rest <id> users "limit=10" # REST API query
|
|
77
|
+
run402 projects usage <id> # Compute/storage usage
|
|
78
|
+
run402 projects schema <id> # Database schema
|
|
79
|
+
run402 projects renew <id> # Extend lease (pays via x402)
|
|
80
|
+
run402 projects delete <id> # Delete project
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `run402 image`
|
|
84
|
+
|
|
85
|
+
Generate AI images via x402 micropayments.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
run402 image generate "a startup mascot, pixel art"
|
|
89
|
+
run402 image generate "futuristic city at night" --aspect landscape
|
|
90
|
+
run402 image generate "portrait of a cat CEO" --aspect portrait --output cat.png
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Options:** `--aspect square|landscape|portrait` · `--output <file>`
|
|
94
|
+
|
|
95
|
+
## Help
|
|
96
|
+
|
|
97
|
+
Every command supports `--help` / `-h`:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
run402 --help
|
|
101
|
+
run402 wallet --help
|
|
102
|
+
run402 deploy --help
|
|
103
|
+
run402 projects --help
|
|
104
|
+
run402 image --help
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Notes
|
|
108
|
+
|
|
109
|
+
- Wallet stored at `~/.run402/wallet.json`
|
|
110
|
+
- Project credentials stored at `~/.run402/projects.json`
|
|
111
|
+
- Network: Base Sepolia (testnet) — USDC for x402 payments
|
|
112
|
+
- Payments are handled automatically — no manual signing required
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/cli.mjs
CHANGED
|
@@ -13,10 +13,16 @@ Usage:
|
|
|
13
13
|
run402 <command> [subcommand] [options]
|
|
14
14
|
|
|
15
15
|
Commands:
|
|
16
|
-
wallet
|
|
17
|
-
projects
|
|
18
|
-
deploy
|
|
19
|
-
|
|
16
|
+
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)
|
|
19
|
+
functions Manage serverless functions (deploy, invoke, logs, list, delete)
|
|
20
|
+
secrets Manage project secrets (set, list, delete)
|
|
21
|
+
storage Manage file storage (upload, download, list, delete)
|
|
22
|
+
sites Deploy static sites
|
|
23
|
+
subdomains Manage custom subdomains (claim, list, delete)
|
|
24
|
+
apps Browse and manage the app marketplace
|
|
25
|
+
image Generate AI images via x402 micropayments
|
|
20
26
|
|
|
21
27
|
Run 'run402 <command> --help' for detailed usage of each command.
|
|
22
28
|
|
|
@@ -26,6 +32,8 @@ Examples:
|
|
|
26
32
|
run402 deploy --tier prototype --manifest app.json
|
|
27
33
|
run402 projects list
|
|
28
34
|
run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
|
|
35
|
+
run402 functions deploy <project_id> my-fn --code handler.ts
|
|
36
|
+
run402 secrets set <project_id> API_KEY sk-1234
|
|
29
37
|
run402 image generate "a startup mascot, pixel art" --output logo.png
|
|
30
38
|
|
|
31
39
|
Getting started:
|
|
@@ -55,6 +63,36 @@ switch (cmd) {
|
|
|
55
63
|
await run([sub, ...rest].filter(Boolean));
|
|
56
64
|
break;
|
|
57
65
|
}
|
|
66
|
+
case "functions": {
|
|
67
|
+
const { run } = await import("./lib/functions.mjs");
|
|
68
|
+
await run(sub, rest);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "secrets": {
|
|
72
|
+
const { run } = await import("./lib/secrets.mjs");
|
|
73
|
+
await run(sub, rest);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "storage": {
|
|
77
|
+
const { run } = await import("./lib/storage.mjs");
|
|
78
|
+
await run(sub, rest);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "sites": {
|
|
82
|
+
const { run } = await import("./lib/sites.mjs");
|
|
83
|
+
await run(sub, rest);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "subdomains": {
|
|
87
|
+
const { run } = await import("./lib/subdomains.mjs");
|
|
88
|
+
await run(sub, rest);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "apps": {
|
|
92
|
+
const { run } = await import("./lib/apps.mjs");
|
|
93
|
+
await run(sub, rest);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
58
96
|
case "image": {
|
|
59
97
|
const { run } = await import("./lib/image.mjs");
|
|
60
98
|
await run(sub, rest);
|
package/lib/apps.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { findProject, readWallet, loadProjects, saveProjects, API, WALLET_FILE, PROJECTS_FILE } from "./config.mjs";
|
|
3
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
4
|
+
|
|
5
|
+
const HELP = `run402 apps — Browse and manage the app marketplace
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
run402 apps <subcommand> [args...]
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
browse [--tag <tag>] Browse public apps
|
|
12
|
+
fork <version_id> <name> [--tier <tier>] [--subdomain <name>]
|
|
13
|
+
Fork a published app into your own project
|
|
14
|
+
publish <id> [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed]
|
|
15
|
+
Publish a project as an app
|
|
16
|
+
versions <id> List published versions of a project
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
run402 apps browse
|
|
20
|
+
run402 apps browse --tag auth
|
|
21
|
+
run402 apps fork ver_abc123 my-todo --tier prototype
|
|
22
|
+
run402 apps publish proj123 --description "Todo app" --tags todo,auth --visibility public --fork-allowed
|
|
23
|
+
run402 apps versions proj123
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
async function browse(args) {
|
|
27
|
+
let url = `${API}/v1/apps`;
|
|
28
|
+
const tags = [];
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
if (args[i] === "--tag" && args[i + 1]) tags.push(args[++i]);
|
|
31
|
+
}
|
|
32
|
+
if (tags.length > 0) url += "?" + tags.map(t => `tag=${encodeURIComponent(t)}`).join("&");
|
|
33
|
+
const res = await fetch(url);
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
36
|
+
console.log(JSON.stringify(data, null, 2));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function fork(versionId, name, args) {
|
|
40
|
+
const opts = { tier: "prototype", subdomain: undefined };
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
|
|
43
|
+
if (args[i] === "--subdomain" && args[i + 1]) opts.subdomain = args[++i];
|
|
44
|
+
}
|
|
45
|
+
if (!existsSync(WALLET_FILE)) {
|
|
46
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const wallet = readWallet();
|
|
51
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
52
|
+
const { createPublicClient, http } = await import("viem");
|
|
53
|
+
const { baseSepolia } = await import("viem/chains");
|
|
54
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
55
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
56
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
57
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
58
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
59
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
60
|
+
const client = new x402Client();
|
|
61
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
62
|
+
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
63
|
+
|
|
64
|
+
const body = { version_id: versionId, name };
|
|
65
|
+
if (opts.subdomain) body.subdomain = opts.subdomain;
|
|
66
|
+
|
|
67
|
+
const res = await fetchPaid(`${API}/v1/fork/${opts.tier}`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
});
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
74
|
+
|
|
75
|
+
// Save project credentials locally
|
|
76
|
+
if (data.project_id) {
|
|
77
|
+
const projects = loadProjects();
|
|
78
|
+
projects.push({
|
|
79
|
+
project_id: data.project_id, anon_key: data.anon_key, service_key: data.service_key,
|
|
80
|
+
tier: data.tier, lease_expires_at: data.lease_expires_at,
|
|
81
|
+
site_url: data.site_url || data.subdomain_url, deployed_at: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
const dir = PROJECTS_FILE.replace(/\/[^/]+$/, "");
|
|
84
|
+
mkdirSync(dir, { recursive: true });
|
|
85
|
+
writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2), { mode: 0o600 });
|
|
86
|
+
}
|
|
87
|
+
console.log(JSON.stringify(data, null, 2));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function publish(projectId, args) {
|
|
91
|
+
const p = findProject(projectId);
|
|
92
|
+
const opts = { description: undefined, tags: undefined, visibility: undefined, forkAllowed: undefined };
|
|
93
|
+
for (let i = 0; i < args.length; i++) {
|
|
94
|
+
if (args[i] === "--description" && args[i + 1]) opts.description = args[++i];
|
|
95
|
+
if (args[i] === "--tags" && args[i + 1]) opts.tags = args[++i].split(",");
|
|
96
|
+
if (args[i] === "--visibility" && args[i + 1]) opts.visibility = args[++i];
|
|
97
|
+
if (args[i] === "--fork-allowed") opts.forkAllowed = true;
|
|
98
|
+
}
|
|
99
|
+
const body = {};
|
|
100
|
+
if (opts.description) body.description = opts.description;
|
|
101
|
+
if (opts.tags) body.tags = opts.tags;
|
|
102
|
+
if (opts.visibility) body.visibility = opts.visibility;
|
|
103
|
+
if (opts.forkAllowed !== undefined) body.fork_allowed = opts.forkAllowed;
|
|
104
|
+
|
|
105
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/publish`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
});
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
112
|
+
console.log(JSON.stringify(data, null, 2));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function versions(projectId) {
|
|
116
|
+
const p = findProject(projectId);
|
|
117
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/versions`, {
|
|
118
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
119
|
+
});
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
122
|
+
console.log(JSON.stringify(data, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function run(sub, args) {
|
|
126
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
127
|
+
switch (sub) {
|
|
128
|
+
case "browse": await browse(args); break;
|
|
129
|
+
case "fork": await fork(args[0], args[1], args.slice(2)); break;
|
|
130
|
+
case "publish": await publish(args[0], args.slice(1)); break;
|
|
131
|
+
case "versions": await versions(args[0]); break;
|
|
132
|
+
default:
|
|
133
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
134
|
+
console.log(HELP);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { findProject, readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 functions — Manage serverless functions
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 functions <subcommand> [args...]
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
deploy <id> <name> --code <file> [--timeout <s>] [--memory <mb>] [--deps <pkg,...>]
|
|
11
|
+
Deploy a function to a project
|
|
12
|
+
invoke <id> <name> [--method <M>] [--body <json>]
|
|
13
|
+
Invoke a deployed function
|
|
14
|
+
logs <id> <name> [--tail <n>] Get function logs
|
|
15
|
+
list <id> List all functions for a project
|
|
16
|
+
delete <id> <name> Delete a function
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
run402 functions deploy abc123 stripe-webhook --code handler.ts
|
|
20
|
+
run402 functions invoke abc123 stripe-webhook --body '{"event":"test"}'
|
|
21
|
+
run402 functions logs abc123 stripe-webhook --tail 100
|
|
22
|
+
run402 functions list abc123
|
|
23
|
+
run402 functions delete abc123 stripe-webhook
|
|
24
|
+
|
|
25
|
+
Notes:
|
|
26
|
+
- Code must export a default async function: export default async (req: Request) => Response
|
|
27
|
+
- Deploy may require payment if the project lease has expired
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
async function setupPaidFetch() {
|
|
31
|
+
if (!existsSync(WALLET_FILE)) {
|
|
32
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const wallet = readWallet();
|
|
36
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
37
|
+
const { createPublicClient, http } = await import("viem");
|
|
38
|
+
const { baseSepolia } = await import("viem/chains");
|
|
39
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
40
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
41
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
42
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
43
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
44
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
45
|
+
const client = new x402Client();
|
|
46
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
47
|
+
return wrapFetchWithPayment(fetch, client);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function deploy(projectId, name, args) {
|
|
51
|
+
const p = findProject(projectId);
|
|
52
|
+
const opts = { code: null, timeout: undefined, memory: undefined, deps: undefined };
|
|
53
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
|
+
if (args[i] === "--code" && args[i + 1]) opts.code = args[++i];
|
|
55
|
+
if (args[i] === "--timeout" && args[i + 1]) opts.timeout = parseInt(args[++i]);
|
|
56
|
+
if (args[i] === "--memory" && args[i + 1]) opts.memory = parseInt(args[++i]);
|
|
57
|
+
if (args[i] === "--deps" && args[i + 1]) opts.deps = args[++i].split(",");
|
|
58
|
+
}
|
|
59
|
+
if (!opts.code) { console.error(JSON.stringify({ status: "error", message: "Missing --code <file>" })); process.exit(1); }
|
|
60
|
+
const code = readFileSync(opts.code, "utf-8");
|
|
61
|
+
const body = { name, code };
|
|
62
|
+
if (opts.timeout || opts.memory) body.config = {};
|
|
63
|
+
if (opts.timeout) body.config.timeout = opts.timeout;
|
|
64
|
+
if (opts.memory) body.config.memory = opts.memory;
|
|
65
|
+
if (opts.deps) body.deps = opts.deps;
|
|
66
|
+
|
|
67
|
+
const fetchPaid = await setupPaidFetch();
|
|
68
|
+
const res = await fetchPaid(`${API}/admin/v1/projects/${projectId}/functions`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify(body),
|
|
72
|
+
});
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
75
|
+
console.log(JSON.stringify(data, null, 2));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function invoke(projectId, name, args) {
|
|
79
|
+
const p = findProject(projectId);
|
|
80
|
+
const opts = { method: "POST", body: undefined };
|
|
81
|
+
for (let i = 0; i < args.length; i++) {
|
|
82
|
+
if (args[i] === "--method" && args[i + 1]) opts.method = args[++i];
|
|
83
|
+
if (args[i] === "--body" && args[i + 1]) opts.body = args[++i];
|
|
84
|
+
}
|
|
85
|
+
const fetchOpts = {
|
|
86
|
+
method: opts.method,
|
|
87
|
+
headers: { "apikey": p.service_key },
|
|
88
|
+
};
|
|
89
|
+
if (opts.body && opts.method !== "GET" && opts.method !== "HEAD") {
|
|
90
|
+
fetchOpts.headers["Content-Type"] = "application/json";
|
|
91
|
+
fetchOpts.body = opts.body;
|
|
92
|
+
}
|
|
93
|
+
const res = await fetch(`${API}/functions/v1/${name}`, fetchOpts);
|
|
94
|
+
const text = await res.text();
|
|
95
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, body: text })); process.exit(1); }
|
|
96
|
+
try { console.log(JSON.stringify(JSON.parse(text), null, 2)); } catch { process.stdout.write(text + "\n"); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function logs(projectId, name, args) {
|
|
100
|
+
const p = findProject(projectId);
|
|
101
|
+
let tail = 50;
|
|
102
|
+
for (let i = 0; i < args.length; i++) {
|
|
103
|
+
if (args[i] === "--tail" && args[i + 1]) tail = parseInt(args[++i]);
|
|
104
|
+
}
|
|
105
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/functions/${encodeURIComponent(name)}/logs?tail=${tail}`, {
|
|
106
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
107
|
+
});
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
110
|
+
console.log(JSON.stringify(data, null, 2));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function list(projectId) {
|
|
114
|
+
const p = findProject(projectId);
|
|
115
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/functions`, {
|
|
116
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
117
|
+
});
|
|
118
|
+
const data = await res.json();
|
|
119
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
120
|
+
console.log(JSON.stringify(data, null, 2));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function deleteFunction(projectId, name) {
|
|
124
|
+
const p = findProject(projectId);
|
|
125
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/functions/${encodeURIComponent(name)}`, {
|
|
126
|
+
method: "DELETE",
|
|
127
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
128
|
+
});
|
|
129
|
+
if (res.status === 204 || res.ok) {
|
|
130
|
+
console.log(JSON.stringify({ status: "ok", message: `Function '${name}' deleted.` }));
|
|
131
|
+
} else {
|
|
132
|
+
const data = await res.json().catch(() => ({}));
|
|
133
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function run(sub, args) {
|
|
138
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
139
|
+
switch (sub) {
|
|
140
|
+
case "deploy": await deploy(args[0], args[1], args.slice(2)); break;
|
|
141
|
+
case "invoke": await invoke(args[0], args[1], args.slice(2)); break;
|
|
142
|
+
case "logs": await logs(args[0], args[1], args.slice(2)); break;
|
|
143
|
+
case "list": await list(args[0]); break;
|
|
144
|
+
case "delete": await deleteFunction(args[0], args[1]); break;
|
|
145
|
+
default:
|
|
146
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
147
|
+
console.log(HELP);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
package/lib/projects.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { findProject, loadProjects, saveProjects, readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
2
|
-
import { existsSync } from "fs";
|
|
1
|
+
import { findProject, loadProjects, saveProjects, readWallet, API, WALLET_FILE, PROJECTS_FILE } from "./config.mjs";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3
3
|
|
|
4
4
|
const HELP = `run402 projects — Manage your deployed Run402 projects
|
|
5
5
|
|
|
@@ -7,27 +7,35 @@ Usage:
|
|
|
7
7
|
run402 projects <subcommand> [args...]
|
|
8
8
|
|
|
9
9
|
Subcommands:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
quote Show pricing tiers
|
|
11
|
+
provision [--tier <tier>] [--name <n>] Provision a new Postgres project (pays via x402)
|
|
12
|
+
list List all your projects (IDs, tiers, URLs, expiry)
|
|
13
|
+
sql <id> "<query>" Run a SQL query against a project's Postgres DB
|
|
14
|
+
rest <id> <table> [params] Query a table via the REST API (PostgREST)
|
|
15
|
+
usage <id> Show compute/storage usage for a project
|
|
16
|
+
schema <id> Inspect the database schema
|
|
17
|
+
rls <id> <template> <tables_json> Apply Row-Level Security policies
|
|
18
|
+
renew <id> Extend the project lease (pays via x402)
|
|
19
|
+
delete <id> Delete a project and remove it from local state
|
|
17
20
|
|
|
18
21
|
Examples:
|
|
22
|
+
run402 projects quote
|
|
23
|
+
run402 projects provision --tier prototype
|
|
24
|
+
run402 projects provision --tier hobby --name my-app
|
|
19
25
|
run402 projects list
|
|
20
26
|
run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
|
|
21
27
|
run402 projects rest abc123 users "limit=10&select=id,name"
|
|
22
28
|
run402 projects usage abc123
|
|
23
29
|
run402 projects schema abc123
|
|
30
|
+
run402 projects rls abc123 public_read '[{"table":"posts"}]'
|
|
24
31
|
run402 projects renew abc123
|
|
25
32
|
run402 projects delete abc123
|
|
26
33
|
|
|
27
34
|
Notes:
|
|
28
35
|
- <id> is the project_id shown in 'run402 projects list'
|
|
29
36
|
- 'rest' uses PostgREST query syntax (table name + optional query string)
|
|
30
|
-
- 'renew'
|
|
37
|
+
- 'renew' and 'provision' require a funded wallet — payment is automatic via x402
|
|
38
|
+
- RLS templates: user_owns_rows, public_read, public_read_write
|
|
31
39
|
`;
|
|
32
40
|
|
|
33
41
|
async function setupPaidFetch() {
|
|
@@ -50,6 +58,56 @@ async function setupPaidFetch() {
|
|
|
50
58
|
return wrapFetchWithPayment(fetch, client);
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
async function quote() {
|
|
62
|
+
const res = await fetch(`${API}/v1/projects`);
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
65
|
+
console.log(JSON.stringify(data, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function provision(args) {
|
|
69
|
+
const opts = { tier: "prototype", name: undefined };
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
|
|
72
|
+
if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
|
|
73
|
+
}
|
|
74
|
+
const fetchPaid = await setupPaidFetch();
|
|
75
|
+
const body = { tier: opts.tier };
|
|
76
|
+
if (opts.name) body.name = opts.name;
|
|
77
|
+
const res = await fetchPaid(`${API}/v1/projects`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify(body),
|
|
81
|
+
});
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
84
|
+
// Save project credentials locally
|
|
85
|
+
if (data.project_id) {
|
|
86
|
+
const projects = loadProjects();
|
|
87
|
+
projects.push({
|
|
88
|
+
project_id: data.project_id, anon_key: data.anon_key, service_key: data.service_key,
|
|
89
|
+
tier: data.tier, lease_expires_at: data.lease_expires_at, deployed_at: new Date().toISOString(),
|
|
90
|
+
});
|
|
91
|
+
const dir = PROJECTS_FILE.replace(/\/[^/]+$/, "");
|
|
92
|
+
mkdirSync(dir, { recursive: true });
|
|
93
|
+
writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2), { mode: 0o600 });
|
|
94
|
+
}
|
|
95
|
+
console.log(JSON.stringify(data, null, 2));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function rls(projectId, template, tablesJson) {
|
|
99
|
+
const p = findProject(projectId);
|
|
100
|
+
const tables = JSON.parse(tablesJson);
|
|
101
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/rls`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify({ template, tables }),
|
|
105
|
+
});
|
|
106
|
+
const data = await res.json();
|
|
107
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
108
|
+
console.log(JSON.stringify(data, null, 2));
|
|
109
|
+
}
|
|
110
|
+
|
|
53
111
|
async function list() {
|
|
54
112
|
const projects = loadProjects();
|
|
55
113
|
if (projects.length === 0) { console.log(JSON.stringify({ status: "ok", projects: [], message: "No projects yet." })); return; }
|
|
@@ -113,13 +171,16 @@ export async function run(sub, args) {
|
|
|
113
171
|
process.exit(0);
|
|
114
172
|
}
|
|
115
173
|
switch (sub) {
|
|
116
|
-
case "
|
|
117
|
-
case "
|
|
118
|
-
case "
|
|
119
|
-
case "
|
|
120
|
-
case "
|
|
121
|
-
case "
|
|
122
|
-
case "
|
|
174
|
+
case "quote": await quote(); break;
|
|
175
|
+
case "provision": await provision(args); break;
|
|
176
|
+
case "list": await list(); break;
|
|
177
|
+
case "sql": await sqlCmd(args[0], args[1]); break;
|
|
178
|
+
case "rest": await rest(args[0], args[1], args[2]); break;
|
|
179
|
+
case "usage": await usage(args[0]); break;
|
|
180
|
+
case "schema": await schema(args[0]); break;
|
|
181
|
+
case "rls": await rls(args[0], args[1], args[2]); break;
|
|
182
|
+
case "renew": await renew(args[0]); break;
|
|
183
|
+
case "delete": await deleteProject(args[0]); break;
|
|
123
184
|
default:
|
|
124
185
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
125
186
|
console.log(HELP);
|
package/lib/secrets.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { findProject, API } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
const HELP = `run402 secrets — Manage project secrets
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
run402 secrets <subcommand> [args...]
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
set <id> <key> <value> Set a secret on a project
|
|
10
|
+
list <id> List all secrets for a project
|
|
11
|
+
delete <id> <key> Delete a secret from a project
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
run402 secrets set abc123 STRIPE_KEY sk-1234
|
|
15
|
+
run402 secrets list abc123
|
|
16
|
+
run402 secrets delete abc123 STRIPE_KEY
|
|
17
|
+
|
|
18
|
+
Notes:
|
|
19
|
+
- Secrets are injected as process.env in serverless functions
|
|
20
|
+
- Values are never shown after being set
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
async function set(projectId, key, value) {
|
|
24
|
+
const p = findProject(projectId);
|
|
25
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/secrets`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify({ key, value }),
|
|
29
|
+
});
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
32
|
+
console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' set for project ${projectId}.` }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function list(projectId) {
|
|
36
|
+
const p = findProject(projectId);
|
|
37
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/secrets`, {
|
|
38
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
39
|
+
});
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
42
|
+
console.log(JSON.stringify(data, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function deleteSecret(projectId, key) {
|
|
46
|
+
const p = findProject(projectId);
|
|
47
|
+
const res = await fetch(`${API}/admin/v1/projects/${projectId}/secrets/${encodeURIComponent(key)}`, {
|
|
48
|
+
method: "DELETE",
|
|
49
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
50
|
+
});
|
|
51
|
+
if (res.status === 204 || res.ok) {
|
|
52
|
+
console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' deleted.` }));
|
|
53
|
+
} else {
|
|
54
|
+
const data = await res.json().catch(() => ({}));
|
|
55
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function run(sub, args) {
|
|
60
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
61
|
+
switch (sub) {
|
|
62
|
+
case "set": await set(args[0], args[1], args[2]); break;
|
|
63
|
+
case "list": await list(args[0]); break;
|
|
64
|
+
case "delete": await deleteSecret(args[0], args[1]); break;
|
|
65
|
+
default:
|
|
66
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
67
|
+
console.log(HELP);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/lib/sites.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 sites — Deploy static sites
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 sites deploy --name <name> --manifest <file> [--project <id>] [--target <target>]
|
|
8
|
+
cat manifest.json | run402 sites deploy --name <name>
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--name <name> Site name (e.g. 'portfolio', 'family-todo')
|
|
12
|
+
--manifest <file> Path to manifest JSON file (or read from stdin)
|
|
13
|
+
--project <id> Optional project ID to link this deployment to
|
|
14
|
+
--target <target> Deployment target (e.g. 'production')
|
|
15
|
+
--help, -h Show this help message
|
|
16
|
+
|
|
17
|
+
Manifest format (JSON):
|
|
18
|
+
{
|
|
19
|
+
"files": [
|
|
20
|
+
{ "file": "index.html", "data": "<html>...</html>" },
|
|
21
|
+
{ "file": "style.css", "data": "body { margin: 0; }" }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
run402 sites deploy --name my-site --manifest site.json
|
|
27
|
+
cat site.json | run402 sites deploy --name my-site
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
- Must include at least index.html in the files array
|
|
31
|
+
- Requires a funded wallet — payment ($0.05 USDC) is automatic via x402
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
async function readStdin() {
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
37
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function deploy(args) {
|
|
41
|
+
const opts = { name: null, manifest: null, project: undefined, target: undefined };
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
|
|
44
|
+
if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
|
|
45
|
+
if (args[i] === "--manifest" && args[i + 1]) opts.manifest = args[++i];
|
|
46
|
+
if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
|
|
47
|
+
if (args[i] === "--target" && args[i + 1]) opts.target = args[++i];
|
|
48
|
+
}
|
|
49
|
+
if (!opts.name) { console.error(JSON.stringify({ status: "error", message: "Missing --name <name>" })); process.exit(1); }
|
|
50
|
+
if (!existsSync(WALLET_FILE)) {
|
|
51
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const manifest = opts.manifest ? JSON.parse(readFileSync(opts.manifest, "utf-8")) : JSON.parse(await readStdin());
|
|
56
|
+
const body = { name: opts.name, files: manifest.files };
|
|
57
|
+
if (opts.project) body.project = opts.project;
|
|
58
|
+
if (opts.target) body.target = opts.target;
|
|
59
|
+
|
|
60
|
+
const wallet = readWallet();
|
|
61
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
62
|
+
const { createPublicClient, http } = await import("viem");
|
|
63
|
+
const { baseSepolia } = await import("viem/chains");
|
|
64
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
65
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
66
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
67
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
68
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
69
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
70
|
+
const client = new x402Client();
|
|
71
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
72
|
+
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
73
|
+
|
|
74
|
+
const res = await fetchPaid(`${API}/v1/deployments`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
});
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
81
|
+
console.log(JSON.stringify(data, null, 2));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function run(sub, args) {
|
|
85
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
86
|
+
if (sub !== "deploy") {
|
|
87
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
88
|
+
console.log(HELP);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
await deploy(args);
|
|
92
|
+
}
|
package/lib/storage.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { findProject, API } from "./config.mjs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 storage — Manage project file storage
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 storage <subcommand> [args...]
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
upload <id> <bucket> <path> [--file <local>] [--content-type <mime>]
|
|
11
|
+
Upload a file to storage
|
|
12
|
+
download <id> <bucket> <path> Download a file from storage
|
|
13
|
+
delete <id> <bucket> <path> Delete a file from storage
|
|
14
|
+
list <id> <bucket> List files in a bucket
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
run402 storage upload abc123 assets logo.png --file ./logo.png --content-type image/png
|
|
18
|
+
echo "hello" | run402 storage upload abc123 data notes.txt
|
|
19
|
+
run402 storage download abc123 assets logo.png
|
|
20
|
+
run402 storage list abc123 assets
|
|
21
|
+
run402 storage delete abc123 assets logo.png
|
|
22
|
+
|
|
23
|
+
Notes:
|
|
24
|
+
- <id> is the project_id from 'run402 projects list'
|
|
25
|
+
- Upload reads from --file or stdin if no --file is given
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
async function readStdin() {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
31
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function upload(projectId, bucket, path, args) {
|
|
35
|
+
const p = findProject(projectId);
|
|
36
|
+
const opts = { file: null, contentType: "text/plain" };
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
if (args[i] === "--file" && args[i + 1]) opts.file = args[++i];
|
|
39
|
+
if (args[i] === "--content-type" && args[i + 1]) opts.contentType = args[++i];
|
|
40
|
+
}
|
|
41
|
+
const content = opts.file ? readFileSync(opts.file, "utf-8") : await readStdin();
|
|
42
|
+
const res = await fetch(`${API}/storage/v1/object/${bucket}/${path}`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": opts.contentType, "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
|
|
45
|
+
body: content,
|
|
46
|
+
});
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
49
|
+
console.log(JSON.stringify(data, null, 2));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function download(projectId, bucket, path) {
|
|
53
|
+
const p = findProject(projectId);
|
|
54
|
+
const res = await fetch(`${API}/storage/v1/object/${bucket}/${path}`, {
|
|
55
|
+
headers: { "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
const data = await res.json().catch(() => ({}));
|
|
59
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
process.stdout.write(text);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function deleteFile(projectId, bucket, path) {
|
|
66
|
+
const p = findProject(projectId);
|
|
67
|
+
const res = await fetch(`${API}/storage/v1/object/${bucket}/${path}`, {
|
|
68
|
+
method: "DELETE",
|
|
69
|
+
headers: { "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
|
|
70
|
+
});
|
|
71
|
+
if (res.status === 204 || res.ok) {
|
|
72
|
+
console.log(JSON.stringify({ status: "ok", message: `File '${bucket}/${path}' deleted.` }));
|
|
73
|
+
} else {
|
|
74
|
+
const data = await res.json().catch(() => ({}));
|
|
75
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function list(projectId, bucket) {
|
|
80
|
+
const p = findProject(projectId);
|
|
81
|
+
const res = await fetch(`${API}/storage/v1/object/list/${bucket}`, {
|
|
82
|
+
headers: { "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
|
|
83
|
+
});
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
86
|
+
console.log(JSON.stringify(data, null, 2));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function run(sub, args) {
|
|
90
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
91
|
+
switch (sub) {
|
|
92
|
+
case "upload": await upload(args[0], args[1], args[2], args.slice(3)); break;
|
|
93
|
+
case "download": await download(args[0], args[1], args[2]); break;
|
|
94
|
+
case "delete": await deleteFile(args[0], args[1], args[2]); break;
|
|
95
|
+
case "list": await list(args[0], args[1]); break;
|
|
96
|
+
default:
|
|
97
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
98
|
+
console.log(HELP);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { findProject, API } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
const HELP = `run402 subdomains — Manage custom subdomains
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
run402 subdomains <subcommand> [args...]
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
claim <deployment_id> <name> [--project <id>] Claim a subdomain for a deployment
|
|
10
|
+
delete <name> [--project <id>] Release a subdomain
|
|
11
|
+
list <id> List subdomains for a project
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
run402 subdomains claim dpl_abc123 myapp
|
|
15
|
+
run402 subdomains claim dpl_abc123 myapp --project proj123
|
|
16
|
+
run402 subdomains delete myapp
|
|
17
|
+
run402 subdomains list proj123
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- Subdomain names: 3-63 chars, lowercase alphanumeric + hyphens
|
|
21
|
+
- Creates <name>.run402.com pointing to the deployment
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
async function claim(deploymentId, name, args) {
|
|
25
|
+
const opts = { project: null };
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
|
|
28
|
+
}
|
|
29
|
+
const headers = { "Content-Type": "application/json" };
|
|
30
|
+
if (opts.project) {
|
|
31
|
+
const p = findProject(opts.project);
|
|
32
|
+
headers["Authorization"] = `Bearer ${p.service_key}`;
|
|
33
|
+
}
|
|
34
|
+
const res = await fetch(`${API}/v1/subdomains`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers,
|
|
37
|
+
body: JSON.stringify({ name, deployment_id: deploymentId }),
|
|
38
|
+
});
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
41
|
+
console.log(JSON.stringify(data, null, 2));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function deleteSubdomain(name, args) {
|
|
45
|
+
const opts = { project: null };
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
|
|
48
|
+
}
|
|
49
|
+
const headers = {};
|
|
50
|
+
if (opts.project) {
|
|
51
|
+
const p = findProject(opts.project);
|
|
52
|
+
headers["Authorization"] = `Bearer ${p.service_key}`;
|
|
53
|
+
}
|
|
54
|
+
const res = await fetch(`${API}/v1/subdomains/${encodeURIComponent(name)}`, {
|
|
55
|
+
method: "DELETE",
|
|
56
|
+
headers,
|
|
57
|
+
});
|
|
58
|
+
if (res.status === 204 || res.ok) {
|
|
59
|
+
console.log(JSON.stringify({ status: "ok", message: `Subdomain '${name}' released.` }));
|
|
60
|
+
} else {
|
|
61
|
+
const data = await res.json().catch(() => ({}));
|
|
62
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function list(projectId) {
|
|
67
|
+
const p = findProject(projectId);
|
|
68
|
+
const res = await fetch(`${API}/v1/subdomains`, {
|
|
69
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
70
|
+
});
|
|
71
|
+
const data = await res.json();
|
|
72
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
73
|
+
console.log(JSON.stringify(data, null, 2));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function run(sub, args) {
|
|
77
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
78
|
+
switch (sub) {
|
|
79
|
+
case "claim": await claim(args[0], args[1], args.slice(2)); break;
|
|
80
|
+
case "delete": await deleteSubdomain(args[0], args.slice(1)); break;
|
|
81
|
+
case "list": await list(args[0]); break;
|
|
82
|
+
default:
|
|
83
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
84
|
+
console.log(HELP);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/lib/wallet.mjs
CHANGED
|
@@ -9,6 +9,7 @@ Subcommands:
|
|
|
9
9
|
status Show wallet address, network, and funding status
|
|
10
10
|
create Generate a new wallet and save it locally
|
|
11
11
|
fund Request test USDC from the Run402 faucet (Base Sepolia)
|
|
12
|
+
balance Check billing balance for this wallet
|
|
12
13
|
export Print the wallet address (useful for scripting)
|
|
13
14
|
|
|
14
15
|
Notes:
|
|
@@ -65,6 +66,15 @@ async function fund() {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
async function balance() {
|
|
70
|
+
const w = readWallet();
|
|
71
|
+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
|
|
72
|
+
const res = await fetch(`${API}/v1/billing/accounts/${w.address.toLowerCase()}`);
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
75
|
+
console.log(JSON.stringify(data, null, 2));
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
async function exportAddr() {
|
|
69
79
|
const w = readWallet();
|
|
70
80
|
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet." })); process.exit(1); }
|
|
@@ -77,10 +87,11 @@ export async function run(sub, args) {
|
|
|
77
87
|
process.exit(0);
|
|
78
88
|
}
|
|
79
89
|
switch (sub) {
|
|
80
|
-
case "status":
|
|
81
|
-
case "create":
|
|
82
|
-
case "fund":
|
|
83
|
-
case "
|
|
90
|
+
case "status": await status(); break;
|
|
91
|
+
case "create": await create(); break;
|
|
92
|
+
case "fund": await fund(); break;
|
|
93
|
+
case "balance": await balance(); break;
|
|
94
|
+
case "export": await exportAddr(); break;
|
|
84
95
|
default:
|
|
85
96
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
86
97
|
console.log(HELP);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run402",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|
|
7
|
-
"run402": "cli.mjs"
|
|
7
|
+
"run402": "./cli.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
@@ -22,14 +22,8 @@
|
|
|
22
22
|
"homepage": "https://run402.com",
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "
|
|
25
|
+
"url": "https://github.com/kychee-com/run402.git",
|
|
26
26
|
"directory": "cli"
|
|
27
27
|
},
|
|
28
|
-
"keywords": [
|
|
29
|
-
"run402",
|
|
30
|
-
"x402",
|
|
31
|
-
"postgres",
|
|
32
|
-
"ai-agent",
|
|
33
|
-
"openclaw"
|
|
34
|
-
]
|
|
28
|
+
"keywords": ["run402", "x402", "postgres", "ai-agent", "openclaw"]
|
|
35
29
|
}
|