@artblocks/abx-cli 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +332 -14
- package/dist/main.js.map +1 -1
- package/dist/preview.d.ts +63 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +483 -0
- package/dist/preview.js.map +1 -0
- package/package.json +6 -3
- package/skill/SKILL.md +49 -5
- package/skill/reference/code-projects.md +35 -0
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)
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
* the tx for a multisig / offline signer). The agent picks the lane; the CLI signs.
|
|
43
44
|
*/
|
|
44
45
|
import { appendFileSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
45
|
-
import { randomBytes } from 'node:crypto';
|
|
46
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
46
47
|
import { basename, extname, join as joinPath, resolve as resolvePath } from 'node:path';
|
|
47
48
|
import { fileURLToPath } from 'node:url';
|
|
48
49
|
import { homedir } from 'node:os';
|
|
@@ -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
|
|
@@ -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);
|
|
@@ -983,8 +986,38 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
983
986
|
// editable later via `abx add --traits`); `--traits-onchain` inlines them on-chain (durable,
|
|
984
987
|
// lockable) — mirrors the description model. On-chain wins if both ever set the same trait.
|
|
985
988
|
const traits = parseDeployTraits(flags);
|
|
986
|
-
console.log(
|
|
987
|
-
|
|
989
|
+
console.log(serveAfter
|
|
990
|
+
? bold(`\n ABX — guided first run\n ${dim('a token deployed, then rebuilt from the chain alone')}`)
|
|
991
|
+
: bold(`\n ABX Self-Host Toolkit — deploy\n ${dim('a project, served from chain alone')}`));
|
|
992
|
+
// Signing lane. `demo` used to hard-force the hot lane, which SILENTLY dropped `--sign`: with no
|
|
993
|
+
// env key it died confusingly, and WITH one it signed from that key while the operator had asked
|
|
994
|
+
// for their browser wallet — a signing choke point that ignored the lane it was handed. The demo
|
|
995
|
+
// deploy is a single tx, so the wallet lane works here exactly as it does for `deploy`.
|
|
996
|
+
const lane = laneFromFlags(flags);
|
|
997
|
+
// The cold lane only PRINTS a tx; demo's whole point is to index + serve what it just deployed,
|
|
998
|
+
// and there is nothing to index until someone broadcasts. Refuse the combo instead of doing
|
|
999
|
+
// half the job — `abx deploy --unsigned` is the command for that lane.
|
|
1000
|
+
if (serveAfter && lane === 'unsigned') {
|
|
1001
|
+
throw new Error('`abx demo --unsigned` can\'t work: the cold lane only prints a transaction, and the demo ' +
|
|
1002
|
+
'indexes + serves the contract it just deployed. Use `abx demo` (hot key) or `abx demo --sign` ' +
|
|
1003
|
+
'(browser wallet) — or `abx deploy --unsigned` if you only want the raw tx.');
|
|
1004
|
+
}
|
|
1005
|
+
// Same shape for --dry-run: previewing sends nothing, so there is nothing to serve.
|
|
1006
|
+
if (serveAfter && flags['dry-run']) {
|
|
1007
|
+
throw new Error('`abx demo --dry-run` can\'t work: a dry run sends nothing, and the demo indexes + serves what ' +
|
|
1008
|
+
'it deployed. Use `abx deploy --dry-run` to preview a 1/1 deploy without sending.');
|
|
1009
|
+
}
|
|
1010
|
+
// Port preflight BEFORE anything irreversible. The demo ends by serving, and `listen` used to be
|
|
1011
|
+
// the first thing to discover the port was taken — after the deploy tx had already been signed and
|
|
1012
|
+
// paid for, so a second `abx demo` (a very normal thing to try) spent gas and then died with a raw
|
|
1013
|
+
// Node EADDRINUSE stack trace. Check first, and name the fix.
|
|
1014
|
+
if (serveAfter) {
|
|
1015
|
+
const wanted = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
|
|
1016
|
+
if (await portInUse(wanted)) {
|
|
1017
|
+
throw new Error(`port ${wanted} is already in use — probably an \`abx demo\`/\`abx serve\` still running in another terminal.\n` +
|
|
1018
|
+
` Nothing was deployed. Stop that one (Ctrl-C), or run this on another port: \`abx demo --port ${wanted + 1}\`.`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
988
1021
|
const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
|
|
989
1022
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
990
1023
|
// Verify the RPC really is CHAIN before any send (factory/renderer/staging/deploy). A dry run
|
|
@@ -1030,6 +1063,12 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1030
1063
|
else {
|
|
1031
1064
|
factory = await ensureFactory(flags.factory, !!flags['bootstrap-factory']);
|
|
1032
1065
|
}
|
|
1066
|
+
// The demo explains what it just did; `deploy` stays terse. The authenticity point is the least
|
|
1067
|
+
// obvious thing about ABX and the easiest to get wrong, so it's worth a sentence here.
|
|
1068
|
+
if (serveAfter) {
|
|
1069
|
+
info(dim('why it matters: a contract counts as ABX because THIS factory made it (isAbxClone),'));
|
|
1070
|
+
info(dim('not because it emitted an event claiming to be — anyone can emit that event.'));
|
|
1071
|
+
}
|
|
1033
1072
|
// --onchain-uri: resolve tokenURI/contractURI fully on-chain via the canonical renderer
|
|
1034
1073
|
// (the JSON is assembled from on-chain fields → no resolver needed, ever). The off-chain
|
|
1035
1074
|
// URLs are still baked as a fallback if the renderer is later cleared.
|
|
@@ -1056,7 +1095,13 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1056
1095
|
renderer = await ensureRenderer(flags.renderer);
|
|
1057
1096
|
}
|
|
1058
1097
|
}
|
|
1059
|
-
step(`Deploy a ${DIMENSIONS[dimension].label} (--type ${dimension}) to ${CHAIN}`);
|
|
1098
|
+
step(serveAfter ? `Deploy — one transaction to ${CHAIN}` : `Deploy a ${DIMENSIONS[dimension].label} (--type ${dimension}) to ${CHAIN}`);
|
|
1099
|
+
// The custody split is the single most useful thing to understand about an ABX token, and it's
|
|
1100
|
+
// invisible unless someone says it out loud. Demo only — `deploy` prints the same facts per-field.
|
|
1101
|
+
if (serveAfter) {
|
|
1102
|
+
info(dim(`on chain: name · symbol · royalty · a tokenURI base · a keccak256 commitment to the image`));
|
|
1103
|
+
info(dim(`NOT on chain: the image bytes themselves — this node serves them, the chain pins their hash`));
|
|
1104
|
+
}
|
|
1060
1105
|
// Off-chain custody (no renderer) bakes the resolver URL straight into the on-chain
|
|
1061
1106
|
// tokenURI/contractURI at deploy. A localhost / loopback URL there resolves for NO ONE —
|
|
1062
1107
|
// not marketplaces, not wallets, not even your own browser unless `abx serve` is running —
|
|
@@ -1354,10 +1399,13 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1354
1399
|
attributes: offChainTraits,
|
|
1355
1400
|
};
|
|
1356
1401
|
indexer.register(baseReg);
|
|
1357
|
-
const { state, elapsedMs } = await indexer
|
|
1358
|
-
|
|
1402
|
+
const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone);
|
|
1403
|
+
if (state.eventCount > 0)
|
|
1404
|
+
ok(`reconstructed ${state.eventCount} events in ${elapsedMs}ms — no provider involved`);
|
|
1359
1405
|
info(`name "${state.name}" · owner ${state.owner} · canonical: ${state.isCanonical ? 'yes (factory-verified)' : 'unverified'}`);
|
|
1360
1406
|
info(`extensions: ${state.extensions.map((e) => e.name).join(', ') || 'none'}`);
|
|
1407
|
+
if (serveAfter)
|
|
1408
|
+
walkthroughSpine(state);
|
|
1361
1409
|
// For off-chain custody (image committed as keccak256), resolve the durable locator (ipfs://…)
|
|
1362
1410
|
// from this machine's content index and store it on the registration — so the LOCAL resolver
|
|
1363
1411
|
// points `image` at IPFS, and `abx add --remote` can ship it to a hosted one (the localhost-image fix).
|
|
@@ -1406,8 +1454,17 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1406
1454
|
}
|
|
1407
1455
|
return;
|
|
1408
1456
|
}
|
|
1409
|
-
|
|
1457
|
+
// The demo's teaching sections — see the walkthrough helpers. `deploy` skips them: someone
|
|
1458
|
+
// shipping real work doesn't need their projection deleted to make a point.
|
|
1459
|
+
if (serveAfter)
|
|
1460
|
+
await walkthroughRebuild(indexer, clone, state);
|
|
1461
|
+
// Start the server BEFORE the read-back step (which fetches the served metadata over HTTP), but
|
|
1462
|
+
// print the serve banner after it — otherwise the "Serve" step header lands with nothing under it
|
|
1463
|
+
// while the read-back prints below, which reads like the step failed.
|
|
1410
1464
|
const { url } = await startTokenApiServer({ indexer, port, baseUrl, storage: resolveBackend(storageOptions()) });
|
|
1465
|
+
if (serveAfter)
|
|
1466
|
+
await walkthroughReadBack(state, url);
|
|
1467
|
+
step('Serve the token API + dashboard');
|
|
1411
1468
|
printServing(url, clone);
|
|
1412
1469
|
keepAlive();
|
|
1413
1470
|
}
|
|
@@ -1878,8 +1935,9 @@ async function cmdDeploySeries(flags) {
|
|
|
1878
1935
|
tokenAttributes: offChainTokenTraits,
|
|
1879
1936
|
};
|
|
1880
1937
|
indexer.register(baseReg);
|
|
1881
|
-
const { state, elapsedMs } = await indexer
|
|
1882
|
-
|
|
1938
|
+
const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone);
|
|
1939
|
+
if (state.eventCount > 0)
|
|
1940
|
+
ok(`reconstructed ${state.eventCount} events in ${elapsedMs}ms — ${state.tokens.length} token(s), max ${state.maxInvocations}`);
|
|
1883
1941
|
info(`extensions: ${state.extensions.map((e) => e.name).join(', ') || 'none'}`);
|
|
1884
1942
|
// Off-chain custody: bridge each token's keccak → durable locator so the resolver (local and,
|
|
1885
1943
|
// via `abx add --remote`, a hosted one) points images off this node.
|
|
@@ -2143,7 +2201,11 @@ async function cmdAdd(address, flags) {
|
|
|
2143
2201
|
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.`);
|
|
2144
2202
|
}
|
|
2145
2203
|
catch { /* advisory only — the real scan still runs */ }
|
|
2146
|
-
|
|
2204
|
+
// Don't accept a zero here either: `deploy-code` finishes through this command, so this IS the
|
|
2205
|
+
// post-deploy index for a code project — and a real ABX clone always emits a spine (its extension
|
|
2206
|
+
// registrations at minimum), so 0 events means the RPC hasn't served the logs yet, not that the
|
|
2207
|
+
// project is empty. See reindexAfterDeploy.
|
|
2208
|
+
const { state, elapsedMs } = await reindexAfterDeploy(indexer, address);
|
|
2147
2209
|
// Resolve durable locators for off-chain-by-hash content from this machine's index.
|
|
2148
2210
|
const locators = await collectContentLocators(state, resolveBackend(storageOptions(storageOverrides(flags))));
|
|
2149
2211
|
if (Object.keys(locators).length) {
|
|
@@ -2160,7 +2222,14 @@ async function cmdAdd(address, flags) {
|
|
|
2160
2222
|
contentLocators: JSON.stringify(locators),
|
|
2161
2223
|
});
|
|
2162
2224
|
}
|
|
2163
|
-
|
|
2225
|
+
// A ✓ on 0 events is the lie that produced an empty dashboard; reindexAfterDeploy has already
|
|
2226
|
+
// explained the failure and named the recovery command, so don't stamp it as success too.
|
|
2227
|
+
if (state.eventCount > 0) {
|
|
2228
|
+
ok(`registered + indexed ${state.name ?? address} LOCALLY (this machine): ${state.eventCount} events in ${elapsedMs}ms`);
|
|
2229
|
+
}
|
|
2230
|
+
else {
|
|
2231
|
+
warn(`registered ${state.name ?? address}, but with NO reconstructed state — it will serve empty until the index succeeds.`);
|
|
2232
|
+
}
|
|
2164
2233
|
info(`serve it from here with ${bold('abx serve')} — or push it to a hosted resolver with ${bold('abx add ' + address + ' --remote')}`);
|
|
2165
2234
|
}
|
|
2166
2235
|
/** The code-project trust anchor: use the canonical factory, else deploy one (a sandbox /
|
|
@@ -2227,6 +2296,230 @@ async function ensureSeedSource(publicClient) {
|
|
|
2227
2296
|
* Schemas: --schema key:Type:Auth[,key:Type:Auth…] (e.g. palette:HexColor:TokenOwner).
|
|
2228
2297
|
* Seeds: canonical randomizer by default; --no-seed opts out.
|
|
2229
2298
|
*/
|
|
2299
|
+
// ── the demo walkthrough: teaching sections, demo-only ────────────────────────
|
|
2300
|
+
//
|
|
2301
|
+
// `abx demo` is a TEACHING command, not a shortcut — the docs point a first-time reader here to
|
|
2302
|
+
// learn what the toolkit does on their behalf. Its old form asserted the interesting claims
|
|
2303
|
+
// ("reconstructed 9 events — no provider involved") without ever showing them, which made it a
|
|
2304
|
+
// smoke test wearing a demo's clothes. These sections demonstrate instead: print the spine the
|
|
2305
|
+
// chain now holds, throw the local projection away and rebuild it, then read the token back the way
|
|
2306
|
+
// a marketplace would. They run only for `demo` (never `deploy`), and never pause — an agent or CI
|
|
2307
|
+
// run has to behave identically.
|
|
2308
|
+
/**
|
|
2309
|
+
* The deterministic part of a projection: everything that is a pure function of the chain.
|
|
2310
|
+
* Deliberately EXCLUDES `reconstructedAt`, `rpcUrl` and `toBlock` — a timestamp, the endpoint that
|
|
2311
|
+
* happened to answer, and the head at scan time all legitimately differ between two replays, so
|
|
2312
|
+
* folding them in would make the rebuild proof fail for reasons that aren't about correctness.
|
|
2313
|
+
*/
|
|
2314
|
+
function projectionFingerprint(s) {
|
|
2315
|
+
const canonical = {
|
|
2316
|
+
address: s.address.toLowerCase(),
|
|
2317
|
+
name: s.name,
|
|
2318
|
+
symbol: s.symbol,
|
|
2319
|
+
owner: s.owner?.toLowerCase() ?? null,
|
|
2320
|
+
isCanonical: s.isCanonical,
|
|
2321
|
+
deployBlock: s.deployBlock,
|
|
2322
|
+
eventCount: s.eventCount,
|
|
2323
|
+
royalty: s.royalty ? { bps: s.royalty.bps, receiver: s.royalty.receiver.toLowerCase() } : null,
|
|
2324
|
+
extensions: s.extensions.map((e) => e.name).sort(),
|
|
2325
|
+
collectionFields: s.collectionFields.map((f) => `${f.field}=${f.value}`).sort(),
|
|
2326
|
+
tokens: s.tokens.map((t) => ({ id: t.tokenId, minted: t.minted, owner: t.owner?.toLowerCase() ?? null })),
|
|
2327
|
+
events: s.events.map((e) => `${e.blockNumber}:${e.logIndex}:${e.name}`),
|
|
2328
|
+
};
|
|
2329
|
+
return createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Print the reconstructed spine — the point being that this list IS the database. Rendered inside
|
|
2333
|
+
* the index step (no header of its own: "Index it — replay the event spine" immediately followed by
|
|
2334
|
+
* a separate "The event spine" step read as a stutter).
|
|
2335
|
+
*/
|
|
2336
|
+
function walkthroughSpine(state) {
|
|
2337
|
+
if (state.events.length === 0) {
|
|
2338
|
+
warn('no events to show (the index came back empty — see the recovery hint above).');
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
const width = Math.max(...state.events.map((e) => e.name.length));
|
|
2342
|
+
state.events.forEach((e, i) => {
|
|
2343
|
+
// ERC vs ABX register: standard ERC-721 events a marketplace already understands, versus ABX's
|
|
2344
|
+
// own. Worth surfacing — it's why an ABX token indexes fine on tools that know nothing about ABX.
|
|
2345
|
+
const reg = e.register === 2 ? p('ABX') : dim('ERC');
|
|
2346
|
+
console.log(` ${dim(`#${String(i + 1).padStart(2)}`)} ${reg} ${e.name.padEnd(width)} ${dim(e.what)}`);
|
|
2347
|
+
});
|
|
2348
|
+
console.log(` ${dim('→')} those ${bold(String(state.events.length))} rows are the entire database — ` +
|
|
2349
|
+
dim('no row in a server anywhere is authoritative.'));
|
|
2350
|
+
console.log(` ${p('ABX')} ${dim('= a native ABX event')} ${dim('ERC')} ${dim('= plain ERC-721/7572, so wallets and marketplaces that have never heard of ABX still index this token.')}`);
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* [4] The claim, demonstrated: delete the local projection and rebuild it from the chain.
|
|
2354
|
+
*
|
|
2355
|
+
* This is the one step that can't be faked by good output — it drops the projection for real
|
|
2356
|
+
* (registration kept), confirms it's gone, replays from the deploy block, and compares a
|
|
2357
|
+
* fingerprint of everything chain-derived. If ABX's premise is wrong, this step fails loudly.
|
|
2358
|
+
*/
|
|
2359
|
+
async function walkthroughRebuild(indexer, address, before) {
|
|
2360
|
+
step('Prove it — rebuild from nothing');
|
|
2361
|
+
const fpBefore = projectionFingerprint(before);
|
|
2362
|
+
indexer.dropProjection(address);
|
|
2363
|
+
const gone = indexer.getProject(address) === null;
|
|
2364
|
+
console.log(` ${dim('deleting this machine\'s projection …')} ${gone ? g('✓ gone') : `${c.orange}⚠ still present${c.reset}`}`);
|
|
2365
|
+
const { state: after, elapsedMs } = await indexer.reindex(address, { full: true });
|
|
2366
|
+
const fpAfter = projectionFingerprint(after);
|
|
2367
|
+
console.log(` ${dim(`replaying from block ${after.deployBlock ?? after.fromBlock} …`)} ` +
|
|
2368
|
+
`${g('✓')} ${after.eventCount} events in ${elapsedMs}ms`);
|
|
2369
|
+
if (fpBefore === fpAfter) {
|
|
2370
|
+
ok(`identical state ${dim(`(sha256 ${fpAfter.slice(0, 12)}… over every chain-derived field)`)}`);
|
|
2371
|
+
info('no provider, no backup, no API key — an RPC endpoint and the chain were enough.');
|
|
2372
|
+
}
|
|
2373
|
+
else {
|
|
2374
|
+
warn('the rebuilt state does NOT match what we just had — that is a real bug, please report it.');
|
|
2375
|
+
info(`before ${fpBefore.slice(0, 16)}… · after ${fpAfter.slice(0, 16)}…`);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
/** [5] Read the token back the way a marketplace would, and check the bytes against the chain. */
|
|
2379
|
+
async function walkthroughReadBack(state, baseUrl) {
|
|
2380
|
+
step('Read it back the way a marketplace would');
|
|
2381
|
+
const token = state.tokens[0];
|
|
2382
|
+
if (!token?.minted) {
|
|
2383
|
+
info('no minted token to read yet.');
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
const uri = token.tokenURI;
|
|
2387
|
+
info(`tokenURI(0) ${dim('on chain →')} ${uri ?? dim('(none)')}`);
|
|
2388
|
+
// The demo's default is off-chain custody: the CHAIN holds a keccak256 commitment to the image and
|
|
2389
|
+
// a URI base; this NODE holds the bytes. That split is the thing worth understanding, so name it
|
|
2390
|
+
// rather than implying the JSON came from the chain.
|
|
2391
|
+
info(dim('the chain stored a URI base + a keccak256 commitment; this node serves the bytes.'));
|
|
2392
|
+
try {
|
|
2393
|
+
const res = await fetch(`${baseUrl}/t/${resolveChain(CHAIN).id}/${state.address}/0`);
|
|
2394
|
+
const json = (await res.json());
|
|
2395
|
+
const shown = { name: json.name, image: json.image };
|
|
2396
|
+
console.log(` ${dim(JSON.stringify(shown))}`);
|
|
2397
|
+
if (Array.isArray(json.abx_provenance)) {
|
|
2398
|
+
info(dim(`every field is tagged with where it came from (abx_provenance: ${json.abx_provenance.length} entries)`));
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
catch (e) {
|
|
2402
|
+
info(dim(`could not read the served metadata: ${e.message}`));
|
|
2403
|
+
}
|
|
2404
|
+
console.log(` ${dim('prove the bytes match the chain:')} ${bold(`abx verify ${state.address}`)}`);
|
|
2405
|
+
}
|
|
2406
|
+
/** Is a TCP port already bound on localhost? Used to preflight a serve BEFORE spending a tx. */
|
|
2407
|
+
async function portInUse(port) {
|
|
2408
|
+
const { createServer } = await import('node:net');
|
|
2409
|
+
return new Promise((resolve) => {
|
|
2410
|
+
const probe = createServer();
|
|
2411
|
+
probe.once('error', (e) => resolve(e.code === 'EADDRINUSE'));
|
|
2412
|
+
probe.once('listening', () => probe.close(() => resolve(false)));
|
|
2413
|
+
probe.listen(port);
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Index a project we *just* deployed — and don't believe a zero.
|
|
2418
|
+
*
|
|
2419
|
+
* `eth_getLogs` is read-after-write inconsistent on load-balanced RPCs: `waitForTransactionReceipt`
|
|
2420
|
+
* resolves against a node that has the block, then the log query lands on one that doesn't yet, and
|
|
2421
|
+
* returns an empty set for a block we KNOW contains our deploy. The old code took that single read at
|
|
2422
|
+
* face value, printed `✓ reconstructed 0 events`, stored the empty projection, and served an empty
|
|
2423
|
+
* dashboard — a first-run that looks like the toolkit simply doesn't work. It reproduced 100% of the
|
|
2424
|
+
* time on `https://sepolia.base.org`, which is the DEFAULT endpoint when there's no `.env`, i.e. the
|
|
2425
|
+
* documented first run was the broken path. The same block returned all 9 logs seconds later.
|
|
2426
|
+
*
|
|
2427
|
+
* We have the one thing that makes this checkable: we just minted, so the spine cannot be empty.
|
|
2428
|
+
* So verify instead of trusting — re-scan with backoff until events appear, and if they never do,
|
|
2429
|
+
* say so as a FAILURE with the recovery command rather than dressing a zero up as a ✓.
|
|
2430
|
+
*/
|
|
2431
|
+
async function reindexAfterDeploy(indexer, address, opts = {}) {
|
|
2432
|
+
const attempts = opts.attempts ?? 6;
|
|
2433
|
+
const delayMs = opts.delayMs ?? 1500;
|
|
2434
|
+
let last = await indexer.reindex(address);
|
|
2435
|
+
for (let i = 1; i < attempts && last.state.eventCount === 0; i++) {
|
|
2436
|
+
if (i === 1) {
|
|
2437
|
+
info(dim("no events yet — the RPC hasn't served the logs for that block; re-scanning…"));
|
|
2438
|
+
}
|
|
2439
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
2440
|
+
// `full: true` — the stored projection has 0 events, so there is no valid checkpoint to
|
|
2441
|
+
// resume from; a full replay from the deploy block is the only correct re-scan.
|
|
2442
|
+
last = await indexer.reindex(address, { full: true });
|
|
2443
|
+
}
|
|
2444
|
+
if (last.state.eventCount === 0) {
|
|
2445
|
+
warn(`the RPC still reports no logs for this project after ${attempts} tries — the deploy DID succeed ` +
|
|
2446
|
+
`(it's on chain), but this node can't reconstruct it yet.`);
|
|
2447
|
+
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).')}`);
|
|
2448
|
+
}
|
|
2449
|
+
return last;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* `abx preview` — serve the program on localhost, live, for as long as the work is being made.
|
|
2453
|
+
*
|
|
2454
|
+
* The studio lane, and deliberately the FIRST thing to reach for on a code project: it renders the
|
|
2455
|
+
* same document the generator serves (real `abx.js`, real tokenData shape, real dependency tags)
|
|
2456
|
+
* with a synthetic seed, so a creator can refresh for new seeds, drive their PostParams from real
|
|
2457
|
+
* inputs, and watch an animated piece actually move — none of which a still-image sweep can show.
|
|
2458
|
+
* No chain, no key, no deploy. `--shoot` renders the same document headlessly for an agent that
|
|
2459
|
+
* can't open a browser.
|
|
2460
|
+
*/
|
|
2461
|
+
async function cmdPreview(flags) {
|
|
2462
|
+
warnStrayFlags(flags, PREVIEW_FLAGS, 'preview');
|
|
2463
|
+
const cfg = previewConfigFromFlags(flags);
|
|
2464
|
+
const shootDir = flags.shoot && flags.shoot !== 'true' ? String(flags.shoot) : flags.shoot === 'true' ? 'abx-preview' : undefined;
|
|
2465
|
+
const count = Math.min(Math.max(Number(flags.count ?? 9) || 9, 1), 64);
|
|
2466
|
+
console.log(bold(`\n ABX Self-Host Toolkit — preview\n ${dim('the program, running locally — no chain, no deploy')}`));
|
|
2467
|
+
const { notes } = previewDepTags(cfg.deps);
|
|
2468
|
+
step('Program');
|
|
2469
|
+
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)')}`);
|
|
2470
|
+
if (cfg.schemas.length)
|
|
2471
|
+
info(`params: ${cfg.schemas.map(describeSchema).join(' · ')}`);
|
|
2472
|
+
else
|
|
2473
|
+
info(dim('params: none declared — add --schema key:Type:Auth to drive them from the studio'));
|
|
2474
|
+
for (const n of notes)
|
|
2475
|
+
info(`dep ${n}`);
|
|
2476
|
+
// A raw on-chain dependency can't be fetched without a chain, so the preview would render a
|
|
2477
|
+
// sketch missing its runtime and look broken for the wrong reason. Say so rather than let them
|
|
2478
|
+
// debug their own art.
|
|
2479
|
+
if (cfg.deps.some((d) => d.display.startsWith('0x'))) {
|
|
2480
|
+
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.');
|
|
2481
|
+
}
|
|
2482
|
+
const server = await startPreviewServer(cfg, shootDir ? 0 : Number(flags.port ?? DEFAULT_PREVIEW_PORT));
|
|
2483
|
+
if (shootDir) {
|
|
2484
|
+
step(`Render ${count} seeds headlessly`);
|
|
2485
|
+
try {
|
|
2486
|
+
const shots = await shootPreview(server.url, shootDir, count, {
|
|
2487
|
+
width: Number(flags.width ?? 1000) || 1000,
|
|
2488
|
+
timeoutMs: Number(flags['timeout-ms'] ?? 10_000) || 10_000,
|
|
2489
|
+
});
|
|
2490
|
+
ok(`${shots.length} frames → ${shootDir}/ ${dim('(traits in traits.json)')}`);
|
|
2491
|
+
for (const s of shots) {
|
|
2492
|
+
const t = s.traits && Object.keys(s.traits).length
|
|
2493
|
+
? Object.entries(s.traits).map(([k, v]) => `${k} ${String(v)}`).join(' · ')
|
|
2494
|
+
: `${c.orange}no traits reported${c.reset}`;
|
|
2495
|
+
console.log(` ${dim(s.seed.slice(0, 10) + '…')} ${t}${s.done ? '' : dim(' (no abx.done())')}`);
|
|
2496
|
+
}
|
|
2497
|
+
const silent = shots.filter((s) => !s.traits || !Object.keys(s.traits).length).length;
|
|
2498
|
+
if (silent === shots.length) {
|
|
2499
|
+
warn('NO frame reported traits — `abx.traits({…})` is the only thing that becomes marketplace `attributes`. Verify with `abx inspect`.');
|
|
2500
|
+
}
|
|
2501
|
+
else if (shots.length > 1 && new Set(shots.map((s) => JSON.stringify(s.traits))).size === 1) {
|
|
2502
|
+
// Only meaningful when traits DID come back: identical values across seeds is the signature
|
|
2503
|
+
// of a sketch that never reads `abx.tokenData.seed` (prototyped on Math.random()), which
|
|
2504
|
+
// deploys as N visually identical tokens. Skipped when nothing reported at all — the
|
|
2505
|
+
// warning above already covers that, and firing both reads as noise.
|
|
2506
|
+
warn('every seed produced identical traits — check the sketch actually reads `abx.tokenData.seed` (the silent "all tokens the same" failure).');
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
finally {
|
|
2510
|
+
await server.close();
|
|
2511
|
+
}
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
step('Studio');
|
|
2515
|
+
console.log(`\n ${g('●')} ${bold('preview')} ${server.url}`);
|
|
2516
|
+
console.log(` ${dim('studio ')}${server.url} ${dim('seed + params + live traits')}`);
|
|
2517
|
+
console.log(` ${dim('grid ')}${server.url}/grid ${dim('9 seeds at once, all live')}`);
|
|
2518
|
+
console.log(` ${dim('bare view ')}${server.url}/view ${dim('the generator document itself')}`);
|
|
2519
|
+
console.log(`\n ${dim('Edit the program and refresh — it is re-read from disk. Ctrl-C to stop.')}`);
|
|
2520
|
+
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`);
|
|
2521
|
+
await new Promise(() => { }); // block like `serve` — the creator drives it
|
|
2522
|
+
}
|
|
2230
2523
|
/**
|
|
2231
2524
|
* `abx inspect <script.js>` — read a generative script and, WITHOUT executing it, report what it
|
|
2232
2525
|
* needs (traits + their on-chain reproducibility, dependency hints, size → assembled-document size →
|
|
@@ -4284,7 +4577,13 @@ function printServing(url, address) {
|
|
|
4284
4577
|
console.log(` ${dim('image ')}${url}/t/${cid}/${address}/0/image`);
|
|
4285
4578
|
console.log(` ${dim('state API ')}${url}/api/project/${address}`);
|
|
4286
4579
|
}
|
|
4287
|
-
|
|
4580
|
+
// The dashboard is READ-ONLY: re-index/verify are admin actions that 404 unless the node has an
|
|
4581
|
+
// ABX_RESOLVER_ADMIN_TOKEN, so there is no button to press. This line used to say "hit Re-index
|
|
4582
|
+
// from chain", which sent every first-run user hunting for a control that isn't there.
|
|
4583
|
+
console.log(`\n ${dim('The dashboard shows the event spine it replayed — that table IS the reconstruction.')}`);
|
|
4584
|
+
if (address) {
|
|
4585
|
+
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.')}`);
|
|
4586
|
+
}
|
|
4288
4587
|
console.log(` ${dim('Ctrl-C to stop.')}\n`);
|
|
4289
4588
|
}
|
|
4290
4589
|
// ── deploy-resolver: scaffold a hosted resolver for a provider the operator owns ──────────
|
|
@@ -4634,6 +4933,20 @@ const COMMAND_HELP = {
|
|
|
4634
4933
|
${g('abx deploy-series')} --dir ./photos --name "My Series" --symbol MS --onchain-uri --backend ipfs --mint-all --sign
|
|
4635
4934
|
${dim('# tiny SVGs → fully on-chain (no storage at all):')}
|
|
4636
4935
|
${g('abx deploy-series')} --dir ./svgs --name "My Series" --symbol MS --onchain-image --compress fastlz --mint-all --sign`,
|
|
4936
|
+
preview: `
|
|
4937
|
+
${bold('abx preview')} (--script <file.js> | --code-dir <dir>) ${dim('— run the program on localhost, live. No chain, no key, no deploy.')}
|
|
4938
|
+
${g('--schema key:Type:Auth')}[,…] declare PostParams so the studio gives you real inputs for them (e.g. palette:HexColor:TokenOwner)
|
|
4939
|
+
${g('--dep <name@version>')}[,…] load a library the way the resolver would (built-in CDN map; on-chain refs can't be fetched offline)
|
|
4940
|
+
${g('--port')} <n> studio port (default ${DEFAULT_PREVIEW_PORT}; the resolver's ${DEFAULT_PORT} stays free)
|
|
4941
|
+
${g('--shoot')} <dir> render headlessly to PNGs + traits.json and EXIT ${dim('(for an agent that has no browser)')}
|
|
4942
|
+
${g('--count')} <n> seeds to shoot / show in the grid (default 9) ${g('--width')} <px> ${g('--timeout-ms')} <n>
|
|
4943
|
+
Serves the ${bold('same document the generator serves')} — real ${g('abx.js')}, real tokenData shape, real dep tags — with a synthetic
|
|
4944
|
+
seed, so what you iterate on is what deploys. Routes: ${g('/')} studio (seed + params + live traits) · ${g('/grid')} N seeds at once,
|
|
4945
|
+
all live · ${g('/view')} the bare document. The program is re-read from disk per render, so ${bold('edit and refresh')} — no watcher.
|
|
4946
|
+
Unlike a still-image sweep this shows ${bold('animation')}, which is most of what a screenshot throws away.
|
|
4947
|
+
${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).')}
|
|
4948
|
+
${g('abx preview')} --script art.js --schema palette:HexColor:TokenOwner
|
|
4949
|
+
${g('abx preview')} --script art.js --shoot ./frames --count 12`,
|
|
4637
4950
|
inspect: `
|
|
4638
4951
|
${bold('abx inspect')} <script.js> ${dim('— static analysis of a generative script + a lane recommendation. Read-only; the script is never executed.')}
|
|
4639
4952
|
${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)
|
|
@@ -4891,7 +5204,10 @@ const COMMAND_HELP = {
|
|
|
4891
5204
|
${g('upload')} <path> upload ONE file → prints its locator (the URI ${g('abx attach')} wants) [--backend …] [--dry-run]
|
|
4892
5205
|
${g('balance')} · ${g('topup')} --usd <n> Turbo (arweave) upload credits · ${g('backup-key')} --out <path> copy the managed key`,
|
|
4893
5206
|
demo: `
|
|
4894
|
-
${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.')}
|
|
5207
|
+
${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.')}
|
|
5208
|
+
${g('--sign')} ${dim('approve in your browser wallet instead of a hot env key (no key needed)')}
|
|
5209
|
+
${g('--for')} <0x…> ${dim('pin who must connect on --sign (owner + royalty receiver + mint recipient)')}
|
|
5210
|
+
${dim('--unsigned / --dry-run are refused here: both skip the broadcast, and the demo indexes + serves what it deployed.')}`,
|
|
4895
5211
|
minter: `
|
|
4896
5212
|
${bold('abx minter')} <configure|show|buy> <token> ${dim('— sell a Series via the shared fixed-price minter (Minter spine).')}
|
|
4897
5213
|
${g('configure')} <token> (--price <eth> | --price-raw <units>) --allocation <n> [--erc20 0x..] ${dim('(token-owner only)')}
|
|
@@ -4985,7 +5301,9 @@ function help() {
|
|
|
4985
5301
|
project + notify the effects layer on change (${g('ABX_WATCH_INTERVAL_MS')}; 0 = off)
|
|
4986
5302
|
|
|
4987
5303
|
${bold('code / generative projects')} ${dim('— a program is the content; output is a function of live on-chain state')}
|
|
4988
|
-
${g('abx
|
|
5304
|
+
${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,
|
|
5305
|
+
drive your PostParams, watch it animate. Same document the generator serves. ${g('--shoot <dir>')} for headless frames. No chain.
|
|
5306
|
+
${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
|
|
4989
5307
|
${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)
|
|
4990
5308
|
${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)
|
|
4991
5309
|
${bold('--public-base-url <url>')} OR ${bold('--onchain-uri')} · --schema key:Type:Auth · ${g('--dep')} name@version|0x.. (ordered; index 0 = the runtime) ·
|