@artblocks/abx-cli 0.1.0-alpha.11 → 0.1.0-alpha.12

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/dist/main.js CHANGED
@@ -3613,8 +3613,15 @@ async function cmdDeployCode(flags) {
3613
3613
  // has nothing to run — point at the from-chain check instead.
3614
3614
  if (hasImageRenderer && !hasProgram)
3615
3615
  info(` ${g('nothing to run')} — verify from chain: ${bold('abx tokenuri ' + (predicted ?? '<address>'))} ${dim('(decodes name + on-chain SVG + traits)')}`);
3616
- else
3617
- info(` ${dim('stand up the runner:')} ${bold('abx deploy-effects --resolver-url ' + baseUrl)} · one-shot: ${bold('abx render ' + (predicted ?? '<address>') + (onChainUri && hasImageBase ? '' : ' --remote ' + baseUrl))} · verify: ${bold('abx verify ' + (predicted ?? '<address>'))}`);
3616
+ else {
3617
+ const remoteRender = !(onChainUri && hasImageBase);
3618
+ info(` ${dim('stand up the runner:')} ${bold('abx deploy-effects --resolver-url ' + baseUrl)} · one-shot: ${bold('abx render ' + (predicted ?? '<address>') + (remoteRender ? ' --remote ' + baseUrl : ''))} · verify: ${bold('abx verify ' + (predicted ?? '<address>'))}`);
3619
+ // Rendering against a resolver you don't share a disk with means YOU hold the bytes and it
3620
+ // holds the URL — so say now which backend that needs, rather than letting the render fail.
3621
+ if (remoteRender && !resolveBackend(storageOptions(storageOverrides(flags))).locator) {
3622
+ info(` ${dim('that render publishes a URL, so it needs a backend that can name one:')} ${bold('--backend cloud')} ${dim('(S3/R2 + public base) ·')} ${bold('ipfs')} ${dim('·')} ${bold('arweave')} ${dim('— or render co-located with the resolver (`abx effects` on its host).')}`);
3623
+ }
3624
+ }
3618
3625
  if (dirUpload)
3619
3626
  await noteStorageReadiness(storageOptions(storageOverrides(flags)), dirUpload.sizes);
3620
3627
  if (deployer && salt && !parseSaltFlag(flags.salt)) {
@@ -3892,6 +3899,10 @@ async function cmdRender(address, tokenIds, flags) {
3892
3899
  const remote = remoteFlag(flags);
3893
3900
  const resolverUrl = (remote?.url ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
3894
3901
  const adminToken = remote ? requireRemoteToken(remote) : undefined;
3902
+ // Refuse the combination that can't work before launching Chromium (a laptop render against a
3903
+ // hosted resolver, on a backend with no public URL) — the render would end in a 400 either way.
3904
+ if (remote)
3905
+ requirePublishableBackend(flags, 'abx render --remote');
3895
3906
  // Co-located (no --remote): record each declared output into the shared store's artifact
3896
3907
  // registry so the local resolver's `artifacts` manifest enumerates it. Remote: the publish
3897
3908
  // lane (adminToken) records rows on the hosted resolver instead.
@@ -3916,6 +3927,41 @@ async function cmdRender(address, tokenIds, flags) {
3916
3927
  else
3917
3928
  ok(summary);
3918
3929
  }
3930
+ /**
3931
+ * The publish topology's one hard prerequisite, checked BEFORE any capture.
3932
+ *
3933
+ * A resolver that doesn't share this machine's disk serves referenced output (the still, a video, a
3934
+ * model) by **redirect** — so it needs a URL, and it refuses the bytes (`specs/protocol/effects.md →
3935
+ * Bound vs referenced`). A backend that can't name a locator therefore has no publish lane at all,
3936
+ * and every render against one would be work spent to earn a 400.
3937
+ *
3938
+ * So this refuses up front and names the ways out, rather than warning and letting the render run.
3939
+ * It deliberately does NOT pick a backend for the operator: which store holds their art — and which
3940
+ * gateway serves it — is theirs to decide. The three that work are peers, not a ranking: `https://`
3941
+ * from S3/R2, an IPFS gateway, and Arweave are all fine, because derived output is re-creatable and
3942
+ * the only real requirement is that a third party can fetch it.
3943
+ */
3944
+ function requirePublishableBackend(flags, what) {
3945
+ const backend = resolveBackend(storageOptions(storageOverrides(flags)));
3946
+ // `cloud` is the one backend that HAS a locator method and can still return null: the URL is
3947
+ // `<publicBase>/<key>`, so without a public base it can name nothing. Catch that here too — it
3948
+ // would otherwise be the same failure one render later.
3949
+ if (backend.id === 'cloud' && !backend.publicBase) {
3950
+ throw new Error(`${what} hands the resolver a URL for each render, but the 'cloud' backend has no public read base — ` +
3951
+ `set ${bold('ABX_S3_PUBLIC_BASE')} to the bucket's public URL (an R2/CloudFront/S3-website base), ` +
3952
+ `or use ${bold('--backend ipfs')} / ${bold('--backend arweave')}, or render co-located with the resolver.`);
3953
+ }
3954
+ if (backend.locator)
3955
+ return;
3956
+ const { backend: name, source } = backendResolution(storageOverrides(flags));
3957
+ throw new Error(`${what} publishes each render to a resolver that doesn't share this machine's disk, but the '${name}' backend ` +
3958
+ `(${source === 'default' ? 'the default' : `from ${source}`}) can't produce a URL for what it stores — so the resolver would have nothing to serve.\n` +
3959
+ ` Point it at a backend that can (equal options — pick on cost/ops, not durability dogma):\n` +
3960
+ ` ${bold('--backend cloud')} S3 / R2 / B2 with a public base (ABX_S3_* + ABX_S3_PUBLIC_BASE)\n` +
3961
+ ` ${bold('--backend ipfs')} Pinata or your own Kubo + a public gateway (PINATA_JWT / ABX_IPFS_*)\n` +
3962
+ ` ${bold('--backend arweave')} pay-once permanent (uploads under 100KiB are free, no setup)\n` +
3963
+ ` …or run co-located instead: ${bold('abx effects')} on the same host as ${bold('abx serve')}, sharing one backend and no remote token.`);
3964
+ }
3919
3965
  /**
3920
3966
  * `abx effects` — run the reference effect runner LOCALLY, in-process (the local counterpart to
3921
3967
  * `abx deploy-effects`, which scaffolds a HOSTED runner). This is what makes a param change / new
@@ -3935,6 +3981,8 @@ async function cmdEffects(flags) {
3935
3981
  // the shared artifact registry so the resolver's `artifacts` manifest enumerates it. With an
3936
3982
  // admin token, the publish lane records rows on the hosted resolver instead.
3937
3983
  const localStore = adminToken ? null : new SqliteStore();
3984
+ if (adminToken)
3985
+ requirePublishableBackend(flags, 'abx effects against a remote resolver');
3938
3986
  const { EffectRunner, renderEffect } = await loadEffects();
3939
3987
  const runner = new EffectRunner({
3940
3988
  resolverUrl,
@@ -5320,10 +5368,16 @@ async function cmdDoctor(flags) {
5320
5368
  try {
5321
5369
  const backend = resolveBackend(storageOptions());
5322
5370
  const h = await backend.health?.();
5371
+ // Whether this backend can name a public URL for what it stores decides one thing operators hit
5372
+ // later: a REMOTE effects runner needs it (the resolver serves referenced output by redirect, so
5373
+ // it takes a URL and refuses bytes). Co-located rendering doesn't care — hence a note, not a fail.
5374
+ const publishable = backend.locator
5375
+ ? 'can publish to a remote resolver'
5376
+ : 'local-only — a remote effects runner needs cloud/ipfs/arweave (co-located rendering is fine)';
5323
5377
  if (h)
5324
- check('storage', h.ok, `${backend.id} · ${h.detail ?? ''}`);
5378
+ check('storage', h.ok, `${backend.id} · ${h.detail ?? ''} · ${publishable}`);
5325
5379
  else
5326
- check('storage', true, `${backend.id} · configured`);
5380
+ check('storage', true, `${backend.id} · configured · ${publishable}`);
5327
5381
  }
5328
5382
  catch (err) {
5329
5383
  check('storage', false, `${backendId} · ${err.message}`);
@@ -5532,13 +5586,19 @@ async function cmdDeployEffects(flags) {
5532
5586
  const intervalMs = flags['interval-ms'] ? Number(flags['interval-ms']) : undefined;
5533
5587
  const environmentId = flags['env-id'] ?? process.env.ABX_ENVIRONMENT_ID;
5534
5588
  const storageBackend = process.env.ABX_STORAGE_BACKEND;
5535
- // A HOSTED runner must publish renders where the resolver can read them a durable locator
5536
- // (ipfs/arweave) or a shared bucket (s3). The default `fs` writes to the runner CONTAINER's disk,
5537
- // which the resolver can't see the thumbnail never clears. Flag it before scaffolding (not just
5538
- // in the artifact's comments), since it's the #1 silent misconfig for a hosted effects runner.
5589
+ // A HOSTED runner holds its own render bytes and hands the resolver a URLthat's the whole
5590
+ // topology (`specs/protocol/effects.md Bound vs referenced`). The default `fs` writes to the
5591
+ // runner CONTAINER's disk, which nothing else can reach, so there is no URL to publish: the runner
5592
+ // now REFUSES to start on that config. Scaffolding it anyway would just deploy a container that
5593
+ // exits, so this is a hard stop rather than the warning it used to be.
5539
5594
  if (!storageBackend || storageBackend === 'fs') {
5540
- warn(`ABX_STORAGE_BACKEND is ${storageBackend ? "'fs'" : 'unset (defaults to fs)'} — a HOSTED runner can't publish 'fs' renders anywhere the resolver can read ` +
5541
- `(the thumbnail will stay a placeholder). Set ABX_STORAGE_BACKEND to ${bold('ipfs')} / ${bold('arweave')} (durable locator) or ${bold('s3')} before deploying the runner.`);
5595
+ throw new Error(`ABX_STORAGE_BACKEND is ${storageBackend ? "'fs'" : 'unset (defaults to fs)'} — a hosted runner can't serve renders off its own container disk, ` +
5596
+ `and it refuses to start without a backend that can name a public URL for what it stores.\n` +
5597
+ ` Set ABX_STORAGE_BACKEND to one of (equal options — pick on cost/ops):\n` +
5598
+ ` ${bold('cloud')} (alias s3) S3 / R2 / B2 with ABX_S3_* + a public base\n` +
5599
+ ` ${bold('ipfs')} Pinata or your own Kubo + a public gateway\n` +
5600
+ ` ${bold('arweave')} pay-once permanent (<100KiB uploads are free)\n` +
5601
+ ` …then re-run. (Rendering on the resolver's own host instead? Use ${bold('abx effects')} co-located — no publish lane, no locator needed.)`);
5542
5602
  }
5543
5603
  const repoRoot = findRepoRoot();
5544
5604
  if (!repoRoot) {
@@ -5560,10 +5620,7 @@ async function cmdDeployEffects(flags) {
5560
5620
  const outDir = resolvePath(joinPath(dir, 'effects'));
5561
5621
  console.log(bold(`\n ABX Self-Host Toolkit — provision effects runner (fly)`));
5562
5622
  info(`resolver: ${bold(cleanResolver)} ${dim('(reads token state + publishes renders here)')}`);
5563
- info(`storage home: ${bold(storageBackend ?? 'fs')} ${dim('— where rendered thumbnails live; the resolver redirects/serves from here')}`);
5564
- if (!storageBackend || storageBackend === 'fs') {
5565
- warn('storage backend is fs (LOCAL) — a HOSTED runner needs a PUBLIC home (ipfs/arweave/s3) so the resolver can serve the render. Set ABX_STORAGE_BACKEND before deploying, or the runner will push raw bytes to the resolver.');
5566
- }
5623
+ info(`storage home: ${bold(storageBackend)} ${dim('— the runner HOLDS the render bytes here and publishes their URL; the resolver redirects to it and never proxies')}`);
5567
5624
  step('Write the effects artifact (self-contained — Dockerfile.effects + fly.toml + vendored source)');
5568
5625
  mkdirSync(outDir, { recursive: true });
5569
5626
  for (const f of art.files) {
@@ -5807,8 +5864,12 @@ const COMMAND_HELP = {
5807
5864
  No tokenId → sweeps all minted tokens; pass ids (${g('0 1 2')}) to target specific tokens.
5808
5865
  ${g('--force')} RE-RENDER even when the still already exists — the fix for a bad / blank / timed-out capture
5809
5866
  (the art is otherwise deterministic, so a plain render idempotent-skips an existing still). Overwrites it (+ republishes on --remote).
5810
- ${g('--remote [name|url]')} publish each render to a REMOTE resolver (the locator bridge: upload to ${bold('ABX_STORAGE_BACKEND')}, POST /v1/effect-artifacts).
5811
- Idempotent; a re-run restores a resolver that lost its volume. Needs its token (a named remote's
5867
+ ${g('--remote [name|url]')} register each render with a REMOTE resolver: the bytes go to ${bold('ABX_STORAGE_BACKEND')} (YOU hold them), and
5868
+ POST /v1/effect-artifacts hands the resolver the URL it redirects there and never proxies. Traits are the
5869
+ exception: they stitch into the token JSON, so their content (≤64KB) goes to the resolver itself.
5870
+ ${bold('Requires a backend that can name a public URL')} — ${g('--backend cloud')} (S3/R2 + public base), ${g('ipfs')}, or ${g('arweave')};
5871
+ equal options, pick on cost/ops. The default ${g('fs')} is refused up front rather than after the render.
5872
+ Idempotent; a re-run re-registers rows on a resolver that lost them. Needs its token (a named remote's
5812
5873
  ${g('ABX_REMOTE_<NAME>_TOKEN')}, else ${g('ABX_RESOLVER_ADMIN_TOKEN')}).
5813
5874
  --effects-url <url> enqueue on a running effect-runner service instead of rendering inline (else ${g('ABX_EFFECTS_URL')})
5814
5875
  ${dim('Inline (no --remote) renders on THIS machine (needs `npx playwright install chromium` once), pointing Chromium at the live')}
@@ -5825,8 +5886,10 @@ const COMMAND_HELP = {
5825
5886
  --port <n> HTTP port (default ${g('ABX_EFFECTS_PORT')} / 8788) — ${g('POST /notify')} enqueues (watcher lane) · ${g('POST /run')} sweeps synchronously (command lane)
5826
5887
  --interval-ms <n> the SAFETY-FLOOR sweep (default ${g('ABX_EFFECTS_INTERVAL_MS')} / 300000) — catches a missed notify / cold start; the watcher is the trigger
5827
5888
  --concurrency <n> parallel renders while draining (default ${g('ABX_EFFECTS_CONCURRENCY')} / 1 — Chromium is heavy; raise deliberately)
5828
- ${dim('Co-located with a local `abx serve` (same store) → no tokens needed. Against a REMOTE resolver: --remote <name|url> (its')}
5829
- ${dim('token publishes renders + reports status), or ABX_RESOLVER_URL + ABX_RESOLVER_ADMIN_TOKEN. PUBLIC runner? set ABX_EFFECTS_TOKEN — it gates /run + /notify.')}
5889
+ ${dim('Co-located with a local `abx serve` (same store) → no tokens needed, any backend works (fs included).')}
5890
+ ${dim('Against a REMOTE resolver: --remote <name|url> (its token registers renders + reports status), or ABX_RESOLVER_URL +')}
5891
+ ${dim('ABX_RESOLVER_ADMIN_TOKEN — and a backend that can name a public URL (cloud/ipfs/arweave), since the resolver takes the')}
5892
+ ${dim('URL and refuses the bytes. PUBLIC runner? set ABX_EFFECTS_TOKEN — it gates /run + /notify.')}
5830
5893
  ${dim('To HOST the runner (fly/docker), use `abx deploy-effects`.')}`,
5831
5894
  'configure-param': `
5832
5895
  ${bold('abx configure-param')} <address> <tokenId|-> <key> <value> ${dim('— set a PostParam (typed, canonical encode). Sends a tx.')}