@artblocks/abx-cli 0.1.0-alpha.8 → 0.1.0-alpha.9

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
@@ -17,8 +17,9 @@
17
17
  * abx preview run a code project on localhost while it's still being made (no chain)
18
18
  * abx deploy-code deploy a code project (SeriesCode): --script <file> (on-chain template)
19
19
  * or --code-dir <dir> (build directory → ipfs/arweave `code` field)
20
- * abx add <address> register + index a project (--remote: on a hosted resolver, not this machine)
21
- * abx index [<address>] re-index a project from chain (replay; --remote to nudge a hosted resolver)
20
+ * abx add <address> register + index a project (--remote <name|url>: on a remote resolver, not this machine)
21
+ * abx remote [<name|url>] inspect a remote service (descriptor · chains · managed rendering · your projects there)
22
+ * abx index [<address>] re-index a project from chain (replay; --remote to nudge a remote resolver)
22
23
  * abx verify <address> re-hash served bytes vs the on-chain commitment (no server)
23
24
  * abx configure-param <addr> <id> <key> <value> set a governed PostParam (typed encode; any lane)
24
25
  * abx set-param-hooks <addr> wire/clear a SeriesCode's configure/augment/transfer param hooks
@@ -70,13 +71,13 @@ const SCHEMA_CATALOG = 'Types: Bool·Select·Uint256Range·Int256Range·DecimalR
70
71
  'Select needs options — key:Select[A|B|C]:Auth; a Range takes bounds — key:Uint256Range[0..100]:Auth. ' +
71
72
  'A palette collectors set = palette:HexColor:TokenOwner';
72
73
  import { uploadAndLocate } from './upload.js';
73
- import { assertChainId, discoverDeployBlock, deployFactory, deploySeriesFactory, deploySeries, deployOneOfOne, deployRenderer, predictRenderer, predictSeedSource, encodeTag, encodeFieldRenderer, isCodeProject, loadDotEnv, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererAbi, predictClone, probeRpcEndpoints, prepareDeployOneOfOne, prepareDeploySeries, reconstructProject, saltFor, saltGuard, resolveChain, explorerUrl, DEFAULT_CHAIN_KEY, normalizeAttributes, parseTraitPairs, METADATA_FIELD as F, METADATA_REPRESENTATION as R, } from '@artblocks/abx-sdk';
74
+ import { assertChainId, discoverDeployBlock, deployFactory, deploySeriesFactory, deploySeries, deployOneOfOne, deployRenderer, predictRenderer, predictSeedSource, encodeTag, encodeFieldRenderer, isCodeProject, loadDotEnv, makePublicClient, makeWalletClient, oneOfOneImageAbi, oneOfOneImageFactoryAbi, seriesImageAbi, seriesImageFactoryAbi, abxMetadataRendererAbi, predictClone, probeRpcEndpoints, prepareDeployOneOfOne, prepareDeploySeries, reconstructProject, saltFor, saltGuard, resolveChain, resolveRpcUrl, redactRpcUrl, explorerUrl, DEFAULT_CHAIN_KEY, normalizeAttributes, parseTraitPairs, METADATA_FIELD as F, METADATA_REPRESENTATION as R, AbxServiceError, } from '@artblocks/abx-sdk';
74
75
  import { SelfHostIndexer, SqliteStore } from '@artblocks/abx-indexer';
75
76
  import { artContentHash, currentRenderArtifact, generateArt, resolveBaseUrl, startChainWatcher, startTokenApiServer, verifyProject, watchIntervalMs, DEFAULT_PORT, } from '@artblocks/abx-token-api';
76
77
  import { ARWEAVE_FREE_UPLOAD_LIMIT, arweaveAddress, arweaveFunding, contentTypeFromPath, hashContent, resolveBackend, turboBalanceForAddress, turboUploadCostUsd, turboUploadWinc } from '@artblocks/abx-storage';
77
78
  import { cmdTransfer, cmdMint, cmdSetMinter, cmdSetMaxInvocations, cmdConfigureParam, cmdSetParamHooks, cmdSetDependency, cmdRemoveLastDependency, cmdSetDependencyRegistry, cmdLockDependencies, cmdSetPrimaryPayee, cmdPause, cmdUnpause, cmdRefresh, cmdSetTokenUri, cmdSetContractUri, cmdSetRoyalty, cmdSetField, cmdAttach, cmdLockField, cmdSetRenderer, cmdLockUri, cmdSetAdmin, cmdMinterConfigure, cmdMinterShow, cmdMinterBuy, computeContentPlan, envStagingSender, laneFromFlags, ONCHAIN_PROJECT_SOFT_LIMIT, parseCompress, previewImageStaging, sessionStagingSender, stageImageField, stageImageFieldsBatch, authorshipContractFields, AUTHORSHIP_DEPLOY_FIELDS, } from './ownerops.js';
78
79
  import { openWalletSession, signTx } from './signer.js';
79
- import { remoteAddProject, remoteRemoveProject } from './remote.js';
80
+ import { describeRemoteError, listConfiguredRemotes, misnamedRemoteVars, requireRemoteToken, resolveRemote, serviceClient } from './remote.js';
80
81
  import { buildMigrationPlan, repinNodeCustody, verifyParity } from './migrate.js';
81
82
  import { activeBackendId, arweaveKeyFilePath, backendResolution, ensureArweaveJwk, factoryAddress, seriesFactoryAddress, fixedPriceMinterAddress, loadArweaveJwk, loopbackBaseUrl, faucetHint, rendererAddress, storageOptions, storageSignerChoice, } from './config.js';
82
83
  const CHAIN = process.env.ABX_CHAIN ?? DEFAULT_CHAIN_KEY;
@@ -207,6 +208,7 @@ async function main() {
207
208
  case 'set-admin': return cmdSetAdmin(rest[0], flags);
208
209
  case 'forget': return cmdForget(rest[0], flags);
209
210
  case 'migrate': return cmdMigrate(rest[0], flags);
211
+ case 'remote': return cmdRemote(rest[0], flags);
210
212
  case 'storage': return cmdStorage(rest);
211
213
  case 'status': return cmdStatus();
212
214
  case 'state': return cmdState(rest[0], flags);
@@ -223,6 +225,65 @@ async function main() {
223
225
  process.exit(1);
224
226
  }
225
227
  }
