@artblocks/abx-cli 0.1.0-alpha.15 → 0.1.0-alpha.16

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.
@@ -10,6 +10,13 @@ pragma solidity ^0.8.20;
10
10
  /// The tokenData merge rule: a token-scope value overrides the contract-scope one. The
11
11
  /// common pattern is `tokenParam` first, then `contractParam` as a fallback (see the
12
12
  /// `_param` helper in MyRenderer.sol).
13
+ ///
14
+ /// TWO READERS, PICK BY TYPE. Scalar types (`Bool`, `Select`, the ranges, `HexColor`,
15
+ /// `Timestamp`) live entirely in the `bytes32` — read them with `tokenParam` /
16
+ /// `contractParam`. The two payload types, **`Bytes` and `String`**, do NOT: their
17
+ /// `bytes32` is a keccak COMMITMENT (`valueIsHash == true`) and the content is fetched with
18
+ /// `tokenParamData` / `contractParamData` below. Reading a `Bytes` param with `tokenParam`
19
+ /// hands you a hash, not the artwork — that is the tell you want the data reader.
13
20
  interface IAbxParams {
14
21
  /// Returns the token-scope param: its bytes32 `value`, `valueIsHash` (true if it commits to
15
22
  /// off-chain bytes — a literal scalar like a seed/HexColor is false), and `isSet` (does it exist).
@@ -23,4 +30,14 @@ interface IAbxParams {
23
30
  external
24
31
  view
25
32
  returns (bytes32 value, bool valueIsHash, bool isSet);
33
+
34
+ /// @notice A `Bytes`/`String` token param's FULL content — the payload types' real reader.
35
+ /// Returns empty bytes for a literal scalar or an unset key, so an empty return is your
36
+ /// "fall back to a default" signal. When non-empty, `keccak256(returned bytes)` equals
37
+ /// the `value` that `tokenParam` reports — verify it if you want the commitment proof.
38
+ /// Up to ~24KB per key: enough to carry an actual artwork payload on-chain.
39
+ function tokenParamData(uint256 tokenId, bytes32 key) external view returns (bytes memory);
40
+
41
+ /// @notice The contract-scope (collection-wide) blob — the fallback when a token has none.
42
+ function contractParamData(bytes32 key) external view returns (bytes memory);
26
43
  }
@@ -13,15 +13,38 @@ contract MockParams is IAbxParams {
13
13
  mapping(uint256 => mapping(bytes32 => bool)) private ts;
14
14
  mapping(bytes32 => bytes32) private cv;
15
15
  mapping(bytes32 => bool) private cs;
16
+ // `Bytes`/`String` params: the bytes32 holds keccak(content), the content lives here.
17
+ mapping(uint256 => mapping(bytes32 => bytes)) private td;
18
+ mapping(bytes32 => bytes) private cd;
16
19
 
17
20
  function setToken(uint256 id, bytes32 key, bytes32 val) external { tv[id][key] = val; ts[id][key] = true; }
18
21
  function setContract(bytes32 key, bytes32 val) external { cv[key] = val; cs[key] = true; }
19
22
 
23
+ /// Set a payload-typed (`Bytes`/`String`) param the way the real contract does: the scalar slot
24
+ /// carries the keccak COMMITMENT and `valueIsHash` is true, so a renderer that reads the bytes32
25
+ /// gets a hash — the data reader is the only way to the content.
26
+ function setTokenData(uint256 id, bytes32 key, bytes memory content) external {
27
+ td[id][key] = content;
28
+ tv[id][key] = keccak256(content);
29
+ ts[id][key] = true;
30
+ }
31
+ function setContractData(bytes32 key, bytes memory content) external {
32
+ cd[key] = content;
33
+ cv[key] = keccak256(content);
34
+ cs[key] = true;
35
+ }
36
+
20
37
  function tokenParam(uint256 id, bytes32 key) external view returns (bytes32, bool, bool) {
21
- return (tv[id][key], false, ts[id][key]);
38
+ return (tv[id][key], td[id][key].length > 0, ts[id][key]);
22
39
  }
23
40
  function contractParam(bytes32 key) external view returns (bytes32, bool, bool) {
24
- return (cv[key], false, cs[key]);
41
+ return (cv[key], cd[key].length > 0, cs[key]);
42
+ }
43
+ function tokenParamData(uint256 id, bytes32 key) external view returns (bytes memory) {
44
+ return td[id][key];
45
+ }
46
+ function contractParamData(bytes32 key) external view returns (bytes memory) {
47
+ return cd[key];
25
48
  }
26
49
  }
27
50
 
@@ -107,4 +130,30 @@ contract MyRendererTest is Test {
107
130
  (, bytes memory data) = img.render(address(params), tokenId, IMAGE);
108
131
  assertTrue(data.length > 0);
109
132
  }
133
+
134
+ /// READING A PAYLOAD PARAM (`Bytes`/`String`) — the pattern for carrying an actual artwork
135
+ /// payload on-chain. The scalar reader hands you a keccak COMMITMENT with `valueIsHash == true`;
136
+ /// the content only comes from `tokenParamData`. If your renderer reads a `Bytes` param through
137
+ /// `tokenParam` it will draw from a hash and produce garbage, silently — hence this test.
138
+ function test_bytesParam_readViaDataReader() public {
139
+ bytes memory grid = hex"00112233445566778899aabbccddeeff";
140
+ params.setTokenData(0, "grid", grid);
141
+
142
+ // The scalar surface: a commitment, explicitly flagged as one — NOT the content.
143
+ (bytes32 value, bool valueIsHash, bool isSet) = IAbxParams(address(params)).tokenParam(0, "grid");
144
+ assertTrue(isSet);
145
+ assertTrue(valueIsHash, "a Bytes param reports valueIsHash: read the data instead");
146
+ assertEq(value, keccak256(grid));
147
+
148
+ // The data surface: the real bytes, verifiable against that commitment.
149
+ bytes memory got = IAbxParams(address(params)).tokenParamData(0, "grid");
150
+ assertEq(got, grid);
151
+ assertEq(keccak256(got), value, "content must match the on-chain commitment");
152
+ }
153
+
154
+ /// An unset payload param returns empty bytes — your "use a default" signal, never a revert.
155
+ function test_bytesParam_unsetIsEmpty() public view {
156
+ assertEq(IAbxParams(address(params)).tokenParamData(0, "grid").length, 0);
157
+ assertEq(IAbxParams(address(params)).contractParamData("grid").length, 0);
158
+ }
110
159
  }
