@artblocks/abx-cli 0.1.0-alpha.2 → 0.1.0-alpha.4

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
@@ -14,6 +14,7 @@
14
14
  * abx demo deploy a 1/1 to Sepolia, index it, and serve it
15
15
  * abx deploy [--image ..] deploy + index a 1/1 (--image to custody your own bytes; --no-mint to defer)
16
16
  * abx predict [--salt ..] pre-compute a deploy address (reserve / vanity it before signing)
17
+ * abx preview run a code project on localhost while it's still being made (no chain)
17
18
  * abx deploy-code deploy a code project (SeriesCode): --script <file> (on-chain template)
18
19
  * or --code-dir <dir> (build directory → ipfs/arweave `code` field)
19
20
  * abx add <address> register + index a project (--remote: on a hosted resolver, not this machine)
@@ -57,6 +58,7 @@ import { composeParamsKeys, expectedChainComplete, hasOnChainUriLane, onchainUri
57
58
  import { parseFlags, unknownFlags } from './flags.js';
58
59
  import { AGENT_SKILL_PARENTS, checkForCliUpdate, compareVersions, installedSkillVersions, readCliVersion, readSkillVersion, SKILL_DIR_NAME, } from './update-check.js';
59
60
  import { analyzeScript, recommendLane } from './inspect.js';
61
+ import { previewConfigFromFlags, previewDepTags, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
60
62
  import { parseSchemaSpecs, describeSchema } from './schema.js';
61
63
  import { parseSeriesTraits, looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
62
64
  /** The `--schema` type/auth/format catalog, shown wherever the CLI nudges `--schema`. Kept accurate
@@ -134,7 +136,7 @@ async function maybeNotifyUpdate(flags) {
134
136
  const latest = await checkForCliUpdate(current);
135
137
  if (latest) {
136
138
  console.error(`\n ${c.orange}⚠${c.reset} update available: ${bold('abx')} ${dim(current)} → ${g(latest)}\n` +
137
- ` upgrade: ${g('npm i -g @artblocks/abx-cli@latest')} ${dim('· or invoke:')} ${g('npx abx@latest <command>')}\n` +
139
+ ` upgrade: ${g('npm i -g @artblocks/abx-cli@latest')} ${dim('· or invoke:')} ${g('npx @artblocks/abx-cli@latest <command>')}\n` +
138
140
  ` release notes: https://github.com/ArtBlocks/abx/releases ${dim('· silence: ABX_NO_UPDATE_CHECK=1')}\n`);
139
141
  }
140
142
  }
@@ -166,6 +168,7 @@ async function main() {
166
168
  case 'deploy-series': return cmdDeploySeries(flags);
167
169
  case 'deploy-code': return cmdDeployCode(flags);
168
170
  case 'inspect': return cmdInspect(rest[0], flags);
171
+ case 'preview': return cmdPreview(flags);
169
172
  case 'scaffold-renderer': return cmdScaffoldRenderer(rest, flags);
170
173
  case 'predict': return cmdPredict(flags);
171
174
  case 'add': return cmdAdd(rest[0], flags);
@@ -984,7 +987,24 @@ async function cmdDeploy(flags, serveAfter) {
984
987
  // lockable) — mirrors the description model. On-chain wins if both ever set the same trait.
985
988
  const traits = parseDeployTraits(flags);
986
989
  console.log(bold(`\n ABX Self-Host Toolkit — ${serveAfter ? 'demo' : 'deploy'}\n ${dim('a project, served from chain alone')}`));
987
- const lane = serveAfter ? 'send' : laneFromFlags(flags); // demo is always hot (one-shot)
990
+ // Signing lane. `demo` used to hard-force the hot lane, which SILENTLY dropped `--sign`: with no
991
+ // env key it died confusingly, and WITH one it signed from that key while the operator had asked
992
+ // for their browser wallet — a signing choke point that ignored the lane it was handed. The demo
993
+ // deploy is a single tx, so the wallet lane works here exactly as it does for `deploy`.
994
+ const lane = laneFromFlags(flags);
995
+ // The cold lane only PRINTS a tx; demo's whole point is to index + serve what it just deployed,
996
+ // and there is nothing to index until someone broadcasts. Refuse the combo instead of doing
997
+ // half the job — `abx deploy --unsigned` is the command for that lane.
998
+ if (serveAfter && lane === 'unsigned') {
999
+ throw new Error('`abx demo --unsigned` can\'t work: the cold lane only prints a transaction, and the demo ' +
1000
+ 'indexes + serves the contract it just deployed. Use `abx demo` (hot key) or `abx demo --sign` ' +
1001
+ '(browser wallet) — or `abx deploy --unsigned` if you only want the raw tx.');
1002
+ }
1003
+ // Same shape for --dry-run: previewing sends nothing, so there is nothing to serve.
1004
+ if (serveAfter && flags['dry-run']) {
1005
+ throw new Error('`abx demo --dry-run` can\'t work: a dry run sends nothing, and the demo indexes + serves what ' +
1006
+ 'it deployed. Use `abx deploy --dry-run` to preview a 1/1 deploy without sending.');
1007
+ }
988
1008
  const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
989
1009
  const publicClient = makePublicClient({ chainKey: CHAIN });
990
1010
  // Verify the RPC really is CHAIN before any send (factory/renderer/staging/deploy). A dry run
@@ -2227,6 +2247,78 @@ async function ensureSeedSource(publicClient) {
2227
2247
  * Schemas: --schema key:Type:Auth[,key:Type:Auth…] (e.g. palette:HexColor:TokenOwner).
2228
2248
  * Seeds: canonical randomizer by default; --no-seed opts out.
2229
2249
  */
2250
+ /**
2251
+ * `abx preview` — serve the program on localhost, live, for as long as the work is being made.
2252
+ *
2253
+ * The studio lane, and deliberately the FIRST thing to reach for on a code project: it renders the
2254
+ * same document the generator serves (real `abx.js`, real tokenData shape, real dependency tags)
2255
+ * with a synthetic seed, so a creator can refresh for new seeds, drive their PostParams from real
2256
+ * inputs, and watch an animated piece actually move — none of which a still-image sweep can show.
2257
+ * No chain, no key, no deploy. `--shoot` renders the same document headlessly for an agent that
2258
+ * can't open a browser.
2259
+ */
2260
+ async function cmdPreview(flags) {
2261
+ warnStrayFlags(flags, PREVIEW_FLAGS, 'preview');
2262
+ const cfg = previewConfigFromFlags(flags);
2263
+ const shootDir = flags.shoot && flags.shoot !== 'true' ? String(flags.shoot) : flags.shoot === 'true' ? 'abx-preview' : undefined;
2264
+ const count = Math.min(Math.max(Number(flags.count ?? 9) || 9, 1), 64);
2265
+ console.log(bold(`\n ABX Self-Host Toolkit — preview\n ${dim('the program, running locally — no chain, no deploy')}`));
2266
+ const { notes } = previewDepTags(cfg.deps);
2267
+ step('Program');
2268
+ info(cfg.source.kind === 'dir' ? `directory build ${cfg.source.path}/ (its own index.html + abx.js)` : `script ${cfg.source.path} ${dim('(re-read from disk on every render)')}`);
2269
+ if (cfg.schemas.length)
2270
+ info(`params: ${cfg.schemas.map(describeSchema).join(' · ')}`);
2271
+ else
2272
+ info(dim('params: none declared — add --schema key:Type:Auth to drive them from the studio'));
2273
+ for (const n of notes)
2274
+ info(`dep ${n}`);
2275
+ // A raw on-chain dependency can't be fetched without a chain, so the preview would render a
2276
+ // sketch missing its runtime and look broken for the wrong reason. Say so rather than let them
2277
+ // debug their own art.
2278
+ if (cfg.deps.some((d) => d.display.startsWith('0x'))) {
2279
+ warn('an on-chain data-contract dep is NOT loaded in preview — the sketch will run without it here. Use a name@version ref to preview against the CDN copy.');
2280
+ }
2281
+ const server = await startPreviewServer(cfg, shootDir ? 0 : Number(flags.port ?? DEFAULT_PREVIEW_PORT));
2282
+ if (shootDir) {
2283
+ step(`Render ${count} seeds headlessly`);
2284
+ try {
2285
+ const shots = await shootPreview(server.url, shootDir, count, {
2286
+ width: Number(flags.width ?? 1000) || 1000,
2287
+ timeoutMs: Number(flags['timeout-ms'] ?? 10_000) || 10_000,
2288
+ });
2289
+ ok(`${shots.length} frames → ${shootDir}/ ${dim('(traits in traits.json)')}`);
2290
+ for (const s of shots) {
2291
+ const t = s.traits && Object.keys(s.traits).length
2292
+ ? Object.entries(s.traits).map(([k, v]) => `${k} ${String(v)}`).join(' · ')
2293
+ : `${c.orange}no traits reported${c.reset}`;
2294
+ console.log(` ${dim(s.seed.slice(0, 10) + '…')} ${t}${s.done ? '' : dim(' (no abx.done())')}`);
2295
+ }
2296
+ const silent = shots.filter((s) => !s.traits || !Object.keys(s.traits).length).length;
2297
+ if (silent === shots.length) {
2298
+ warn('NO frame reported traits — `abx.traits({…})` is the only thing that becomes marketplace `attributes`. Verify with `abx inspect`.');
2299
+ }
2300
+ else if (shots.length > 1 && new Set(shots.map((s) => JSON.stringify(s.traits))).size === 1) {
2301
+ // Only meaningful when traits DID come back: identical values across seeds is the signature
2302
+ // of a sketch that never reads `abx.tokenData.seed` (prototyped on Math.random()), which
2303
+ // deploys as N visually identical tokens. Skipped when nothing reported at all — the
2304
+ // warning above already covers that, and firing both reads as noise.
2305
+ warn('every seed produced identical traits — check the sketch actually reads `abx.tokenData.seed` (the silent "all tokens the same" failure).');
2306
+ }
2307
+ }
2308
+ finally {
2309
+ await server.close();
2310
+ }
2311
+ return;
2312
+ }
2313
+ step('Studio');
2314
+ console.log(`\n ${g('●')} ${bold('preview')} ${server.url}`);
2315
+ console.log(` ${dim('studio ')}${server.url} ${dim('seed + params + live traits')}`);
2316
+ console.log(` ${dim('grid ')}${server.url}/grid ${dim('9 seeds at once, all live')}`);
2317
+ console.log(` ${dim('bare view ')}${server.url}/view ${dim('the generator document itself')}`);
2318
+ console.log(`\n ${dim('Edit the program and refresh — it is re-read from disk. Ctrl-C to stop.')}`);
2319
+ console.log(` ${dim('This is a preview: `abx inspect` is still the wiring check, and a testnet deploy is the faithful end-to-end.')}\n`);
2320
+ await new Promise(() => { }); // block like `serve` — the creator drives it
2321
+ }
2230
2322
  /**
2231
2323
  * `abx inspect <script.js>` — read a generative script and, WITHOUT executing it, report what it
2232
2324
  * needs (traits + their on-chain reproducibility, dependency hints, size → assembled-document size →
@@ -4167,114 +4259,109 @@ async function cmdMigrate(address, flags) {
4167
4259
  }
4168
4260
  // ── doctor ────────────────────────────────────────────────────────────────--
4169
4261
  async function cmdDoctor(flags) {
4170
- console.log(bold('\n abx doctor\n'));
4171
- const check = (label, pass, detail = '') => console.log(` ${pass ? g('✓') : `${c.red}✗${c.reset}`} ${label}${detail ? dim(' ' + detail) : ''}`);
4262
+ console.log(bold('\n abx doctor') + dim(` · ${CHAIN}`) + '\n');
4263
+ // Two visual tiers: PASS/FAIL checks (✓/✗) for things that are either working or broken, and an
4264
+ // "Optional" block (·) for path-dependent setup that is fine to be unset. We deliberately do NOT
4265
+ // use ⚠ for "unset but often fine" — that read as noise; ⚠ is reserved for a real gotcha (a
4266
+ // range-capped RPC). Labels are padded so both tiers align.
4267
+ const CONT = ' '.repeat(17); // continuation indent: aligns under a check/opt detail column
4268
+ const check = (label, pass, detail = '') => console.log(` ${pass ? g('✓') : `${c.red}✗${c.reset}`} ${label.padEnd(13)}${detail ? dim(detail) : ''}`);
4269
+ const opt = (label, detail) => console.log(` ${dim('·')} ${label.padEnd(13)}${dim(detail)}`);
4172
4270
  const hasKey = !!(process.env.ABX_DEPLOYER_PK ?? process.env.SEPOLIA_FUNDED_PK ?? process.env.SEPOLIA_WALLET_PK);
4173
- // Not having an env key is NOT fatal: the wallet lane (`--sign`) signs in the user's own wallet,
4174
- // key-free. Only flag it as a soft note + point at the lanes never imply "paste a private key."
4175
- if (hasKey)
4176
- check('signing key in env', true, 'hot lane ready (env key signs unattended)');
4177
- else
4178
- console.log(` ${c.orange}⚠${c.reset} signing key in env${dim(' none fine if you sign in your own wallet (`--sign`; the key never touches the CLI). Only set ABX_DEPLOYER_PK / SEPOLIA_FUNDED_PK / SEPOLIA_WALLET_PK for hot/unattended signing.')}`);
4271
+ // 1. Agent skill FIRST and prominent. The primary way to use abx is to let a coding agent drive
4272
+ // it, so a missing/stale skill is a ✗: not broken infra, but the main UX isn't set up. Its
4273
+ // version lives in SKILL.md frontmatter (version-locked to this CLI). Notify-only — no exit code.
4274
+ const cliVersion = readCliVersion();
4275
+ const skillVersions = installedSkillVersions();
4276
+ const staleSkills = skillVersions.filter((v) => compareVersions(cliVersion, v) > 0);
4277
+ if (skillVersions.length === 0) {
4278
+ check('agent skill', false, `not installed — run ${g('abx skill install')}`);
4279
+ console.log(`${CONT}${dim('(recommended: let a coding agent drive abx)')}`);
4280
+ }
4281
+ else if (staleSkills.length > 0) {
4282
+ check('agent skill', false, `v${staleSkills.join(', v')} behind CLI v${cliVersion} — run ${g('abx skill install')}`);
4283
+ }
4284
+ else {
4285
+ check('agent skill', true, `in sync (v${cliVersion})`);
4286
+ }
4287
+ console.log('');
4288
+ // 2. Core environment (✓/✗). Signing-wallet balances are computed here (they need the RPC) but
4289
+ // printed in the Optional block below, so buffer them.
4290
+ let signingOpt = null;
4291
+ let forOpt = null;
4179
4292
  try {
4180
4293
  const publicClient = makePublicClient({ chainKey: CHAIN });
4181
4294
  const bn = await publicClient.getBlockNumber();
4182
- check(`RPC reachable (${CHAIN})`, true, `head block ${bn}`);
4183
- // RPC endpointsprobe each for reachability, getLogs range, and archive depth,
4184
- // so the toolkit uses (and recommends) the one fit for the job — and says so if none are.
4295
+ // Collapse the RPC report to one line (best endpoint + head), and only add a ⚠ when there is a
4296
+ // genuine problema range-capped-only set that will grind a resolver under load.
4185
4297
  const probes = await probeRpcEndpoints({ chainKey: CHAIN });
4186
4298
  const usable = probes.filter((pr) => pr.verdict !== 'unusable');
4187
4299
  const best = probes.find((pr) => pr.verdict === 'best') ?? usable[0];
4188
- check(`RPC endpoints for ${CHAIN} (${probes.length})`, usable.length > 0, usable.length > 0
4189
- ? `best for reconstruction: ${best.label} (${best.verdict === 'best' ? 'wide range + archive' : 'usable, range-capped'})`
4190
- : 'none usable for reconstruction');
4191
- for (const pr of probes) {
4192
- const glyph = pr.verdict === 'best' ? g('✓') : pr.verdict === 'capped' ? `${c.orange}⚠${c.reset}` : `${c.red}✗${c.reset}`;
4193
- console.log(` ${glyph} ${pr.label} ${dim('— ' + (pr.verdict === 'best' ? 'wide getLogs range + archive' : pr.reason ?? pr.verdict))}`);
4194
- }
4195
- if (usable.length === 0) {
4196
- console.log(` ${c.orange}↳${c.reset} ${dim('research a current free archive RPC with a wide getLogs range and add it to ABX_RPC_URLS — see the skill’s “Choosing an RPC”')}`);
4197
- }
4198
- else if (!probes.some((pr) => pr.verdict === 'best')) {
4199
- // Every usable endpoint is range-capped. Indexing still works (chunking), but a first
4200
- // reconstruction — and a resolver under marketplace load — is slow + rate-limit-prone. That's
4201
- // a serious infra signal, NOT "a paid plan is required": flag it. A normal deploy→index only
4202
- // scans from the deploy block, so this bites first reconstructions and busy resolvers most.
4203
- console.log(` ${c.orange}⚠ every usable RPC is getLogs-range-capped${c.reset} ${dim('— add a wide-range archive endpoint to ABX_RPC_URLS before running a resolver under load; capped ones grind on wide scans (skill → “Choosing an RPC”).')}`);
4204
- }
4205
- if (hasKey) {
4206
- const { account } = makeWalletClient({ chainKey: CHAIN });
4207
- const bal = await publicClient.getBalance({ address: account.address });
4208
- check('deployer funded', bal > 0n, `${account.address} · ${formatEther(bal)} ETH`);
4209
- }
4210
- // Wallet lane has no env key — let a creator preflight THEIR OWN signing wallet's balance,
4211
- // the one gap where an unfunded wallet otherwise only surfaces at the signing step.
4212
- if (flags.for) {
4213
- const bal = await publicClient.getBalance({ address: flags.for });
4214
- check('wallet funded (--for)', bal > 0n, `${flags.for} · ${formatEther(bal)} ETH${bal > 0n ? '' : ` — ${faucetHint(CHAIN)}`}`);
4300
+ if (usable.length > 0) {
4301
+ check('RPC', true, `${best.label} · head ${bn} · ${best.verdict === 'best' ? 'wide range + archive' : 'range-capped'}`);
4302
+ if (!probes.some((pr) => pr.verdict === 'best')) {
4303
+ console.log(`${CONT}${c.orange}⚠${c.reset}${dim(' every endpoint is getLogs-range-capped — add a wide-range archive RPC to ABX_RPC_URLS before running a resolver under load')}`);
4304
+ }
4215
4305
  }
4216
- else if (!hasKey) {
4217
- console.log(` ${dim(' wallet lane: run `abx doctor --for 0x<your wallet>` to check your signing wallet is funded')}`);
4306
+ else {
4307
+ check('RPC', false, `${CHAIN} no endpoint usable for reconstruction; add a wide-range archive RPC to ABX_RPC_URLS`);
4218
4308
  }
4219
4309
  const factory = factoryAddress();
4220
4310
  if (factory) {
4221
4311
  const code = await publicClient.getCode({ address: factory });
4222
- if (!code || code === '0x') {
4223
- check('canonical factory deployed', false, `${factory} — no code on ${CHAIN}; \`abx deploy\` redeploys`);
4224
- }
4225
- else if (await isCurrentFactory(publicClient, factory)) {
4226
- check('canonical factory deployed', true, factory);
4227
- }
4228
- else {
4229
- check('canonical factory deployed', false, `${factory} — older/incompatible version; \`abx deploy\` redeploys`);
4230
- }
4312
+ if (!code || code === '0x')
4313
+ check('factory', false, `${factory} — no code on ${CHAIN}; \`abx deploy\` redeploys`);
4314
+ else if (await isCurrentFactory(publicClient, factory))
4315
+ check('factory', true, factory);
4316
+ else
4317
+ check('factory', false, `${factory} — older/incompatible; \`abx deploy\` redeploys`);
4231
4318
  }
4232
4319
  else {
4233
- check('canonical factory deployed', false, 'none yet — `abx demo` deploys one');
4320
+ check('factory', false, 'none yet — `abx demo` deploys one');
4321
+ }
4322
+ if (hasKey) {
4323
+ const { account } = makeWalletClient({ chainKey: CHAIN });
4324
+ const bal = await publicClient.getBalance({ address: account.address });
4325
+ signingOpt = `env key ${account.address} · ${bal > 0n ? `funded ${formatEther(bal)} ETH` : `empty — fund it (${faucetHint(CHAIN)})`}`;
4326
+ }
4327
+ if (flags.for) {
4328
+ const bal = await publicClient.getBalance({ address: flags.for });
4329
+ forOpt = `${flags.for} · ${bal > 0n ? `funded ${formatEther(bal)} ETH` : `empty — ${faucetHint(CHAIN)}`}`;
4234
4330
  }
4235
4331
  }
4236
4332
  catch (err) {
4237
- check('RPC reachable', false, err.message);
4333
+ check('RPC', false, err.message);
4238
4334
  }
4239
- // public base URL — baked into on-chain URIs at deploy; unset is fine for a demo
4240
- // but a silent footgun for a real launch, so surface it explicitly.
4241
- const baseUrl = process.env.ABX_PUBLIC_BASE_URL;
4242
- if (baseUrl)
4243
- check('public base URL set (baked into on-chain URIs)', true, baseUrl);
4244
- else
4245
- console.log(` ${c.orange}⚠${c.reset} public base URL${dim(' ABX_PUBLIC_BASE_URL unset → resolver-served deploys REFUSE (localhost on-chain resolves for no one). Set it (or --public-base-url) for a hosted resolver. IRRELEVANT if you go fully on-chain: --onchain-image (1/1) or --onchain-uri (code project) bake no resolver base.')}`);
4246
4335
  // storage backend — resolve it (catches missing config), then probe liveness/creds
4247
4336
  const backendId = activeBackendId();
4248
4337
  try {
4249
4338
  const backend = resolveBackend(storageOptions());
4250
4339
  const h = await backend.health?.();
4251
4340
  if (h)
4252
- check(`storage backend '${backend.id}' reachable`, h.ok, h.detail ?? '');
4341
+ check('storage', h.ok, `${backend.id} · ${h.detail ?? ''}`);
4253
4342
  else
4254
- check(`storage backend '${backend.id}' configured`, true);
4343
+ check('storage', true, `${backend.id} · configured`);
4255
4344
  }
4256
4345
  catch (err) {
4257
- check(`storage backend '${backendId}' configured`, false, err.message);
4346
+ check('storage', false, `${backendId} · ${err.message}`);
4258
4347
  }
4259
- // Managed Turbo/Arweave key if it exists, it holds prepaid credits, so remind them to back it up.
4348
+ // 3. Optionalpath-dependent setup. Unset is fine; these say WHEN you'll need each, so an unset
4349
+ // value never reads as a warning.
4350
+ console.log(`\n ${dim('Optional — depends how you deploy:')}`);
4351
+ if (signingOpt)
4352
+ opt('signing', signingOpt);
4353
+ else
4354
+ opt('signing', 'no env key → sign in your browser wallet (--sign). Preflight yours: `abx doctor --for 0x<addr>`. Set ABX_DEPLOYER_PK for the unattended hot lane.');
4355
+ if (forOpt)
4356
+ opt('wallet --for', forOpt);
4357
+ const baseUrl = process.env.ABX_PUBLIC_BASE_URL;
4358
+ if (baseUrl)
4359
+ opt('resolver URL', baseUrl);
4360
+ else
4361
+ opt('resolver URL', 'ABX_PUBLIC_BASE_URL unset → needed for off-chain/resolver-served deploys; skip if fully on-chain (--onchain-image / --onchain-uri).');
4260
4362
  if (!process.env.ARWEAVE_JWK && existsSync(arweaveKeyFilePath())) {
4261
4363
  const jwk = loadArweaveJwk();
4262
- console.log(` ${dim(' Turbo/Arweave key ' + (jwk ? arweaveAddress(jwk) + ' ' : '') + 'holds your upload credits — back it up: `abx storage backup-key --out <path>`')}`);
4263
- }
4264
- // Agent skill co-versioning — the skill drives this CLI and is version-locked to it (its version
4265
- // lives in SKILL.md frontmatter). Report any installed copy and flag drift; notify-only, so a
4266
- // stale/absent skill never fails doctor (`abx skill install` resyncs).
4267
- const cliVersion = readCliVersion();
4268
- const skillVersions = installedSkillVersions();
4269
- const staleSkills = skillVersions.filter((v) => compareVersions(cliVersion, v) > 0);
4270
- if (skillVersions.length === 0) {
4271
- console.log(` ${dim('↳ agent skill: not installed here — `abx skill install` so a coding agent can drive abx')}`);
4272
- }
4273
- else if (staleSkills.length > 0) {
4274
- console.log(` ${c.orange}⚠${c.reset} agent skill behind CLI${dim(` installed v${staleSkills.join(', v')} · CLI v${cliVersion} — refresh: `)}${g('abx skill install')}`);
4275
- }
4276
- else {
4277
- check(`agent skill in sync (v${cliVersion})`, true);
4364
+ opt('arweave key', `${jwk ? arweaveAddress(jwk) + ' ' : ''}holds upload credits — back it up: \`abx storage backup-key --out <path>\``);
4278
4365
  }
4279
4366
  console.log('');
4280
4367
  }
@@ -4639,6 +4726,20 @@ const COMMAND_HELP = {
4639
4726
  ${g('abx deploy-series')} --dir ./photos --name "My Series" --symbol MS --onchain-uri --backend ipfs --mint-all --sign
4640
4727
  ${dim('# tiny SVGs → fully on-chain (no storage at all):')}
4641
4728
  ${g('abx deploy-series')} --dir ./svgs --name "My Series" --symbol MS --onchain-image --compress fastlz --mint-all --sign`,
4729
+ preview: `
4730
+ ${bold('abx preview')} (--script <file.js> | --code-dir <dir>) ${dim('— run the program on localhost, live. No chain, no key, no deploy.')}
4731
+ ${g('--schema key:Type:Auth')}[,…] declare PostParams so the studio gives you real inputs for them (e.g. palette:HexColor:TokenOwner)
4732
+ ${g('--dep <name@version>')}[,…] load a library the way the resolver would (built-in CDN map; on-chain refs can't be fetched offline)
4733
+ ${g('--port')} <n> studio port (default ${DEFAULT_PREVIEW_PORT}; the resolver's ${DEFAULT_PORT} stays free)
4734
+ ${g('--shoot')} <dir> render headlessly to PNGs + traits.json and EXIT ${dim('(for an agent that has no browser)')}
4735
+ ${g('--count')} <n> seeds to shoot / show in the grid (default 9) ${g('--width')} <px> ${g('--timeout-ms')} <n>
4736
+ Serves the ${bold('same document the generator serves')} — real ${g('abx.js')}, real tokenData shape, real dep tags — with a synthetic
4737
+ seed, so what you iterate on is what deploys. Routes: ${g('/')} studio (seed + params + live traits) · ${g('/grid')} N seeds at once,
4738
+ all live · ${g('/view')} the bare document. The program is re-read from disk per render, so ${bold('edit and refresh')} — no watcher.
4739
+ Unlike a still-image sweep this shows ${bold('animation')}, which is most of what a screenshot throws away.
4740
+ ${dim('Still do both after the art settles:')} ${g('abx inspect')} ${dim('(is it wired right?) and a testnet deploy (the faithful end-to-end).')}
4741
+ ${g('abx preview')} --script art.js --schema palette:HexColor:TokenOwner
4742
+ ${g('abx preview')} --script art.js --shoot ./frames --count 12`,
4642
4743
  inspect: `
4643
4744
  ${bold('abx inspect')} <script.js> ${dim('— static analysis of a generative script + a lane recommendation. Read-only; the script is never executed.')}
4644
4745
  ${g('--dep <name@version>[,…]')} the on-chain deps you plan to declare, so the assembled-document size estimate is realistic (e.g. --dep p5@1.0.0)
@@ -4896,7 +4997,10 @@ const COMMAND_HELP = {
4896
4997
  ${g('upload')} <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend …] [--dry-run]
4897
4998
  ${g('balance')} · ${g('topup')} --usd <n> Turbo (arweave) upload credits · ${g('backup-key')} --out <path> copy the managed key`,
4898
4999
  demo: `
4899
- ${bold('abx demo')} <${dim('no args')}> ${dim('— deploy a throwaway 1/1 to the testnet, index it, and serve it — a guided first run. Sends a tx.')}`,
5000
+ ${bold('abx demo')} <${dim('no args')}> ${dim('— deploy a throwaway 1/1 to the testnet, index it, and serve it — a guided first run. Sends a tx.')}
5001
+ ${g('--sign')} ${dim('approve in your browser wallet instead of a hot env key (no key needed)')}
5002
+ ${g('--for')} <0x…> ${dim('pin who must connect on --sign (owner + royalty receiver + mint recipient)')}
5003
+ ${dim('--unsigned / --dry-run are refused here: both skip the broadcast, and the demo indexes + serves what it deployed.')}`,
4900
5004
  minter: `
4901
5005
  ${bold('abx minter')} <configure|show|buy> <token> ${dim('— sell a Series via the shared fixed-price minter (Minter spine).')}
4902
5006
  ${g('configure')} <token> (--price <eth> | --price-raw <units>) --allocation <n> [--erc20 0x..] ${dim('(token-owner only)')}
@@ -4990,7 +5094,9 @@ function help() {
4990
5094
  project + notify the effects layer on change (${g('ABX_WATCH_INTERVAL_MS')}; 0 = off)
4991
5095
 
4992
5096
  ${bold('code / generative projects')} ${dim('— a program is the content; output is a function of live on-chain state')}
4993
- ${g('abx inspect')} <script.js> ${bold('start here')} — static analysis (traits + on-chain reproducibility, deps, doc size RPC viability) + a lane recommendation
5097
+ ${g('abx preview')} (--script <f> | --code-dir <d>) ${bold('while you are still making it')} — run the program on localhost, live: refresh for new seeds,
5098
+ drive your PostParams, watch it animate. Same document the generator serves. ${g('--shoot <dir>')} for headless frames. No chain.
5099
+ ${g('abx inspect')} <script.js> ${bold('before you pick a lane')} — static analysis (traits + on-chain reproducibility, deps, doc size → RPC viability) + a lane recommendation
4994
5100
  ${g('abx scaffold-renderer')} [<dir>] write a buildable Foundry project for the ${bold('in-chain Solidity art lane')} (seed + PostParam → on-chain SVG + traits; you forge build/test/deploy)
4995
5101
  ${g('abx deploy-code')} (--script <file> | --code-dir <dir> | ${g('--image-renderer 0x..')}) deploy a ${bold('generative / code project')} (on-chain script, a build directory, or a Solidity SVG renderer — in-chain art)
4996
5102
  ${bold('--public-base-url <url>')} OR ${bold('--onchain-uri')} · --schema key:Type:Auth · ${g('--dep')} name@version|0x.. (ordered; index 0 = the runtime) ·