run402 1.10.1 → 1.11.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
@@ -4,9 +4,15 @@
4
4
  * https://run402.com
5
5
  */
6
6
 
7
+ import { readFileSync } from "node:fs";
8
+
7
9
  const [,, cmd, sub, ...rest] = process.argv;
8
10
 
9
- const HELP = `run402 v1.0.0 — Full-stack backend infra for AI agents
11
+ const { version } = JSON.parse(
12
+ readFileSync(new URL("./package.json", import.meta.url), "utf8")
13
+ );
14
+
15
+ const HELP = `run402 v${version} — Full-stack backend infra for AI agents
10
16
  https://run402.com
11
17
 
12
18
  Usage:
@@ -46,9 +52,14 @@ Getting started:
46
52
  run402 deploy --manifest app.json
47
53
  `;
48
54
 
55
+ if (cmd === '--version' || cmd === '-v') {
56
+ console.log(version);
57
+ process.exit(0);
58
+ }
59
+
49
60
  if (!cmd || cmd === '--help' || cmd === '-h') {
50
61
  console.log(HELP);
51
- process.exit(cmd ? 0 : 0);
62
+ process.exit(0);
52
63
  }
53
64
 
54
65
  switch (cmd) {
@@ -21,8 +21,6 @@ export function loadKeyStore(path) {
21
21
  projects[item.project_id] = {
22
22
  anon_key: item.anon_key,
23
23
  service_key: item.service_key,
24
- tier: item.tier,
25
- lease_expires_at: item.lease_expires_at || item.expires_at || "",
26
24
  ...(item.site_url && { site_url: item.site_url }),
27
25
  ...(item.deployed_at && { deployed_at: item.deployed_at }),
28
26
  };
@@ -31,15 +29,17 @@ export function loadKeyStore(path) {
31
29
  return { projects };
32
30
  }
33
31
  if (parsed && typeof parsed === "object" && parsed.projects) {
34
- // Auto-normalize expires_at lease_expires_at
32
+ // Strip legacy fields (tier, lease_expires_at, expires_at) from projects
35
33
  for (const proj of Object.values(parsed.projects)) {
36
34
  const rec = proj;
37
- if (rec.expires_at && !rec.lease_expires_at) {
38
- rec.lease_expires_at = rec.expires_at;
39
- delete rec.expires_at;
40
- }
35
+ delete rec.tier;
36
+ delete rec.lease_expires_at;
37
+ delete rec.expires_at;
41
38
  }
42
- return parsed;
39
+ return {
40
+ ...(parsed.active_project_id && { active_project_id: parsed.active_project_id }),
41
+ projects: parsed.projects,
42
+ };
43
43
  }
44
44
  return { projects: {} };
45
45
  }
@@ -66,10 +66,32 @@ export function saveProject(projectId, project, path) {
66
66
  store.projects[projectId] = project;
67
67
  saveKeyStore(store, p);
68
68
  }
69
+ export function updateProject(projectId, update, path) {
70
+ const p = path ?? getKeystorePath();
71
+ const store = loadKeyStore(p);
72
+ const existing = store.projects[projectId];
73
+ if (existing) {
74
+ store.projects[projectId] = { ...existing, ...update };
75
+ saveKeyStore(store, p);
76
+ }
77
+ }
69
78
  export function removeProject(projectId, path) {
70
79
  const p = path ?? getKeystorePath();
71
80
  const store = loadKeyStore(p);
72
81
  delete store.projects[projectId];
82
+ if (store.active_project_id === projectId) {
83
+ delete store.active_project_id;
84
+ }
85
+ saveKeyStore(store, p);
86
+ }
87
+ export function getActiveProjectId(path) {
88
+ const store = loadKeyStore(path);
89
+ return store.active_project_id;
90
+ }
91
+ export function setActiveProjectId(projectId, path) {
92
+ const p = path ?? getKeystorePath();
93
+ const store = loadKeyStore(p);
94
+ store.active_project_id = projectId;
73
95
  saveKeyStore(store, p);
74
96
  }
75
97
  //# sourceMappingURL=keystore.js.map
package/lib/config.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { getApiBase, getConfigDir, getKeystorePath, getAllowancePath } from "../core-dist/config.js";
7
7
  import { readAllowance as coreReadAllowance, saveAllowance as coreSaveAllowance } from "../core-dist/allowance.js";
8
- import { loadKeyStore, getProject, saveProject, removeProject, saveKeyStore } from "../core-dist/keystore.js";
8
+ import { loadKeyStore, getProject, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId } from "../core-dist/keystore.js";
9
9
 
10
10
  export const CONFIG_DIR = getConfigDir();
11
11
  export const ALLOWANCE_FILE = getAllowancePath();
@@ -36,5 +36,17 @@ export function findProject(id) {
36
36
  return p;
37
37
  }
38
38
 
39
+ export function resolveProject(id) {
40
+ const projectId = id || getActiveProjectId();
41
+ if (!projectId) { console.error("Error: no project specified and no active project set. Run: run402 projects provision"); process.exit(1); }
42
+ return findProject(projectId);
43
+ }
44
+
45
+ export function resolveProjectId(id) {
46
+ const projectId = id || getActiveProjectId();
47
+ if (!projectId) { console.error("Error: no project specified and no active project set. Run: run402 projects provision"); process.exit(1); }
48
+ return projectId;
49
+ }
50
+
39
51
  // Re-export core keystore functions for direct use
40
- export { loadKeyStore, saveProject, removeProject, saveKeyStore };
52
+ export { loadKeyStore, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId };
package/lib/deploy.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "fs";
2
- import { API, allowanceAuthHeaders, saveProject } from "./config.mjs";
2
+ import { API, allowanceAuthHeaders, saveProject, setActiveProjectId } from "./config.mjs";
3
3
 
4
4
  const HELP = `run402 deploy — Deploy a full-stack app or static site on Run402