package/dist/flags.d.ts CHANGED
@@ -19,6 +19,13 @@ export declare const REPEATABLE_FLAGS: ReadonlySet<string>;
19
19
  */
20
20
  export declare const GLOBAL_FLAGS: ReadonlySet<string>;
21
21
  export declare function parseFlags(args: string[]): Flags;
22
+ /**
23
+ * The bare (non-flag) arguments, with flag VALUES removed — the mirror of {@link parseFlags}, and
24
+ * deliberately next to it: the two must consume argv by the same rule or a flag's value looks like a
25
+ * positional. (Hand-rolling `args.filter(a => !a.startsWith('-'))` reads `--token 0` as a stray
26
+ * positional `0`, which is exactly the bug a stray-positional check is meant to catch.)
27
+ */
28
+ export declare function positionalArgs(args: string[]): string[];
22
29
  /**
23
30
  * The flag keys in `flags` that are NOT in `allowed` — for a non-fatal "unrecognized flag"
24
31
  * notice at a command's entry. {@link parseFlags} keeps any `--k` it sees, so a typo'd or
@@ -1 +1 @@
1
- {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../src/flags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEvD,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAuC,CAAC;AAEzF;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,WAAW,CAAC,MAAM,CAAgC,CAAC;AAE9E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAchD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAG9E"}
1
+ {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../src/flags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEvD,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAuC,CAAC;AAEzF;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,WAAW,CAAC,MAAM,CAAgC,CAAC;AAE9E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAchD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAWvD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAG9E"}
package/dist/flags.js CHANGED
@@ -25,6 +25,25 @@ export function parseFlags(args) {
25
25
  }
26
26
  return out;
27
27
  }
28
+ /**
29
+ * The bare (non-flag) arguments, with flag VALUES removed — the mirror of {@link parseFlags}, and
30
+ * deliberately next to it: the two must consume argv by the same rule or a flag's value looks like a
31
+ * positional. (Hand-rolling `args.filter(a => !a.startsWith('-'))` reads `--token 0` as a stray
32
+ * positional `0`, which is exactly the bug a stray-positional check is meant to catch.)
33
+ */
34
+ export function positionalArgs(args) {
35
+ const out = [];
36
+ for (let i = 0; i < args.length; i++) {
37
+ const a = args[i];
38
+ if (a.startsWith('--')) {
39
+ if (a.indexOf('=') === -1 && args[i + 1] && !args[i + 1].startsWith('--'))
40
+ i++; // this flag consumes the next token
41
+ continue;
42
+ }
43
+ out.push(a);
44
+ }
45
+ return out;
46
+ }
28
47
  /**
29
48
  * The flag keys in `flags` that are NOT in `allowed` — for a non-fatal "unrecognized flag"
30
49
  * notice at a command's entry. {@link parseFlags} keeps any `--k` it sees, so a typo'd or
package/dist/flags.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"flags.js","sourceRoot":"","sources":["../src/flags.ts"],"names":[],"mappings":"AAaA,8EAA8E;AAC9E,MAAM,CAAC,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAEzF;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAE9E,MAAM,UAAU,UAAU,CAAC,IAAc;IACvC,MAAM,GAAG,GAAU,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE;QACnC,GAAG,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC,CAAC;IACF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAClC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;aAC/C,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;;YAC7E,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAY,EAAE,OAAyB;IAClE,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC"}
1
+ {"version":3,"file":"flags.js","sourceRoot":"","sources":["../src/flags.ts"],"names":[],"mappings":"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"}
package/dist/main.js CHANGED
@@ -58,11 +58,12 @@ import { encodeFunctionData } from 'viem';
58
58
  import { DEP_RESOLUTION, deploySeedSource, deploySeriesCodeFactory, prepareCodeSetup, prepareDeploySeriesCode, resolveGenerator, resolveSeedSource, resolveSeriesCodeFactory, seriesCodeAbi, readParamSchema, seriesCodeFactoryAbi, } from '@artblocks/abx-sdk';
59
59
  import { checkRegistryDeps, dependencySetupCalls, parseDepFlag, resolveDepRegistryPointer } from './deps.js';
60
60
  import { expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport, readLegacyParamsKeys, readParamSchemaKeys, readSetParamKeys, } from './onchain-uri.js';
61
- import { parseFlags, unknownFlags } from './flags.js';
61
+ import { parseFlags, positionalArgs, unknownFlags } from './flags.js';
62
62
  import { AGENT_SKILL_PARENTS, checkForCliUpdate, compareVersions, installedSkillVersions, readCliVersion, readSkillVersion, SKILL_DIR_NAME, } from './update-check.js';
63
63
  import { analyzeScript, recommendLane } from './inspect.js';
64
64
  import { previewConfigFromFlags, previewDepTags, parsePreviewParams, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
65
65
  import { parseSchemaSpecs, describeSchema } from './schema.js';
66
+ import { copyRendererScaffold } from './scaffold.js';
66
67
  import { declinesSkillInstall } from './prompt.js';
67
68
  import { pinGas, waitForCodeAt } from './gas.js';
68
69
  import { parseSeriesTraits, looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
@@ -161,9 +162,16 @@ async function maybeNotifyUpdate(flags) {
161
162
  }
162
163
  const latest = await checkForCliUpdate(current);
163
164
  if (latest) {
165
+ // The notes pointer must RESOLVE. This printed github.com/ArtBlocks/abx/releases, which 404s
166
+ // for everyone outside the org (the repo is private) — so a tester reconstructed the diff by
167
+ // running the same dry run on both versions, which is how they discovered the canonical
168
+ // singletons had moved and then had no way to tell whether that needed them to act. The
169
+ // packaged CHANGELOG.md ships with every install (see package.json `files`) and carries the
170
+ // real per-release notes, so it's readable offline and always matches the version you have.
164
171
  console.error(`\n ${c.orange}⚠${c.reset} update available: ${bold('abx')} ${dim(current)} → ${g(latest)}\n` +
165
172
  ` upgrade: ${g('npm i -g @artblocks/abx-cli@latest')} ${dim('· or invoke:')} ${g('npx @artblocks/abx-cli@latest <command>')}\n` +
166
- ` release notes: https://github.com/ArtBlocks/abx/releases ${dim('· silence: ABX_NO_UPDATE_CHECK=1')}\n`);
173
+ ` release notes: ${g('abx changelog')} ${dim('(ships with the CLI) · all versions: https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions')}\n` +
174
+ ` ${dim('silence: ABX_NO_UPDATE_CHECK=1')}\n`);
167
175
  }
168
176
  }
169
177
  catch {
@@ -212,13 +220,14 @@ async function main() {
212
220
  case 'inspect': return cmdInspect(rest[0], flags);
213
221
  case 'preview': return cmdPreview(flags);
214
222
  case 'scaffold-renderer': return cmdScaffoldRenderer(rest, flags);
223
+ case 'changelog': return cmdChangelog(flags);
215
224
  case 'predict': return cmdPredict(flags);
216
225
  case 'add': return cmdAdd(rest[0], flags);
217
226
  case 'index': return cmdIndex(rest[0], flags);
218
227
  case 'verify': return cmdVerify(rest[0], flags);
219
228
  case 'render': return cmdRender(rest[0], rest.slice(1), flags);
220
229
  case 'effects': return cmdEffects(flags);
221
- case 'tokenuri': return cmdTokenUri(rest[0], flags);
230
+ case 'tokenuri': return cmdTokenUri(rest[0], flags, rest.slice(1));
222
231
  case 'contracturi': return cmdContractUri(rest[0], flags);
223
232
  case 'serve': return cmdServe(flags);
224
233
  // owner operations — write + sign (hot/wallet/cold lane), then re-index
@@ -1202,6 +1211,8 @@ async function cmdDeploy(flags, serveAfter) {
1202
1211
  ` Nothing was deployed. Stop that one (Ctrl-C), or run this on another port: \`abx demo --port ${wanted + 1}\`.`);
1203
1212
  }
1204
1213
  }
1214
+ // (demo keeps its own message: "Nothing was deployed" is the load-bearing part here, and it must
1215
+ // be true — this check runs before anything irreversible. assertPortFree covers serve/preview.)
1205
1216
  const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
1206
1217
  // A keyless preview needs `--for` (the address is a pure function of factory+salt+deployer). Check
1207
1218
  // it HERE, before the trust-anchor/content/plan steps print — hitting this after a wall of output
@@ -2881,6 +2892,21 @@ async function walkthroughReadBack(state, baseUrl, onChainUri) {
2881
2892
  }
2882
2893
  console.log(` ${dim('prove the bytes match the chain:')} ${bold(`abx verify ${state.address}`)}`);
2883
2894
  }
2895
+ /**
2896
+ * Refuse a port that's already bound, with a formatted one-liner naming the port and the fix.
2897
+ *
2898
+ * Without this, `listen()` has no `'error'` handler and EADDRINUSE reaches Node's default handler:
2899
+ * the creator gets a raw stack trace through `node:net` and our own `dist/` paths, which reads as a
2900
+ * crash inside abx rather than "something else is on this port" — and it's the one unformatted error
2901
+ * surface in a CLI where every other error is formatted. `preview`'s default port colliding with a
2902
+ * studio left running in another terminal was reported as especially hard to diagnose.
2903
+ */
2904
+ async function assertPortFree(port, cmd) {
2905
+ if (!(await portInUse(port)))
2906
+ return;
2907
+ throw new Error(`port ${port} is already in use — most likely an \`abx ${cmd}\`/\`abx serve\`/\`abx preview\` still running in another terminal.\n` +
2908
+ ` Stop that one (Ctrl-C), or run this on a different port: \`abx ${cmd} --port ${port + 1}\`.`);
2909
+ }
2884
2910
  /** Is a TCP port already bound on localhost? Used to preflight a serve BEFORE spending a tx. */
