openzoo 0.51.2 → 0.51.3

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/bin/openzoo.js CHANGED
@@ -170,18 +170,21 @@ usage:
170
170
  npx openzoo balance wallet balance on every rail — Solana (USDC/TOKEN/SOL),
171
171
  Base (USDC/ETH), Robinhood Chain (USDG/memecoins/ETH)
172
172
  npx openzoo build [dir] transmute a Vercel-shaped app (Next.js pages/api,
173
- app/**/route, Vite + api/*) into a Pinocchio Rust
174
- program + asset plan (--out .zoo-out --arch v0|v3)
175
- npx openzoo deploy [dir] deploy the program and the static frontend to
176
- Solana accounts (--cluster mainnet|devnet|localnet,
177
- --yes to accept the rent estimate; burner wallet)
178
- npx openzoo serve <program> local gateway/explorer for a deployed site
179
- (--cluster, --port 4402); /api/* reads are free
173
+ app/**/route, Vite + api/*) for Solana. Default
174
+ --target shared: bytecode for the shared zoo-vm
175
+ runtime, no Rust toolchain, a site is data accounts
176
+ (~0.03-0.1 SOL). --target program: own Pinocchio
177
+ program per site (--arch v0|v3, ~1 SOL+)
178
+ npx openzoo deploy [dir] deploy the site to Solana accounts (--cluster mainnet,
179
+ --runtime <zoo-vm id> or OPENZOO_VM_PROGRAM, --yes to
180
+ accept the rent estimate; burner wallet)
181
+ npx openzoo serve [site] local gateway/explorer for a deployed site (--runtime,
182
+ --cluster, --port 4402); /api/* reads are free
180
183
  simulations, writes are signed by the burner
184
+ npx openzoo runtime deploy deploy the shared zoo-vm runtime once per cluster
181
185
  npx openzoo inspect [dir] print the app in Vercel terms + eligibility report
182
186
  npx openzoo hub hosted explorer for every transmuted site on the
183
187
  cluster (what sites.openzoo.fun runs)
184
- npx openzoo hub hosted explorer for every transmuted site on the cluster
185
188
  npx openzoo address print both funding addresses (Solana + EVM)
186
189
  npx openzoo help this text
187
190
 
@@ -421,6 +424,7 @@ async function main() {
421
424
  case 'serve':
422
425
  case 'inspect':
423
426
  case 'hub':
427
+ case 'runtime':
424
428
  case 'transmute': {
425
429
  // VERCEL-SHAPED APP -> SOLANA MAINNET. `openzoo build|deploy|serve` hand
426
430
  // the argv to openzoo-transmute (a sibling npm package): it reads the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.51.2",
4
- "description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental. `openzoo build|deploy|serve|hub` transmute a Vercel-shaped app (Next.js / Vite + /api) into a Pinocchio program + asset accounts on Solana mainnet.",
3
+ "version": "0.51.3",
4
+ "description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental. `openzoo build|deploy|serve|hub|runtime` transmute a Vercel-shaped app (Next.js / Vite + /api) into Solana accounts on the shared zoo-vm Pinocchio runtime (~0.05 SOL a site) or a dedicated program, mainnet.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -63,17 +63,48 @@ Requirements: Node ≥ 18, and for `build`/`deploy` the Solana toolchain
63
63
  (`cargo build-sbf`): `sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"`.
64
64
  `build --skip-cargo` works without it (you get the crate and the cost sheet, no `.so`).
65
65
 
66
- ## Quickstart
66
+ ## Quickstart (shared runtime — the default)
67
+
68
+ A site does not need its own program. The `zoo-vm` runtime
69
+ (`runtime/zoo-vm`, prebuilt at `prebuilt/zoo_vm.v0.so`) is deployed once per
70
+ cluster; every site is then just data accounts under it — a 66-byte site
71
+ account, its bytecode module (`/.zoo/code.bin`, ~1–2 KB for a handful of
72
+ routes), its static files and its manifest. Rent for a small site is
73
+ **≈0.03–0.1 SOL** instead of ≈1 SOL+ for a dedicated program.
67
74
 
68
75
  ```sh
69
76
  cd my-next-app # or a Vite repo with api/*.js, or a .vercel/output
70
77
  npx openzoo-transmute inspect # the app in Vercel terms + eligibility report
71
- npx openzoo-transmute build # → .zoo-out/.zoo/{crate,manifest.json,deploy/*.so}
72
- npx openzoo-transmute deploy --cluster devnet
73
- npx openzoo-transmute serve <programId> --cluster devnet
78
+ npx openzoo-transmute build # → .zoo-out/.zoo/{code.bin,manifest.json,…}; no cargo needed
79
+ npx openzoo-transmute deploy --cluster mainnet --runtime <zoo-vm id> --yes
80
+ npx openzoo-transmute serve # site id + runtime read from .zoo/deploy.json
74
81
  open http://127.0.0.1:4402/ # explorer at /.zoo/, manifest at /.zoo/manifest.json
75
82
  ```
76
83
 
84
+ The site id is the public key of `.zoo/site-keypair.json` (looks like a
85
+ program id, costs nothing; keep it — redeploys update the same site). The hub
86
+ serves it at `https://sites.openzoo.fun/s/<siteId>` once its
87
+ `OPENZOO_VM_PROGRAM` points at the same runtime.
88
+
89
+ Deploying the runtime itself (once, ≈2.2 SOL on mainnet for the 313 KB .so,
90
+ upgradeable by the deployer's key):
91
+
92
+ ```sh
93
+ npx openzoo-transmute runtime deploy --cluster mainnet --yes # keypair → ./zoo-vm-keypair.json
94
+ export OPENZOO_VM_PROGRAM=<printed id>
95
+ ```
96
+
97
+ `--target program` keeps the original path: one Pinocchio program per site,
98
+ compiled from the generated crate with `cargo build-sbf`.
99
+
100
+ ## Quickstart (dedicated program)
101
+
102
+ ```sh
103
+ npx openzoo-transmute build --target program # → .zoo-out/.zoo/{crate,manifest.json,deploy/*.so}
104
+ npx openzoo-transmute deploy --cluster mainnet --yes
105
+ npx openzoo-transmute serve <programId> --cluster mainnet
106
+ ```
107
+
77
108
  `inspect` prints one line per function with its route, style, runtime,
78
109
  `maxDuration`, `memory` and methods, marks each `✓`/`✗` and says why:
79
110
 
@@ -115,14 +146,16 @@ POST /api/counter → 402 payment required (gateway started without
115
146
 
116
147
  | command | what it does |
117
148
  |---|---|
118
- | `build [dir] [--out .zoo-out] [--name <crate>] [--arch v0\|v3] [--cluster <c>] [--skip-cargo] [--json]` | read the app (`lib/vercel.js`), transmute (`lib/compile/`), write `<out>/.zoo/{crate/, manifest.json, report.json, static-plan.json, build.json}`, run `cargo build-sbf --arch <arch>` → `.zoo/deploy/<name>.so`, print the cost sheet (`--cluster` only chooses the RPC whose rent parameters price it, default `localnet`; unreachable → the 6.96 SOL/MB rule). Default arch is `v0` (mainnet); `deploy` re-detects the cluster's SBPF version and rebuilds if needed. Exit code 2 when nothing was eligible |
119
- | `deploy [dir\|outDir] [--cluster mainnet\|devnet\|testnet\|localnet\|<url>] [--keypair <path>] [--yes] [--program <id>] [--concurrency 4] [--skip-assets] [--force] [--json]` | deploy or upgrade the program (keypair kept at `.zoo/program-keypair.json`, so redeploys upgrade in place), upload every static file (unchanged ones skipped by comparing bytes on chain), write `/.zoo/manifest.json`, record `.zoo/deploy.json` |
120
- | `serve [programId] [--cluster <c>] [--port 4402] [--host 127.0.0.1] [--keypair <path>\|--no-keypair] [--quiet]` | the local gateway; `programId` defaults to the `.zoo/deploy.json` under the current directory (or its `.zoo-out/`). Reads are simulated, writes are signed by the wallet; without a wallet (or with `--no-keypair`) writes answer 402. Without `--cluster`/`OPENZOO_CLUSTER` this command assumes `localnet`, unlike the others |
149
+ | `build [dir] [--target shared\|program] [--runtime <id>] [--out .zoo-out] [--name <crate>] [--arch v0\|v3] [--cluster <c>] [--skip-cargo] [--json]` | `--target shared` (default): transmute to a `ZOOB` bytecode module at `<out>/.zoo/code.bin` for the shared runtime, no cargo, site-sized cost sheet. `--target program`: read the app (`lib/vercel.js`), transmute (`lib/compile/`), write `<out>/.zoo/{crate/, manifest.json, report.json, static-plan.json, build.json}`, run `cargo build-sbf --arch <arch>` → `.zoo/deploy/<name>.so`, print the cost sheet (`--cluster` only chooses the RPC whose rent parameters price it, default `localnet`; unreachable → the 6.96 SOL/MB rule). Default arch is `v0` (mainnet); `deploy` re-detects the cluster's SBPF version and rebuilds if needed. Exit code 2 when nothing was eligible |
150
+ | `deploy [dir\|outDir] [--cluster mainnet\|devnet\|testnet\|localnet\|<url>] [--keypair <path>] [--yes] [--runtime <id>] [--program <id>] [--concurrency 4] [--skip-assets] [--force] [--json]` | shared build: create the site account under `--runtime` (or `OPENZOO_VM_PROGRAM`), upload `code.bin`, assets and manifest as PDAs namespaced by the site id (keypair at `.zoo/site-keypair.json`). Program build: deploy or upgrade the program (keypair kept at `.zoo/program-keypair.json`, so redeploys upgrade in place), upload every static file (unchanged ones skipped by comparing bytes on chain), write `/.zoo/manifest.json`, record `.zoo/deploy.json` |
151
+ | `serve [siteId\|programId] [--runtime <id>] [--cluster <c>] [--port 4402] [--host 127.0.0.1] [--keypair <path>\|--no-keypair] [--quiet]` | the local gateway; with `--runtime` the positional is a site id on the shared runtime. It defaults to the `.zoo/deploy.json` under the current directory (or its `.zoo-out/`). Reads are simulated, writes are signed by the wallet; without a wallet (or with `--no-keypair`) writes answer 402. Without `--cluster`/`OPENZOO_CLUSTER` this command assumes `localnet`, unlike the others |
121
152
  | `inspect [dir] [--json]` | the Vercel model (functions, static files, routes, crons) plus the eligibility report |
122
- | `status <programId> [--cluster <c>] [--json]` | program account, authority, `maxDataLen`, deploy slot, and the on-chain manifest |
153
+ | `status <siteId\|programId> [--runtime <id>] [--cluster <c>] [--json]` | program account, authority, `maxDataLen`, deploy slot, and the on-chain manifest; with `--runtime`, the site account and its manifest |
154
+ | `runtime deploy [--cluster <c>] [--keypair <path>] [--yes] [--program-keypair zoo-vm-keypair.json] [--so <path>]` | deploy (or upgrade in place) the shared `zoo-vm` runtime: `prebuilt/zoo_vm.<arch>.so`, or built from `runtime/zoo-vm` when no prebuilt matches the cluster's SBPF version |
155
+ | `runtime status <id> [--cluster <c>]` | the runtime program's account |
123
156
  | `help`, `--version` | |
124
157
 
125
- Environment: `OPENZOO_CLUSTER` (default `mainnet` for `deploy`/`status`, `localnet` for `serve`), `OPENZOO_RPC` (mainnet RPC
158
+ Environment: `OPENZOO_VM_PROGRAM` (shared runtime id; default for `--runtime`), `OPENZOO_TARGET` (`shared`/`program`), `OPENZOO_CLUSTER` (default `mainnet` for `deploy`/`status`, `localnet` for `serve`), `OPENZOO_RPC` (mainnet RPC
126
159
  URL), `OPENZOO_KEYPAIR` / `OPENZOO_WALLET` (signer path), `OPENZOO_DEBUG=1`
127
160
  (stack traces). Signer discovery order (`lib/wallet.js`): `--keypair` →
128
161
  `OPENZOO_KEYPAIR` → `OPENZOO_WALLET` or `~/.openzoo/wallet.json` (the openzoo
@@ -251,8 +251,10 @@ export async function build(root = '.', opts = {}) {
251
251
  log(`reading ${root} → ${deployment.source}${deployment.framework ? ` (${deployment.framework})` : ''}: ${deployment.functions.length} function(s), ${deployment.staticFiles.length} static file(s)`);
252
252
  for (const n of deployment.notes) { log(`note: ${n}`); notes.push(n); }
253
253
 
254
+ const target = opts.target === 'shared' ? 'shared' : 'program';
254
255
  const transmute = opts.transmute || await loadTransmute();
255
- const result = await transmute(deployment, { name, runtimePath, arch: opts.arch, root, outDir });
256
+ const result = await transmute(deployment, { name, runtimePath, arch: opts.arch, root, outDir, target });
257
+ if (target === 'shared') return buildShared({ root, outDir, zooDir, deployment, name, result, notes, log, opts });
256
258
  if (!result || !result.crate || !result.crate['Cargo.toml'] || !result.crate['src/lib.rs']) throw new Error('transmute() returned no crate (expected {crate:{"Cargo.toml","src/lib.rs"}, manifest, report})');
257
259
  const manifest = { name, ...(result.manifest || {}) };
258
260
  if (!manifest.version) manifest.version = 1;
@@ -295,7 +297,7 @@ export async function build(root = '.', opts = {}) {
295
297
  const soSize = soPath ? fs.statSync(soPath).size : null;
296
298
  writeJson(path.join(zooDir, 'build.json'), {
297
299
  name, crateName, root, outDir, arch, soPath, soSize, soName: soNameFor(crateName), builtAt: new Date().toISOString(),
298
- runtimePath, cargo: cargo ? { ok: cargo.ok, missing: !!cargo.missing } : { skipped: true },
300
+ runtimePath, cargo: cargo ? { ok: cargo.ok, missing: !!cargo.missing } : { skipped: true }, target: 'program',
299
301
  });
300
302
 
301
303
  // Cost sheet.
@@ -309,3 +311,71 @@ export async function build(root = '.', opts = {}) {
309
311
  log(`next: openzoo-transmute deploy ${path.relative(process.cwd(), outDir) || outDir} --cluster <localnet|devnet|mainnet>`);
310
312
  return { outDir, zooDir, crateDir, manifest, report, soPath, arch, costEstimate, deployment, notes, staticPlan };
311
313
  }
314
+
315
+ // ---------------------------------------------------------------- shared runtime
316
+
317
+ /** Bytes of the site account the shared runtime creates per site. */
318
+ export const SITE_ACCOUNT_BYTES = 66;
319
+
320
+ /** Rent for a shared-runtime site: site account + code asset + assets + manifest (no program). */
321
+ export async function estimateSharedCost({ staticFiles = [], codeSize = 0, manifestBytes = 0, connection = null }) {
322
+ const est = await estimateCost({ staticFiles, soSize: null, manifestBytes, connection });
323
+ est.items = est.items.filter((i) => i.kind !== 'program');
324
+ const rent = async (n) => { if (connection) { try { return await connection.getMinimumBalanceForRentExemption(n); } catch {} } return (ACCOUNT_OVERHEAD_BYTES + n) * LAMPORTS_PER_BYTE; };
325
+ const codeBytes = accountBytesFor('asset', codeSize, 'application/x-zoo-bytecode');
326
+ const extra = [
327
+ { label: 'site account (shared runtime)', kind: 'site', bytes: SITE_ACCOUNT_BYTES, lamports: await rent(SITE_ACCOUNT_BYTES) },
328
+ { label: `/.zoo/code.bin (bytecode = ${codeSize.toLocaleString()} B)`, kind: 'asset', bytes: codeBytes, lamports: await rent(codeBytes) },
329
+ ];
330
+ for (const it of extra) it.sol = it.lamports / LAMPORTS_PER_SOL;
331
+ est.items = [...extra, ...est.items];
332
+ est.lamports += extra.reduce((n, i) => n + i.lamports, 0);
333
+ est.sol = est.lamports / LAMPORTS_PER_SOL;
334
+ est.feeLamports = 5000 * (2 + Math.ceil(codeSize / 900) + staticFiles.reduce((n, f) => n + 1 + Math.ceil(f.size / 900), 0) + 2);
335
+ est.feeSol = est.feeLamports / LAMPORTS_PER_SOL;
336
+ est.totalLamports = est.lamports + est.transientLamports + est.feeLamports;
337
+ est.totalSol = est.totalLamports / LAMPORTS_PER_SOL;
338
+ est.unknownProgram = false;
339
+ est.shared = true;
340
+ return est;
341
+ }
342
+
343
+ /** `build --target shared`: bytecode module instead of a crate; nothing to compile. */
344
+ async function buildShared({ root, outDir, zooDir, deployment, name, result, notes, log, opts }) {
345
+ if (!result || !result.code || !result.code.length) throw new Error('transmute() returned no bytecode module for --target shared');
346
+ const manifest = { name, ...(result.manifest || {}), target: 'shared' };
347
+ if (!manifest.version) manifest.version = 1;
348
+ if (!manifest.framework) manifest.framework = deployment.framework;
349
+ if (!Array.isArray(manifest.routes)) manifest.routes = [];
350
+ if (!Array.isArray(manifest.static)) manifest.static = deployment.staticFiles.map((f) => ({ path: f.path, contentType: f.contentType, size: f.size }));
351
+ if (!manifest.config) manifest.config = deployment.config;
352
+ const runtime = opts.runtime || process.env.OPENZOO_VM_PROGRAM || null;
353
+ if (runtime) manifest.runtime = runtime;
354
+ const report = { eligible: [], ineligible: [], warnings: [], ...(result.report || {}) };
355
+
356
+ fs.rmSync(path.join(zooDir, 'crate'), { recursive: true, force: true });
357
+ fs.mkdirSync(zooDir, { recursive: true });
358
+ const codePath = path.join(zooDir, 'code.bin');
359
+ fs.writeFileSync(codePath, result.code);
360
+ const byPath = new Map(deployment.staticFiles.map((f) => [f.path, f]));
361
+ const staticPlan = manifest.static.map((s) => ({ ...s, file: byPath.get(s.path)?.file || s.file || null }));
362
+ writeJson(path.join(zooDir, 'manifest.json'), manifest);
363
+ writeJson(path.join(zooDir, 'report.json'), report);
364
+ writeJson(path.join(zooDir, 'static-plan.json'), staticPlan);
365
+ const codeSize = result.code.length;
366
+ writeJson(path.join(zooDir, 'build.json'), { name, root, outDir, target: 'shared', codePath, codeSize, runtime, builtAt: new Date().toISOString(), cargo: { skipped: true } });
367
+ log(`wrote ${path.relative(process.cwd(), zooDir) || zooDir}/{code.bin (${codeSize.toLocaleString()} B bytecode), manifest.json, report.json, static-plan.json}`);
368
+ log(`functions: ${report.eligible.length} eligible, ${report.ineligible.length} ineligible${report.warnings.length ? `, ${report.warnings.length} warning(s)` : ''}`);
369
+ for (const i of report.ineligible) log(` ✗ ${i.name}: ${i.reason}${i.file ? ` (${path.relative(root, i.file)}${i.line ? ':' + i.line : ''})` : ''}`);
370
+ for (const w of report.warnings) log(` ! ${typeof w === 'string' ? w : w.message || JSON.stringify(w)}`);
371
+ if (!runtime) { const n = 'no --runtime given: pass --runtime <zoo-vm program id> (or OPENZOO_VM_PROGRAM) at deploy time'; notes.push(n); log(`note: ${n}`); }
372
+
373
+ const connection = opts.connection !== undefined ? opts.connection : await probeConnection(opts.cluster || 'localnet');
374
+ const manifestBytes = Buffer.byteLength(JSON.stringify({ ...manifest, runtime: 'x'.repeat(44), site: 'x'.repeat(44), deployedAt: new Date().toISOString() }));
375
+ const costEstimate = await estimateSharedCost({ staticFiles: staticPlan, codeSize, manifestBytes, connection });
376
+ log('');
377
+ log(formatCostTable(costEstimate));
378
+ log('');
379
+ log(`next: openzoo-transmute deploy ${path.relative(process.cwd(), outDir) || outDir} --cluster mainnet${runtime ? '' : ' --runtime <id>'}`);
380
+ return { outDir, zooDir, crateDir: null, manifest, report, soPath: null, codePath, codeSize, arch: null, target: 'shared', costEstimate, deployment, notes, staticPlan };
381
+ }
@@ -14,20 +14,25 @@ import path from 'node:path';
14
14
  import { PublicKey } from '@solana/web3.js';
15
15
  import { readDeployment } from './vercel.js';
16
16
  import { build } from './build.js';
17
- import { deploy, resolveOutDir, clusterLabel } from './deploy.js';
17
+ import { deploy, deployRuntime, resolveOutDir, clusterLabel } from './deploy.js';
18
18
  import { startGateway, DEFAULT_PORT } from './gateway.js';
19
19
  import { startHub, DEFAULT_HUB_PORT } from './hub.js';
20
- import { connect, getProgramInfo, readManifest } from './solana.js';
20
+ import { connect, getProgramInfo, getSiteInfo, readManifest } from './solana.js';
21
21
  import { loadWallet, rpcUrl } from './wallet.js';
22
22
  import { MANIFEST_PATH } from './wire.js';
23
23
 
24
24
  export const USAGE = `openzoo-transmute — Vercel app → Solana program + asset accounts
25
25
 
26
26
  usage:
27
- openzoo-transmute build [dir] [--out .zoo-out] [--name <crate>] [--arch v0|v3] [--cluster <c>] [--skip-cargo]
28
- openzoo-transmute deploy [dir|outDir] [--cluster mainnet|devnet|localnet|<url>] [--keypair <path>] [--yes] [--program <id>]
29
- [--concurrency 4] [--skip-assets] [--force] [--headroom 1.5]
30
- openzoo-transmute serve <programId> [--cluster <c>] [--port ${DEFAULT_PORT}] [--keypair <path>] [--host 127.0.0.1] [--quiet]
27
+ openzoo-transmute build [dir] [--target shared|program] [--out .zoo-out] [--name <crate>] [--arch v0|v3] [--cluster <c>] [--skip-cargo]
28
+ --target shared (default): bytecode for the shared zoo-vm runtime a site is data accounts only (≈0.05–0.1 SOL)
29
+ --target program: your own Pinocchio program per site (≈1 SOL+)
30
+ openzoo-transmute deploy [dir|outDir] [--cluster mainnet|devnet|localnet|<url>] [--keypair <path>] [--yes] [--runtime <zoo-vm id>]
31
+ [--program <id>] [--concurrency 4] [--skip-assets] [--force] [--headroom 1.5]
32
+ openzoo-transmute serve <siteId|programId> [--runtime <zoo-vm id>] [--cluster <c>] [--port ${DEFAULT_PORT}] [--keypair <path>] [--host 127.0.0.1] [--quiet]
33
+ openzoo-transmute runtime deploy [--cluster <c>] [--keypair <path>] [--yes] [--program-keypair zoo-vm-keypair.json] [--so <path>]
34
+ deploy (or upgrade) the shared zoo-vm runtime once per cluster
35
+ openzoo-transmute runtime status <zoo-vm id> [--cluster <c>]
31
36
  openzoo-transmute hub [--cluster mainnet] [--port 8080] [--host 0.0.0.0] [--public-url <https://…>]
32
37
  hosted explorer for EVERY program on the cluster (/s/<programId>), read-only
33
38
  openzoo-transmute inspect [dir] print the Vercel model + eligibility report
@@ -35,7 +40,7 @@ usage:
35
40
  openzoo-transmute help
36
41
 
37
42
  clusters: mainnet (default, needs --yes to deploy), devnet, testnet, localnet, or an RPC URL.
38
- env: OPENZOO_CLUSTER, OPENZOO_RPC (mainnet RPC), OPENZOO_KEYPAIR / OPENZOO_WALLET (signer).
43
+ env: OPENZOO_CLUSTER, OPENZOO_RPC (mainnet RPC), OPENZOO_KEYPAIR / OPENZOO_WALLET (signer), OPENZOO_VM_PROGRAM (shared runtime id).
39
44
  `;
40
45
 
41
46
  /** `--k v`, `--k=v`, `--flag`, `--no-flag`; everything else is positional. */
@@ -114,26 +119,49 @@ async function tryTransmute(deployment) {
114
119
  }
115
120
  }