228
+ /**
229
+ * A `--dry-run` computes the deterministic deploy address, which is a pure function of
230
+ * (factory, salt, deployer) — so it needs a deployer even though it signs nothing. Resolve it the
231
+ * same way the preview will (`--for`, else an env key) and fail EARLY with the fix if neither
232
+ * exists, rather than after the preview has printed several steps of work.
233
+ */
234
+ /**
235
+ * A dry run only checks whether a factory address is CONFIGURED, not whether it has code on this
236
+ * chain — and the manifest always has an address, so a chain where the trust anchor isn't deployed
237
+ * (a private/local chain, or a wrong-network RPC) sailed past this and died inside
238
+ * `predictDeterministicAddress` with a raw `returned no data ("0x")` and a list of ABI hypotheses.
239
+ * The real deploy and `abx predict` both explain that case; a preview of the same deploy must too.
240
+ * Returns false when the caller should stop (message already printed).
241
+ */
242
+ async function previewFactoryLive(client, factory, label) {
243
+ const code = await client.getCode({ address: factory }).catch(() => undefined);
244
+ if (code && code !== '0x')
245
+ return true;
246
+ warn(`the configured ${label} ${factory} has no code on '${CHAIN}' (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}).`);
247
+ info('so this preview can\'t compute the deterministic address. Either point at a chain where the trust anchor is deployed');
248
+ info(`(${bold('ABX_CHAIN=' + DEFAULT_CHAIN_KEY)} is the default and has one), or deploy your own on this chain with ${bold('--bootstrap-factory')}`);
249
+ info(dim('(a private anchor — platforms won\'t recognize its clones, so it\'s for private/sandbox chains).'));
250
+ console.log(`\n ${g('dry run')} ${dim('— nothing sent.')}\n`);
251
+ return false;
252
+ }
253
+ /**
254
+ * Placeholder-identity guard, shared by ALL THREE deploy commands. `name`/`symbol` are written
255
+ * on-chain as the public collection identity and are effectively permanent, so a real deploy must
256
+ * never bake a tool default silently: warn in a preview, HARD-STOP a real send unless `--yes`.
257
+ *
258
+ * It was duplicated per command, and `deploy-series` simply never got a copy — its default
259
+ * "ABX Series"/"ABXS" went on-chain with at most a warning, while the skill promises the CLI
260
+ * refuses demo defaults. (deploy-code's copy even said "mirror deploy/deploy-series", which made
261
+ * the gap look closed.) One predicate now, like `loopbackBaseUrl()`, so a fourth command can't drift.
262
+ */
263
+ function assertRealIdentity(flags, o) {
264
+ if (flags.name && flags.symbol)
265
+ return;
266
+ if (!flags.name)
267
+ warn(`no --name → default "${o.name}" would be the on-chain collection name`);
268
+ if (!flags.symbol)
269
+ warn(`no --symbol → default "${o.symbol}" would be the on-chain symbol`);
270
+ if (o.dryRun || flags.yes)
271
+ return; // a preview still runs; --yes is the explicit opt-in
272
+ throw new Error('refusing to write tool placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM ' +
273
+ '(or --yes to accept the defaults). On-chain identity is effectively permanent.');
274
+ }
275
+ function assertPreviewDeployer(flags) {
276
+ if (flags.for)
277
+ return;
278
+ try {
279
+ makeWalletClient({ chainKey: CHAIN });
280
+ }
281
+ catch {
282
+ throw new Error('dry run needs a deployer address to compute the deterministic deploy address — pass --for 0x.. ' +
283
+ '(a preview signs nothing, so no key is needed). For the REAL deploy with no key in .env, use the ' +
284
+ 'wallet lane: --sign --for 0x.. (you approve in your own wallet).');
285
+ }
286
+ }
226
287
  // When the user/agent opts in (--yes), lift the getLogs chunk cap so a large
227
288
  // reconstruction proceeds despite a range-limited RPC (otherwise it stops early with
228
289
  // guidance — see GetLogsScanTooLargeError + the skill's "Choosing an RPC" decision).
@@ -1010,23 +1071,20 @@ async function cmdDeploy(flags, serveAfter) {
1010
1071
  }
1011
1072
  }
1012
1073
  const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
1074
+ // A keyless preview needs `--for` (the address is a pure function of factory+salt+deployer). Check
1075
+ // it HERE, before the trust-anchor/content/plan steps print — hitting this after a wall of output
1076
+ // reads as "it half-worked", and a first-timer previewing with no key in .env always hits it.
1077
+ if (dryRun)
1078
+ assertPreviewDeployer(flags);
1013
1079
  const publicClient = makePublicClient({ chainKey: CHAIN });
1014
1080
  // Verify the RPC really is CHAIN before any send (factory/renderer/staging/deploy). A dry run
1015
1081
  // sends nothing, but it DOES read the chain (predict address, resolve the factory/renderer), so
1016
1082
  // a wrong-network RPC must still be caught with the clear mismatch message rather than failing
1017
1083
  // opaquely inside predict; `allowUnreachable` keeps a genuinely offline dry-run previewable.
1018
1084
  await assertChainId(CHAIN, { allowUnreachable: dryRun });
1019
- // Identity guard: the demo placeholders ("ABX Self-Host Demo"/"ABXSH") are written on-chain as
1020
- // the public collection identity. Warn in preview; HARD-STOP a real deploy unless acknowledged.
1021
- if (!serveAfter && (!flags.name || !flags.symbol)) {
1022
- if (!flags.name)
1023
- warn(`no --name → default "${name}" would be the on-chain collection name`);
1024
- if (!flags.symbol)
1025
- warn(`no --symbol → default "${symbol}" would be the on-chain symbol`);
1026
- if (!dryRun && !flags.yes) {
1027
- throw new Error('refusing to write demo placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM (or --yes to accept the defaults).');
1028
- }
1029
- }
1085
+ // `demo` is exempt: its whole job is a zero-argument first token.
1086
+ if (!serveAfter)
1087
+ assertRealIdentity(flags, { name, symbol, dryRun });
1030
1088
  // Funding preflight (real deploys): warn now if the signer is unfunded, not at the tx.
1031
1089
  if (!dryRun) {
1032
1090
  let signer = flags.for;
@@ -1055,6 +1113,8 @@ async function cmdDeploy(flags, serveAfter) {
1055
1113
  return;
1056
1114
  }
1057
1115
  factory = existing;
1116
+ if (!(await previewFactoryLive(publicClient, factory, 'factory')))
1117
+ return;
1058
1118
  info(`would reuse canonical factory ${factory}`);
1059
1119
  }