2885
2911
  async function portInUse(port) {
2886
2912
  const { createServer } = await import('node:net');
@@ -2957,7 +2983,11 @@ async function cmdPreview(flags) {
2957
2983
  if (cfg.deps.some((d) => d.display.startsWith('0x'))) {
2958
2984
  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.');
2959
2985
  }
2960
- const server = await startPreviewServer(cfg, shootDir ? 0 : Number(flags.port ?? DEFAULT_PREVIEW_PORT));
2986
+ // `--shoot` takes an ephemeral port (0), so only the studio lane can collide.
2987
+ const previewPort = shootDir ? 0 : Number(flags.port ?? DEFAULT_PREVIEW_PORT);
2988
+ if (previewPort !== 0)
2989
+ await assertPortFree(previewPort, 'preview');
2990
+ const server = await startPreviewServer(cfg, previewPort);
2961
2991
  if (shootDir) {
2962
2992
  step(`Render ${count} seeds headlessly`);
2963
2993
  try {
@@ -3800,7 +3830,13 @@ async function cmdDeployCode(flags) {
3800
3830
  catch { /* gas pricing unavailable — cost guidance is a bonus, skip silently */ }
3801
3831
  // ── Surfaces — every marketplace-facing dimension, resolved NOW (none is backfillable) ──────
3802
3832
  const renderHome = process.env.ABX_STORAGE_BACKEND || 'fs'; // empty string counts as unset (=fs)
3803
- const anySurfaceBroken = imageOrphaned || traitsBroken || undeclaredParams.length > 0;
3833
+ // A surface is BROKEN only when it resolves to nothing a marketplace can see. A dropped param is
3834
+ // NOT that: the token still renders, the param just takes its default. Folding it in here made
3835
+ // the block contradict itself two lines apart — "thumbnail: ON-CHAIN ✓ / traits: on-chain ✓"
3836
+ // followed by "one or more surfaces resolve to NOTHING", re-recommending the very flags that
3837
+ // were already set. The alarm now covers only the surfaces it can honestly speak for, and names
3838
+ // only the remedies for what is actually broken.
3839
+ const anySurfaceBroken = imageOrphaned || traitsBroken;
3804
3840
  step('Surfaces — what marketplaces will see (all DEPLOY-TIME; not backfillable)');
3805
3841
  (imageOrphaned ? warn : info)(`thumbnail: ${imageDisposition}`);
3806
3842
  // Render-mode / render-home guidance is for the RENDERED-still lanes only. The in-chain
@@ -3818,12 +3854,25 @@ async function cmdDeployCode(flags) {
3818
3854
  (traitsBroken ? warn : info)(`traits: ${traitsDisposition}`);
3819
3855
  const paramsNudge = !scriptAnalysis && !schemas.length && (hasImageRenderer || hasAttributesRenderer);
3820
3856
  ((undeclaredParams.length || paramsNudge ? warn : info))(`postparams: ${paramsDisposition}`);
3821
- if (anySurfaceBroken)
3822
- warn(`${bold('one or more surfaces resolve to NOTHING a marketplace can see')} — fix before deploy (these are on-chain decisions you can't add later without a re-point tx): thumbnail ⇒ ${bold('--image-renderer <Solidity SVG>')} / ${bold('--image-base <public bucket>')} / a resolver · traits ⇒ ${bold('--attributes-renderer')} or a resolver · dropped params ⇒ ${bold('--schema')}.`);
3823
- // The runner/verify line is for rendered-still lanes. Fully in-chain (image on-chain, no program)
3824
- // has nothing to run point at the from-chain check instead.
3825
- if (hasImageRenderer && !hasProgram)
3826
- info(` ${g('nothing to run')} verify from chain: ${bold('abx tokenuri ' + (predicted ?? '<address>'))} ${dim('(decodes name + on-chain SVG + traits)')}`);
3857
+ if (anySurfaceBroken) {
3858
+ const remedies = [
3859
+ imageOrphaned && `thumbnail ${bold('--image-renderer <Solidity SVG>')} / ${bold('--image-base <public bucket>')} / a resolver`,
3860
+ traitsBroken && `traits ${bold('--attributes-renderer')} or a resolver`,
3861
+ ].filter(Boolean).join(' · ');
3862
+ const which = imageOrphaned && traitsBroken ? 'the thumbnail and traits surfaces resolve' : `the ${imageOrphaned ? 'thumbnail' : 'traits'} surface resolves`;
3863
+ warn(`${bold(`${which} to NOTHING a marketplace can see`)} — fix before deploy (a deploy-time decision you can't add later without a re-point tx): ${remedies}.`);
3864
+ }
3865
+ // A dropped param is its own, milder problem: the piece renders, that input just takes its
3866
+ // default. Kept separate from the broken-surface alarm above (see the note there).
3867
+ if (undeclaredParams.length)
3868
+ info(` ${dim('dropped params render with their defaults — declare them to make them real:')} ${bold('--schema <key>:<Type>:<Auth>')}`);
3869
+ // The runner/verify line is for lanes that need an off-chain STILL. An on-chain image renderer has
3870
+ // no still to render, host, or refresh — whether or not there's also a program driving
3871
+ // animation_url — so pointing at a runner and a bucket backend there is simply wrong.
3872
+ if (hasImageRenderer) {
3873
+ info(` ${g('nothing to render')} — the thumbnail is computed on-chain${hasProgram ? ' and the animation assembles on-chain from your script' : ''}; no runner, no bucket, no refresh. ` +
3874
+ `verify from chain: ${bold('abx tokenuri ' + (predicted ?? '<address>'))} ${dim('(decodes name + on-chain SVG + traits)')}`);
3875
+ }
3827
3876
  else {
3828
3877
  const remoteRender = !(onChainUri && hasImageBase);
3829
3878
  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>'))}`);
@@ -4149,6 +4198,31 @@ async function cmdRender(address, tokenIds, flags) {
4149
4198
  warn(`${summary}\n ${dim(stats.errors[0] ?? 'see error above')}`);
4150
4199
  else
4151
4200
  ok(summary);
4201
+ if (stats.ran)
4202
+ noteArweavePropagation(flags);
4203
+ }
4204
+ /**
4205
+ * After an Arweave publish, say that a fresh 404 at the gateway is PROPAGATION, not a failed render.
4206
+ *
4207
+ * `arweave.net` lags Turbo uploads by minutes: a tester found 32/32 of their renders 404ing there
4208
+ * while Turbo reported CONFIRMED and 22/32 already served fine from other ar.io gateways. The bytes
4209
+ * were never in doubt — only the gateway was behind. Unexplained, a "broken" thumbnail on a fresh
4210
+ * drop reads as a failed render, and the natural next move is `abx render --force` on all of them:
4211
+ * a full re-upload that fixes nothing.
4212
+ *
4213
+ * Note this is advisory only, and deliberately so — the locator is baked into what the resolver
4214
+ * registers at publish time, so it cannot be repaired by a redirect later. Choosing the gateway is
4215
+ * the operator's call (`ABX_ARWEAVE_GATEWAY`), which is why this names it.
4216
+ */
4217
+ function noteArweavePropagation(flags) {
4218
+ const opts = storageOptions(storageOverrides(flags));
4219
+ if (resolveBackend(opts).id !== 'arweave')
4220
+ return;
4221
+ const gateway = opts.arweave?.gateway ?? 'https://arweave.net';
4222
+ if (!/(^|\/\/)([^/]*\.)?arweave\.net/.test(gateway))
4223
+ return; // a gateway they chose — don't lecture
4224
+ info(`${dim('arweave: the locator points at')} ${gateway}${dim(', which can 404 for several minutes after upload while it catches up. That is PROPAGATION, not a failed render — the bytes are already confirmed. Do NOT re-run with')} ${bold('--force')}${dim('; the URL starts working on its own.')}`);
4225
+ info(` ${dim('to bake a different gateway into the locator instead (it is fixed at publish time):')} ${bold('ABX_ARWEAVE_GATEWAY=https://<gateway>')} ${dim('before you render.')}`);
4152
4226
  }
4153
4227
  /**
4154
4228
  * The publish topology's one hard prerequisite, checked BEFORE any capture.
@@ -4361,14 +4435,27 @@ async function cmdVerify(address, flags) {
4361
4435
  // `abx verify <addr> --remote <resolver>` probes what the resolver actually serves (the truthful check).
4362
4436
  if (minted.length)
4363
4437
  info(dim(`render check is against THIS node's store; for a HOSTED drop use \`abx verify ${address} --remote <resolver>\``));
4438
+ // ONE line per outcome, not per token. This printed the same full-sentence advisory 32 times on a
4439
+ // 32-token project (~4KB of identical text) and pushed the four lines that answer "did my deploy
4440
+ // work" off the top of the screen; at a 1000-token supply it is unreadable. The per-token detail
4441
+ // that survives is the token LIST, which is the only part that differs.
4442
+ const missing = [];
4443
+ let present = 0;
4364
4444
  for (const token of minted) {
4365
4445
  const { found } = await currentRenderArtifact(client, state, token, storageForRender, 'image');
4366
4446
  if (found)
4367
- ok(`token #${token.tokenId} image: real render present (in this node's store)`);
4368
- else {
4369
- renderGap = true;
4370
- console.log(` ${c.orange}⚠${c.reset} token #${token.tokenId} image: no render in THIS node's store — if you published to a hosted resolver, check it with \`abx verify ${address} --remote <resolver>\`; else render it: \`abx render ${address}\` (once) or \`abx effects\` (continuous)`);
4371
- }
4447
+ present++;
4448
+ else
4449
+ missing.push(String(token.tokenId));
4450
+ }
4451
+ if (present)
4452
+ ok(`${present}/${minted.length} minted token(s): real render present (in this node's store)`);
4453
+ if (missing.length) {
4454
+ renderGap = true;
4455
+ const ids = missing.length > 12 ? `${missing.slice(0, 12).join(', ')}, …+${missing.length - 12} more` : missing.join(', ');
4456
+ console.log(` ${c.orange}⚠${c.reset} ${missing.length}/${minted.length} token(s) have no render in THIS node's store ${dim(`(#${ids})`)}`);
4457
+ console.log(` ${dim('published to a hosted resolver? check there:')} ${bold(`abx verify ${address} --remote <resolver>`)}`);
4458
+ console.log(` ${dim('else render them:')} ${bold(`abx render ${address}`)} ${dim('(once) ·')} ${bold('abx effects')} ${dim('(continuous)')}`);
4372
4459
  }
4373
4460
  }
4374
4461
  // The on-chain URI lane (a non-zero tokenURIRenderer, or an animation field pointing at the
@@ -4642,7 +4729,7 @@ function decodeOnChainJson(uri, verbatim = false) {
4642
4729
  return verbatim ? raw : raw.slice(0, 600);
4643
4730
  }
4644
4731
  }
4645
- async function cmdTokenUri(address, flags) {
4732
+ async function cmdTokenUri(address, flags, extra = []) {
4646
4733
  if (!address || address.startsWith('--')) {
4647
4734
  console.error('usage: abx tokenuri <address> [--token <id>] [--json]\n');
4648
4735
  process.exit(1);
@@ -4651,6 +4738,19 @@ async function cmdTokenUri(address, flags) {
4651
4738
  console.error(`abx tokenuri: '${address}' isn't a 0x contract address.\n`);
4652
4739
  process.exit(1);
4653
4740
  }
4741
+ // `abx tokenuri <addr> 0` silently ignored the `0` and printed token 0 — a COINCIDENTALLY correct
4742
+ // answer, which is the dangerous kind: `… <addr> 7` would have printed token 0 just as confidently
4743
+ // and exited 0. The token id is a flag here, so name it rather than guessing at intent.
4744
+ const strayPositionals = positionalArgs(extra);
4745
+ if (strayPositionals.length) {
4746
+ const first = strayPositionals[0];
4747
+ const looksLikeTokenId = /^\d+$/.test(first);
4748
+ console.error(`abx tokenuri: unexpected extra argument '${first}'.` +
4749
+ (looksLikeTokenId
4750
+ ? ` The token id is a flag — did you mean:\n abx tokenuri ${address} --token ${first}\n`
4751
+ : `\n usage: abx tokenuri <address> [--token <id>] [--json]\n`));
4752
+ process.exit(1);
4753
+ }
4654
4754
  const tokenId = BigInt(flags.token ?? '0');
4655
4755
  const publicClient = makePublicClient({ chainKey: CHAIN });
4656
4756
  // A creator verifying their work often runs this on a wrong / not-yet-mined address — turn viem's
@@ -4793,6 +4893,7 @@ async function cmdContractUri(address, _flags) {
4793
4893
  // ── serve ──────────────────────────────────────────────────────────────────--
4794
4894
  async function cmdServe(flags) {
4795
4895
  const port = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
4896
+ await assertPortFree(port, 'serve');
4796
4897
  const baseUrl = resolveBaseUrl(port);
4797
4898
  const indexer = new SelfHostIndexer();
4798
4899
  // Surface the most-recently-reconstructed project (what you just deployed), not
@@ -6255,6 +6356,9 @@ const COMMAND_HELP = {
6255
6356
  like ${g('display.gateway')}. ≤31 printable-ASCII chars ride as a literal bytes32; longer takes the data path.
6256
6357
  A contract-scope param applies to every token, and enumerates on-chain like any other.
6257
6358
  --file <path> read the value from a file (String / Bytes payloads)
6359
+ ${dim('PAYLOAD TYPES:')} ${g('String')} ${dim('takes literal text (UTF-8).')} ${g('Bytes')} ${dim('takes')} ${bold('0x-prefixed hex')} ${dim(`or ${g('--file')} — a bare`)}
6360
+ ${dim('string is refused, because there is no safe guess between "these characters" and "these bytes".')}
6361
+ ${dim(`(The docs' "Bytes becomes base64" describes how your ${bold('program')} receives the value, not how you write it.)`)}
6258
6362
  ${g('--remote [name|url]')} nudge a REMOTE resolver to re-index IMMEDIATELY after the change (else ABX_PUBLIC_BASE_URL) — it pings
6259
6363
  the resolver's effect runner, so the thumbnail re-renders without waiting. Usually OPTIONAL now: a
6260
6364
  resolver running the chain watcher (the ${g('abx serve')} default) sees the change on its next poll (~12s)
@@ -6383,8 +6487,12 @@ const COMMAND_HELP = {
6383
6487
  ${g('--dry-run')} preview the tx, send nothing
6384
6488
  --collection target collection (ERC-7572) scope · else --token <id> (default 0)`,
6385
6489
  'lock-field': `
6386
- ${bold('abx lock-field')} <address> --field <name> ${dim('— freeze a field FOREVER (irreversible). Sends a tx.')}
6387
- --collection | --token <id>`,
6490
+ ${bold('abx lock-field')} <address> --field <name> ${dim('— freeze a metadata FIELD forever (irreversible). Sends a tx.')}
6491
+ --collection | --token <id>
6492
+ --force-field proceed even if a PostParam shares this name (you mean the field)
6493
+ ${dim('FIELDS AND PARAMS ARE DIFFERENT NAMESPACES and may share a name. This locks the field only —')}
6494
+ ${dim(`to weld a PostParam use ${g('abx set-schema <addr> --schema <key>:<Type>:<Auth>:lock=now')}. Passing a`)}
6495
+ ${dim('declared param key here is refused, because locking the field leaves the param writable.')}`,
6388
6496
  'set-renderer': `
6389
6497
  ${bold('abx set-renderer')} <address> ${dim('— toggle URI resolution between off-chain and on-chain. Sends a tx.')}
6390
6498
  (default) point at the chain's canonical renderer (deploys it if needed) → resolve ON-CHAIN
@@ -6514,6 +6622,11 @@ const COMMAND_HELP = {
6514
6622
  project to the effects layer (${g('ABX_EFFECTS_URL')} + ${g('ABX_EFFECTS_TOKEN')}) → thumbnails auto-re-render.
6515
6623
  ${g('ABX_WATCH_INTERVAL_MS')} tunes the cadence; ${g('0')} disables (state then updates only on explicit add/index).
6516
6624
  ${dim('Reorg note: no lookback (post-PoS reorgs are rare); the repair is the deterministic full replay — `abx index <addr> --full`.')}`,
6625
+ changelog: `
6626
+ ${bold('abx changelog')} ${dim('— what changed in this and recent releases. Reads nothing from the network. No tx.')}
6627
+ ${g('--all')} the full history (default: the 3 most recent releases)
6628
+ ${dim('The notes ship INSIDE the package, so they always match the version you have and work offline.')}
6629
+ ${dim('Every published version is listed at https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions')}`,
6517
6630
  };
6518
6631
  function printCommandHelp(cmd) {
6519
6632
  if (cmd && COMMAND_HELP[cmd]) {
@@ -6616,6 +6729,7 @@ function help() {
6616
6729
  ${g('abx doctor')} check environment (key, RPC, balance, factory, storage)
6617
6730
  ${g('abx skill install')} install the version-locked abx skill into your agent(s) [--agent <name>] [--global] [--target <dir>] · ${g('abx skill path')} prints the bundled skill
6618
6731
  ${g('abx version')} print the installed CLI version
6732
+ ${g('abx changelog')} what changed in this and recent releases (ships with the CLI; offline) [--all]
6619
6733
 
6620
6734
  ${dim('Run')} ${g('abx <command> --help')} ${dim('for per-command usage. --help / -h never executes — it only prints usage.')}
6621
6735
  ${dim('abx checks npm for a newer release (every 6h, notify-only). Silence with')} ${g('ABX_NO_UPDATE_CHECK=1')} ${dim('or')} ${g('--no-update-check')}${dim('.')}
@@ -6709,6 +6823,40 @@ function resolveBundledSkill() {
6709
6823
  return canonical;
6710
6824
  return null;
6711
6825
  }
6826
+ /**
6827
+ * `abx changelog [--all]` — print the release notes that ship with THIS install.
6828
+ *
6829
+ * The update banner used to point at `github.com/ArtBlocks/abx/releases`, which 404s for anyone
6830
+ * outside the org, and no changelog shipped in the package — so "what changed?" was unanswerable
6831
+ * without diffing two versions' dry-run output by hand. `CHANGELOG.md` now ships (package.json
6832
+ * `files`), which makes the notes offline, version-matched, and readable by an agent.
6833
+ *
6834
+ * Default: the most recent few entries (what an upgrade needs). `--all`: the whole file.
6835
+ */
6836
+ function cmdChangelog(flags) {
6837
+ const pkgDir = resolvePath(fileURLToPath(import.meta.url), '..', '..');
6838
+ const candidates = [joinPath(pkgDir, 'CHANGELOG.md')];
6839
+ const root = findRepoRoot();
6840
+ if (root)
6841
+ candidates.push(joinPath(root, 'packages', 'cli', 'CHANGELOG.md')); // dev checkout
6842
+ const path = candidates.find((p) => existsSync(p));
6843
+ if (!path) {
6844
+ throw new Error(`no CHANGELOG.md found beside this install (looked in ${candidates.join(', ')}). ` +
6845
+ 'Version history: https://www.npmjs.com/package/@artblocks/abx-cli?activeTab=versions');
6846
+ }
6847
+ const text = readFileSync(path, 'utf8');
6848
+ if (flags.all !== undefined) {
6849
+ console.log(text);
6850
+ return;
6851
+ }
6852
+ // Entries are `## <version>` sections; show the newest few and say how to see the rest.
6853
+ const lines = text.split('\n');
6854
+ const heads = lines.map((l, i) => (/^## /.test(l) ? i : -1)).filter((i) => i >= 0);
6855
+ const end = heads.length > 3 ? heads[3] : lines.length;
6856
+ console.log(`\n${lines.slice(0, end).join('\n').trimEnd()}\n`);
6857
+ if (heads.length > 3)
6858
+ info(`showing the ${3} most recent releases — ${bold('abx changelog --all')} for the full history ${dim(`(${path})`)}`);
6859
+ }
6712
6860
  /** Locate the in-chain renderer Foundry scaffold: bundled beside the CLI (published), else the
6713
6861
  * repo copy (dev). Same layout in both — it lives under the CLI package's `assets/`. */
6714
6862
  function resolveRendererScaffold() {
@@ -6737,12 +6885,9 @@ function cmdScaffoldRenderer(rest, flags) {
6737
6885
  if (existsSync(dir) && readdirSync(dir).length > 0 && flags.force === undefined) {
6738
6886
  throw new Error(`${dir} already exists and is not empty — pass a fresh path, or --force to write into it.`);
6739
6887
  }
6740
- mkdirSync(dir, { recursive: true });
6741
- // Copy the committed scaffold; never carry a stale build/deps dir if one somehow exists.
6742
- cpSync(src, dir, {
6743
- recursive: true,
6744
- filter: (s) => !/(^|\/)(out|cache|dependencies|broadcast|node_modules)(\/|$)/.test(s),
6745
- });
6888
+ // Copy + assert it landed. Throws rather than printing a success banner over an empty directory —
6889
+ // the alpha.9→alpha.14 failure mode. See src/scaffold.ts for why this is not inline.
6890
+ copyRendererScaffold(src, dir);
6746
6891
  ok(`in-chain renderer scaffold → ${dir}`);
6747
6892
  step('The in-chain Solidity art lane — nothing to run after deploy, tokenURI resolves from chain forever');
6748
6893
  info(`${bold('src/MyRenderer.sol')} draws an SVG from the token ${bold('seed')} + a ${bold('palette')} HexColor PostParam; ${bold('src/MyTraits.sol')} reads the SAME seed for coherent on-chain traits. Fork the art; keep the invariants in ${bold('src/interfaces/IAbxFieldRenderer.sol')} (above all: render() must NEVER revert).`);