116
121
 
122
+ export const DEFAULT_TARGET = 'shared';
123
+
124
+ /** `--runtime` is the zoo-vm program id (shared) — or, for `--target program`, a zoo-host checkout path. */
125
+ function runtimeFlag(f) {
126
+ const v = f.runtime;
127
+ if (typeof v !== 'string') return { runtime: process.env.OPENZOO_VM_PROGRAM || null, runtimePath: f.runtimePath || undefined };
128
+ if (fs.existsSync(v) && fs.existsSync(path.join(v, 'Cargo.toml'))) return { runtime: process.env.OPENZOO_VM_PROGRAM || null, runtimePath: v };
129
+ new PublicKey(v);
130
+ return { runtime: v, runtimePath: f.runtimePath || undefined };
131
+ }
132
+
117
133
  async function cmdBuild(p, f, log) {
118
- const r = await build(p[0] || '.', { out: f.out, name: f.name, arch: f.arch, cluster: f.cluster, runtimePath: f.runtime, skipCargo: !!f.skipCargo, log });
119
- if (f.json) log(JSON.stringify({ outDir: r.outDir, soPath: r.soPath, arch: r.arch, report: r.report, costEstimate: { sol: r.costEstimate.sol, totalSol: r.costEstimate.totalSol, source: r.costEstimate.source } }, null, 2));
134
+ const target = f.target || process.env.OPENZOO_TARGET || DEFAULT_TARGET;
135
+ if (!['shared', 'program'].includes(target)) throw new Error(`--target must be shared or program (got ${target})`);
136
+ const { runtime, runtimePath } = runtimeFlag(f);
137
+ const r = await build(p[0] || '.', { out: f.out, name: f.name, arch: f.arch, cluster: f.cluster, target, runtime, runtimePath, skipCargo: !!f.skipCargo, log });
138
+ if (f.json) log(JSON.stringify({ outDir: r.outDir, target: r.target || 'program', soPath: r.soPath, codePath: r.codePath || null, codeSize: r.codeSize || null, arch: r.arch, report: r.report, costEstimate: { sol: r.costEstimate.sol, totalSol: r.costEstimate.totalSol, source: r.costEstimate.source } }, null, 2));
120
139
  return r.report.ineligible.length && !r.report.eligible.length && r.manifest.routes.length === 0 && r.deployment.functions.length ? 2 : 0;
121
140
  }
