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

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +1762 -0
  2. package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +17 -0
  3. package/assets/renderer-scaffold/test/MyRenderer.t.sol +51 -2
  4. package/dist/commands/deploy.d.ts +206 -0
  5. package/dist/commands/deploy.d.ts.map +1 -0
  6. package/dist/commands/deploy.js +4004 -0
  7. package/dist/commands/deploy.js.map +1 -0
  8. package/dist/commands/project.d.ts +241 -0
  9. package/dist/commands/project.d.ts.map +1 -0
  10. package/dist/commands/project.js +1044 -0
  11. package/dist/commands/project.js.map +1 -0
  12. package/dist/commands/reads.d.ts +64 -0
  13. package/dist/commands/reads.d.ts.map +1 -0
  14. package/dist/commands/reads.js +667 -0
  15. package/dist/commands/reads.js.map +1 -0
  16. package/dist/commands/scaffold.d.ts +71 -0
  17. package/dist/commands/scaffold.d.ts.map +1 -0
  18. package/dist/commands/scaffold.js +544 -0
  19. package/dist/commands/scaffold.js.map +1 -0
  20. package/dist/commands/service.d.ts +41 -0
  21. package/dist/commands/service.d.ts.map +1 -0
  22. package/dist/commands/service.js +618 -0
  23. package/dist/commands/service.js.map +1 -0
  24. package/dist/commands/storage.d.ts +37 -0
  25. package/dist/commands/storage.d.ts.map +1 -0
  26. package/dist/commands/storage.js +335 -0
  27. package/dist/commands/storage.js.map +1 -0
  28. package/dist/config.d.ts +91 -2
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +244 -11
  31. package/dist/config.js.map +1 -1
  32. package/dist/conformance.d.ts +31 -0
  33. package/dist/conformance.d.ts.map +1 -0
  34. package/dist/conformance.js +371 -0
  35. package/dist/conformance.js.map +1 -0
  36. package/dist/deps.d.ts +6 -39
  37. package/dist/deps.d.ts.map +1 -1
  38. package/dist/deps.js +4 -68
  39. package/dist/deps.js.map +1 -1
  40. package/dist/errors.d.ts +20 -0
  41. package/dist/errors.d.ts.map +1 -0
  42. package/dist/errors.js +25 -0
  43. package/dist/errors.js.map +1 -0
  44. package/dist/flags.d.ts +41 -0
  45. package/dist/flags.d.ts.map +1 -1
  46. package/dist/flags.js +94 -1
  47. package/dist/flags.js.map +1 -1
  48. package/dist/jsonout.d.ts +37 -0
  49. package/dist/jsonout.d.ts.map +1 -0
  50. package/dist/jsonout.js +68 -0
  51. package/dist/jsonout.js.map +1 -0
  52. package/dist/kind.d.ts +48 -0
  53. package/dist/kind.d.ts.map +1 -0
  54. package/dist/kind.js +69 -0
  55. package/dist/kind.js.map +1 -0
  56. package/dist/main.js +509 -4708
  57. package/dist/main.js.map +1 -1
  58. package/dist/mintpage.d.ts +17 -2
  59. package/dist/mintpage.d.ts.map +1 -1
  60. package/dist/mintpage.js +214 -52
  61. package/dist/mintpage.js.map +1 -1
  62. package/dist/output.d.ts +148 -0
  63. package/dist/output.d.ts.map +1 -0
  64. package/dist/output.js +634 -0
  65. package/dist/output.js.map +1 -0
  66. package/dist/ownerops.d.ts +132 -44
  67. package/dist/ownerops.d.ts.map +1 -1
  68. package/dist/ownerops.js +1067 -327
  69. package/dist/ownerops.js.map +1 -1
  70. package/dist/preview.d.ts +81 -0
  71. package/dist/preview.d.ts.map +1 -0
  72. package/dist/preview.js +535 -0
  73. package/dist/preview.js.map +1 -0
  74. package/dist/prompt.d.ts +17 -0
  75. package/dist/prompt.d.ts.map +1 -0
  76. package/dist/prompt.js +19 -0
  77. package/dist/prompt.js.map +1 -0
  78. package/dist/provision.d.ts.map +1 -1
  79. package/dist/provision.js +10 -7
  80. package/dist/provision.js.map +1 -1
  81. package/dist/remote.d.ts +134 -53
  82. package/dist/remote.d.ts.map +1 -1
  83. package/dist/remote.js +352 -46
  84. package/dist/remote.js.map +1 -1
  85. package/dist/riskgate.d.ts +29 -0
  86. package/dist/riskgate.d.ts.map +1 -0
  87. package/dist/riskgate.js +91 -0
  88. package/dist/riskgate.js.map +1 -0
  89. package/dist/scaffold.d.ts +10 -0
  90. package/dist/scaffold.d.ts.map +1 -0
  91. package/dist/scaffold.js +52 -0
  92. package/dist/scaffold.js.map +1 -0
  93. package/dist/schema.d.ts +18 -1
  94. package/dist/schema.d.ts.map +1 -1
  95. package/dist/schema.js +74 -23
  96. package/dist/schema.js.map +1 -1
  97. package/dist/served.d.ts +30 -0
  98. package/dist/served.d.ts.map +1 -0
  99. package/dist/served.js +112 -0
  100. package/dist/served.js.map +1 -0
  101. package/dist/signer.d.ts +13 -0
  102. package/dist/signer.d.ts.map +1 -1
  103. package/dist/signer.js +80 -14
  104. package/dist/signer.js.map +1 -1
  105. package/dist/update-check.d.ts +58 -4
  106. package/dist/update-check.d.ts.map +1 -1
  107. package/dist/update-check.js +92 -11
  108. package/dist/update-check.js.map +1 -1
  109. package/package.json +10 -6
  110. package/skill/SKILL.md +104 -60
  111. package/skill/reference/code-projects.md +87 -13
  112. package/skill/reference/creator-token.md +86 -0
  113. package/skill/reference/decisions.md +61 -0
  114. package/skill/reference/hosting.md +48 -15
  115. package/skill/reference/operating.md +29 -8
  116. package/skill/reference/setup.md +10 -3
  117. package/skill/reference/troubleshooting.md +31 -4
  118. package/dist/inspect.d.ts +0 -48
  119. package/dist/inspect.d.ts.map +0 -1
  120. package/dist/inspect.js +0 -184
  121. package/dist/inspect.js.map +0 -1
  122. package/dist/migrate.d.ts +0 -65
  123. package/dist/migrate.d.ts.map +0 -1
  124. package/dist/migrate.js +0 -180
  125. package/dist/migrate.js.map +0 -1
  126. package/dist/onchain-uri.d.ts +0 -97
  127. package/dist/onchain-uri.d.ts.map +0 -1
  128. package/dist/onchain-uri.js +0 -243
  129. package/dist/onchain-uri.js.map +0 -1
  130. package/dist/upload.d.ts +0 -28
  131. package/dist/upload.d.ts.map +0 -1
  132. package/dist/upload.js +0 -41
  133. package/dist/upload.js.map +0 -1