5
5
 
@@ -55,10 +55,10 @@ export async function run(args) {
55
55
  if (result.project_id) {
56
56
  saveProject(result.project_id, {
57
57
  anon_key: result.anon_key, service_key: result.service_key,
58
- tier: result.tier, lease_expires_at: result.lease_expires_at,
59
58
  site_url: result.site_url || result.subdomain_url,
60
59
  deployed_at: new Date().toISOString(),
61
60
  });
61
+ setActiveProjectId(result.project_id);
62
62
  }
63
63
  console.log(JSON.stringify(result, null, 2));
64
64
  }
package/lib/init.mjs CHANGED
@@ -84,13 +84,6 @@ export async function run() {
84
84
  if (res.ok) tierInfo = await res.json();
85
85
  } catch {}
86
86
 
87
- // Fall back to keystore if the API call failed or returned no tier
88
- if (!tierInfo || !tierInfo.tier) {
89
- const projects = Object.values(store.projects);
90
- const active = projects.find(p => p.tier && p.lease_expires_at && new Date(p.lease_expires_at) > new Date());
91
- if (active) tierInfo = { tier: active.tier, status: "active", lease_expires_at: active.lease_expires_at };
92
- }
93
-
94
87
  if (tierInfo && tierInfo.tier && tierInfo.status === "active") {
95
88
  const expiry = tierInfo.lease_expires_at ? tierInfo.lease_expires_at.split("T")[0] : "unknown";
96
89
  line("Tier", `${tierInfo.tier} (expires ${expiry})`);
package/lib/projects.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { findProject, loadKeyStore, saveProject, removeProject, API, allowanceAuthHeaders } from "./config.mjs";
1
+ import { findProject, loadKeyStore, saveProject, removeProject, API, allowanceAuthHeaders, setActiveProjectId, getActiveProjectId } from "./config.mjs";
2
2
 
3
3
  const HELP = `run402 projects — Manage your deployed Run402 projects
4
4
 
@@ -8,8 +8,9 @@ Usage:
8
8
  Subcommands:
9
9
  quote Show pricing tiers
10
10
  provision [--tier <tier>] [--name <n>] Provision a new Postgres project (pays via x402)
11
- list List all your projects (IDs, tiers, URLs, expiry)
12
- info <id> Show project details: REST URL, keys, expiry
11
+ use <id> Set the active project (used as default for other commands)
12
+ list List all your projects (IDs, URLs, active marker)
13
+ info <id> Show project details: REST URL, keys
13
14
  sql <id> "<query>" Run a SQL query against a project's Postgres DB
14
15
  rest <id> <table> [params] Query a table via the REST API (PostgREST)
15
16
  usage <id> Show compute/storage usage for a project
@@ -21,6 +22,7 @@ Examples:
21
22
  run402 projects quote
22
23
  run402 projects provision --tier prototype
23
24
  run402 projects provision --tier hobby --name my-app
25
+ run402 projects use prj_abc123
24
26
  run402 projects list
25
27
  run402 projects info abc123
26
28
  run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
@@ -32,6 +34,7 @@ Examples:
32
34
 
33
35
  Notes:
34
36
  - <id> is the project_id shown in 'run402 projects list'
37
+ - Most commands that take <id> default to the active project if omitted
35
38
  - 'rest' uses PostgREST query syntax (table name + optional query string)
36
39
  - 'provision' requires a funded allowance — payment is automatic via x402
37
40
  - RLS templates: user_owns_rows, public_read, public_read_write
@@ -60,13 +63,13 @@ async function provision(args) {
60
63
  });
61
64
  const data = await res.json();
62
65
  if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
63
- // Save project credentials locally
66
+ // Save project credentials locally and set as active
64
67
  if (data.project_id) {
65
68
  saveProject(data.project_id, {
66
69
  anon_key: data.anon_key, service_key: data.service_key,
67
- tier: data.tier, lease_expires_at: data.lease_expires_at,
68
70
  deployed_at: new Date().toISOString(),
69
71
  });
72
+ setActiveProjectId(data.project_id);
70
73
  }
71
74
  console.log(JSON.stringify(data, null, 2));
72
75
  }
@@ -88,17 +91,14 @@ async function list() {
88
91
  const store = loadKeyStore();
89
92
  const entries = Object.entries(store.projects);
90
93
  if (entries.length === 0) { console.log(JSON.stringify({ status: "ok", projects: [], message: "No projects yet." })); return; }
91
- console.log(JSON.stringify(entries.map(([id, p]) => ({ project_id: id, tier: p.tier, site_url: p.site_url, lease_expires_at: p.lease_expires_at, deployed_at: p.deployed_at })), null, 2));
94
+ const activeId = store.active_project_id;
95
+ console.log(JSON.stringify(entries.map(([id, p]) => ({ project_id: id, active: id === activeId, site_url: p.site_url, deployed_at: p.deployed_at })), null, 2));
92
96
  }
93
97
 
94
98
  async function info(projectId) {
95
99
  const p = findProject(projectId);
96
- const active = p.lease_expires_at ? new Date(p.lease_expires_at) > new Date() : null;
97
100
  console.log(JSON.stringify({
98
101
  project_id: projectId,
99
- tier: p.tier,
100
- active,
101
- lease_expires_at: p.lease_expires_at,
102
102
  rest_url: `${API}/rest/v1`,
103
103
  anon_key: p.anon_key,
104
104
  service_key: p.service_key,
@@ -135,6 +135,13 @@ async function schema(projectId) {
135
135
  console.log(JSON.stringify(data, null, 2));
136
136
  }
137
137
 
138
+ async function use(projectId) {
139
+ if (!projectId) { console.error("Usage: run402 projects use <project_id>"); process.exit(1); }
140
+ findProject(projectId); // verify it exists
141
+ setActiveProjectId(projectId);
142
+ console.log(JSON.stringify({ status: "ok", active_project_id: projectId }));
143
+ }
144
+
138
145
  async function deleteProject(projectId) {
139
146
  const p = findProject(projectId);
140
147
  const res = await fetch(`${API}/projects/v1/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
@@ -155,6 +162,7 @@ export async function run(sub, args) {
155
162
  switch (sub) {
156
163
  case "quote": await quote(); break;
157
164
  case "provision": await provision(args); break;
165
+ case "use": await use(args[0]); break;
158
166
  case "list": await list(); break;
159
167
  case "info": await info(args[0]); break;
160
168
  case "sql": await sqlCmd(args[0], args[1]); break;
package/lib/sites.mjs CHANGED
@@ -1,21 +1,20 @@
1
1
  import { readFileSync } from "fs";
2
- import { API, allowanceAuthHeaders } from "./config.mjs";
2
+ import { API, allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
3
3
 
4
4
  const HELP = `run402 sites — Deploy and manage static sites
5
5
 
6
6
  Usage:
7
- run402 sites deploy --name <name> --manifest <file> [--project <id>] [--target <target>]
7
+ run402 sites deploy --manifest <file> [--project <id>] [--target <target>]
8
8
  run402 sites status <deployment_id>
9
- cat manifest.json | run402 sites deploy --name <name>
9
+ cat manifest.json | run402 sites deploy
10
10
 
11
11
  Subcommands:
12
12
  deploy Deploy a static site
13
13
  status Check the status of a deployment
14
14
 
15
15
  Options (deploy):
16
- --name <name> Site name (e.g. 'portfolio', 'family-todo')
17
16
  --manifest <file> Path to manifest JSON file (or read from stdin)
18
- --project <id> Optional project ID to link this deployment to
17
+ --project <id> Project ID (defaults to active project)
19
18
  --target <target> Deployment target (e.g. 'production')
20
19
  --help, -h Show this help message
21
20
 
@@ -28,9 +27,9 @@ Manifest format (JSON):
28
27
  }
29
28
 
30
29
  Examples:
31
- run402 sites deploy --name my-site --manifest site.json
32
- run402 sites status dep_abc123
33
- cat site.json | run402 sites deploy --name my-site
30
+ run402 sites deploy --manifest site.json
31
+ run402 sites status dpl_abc123
32
+ cat site.json | run402 sites deploy
34
33
 
35
34
  Notes:
36
35
  - Must include at least index.html in the files array
@@ -44,18 +43,16 @@ async function readStdin() {
44
43
  }
45
44
 
46
45
  async function deploy(args) {
47
- const opts = { name: null, manifest: null, project: undefined, target: undefined };
46
+ const opts = { manifest: null, project: undefined, target: undefined };
48
47
  for (let i = 0; i < args.length; i++) {
49
48
  if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
50
- if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
51
49
  if (args[i] === "--manifest" && args[i + 1]) opts.manifest = args[++i];
52
50
  if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
53
51
  if (args[i] === "--target" && args[i + 1]) opts.target = args[++i];
54
52
  }
55
- if (!opts.name) { console.error(JSON.stringify({ status: "error", message: "Missing --name <name>" })); process.exit(1); }
53
+ const projectId = resolveProjectId(opts.project);
56
54
  const manifest = opts.manifest ? JSON.parse(readFileSync(opts.manifest, "utf-8")) : JSON.parse(await readStdin());
57
- const body = { name: opts.name, files: manifest.files };
58
- if (opts.project) body.project = opts.project;
55
+ const body = { files: manifest.files, project: projectId };
59
56
  if (opts.target) body.target = opts.target;
60
57
 
61
58
  const authHeaders = await allowanceAuthHeaders();
@@ -66,6 +63,9 @@ async function deploy(args) {
66
63
  });
67
64
  const data = await res.json();
68
65
  if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
66
+ if (data.deployment_id) {
67
+ updateProject(projectId, { last_deployment_id: data.deployment_id });
68
+ }
69
69
  console.log(JSON.stringify(data, null, 2));
70
70
  }
71
71
 
@@ -1,4 +1,4 @@
1
- import { findProject, API } from "./config.mjs";
1
+ import { resolveProject, resolveProjectId, API } from "./config.mjs";
2
2
 
3
3
  const HELP = `run402 subdomains — Manage custom subdomains
4
4
 
@@ -6,31 +6,44 @@ Usage:
6
6
  run402 subdomains <subcommand> [args...]
7
7
 
8
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
9
+ claim <name> [--project <id>] [--deployment <id>] Claim a subdomain
10
+ delete <name> [--project <id>] Release a subdomain
11
+ list [<id>] List subdomains for a project
12
+
13
+ Options default to the active project and its last deployment when omitted.
14
+ Legacy syntax 'claim <deployment_id> <name>' is still supported.
12
15
 
13
16
  Examples:
14
- run402 subdomains claim dpl_abc123 myapp
15
- run402 subdomains claim dpl_abc123 myapp --project proj123
17
+ run402 subdomains claim myapp
18
+ run402 subdomains claim myapp --deployment dpl_abc123 --project proj123
16
19
  run402 subdomains delete myapp
17
- run402 subdomains list proj123
20
+ run402 subdomains list
18
21
 
19
22
  Notes:
20
23
  - Subdomain names: 3-63 chars, lowercase alphanumeric + hyphens
21
24
  - Creates <name>.run402.com pointing to the deployment
22
25
  `;
23
26
 
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];
27
+ async function claim(positionalArgs, flagArgs) {
28
+ const opts = { project: null, deployment: null };
29
+ for (let i = 0; i < flagArgs.length; i++) {
30
+ if (flagArgs[i] === "--project" && flagArgs[i + 1]) opts.project = flagArgs[++i];
31
+ if (flagArgs[i] === "--deployment" && flagArgs[i + 1]) opts.deployment = flagArgs[++i];
28
32
  }
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
+ // positional: [name] or [deployment_id, name]
34
+ let name, deploymentId;
35
+ if (positionalArgs.length >= 2) {
36
+ deploymentId = positionalArgs[0];
37
+ name = positionalArgs[1];
38
+ } else if (positionalArgs.length === 1) {
39
+ name = positionalArgs[0];
33
40
  }
41
+ if (!name) { console.error("Usage: run402 subdomains claim <name> [--project <id>] [--deployment <id>]"); process.exit(1); }
42
+ const projectId = resolveProjectId(opts.project);
43
+ const p = resolveProject(opts.project);
44
+ deploymentId = opts.deployment || deploymentId || p.last_deployment_id;
45
+ if (!deploymentId) { console.error("Error: no deployment_id specified and no recent deployment found. Deploy a site first or pass --deployment <id>."); process.exit(1); }
46
+ const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${p.service_key}` };
34
47
  const res = await fetch(`${API}/subdomains/v1`, {
35
48
  method: "POST",
36
49
  headers,
@@ -46,11 +59,8 @@ async function deleteSubdomain(name, args) {
46
59
  for (let i = 0; i < args.length; i++) {
47
60
  if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
48
61
  }
49
- const headers = {};
50
- if (opts.project) {
51
- const p = findProject(opts.project);
52
- headers["Authorization"] = `Bearer ${p.service_key}`;
53
- }
62
+ const p = resolveProject(opts.project);
63
+ const headers = { "Authorization": `Bearer ${p.service_key}` };
54
64
  const res = await fetch(`${API}/subdomains/v1/${encodeURIComponent(name)}`, {
55
65
  method: "DELETE",
56
66
  headers,
@@ -64,7 +74,7 @@ async function deleteSubdomain(name, args) {
64
74
  }
65
75
 
66
76
  async function list(projectId) {
67
- const p = findProject(projectId);
77
+ const p = resolveProject(projectId);
68
78
  const res = await fetch(`${API}/subdomains/v1`, {
69
79
  headers: { "Authorization": `Bearer ${p.service_key}` },
70
80
  });
@@ -76,7 +86,17 @@ async function list(projectId) {
76
86
  export async function run(sub, args) {
77
87
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
78
88
  switch (sub) {
79
- case "claim": await claim(args[0], args[1], args.slice(2)); break;
89
+ case "claim": {
90
+ const positional = [];
91
+ const flags = [];
92
+ let i = 0;
93
+ while (i < args.length) {
94
+ if (args[i].startsWith("--")) { flags.push(args[i], args[i + 1]); i += 2; }
95
+ else { positional.push(args[i]); i++; }
96
+ }
97
+ await claim(positional, flags);
98
+ break;
99
+ }
80
100
  case "delete": await deleteSubdomain(args[0], args.slice(1)); break;
81
101
  case "list": await list(args[0]); break;
82
102
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.10.1",
3
+ "version": "1.11.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": {