122
141
 
123
142
  async function cmdDeploy(p, f, log) {
124
- const r = await deploy({ outDir: p[0] || '.', cluster: f.cluster, keypair: f.keypair, programId: f.program, yes: !!f.yes, concurrency: f.concurrency ? Number(f.concurrency) : undefined, skipAssets: !!f.skipAssets, force: !!f.force, log });
143
+ const { runtime } = runtimeFlag(f);
144
+ const r = await deploy({ outDir: p[0] || '.', cluster: f.cluster, keypair: f.keypair, programId: f.program, runtime, yes: !!f.yes, concurrency: f.concurrency ? Number(f.concurrency) : undefined, skipAssets: !!f.skipAssets, force: !!f.force, headroom: f.headroom, log });
125
145
  if (f.json) log(JSON.stringify(r, null, 2));
126
146
  return 0;
127
147
  }
128
148
 
129
149
  async function cmdServe(p, f, log) {
130
150
  let programId = p[0];
151
+ let { runtime } = runtimeFlag(f);
131
152
  if (!programId) {
132
153
  // Fall back to the last deploy in ./ or ./.zoo-out.
133
- try { programId = JSON.parse(fs.readFileSync(path.join(resolveOutDir('.'), '.zoo', 'deploy.json'), 'utf8')).programId; } catch { /* none */ }
134
- if (!programId) throw new Error('usage: serve <programId> (no .zoo/deploy.json found to infer it from)');
154
+ try {
155
+ const d = JSON.parse(fs.readFileSync(path.join(resolveOutDir('.'), '.zoo', 'deploy.json'), 'utf8'));
156
+ programId = d.target === 'shared' ? d.site : d.programId;
157
+ if (d.target === 'shared' && !runtime) runtime = d.runtime;
158
+ } catch { /* none */ }
159
+ if (!programId) throw new Error('usage: serve <siteId|programId> [--runtime <zoo-vm id>] (no .zoo/deploy.json found to infer it from)');
135
160
  }
136
161
  new PublicKey(programId); // validate
162
+ // Shared runtime: the positional is the site id, the program is zoo-vm.
163
+ let site = null;
164
+ if (runtime) { site = programId; programId = runtime; }
137
165
  let keypair = null, walletPath = null;
138
166
  if (f.keypair !== false) {
139
167
  try { const w = loadWallet({ keypair: typeof f.keypair === 'string' ? f.keypair : undefined }); keypair = w.keypair; walletPath = w.path; }
@@ -142,10 +170,10 @@ async function cmdServe(p, f, log) {
142
170
  const port = f.port != null ? Number(f.port) : DEFAULT_PORT;
143
171
  const host = f.host || '127.0.0.1';
144
172
  if (keypair && !/^(127\.0\.0\.1|localhost|::1|\[::1\])$/.test(host)) log(`warning: binding ${host} with a signer: anyone who can reach this gateway can send transactions paid by ${keypair.publicKey.toBase58()}; use --no-keypair unless that is intended`);
145
- const gw = await startGateway({ programId, cluster: f.cluster, port, host, keypair, log, quiet: !!f.quiet });
173
+ const gw = await startGateway({ programId, site, cluster: f.cluster, port, host, keypair, log, quiet: !!f.quiet });
146
174
  const m = gw.state.manifest;
147
175
  log(`openzoo-transmute gateway → ${gw.url}/ (explorer ${gw.url}/.zoo/, manifest ${gw.url}${MANIFEST_PATH})`);
148
- log(`program ${programId} on ${gw.state.cluster} · ${m.routes.length} function(s), ${m.static ? m.static.length : '?'} asset(s) · signer ${keypair ? keypair.publicKey.toBase58() + (walletPath ? ` (${walletPath})` : '') : 'none (reads only)'}`);
176
+ log(`${site ? `site ${site} on runtime ${programId}` : `program ${programId}`} on ${gw.state.cluster} · ${m.routes.length} function(s), ${m.static ? m.static.length : '?'} asset(s) · signer ${keypair ? keypair.publicKey.toBase58() + (walletPath ? ` (${walletPath})` : '') : 'none (reads only)'}`);
149
177
  for (const r of m.routes) log(` ${(r.methods && r.methods.length ? r.methods.join('|') : 'ANY').padEnd(16)} ${r.routePath}`);
150
178
  const stop = () => gw.close().then(() => process.exit(0));
151
179
  process.once('SIGINT', stop);
@@ -165,10 +193,24 @@ async function cmdInspect(p, f, log) {
165
193
  }
166
194
 
167
195
  async function cmdStatus(p, f, log) {
168
- if (!p[0]) throw new Error('usage: status <programId> [--cluster <c>]');
169
- const programId = new PublicKey(p[0]);
196
+ if (!p[0]) throw new Error('usage: status <siteId|programId> [--runtime <zoo-vm id>] [--cluster <c>]');
197
+ const { runtime } = runtimeFlag(f);
170
198
  const rpc = rpcUrl(f.cluster);
171
199
  const connection = connect(rpc);
200
+ if (runtime) {
201
+ const site = new PublicKey(p[0]);
202
+ const [info, manifest] = await Promise.all([getSiteInfo(connection, new PublicKey(runtime), site), readManifest(connection, new PublicKey(runtime), site).catch(() => null)]);
203
+ const out = { site: site.toBase58(), runtime, cluster: clusterLabel(f.cluster), rpc: rpc.replace(/\?.*$/, ''), exists: info.exists, authority: info.authority ? info.authority.toBase58() : null, account: info.pda.toBase58(), manifest: manifest ? { name: manifest.name, routes: manifest.routes?.length ?? 0, static: manifest.static?.length ?? 0, deployedAt: manifest.deployedAt } : null };
204
+ if (f.json) { log(JSON.stringify(out, null, 2)); return info.exists ? 0 : 1; }
205
+ log(`site ${out.site} on runtime ${runtime} (${out.cluster})`);
206
+ if (!info.exists) { log('status NOT DEPLOYED'); return 1; }
207
+ log(`authority ${out.authority}`);
208
+ log(`account ${out.account}`);
209
+ if (manifest) { log(`manifest ${manifest.name || ''} · ${out.manifest.routes} function(s), ${out.manifest.static} asset(s) · deployed ${manifest.deployedAt || '?'}`); for (const r of manifest.routes || []) log(` ${(r.methods && r.methods.length ? r.methods.join('|') : 'ANY').padEnd(16)} ${r.routePath}`); }
210
+ else log(`manifest none at ${MANIFEST_PATH}`);
211
+ return 0;
212
+ }
213
+ const programId = new PublicKey(p[0]);
172
214
  const [info, slot, manifest] = await Promise.all([getProgramInfo(connection, programId), connection.getSlot('confirmed'), readManifest(connection, programId).catch(() => null)]);
173
215
  const out = {
174
216
  programId: programId.toBase58(), cluster: clusterLabel(f.cluster), rpc: rpc.replace(/\?.*$/, ''), slot,
@@ -215,13 +257,27 @@ async function cmdHub(p, f, log) {
215
257
  return 0;
216
258
  }
217
259
 
260
+ async function cmdRuntime(p, f, log) {
261
+ const sub = p[0];
262
+ if (sub === 'deploy') {
263
+ const r = await deployRuntime({ cluster: f.cluster, keypair: f.keypair, yes: !!f.yes, programKeypair: f.programKeypair, so: f.so, headroom: f.headroom, log });
264
+ if (f.json) log(JSON.stringify(r, null, 2));
265
+ return 0;
266
+ }
267
+ if (sub === 'status') {
268
+ if (!p[1]) throw new Error('usage: runtime status <zoo-vm id> [--cluster <c>]');
269
+ return cmdStatus([p[1]], { ...f, runtime: undefined }, log);
270
+ }
271
+ throw new Error('usage: runtime deploy|status');
272
+ }
273
+
218
274
  export async function run(argv = process.argv.slice(2), io = {}) {
219
275
  const log = io.log ?? console.log;
220
276
  const error = io.error ?? console.error;
221
277
  const { cmd, positionals, flags } = parseArgs(argv);
222
278
  if (flags.version) { log(JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version); return 0; }
223
279
  if (flags.help || cmd === 'help' || cmd === '--help') { log(USAGE); return 0; }
224
- const commands = { build: cmdBuild, deploy: cmdDeploy, serve: cmdServe, inspect: cmdInspect, status: cmdStatus, hub: cmdHub };
280
+ const commands = { build: cmdBuild, deploy: cmdDeploy, serve: cmdServe, inspect: cmdInspect, status: cmdStatus, hub: cmdHub, runtime: cmdRuntime };
225
281
  const fn = commands[cmd];
226
282
  if (!fn) { error(`unknown command: ${cmd}\n`); error(USAGE); process.exitCode = 1; return 1; }
227
283
  try {