1060
1120
  else {
@@ -1561,21 +1621,15 @@ async function cmdDeploySeries(flags) {
1561
1621
  throw new Error(`--count ${count} exceeds the ${files.length} media file(s) in ${dirPath}`);
1562
1622
  const slots = files.slice(0, count);
1563
1623
  const dryRun = !!flags['dry-run'];
1624
+ if (dryRun)
1625
+ assertPreviewDeployer(flags); // fail fast, before the preview does any work (see cmdDeploy)
1626
+ assertRealIdentity(flags, { name, symbol, dryRun });
1564
1627
  const lane = laneFromFlags(flags);
1565
1628
  const publicClient = makePublicClient({ chainKey: CHAIN });
1566
1629
  // Catch a wrong-network RPC with the clear mismatch message even on dry-run (which still reads
1567
1630
  // the chain to predict the address); tolerate an unreachable RPC so an offline preview still works.
1568
1631
  await assertChainId(CHAIN, { allowUnreachable: dryRun });
1569
- // Identity guard (mirror cmdDeploy): never write demo placeholders as the public identity.
1570
- if (!flags.name || !flags.symbol) {
1571
- if (!flags.name)
1572
- warn(`no --name → default "${name}" would be the on-chain collection name`);
1573
- if (!flags.symbol)
1574
- warn(`no --symbol → default "${symbol}" would be the on-chain symbol`);
1575
- if (!dryRun && !flags.yes) {
1576
- throw new Error('refusing to write demo placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM (or --yes).');
1577
- }
1578
- }
1632
+ // (identity guard already ran above, via the shared assertRealIdentity before any RPC)
1579
1633
  // Mint timing: mint-all → the whole series; mint-count N → the first N; else deferred.
1580
1634
  const mintCount = flags['mint-all'] !== undefined ? count : flags['mint-count'] ? Number(flags['mint-count']) : 0;
1581
1635
  if (mintCount > count)
@@ -1619,6 +1673,8 @@ async function cmdDeploySeries(flags) {
1619
1673
  return;
1620
1674
  }
1621
1675
  factory = existing;
1676
+ if (!(await previewFactoryLive(publicClient, factory, 'Series factory')))
1677
+ return;
1622
1678
  info(`would reuse canonical Series factory ${factory}`);
1623
1679
  }
1624
1680
  else {
@@ -2080,18 +2136,11 @@ async function cmdPredict(flags) {
2080
2136
  info(dim('(this is the 1/1 lane; a Series/code drop uses a different factory → a different address — pass --dir / --script to predict those, or use that command\'s --dry-run)'));
2081
2137
  console.log('');
2082
2138
  }
2083
- // A `--remote [url]` flag targets a HOSTED resolver's admin control plane instead of
2084
- // this machine's local projection. Bare `--remote` falls back to ABX_PUBLIC_BASE_URL
2085
- // (the URL baked on-chain). Returns null for a local op (the default).
2086
- function remoteTarget(flags) {
2087
- const v = flags.remote;
2088
- if (v === undefined)
2089
- return null;
2090
- const base = v !== 'true' ? v : process.env.ABX_PUBLIC_BASE_URL;
2091
- if (!base) {
2092
- throw new Error('`--remote` needs a resolver URL: pass `--remote https://host` or set ABX_PUBLIC_BASE_URL in .env');
2093
- }
2094
- return base;
2139
+ // The `--remote <name|url>` target (remote.ts owns the convention): a named remote's
2140
+ // `ABX_REMOTE_<NAME>_URL/_TOKEN`, an ad-hoc URL, or bare `--remote` = the self-host default.
2141
+ // Returns null for a local op (the default).
2142
+ function remoteFlag(flags) {
2143
+ return resolveRemote(flags.remote, flags['remote-token']);
2095
2144
  }
2096
2145
  /**
2097
2146
  * Ensure a resolver admin token exists locally, generating + persisting one to `.env`
@@ -2124,22 +2173,13 @@ function ensureEffectsToken() {
2124
2173
  process.env.ABX_EFFECTS_TOKEN = token;
2125
2174
  return { token, generated: true };
2126
2175
  }
2127
- /** The shared secret that authorizes remote indexing control (never on-chain signing). */
2128
- function requireAdminToken() {
2129
- const t = process.env.ABX_RESOLVER_ADMIN_TOKEN;
2130
- if (!t) {
2131
- throw new Error('remote ops need ABX_RESOLVER_ADMIN_TOKEN in your .env — it must match the token set on the resolver ' +
2132
- '(`abx deploy-resolver` generates one and wires both sides).');
2133
- }
2134
- return t;
2135
- }
2136
2176
  // ── add ──────────────────────────────────────────────────────────────────────
2137
2177
  // Register + index a project this node didn't deploy. LOCAL by default (this
2138
2178
  // machine's store); `--remote [url]` instead tells a HOSTED resolver to index it —
2139
2179
  // the bridge a local deploy can't make on its own (separate projection stores).
2140
2180
  async function cmdAdd(address, flags) {
2141
2181
  if (!address || address.startsWith('--')) {
2142
- console.error('usage: abx add <address> [--from-block N] [--factory 0x..] [--label "..."] [--remote [url]]\n');
2182
+ console.error('usage: abx add <address> [--from-block N] [--factory 0x..] [--label "..."] [--remote [name|url]]\n');
2143
2183
  process.exit(1);
2144
2184
  }
2145
2185
  // `--attributes` is lane-aware here exactly as at deploy: a PER-TOKEN payload edits a Series'
@@ -2153,10 +2193,10 @@ async function cmdAdd(address, flags) {
2153
2193
  if (flags.traits)
2154
2194
  flagTraits.push(...parseTraitPairs(flags.traits));
2155
2195
  const editedTokenAttributes = perTokenEdit ? parseSeriesTraitsById(attrRaw) : undefined;
2156
- const remote = remoteTarget(flags);
2196
+ const remote = remoteFlag(flags);
2157
2197
  if (remote) {
2158
- const token = requireAdminToken();
2159
- // Bridge what a hosted resolver can't derive itself: the off-chain traits and the durable
2198
+ requireRemoteToken(remote);
2199
+ // Bridge what a remote resolver can't derive itself: the off-chain traits and the durable
2160
2200
  // content locators (ipfs://…). Prefer flags; otherwise forward what the LOCAL deploy stored
2161
2201
  // (the local registration), and compute locators from this machine's content index if needed.
2162
2202
  const localReg = new SelfHostIndexer().store.getRegistration(address);
@@ -2165,7 +2205,7 @@ async function cmdAdd(address, flags) {
2165
2205
  : localReg?.attributes
2166
2206
  ? normalizeAttributes(JSON.parse(localReg.attributes))
2167
2207
  : undefined;
2168
- // Bridge a Series' per-token off-chain traits to the hosted resolver (the resolver has no other
2208
+ // Bridge a Series' per-token off-chain traits to the remote resolver (the resolver has no other
2169
2209
  // way to derive them — they're operator metadata, not chain state). A fresh per-token `--attributes`
2170
2210
  // EDITS them; otherwise forward what the LOCAL deploy stored. Best-effort parse.
2171
2211
  let tokenAttributes;
@@ -2191,6 +2231,7 @@ async function cmdAdd(address, flags) {
2191
2231
  // (Its ABSENCE here was the bug: a hosted resolver defaulted to genesis and scanned the whole
2192
2232
  // chain.) Re-sending the same floor stays incremental server-side, so a nudge ≠ a re-scan.
2193
2233
  const body = {
2234
+ chainId: resolveChain(CHAIN).id,
2194
2235
  address,
2195
2236
  fromBlock: await resolveScanFloor(address, localReg?.fromBlock, flags),
2196
2237
  factory: await detectCanonicalFactory(address, flags.factory, localReg?.factory),
@@ -2202,12 +2243,18 @@ async function cmdAdd(address, flags) {
2202
2243
  contentLocators: Object.keys(contentLocators).length ? contentLocators : undefined,
2203
2244
  full: flags.full ? true : undefined,
2204
2245
  };
2205
- info(`${bold('REMOTE')} → ${remote} ${dim('(registering with the hosted resolver — NOT this machine)')}`);
2246
+ info(`${bold('REMOTE')} → ${remote.url} ${dim('(registering with the remote resolver — NOT this machine)')}`);
2206
2247
  if (body.contentLocators)
2207
2248
  info(`bridging image locator → ${Object.values(body.contentLocators)[0]} ${dim('(so the resolver points at IPFS, not its own localhost)')}`);
2208
- const r = await remoteAddProject(remote, token, body);
2249
+ let r;
2250
+ try {
2251
+ r = await serviceClient(remote).registerProject(body);
2252
+ }
2253
+ catch (err) {
2254
+ throw describeRemoteError(err, remote, 'remote add');
2255
+ }
2209
2256
  ok(`remote resolver indexed ${r.project.name ?? address}: ${r.project.eventCount} events ${dim(`(${r.mode}, ${r.elapsedMs}ms)`)}`);
2210
- info(`it now serves ${remote.replace(/\/+$/, '')}/t/${resolveChain(CHAIN).id}/${address.toLowerCase()}/0`);
2257
+ info(`it now serves ${remote.url}/t/${body.chainId}/${address.toLowerCase()}/0`);
2211
2258
  return;
2212
2259
  }
2213
2260
  allowLargeScan(flags);
@@ -2790,17 +2837,7 @@ async function cmdDeployCode(flags) {
2790
2837
  const dryRun = !!flags['dry-run'];
2791
2838
  const name = flags.name ?? 'ABX Code';
2792
2839
  const symbol = flags.symbol ?? 'ABXC';
2793
- // Identity guard (mirror deploy/deploy-series): never write demo placeholders as the public
2794
- // on-chain identity. dry-run only warns so a preview still runs without --name/--symbol.
2795
- if (!flags.name || !flags.symbol) {
2796
- if (!flags.name)
2797
- warn(`no --name → default "${name}" would be the on-chain collection name`);
2798
- if (!flags.symbol)
2799
- warn(`no --symbol → default "${symbol}" would be the on-chain symbol`);
2800
- if (!dryRun && !flags.yes) {
2801
- throw new Error('refusing to write demo placeholders as your public on-chain identity — pass --name "Your Title" --symbol SYM (or --yes).');
2802
- }
2803
- }
2840
+ assertRealIdentity(flags, { name, symbol, dryRun });
2804
2841
  const maxProvided = flags.max !== undefined;
2805
2842
  const max = Number(flags.max ?? 16);
2806
2843
  if (!Number.isInteger(max) || max <= 0)
@@ -3641,7 +3678,7 @@ async function loadEffects() {
3641
3678
  */
3642
3679
  async function cmdRender(address, tokenIds, flags) {
3643
3680
  if (!address || address.startsWith('--')) {
3644
- console.error('usage: abx render <address> [tokenId…] [--force] [--remote [url]] (missing stills/traits; --force re-renders an existing one — abx render --help)\n');
3681
+ console.error('usage: abx render <address> [tokenId…] [--force] [--remote [name|url]] (missing stills/traits; --force re-renders an existing one — abx render --help)\n');
3645
3682
  process.exit(1);
3646
3683
  }
3647
3684
  // Token ids are decimal (mint order). Keep only those — `parseFlags` leaves `--flag value` pairs
@@ -3678,12 +3715,12 @@ async function cmdRender(address, tokenIds, flags) {
3678
3715
  ok(line);
3679
3716
  return;
3680
3717
  }
3681
- // --remote publishes each render to the hosted resolver's control plane (the locator bridge) so a
3718
+ // --remote publishes each render to the remote resolver's control plane (the locator bridge) so a
3682
3719
  // laptop render lands on a resolver that doesn't share this disk; republish=true makes a re-run
3683
3720
  // restore a resolver that lost its volume without re-rendering. Local (no --remote): shared backend.
3684
- const remote = remoteTarget(flags);
3685
- const resolverUrl = (remote ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
3686
- const adminToken = remote ? requireAdminToken() : undefined;
3721
+ const remote = remoteFlag(flags);
3722
+ const resolverUrl = (remote?.url ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
3723
+ const adminToken = remote ? requireRemoteToken(remote) : undefined;
3687
3724
  // Co-located (no --remote): record each declared output into the shared store's artifact
3688
3725
  // registry so the local resolver's `artifacts` manifest enumerates it. Remote: the publish
3689
3726
  // lane (adminToken) records rows on the hosted resolver instead.
@@ -3718,8 +3755,11 @@ async function cmdRender(address, tokenIds, flags) {
3718
3755
  * single time and exits (vs `abx render <addr>` which is the per-project repair lane).
3719
3756
  */
3720
3757
  async function cmdEffects(flags) {
3721
- const resolverUrl = (remoteTarget(flags) ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
3722
- const adminToken = process.env.ABX_RESOLVER_ADMIN_TOKEN;
3758
+ const effectsRemote = remoteFlag(flags);
3759
+ const resolverUrl = (effectsRemote?.url ?? process.env.ABX_RESOLVER_URL ?? resolveBaseUrl()).replace(/\/$/, '');
3760
+ // Token optional BY DESIGN: no token = the co-located topology (shared store, no publish lane).
3761
+ // A named remote brings its own token; otherwise the self-host env token.
3762
+ const adminToken = effectsRemote?.token ?? process.env.ABX_RESOLVER_ADMIN_TOKEN;
3723
3763
  // Co-located (no admin token — we share the resolver's store): record each declared output into
3724
3764
  // the shared artifact registry so the resolver's `artifacts` manifest enumerates it. With an
3725
3765
  // admin token, the publish lane records rows on the hosted resolver instead.
@@ -3759,15 +3799,21 @@ async function cmdEffects(flags) {
3759
3799
  await new Promise(() => { }); // block like `serve`
3760
3800
  }
3761
3801
  async function cmdIndex(address, flags) {
3762
- const remote = remoteTarget(flags);
3802
+ const remote = remoteFlag(flags);
3763
3803
  if (remote) {
3764
3804
  if (!address || address.startsWith('--')) {
3765
- console.error('usage: abx index <address> --remote [url] (re-index one project on a hosted resolver)\n');
3805
+ console.error('usage: abx index <address> --remote [name|url] (re-index one project on a remote resolver)\n');
3766
3806
  process.exit(1);
3767
3807
  }
3768
- const token = requireAdminToken();
3769
- info(`${bold('REMOTE')} → ${remote} ${dim('(re-indexing on the hosted resolver — the post-deploy nudge)')}`);
3770
- const r = await remoteAddProject(remote, token, { address, full: flags.full ? true : undefined });
3808
+ requireRemoteToken(remote);
3809
+ info(`${bold('REMOTE')} → ${remote.url} ${dim('(re-indexing on the remote resolver — the post-deploy nudge)')}`);
3810
+ let r;
3811
+ try {
3812
+ r = await serviceClient(remote).registerProject({ chainId: resolveChain(CHAIN).id, address, full: flags.full ? true : undefined });
3813
+ }
3814
+ catch (err) {
3815
+ throw describeRemoteError(err, remote, 'remote index');
3816
+ }
3771
3817
  ok(`remote resolver re-indexed ${r.project.name ?? address}: ${r.project.eventCount} events ${dim(`(${r.mode}, ${r.elapsedMs}ms)`)}`);
3772
3818
  return;
3773
3819
  }
@@ -3798,9 +3844,9 @@ async function cmdVerify(address, flags) {
3798
3844
  process.exit(1);
3799
3845
  }
3800
3846
  allowLargeScan(flags);
3801
- const remote = remoteTarget(flags);
3847
+ const remote = remoteFlag(flags);
3802
3848
  if (remote)
3803
- return cmdVerifyRemote(address, remote);
3849
+ return cmdVerifyRemote(address, remote.url);
3804
3850
  const indexer = new SelfHostIndexer();
3805
3851
  let state = indexer.getProject(address);
3806
3852
  if (indexer.store.getRegistration(address)) {
@@ -4072,7 +4118,12 @@ async function cmdTokenUri(address, flags) {
4072
4118
  catch {
4073
4119
  const code = await publicClient.getCode({ address }).catch(() => undefined);
4074
4120
  if (!code || code === '0x') {
4075
- console.error(`abx tokenuri: no contract at ${address} on ${CHAIN} double-check the address and that you're on the right chain. If you JUST deployed, give the tx a block or two to mine.\n`);
4121
+ // Name the endpoint we actually asked. "No contract here" is indistinguishable from "you're
4122
+ // pointed at the wrong node", and a chain KEY doesn't disambiguate that — two endpoints can
4123
+ // both claim `sepolia` (a fork, a stale duplicate .env line) and only one has your contract.
4124
+ console.error(`abx tokenuri: no contract at ${address} on ${CHAIN} (asked ${redactRpcUrl(resolveRpcUrl(CHAIN))}) — ` +
4125
+ `double-check the address, and that this endpoint is the network you deployed to. ` +
4126
+ `If you JUST deployed, give the tx a block or two to mine.\n`);
4076
4127
  }
4077
4128
  else {
4078
4129
  console.error(`abx tokenuri: ${address} didn't return a tokenURI for token ${tokenId} — it may not be an ABX/ERC-721 token, token ${tokenId} may be unminted (try --token <id>), or — on a large on-chain tokenURI — an unauthenticated RPC read hit its gas cap (try a wallet-connected / high-gas RPC).\n`);
@@ -4415,15 +4466,24 @@ async function cmdStatus() {
4415
4466
  // test/junk deploys. On-chain data is untouched; `abx add` can re-register it.
4416
4467
  async function cmdForget(address, flags) {
4417
4468
  if (!address || address.startsWith('--')) {
4418
- console.error('usage: abx forget <address> [--remote [url]]\n');
4469
+ console.error('usage: abx forget <address> [--remote [name|url]]\n');
4419
4470
  process.exit(1);
4420
4471
  }
4421
- const remote = remoteTarget(flags);
4472
+ const remote = remoteFlag(flags);
4422
4473
  if (remote) {
4423
- const token = requireAdminToken();
4424
- info(`${bold('REMOTE')} → ${remote} ${dim('(deregistering on the hosted resolver — NOT this machine)')}`);
4425
- await remoteRemoveProject(remote, token, address);
4426
- ok(`remote resolver forgot ${address} — it will stop serving it. On-chain data is untouched.`);
4474
+ requireRemoteToken(remote);
4475
+ info(`${bold('REMOTE')} → ${remote.url} ${dim('(deregistering on the remote resolver — NOT this machine)')}`);
4476
+ let removed;
4477
+ try {
4478
+ ({ removed } = await serviceClient(remote).removeProject(resolveChain(CHAIN).id, address));
4479
+ }
4480
+ catch (err) {
4481
+ throw describeRemoteError(err, remote, 'remote forget');
4482
+ }
4483
+ if (removed)
4484
+ ok(`remote resolver forgot ${address} — it will stop serving it. On-chain data is untouched.`);
4485
+ else
4486
+ console.log(dim(` ${address} wasn't registered on ${remote.url} — nothing to forget.`));
4427
4487
  return;
4428
4488
  }
4429
4489
  const indexer = new SelfHostIndexer();
@@ -4434,6 +4494,94 @@ async function cmdForget(address, flags) {
4434
4494
  indexer.store.deregister(address);
4435
4495
  ok(`forgot ${address} — dropped its registration + projection. On-chain data is untouched.`);
4436
4496
  }
4497
+ // ── remote ────────────────────────────────────────────────────────────────--
4498
+ // Inspect a remote service. Bare `abx remote` lists the named remotes configured in .env plus the
4499
+ // self-host default pair (URLs + whether a token is set — never the secret itself). With a target,
4500
+ // fetches its PUBLIC service descriptor (what it serves: interfaces, chains, auth, managed
4501
+ // rendering) and — when a token resolves — lists the projects visible to that token, which makes
4502
+ // this the one-command "is my provider key valid?" check. Read-only; registers nothing.
4503
+ async function cmdRemote(spec, flags) {
4504
+ if (!spec || spec.startsWith('--')) {
4505
+ const remotes = listConfiguredRemotes();
4506
+ console.log(`\n ${bold('named remotes')} ${dim('(ABX_REMOTE_<NAME>_URL/_TOKEN in .env — inspect one: abx remote <name>)')}`);
4507
+ if (remotes.length === 0)
4508
+ console.log(dim(' none configured'));
4509
+ for (const r of remotes) {
4510
+ console.log(` ${g('●')} ${r.name.toLowerCase()} ${dim(r.url)} ${r.hasToken ? g('token set') : dim('no token')}`);
4511
+ }
4512
+ // A near-miss var reads as "no token" while the value is sitting in .env under the wrong name.
4513
+ for (const bad of misnamedRemoteVars()) {
4514
+ warn(`${bad.key} isn't a recognized remote var — the convention is ${bold(bad.suggestion)} (only _URL and _TOKEN are read).`);
4515
+ }
4516
+ const def = process.env.ABX_PUBLIC_BASE_URL ?? process.env.ABX_RESOLVER_URL;
4517
+ console.log(`\n ${bold('self-host default')} ${dim('(bare --remote)')}`);
4518
+ console.log(def
4519
+ ? ` ${g('●')} ${def} ${process.env.ABX_RESOLVER_ADMIN_TOKEN ? g('token set') : dim('no ABX_RESOLVER_ADMIN_TOKEN')}`
4520
+ : dim(' none (set ABX_PUBLIC_BASE_URL in .env)'));
4521
+ console.log('');
4522
+ return;
4523
+ }
4524
+ const target = resolveRemote(spec, flags['remote-token']);
4525
+ if (!target)
4526
+ return;
4527
+ const client = serviceClient(target);
4528
+ console.log(`\n ${bold(target.name ? `remote ${target.name.toLowerCase()}` : 'remote')} ${dim(`→ ${target.url}`)}`);
4529
+ let d;
4530
+ try {
4531
+ d = await client.descriptor();
4532
+ }
4533
+ catch (err) {
4534
+ // Two very different situations, and the fix differs — so don't nest the raw client error
4535
+ // (it repeats the URL and leaks `GET`/`fetch failed` at a creator).
4536
+ const status = err instanceof AbxServiceError ? err.status : -1;
4537
+ if (status === 0) {
4538
+ throw new Error(`nothing responded at ${target.url} — check the address. A provider gives you an https:// base ` +
4539
+ `(e.g. https://meta.provider.xyz); if it's your own node, is it running?`);
4540
+ }
4541
+ throw new Error(`${target.url} answered, but serves no ABX service descriptor at /.well-known/abx-service. ` +
4542
+ `That's either an older self-hosted node (fine if it's yours — the remote commands still work against it) ` +
4543
+ `or not an ABX service at all. Verify the URL before registering anything with it.`);
4544
+ }
4545
+ info(`service ${d.service?.name ?? '—'} ${dim(d.service?.version ?? '')}`);
4546
+ info(`serves ${(d.interfaces ?? []).join(' · ') || '—'}`);
4547
+ const chainId = resolveChain(CHAIN).id;
4548
+ const coversChain = (d.chains ?? []).includes(chainId);
4549
+ info(`chains ${(d.chains ?? []).join(', ') || '—'} ${coversChain ? g(`✓ covers ${CHAIN} (${chainId})`) : `${c.orange}⚠${c.reset} does NOT cover ${CHAIN} (${chainId}) — registrations will be refused`}`);
4550
+ if (d.render?.attached) {
4551
+ const outputs = d.render.effects?.flatMap((e) => e.outputs.map((o) => `${e.key}/${o.key}`)).join(', ');
4552
+ info(`rendering managed behind this service${outputs ? ` (${outputs})` : d.render.effects === null ? dim(' (attached — runner unverified right now)') : ''} — code drops need no effects runner here`);
4553
+ }
4554
+ if (d.auth) {
4555
+ info(`auth bearer${d.auth.signupUrl ? ` · get a key: ${d.auth.signupUrl}` : ''}${d.auth.docsUrl ? ` · docs: ${d.auth.docsUrl}` : ''}`);
4556
+ }
4557
+ else {
4558
+ info(`auth none advertised ${dim('(control plane disabled on this node)')}`);
4559
+ }
4560
+ if (!target.token) {
4561
+ // Same near-miss check the register path does — this is where someone lands FIRST when their
4562
+ // credential is set under a name the CLI doesn't read, so the hint has to be here too.
4563
+ const nearMiss = misnamedRemoteVars().find((v) => v.suggestion === target.tokenVar);
4564
+ if (nearMiss)
4565
+ warn(`${nearMiss.key} is set but is NOT read — the convention is ${bold(target.tokenVar)} (only _URL and _TOKEN). Rename it and re-run.`);
4566
+ else
4567
+ info(dim(`no token resolved (set ${target.tokenVar} or pass --remote-token) — descriptor only; can't list your projects.`));
4568
+ console.log('');
4569
+ return;
4570
+ }
4571
+ try {
4572
+ const projects = await client.listProjects();
4573
+ ok(`token accepted — ${projects.length} project(s) visible to it`);
4574
+ for (const p of projects.slice(0, 10)) {
4575
+ console.log(` ${g('●')} ${p.name ?? p.label ?? p.address} ${dim(`${p.address} · ${p.tokenCount ?? '?'} token(s)`)}`);
4576
+ }
4577
+ if (projects.length > 10)
4578
+ console.log(dim(` … and ${projects.length - 10} more`));
4579
+ }
4580
+ catch (err) {
4581
+ throw describeRemoteError(err, target, 'remote list');
4582
+ }
4583
+ console.log('');
4584
+ }
4437
4585
  // ── migrate ───────────────────────────────────────────────────────────────--
4438
4586
  // Move a contract's OFF-CHAIN operator state from one resolver to another (e.g. fly.io →
4439
4587
  // a droplet). The destination replays all ON-CHAIN state from chain itself; this bridges
@@ -4442,13 +4590,17 @@ async function cmdForget(address, flags) {
4442
4590
  // and write through the admin control plane. It does NOT cut over — after a clean migration the
4443
4591
  // operator re-points DNS (custom domain) or the on-chain base URI (provider endpoint).
4444
4592
  async function cmdMigrate(address, flags) {
4445
- const from = typeof flags.from === 'string' ? flags.from : undefined;
4446
- const to = typeof flags.to === 'string' ? flags.to : undefined;
4447
- if (!address || address.startsWith('--') || !from || !to) {
4448
- console.error('usage: abx migrate <address> --from <source-resolver-url> --to <dest-resolver-url> [--from-block N]\n');
4593
+ // Both sides accept a named remote or a URL. Only the DESTINATION needs a credential — the
4594
+ // source is read via its PUBLIC api (the exit ramp works with zero provider cooperation).
4595
+ const fromTarget = typeof flags.from === 'string' ? resolveRemote(flags.from) : null;
4596
+ const toTarget = typeof flags.to === 'string' ? resolveRemote(flags.to, flags['remote-token']) : null;
4597
+ if (!address || address.startsWith('--') || !fromTarget || !toTarget) {
4598
+ console.error('usage: abx migrate <address> --from <source-resolver name|url> --to <dest-resolver name|url> [--from-block N]\n');
4449
4599
  process.exit(1);
4450
4600
  }
4451
- const token = requireAdminToken();
4601
+ requireRemoteToken(toTarget);
4602
+ const from = fromTarget.url;
4603
+ const to = toTarget.url;
4452
4604
  const chainId = resolveChain(CHAIN).id;
4453
4605
  allowLargeScan(flags);
4454
4606
  console.log(bold(`\n abx migrate ${dim('— port off-chain state between resolvers (no cutover)')}`));
@@ -4505,6 +4657,7 @@ async function cmdMigrate(address, flags) {
4505
4657
  step('Populate the destination');
4506
4658
  const locCount = Object.keys(plan.contentLocators).length;
4507
4659
  const body = {
4660
+ chainId,
4508
4661
  address,
4509
4662
  // The deploy block (locally known); if somehow absent, omit it so the destination derives it
4510
4663
  // rather than scanning from genesis (never bake a from-0 floor into a fresh resolver).
@@ -4517,8 +4670,14 @@ async function cmdMigrate(address, flags) {
4517
4670
  contentLocators: locCount ? plan.contentLocators : undefined,
4518
4671
  full: true, // first registration on the destination — replay from the deploy block
4519
4672
  };
4520
- info(`${bold('REMOTE')} → ${to} ${dim('(admin control plane — chain replay + off-chain enrichment)')}`);
4521
- const r = await remoteAddProject(to, token, body);
4673
+ info(`${bold('REMOTE')} → ${to} ${dim('(control plane — chain replay + off-chain enrichment)')}`);
4674
+ let r;
4675
+ try {
4676
+ r = await serviceClient(toTarget).registerProject(body);
4677
+ }
4678
+ catch (err) {
4679
+ throw describeRemoteError(err, toTarget, 'migrate destination');
4680
+ }
4522
4681
  ok(`destination indexed ${r.project.name ?? address}: ${r.project.eventCount} events ${dim(`(${r.mode}, ${r.elapsedMs}ms)`)}`);
4523
4682
  // 5) Parity check — does the destination now serve the same metadata as the source? Sample a
4524
4683
  // token we did NOT re-pin (a re-pinned image is durable-locator-on-dest vs old-host-on-source
@@ -4854,7 +5013,8 @@ async function cmdDeployResolver(flags) {
4854
5013
  const url = art.baseUrl;
4855
5014
  info(`abx deploy --image <art> --name … --public-base-url ${url} (or export ABX_PUBLIC_BASE_URL=${url})`);
4856
5015
  info(`the contract derives ${url}/t/${resolveChain(CHAIN).id}/{address}/{tokenId} from that base.`);
4857
- info(`${bold('then')} abx add <clone> --remote ${dim('# tell the hosted resolver to index it — a LOCAL deploy does NOT')}`);
5016
+ info(`${bold('then')} abx add <clone> --remote ${dim('# tell the remote resolver to index it — a LOCAL deploy does NOT')}`);
5017
+ info(dim(`prefer addressing it by name? add ABX_REMOTE_<NAME>_URL=${url} (+ ABX_REMOTE_<NAME>_TOKEN=<the same token>) to .env → abx add <clone> --remote <name>`));
4858
5018
  console.log('');
4859
5019
  }
4860
5020
  // ── deploy-effects: scaffold the render runner (the resolver's browser-bearing companion) ─────
@@ -5153,8 +5313,9 @@ const COMMAND_HELP = {
5153
5313
  No tokenId → sweeps all minted tokens; pass ids (${g('0 1 2')}) to target specific tokens.
5154
5314
  ${g('--force')} RE-RENDER even when the still already exists — the fix for a bad / blank / timed-out capture
5155
5315
  (the art is otherwise deterministic, so a plain render idempotent-skips an existing still). Overwrites it (+ republishes on --remote).
5156
- ${g('--remote [url]')} publish each render to a HOSTED resolver (the locator bridge: upload to ${bold('ABX_STORAGE_BACKEND')}, POST /admin/effect-artifacts).
5157
- Idempotent; a re-run restores a resolver that lost its volume. Needs ${g('ABX_RESOLVER_ADMIN_TOKEN')}.
5316
+ ${g('--remote [name|url]')} publish each render to a REMOTE resolver (the locator bridge: upload to ${bold('ABX_STORAGE_BACKEND')}, POST /v1/effect-artifacts).
5317
+ Idempotent; a re-run restores a resolver that lost its volume. Needs its token (a named remote's
5318
+ ${g('ABX_REMOTE_<NAME>_TOKEN')}, else ${g('ABX_RESOLVER_ADMIN_TOKEN')}).
5158
5319
  --effects-url <url> enqueue on a running effect-runner service instead of rendering inline (else ${g('ABX_EFFECTS_URL')})
5159
5320
  ${dim('Inline (no --remote) renders on THIS machine (needs `npx playwright install chromium` once), pointing Chromium at the live')}
5160
5321
  ${dim(`view of ${g('ABX_RESOLVER_URL')} (else your configured base / local ${g('abx serve')}), and stores to the resolved backend.`)}
@@ -5170,8 +5331,8 @@ const COMMAND_HELP = {
5170
5331
  --port <n> HTTP port (default ${g('ABX_EFFECTS_PORT')} / 8788) — ${g('POST /notify')} enqueues (watcher lane) · ${g('POST /run')} sweeps synchronously (command lane)
5171
5332
  --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
5172
5333
  --concurrency <n> parallel renders while draining (default ${g('ABX_EFFECTS_CONCURRENCY')} / 1 — Chromium is heavy; raise deliberately)
5173
- ${dim('Co-located with a local `abx serve` (same store) → no tokens needed. Against a HOSTED resolver: ABX_RESOLVER_URL +')}
5174
- ${dim('ABX_RESOLVER_ADMIN_TOKEN (publish renders + report status). PUBLIC runner? set ABX_EFFECTS_TOKEN — it gates /run + /notify.')}
5334
+ ${dim('Co-located with a local `abx serve` (same store) → no tokens needed. Against a REMOTE resolver: --remote <name|url> (its')}
5335
+ ${dim('token publishes renders + reports status), or ABX_RESOLVER_URL + ABX_RESOLVER_ADMIN_TOKEN. PUBLIC runner? set ABX_EFFECTS_TOKEN — it gates /run + /notify.')}
5175
5336
  ${dim('To HOST the runner (fly/docker), use `abx deploy-effects`.')}`,
5176
5337
  'configure-param': `
5177
5338
  ${bold('abx configure-param')} <address> <tokenId|-> <key> <value> ${dim('— set a PostParam (typed, canonical encode). Sends a tx.')}
@@ -5182,7 +5343,7 @@ const COMMAND_HELP = {
5182
5343
  like ${g('params.keys')} / ${g('display.gateway')}. ≤31 printable-ASCII chars ride as a literal bytes32; longer takes the data path.
5183
5344
  After a token write, a project whose ${g('params.keys')} doesn't list the key gets a one-line fix suggestion.
5184
5345
  --file <path> read the value from a file (String / Bytes payloads)
5185
- ${g('--remote [url]')} nudge a HOSTED resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
5346
+ ${g('--remote [name|url]')} nudge a REMOTE resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
5186
5347
  the resolver's effect runner, so the thumbnail re-renders without waiting. Usually OPTIONAL now: a
5187
5348
  resolver running the chain watcher (the ${g('abx serve')} default) sees the change on its next poll (~12s)
5188
5349
  and auto-re-renders on its own. Keep --remote for a watcher-disabled resolver or when seconds matter.
@@ -5366,13 +5527,16 @@ const COMMAND_HELP = {
5366
5527
  ${bold('abx add')} <address> ${dim('— register + index a project. Also edits off-chain display metadata + traits.')}
5367
5528
  --from-block <n> --factory 0x.. --label "<s>" --description "<s>" --external-url <url> [--full] [--yes]
5368
5529
  --traits "K=V; K2=V2" / --attributes <file.json> set the off-chain operator traits (on-chain attributes always win)
5369
- ${g('--remote [url]')} target a HOSTED resolver instead of this machine — bridges the image locator (ipfs://…) + traits to it`,
5530
+ ${g('--remote [name|url]')} target a REMOTE resolver instead of this machine — bridges the image locator (ipfs://…) + traits to it.
5531
+ A ${bold('name')} reads ${g('ABX_REMOTE_<NAME>_URL')} + ${g('ABX_REMOTE_<NAME>_TOKEN')} from .env (a managed provider's API key);
5532
+ a URL (or bare --remote = ${g('ABX_PUBLIC_BASE_URL')}) uses ${g('ABX_RESOLVER_ADMIN_TOKEN')}. Inspect first: ${g('abx remote <name>')}
5533
+ --remote-token <t> override the token for this invocation (--token means a token ID elsewhere, hence the name)`,
5370
5534
  index: `
5371
5535
  ${bold('abx index')} [<address>] ${dim('— re-index from chain (read-only). Incremental by default.')}
5372
5536
  ${g('--full')} force a full replay from the deploy block (the durability proof) --yes allow a very large scan`,
5373
5537
  verify: `
5374
5538
  ${bold('abx verify')} <address> ${dim('— re-hash the served bytes against the on-chain commitment (read-only; no server).')}
5375
- ${g('--remote [url]')} verify what a HOSTED resolver actually serves (else ABX_PUBLIC_BASE_URL) — probes its \`/image\` (302→locator
5539
+ ${g('--remote [name|url]')} verify what a REMOTE resolver actually serves (else ABX_PUBLIC_BASE_URL) — probes its \`/image\` (302→locator
5376
5540
  or 200 bytes), so it accounts for a render PUBLISHED to that resolver. ${bold('Use this for a code project whose')}
5377
5541
  ${bold('renders were published to a hosted resolver')} — a plain \`abx verify\` only checks THIS machine's store and will
5378
5542
  report a false placeholder for a render that lives on the resolver.
@@ -5382,11 +5546,21 @@ const COMMAND_HELP = {
5382
5546
  ${bold('abx tokenuri')} <address> [--token <id>] ${dim('— read tokenURI(id) straight from the contract on-chain + decode the JSON (read-only; no server).')}
5383
5547
  ${dim('The proof a fully on-chain token self-resolves: any RPC returns the renderer-assembled metadata. Default token 0.')}`,
5384
5548
  forget: `
5385
- ${bold('abx forget')} <address> ${dim('— drop a project’s local registration + projection. On-chain data is untouched.')}`,
5549
+ ${bold('abx forget')} <address> ${dim('— drop a project’s local registration + projection. On-chain data is untouched.')}
5550
+ ${g('--remote [name|url]')} deregister on a REMOTE resolver instead (it stops serving the project; re-add any time)`,
5551
+ remote: `
5552
+ ${bold('abx remote')} [<name|url>] ${dim('— inspect a remote service (read-only; registers nothing).')}
5553
+ Bare: list the named remotes in .env (${g('ABX_REMOTE_<NAME>_URL')} / ${g('_TOKEN')} — token shown as set/unset, never printed)
5554
+ plus the self-host default (bare --remote = ${g('ABX_PUBLIC_BASE_URL')} + ${g('ABX_RESOLVER_ADMIN_TOKEN')}).
5555
+ With a target: fetch its PUBLIC ${g('/.well-known/abx-service')} descriptor — what it serves (interfaces), which chains
5556
+ (flags a mismatch with your ${g('ABX_CHAIN')}), whether ${bold('rendering is managed')} behind it (code drops then need no effects
5557
+ runner), and where a human gets an API key (${g('auth.signupUrl')}). With a token: lists the projects visible to it —
5558
+ ${bold('the one-command "is my provider key valid?" check')} (401 = fix the key · 403 = provider-side scoping, not a typo).`,
5386
5559
  migrate: `
5387
5560
  ${bold('abx migrate')} <address> ${dim('— move a contract\'s OFF-CHAIN state to another resolver (read-only on both; no cutover).')}
5388
- --from <url> the SOURCE resolver (currently serving the contract)
5389
- --to <url> the DESTINATION resolver (its admin control plane) — needs ${g('ABX_RESOLVER_ADMIN_TOKEN')} in .env (the dest's token, from ${g('deploy-resolver')})
5561
+ --from <name|url> the SOURCE resolver (currently serving the contract) — read via its PUBLIC api; no source credential needed
5562
+ --to <name|url> the DESTINATION resolver (its control plane) — the ONE credential migrate needs: a named remote's
5563
+ ${g('ABX_REMOTE_<NAME>_TOKEN')}, else ${g('ABX_RESOLVER_ADMIN_TOKEN')} (your own node's, from ${g('deploy-resolver')}), else --remote-token
5390
5564
  --from-block <n> chain scan floor for the local read (default: the deploy block, discovered on-chain — never genesis) --yes allow a very large scan
5391
5565
  --backend <id> durable custody for re-pinning source-only images (ipfs · arweave); else config/env
5392
5566
  ${dim('replays on-chain state on the dest from chain, then bridges description / external_url / off-chain')}
@@ -5436,7 +5610,8 @@ function help() {
5436
5610
  --minter 0x.. · --primary-payee 0x.. · --unpaused · ${g('--dry-run')} · see ${g('abx help deploy-series')}
5437
5611
  ${g('abx predict')} pre-compute a deploy address flags: [--salt 0x..] [--for 0x..] [--factory 0x..]
5438
5612
  ${g('abx add')} <address> register + index a project this node didn't deploy
5439
- flags: --from-block --factory --label
5613
+ flags: --from-block --factory --label · ${g('--remote <name|url>')} registers on a REMOTE resolver instead
5614
+ ${g('abx remote')} [<name|url>] inspect a remote service: its descriptor (chains · managed rendering · where to get a key) + your projects there
5440
5615
  ${g('abx index')} [<address>] re-index from chain (incremental by default; ${g('--full')} forces a replay from deploy)
5441
5616
  ${g('abx verify')} <addr> re-hash served bytes vs the on-chain commitment (no server needed)
5442
5617
  ${g('abx tokenuri')} <addr> read tokenURI(0) on-chain + decode the JSON (proof a self-resolving token works)