@artblocks/abx-cli 0.1.0-alpha.4 → 0.1.0-alpha.6
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/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/main.js +418 -53
- package/dist/main.js.map +1 -1
- package/dist/prompt.d.ts +17 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +19 -0
- package/dist/prompt.js.map +1 -0
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +7 -0
- package/dist/signer.js.map +1 -1
- package/dist/update-check.d.ts +31 -4
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +58 -10
- package/dist/update-check.js.map +1 -1
- package/package.json +3 -3
- package/skill/SKILL.md +3 -3
package/dist/main.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* the tx for a multisig / offline signer). The agent picks the lane; the CLI signs.
|
|
44
44
|
*/
|
|
45
45
|
import { appendFileSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
46
|
-
import { randomBytes } from 'node:crypto';
|
|
46
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
47
47
|
import { basename, extname, join as joinPath, resolve as resolvePath } from 'node:path';
|
|
48
48
|
import { fileURLToPath } from 'node:url';
|
|
49
49
|
import { homedir } from 'node:os';
|
|
@@ -60,6 +60,7 @@ import { AGENT_SKILL_PARENTS, checkForCliUpdate, compareVersions, installedSkill
|
|
|
60
60
|
import { analyzeScript, recommendLane } from './inspect.js';
|
|
61
61
|
import { previewConfigFromFlags, previewDepTags, shootPreview, startPreviewServer, DEFAULT_PREVIEW_PORT, PREVIEW_FLAGS } from './preview.js';
|
|
62
62
|
import { parseSchemaSpecs, describeSchema } from './schema.js';
|
|
63
|
+
import { declinesSkillInstall } from './prompt.js';
|
|
63
64
|
import { parseSeriesTraits, looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
|
|
64
65
|
/** The `--schema` type/auth/format catalog, shown wherever the CLI nudges `--schema`. Kept accurate
|
|
65
66
|
* to the on-chain enums (PARAM_TYPES / AUTH_OPTIONS) + the Select-options / Range-bounds format —
|
|
@@ -77,7 +78,7 @@ import { cmdTransfer, cmdMint, cmdSetMinter, cmdSetMaxInvocations, cmdConfigureP
|
|
|
77
78
|
import { openWalletSession, signTx } from './signer.js';
|
|
78
79
|
import { remoteAddProject, remoteRemoveProject } from './remote.js';
|
|
79
80
|
import { buildMigrationPlan, repinNodeCustody, verifyParity } from './migrate.js';
|
|
80
|
-
import { activeBackendId, arweaveKeyFilePath, backendResolution, ensureArweaveJwk, factoryAddress, seriesFactoryAddress, fixedPriceMinterAddress, loadArweaveJwk, loopbackBaseUrl, rendererAddress, storageOptions, storageSignerChoice, } from './config.js';
|
|
81
|
+
import { activeBackendId, arweaveKeyFilePath, backendResolution, ensureArweaveJwk, factoryAddress, seriesFactoryAddress, fixedPriceMinterAddress, loadArweaveJwk, loopbackBaseUrl, faucetHint, rendererAddress, storageOptions, storageSignerChoice, } from './config.js';
|
|
81
82
|
const CHAIN = process.env.ABX_CHAIN ?? DEFAULT_CHAIN_KEY;
|
|
82
83
|
// Block explorer base per chain (keyed by EIP-155 chainId), for the tx/address links the CLI prints.
|
|
83
84
|
const EXPLORERS = {
|
|
@@ -297,17 +298,6 @@ async function detectCanonicalFactory(address, override, stored) {
|
|
|
297
298
|
}
|
|
298
299
|
return factoryAddress() ?? undefined;
|
|
299
300
|
}
|
|
300
|
-
/** A short faucet pointer for the active testnet — a 0-balance signer's #1 next step. A known URL
|
|
301
|
-
* plus a search hint (URLs rot; the search always works), so "fund it" isn't a dead end. */
|
|
302
|
-
function faucetHint(chainKey) {
|
|
303
|
-
const url = {
|
|
304
|
-
'base-sepolia': 'https://portal.cdp.coinbase.com/products/faucet',
|
|
305
|
-
sepolia: 'https://www.alchemy.com/faucets/ethereum-sepolia',
|
|
306
|
-
};
|
|
307
|
-
return url[chainKey]
|
|
308
|
-
? `get free test ETH from a ${chainKey} faucet (${url[chainKey]} — or search "${chainKey} faucet"), usually ≤1 min`
|
|
309
|
-
: `get test ETH from a "${chainKey}" faucet`;
|
|
310
|
-
}
|
|
311
301
|
// Funding preflight — a 0-balance signer fails only at the tx, with a confusing error. Surface it
|
|
312
302
|
// up front. Especially the wallet lane (--sign/--for), which has no env key for `doctor` to check.
|
|
313
303
|
async function warnUnfunded(publicClient, address) {
|
|
@@ -986,7 +976,9 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
986
976
|
// editable later via `abx add --traits`); `--traits-onchain` inlines them on-chain (durable,
|
|
987
977
|
// lockable) — mirrors the description model. On-chain wins if both ever set the same trait.
|
|
988
978
|
const traits = parseDeployTraits(flags);
|
|
989
|
-
console.log(
|
|
979
|
+
console.log(serveAfter
|
|
980
|
+
? bold(`\n ABX · your first token\n ${dim("we'll put art on a blockchain — then delete our copy and get all of it back")}`)
|
|
981
|
+
: bold(`\n ABX Self-Host Toolkit — deploy\n ${dim('a project, served from chain alone')}`));
|
|
990
982
|
// Signing lane. `demo` used to hard-force the hot lane, which SILENTLY dropped `--sign`: with no
|
|
991
983
|
// env key it died confusingly, and WITH one it signed from that key while the operator had asked
|
|
992
984
|
// for their browser wallet — a signing choke point that ignored the lane it was handed. The demo
|
|
@@ -1005,6 +997,17 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1005
997
|
throw new Error('`abx demo --dry-run` can\'t work: a dry run sends nothing, and the demo indexes + serves what ' +
|
|
1006
998
|
'it deployed. Use `abx deploy --dry-run` to preview a 1/1 deploy without sending.');
|
|
1007
999
|
}
|
|
1000
|
+
// Port preflight BEFORE anything irreversible. The demo ends by serving, and `listen` used to be
|
|
1001
|
+
// the first thing to discover the port was taken — after the deploy tx had already been signed and
|
|
1002
|
+
// paid for, so a second `abx demo` (a very normal thing to try) spent gas and then died with a raw
|
|
1003
|
+
// Node EADDRINUSE stack trace. Check first, and name the fix.
|
|
1004
|
+
if (serveAfter) {
|
|
1005
|
+
const wanted = Number(flags.port ?? process.env.ABX_PORT ?? DEFAULT_PORT);
|
|
1006
|
+
if (await portInUse(wanted)) {
|
|
1007
|
+
throw new Error(`port ${wanted} is already in use — probably an \`abx demo\`/\`abx serve\` still running in another terminal.\n` +
|
|
1008
|
+
` Nothing was deployed. Stop that one (Ctrl-C), or run this on another port: \`abx demo --port ${wanted + 1}\`.`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1008
1011
|
const dryRun = !serveAfter && !!flags['dry-run']; // preview only — no send, no custody, no factory deploy
|
|
1009
1012
|
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
1010
1013
|
// Verify the RPC really is CHAIN before any send (factory/renderer/staging/deploy). A dry run
|
|
@@ -1035,7 +1038,7 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1035
1038
|
if (signer)
|
|
1036
1039
|
await warnUnfunded(publicClient, signer);
|
|
1037
1040
|
}
|
|
1038
|
-
step('Trust anchor');
|
|
1041
|
+
step(serveAfter ? 'Who vouches for this token?' : 'Trust anchor');
|
|
1039
1042
|
let factory;
|
|
1040
1043
|
if (dryRun) {
|
|
1041
1044
|
const existing = factoryAddress(flags.factory);
|
|
@@ -1050,24 +1053,47 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1050
1053
|
else {
|
|
1051
1054
|
factory = await ensureFactory(flags.factory, !!flags['bootstrap-factory']);
|
|
1052
1055
|
}
|
|
1056
|
+
// The demo explains what it just did; `deploy` stays terse. The authenticity point is the least
|
|
1057
|
+
// obvious thing about ABX and the easiest to get wrong, so it's worth spelling out — but in plain
|
|
1058
|
+
// language, with the actual term parked in a dim aside for whoever wants to look it up.
|
|
1059
|
+
if (serveAfter) {
|
|
1060
|
+
console.log(` ${g('✦')} anyone can write a contract that ${bold('claims')} to be an ABX token`);
|
|
1061
|
+
console.log(` ${g('✦')} only this one factory can make a token that ${bold('is')} one`);
|
|
1062
|
+
info(dim('so "is this real?" is a yes/no question anyone can ask the chain — not a matter of trusting us'));
|
|
1063
|
+
info(dim(`the factory answers it on-chain: isAbxClone(yourToken) · ${EXPLORER}/address/${factory}`));
|
|
1064
|
+
}
|
|
1053
1065
|
// --onchain-uri: resolve tokenURI/contractURI fully on-chain via the canonical renderer
|
|
1054
1066
|
// (the JSON is assembled from on-chain fields → no resolver needed, ever). The off-chain
|
|
1055
1067
|
// URLs are still baked as a fallback if the renderer is later cleared.
|
|
1056
1068
|
// --onchain-image: stage the --image bytes on-chain (chunk store, ownerless) and bake a
|
|
1057
1069
|
// `reader` field into the deploy — so large on-chain content needs NO post-deploy tx. It
|
|
1058
1070
|
// implies on-chain URI resolution (the renderer emits the reader-backed image).
|
|
1059
|
-
const onchainImage = !!flags['onchain-image'];
|
|
1060
|
-
const onChainUri = !!flags['onchain-uri'] || onchainImage;
|
|
1061
|
-
// On-chain traits when explicitly asked, or implied by --onchain-uri (the renderer can only
|
|
1062
|
-
// emit on-chain fields, so off-chain-only traits would be invisible there). Else off-chain.
|
|
1063
|
-
const traitsOnchain = traits.length > 0 && (!!flags['traits-onchain'] || onChainUri);
|
|
1064
1071
|
// A fully on-chain token self-resolves via the renderer; the stored off-chain pointer is
|
|
1065
1072
|
// never read while a renderer is set. So bake a real URL only if one was explicitly given —
|
|
1066
1073
|
// otherwise leave it EMPTY rather than baking a misleading localhost into the contract.
|
|
1067
1074
|
const hasPublicUrl = !!(flags['public-base-url'] || process.env.ABX_PUBLIC_BASE_URL);
|
|
1075
|
+
const onchainImage = !!flags['onchain-image'];
|
|
1076
|
+
// `abx demo` defaults to FULLY ON-CHAIN. Its default art is a generative SVG, which the renderer
|
|
1077
|
+
// can inline — so the token self-resolves and, crucially, NO localhost gets baked into the
|
|
1078
|
+
// contract as the tokenURI base. The old default shipped a first-ever token that resolved for
|
|
1079
|
+
// nobody but its author (broken on every marketplace, dead the moment `abx serve` stops) and
|
|
1080
|
+
// taught that as the normal shape of an NFT. It also undercut the demo's own claim: with the art
|
|
1081
|
+
// on-chain, "rebuilt from the chain alone" now covers the IMAGE, not just the metadata.
|
|
1082
|
+
//
|
|
1083
|
+
// Opting back into off-chain custody is anything that says "I have somewhere to host": a real base
|
|
1084
|
+
// URL, an explicit --backend, or a RASTER --image. An SVG --image still goes on-chain — inlining is
|
|
1085
|
+
// exactly what the renderer supports (v1 is SVG-only), so there's no reason to send someone's own
|
|
1086
|
+
// vector art down the localhost path. Raster stays off-chain because forcing it on-chain would
|
|
1087
|
+
// silently inline a placeholder instead of their image.
|
|
1088
|
+
const demoImageInlineable = !flags.image || contentTypeFromPath(flags.image) === 'image/svg+xml';
|
|
1089
|
+
const demoDefaultsOnChain = serveAfter && demoImageInlineable && !flags.backend && !hasPublicUrl;
|
|
1090
|
+
const onChainUri = !!flags['onchain-uri'] || onchainImage || demoDefaultsOnChain;
|
|
1091
|
+
// On-chain traits when explicitly asked, or implied by --onchain-uri (the renderer can only
|
|
1092
|
+
// emit on-chain fields, so off-chain-only traits would be invisible there). Else off-chain.
|
|
1093
|
+
const traitsOnchain = traits.length > 0 && (!!flags['traits-onchain'] || onChainUri);
|
|
1068
1094
|
let renderer = zeroAddress;
|
|
1069
1095
|
if (onChainUri) {
|
|
1070
|
-
step('On-chain renderer');
|
|
1096
|
+
step(serveAfter ? 'What will answer when someone asks about your token' : 'On-chain renderer');
|
|
1071
1097
|
if (dryRun) {
|
|
1072
1098
|
renderer = rendererAddress(flags.renderer) ?? zeroAddress;
|
|
1073
1099
|
info(renderer === zeroAddress ? 'would deploy the canonical renderer first' : `would use renderer ${renderer}`);
|
|
@@ -1075,8 +1101,28 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1075
1101
|
else {
|
|
1076
1102
|
renderer = await ensureRenderer(flags.renderer);
|
|
1077
1103
|
}
|
|
1104
|
+
// A first-timer has no idea what a "renderer" is, and the word suggests something that draws
|
|
1105
|
+
// pictures. What it actually does is assemble the JSON a marketplace asks for, on-chain, out of
|
|
1106
|
+
// the fields your contract holds — worth one plain sentence, since it's why no server is needed.
|
|
1107
|
+
if (serveAfter) {
|
|
1108
|
+
info(dim('a marketplace asks your contract a question; this shared contract composes the answer'));
|
|
1109
|
+
info(dim('already deployed, used by every ABX token, owned by no one — you are not paying to set it up'));
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
step(serveAfter ? `Mint it · one transaction on ${CHAIN}` : `Deploy a ${DIMENSIONS[dimension].label} (--type ${dimension}) to ${CHAIN}`);
|
|
1113
|
+
// What the chain ends up holding is the single most useful thing to understand about an ABX token,
|
|
1114
|
+
// and it's invisible unless someone says it out loud. Two honest versions, because the answer is
|
|
1115
|
+
// genuinely different per lane. Demo only — `deploy` prints the same facts per-field.
|
|
1116
|
+
if (serveAfter && onChainUri) {
|
|
1117
|
+
console.log(` Your art goes ${bold('INTO')} the contract. Not a link to it — the image itself.`);
|
|
1118
|
+
console.log(` ${g('✦')} the chain will hold ${dim('your art · your name on it · your royalty · you as owner')}`);
|
|
1119
|
+
console.log(` ${g('✦')} you will need ${dim('nothing else. no server, no IPFS pin, no monthly bill to forget')}`);
|
|
1120
|
+
}
|
|
1121
|
+
else if (serveAfter) {
|
|
1122
|
+
console.log(` ${g('✦')} the chain will hold ${dim('your name · your royalty · you as owner · a fingerprint of the art')}`);
|
|
1123
|
+
console.log(` ${g('✦')} this computer holds ${dim('the image bytes themselves')}`);
|
|
1124
|
+
info(dim('the chain proves the bytes are unaltered; it does not store them (that fingerprint is a keccak256 hash)'));
|
|
1078
1125
|
}
|
|
1079
|
-
step(`Deploy a ${DIMENSIONS[dimension].label} (--type ${dimension}) to ${CHAIN}`);
|
|
1080
1126
|
// Off-chain custody (no renderer) bakes the resolver URL straight into the on-chain
|
|
1081
1127
|
// tokenURI/contractURI at deploy. A localhost / loopback URL there resolves for NO ONE —
|
|
1082
1128
|
// not marketplaces, not wallets, not even your own browser unless `abx serve` is running —
|
|
@@ -1103,7 +1149,14 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1103
1149
|
}
|
|
1104
1150
|
}
|
|
1105
1151
|
}
|
|
1106
|
-
if (onChainUri) {
|
|
1152
|
+
if (onChainUri && serveAfter) {
|
|
1153
|
+
// The demo's version of the same fact, in words a first-timer can act on. Naming what we are
|
|
1154
|
+
// NOT doing matters here: a localhost URL written into a contract is the single most common way
|
|
1155
|
+
// a first NFT ends up permanently broken, and the demo used to model exactly that.
|
|
1156
|
+
info(dim('nothing points at this computer — no http://localhost anywhere in your contract.'));
|
|
1157
|
+
info(dim('anyone can read your token from the chain, forever, with you offline.'));
|
|
1158
|
+
}
|
|
1159
|
+
else if (onChainUri) {
|
|
1107
1160
|
info('tokenURI/contractURI resolve ON-CHAIN via the renderer — no resolver, no server, no localhost.');
|
|
1108
1161
|
if (!hasPublicUrl)
|
|
1109
1162
|
info('off-chain fallback pointer left empty (the renderer is authoritative); set --public-base-url to bake one anyway.');
|
|
@@ -1280,7 +1333,9 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1280
1333
|
// wallet lane + on-chain staging: ONE sign session signs every chunk write AND the deploy.
|
|
1281
1334
|
// The connecting wallet pays for (and is the deployer of) all of it — staging can't precede
|
|
1282
1335
|
// the connect here, so it happens inside the session, then the deploy bakes in the manifest.
|
|
1283
|
-
|
|
1336
|
+
// --onchain-image implies on-chain resolution, so NOTHING points at baseUrl — saying it did was
|
|
1337
|
+
// a flat contradiction of the line above it (and reintroduced the localhost the lane exists to avoid).
|
|
1338
|
+
info('a wallet will become the owner; the token resolves from chain — no URI base is baked in.');
|
|
1284
1339
|
const plan = computeContentPlan(readFileSync(resolvePath(flags.image)), parseCompress(flags.compress)).plan;
|
|
1285
1340
|
const stagingTxs = plan.mode === 'single' ? 1 : plan.txCount;
|
|
1286
1341
|
const session = await openWalletSession({
|
|
@@ -1345,8 +1400,12 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1345
1400
|
info(`tx ${EXPLORER}/tx/${r.txHash} (block ${blockNumber})`);
|
|
1346
1401
|
}
|
|
1347
1402
|
else {
|
|
1348
|
-
// wallet lane (off-chain custody) or cold lane: a single deploy tx, no staging
|
|
1349
|
-
|
|
1403
|
+
// wallet lane (off-chain custody OR on-chain URI) or cold lane: a single deploy tx, no staging.
|
|
1404
|
+
// Only claim a URI base when one is actually written — on the on-chain lane this line used to
|
|
1405
|
+
// announce `http://localhost:8787` two lines after promising no localhost anywhere.
|
|
1406
|
+
info(onChainUri
|
|
1407
|
+
? 'a wallet will become the owner; the token resolves from chain — no URI base is baked in.'
|
|
1408
|
+
: `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
1350
1409
|
const result = await signTx(async (signer) => {
|
|
1351
1410
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
1352
1411
|
return prepareDeployOneOfOne({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
@@ -1359,7 +1418,7 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1359
1418
|
blockNumber = result.blockNumber;
|
|
1360
1419
|
ok(`deployed ${clone}`);
|
|
1361
1420
|
}
|
|
1362
|
-
step('Index it — replay the event spine from chain');
|
|
1421
|
+
step(serveAfter ? 'What the chain knows now' : 'Index it — replay the event spine from chain');
|
|
1363
1422
|
const indexer = new SelfHostIndexer();
|
|
1364
1423
|
// Off-chain traits ride in the registration (on-chain ones are already in the contract fields).
|
|
1365
1424
|
const offChainTraits = !traitsOnchain && traits.length ? JSON.stringify(traits) : undefined;
|
|
@@ -1374,10 +1433,20 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1374
1433
|
attributes: offChainTraits,
|
|
1375
1434
|
};
|
|
1376
1435
|
indexer.register(baseReg);
|
|
1377
|
-
const { state, elapsedMs } = await indexer
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1436
|
+
const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone);
|
|
1437
|
+
if (serveAfter) {
|
|
1438
|
+
if (state.eventCount > 0)
|
|
1439
|
+
ok(`read ${bold(String(state.eventCount))} events straight off ${CHAIN} in ${elapsedMs}ms — no API key, no company's server`);
|
|
1440
|
+
info(`it says: "${state.name}" · owned by ${state.owner} · ${state.isCanonical ? `${g('verified real')} ${dim('(that factory vouched for it)')}` : `${c.orange}unverified${c.reset}`}`);
|
|
1441
|
+
info(dim(`optional features switched on: ${state.extensions.map((e) => e.name.replace(/^abx\.extension\./, '')).join(' · ') || 'none'}`));
|
|
1442
|
+
walkthroughSpine(state);
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
if (state.eventCount > 0)
|
|
1446
|
+
ok(`reconstructed ${state.eventCount} events in ${elapsedMs}ms — no provider involved`);
|
|
1447
|
+
info(`name "${state.name}" · owner ${state.owner} · canonical: ${state.isCanonical ? 'yes (factory-verified)' : 'unverified'}`);
|
|
1448
|
+
info(`extensions: ${state.extensions.map((e) => e.name).join(', ') || 'none'}`);
|
|
1449
|
+
}
|
|
1381
1450
|
// For off-chain custody (image committed as keccak256), resolve the durable locator (ipfs://…)
|
|
1382
1451
|
// from this machine's content index and store it on the registration — so the LOCAL resolver
|
|
1383
1452
|
// points `image` at IPFS, and `abx add --remote` can ship it to a hosted one (the localhost-image fix).
|
|
@@ -1426,8 +1495,21 @@ async function cmdDeploy(flags, serveAfter) {
|
|
|
1426
1495
|
}
|
|
1427
1496
|
return;
|
|
1428
1497
|
}
|
|
1429
|
-
|
|
1498
|
+
// The demo's teaching sections — see the walkthrough helpers. `deploy` skips them: someone
|
|
1499
|
+
// shipping real work doesn't need their projection deleted to make a point.
|
|
1500
|
+
if (serveAfter)
|
|
1501
|
+
await walkthroughRebuild(indexer, clone, state);
|
|
1502
|
+
// Start the server BEFORE the read-back step (which fetches the served metadata over HTTP), but
|
|
1503
|
+
// print the serve banner after it — otherwise the "Serve" step header lands with nothing under it
|
|
1504
|
+
// while the read-back prints below, which reads like the step failed.
|
|
1430
1505
|
const { url } = await startTokenApiServer({ indexer, port, baseUrl, storage: resolveBackend(storageOptions()) });
|
|
1506
|
+
if (serveAfter)
|
|
1507
|
+
await walkthroughReadBack(state, url, onChainUri);
|
|
1508
|
+
// On the on-chain lane this server is a convenience, not infrastructure — say so, or standing one
|
|
1509
|
+
// up as the finale re-teaches the dependency the whole run just disproved.
|
|
1510
|
+
step(onChainUri ? 'Go look at it' : 'Serve the token API + dashboard');
|
|
1511
|
+
if (onChainUri)
|
|
1512
|
+
info(dim('a local viewer, purely for your eyes — your token does not need it. Ctrl-C whenever; the token stays up.'));
|
|
1431
1513
|
printServing(url, clone);
|
|
1432
1514
|
keepAlive();
|
|
1433
1515
|
}
|
|
@@ -1869,8 +1951,11 @@ async function cmdDeploySeries(flags) {
|
|
|
1869
1951
|
}
|
|
1870
1952
|
else {
|
|
1871
1953
|
// wallet lane (off-chain / inline) or cold lane: a single deploy tx, no staging sequence —
|
|
1872
|
-
// tokenFields is already built.
|
|
1873
|
-
|
|
1954
|
+
// tokenFields is already built. Same correction as the 1/1 lane: only claim a URI base when one
|
|
1955
|
+
// is actually written, or an --onchain-uri Series announces a localhost it never bakes.
|
|
1956
|
+
info(onChainUri
|
|
1957
|
+
? 'a wallet will become the owner; tokens resolve from chain — no URI base is baked in.'
|
|
1958
|
+
: `a wallet will become the owner; URIs point at ${baseUrl}`);
|
|
1874
1959
|
const result = await signTx(async (signer) => {
|
|
1875
1960
|
const { clone: predicted, params, salt } = await buildForDeployer(signer);
|
|
1876
1961
|
return prepareDeploySeries({ factory, params, salt, chainId: resolveChain(CHAIN).id, clone: predicted });
|
|
@@ -1898,8 +1983,9 @@ async function cmdDeploySeries(flags) {
|
|
|
1898
1983
|
tokenAttributes: offChainTokenTraits,
|
|
1899
1984
|
};
|
|
1900
1985
|
indexer.register(baseReg);
|
|
1901
|
-
const { state, elapsedMs } = await indexer
|
|
1902
|
-
|
|
1986
|
+
const { state, elapsedMs } = await reindexAfterDeploy(indexer, clone);
|
|
1987
|
+
if (state.eventCount > 0)
|
|
1988
|
+
ok(`reconstructed ${state.eventCount} events in ${elapsedMs}ms — ${state.tokens.length} token(s), max ${state.maxInvocations}`);
|
|
1903
1989
|
info(`extensions: ${state.extensions.map((e) => e.name).join(', ') || 'none'}`);
|
|
1904
1990
|
// Off-chain custody: bridge each token's keccak → durable locator so the resolver (local and,
|
|
1905
1991
|
// via `abx add --remote`, a hosted one) points images off this node.
|
|
@@ -2163,7 +2249,11 @@ async function cmdAdd(address, flags) {
|
|
|
2163
2249
|
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.`);
|
|
2164
2250
|
}
|
|
2165
2251
|
catch { /* advisory only — the real scan still runs */ }
|
|
2166
|
-
|
|
2252
|
+
// Don't accept a zero here either: `deploy-code` finishes through this command, so this IS the
|
|
2253
|
+
// post-deploy index for a code project — and a real ABX clone always emits a spine (its extension
|
|
2254
|
+
// registrations at minimum), so 0 events means the RPC hasn't served the logs yet, not that the
|
|
2255
|
+
// project is empty. See reindexAfterDeploy.
|
|
2256
|
+
const { state, elapsedMs } = await reindexAfterDeploy(indexer, address);
|
|
2167
2257
|
// Resolve durable locators for off-chain-by-hash content from this machine's index.
|
|
2168
2258
|
const locators = await collectContentLocators(state, resolveBackend(storageOptions(storageOverrides(flags))));
|
|
2169
2259
|
if (Object.keys(locators).length) {
|
|
@@ -2180,7 +2270,14 @@ async function cmdAdd(address, flags) {
|
|
|
2180
2270
|
contentLocators: JSON.stringify(locators),
|
|
2181
2271
|
});
|
|
2182
2272
|
}
|
|
2183
|
-
|
|
2273
|
+
// A ✓ on 0 events is the lie that produced an empty dashboard; reindexAfterDeploy has already
|
|
2274
|
+
// explained the failure and named the recovery command, so don't stamp it as success too.
|
|
2275
|
+
if (state.eventCount > 0) {
|
|
2276
|
+
ok(`registered + indexed ${state.name ?? address} LOCALLY (this machine): ${state.eventCount} events in ${elapsedMs}ms`);
|
|
2277
|
+
}
|
|
2278
|
+
else {
|
|
2279
|
+
warn(`registered ${state.name ?? address}, but with NO reconstructed state — it will serve empty until the index succeeds.`);
|
|
2280
|
+
}
|
|
2184
2281
|
info(`serve it from here with ${bold('abx serve')} — or push it to a hosted resolver with ${bold('abx add ' + address + ' --remote')}`);
|
|
2185
2282
|
}
|
|
2186
2283
|
/** The code-project trust anchor: use the canonical factory, else deploy one (a sandbox /
|
|
@@ -2247,6 +2344,199 @@ async function ensureSeedSource(publicClient) {
|
|
|
2247
2344
|
* Schemas: --schema key:Type:Auth[,key:Type:Auth…] (e.g. palette:HexColor:TokenOwner).
|
|
2248
2345
|
* Seeds: canonical randomizer by default; --no-seed opts out.
|
|
2249
2346
|
*/
|
|
2347
|
+
// ── the demo walkthrough: teaching sections, demo-only ────────────────────────
|
|
2348
|
+
//
|
|
2349
|
+
// `abx demo` is a TEACHING command, not a shortcut — the docs point a first-time reader here to
|
|
2350
|
+
// learn what the toolkit does on their behalf. Its old form asserted the interesting claims
|
|
2351
|
+
// ("reconstructed 9 events — no provider involved") without ever showing them, which made it a
|
|
2352
|
+
// smoke test wearing a demo's clothes. These sections demonstrate instead: print the spine the
|
|
2353
|
+
// chain now holds, throw the local projection away and rebuild it, then read the token back the way
|
|
2354
|
+
// a marketplace would. They run only for `demo` (never `deploy`), and never pause — an agent or CI
|
|
2355
|
+
// run has to behave identically.
|
|
2356
|
+
/**
|
|
2357
|
+
* The deterministic part of a projection: everything that is a pure function of the chain.
|
|
2358
|
+
* Deliberately EXCLUDES `reconstructedAt`, `rpcUrl` and `toBlock` — a timestamp, the endpoint that
|
|
2359
|
+
* happened to answer, and the head at scan time all legitimately differ between two replays, so
|
|
2360
|
+
* folding them in would make the rebuild proof fail for reasons that aren't about correctness.
|
|
2361
|
+
*/
|
|
2362
|
+
function projectionFingerprint(s) {
|
|
2363
|
+
const canonical = {
|
|
2364
|
+
address: s.address.toLowerCase(),
|
|
2365
|
+
name: s.name,
|
|
2366
|
+
symbol: s.symbol,
|
|
2367
|
+
owner: s.owner?.toLowerCase() ?? null,
|
|
2368
|
+
isCanonical: s.isCanonical,
|
|
2369
|
+
deployBlock: s.deployBlock,
|
|
2370
|
+
eventCount: s.eventCount,
|
|
2371
|
+
royalty: s.royalty ? { bps: s.royalty.bps, receiver: s.royalty.receiver.toLowerCase() } : null,
|
|
2372
|
+
extensions: s.extensions.map((e) => e.name).sort(),
|
|
2373
|
+
collectionFields: s.collectionFields.map((f) => `${f.field}=${f.value}`).sort(),
|
|
2374
|
+
tokens: s.tokens.map((t) => ({ id: t.tokenId, minted: t.minted, owner: t.owner?.toLowerCase() ?? null })),
|
|
2375
|
+
events: s.events.map((e) => `${e.blockNumber}:${e.logIndex}:${e.name}`),
|
|
2376
|
+
};
|
|
2377
|
+
return createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Print the reconstructed spine — the point being that this list IS the database. Rendered inside
|
|
2381
|
+
* the index step (no header of its own: "Index it — replay the event spine" immediately followed by
|
|
2382
|
+
* a separate "The event spine" step read as a stutter).
|
|
2383
|
+
*/
|
|
2384
|
+
function walkthroughSpine(state) {
|
|
2385
|
+
if (state.events.length === 0) {
|
|
2386
|
+
warn('no events to show (the index came back empty — see the recovery hint above).');
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
const width = Math.max(...state.events.map((e) => e.name.length));
|
|
2390
|
+
state.events.forEach((e, i) => {
|
|
2391
|
+
// ERC vs ABX register: standard ERC-721 events a marketplace already understands, versus ABX's
|
|
2392
|
+
// own. Worth surfacing — it's why an ABX token indexes fine on tools that know nothing about ABX.
|
|
2393
|
+
const reg = e.register === 2 ? p('ABX') : dim('ERC');
|
|
2394
|
+
console.log(` ${dim(`#${String(i + 1).padStart(2)}`)} ${reg} ${e.name.padEnd(width)} ${dim(e.what)}`);
|
|
2395
|
+
});
|
|
2396
|
+
console.log(` ${dim('→')} those ${bold(String(state.events.length))} lines ${bold('are')} the database. ` +
|
|
2397
|
+
dim('There is no other copy that counts — not ours, not anyone\'s.'));
|
|
2398
|
+
console.log(` ${p('ABX')} ${dim('= ABX\'s own events')} ${dim('ERC')} ${dim('= bog-standard ERC-721/7572, which is why wallets and marketplaces that have never heard of ABX still show your token.')}`);
|
|
2399
|
+
}
|
|
2400
|
+
/**
|
|
2401
|
+
* [4] The claim, demonstrated: delete the local projection and rebuild it from the chain.
|
|
2402
|
+
*
|
|
2403
|
+
* This is the one step that can't be faked by good output — it drops the projection for real
|
|
2404
|
+
* (registration kept), confirms it's gone, replays from the deploy block, and compares a
|
|
2405
|
+
* fingerprint of everything chain-derived. If ABX's premise is wrong, this step fails loudly.
|
|
2406
|
+
*/
|
|
2407
|
+
async function walkthroughRebuild(indexer, address, before) {
|
|
2408
|
+
step('The moment of truth · delete it all');
|
|
2409
|
+
const fpBefore = projectionFingerprint(before);
|
|
2410
|
+
indexer.dropProjection(address);
|
|
2411
|
+
const gone = indexer.getProject(address) === null;
|
|
2412
|
+
console.log(` ${dim('wiping this computer\'s copy …')} ${gone ? g('gone. nothing left locally.') : `${c.orange}⚠ still present${c.reset}`}`);
|
|
2413
|
+
const { state: after, elapsedMs } = await indexer.reindex(address, { full: true });
|
|
2414
|
+
const fpAfter = projectionFingerprint(after);
|
|
2415
|
+
console.log(` ${dim(`asking ${CHAIN} to tell us everything again …`)} ` +
|
|
2416
|
+
`${g(`${after.eventCount} events, ${elapsedMs}ms`)}`);
|
|
2417
|
+
if (fpBefore === fpAfter) {
|
|
2418
|
+
ok(bold('byte-for-byte identical.'));
|
|
2419
|
+
info('Your token just survived losing every local file. No backup, no API key, no company —');
|
|
2420
|
+
info(`the chain remembered. ${dim('That is the whole point of ABX.')}`);
|
|
2421
|
+
info(dim(`checked by hashing every chain-derived field, not by eyeballing it: sha256 ${fpAfter.slice(0, 12)}…`));
|
|
2422
|
+
}
|
|
2423
|
+
else {
|
|
2424
|
+
warn('the rebuilt state does NOT match what we just had — that is a real bug, please report it.');
|
|
2425
|
+
info(`before ${fpBefore.slice(0, 16)}… · after ${fpAfter.slice(0, 16)}…`);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* [5] Read the token back the way a marketplace would.
|
|
2430
|
+
*
|
|
2431
|
+
* Which is genuinely a different act per lane, so it reads from the real source in each case rather
|
|
2432
|
+
* than always going through the local server:
|
|
2433
|
+
* • fully on-chain → call `tokenURI(0)` on the contract. That IS what a marketplace does, and on
|
|
2434
|
+
* this lane the whole answer (art included) comes back from the chain with nothing else running.
|
|
2435
|
+
* • off-chain custody → fetch the resolver, because that's what the baked URI points at.
|
|
2436
|
+
* Reading the on-chain lane over HTTP would have quietly implied the local server was load-bearing
|
|
2437
|
+
* when it isn't — the opposite of the lesson.
|
|
2438
|
+
*/
|
|
2439
|
+
async function walkthroughReadBack(state, baseUrl, onChainUri) {
|
|
2440
|
+
step('Read it back the way a marketplace would');
|
|
2441
|
+
const token = state.tokens[0];
|
|
2442
|
+
if (!token?.minted) {
|
|
2443
|
+
info('no minted token to read yet.');
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (onChainUri) {
|
|
2447
|
+
info(dim(`calling tokenURI(0) on your contract — the same call OpenSea makes …`));
|
|
2448
|
+
try {
|
|
2449
|
+
const uri = (await makePublicClient({ chainKey: CHAIN }).readContract({
|
|
2450
|
+
address: state.address,
|
|
2451
|
+
abi: oneOfOneImageAbi,
|
|
2452
|
+
functionName: 'tokenURI',
|
|
2453
|
+
args: [0n],
|
|
2454
|
+
}));
|
|
2455
|
+
const json = decodeOnChainJson(uri);
|
|
2456
|
+
if (json) {
|
|
2457
|
+
const parsed = JSON.parse(json);
|
|
2458
|
+
const img = typeof parsed.image === 'string' ? parsed.image : '';
|
|
2459
|
+
console.log(` ${g('✓')} came back with: ${bold(String(parsed.name ?? '(no name)'))}`);
|
|
2460
|
+
console.log(` ${dim(`image: ${img.slice(0, 48)}${img.length > 48 ? '…' : ''}`)}`);
|
|
2461
|
+
// The punchline of the whole lane: a data: URI means the art travelled IN the answer.
|
|
2462
|
+
if (img.startsWith('data:'))
|
|
2463
|
+
info(`${g('the art itself came back in that answer')} ${dim('— no link to follow, nothing to go missing')}`);
|
|
2464
|
+
}
|
|
2465
|
+
else {
|
|
2466
|
+
console.log(` ${dim(uri.slice(0, 160))}${uri.length > 160 ? dim('…') : ''}`);
|
|
2467
|
+
}
|
|
2468
|
+
info(dim('nothing was running to answer that. no server of ours, no server of yours.'));
|
|
2469
|
+
}
|
|
2470
|
+
catch (e) {
|
|
2471
|
+
info(dim(`could not read tokenURI from the chain: ${e.message}`));
|
|
2472
|
+
}
|
|
2473
|
+
console.log(` ${dim('read it yourself any time:')} ${bold(`abx tokenuri ${state.address}`)}`);
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
info(`tokenURI(0) ${dim('on chain →')} ${token.tokenURI ?? dim('(none)')}`);
|
|
2477
|
+
// Off-chain custody: the CHAIN holds a keccak256 commitment to the image and a URI base; this NODE
|
|
2478
|
+
// holds the bytes. That split is the thing worth understanding, so name it rather than implying the
|
|
2479
|
+
// JSON came from the chain.
|
|
2480
|
+
info(dim('the chain stored a URI base + a keccak256 commitment; this node serves the bytes.'));
|
|
2481
|
+
try {
|
|
2482
|
+
const res = await fetch(`${baseUrl}/t/${resolveChain(CHAIN).id}/${state.address}/0`);
|
|
2483
|
+
const json = (await res.json());
|
|
2484
|
+
const shown = { name: json.name, image: json.image };
|
|
2485
|
+
console.log(` ${dim(JSON.stringify(shown))}`);
|
|
2486
|
+
if (Array.isArray(json.abx_provenance)) {
|
|
2487
|
+
info(dim(`every field is tagged with where it came from (abx_provenance: ${json.abx_provenance.length} entries)`));
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
catch (e) {
|
|
2491
|
+
info(dim(`could not read the served metadata: ${e.message}`));
|
|
2492
|
+
}
|
|
2493
|
+
console.log(` ${dim('prove the bytes match the chain:')} ${bold(`abx verify ${state.address}`)}`);
|
|
2494
|
+
}
|
|
2495
|
+
/** Is a TCP port already bound on localhost? Used to preflight a serve BEFORE spending a tx. */
|
|
2496
|
+
async function portInUse(port) {
|
|
2497
|
+
const { createServer } = await import('node:net');
|
|
2498
|
+
return new Promise((resolve) => {
|
|
2499
|
+
const probe = createServer();
|
|
2500
|
+
probe.once('error', (e) => resolve(e.code === 'EADDRINUSE'));
|
|
2501
|
+
probe.once('listening', () => probe.close(() => resolve(false)));
|
|
2502
|
+
probe.listen(port);
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Index a project we *just* deployed — and don't believe a zero.
|
|
2507
|
+
*
|
|
2508
|
+
* `eth_getLogs` is read-after-write inconsistent on load-balanced RPCs: `waitForTransactionReceipt`
|
|
2509
|
+
* resolves against a node that has the block, then the log query lands on one that doesn't yet, and
|
|
2510
|
+
* returns an empty set for a block we KNOW contains our deploy. The old code took that single read at
|
|
2511
|
+
* face value, printed `✓ reconstructed 0 events`, stored the empty projection, and served an empty
|
|
2512
|
+
* dashboard — a first-run that looks like the toolkit simply doesn't work. It reproduced 100% of the
|
|
2513
|
+
* time on `https://sepolia.base.org`, which is the DEFAULT endpoint when there's no `.env`, i.e. the
|
|
2514
|
+
* documented first run was the broken path. The same block returned all 9 logs seconds later.
|
|
2515
|
+
*
|
|
2516
|
+
* We have the one thing that makes this checkable: we just minted, so the spine cannot be empty.
|
|
2517
|
+
* So verify instead of trusting — re-scan with backoff until events appear, and if they never do,
|
|
2518
|
+
* say so as a FAILURE with the recovery command rather than dressing a zero up as a ✓.
|
|
2519
|
+
*/
|
|
2520
|
+
async function reindexAfterDeploy(indexer, address, opts = {}) {
|
|
2521
|
+
const attempts = opts.attempts ?? 6;
|
|
2522
|
+
const delayMs = opts.delayMs ?? 1500;
|
|
2523
|
+
let last = await indexer.reindex(address);
|
|
2524
|
+
for (let i = 1; i < attempts && last.state.eventCount === 0; i++) {
|
|
2525
|
+
if (i === 1) {
|
|
2526
|
+
info(dim("no events yet — the RPC hasn't served the logs for that block; re-scanning…"));
|
|
2527
|
+
}
|
|
2528
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
2529
|
+
// `full: true` — the stored projection has 0 events, so there is no valid checkpoint to
|
|
2530
|
+
// resume from; a full replay from the deploy block is the only correct re-scan.
|
|
2531
|
+
last = await indexer.reindex(address, { full: true });
|
|
2532
|
+
}
|
|
2533
|
+
if (last.state.eventCount === 0) {
|
|
2534
|
+
warn(`the RPC still reports no logs for this project after ${attempts} tries — the deploy DID succeed ` +
|
|
2535
|
+
`(it's on chain), but this node can't reconstruct it yet.`);
|
|
2536
|
+
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).')}`);
|
|
2537
|
+
}
|
|
2538
|
+
return last;
|
|
2539
|
+
}
|
|
2250
2540
|
/**
|
|
2251
2541
|
* `abx preview` — serve the program on localhost, live, for as long as the work is being made.
|
|
2252
2542
|
*
|
|
@@ -4258,6 +4548,44 @@ async function cmdMigrate(address, flags) {
|
|
|
4258
4548
|
info('keep the source running until DNS / base-URI propagates (source-only images were already re-pinned above, unless a warning said otherwise).');
|
|
4259
4549
|
}
|
|
4260
4550
|
// ── doctor ────────────────────────────────────────────────────────────────--
|
|
4551
|
+
/**
|
|
4552
|
+
* Doctor's "want me to fix that?" for a missing or stale skill. Three lanes, deliberately:
|
|
4553
|
+
* `--fix` install without asking (CI, scripts, an agent running doctor for someone)
|
|
4554
|
+
* interactive name the exact directories, then ask — Enter accepts, since doctor's whole job is
|
|
4555
|
+
* getting setup right and this is the one check whose fix is a local file copy
|
|
4556
|
+
* non-TTY change NOTHING and print how to do it; a diagnostic must never mutate a
|
|
4557
|
+
* scripted environment just because nobody was there to say no
|
|
4558
|
+
* Honors `--global` / `--agent` so the fix can target the same place an explicit install would.
|
|
4559
|
+
*/
|
|
4560
|
+
async function offerSkillInstall(flags, stale, indent) {
|
|
4561
|
+
const opts = { global: flags.global !== undefined, agent: flags.agent };
|
|
4562
|
+
const src = resolveBundledSkill();
|
|
4563
|
+
if (!src)
|
|
4564
|
+
return; // no bundled skill to install (dev checkout oddity) — the hint above still stands
|
|
4565
|
+
const verb = stale ? 'resync' : 'install';
|
|
4566
|
+
const dests = defaultSkillDests(opts);
|
|
4567
|
+
if (flags.fix === undefined) {
|
|
4568
|
+
if (!process.stdin.isTTY) {
|
|
4569
|
+
console.log(`${indent}${dim(`non-interactive — run \`abx doctor --fix\` (or \`abx skill install\`) to ${verb} it.`)}`);
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4573
|
+
const answer = await new Promise((resolve) => {
|
|
4574
|
+
// EOF (Ctrl-D) closes the interface WITHOUT firing the question callback — awaiting only the
|
|
4575
|
+
// callback would hang doctor forever. Treat a closed stream as a decline: the safe direction is
|
|
4576
|
+
// always "change nothing", never "write files because nobody answered".
|
|
4577
|
+
rl.once('close', () => resolve('n'));
|
|
4578
|
+
rl.question(`${indent}${verb} the abx skill into ${bold(dests)} now? [Y/n] `, resolve);
|
|
4579
|
+
});
|
|
4580
|
+
rl.close();
|
|
4581
|
+
if (declinesSkillInstall(answer)) {
|
|
4582
|
+
console.log(`${indent}${dim(`skipped — \`abx skill install\` when you want it.`)}`);
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
console.log('');
|
|
4587
|
+
installSkillToDefaults(src, opts);
|
|
4588
|
+
}
|
|
4261
4589
|
async function cmdDoctor(flags) {
|
|
4262
4590
|
console.log(bold('\n abx doctor') + dim(` · ${CHAIN}`) + '\n');
|
|
4263
4591
|
// Two visual tiers: PASS/FAIL checks (✓/✗) for things that are either working or broken, and an
|
|
@@ -4274,16 +4602,28 @@ async function cmdDoctor(flags) {
|
|
|
4274
4602
|
const cliVersion = readCliVersion();
|
|
4275
4603
|
const skillVersions = installedSkillVersions();
|
|
4276
4604
|
const staleSkills = skillVersions.filter((v) => compareVersions(cliVersion, v) > 0);
|
|
4277
|
-
|
|
4605
|
+
const skillMissing = skillVersions.length === 0;
|
|
4606
|
+
const skillStale = !skillMissing && staleSkills.length > 0;
|
|
4607
|
+
if (skillMissing) {
|
|
4278
4608
|
check('agent skill', false, `not installed — run ${g('abx skill install')}`);
|
|
4279
4609
|
console.log(`${CONT}${dim('(recommended: let a coding agent drive abx)')}`);
|
|
4280
4610
|
}
|
|
4281
|
-
else if (
|
|
4611
|
+
else if (skillStale) {
|
|
4282
4612
|
check('agent skill', false, `v${staleSkills.join(', v')} behind CLI v${cliVersion} — run ${g('abx skill install')}`);
|
|
4283
4613
|
}
|
|
4284
4614
|
else {
|
|
4285
4615
|
check('agent skill', true, `in sync (v${cliVersion})`);
|
|
4286
4616
|
}
|
|
4617
|
+
// Offer to fix it here rather than only naming the command. `npm i -g` + `abx skill install` was a
|
|
4618
|
+
// two-step install flow where the second step is easy to skip and invisible when skipped (an agent
|
|
4619
|
+
// that never learned abx just... doesn't use it). Doctor is already the documented first run, so
|
|
4620
|
+
// this collapses the flow without an npm `postinstall` hook — which could not work anyway: npm runs
|
|
4621
|
+
// lifecycle scripts with cwd set to the installed package dir (so the skill would land inside
|
|
4622
|
+
// node_modules), pnpm gates install scripts by default, and writing to a user's ~/.claude on
|
|
4623
|
+
// install is the kind of side effect that belongs to the user, not to us.
|
|
4624
|
+
if (skillMissing || skillStale) {
|
|
4625
|
+
await offerSkillInstall(flags, skillStale, CONT);
|
|
4626
|
+
}
|
|
4287
4627
|
console.log('');
|
|
4288
4628
|
// 2. Core environment (✓/✗). Signing-wallet balances are computed here (they need the RPC) but
|
|
4289
4629
|
// printed in the Optional block below, so buffer them.
|
|
@@ -4376,7 +4716,13 @@ function printServing(url, address) {
|
|
|
4376
4716
|
console.log(` ${dim('image ')}${url}/t/${cid}/${address}/0/image`);
|
|
4377
4717
|
console.log(` ${dim('state API ')}${url}/api/project/${address}`);
|
|
4378
4718
|
}
|
|
4379
|
-
|
|
4719
|
+
// The dashboard is READ-ONLY: re-index/verify are admin actions that 404 unless the node has an
|
|
4720
|
+
// ABX_RESOLVER_ADMIN_TOKEN, so there is no button to press. This line used to say "hit Re-index
|
|
4721
|
+
// from chain", which sent every first-run user hunting for a control that isn't there.
|
|
4722
|
+
console.log(`\n ${dim('The dashboard shows the event spine it replayed — that table IS the reconstruction.')}`);
|
|
4723
|
+
if (address) {
|
|
4724
|
+
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.')}`);
|
|
4725
|
+
}
|
|
4380
4726
|
console.log(` ${dim('Ctrl-C to stop.')}\n`);
|
|
4381
4727
|
}
|
|
4382
4728
|
// ── deploy-resolver: scaffold a hosted resolver for a provider the operator owns ──────────
|
|
@@ -4986,8 +5332,12 @@ const COMMAND_HELP = {
|
|
|
4986
5332
|
${bold('abx set-max-invocations')} <address> --max <N> ${dim('— LOWER the supply cap (Series). Sends a tx.')}
|
|
4987
5333
|
--max <N> the new cap — MONOTONIC: can only DECREASE, and never below what's already minted (else it reverts)`,
|
|
4988
5334
|
doctor: `
|
|
4989
|
-
${bold('abx doctor')} ${dim('— preflight readiness: signing key/wallet, RPC health, canonical factory, storage.
|
|
5335
|
+
${bold('abx doctor')} ${dim('— preflight readiness: agent skill, signing key/wallet, RPC health, canonical factory, storage.')}
|
|
4990
5336
|
--for 0x.. also report that address's balance (fund before signing)
|
|
5337
|
+
${g('--fix')} ${dim('install/resync the agent skill without asking (the one thing doctor can repair)')}
|
|
5338
|
+
${g('--global')} · ${g('--agent')} <a> ${dim('where --fix installs the skill (mirrors `abx skill install`)')}
|
|
5339
|
+
${dim('Read-only apart from --fix. Interactively it OFFERS to install a missing/stale skill; a non-TTY')}
|
|
5340
|
+
${dim('run changes nothing and just prints the command, so scripts and CI are never mutated.')}
|
|
4991
5341
|
${dim('a missing signing key is NOT fatal — the wallet lane (`--sign`) needs no key in `.env`.')}`,
|
|
4992
5342
|
status: `
|
|
4993
5343
|
${bold('abx status')} ${dim('— list the projects this node has indexed, plus node info. Read-only.')}`,
|
|
@@ -5265,6 +5615,31 @@ function installSkillTo(src, dest) {
|
|
|
5265
5615
|
rmSync(dest, { recursive: true, force: true });
|
|
5266
5616
|
cpSync(src, dest, { recursive: true });
|
|
5267
5617
|
}
|
|
5618
|
+
/**
|
|
5619
|
+
* Install the bundled skill into the default per-agent parents (or `~` with `global`), reporting
|
|
5620
|
+
* each destination. Shared by `abx skill install` and `abx doctor`'s offer to fix a missing/stale
|
|
5621
|
+
* skill — one implementation, so the two can't drift on where the skill lands or what it prints.
|
|
5622
|
+
*/
|
|
5623
|
+
function installSkillToDefaults(src, opts = {}) {
|
|
5624
|
+
const version = readSkillVersion(joinPath(src, 'SKILL.md')) ?? readCliVersion();
|
|
5625
|
+
const base = opts.global ? homedir() : process.cwd();
|
|
5626
|
+
const parents = resolveInstallParents(opts.agent);
|
|
5627
|
+
ok(`installed the abx skill v${version}${opts.global ? ' (global, ~)' : ''}:`);
|
|
5628
|
+
for (const parent of parents) {
|
|
5629
|
+
const dest = joinPath(base, parent, SKILL_DIR_NAME);
|
|
5630
|
+
installSkillTo(src, dest);
|
|
5631
|
+
const label = SKILL_PARENT_LABELS[parent];
|
|
5632
|
+
console.log(` ${g(joinPath(parent, SKILL_DIR_NAME))}${label ? dim(' → ' + label) : ''}`);
|
|
5633
|
+
}
|
|
5634
|
+
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
5635
|
+
info(`the skill is version-locked to this CLI (v${version}); re-run ${g('abx skill install')} after upgrading so the two stay in sync.`);
|
|
5636
|
+
return version;
|
|
5637
|
+
}
|
|
5638
|
+
/** The default skill destinations, as a display string — what doctor's prompt has to name up front. */
|
|
5639
|
+
function defaultSkillDests(opts = {}) {
|
|
5640
|
+
const prefix = opts.global ? '~/' : './';
|
|
5641
|
+
return resolveInstallParents(opts.agent).map((p) => `${prefix}${p}`).join(' and ');
|
|
5642
|
+
}
|
|
5268
5643
|
async function cmdSkill(rest, flags) {
|
|
5269
5644
|
const sub = rest[0] ?? 'install';
|
|
5270
5645
|
const src = resolveBundledSkill();
|
|
@@ -5287,17 +5662,7 @@ async function cmdSkill(rest, flags) {
|
|
|
5287
5662
|
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
5288
5663
|
return;
|
|
5289
5664
|
}
|
|
5290
|
-
|
|
5291
|
-
const parents = resolveInstallParents(flags.agent);
|
|
5292
|
-
ok(`installed the abx skill v${version}${flags.global !== undefined ? ' (global, ~)' : ''}:`);
|
|
5293
|
-
for (const parent of parents) {
|
|
5294
|
-
const dest = joinPath(base, parent, SKILL_DIR_NAME);
|
|
5295
|
-
installSkillTo(src, dest);
|
|
5296
|
-
const label = SKILL_PARENT_LABELS[parent];
|
|
5297
|
-
console.log(` ${g(joinPath(parent, SKILL_DIR_NAME))}${label ? dim(' → ' + label) : ''}`);
|
|
5298
|
-
}
|
|
5299
|
-
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
5300
|
-
info(`the skill is version-locked to this CLI (v${version}); re-run ${g('abx skill install')} after upgrading so the two stay in sync.`);
|
|
5665
|
+
installSkillToDefaults(src, { global: flags.global !== undefined, agent: flags.agent });
|
|
5301
5666
|
return;
|
|
5302
5667
|
}
|
|
5303
5668
|
throw new Error('usage: abx skill <install|path> [--agent claude|cursor|codex|gemini|copilot] [--global] [--target <dir>]');
|