package/dist/output.js ADDED
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Shared CLI plumbing used by more than one command domain (phase 3's split of main.ts into
3
+ * `commands/*.ts`): the tiny ANSI color/narration helpers every command prints through, the
4
+ * trust-anchor "ensure*" bootstrap wrappers (narrating the SDK's anchors.ts for the factory /
5
+ * renderer / seed-source singletons), and a handful of small cross-domain utilities (port checks,
6
+ * the post-deploy reindex retry, `printServing`, `findRepoRoot`, `keepAlive`, optional-dependency
7
+ * loading for effects). Nothing here is domain-specific — a helper that only ONE command domain
8
+ * used stayed in that domain's `commands/*.ts` file instead.
9
+ */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join as joinPath, resolve as resolvePath } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { AnchorUnavailableError, makeHotSender, makePublicClient, makeWalletClient, normalizeAttributes, parseTraitPairs, resolveChain, detectCanonicalFactory as sdkDetectCanonicalFactory, ensureFactory as sdkEnsureFactory, ensureRenderer as sdkEnsureRenderer, ensureSeedSource as sdkEnsureSeedSource, ensureSeriesCodeFactory as sdkEnsureSeriesCodeFactory, ensureSeriesFactory as sdkEnsureSeriesFactory, ensureOneOfOneEditionFactory as sdkEnsureOneOfOneEditionFactory, ensureEditionFactory as sdkEnsureEditionFactory, ensureEditionCodeFactory as sdkEnsureEditionCodeFactory, resolveScanFloor as sdkResolveScanFloor, sleep, } from '@artblocks/abx-sdk';
14
+ import { resolveBackend, validateRenderStorageCombo } from '@artblocks/abx-storage';
15
+ import { CHAIN, explorerBase, activeBackendId, backendResolution, collectContentLocators, localIndexer, storageOptions, storageOverrides } from './config.js';
16
+ import { looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
17
+ // ── tiny ANSI helpers ───────────────────────────────────────────────────────
18
+ export const c = {
19
+ reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
20
+ green: '\x1b[38;5;115m', purple: '\x1b[38;5;141m', orange: '\x1b[38;5;215m', red: '\x1b[31m',
21
+ };
22
+ export const g = (s) => `${c.green}${s}${c.reset}`;
23
+ export const p = (s) => `${c.purple}${s}${c.reset}`;
24
+ export const dim = (s) => `${c.dim}${s}${c.reset}`;
25
+ export const bold = (s) => `${c.bold}${s}${c.reset}`;
26
+ export let stepN = 0;
27
+ export const step = (s) => console.log(`\n${p(`[${++stepN}]`)} ${bold(s)}`);
28
+ export const ok = (s) => console.log(` ${g('✓')} ${s}`);
29
+ export const info = (s) => console.log(` ${dim(s)}`);
30
+ export const warn = (s) => console.log(` ${c.orange}⚠${c.reset} ${s}`);
31
+ // When the user/agent opts in (--yes), lift the getLogs chunk cap so a large
32
+ // reconstruction proceeds despite a range-limited RPC (otherwise it stops early with
33
+ // guidance — see GetLogsScanTooLargeError + the skill's "Choosing an RPC" decision).
34
+ export function allowLargeScan(flags) {
35
+ if (flags.yes)
36
+ process.env.ABX_GETLOGS_MAX_CHUNKS = String(1e9);
37
+ }
38
+ /**
39
+ * Resolve the scan floor (fromBlock) for an add/index — the CLI-side shell around the SDK's
40
+ * {@link sdkResolveScanFloor}: builds the client, forwards `--from-block`, and narrates its two
41
+ * possible events (the SDK never prints). See the SDK function's doc comment for the "why" (never
42
+ * a silent genesis default).
43
+ */
44
+ export async function resolveScanFloor(address, localFromBlock, flags) {
45
+ const publicClient = makePublicClient({ chainKey: CHAIN });
46
+ return sdkResolveScanFloor(publicClient, address, {
47
+ explicit: flags['from-block'],
48
+ localFromBlock,
49
+ onEvent: (e) => {
50
+ if (e.kind === 'genesis-warning')
51
+ warn('scanning from genesis (block 0) — slow on a range-capped RPC; pass the deploy block as --from-block to index instantly.');
52
+ else
53
+ info(`deploy block ${e.block} ${dim('(discovered on-chain via getCode — the scan floor)')}`);
54
+ },
55
+ });
56
+ }
57
+ // ── ensure the trust anchor exists ───────────────────────────────────────────
58
+ /**
59
+ * The trust-anchor gate: a factory is THE address platforms allowlist by, so a missing
60
+ * manifest entry must NEVER silently mint a new "canonical" — that fragments trust (and
61
+ * a stale SDK would duplicate factories on real chains). Degrade to guidance; deploying
62
+ * a private trust anchor is an explicit choice (`--bootstrap-factory`).
63
+ */
64
+ export function refuseMissingFactory(kind, envVar, reason) {
65
+ throw new Error(`${reason}\n` +
66
+ ` A canonical ${kind} may already exist for '${CHAIN}' — check with the ABX community / update @artblocks/abx-sdk ` +
67
+ `(the shipped manifest: packages/sdk/src/deployments.ts), or set ${envVar}=0x… if you know the address.\n` +
68
+ ` To deploy your OWN trust anchor instead (private chains, sandboxes — platforms won't recognize its clones), ` +
69
+ `re-run with --bootstrap-factory.`);
70
+ }
71
+ /** A `SendTx` that defers `makeWalletClient`/`makeHotSender` construction until the first actual
72
+ * send — trust-anchor bootstrap is rare (an existing, current anchor is the common case), so the
73
+ * happy path must never require a signing key. Memoized so a multi-tx bootstrap (the SeriesCode
74
+ * factory: 2 libraries + the linked factory itself) reuses ONE sender — and its nonce tracking —
75
+ * rather than a fresh one per send. */
76
+ export function lazyBootstrapSender(publicClient) {
77
+ let cached;
78
+ return (tx) => {
79
+ if (!cached) {
80
+ const { wallet, account } = makeWalletClient({ chainKey: CHAIN });
81
+ cached = makeHotSender({ wallet, account, publicClient });
82
+ }
83
+ return cached(tx);
84
+ };
85
+ }
86
+ /** Get (or deploy) the chain's canonical 1/1 clone factory — narrates the SDK's `ensureFactory`
87
+ * (anchors.ts), which owns the resolve/verify/bootstrap logic; this keeps only the exact console
88
+ * output plus the exit-messaging refusal (`refuseMissingFactory`), which stays CLI-side since the
89
+ * SDK doesn't know a UX's flag names. `quiet` suppresses only the happy-path reuse line (the demo
90
+ * resolves the factory without making a teaching moment of it) — everything else always prints. */
91
+ export async function ensureFactory(override, allowBootstrap = false, quiet = false) {
92
+ const publicClient = makePublicClient({ chainKey: CHAIN });
93
+ const send = lazyBootstrapSender(publicClient);
94
+ try {
95
+ return await sdkEnsureFactory(publicClient, send, {
96
+ chainId: resolveChain(CHAIN).id,
97
+ override,
98
+ allowBootstrap,
99
+ onEvent: (e) => {
100
+ if (e.kind === 'resolved') {
101
+ if (!quiet)
102
+ info(`using canonical factory ${explorerBase()}/address/${e.address}`);
103
+ }
104
+ else if (e.kind === 'stale') {
105
+ info(e.reason === 'no-code'
106
+ ? 'configured factory has no code on this chain — deploying a fresh one'
107
+ : 'configured factory is an older/incompatible version — deploying a fresh trust anchor');
108
+ }
109
+ else if (e.kind === 'deploying') {
110
+ info('deploying the canonical clone factory (ownerless trust anchor)…');
111
+ }
112
+ else if (e.kind === 'deployed') {
113
+ ok(`factory ${e.address}`);
114
+ info(`implementation ${e.implementation}`);
115
+ info(`tx ${explorerBase()}/tx/${e.txHash}`);
116
+ warn(`${CHAIN} (chainId ${resolveChain(CHAIN).id}) isn't in the shipped manifest — to reuse this factory set ${bold(`ABX_FACTORY=${e.address}`)} (or add it to packages/sdk/src/deployments.ts).`);
117
+ }
118
+ },
119
+ });
120
+ }
121
+ catch (err) {
122
+ if (err instanceof AnchorUnavailableError) {
123
+ const reason = err.detail === 'not-configured'
124
+ ? `no canonical 1/1 factory is configured for '${CHAIN}'.`
125
+ : err.detail === 'no-code'
126
+ ? `the configured factory ${err.address} has no code on '${CHAIN}' — check ABX_CHAIN / the RPC endpoint.`
127
+ : `the configured factory ${err.address} is an older/incompatible version on '${CHAIN}'.`;
128
+ refuseMissingFactory('factory', 'ABX_FACTORY', reason);
129
+ }
130
+ throw err;
131
+ }
132
+ }
133
+ // ── ensure the canonical Series trust anchor exists ──────────────────────────
134
+ /** Get (or deploy) the chain's `SeriesImageFactory` — the sibling of {@link ensureFactory}.
135
+ * Narrates the SDK's `ensureSeriesFactory` (anchors.ts), which owns the resolve/verify/bootstrap
136
+ * logic. */
137
+ export async function ensureSeriesFactory(override, allowBootstrap = false) {
138
+ const publicClient = makePublicClient({ chainKey: CHAIN });
139
+ const send = lazyBootstrapSender(publicClient);
140
+ try {
141
+ return await sdkEnsureSeriesFactory(publicClient, send, {
142
+ chainId: resolveChain(CHAIN).id,
143
+ override,
144
+ allowBootstrap,
145
+ onEvent: (e) => {
146
+ if (e.kind === 'resolved')
147
+ info(`using canonical Series factory ${explorerBase()}/address/${e.address}`);
148
+ else if (e.kind === 'stale')
149
+ info('configured Series factory is missing or an older version — deploying a fresh trust anchor');
150
+ else if (e.kind === 'deploying')
151
+ info('deploying the canonical Series clone factory (ownerless trust anchor)…');
152
+ else if (e.kind === 'deployed') {
153
+ ok(`Series factory ${e.address}`);
154
+ info(`implementation ${e.implementation}`);
155
+ info(`tx ${explorerBase()}/tx/${e.txHash}`);
156
+ warn(`${CHAIN} (chainId ${resolveChain(CHAIN).id}) isn't in the shipped manifest — to reuse this factory set ${bold(`ABX_SERIES_FACTORY=${e.address}`)} (or add it to packages/sdk/src/deployments.ts).`);
157
+ }
158
+ },
159
+ });
160
+ }
161
+ catch (err) {
162
+ if (err instanceof AnchorUnavailableError) {
163
+ const reason = err.detail === 'not-configured'
164
+ ? `no canonical Series factory is configured for '${CHAIN}'.`
165
+ : `the configured Series factory ${err.address} is missing or an older version on '${CHAIN}'.`;
166
+ refuseMissingFactory('Series factory', 'ABX_SERIES_FACTORY', reason);
167
+ }
168
+ throw err;
169
+ }
170
+ }
171
+ // ── ensure the canonical on-chain renderer exists ────────────────────────────
172
+ /** Get (or deploy) the chain's shared `AbxMetadataRenderer` — narrates the SDK's `ensureRenderer`
173
+ * (anchors.ts), which also self-heals to the CREATE2-deterministic address before deploying. No
174
+ * `allowBootstrap` gate here (unlike the three clone factories) — the renderer isn't a platform
175
+ * allowlist entry, so there's nothing to refuse; a stale/missing one always just gets redeployed. */
176
+ export async function ensureRenderer(override) {
177
+ const publicClient = makePublicClient({ chainKey: CHAIN });
178
+ const send = lazyBootstrapSender(publicClient);
179
+ return sdkEnsureRenderer(publicClient, send, {
180
+ chainId: resolveChain(CHAIN).id,
181
+ override,
182
+ onEvent: (e) => {
183
+ if (e.kind === 'resolved')
184
+ info(`using canonical renderer ${explorerBase()}/address/${e.address}`);
185
+ else if (e.kind === 'stale')
186
+ info('configured renderer is missing or an older spec version — deploying a fresh one');
187
+ else if (e.kind === 'canonical')
188
+ info(`using canonical renderer at its deterministic address ${explorerBase()}/address/${e.address}`);
189
+ else if (e.kind === 'deploying')
190
+ info('deploying the canonical on-chain metadata renderer (CREATE2)…');
191
+ else if (e.kind === 'deployed') {
192
+ ok(`renderer ${e.address} (spec v${e.specVersion})`);
193
+ info(`tx ${explorerBase()}/tx/${e.txHash}`);
194
+ warn(`not in the shipped manifest for ${CHAIN} — to reuse this renderer set ${bold(`ABX_RENDERER=${e.address}`)} (or add it to packages/sdk/src/deployments.ts).`);
195
+ }
196
+ },
197
+ });
198
+ }
199
+ /** The code-project trust anchor — narrates the SDK's `ensureSeriesCodeFactory` (anchors.ts),
200
+ * which owns the resolve/bootstrap logic (a sandbox / fresh chain links + deploys the two
201
+ * write-path libraries then the factory in-flight; see {@link deploySeriesCodeFactory}). */
202
+ export async function ensureSeriesCodeFactory(publicClient, override, allowBootstrap = false) {
203
+ const send = lazyBootstrapSender(publicClient);
204
+ try {
205
+ return await sdkEnsureSeriesCodeFactory(publicClient, send, {
206
+ chainId: resolveChain(CHAIN).id,
207
+ override,
208
+ allowBootstrap,
209
+ onEvent: (e) => {
210
+ if (e.kind === 'resolved')
211
+ info(`using canonical SeriesCode factory ${explorerBase()}/address/${e.address}`);
212
+ else if (e.kind === 'stale')
213
+ info('configured SeriesCode factory has no code on this chain — deploying a fresh trust anchor');
214
+ else if (e.kind === 'deploying')
215
+ info('deploying the SeriesCode trust anchor (2 libraries → linked factory → implementation)…');
216
+ else if (e.kind === 'deployed') {
217
+ ok(`SeriesCode factory ${e.address} ${dim(`(impl ${e.implementation})`)}`);
218
+ info(`libraries: AbxParamsLib ${e.paramsLib} · AbxCodeLib ${e.codeLib}`);
219
+ warn(`${CHAIN} isn't in the shipped manifest — to reuse, set ${bold(`ABX_SERIES_CODE_FACTORY=${e.address}`)} (or add it to packages/sdk/src/deployments.ts).`);
220
+ }
221
+ },
222
+ });
223
+ }
224
+ catch (err) {
225
+ if (err instanceof AnchorUnavailableError) {
226
+ const reason = err.detail === 'not-configured'
227
+ ? `no canonical SeriesCode factory is configured for '${CHAIN}'.`
228
+ : `the configured SeriesCode factory ${err.address} has no code on '${CHAIN}' — check ABX_CHAIN / the RPC endpoint.`;
229
+ refuseMissingFactory('SeriesCode factory', 'ABX_SERIES_CODE_FACTORY', reason);
230
+ }
231
+ throw err;
232
+ }
233
+ }
234
+ // ── ERC-1155 editions: the three edition trust anchors ────────────────────────
235
+ // The edition twins of `ensureFactory`/`ensureSeriesFactory`/`ensureSeriesCodeFactory` above —
236
+ // same narration shape, same `refuseMissingFactory` refusal on `AnchorUnavailableError`. Unlike the
237
+ // 721 1/1 factory, the two CREATE2-deterministic edition factories also report a `canonical` event
238
+ // (self-healed to the predicted address) — see anchors.ts's `AnchorEvent` doc for why.
239
+ /** Get (or deploy) the chain's canonical 1/1-edition clone factory — narrates the SDK's
240
+ * `ensureOneOfOneEditionFactory` (anchors.ts), the edition twin of {@link ensureFactory}. */
241
+ export async function ensureOneOfOneEditionFactory(override, allowBootstrap = false) {
242
+ const publicClient = makePublicClient({ chainKey: CHAIN });
243
+ const send = lazyBootstrapSender(publicClient);
244
+ try {
245
+ return await sdkEnsureOneOfOneEditionFactory(publicClient, send, {
246
+ chainId: resolveChain(CHAIN).id,
247
+ override,
248
+ allowBootstrap,
249
+ onEvent: (e) => {
250
+ if (e.kind === 'resolved')
251
+ info(`using canonical 1/1-edition factory ${explorerBase()}/address/${e.address}`);
252
+ else if (e.kind === 'canonical')
253
+ info(`using canonical 1/1-edition factory at its deterministic address ${explorerBase()}/address/${e.address}`);
254
+ else if (e.kind === 'stale') {
255
+ info(e.reason === 'no-code'
256
+ ? 'configured 1/1-edition factory has no code on this chain — deploying a fresh one'
257
+ : 'configured 1/1-edition factory is an older/incompatible version — deploying a fresh trust anchor');
258
+ }
259
+ else if (e.kind === 'deploying')
260
+ info('deploying the canonical 1/1-edition clone factory (ownerless trust anchor, CREATE2)…');
261
+ else if (e.kind === 'deployed') {
262
+ ok(`1/1-edition factory ${e.address}`);
263
+ info(`implementation ${e.implementation}`);
264
+ info(`tx ${explorerBase()}/tx/${e.txHash}`);
265
+ warn(`${CHAIN} isn't in the shipped manifest — to reuse this factory set ${bold(`ABX_ONE_OF_ONE_EDITION_FACTORY=${e.address}`)} (or add it to packages/sdk/src/deployments.ts).`);
266
+ }
267
+ },
268
+ });
269
+ }
270
+ catch (err) {
271
+ if (err instanceof AnchorUnavailableError) {
272
+ const reason = err.detail === 'not-configured'
273
+ ? `no canonical 1/1-edition factory is configured for '${CHAIN}'.`
274
+ : err.detail === 'no-code'
275
+ ? `the configured 1/1-edition factory ${err.address} has no code on '${CHAIN}' — check ABX_CHAIN / the RPC endpoint.`
276
+ : `the configured 1/1-edition factory ${err.address} is an older/incompatible version on '${CHAIN}'.`;
277
+ refuseMissingFactory('1/1-edition factory', 'ABX_ONE_OF_ONE_EDITION_FACTORY', reason);
278
+ }
279
+ throw err;
280
+ }
281
+ }
282
+ /** Get (or deploy) the chain's `EditionImageFactory` — narrates the SDK's `ensureEditionFactory`
283
+ * (anchors.ts), the edition twin of {@link ensureSeriesFactory}. */
284
+ export async function ensureEditionFactory(override, allowBootstrap = false) {
285
+ const publicClient = makePublicClient({ chainKey: CHAIN });
286
+ const send = lazyBootstrapSender(publicClient);
287
+ try {
288
+ return await sdkEnsureEditionFactory(publicClient, send, {
289
+ chainId: resolveChain(CHAIN).id,
290
+ override,
291
+ allowBootstrap,
292
+ onEvent: (e) => {
293
+ if (e.kind === 'resolved')
294
+ info(`using canonical edition factory ${explorerBase()}/address/${e.address}`);
295
+ else if (e.kind === 'canonical')
296
+ info(`using canonical edition factory at its deterministic address ${explorerBase()}/address/${e.address}`);
297
+ else if (e.kind === 'stale') {
298
+ info(e.reason === 'no-code'
299
+ ? 'configured edition factory has no code on this chain — deploying a fresh one'
300
+ : 'configured edition factory is an older/incompatible version — deploying a fresh trust anchor');
301
+ }
302
+ else if (e.kind === 'deploying')
303
+ info('deploying the canonical multi-artwork edition clone factory (ownerless trust anchor, CREATE2)…');
304
+ else if (e.kind === 'deployed') {
305
+ ok(`edition factory ${e.address}`);
306
+ info(`implementation ${e.implementation}`);
307
+ info(`tx ${explorerBase()}/tx/${e.txHash}`);
308
+ warn(`${CHAIN} isn't in the shipped manifest — to reuse this factory set ${bold(`ABX_EDITION_FACTORY=${e.address}`)} (or add it to packages/sdk/src/deployments.ts).`);
309
+ }
310
+ },
311
+ });
312
+ }
313
+ catch (err) {
314
+ if (err instanceof AnchorUnavailableError) {
315
+ const reason = err.detail === 'not-configured'
316
+ ? `no canonical edition factory is configured for '${CHAIN}'.`
317
+ : err.detail === 'no-code'
318
+ ? `the configured edition factory ${err.address} has no code on '${CHAIN}' — check ABX_CHAIN / the RPC endpoint.`
319
+ : `the configured edition factory ${err.address} is an older/incompatible version on '${CHAIN}'.`;
320
+ refuseMissingFactory('edition factory', 'ABX_EDITION_FACTORY', reason);
321
+ }
322
+ throw err;
323
+ }
324
+ }
325
+ /** The code-project EDITION trust anchor — narrates the SDK's `ensureEditionCodeFactory`
326
+ * (anchors.ts), the edition twin of {@link ensureSeriesCodeFactory} (three linked libraries
327
+ * instead of two — see `deployEditionCodeFactory`'s doc note). */
328
+ export async function ensureEditionCodeFactory(publicClient, override, allowBootstrap = false) {
329
+ const send = lazyBootstrapSender(publicClient);
330
+ try {
331
+ return await sdkEnsureEditionCodeFactory(publicClient, send, {
332
+ chainId: resolveChain(CHAIN).id,
333
+ override,
334
+ allowBootstrap,
335
+ onEvent: (e) => {
336
+ if (e.kind === 'resolved')
337
+ info(`using canonical EditionCode factory ${explorerBase()}/address/${e.address}`);
338
+ else if (e.kind === 'stale')
339
+ info('configured EditionCode factory has no code on this chain — deploying a fresh trust anchor');
340
+ else if (e.kind === 'deploying')
341
+ info('deploying the EditionCode trust anchor (3 libraries → linked factory → implementation)…');
342
+ else if (e.kind === 'deployed') {
343
+ ok(`EditionCode factory ${e.address} ${dim(`(impl ${e.implementation})`)}`);
344
+ info(`libraries: AbxParamsLib ${e.paramsLib} · AbxCodeLib ${e.codeLib} · AbxEditionLib ${e.editionLib}`);
345
+ warn(`${CHAIN} isn't in the shipped manifest — to reuse, set ${bold(`ABX_EDITION_CODE_FACTORY=${e.address}`)} (or add it to packages/sdk/src/deployments.ts).`);
346
+ }
347
+ },
348
+ });
349
+ }
350
+ catch (err) {
351
+ if (err instanceof AnchorUnavailableError) {
352
+ const reason = err.detail === 'not-configured'
353
+ ? `no canonical EditionCode factory is configured for '${CHAIN}'.`
354
+ : `the configured EditionCode factory ${err.address} has no code on '${CHAIN}' — check ABX_CHAIN / the RPC endpoint.`;
355
+ refuseMissingFactory('EditionCode factory', 'ABX_EDITION_CODE_FACTORY', reason);
356
+ }
357
+ throw err;
358
+ }
359
+ }
360
+ /** The canonical seed source — narrates the SDK's `ensureSeedSource` (anchors.ts): a clean keyless
361
+ * singleton (like the renderer / chunk store / minter), so CREATE2 lands at THE canonical address
362
+ * on any chain. Resolves from the manifest, self-heals to the predicted address (deployed but not
363
+ * yet repointed), else auto-deploys via CREATE2. */
364
+ export async function ensureSeedSource(publicClient) {
365
+ const send = lazyBootstrapSender(publicClient);
366
+ return sdkEnsureSeedSource(publicClient, send, {
367
+ chainId: resolveChain(CHAIN).id,
368
+ onEvent: (e) => {
369
+ if (e.kind === 'canonical')
370
+ info(`using the canonical seed source at its deterministic address ${e.address}`);
371
+ else if (e.kind === 'deployed')
372
+ ok(`AbxSeedSource ${e.address} ${dim('(deployed via CREATE2 — set ABX_SEED_SOURCE to reuse)')}`);
373
+ },
374
+ });
375
+ }
376
+ /**
377
+ * Refuse a port that's already bound, with a formatted one-liner naming the port and the fix.
378
+ *
379
+ * Without this, `listen()` has no `'error'` handler and EADDRINUSE reaches Node's default handler:
380
+ * the creator gets a raw stack trace through `node:net` and our own `dist/` paths, which reads as a
381
+ * crash inside abx rather than "something else is on this port" — and it's the one unformatted error
382
+ * surface in a CLI where every other error is formatted. `preview`'s default port colliding with a
383
+ * studio left running in another terminal was reported as especially hard to diagnose.
384
+ */
385
+ export async function assertPortFree(port, cmd) {
386
+ if (!(await portInUse(port)))
387
+ return;
388
+ throw new Error(`port ${port} is already in use — most likely an \`abx ${cmd}\`/\`abx serve\`/\`abx preview\` still running in another terminal.\n` +
389
+ ` Stop that one (Ctrl-C), or run this on a different port: \`abx ${cmd} --port ${port + 1}\`.`);
390
+ }
391
+ /** Is a TCP port already bound on localhost? Used to preflight a serve BEFORE spending a tx. */
392
+ export async function portInUse(port) {
393
+ const { createServer } = await import('node:net');
394
+ return new Promise((resolve) => {
395
+ const probe = createServer();
396
+ probe.once('error', (e) => resolve(e.code === 'EADDRINUSE'));
397
+ probe.once('listening', () => probe.close(() => resolve(false)));
398
+ probe.listen(port);
399
+ });
400
+ }
401
+ /**
402
+ * Index a project we *just* deployed — and don't believe a zero.
403
+ *
404
+ * `eth_getLogs` is read-after-write inconsistent on load-balanced RPCs: `waitForTransactionReceipt`
405
+ * resolves against a node that has the block, then the log query lands on one that doesn't yet, and
406
+ * returns an empty set for a block we KNOW contains our deploy. The old code took that single read at
407
+ * face value, printed `✓ reconstructed 0 events`, stored the empty projection, and served an empty
408
+ * dashboard — a first-run that looks like the toolkit simply doesn't work. It reproduced 100% of the
409
+ * time on `https://sepolia.base.org`, which is the DEFAULT endpoint when there's no `.env`, i.e. the
410
+ * documented first run was the broken path. The same block returned all 9 logs seconds later.
411
+ *
412
+ * We have the one thing that makes this checkable: we just minted, so the spine cannot be empty.
413
+ * So verify instead of trusting — re-scan at a constant delay until events appear, and if they
414
+ * never do, say so as a FAILURE with the recovery command rather than dressing a zero up as a ✓.
415
+ */
416
+ export async function reindexAfterDeploy(indexer, address, opts = {}) {
417
+ const attempts = opts.attempts ?? 6;
418
+ const delayMs = opts.delayMs ?? 1500;
419
+ // Off by default (the SDK reconstruct default): composed contractURI/tokenURI documents are a
420
+ // live read, never projected. `abx demo`'s read-back step is the one caller that passes `true`
421
+ // — see `walkthroughReadBack`.
422
+ const readUriDocuments = opts.readUriDocuments;
423
+ let last = await indexer.reindex(address, { readUriDocuments });
424
+ for (let i = 1; i < attempts && last.state.eventCount === 0; i++) {
425
+ if (i === 1) {
426
+ info(dim("no events yet — the RPC hasn't served the logs for that block; re-scanning…"));
427
+ }
428
+ await sleep(delayMs);
429
+ // `full: true` — the stored projection has 0 events, so there is no valid checkpoint to
430
+ // resume from; a full replay from the deploy block is the only correct re-scan.
431
+ last = await indexer.reindex(address, { full: true, readUriDocuments });
432
+ }
433
+ if (last.state.eventCount === 0) {
434
+ warn(`the RPC still reports no logs for this project after ${attempts} tries — the deploy DID succeed ` +
435
+ `(it's on chain), but this node can't reconstruct it yet.`);
436
+ console.log(` ${dim('recover with')} ${bold(`abx index ${address} --full`)} ${dim('in a minute, or point ABX_RPC_URLS at a better endpoint (`abx doctor` ranks them).')}`);
437
+ }
438
+ return last;
439
+ }
440
+ // Effects (Playwright + Chromium) is an OPTIONAL dependency of the CLI, so the default
441
+ // `npm i -g @artblocks/abx-cli` stays small + browserless (the resolver serves read-only and never
442
+ // renders). Only `abx render` and `abx effects` need it — load it lazily here so a missing optional
443
+ // dep surfaces ONLY at those call sites, with a fix, instead of breaking startup for everyone.
444
+ export async function loadEffects() {
445
+ try {
446
+ return await import('@artblocks/abx-effects');
447
+ }
448
+ catch {
449
+ throw new Error("effects support isn't installed (the optional @artblocks/abx-effects package + Playwright). " +
450
+ 'Install it with `npm i -g @artblocks/abx-effects`, or reinstall the CLI without --no-optional. ' +
451
+ 'Only `abx render` and `abx effects` need it; everything else runs browserless.');
452
+ }
453
+ }
454
+ /**
455
+ * The publish topology's one hard prerequisite, checked BEFORE any capture.
456
+ *
457
+ * A resolver that doesn't share this machine's disk serves referenced output (the still, a video, a
458
+ * model) by **redirect** — so it needs a URL, and it refuses the bytes (`specs/protocol/effects.md →
459
+ * Bound vs referenced`). A backend that can't name a locator therefore has no publish lane at all,
460
+ * and every render against one would be work spent to earn a 400.
461
+ *
462
+ * So this refuses up front and names the ways out, rather than warning and letting the render run.
463
+ * It deliberately does NOT pick a backend for the operator: which store holds their art — and which
464
+ * gateway serves it — is theirs to decide. The three that work are peers, not a ranking: `https://`
465
+ * from S3/R2, an IPFS gateway, and Arweave are all fine, because derived output is re-creatable and
466
+ * the only real requirement is that a third party can fetch it.
467
+ */
468
+ export function requirePublishableBackend(flags, what) {
469
+ const backend = resolveBackend(storageOptions(storageOverrides(flags)));
470
+ const { backend: name, source } = backendResolution(storageOverrides(flags));
471
+ // The OK/NOT-OK decision is the shared render×storage validator (packages/storage/content-plan.ts)
472
+ // — the same one deploy-code's --image-base guard and dry-run row consult, so this combination
473
+ // can't drift into two disagreeing ideas of "publishable." The wording below stays CLI-specific
474
+ // (it names the exact env vars + every way out) — only the verdict is shared. `backend.id` (not
475
+ // `name`) — `resolveBackend` normalizes the `s3` alias to `cloud`, and the validator's backend set
476
+ // is keyed on the normalized id.
477
+ const result = validateRenderStorageCombo({ backendId: backend.id, cloudHasPublicBase: backend.id === 'cloud' ? !!backend.publicBase : undefined, publishesToRemoteResolver: true });
478
+ if (result.ok)
479
+ return;
480
+ // `cloud` is the one backend that HAS a locator method and can still return null: the URL is
481
+ // `<publicBase>/<key>`, so without a public base it can name nothing. Catch that here too — it
482
+ // would otherwise be the same failure one render later.
483
+ if (name === 'cloud' && !backend.publicBase) {
484
+ throw new Error(`${what} hands the resolver a URL for each render, but the 'cloud' backend has no public read base — ` +
485
+ `set ${bold('ABX_S3_PUBLIC_BASE')} to the bucket's public URL (an R2/CloudFront/S3-website base), ` +
486
+ `or use ${bold('--backend ipfs')} / ${bold('--backend arweave')}, or render co-located with the resolver.`);
487
+ }
488
+ throw new Error(`${what} publishes each render to a resolver that doesn't share this machine's disk, but the '${name}' backend ` +
489
+ `(${source === 'default' ? 'the default' : `from ${source}`}) can't produce a URL for what it stores — so the resolver would have nothing to serve.\n` +
490
+ ` Point it at a backend that can (equal options — pick on cost/ops, not durability dogma):\n` +
491
+ ` ${bold('--backend cloud')} S3 / R2 / B2 with a public base (ABX_S3_* + ABX_S3_PUBLIC_BASE)\n` +
492
+ ` ${bold('--backend ipfs')} Pinata or your own Kubo + a public gateway (PINATA_JWT / ABX_IPFS_*)\n` +
493
+ ` ${bold('--backend arweave')} pay-once permanent (uploads under 100KiB are free, no setup)\n` +
494
+ ` …or run co-located instead: ${bold('abx effects')} on the same host as ${bold('abx serve')}, sharing one backend and no remote token.`);
495
+ }
496
+ // ── shared output ─────────────────────────────────────────────────────────--
497
+ export function printServing(url, address) {
498
+ console.log(`\n ${g('●')} ${bold('serving')} ${url}`);
499
+ console.log(` ${dim('dashboard ')}${url}`);
500
+ console.log(` ${dim('storage ')}${activeBackendId()}`);
501
+ if (address) {
502
+ const cid = resolveChain(CHAIN).id;
503
+ console.log(` ${dim('tokenURI ')}${url}/t/${cid}/${address}/0`);
504
+ console.log(` ${dim('image ')}${url}/t/${cid}/${address}/0/image`);
505
+ console.log(` ${dim('state API ')}${url}/api/project/${address}`);
506
+ }
507
+ // The dashboard is READ-ONLY: re-index/verify are admin actions that 404 unless the node has an
508
+ // ABX_RESOLVER_ADMIN_TOKEN, so there is no button to press. This line used to say "hit Re-index
509
+ // from chain", which sent every first-run user hunting for a control that isn't there.
510
+ console.log(`\n ${dim('The dashboard shows the event spine it replayed — that table IS the reconstruction.')}`);
511
+ if (address) {
512
+ console.log(` ${dim('Rebuild it yourself (read-only, safe):')} ${bold(`abx index ${address} --full`)} ${dim('— replays from the deploy block and must land on identical state.')}`);
513
+ }
514
+ console.log(` ${dim('Ctrl-C to stop.')}\n`);
515
+ }
516
+ /** Walk up from the CLI source to the workspace root (the dir with pnpm-workspace.yaml). Returns
517
+ * null when running from a published install (no workspace) — where --from-source doesn't apply. */
518
+ export function findRepoRoot() {
519
+ let dir = resolvePath(fileURLToPath(import.meta.url), '..');
520
+ for (let i = 0; i < 8; i++) {
521
+ if (existsSync(joinPath(dir, 'pnpm-workspace.yaml')))
522
+ return dir;
523
+ const parent = resolvePath(dir, '..');
524
+ if (parent === dir)
525
+ break;
526
+ dir = parent;
527
+ }
528
+ return null;
529
+ }
530
+ export function keepAlive() {
531
+ process.on('SIGINT', () => {
532
+ console.log(dim('\n stopped.'));
533
+ process.exit(0);
534
+ });
535
+ return new Promise(() => { });
536
+ }
537
+ /**
538
+ * Pick the canonical factory for a clone being added — the CLI-side shell around the SDK's
539
+ * {@link sdkDetectCanonicalFactory} (which does the actual multicall probe; see its doc comment).
540
+ * Shared by `add`'s local + remote paths (project.ts) and the deploy family's post-setup local
541
+ * register (deploy.ts, via {@link registerAndIndexLocally}).
542
+ */
543
+ export async function detectCanonicalFactory(address, override, stored) {
544
+ const publicClient = makePublicClient({ chainKey: CHAIN });
545
+ return sdkDetectCanonicalFactory(publicClient, address, { chainId: resolveChain(CHAIN).id, override, stored });
546
+ }
547
+ /**
548
+ * Register + index a project LOCALLY (this node's own SQLite-backed projection) — the core of
549
+ * `abx add`'s non-`--remote` path, factored out here because it's ALSO how `deploy` / `deploy-series`
550
+ * / `deploy-code` register the project they just deployed (see cmdAdd in commands/project.ts, whose
551
+ * local branch is a thin call to this). Kept self-sufficient (recomputes any `--attributes`/`--traits`
552
+ * edit from `flags` itself) so it never depends on a caller's local variables.
553
+ */
554
+ export async function registerAndIndexLocally(address, flags) {
555
+ const attrRaw = flags.attributes ? readFileSync(resolvePath(process.cwd(), String(flags.attributes)), 'utf8') : undefined;
556
+ const perTokenEdit = attrRaw != null && looksPerTokenAttributes(attrRaw);
557
+ const flagTraits = [];
558
+ if (attrRaw != null && !perTokenEdit)
559
+ flagTraits.push(...normalizeAttributes(JSON.parse(attrRaw)));
560
+ if (flags.traits)
561
+ flagTraits.push(...parseTraitPairs(flags.traits));
562
+ const editedTokenAttributes = perTokenEdit ? parseSeriesTraitsById(attrRaw) : undefined;
563
+ allowLargeScan(flags);
564
+ const indexer = localIndexer();
565
+ const existingReg = indexer.store.getRegistration(address);
566
+ const factory = await detectCanonicalFactory(address, flags.factory, existingReg?.factory);
567
+ // Resolve the scan floor once — the deploy block, never a silent genesis default (see
568
+ // resolveScanFloor). Reused for both register() writes so discovery runs at most once.
569
+ const scanFloor = await resolveScanFloor(address, existingReg?.fromBlock, flags);
570
+ // register() is a full-column upsert, so EVERY off-chain field must be edit-or-preserve here: a flag
571
+ // edits it, its absence keeps what the deploy stored. Omitting one would silently null it on a plain
572
+ // re-index (the clobber bug). Mirrors the remote path's fall-back-to-localReg for the same reason.
573
+ const editDescription = flags.description ?? existingReg?.description ?? undefined;
574
+ const editExternalUrl = flags['external-url'] ?? existingReg?.externalUrl ?? undefined;
575
+ const editAttributes = flagTraits.length ? JSON.stringify(flagTraits) : existingReg?.attributes ?? undefined;
576
+ const editTokenAttributes = editedTokenAttributes && Object.keys(editedTokenAttributes).length
577
+ ? JSON.stringify(editedTokenAttributes)
578
+ : existingReg?.tokenAttributes ?? undefined;
579
+ indexer.register({
580
+ address,
581
+ chainKey: CHAIN,
582
+ fromBlock: scanFloor,
583
+ factory: factory ?? null,
584
+ label: flags.label,
585
+ description: editDescription,
586
+ externalUrl: editExternalUrl,
587
+ attributes: editAttributes,
588
+ tokenAttributes: editTokenAttributes,
589
+ });
590
+ // Set expectations before a potentially multi-minute scan. `eth_getLogs` from a far-back floor
591
+ // auto-chunks but emits no per-block output, so a large span reads as "hung" and invites a Ctrl-C
592
+ // that aborts the index (the #1 add/index friction). Print the span up front when it's large;
593
+ // stay silent on a fresh deploy (tiny recent window). Advisory only — one cheap head read.
594
+ try {
595
+ const prior = indexer.getProject(address);
596
+ const start = prior?.toBlock ? BigInt(prior.toBlock) : BigInt(scanFloor);
597
+ const head = await makePublicClient({ chainKey: CHAIN }).getBlockNumber();
598
+ const span = head - start;
599
+ if (span > 5000n)
600
+ info(`scanning blocks ${start}${dim(' → ')}${head} ${dim(`(~${span} blocks)`)} — on a range-capped RPC (see ${g('abx doctor')}) this can take a few minutes with no per-block output; leave it running.`);
601
+ }
602
+ catch { /* advisory only — the real scan still runs */ }
603
+ // Don't accept a zero here either: `deploy-code` finishes through this command, so this IS the
604
+ // post-deploy index for a code project — and a real ABX clone always emits a spine (its extension
605
+ // registrations at minimum), so 0 events means the RPC hasn't served the logs yet, not that the
606
+ // project is empty. See reindexAfterDeploy.
607
+ const { state, elapsedMs } = await reindexAfterDeploy(indexer, address);
608
+ // Resolve durable locators for off-chain-by-hash content from this machine's index.
609
+ const locators = await collectContentLocators(state, resolveBackend(storageOptions(storageOverrides(flags))));
610
+ if (Object.keys(locators).length) {
611
+ indexer.register({
612
+ address,
613
+ chainKey: CHAIN,
614
+ fromBlock: scanFloor,
615
+ factory: factory ?? null,
616
+ label: flags.label,
617
+ description: editDescription,
618
+ externalUrl: editExternalUrl,
619
+ attributes: editAttributes,
620
+ tokenAttributes: editTokenAttributes,
621
+ contentLocators: JSON.stringify(locators),
622
+ });
623
+ }
624
+ // A ✓ on 0 events is the lie that produced an empty dashboard; reindexAfterDeploy has already
625
+ // explained the failure and named the recovery command, so don't stamp it as success too.
626
+ if (state.eventCount > 0) {
627
+ ok(`registered + indexed ${state.name ?? address} LOCALLY (this machine): ${state.eventCount} events in ${elapsedMs}ms`);
628
+ }
629
+ else {
630
+ warn(`registered ${state.name ?? address}, but with NO reconstructed state — it will serve empty until the index succeeds.`);
631
+ }
632
+ info(`serve it from here with ${bold('abx serve')} — or push it to a hosted resolver with ${bold('abx add ' + address + ' --remote')}`);
633
+ }
634
+ //# sourceMappingURL=output.js.map