@artblocks/abx-cli 0.1.0-alpha.4 → 0.1.0-alpha.41
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/CHANGELOG.md +161 -0
- package/assets/renderer-scaffold/README.md +41 -11
- package/assets/renderer-scaffold/foundry.toml +5 -0
- package/assets/renderer-scaffold/remappings.txt +1 -1
- package/assets/renderer-scaffold/script/DeployHooks.s.sol +24 -0
- package/assets/renderer-scaffold/script/Preview.s.sol +99 -0
- package/assets/renderer-scaffold/src/MyHooks.sol +20 -0
- package/assets/renderer-scaffold/src/MyRenderer.sol +4 -4
- package/assets/renderer-scaffold/src/MyTraits.sol +2 -2
- package/assets/renderer-scaffold/test/MyRenderer.t.sol +60 -3
- package/dist/bin.d.ts +26 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +63 -0
- package/dist/bin.js.map +1 -0
- package/dist/capabilities.d.ts +99 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +144 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/commands/auth.d.ts +54 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +447 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/deploy.d.ts +242 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +5429 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.d.ts.map +1 -0
- package/dist/commands/feedback.js +147 -0
- package/dist/commands/feedback.js.map +1 -0
- package/dist/commands/maintenance.d.ts +4 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +114 -0
- package/dist/commands/maintenance.js.map +1 -0
- package/dist/commands/project.d.ts +429 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +1720 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/reads.d.ts +85 -0
- package/dist/commands/reads.d.ts.map +1 -0
- package/dist/commands/reads.js +868 -0
- package/dist/commands/reads.js.map +1 -0
- package/dist/commands/scaffold.d.ts +89 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +738 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/service.d.ts +67 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +748 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/storage.d.ts +51 -0
- package/dist/commands/storage.d.ts.map +1 -0
- package/dist/commands/storage.js +399 -0
- package/dist/commands/storage.js.map +1 -0
- package/dist/commands/submit-app.d.ts +102 -0
- package/dist/commands/submit-app.d.ts.map +1 -0
- package/dist/commands/submit-app.js +622 -0
- package/dist/commands/submit-app.js.map +1 -0
- package/dist/config.d.ts +105 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +351 -12
- package/dist/config.js.map +1 -1
- package/dist/conformance.d.ts +31 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +390 -0
- package/dist/conformance.js.map +1 -0
- package/dist/contract-read-error.d.ts +5 -0
- package/dist/contract-read-error.d.ts.map +1 -0
- package/dist/contract-read-error.js +37 -0
- package/dist/contract-read-error.js.map +1 -0
- package/dist/deploy-plan.d.ts +150 -0
- package/dist/deploy-plan.d.ts.map +1 -0
- package/dist/deploy-plan.js +31 -0
- package/dist/deploy-plan.js.map +1 -0
- package/dist/deps.d.ts +6 -39
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +4 -68
- package/dist/deps.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/flag-allowlists.d.ts +53 -0
- package/dist/flag-allowlists.d.ts.map +1 -0
- package/dist/flag-allowlists.js +184 -0
- package/dist/flag-allowlists.js.map +1 -0
- package/dist/flags.d.ts +47 -0
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +125 -1
- package/dist/flags.js.map +1 -1
- package/dist/jsonout.d.ts +37 -0
- package/dist/jsonout.d.ts.map +1 -0
- package/dist/jsonout.js +79 -0
- package/dist/jsonout.js.map +1 -0
- package/dist/kind.d.ts +57 -0
- package/dist/kind.d.ts.map +1 -0
- package/dist/kind.js +122 -0
- package/dist/kind.js.map +1 -0
- package/dist/main.js +786 -4838
- package/dist/main.js.map +1 -1
- package/dist/mintpage.d.ts +17 -2
- package/dist/mintpage.d.ts.map +1 -1
- package/dist/mintpage.js +241 -54
- package/dist/mintpage.js.map +1 -1
- package/dist/output.d.ts +179 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +780 -0
- package/dist/output.js.map +1 -0
- package/dist/ownerops.d.ts +381 -57
- package/dist/ownerops.d.ts.map +1 -1
- package/dist/ownerops.js +2014 -359
- package/dist/ownerops.js.map +1 -1
- package/dist/preview.d.ts +23 -5
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +95 -43
- package/dist/preview.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/provision.d.ts +3 -13
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +19 -21
- package/dist/provision.js.map +1 -1
- package/dist/remote.d.ts +157 -52
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +435 -46
- package/dist/remote.js.map +1 -1
- package/dist/riskgate.d.ts +62 -0
- package/dist/riskgate.d.ts.map +1 -0
- package/dist/riskgate.js +234 -0
- package/dist/riskgate.js.map +1 -0
- package/dist/scaffold.d.ts +12 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +56 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/schema.d.ts +36 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +121 -26
- package/dist/schema.js.map +1 -1
- package/dist/script-chunks.d.ts +8 -0
- package/dist/script-chunks.d.ts.map +1 -0
- package/dist/script-chunks.js +35 -0
- package/dist/script-chunks.js.map +1 -0
- package/dist/served.d.ts +30 -0
- package/dist/served.d.ts.map +1 -0
- package/dist/served.js +112 -0
- package/dist/served.js.map +1 -0
- package/dist/signer.d.ts +13 -0
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +84 -15
- package/dist/signer.js.map +1 -1
- package/dist/update-check.d.ts +86 -5
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +161 -20
- package/dist/update-check.js.map +1 -1
- package/package.json +13 -12
- package/skill/SKILL.md +181 -347
- package/skill/agents/openai.yaml +4 -0
- package/skill/reference/capabilities.md +188 -0
- package/skill/reference/code.md +220 -0
- package/skill/reference/creator-token.md +94 -0
- package/skill/reference/deploy.md +174 -0
- package/skill/reference/diagnose.md +178 -0
- package/skill/reference/hosting.md +186 -93
- package/skill/reference/operate.md +220 -0
- package/skill/reference/services.md +121 -0
- package/skill/reference/setup.md +154 -36
- package/assets/renderer-scaffold/src/interfaces/IAbxFieldRenderer.sol +0 -32
- package/assets/renderer-scaffold/src/interfaces/IAbxParams.sol +0 -26
- package/dist/inspect.d.ts +0 -48
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js +0 -184
- package/dist/inspect.js.map +0 -1
- package/dist/migrate.d.ts +0 -65
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -180
- package/dist/migrate.js.map +0 -1
- package/dist/onchain-uri.d.ts +0 -97
- package/dist/onchain-uri.d.ts.map +0 -1
- package/dist/onchain-uri.js +0 -243
- package/dist/onchain-uri.js.map +0 -1
- package/dist/upload.d.ts +0 -28
- package/dist/upload.d.ts.map +0 -1
- package/dist/upload.js +0 -41
- package/dist/upload.js.map +0 -1
- package/skill/reference/code-projects.md +0 -246
- package/skill/reference/operating.md +0 -116
- package/skill/reference/troubleshooting.md +0 -28
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolding + environment commands: `doctor` (check key/RPC/balance/factory/storage — and offer
|
|
3
|
+
* to install/resync the agent skill), `mint-page` (a self-contained Next.js minting app),
|
|
4
|
+
* `scaffold-renderer` (copy the Solidity on-chain-renderer starting point), and `skill` (install
|
|
5
|
+
* the version-locked abx agent skill into a coding agent).
|
|
6
|
+
*/
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { basename, join as joinPath, resolve as resolvePath, sep } from 'node:path';
|
|
10
|
+
import { createInterface } from 'node:readline';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { isCurrentFactory, isCurrentOneOfOneEditionFactory, makePublicClient, makeWalletClient, oneOfOneImageAbi, probeRpcEndpoints, resolveChain, rpcEnvVar, resolveRpcUrls, redactRpcUrl, } from '@artblocks/abx-sdk';
|
|
13
|
+
import { arweaveAddress, probeStorageBackend, resolveBackend } from '@artblocks/abx-storage';
|
|
14
|
+
import { formatEther } from 'viem';
|
|
15
|
+
import { CHAIN, activeBackendId, arweaveKeyFilePath, factoryAddress, faucetHint, fixedPriceMinterAddress, fixedPriceMinter1155Address, oneOfOneEditionFactoryAddress, loadArweaveJwk, storageOptions, } from '../config.js';
|
|
16
|
+
import { CliError } from '../errors.js';
|
|
17
|
+
import { positionalArgs } from '../flags.js';
|
|
18
|
+
import { detectTokenKind } from '../kind.js';
|
|
19
|
+
import { PUBLIC_RPC, mintPageArtifact } from '../mintpage.js';
|
|
20
|
+
import { bold, c, dim, findCliPackageRoot, findRepoRoot, g, info, ok, step, warn } from '../output.js';
|
|
21
|
+
import { declinesSkillInstall } from '../prompt.js';
|
|
22
|
+
import { listConfiguredRemotes, misnamedRemoteVars, probeRemoteCredential, selfTokenMigrationWarning } from '../remote.js';
|
|
23
|
+
import { copyScaffold, SCAFFOLD_ANCHOR } from '../scaffold.js';
|
|
24
|
+
import { AGENT_SKILL_PARENTS, LEGACY_SKILL_DIR_NAMES, SKILL_DIR_NAME, binaryProvenance, checkForCliUpdate, compareVersions, installedSkillCopies, installedLegacySkillCopies, readCliVersion, readSkillName, readSkillVersion, skillRefreshCommands, } from '../update-check.js';
|
|
25
|
+
// ── doctor ────────────────────────────────────────────────────────────────--
|
|
26
|
+
/**
|
|
27
|
+
* Doctor's "want me to fix that?" for a missing or stale skill. Three lanes, deliberately:
|
|
28
|
+
* `--fix` install without asking (CI, scripts, an agent running doctor for someone)
|
|
29
|
+
* interactive name the exact directories, then ask — Enter accepts, since doctor's whole job is
|
|
30
|
+
* getting setup right and this is the one check whose fix is a local file copy
|
|
31
|
+
* non-TTY change NOTHING and print how to do it; a diagnostic must never mutate a
|
|
32
|
+
* scripted environment just because nobody was there to say no
|
|
33
|
+
* Honors `--global` / `--agent` so the fix can target the same place an explicit install would.
|
|
34
|
+
*/
|
|
35
|
+
export async function offerSkillInstall(flags, stale, indent) {
|
|
36
|
+
const opts = { global: flags.global !== undefined, agent: flags.agent };
|
|
37
|
+
const src = resolveBundledSkill();
|
|
38
|
+
if (!src)
|
|
39
|
+
return; // no bundled skill to install (dev checkout oddity) — the hint above still stands
|
|
40
|
+
const verb = stale ? 'resync' : 'install';
|
|
41
|
+
const dests = defaultSkillDests(opts);
|
|
42
|
+
if (flags.fix === undefined) {
|
|
43
|
+
if (!process.stdin.isTTY) {
|
|
44
|
+
console.log(`${indent}${dim(`non-interactive — run \`abx doctor --fix\` (or \`abx skill install\`) to ${verb} it.`)}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
48
|
+
const answer = await new Promise((resolve) => {
|
|
49
|
+
// EOF (Ctrl-D) closes the interface WITHOUT firing the question callback — awaiting only the
|
|
50
|
+
// callback would hang doctor forever. Treat a closed stream as a decline: the safe direction is
|
|
51
|
+
// always "change nothing", never "write files because nobody answered".
|
|
52
|
+
rl.once('close', () => resolve('n'));
|
|
53
|
+
rl.question(`${indent}${verb} the abx skill into ${bold(dests)} now? [Y/n] `, resolve);
|
|
54
|
+
});
|
|
55
|
+
rl.close();
|
|
56
|
+
if (declinesSkillInstall(answer)) {
|
|
57
|
+
console.log(`${indent}${dim(`skipped — \`abx skill install\` when you want it.`)}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log('');
|
|
62
|
+
installSkillToDefaults(src, opts);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The funding half of doctor's signing line. Split out because "is this wallet usable?" is not
|
|
66
|
+
* `balance > 0`.
|
|
67
|
+
*
|
|
68
|
+
* `> 0n` is the wrong test: dust is not usable funding.
|
|
69
|
+
*
|
|
70
|
+
* So there are three states, not two. `DUST_FLOOR` is deliberately a readout threshold and NOT a
|
|
71
|
+
* refusal — we do not estimate anyone's gas, and a cheap L2 deploy under it is the caller's call to
|
|
72
|
+
* make. It only decides whether doctor hands over a faucet link.
|
|
73
|
+
*/
|
|
74
|
+
const DUST_FLOOR = 1000000000000000n; // 0.001 ETH
|
|
75
|
+
export async function describeBalance(publicClient, address) {
|
|
76
|
+
const bal = await publicClient.getBalance({ address });
|
|
77
|
+
if (bal === 0n)
|
|
78
|
+
return `empty — fund it (${faucetHint(CHAIN)})`;
|
|
79
|
+
// Trimmed to 6 decimals: a wei-precise figure is unreadable at a glance and this line exists to
|
|
80
|
+
// be glanced at. The exact number is one `cast balance` away for anyone who needs it.
|
|
81
|
+
const eth = formatEther(bal);
|
|
82
|
+
const short = eth.includes('.') ? `${eth.split('.')[0]}.${eth.split('.')[1].slice(0, 6).replace(/0+$/, '') || '0'}` : eth;
|
|
83
|
+
if (bal < DUST_FLOOR)
|
|
84
|
+
return `${short} ETH — too little to deploy; top it up (${faucetHint(CHAIN)})`;
|
|
85
|
+
return `funded ${short} ETH`;
|
|
86
|
+
}
|
|
87
|
+
/** Actionable doctor detail for a configured remote whose credential is absent. */
|
|
88
|
+
export function describeMissingRemoteToken(name) {
|
|
89
|
+
return name.toLowerCase() === 'abx' ? 'no token — run abx auth login' : 'no token';
|
|
90
|
+
}
|
|
91
|
+
/** Continuation indent: aligns under a check/opt detail column. */
|
|
92
|
+
const CONT = ' '.repeat(17);
|
|
93
|
+
/**
|
|
94
|
+
* Does every configured endpoint agree with itself about this wallet's nonce?
|
|
95
|
+
*
|
|
96
|
+
* `pending` is by definition >= `latest` on a coherent node. An endpoint that has not caught up with
|
|
97
|
+
* its own head answers LOWER, and the first send of the next `abx` command re-uses a spent nonce —
|
|
98
|
+
* silently, because a failed simulation means nothing is broadcast. Measured on `sepolia.base.org`;
|
|
99
|
+
* never on the publicnode fallback. The write path floors at `max(pending, latest)` so this cannot
|
|
100
|
+
* break a send any more — this names the endpoint instead of quietly routing around it.
|
|
101
|
+
*
|
|
102
|
+
* **Says "unknown" rather than passing on no evidence.** A wallet that has never sent reads 0/0
|
|
103
|
+
* everywhere, which proves nothing about the endpoint — and a check that quietly goes green on no
|
|
104
|
+
* signal is the same false green as an RPC answering `[]` because it pruned its logs.
|
|
105
|
+
*/
|
|
106
|
+
async function reportNonceCoherence(address) {
|
|
107
|
+
const urls = resolveRpcUrls(CHAIN);
|
|
108
|
+
if (urls.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
const behind = [];
|
|
111
|
+
let sawAnyActivity = false;
|
|
112
|
+
for (const url of urls) {
|
|
113
|
+
const client = makePublicClient({ chainKey: CHAIN, rpcUrl: url }); // one endpoint, no failover — we are testing THIS one
|
|
114
|
+
try {
|
|
115
|
+
const [pending, latest] = await Promise.all([
|
|
116
|
+
client.getTransactionCount({ address, blockTag: 'pending' }),
|
|
117
|
+
client.getTransactionCount({ address, blockTag: 'latest' }),
|
|
118
|
+
]);
|
|
119
|
+
if (latest > 0 || pending > 0)
|
|
120
|
+
sawAnyActivity = true;
|
|
121
|
+
if (pending < latest)
|
|
122
|
+
behind.push(`${redactRpcUrl(url)} (pending ${pending} < latest ${latest})`);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Unreachable endpoints are already the RPC check's business, not this one's.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (behind.length > 0) {
|
|
129
|
+
console.log(`${CONT}${c.orange}⚠${c.reset}${dim(` nonce view is BEHIND its own head on: ${behind.join(', ')} — that endpoint can hand the next command a spent nonce. abx floors at max(pending, latest) so sends still work; put a coherent endpoint first in ${rpcEnvVar(CHAIN)} if you script against it directly`)}`);
|
|
130
|
+
}
|
|
131
|
+
else if (!sawAnyActivity) {
|
|
132
|
+
console.log(`${CONT}${dim('nonce coherence: unknown — this wallet has sent nothing yet, so every endpoint reads 0 and the check has no signal')}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export async function cmdDoctor(flags) {
|
|
136
|
+
// Node goes in the header, not a check row: below the floor the CLI never reaches this function
|
|
137
|
+
// (src/bin.ts refuses to start), so there is no failing state to render. It is here because
|
|
138
|
+
// doctor's output is what people paste when reporting a problem, and the runtime is one of its
|
|
139
|
+
// most useful diagnostic lines.
|
|
140
|
+
console.log(bold('\n abx doctor') + dim(` · ${CHAIN} · node ${process.version}`) + '\n');
|
|
141
|
+
// Two visual tiers: PASS/FAIL checks (✓/✗) for things that are either working or broken, and an
|
|
142
|
+
// "Optional" block (·) for path-dependent setup that is fine to be unset. We deliberately do NOT
|
|
143
|
+
// use ⚠ for "unset but often fine" — that read as noise; ⚠ is reserved for a real gotcha (a
|
|
144
|
+
// range-capped RPC). Labels are padded so both tiers align.
|
|
145
|
+
// 16 = the longest label ('edition factory', 15) + a one-space gutter. It was 13, so that label
|
|
146
|
+
// ran straight into its address (`edition factory0xe8b1…`) — unreadable on the one row whose
|
|
147
|
+
// detail is a value you copy. Any label added here must fit, or this number moves.
|
|
148
|
+
const check = (label, pass, detail = '') => console.log(` ${pass ? g('✓') : `${c.red}✗${c.reset}`} ${label.padEnd(16)}${detail ? dim(detail) : ''}`);
|
|
149
|
+
const opt = (label, detail) => console.log(` ${dim('·')} ${label.padEnd(16)}${dim(detail)}`);
|
|
150
|
+
const hasKey = !!process.env.ABX_DEPLOYER_PK;
|
|
151
|
+
// 1. Agent skill — FIRST and prominent. The primary way to use abx is to let a coding agent drive
|
|
152
|
+
// it, so a missing/stale skill is a ✗: not broken infra, but the main UX isn't set up. Its
|
|
153
|
+
// version lives in SKILL.md frontmatter (version-locked to this CLI). Notify-only — no exit code.
|
|
154
|
+
const cliVersion = readCliVersion();
|
|
155
|
+
const skillCopies = installedSkillCopies();
|
|
156
|
+
const legacyCopies = installedLegacySkillCopies();
|
|
157
|
+
const staleCopies = skillCopies.filter((copy) => compareVersions(cliVersion, copy.version) > 0);
|
|
158
|
+
const staleSkills = [...new Set(staleCopies.map((copy) => copy.version))];
|
|
159
|
+
const skillMissing = skillCopies.length === 0;
|
|
160
|
+
const skillStale = !skillMissing && staleCopies.length > 0;
|
|
161
|
+
const skillNeedsMigration = legacyCopies.length > 0;
|
|
162
|
+
// Which copy, and the command that refreshes THAT copy — a bare `abx skill install` never touches
|
|
163
|
+
// the global one, so naming it for a stale global copy printed a fix that could not clear the ✗.
|
|
164
|
+
const skillFix = skillRefreshCommands([...staleCopies, ...legacyCopies]).join(' && ');
|
|
165
|
+
const staleWhere = staleCopies.length === 1 ? ` (${staleCopies[0].scope}: ${staleCopies[0].path})` : '';
|
|
166
|
+
if (skillNeedsMigration) {
|
|
167
|
+
const where = legacyCopies.length === 1 ? ` (${legacyCopies[0].scope}: ${legacyCopies[0].path})` : ` (${legacyCopies.length} copies)`;
|
|
168
|
+
check('agent skill', false, `legacy abx-self-host skill still active${where} — migrate: ${g(skillFix)}`);
|
|
169
|
+
for (const copy of legacyCopies)
|
|
170
|
+
console.log(`${CONT}${dim(`${copy.scope}: ${copy.path}`)}`);
|
|
171
|
+
console.log(`${CONT}${dim('the installer archives the old folder outside the discovery tree; it does not delete customizations')}`);
|
|
172
|
+
}
|
|
173
|
+
else if (skillMissing) {
|
|
174
|
+
check('agent skill', false, `not installed — run ${g('abx skill install')}`);
|
|
175
|
+
console.log(`${CONT}${dim('(recommended: let a coding agent drive abx)')}`);
|
|
176
|
+
}
|
|
177
|
+
else if (skillStale) {
|
|
178
|
+
check('agent skill', false, `v${staleSkills.join(', v')} behind CLI v${cliVersion}${staleWhere} — run ${g(skillFix)}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
check('agent skill', true, `in sync (v${cliVersion})`);
|
|
182
|
+
}
|
|
183
|
+
// Offer to fix it here rather than only naming the command. `npm i -g` + `abx skill install` was a
|
|
184
|
+
// two-step install flow where the second step is easy to skip and invisible when skipped (an agent
|
|
185
|
+
// that never learned abx just... doesn't use it). Doctor is already the documented first run, so
|
|
186
|
+
// this collapses the flow without an npm `postinstall` hook — which could not work anyway: npm runs
|
|
187
|
+
// lifecycle scripts with cwd set to the installed package dir (so the skill would land inside
|
|
188
|
+
// node_modules), pnpm gates install scripts by default, and writing to a user's ~/.claude on
|
|
189
|
+
// install is the kind of side effect that belongs to the user, not to us.
|
|
190
|
+
if ((skillMissing || skillStale) && !skillNeedsMigration) {
|
|
191
|
+
await offerSkillInstall(flags, skillStale, CONT);
|
|
192
|
+
}
|
|
193
|
+
console.log('');
|
|
194
|
+
// 2. Core environment (✓/✗). Signing-wallet balances are computed here (they need the RPC) but
|
|
195
|
+
// printed in the Optional block below, so buffer them.
|
|
196
|
+
let signingOpt = null;
|
|
197
|
+
let minter1155Opt = null;
|
|
198
|
+
let forOpt = null;
|
|
199
|
+
try {
|
|
200
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
201
|
+
const bn = await publicClient.getBlockNumber();
|
|
202
|
+
// Collapse the RPC report to one line (best endpoint + head), and only add a ⚠ when there is a
|
|
203
|
+
// genuine problem — a range-capped-only set that will grind a resolver under load.
|
|
204
|
+
const probes = await probeRpcEndpoints({ chainKey: CHAIN });
|
|
205
|
+
const usable = probes.filter((pr) => pr.verdict !== 'unusable');
|
|
206
|
+
const best = probes.find((pr) => pr.verdict === 'best') ?? usable[0];
|
|
207
|
+
if (usable.length > 0) {
|
|
208
|
+
check('RPC', true, `${best.label} · head ${bn} · ${best.verdict === 'best' ? 'wide range + archive' : 'range-capped'}`);
|
|
209
|
+
if (!probes.some((pr) => pr.verdict === 'best')) {
|
|
210
|
+
console.log(`${CONT}${c.orange}⚠${c.reset}${dim(' every endpoint is getLogs-range-capped — add a wide-range archive RPC to ABX_RPC_URLS before running a resolver under load')}`);
|
|
211
|
+
}
|
|
212
|
+
// Name a REACHABLE endpoint that can't serve history — but ONLY when it would actually be asked.
|
|
213
|
+
//
|
|
214
|
+
// The hazard is ordering, not existence: the fallback transport tries endpoints IN ORDER and
|
|
215
|
+
// rotates only on an *error*, so a pruning endpoint listed AHEAD of a good one still answers a
|
|
216
|
+
// deep scan, with an empty and successful `[]`. That is worth a ⚠ every time. A pruning endpoint
|
|
217
|
+
// sitting BEHIND a healthy archive endpoint is never consulted for history and is not a problem
|
|
218
|
+
// the reader can act on — and firing anyway made `doctor` look alarming on a clean setup. Three
|
|
219
|
+
// separate eval rooms flagged the noise; one called it "a poor first impression for an already-
|
|
220
|
+
// nervous creator". `probeRpcEndpoints` preserves `resolveRpcUrls` order, so the index IS the
|
|
221
|
+
// consultation order.
|
|
222
|
+
const bestIdx = probes.findIndex((pr) => pr.verdict === 'best');
|
|
223
|
+
for (const [i, pr] of probes.entries()) {
|
|
224
|
+
if (pr.verdict !== 'unusable' || !pr.reachable)
|
|
225
|
+
continue;
|
|
226
|
+
if (bestIdx !== -1 && i > bestIdx)
|
|
227
|
+
continue; // shadowed by a healthy archive endpoint ahead of it
|
|
228
|
+
console.log(`${CONT}${c.orange}⚠${c.reset}${dim(` ${pr.label} is listed ahead of a full-archive endpoint and can't reconstruct an older project: ${pr.reason ?? 'no archive-depth logs'}. Put a full-archive endpoint FIRST in ${rpcEnvVar(CHAIN)} — the fallback transport rotates on error only, so this one answers a deep scan with an empty, successful []`)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
check('RPC', false, `${CHAIN} — no endpoint usable for reconstruction; add a wide-range archive RPC to ABX_RPC_URLS`);
|
|
233
|
+
}
|
|
234
|
+
const factory = factoryAddress();
|
|
235
|
+
if (factory) {
|
|
236
|
+
const code = await publicClient.getCode({ address: factory });
|
|
237
|
+
if (!code || code === '0x')
|
|
238
|
+
check('factory', false, `${factory} — no code on ${CHAIN}; \`abx deploy\` redeploys`);
|
|
239
|
+
else if (await isCurrentFactory(publicClient, factory))
|
|
240
|
+
check('factory', true, factory);
|
|
241
|
+
else
|
|
242
|
+
check('factory', false, `${factory} — older/incompatible; \`abx deploy\` redeploys`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
check('factory', false, 'none yet — `abx demo` deploys one');
|
|
246
|
+
}
|
|
247
|
+
// The edition twin — same shape as the 721 `factory` check above (isCurrent* probe, same
|
|
248
|
+
// pass/fail posture on "no code yet"). The other two edition factories (EditionImage/
|
|
249
|
+
// EditionCode) and the two edition-related singletons that have no analogous check today
|
|
250
|
+
// either (the 1155 minter, and — same as the 721 minter — no code-version probe to run) are
|
|
251
|
+
// left to their own commands' own bootstrap (`abx deploy-series --copies` / `deploy-code
|
|
252
|
+
// --copies` / `minter configure --token-id`) rather than duplicating a fourth/fifth check here.
|
|
253
|
+
const editionFactory = oneOfOneEditionFactoryAddress();
|
|
254
|
+
if (editionFactory) {
|
|
255
|
+
const code = await publicClient.getCode({ address: editionFactory });
|
|
256
|
+
if (!code || code === '0x')
|
|
257
|
+
check('edition factory', false, `${editionFactory} — no code on ${CHAIN}; \`abx deploy --copies <n|open>\` redeploys`);
|
|
258
|
+
else if (await isCurrentOneOfOneEditionFactory(publicClient, editionFactory))
|
|
259
|
+
check('edition factory', true, editionFactory);
|
|
260
|
+
else
|
|
261
|
+
check('edition factory', false, `${editionFactory} — older/incompatible; \`abx deploy --copies <n|open>\` redeploys`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
check('edition factory', false, 'none yet — `abx deploy --copies <n|open>` deploys one');
|
|
265
|
+
}
|
|
266
|
+
// The shared edition sale singleton — Optional, not pass/fail: unlike the factory (a platform
|
|
267
|
+
// allowlist anchor), a project that never sells through the shared minter has no reason to have
|
|
268
|
+
// one deployed yet (same reason there is no equivalent 721 `fixedPriceMinter` check above).
|
|
269
|
+
const minter1155 = fixedPriceMinter1155Address();
|
|
270
|
+
if (minter1155) {
|
|
271
|
+
const code = await publicClient.getCode({ address: minter1155 });
|
|
272
|
+
minter1155Opt = code && code !== '0x' ? minter1155 : `${minter1155} configured but no code on ${CHAIN} — \`abx minter configure <addr> --token-id <n> …\` redeploys`;
|
|
273
|
+
}
|
|
274
|
+
if (hasKey) {
|
|
275
|
+
const { account } = makeWalletClient({ chainKey: CHAIN });
|
|
276
|
+
signingOpt = `env key ${account.address} · ${await describeBalance(publicClient, account.address)}`;
|
|
277
|
+
await reportNonceCoherence(account.address);
|
|
278
|
+
}
|
|
279
|
+
if (flags.for) {
|
|
280
|
+
forOpt = `${flags.for} · ${await describeBalance(publicClient, flags.for)}`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
check('RPC', false, err.message);
|
|
285
|
+
}
|
|
286
|
+
// Storage backend — resolve it (catches missing config), then probe liveness/creds. The probe
|
|
287
|
+
// is the fuller `probeStorageBackend` (packages/storage/probe.ts), not a bare `.health()` call —
|
|
288
|
+
// for fs/ipfs that's the SAME check as before (reused, not reimplemented); for cloud it's now a
|
|
289
|
+
// real PUT (API) + GET (public base) round trip — the only thing that catches the R2/S3
|
|
290
|
+
// endpoint-vs-public-base trap `.health()` alone couldn't; arweave adds an identity+balance READ
|
|
291
|
+
// (never a paid upload). Kept fast on purpose: only runs once a backend actually resolves, bounded
|
|
292
|
+
// to 1.5s — the same budget `checkForCliUpdate` below uses for its own network check.
|
|
293
|
+
const backendId = activeBackendId();
|
|
294
|
+
try {
|
|
295
|
+
const backend = resolveBackend(storageOptions());
|
|
296
|
+
const h = await probeStorageBackend(storageOptions(), { timeoutMs: 1500 });
|
|
297
|
+
// Whether this backend can name a public URL for what it stores decides one thing operators hit
|
|
298
|
+
// later: a REMOTE effects runner needs it (the resolver serves referenced output by redirect, so
|
|
299
|
+
// it takes a URL and refuses bytes). Co-located rendering doesn't care — hence a note, not a fail.
|
|
300
|
+
const publishable = backend.locator
|
|
301
|
+
? 'can publish to a remote resolver'
|
|
302
|
+
: 'local-only — a remote effects runner needs cloud/ipfs/arweave (co-located rendering is fine)';
|
|
303
|
+
check('storage', h.ok, `${backend.id} · ${h.detail} · ${publishable}`);
|
|
304
|
+
// On failure, name BOTH URLs the cloud check touched — the R2/S3 endpoint-vs-public-base trap is
|
|
305
|
+
// only diagnosable with both hosts on screen, not a bare "fetch failed".
|
|
306
|
+
if (!h.ok && (h.putUrl || h.publicUrl)) {
|
|
307
|
+
console.log(`${CONT}${dim(`put ${h.putUrl ?? '(none)'} · get ${h.publicUrl ?? '(none)'}`)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
check('storage', false, `${backendId} · ${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
// 2b. Version & provenance — the 3-class drift ladder the skill has long taught in prose
|
|
314
|
+
// ("Resolve the CLI before you install anything"), now checked instead of just explained.
|
|
315
|
+
// `checkForCliUpdate` is already bounded to a ~1.5s network timeout + a 6h disk cache (see
|
|
316
|
+
// update-check.ts), so this never adds a meaningful wait — same budget the RPC probe above uses.
|
|
317
|
+
console.log(`\n ${dim('Version & provenance:')}`);
|
|
318
|
+
const provenance = binaryProvenance();
|
|
319
|
+
if (provenance === 'npx') {
|
|
320
|
+
check('binary', false, 'running via npx — can silently keep serving a stale cached copy after an upgrade');
|
|
321
|
+
console.log(`${CONT}${dim('install instead: `npm install --save-dev @artblocks/abx-cli` (project-local) or `-g` (machine-wide)')}`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
// Say WHAT is running, not how the command was invoked.
|
|
325
|
+
check('binary', true, provenance === 'source' ? 'running the source checkout (not an npm install)' : 'npm install');
|
|
326
|
+
}
|
|
327
|
+
const latest = await checkForCliUpdate(cliVersion);
|
|
328
|
+
if (latest)
|
|
329
|
+
check('npm', false, `v${cliVersion} → v${latest} available — npm i -g @artblocks/abx-cli@latest`);
|
|
330
|
+
else
|
|
331
|
+
check('npm', true, `v${cliVersion} (or offline — checked at most every 6h)`);
|
|
332
|
+
if (skillNeedsMigration)
|
|
333
|
+
check('skill ver', false, `legacy skill name remains active — ${skillFix}`);
|
|
334
|
+
else if (skillMissing)
|
|
335
|
+
check('skill ver', false, 'no installed skill to compare — abx skill install');
|
|
336
|
+
else if (skillStale)
|
|
337
|
+
check('skill ver', false, `v${staleSkills.join(', v')} behind CLI v${cliVersion}${staleWhere} — ${skillFix}`);
|
|
338
|
+
else
|
|
339
|
+
check('skill ver', true, `v${cliVersion} matches the CLI`);
|
|
340
|
+
// 3. Optional — path-dependent setup. Unset is fine; these say WHEN you'll need each, so an unset
|
|
341
|
+
// value never reads as a warning.
|
|
342
|
+
console.log(`\n ${dim('Optional — depends how you deploy:')}`);
|
|
343
|
+
if (signingOpt)
|
|
344
|
+
opt('signing', signingOpt);
|
|
345
|
+
else
|
|
346
|
+
opt('signing', 'no env key → sign in your browser wallet (--sign). Preflight yours: `abx doctor --for 0x<addr>`. Set ABX_DEPLOYER_PK for the unattended hot lane.');
|
|
347
|
+
if (forOpt)
|
|
348
|
+
opt('wallet --for', forOpt);
|
|
349
|
+
const baseUrl = process.env.ABX_PUBLIC_BASE_URL;
|
|
350
|
+
if (baseUrl)
|
|
351
|
+
opt('resolver URL', baseUrl);
|
|
352
|
+
else
|
|
353
|
+
opt('resolver URL', 'ABX_PUBLIC_BASE_URL unset → needed for off-chain/resolver-served deploys; skip if fully on-chain (--onchain-image / --onchain-uri).');
|
|
354
|
+
if (minter1155Opt)
|
|
355
|
+
opt('1155 minter', minter1155Opt);
|
|
356
|
+
else
|
|
357
|
+
opt('1155 minter', 'none yet — `abx minter configure <addr> --token-id <n> …` on an edition deploys one');
|
|
358
|
+
if (!process.env.ARWEAVE_JWK && existsSync(arweaveKeyFilePath())) {
|
|
359
|
+
const jwk = loadArweaveJwk();
|
|
360
|
+
opt('arweave key', `${jwk ? arweaveAddress(jwk) + ' ' : ''}holds upload credits — back it up: \`abx storage backup-key --out <path>\``);
|
|
361
|
+
}
|
|
362
|
+
// Named remotes: report what's configured, and — the part doctor was missing — flag a credential
|
|
363
|
+
// stored under a name the CLI does NOT read. That fault presents as "it acts like I never gave it a
|
|
364
|
+
// key", and until now it only surfaced from `abx remote <name>`, which a creator reaches later
|
|
365
|
+
// (doctor is the thing they're told to run FIRST).
|
|
366
|
+
// PROBE each named remote, don't just count it. Reporting "configured" for a credential the
|
|
367
|
+
// service rejects is the same false green as an RPC that answers `[]` because it pruned its logs:
|
|
368
|
+
// presence is not capability, and doctor is the command a creator is told to run FIRST — so the
|
|
369
|
+
// 401 surfaced only later, from `abx remote <name>`, after they had already trusted this line.
|
|
370
|
+
// One cheap authed call per remote, short timeout, and a failure here is never fatal to doctor.
|
|
371
|
+
const remotes = listConfiguredRemotes();
|
|
372
|
+
for (const r of remotes) {
|
|
373
|
+
const name = r.name.toLowerCase();
|
|
374
|
+
if (!r.hasToken) {
|
|
375
|
+
opt('remotes', `${name} ${c.orange}(${describeMissingRemoteToken(name)})${c.reset}`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const verdict = await probeRemoteCredential(r.name);
|
|
379
|
+
if (verdict.ok)
|
|
380
|
+
opt('remotes', `${name} ${dim(verdict.detail)}`);
|
|
381
|
+
else if (verdict.fatal)
|
|
382
|
+
check('remotes', false, `${name} — ${verdict.detail}`);
|
|
383
|
+
else
|
|
384
|
+
opt('remotes', `${name} ${c.orange}${verdict.detail}${c.reset}`);
|
|
385
|
+
}
|
|
386
|
+
for (const bad of misnamedRemoteVars()) {
|
|
387
|
+
check('remote key', false, `${bad.key} is set but is NOT read — the convention is ${bold(bad.suggestion)} (only _URL and _TOKEN). Rename it.`);
|
|
388
|
+
}
|
|
389
|
+
// The self-host default's credential moved to the named-remote grammar (ABX_REMOTE_SELF_TOKEN) —
|
|
390
|
+
// the OLD var is never read now, so flag it here rather than let a creator discover it as a 401
|
|
391
|
+
// that reads like a wrong key. Same condition `requireRemoteToken` hard-stops a real command on.
|
|
392
|
+
const selfTokenMigration = selfTokenMigrationWarning();
|
|
393
|
+
if (selfTokenMigration)
|
|
394
|
+
check('self token', false, selfTokenMigration);
|
|
395
|
+
console.log('');
|
|
396
|
+
}
|
|
397
|
+
// ── mint-page — scaffold a Next.js mint site for a collection (fixed-price minter) ─────────────
|
|
398
|
+
/**
|
|
399
|
+
* Refuse a mint-page target with NO minter lane. The generated page reads
|
|
400
|
+
* `maxInvocations`/`paused` (or, for an edition, the per-id `totalSupply`/`maxSupply`) and mints
|
|
401
|
+
* through a shared fixed-price minter — none of which a plain 1/1 (`OneOfOneImage`) has, since it
|
|
402
|
+
* has no minter lane at all. Before this check, pointing `mint-page` at a 1/1 wrote a page that
|
|
403
|
+
* compiled and ran fine, then sat on "Loading…" forever with no diagnostic.
|
|
404
|
+
*
|
|
405
|
+
* `kind` is the caller's `detectTokenKind` result — every kind EXCEPT `1of1` composes a minter lane
|
|
406
|
+
* (Series/SeriesCode via `AbxFixedPriceMinter`; the three edition twins via its 1155 sibling — see
|
|
407
|
+
* the parity plan's "deliberate asymmetry" note on why even `OneOfOneEdition`, unlike its 721
|
|
408
|
+
* namesake, ships the sale stack). `undefined` means the probe itself couldn't run (RPC unreachable,
|
|
409
|
+
* address has no code, etc.) — that must NOT refuse: offline/best-effort scaffolding is allowed,
|
|
410
|
+
* same as the `name()` read a few lines above this call.
|
|
411
|
+
*/
|
|
412
|
+
export function assertMintableSeries(token, kind) {
|
|
413
|
+
if (kind?.kind === '1of1') {
|
|
414
|
+
throw new CliError(`${token} looks like a 1/1 (OneOfOneImage) — mint-page sells through a shared fixed-price minter, which needs a Series/EditionImage/EditionCode contract (or an edition of one work); a plain 1/1 has no minter lane.\n` +
|
|
415
|
+
` Deploy a Series instead: abx deploy-series --dir <folder> --count 1\n` +
|
|
416
|
+
` …or copies of one work: abx deploy --copies <n|open>`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
export async function cmdMintPage(address, flags) {
|
|
420
|
+
const usage = 'abx mint-page <token> [--dir mint-page] [--name "…"] [--rpc <public-url>] [--minter-contract 0x..]';
|
|
421
|
+
const token = address;
|
|
422
|
+
if (!token || !token.startsWith('0x')) {
|
|
423
|
+
console.error(`usage: ${usage}\n`);
|
|
424
|
+
process.exitCode = 1;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const publicClient = makePublicClient({ chainKey: CHAIN });
|
|
428
|
+
// Kind decides which shared minter (721 `AbxFixedPriceMinter` vs its 1155 `…1155` sibling) and
|
|
429
|
+
// which purchase shape (one project-wide sale vs a per-`(token,id)` sale with a quantity) the
|
|
430
|
+
// generated page needs. A probe failure (RPC unreachable, no code at the address) is left
|
|
431
|
+
// `undefined` and does NOT refuse — matches the best-effort name() read below.
|
|
432
|
+
let kind;
|
|
433
|
+
try {
|
|
434
|
+
kind = await detectTokenKind(publicClient, token);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
warn(`could not confirm ${token}'s kind (RPC unreachable?) — continuing without the check.`);
|
|
438
|
+
}
|
|
439
|
+
assertMintableSeries(token, kind);
|
|
440
|
+
const isEdition = !!kind?.isEdition;
|
|
441
|
+
const minter = isEdition ? fixedPriceMinter1155Address(flags['minter-contract']) : fixedPriceMinterAddress(flags['minter-contract']);
|
|
442
|
+
if (!minter) {
|
|
443
|
+
throw new Error(`no shared ${isEdition ? 'edition ' : ''}fixed-price minter known for ${CHAIN} — run ` +
|
|
444
|
+
`\`abx minter configure ${token}${isEdition ? ' --token-id <n>' : ''} …\` first (it deploys + records one), ` +
|
|
445
|
+
`or set ${isEdition ? 'ABX_FIXED_PRICE_MINTER_1155' : 'ABX_FIXED_PRICE_MINTER'}.`);
|
|
446
|
+
}
|
|
447
|
+
const chain = resolveChain(CHAIN);
|
|
448
|
+
const rpcUrl = flags.rpc && flags.rpc !== 'true' ? flags.rpc : PUBLIC_RPC[chain.id];
|
|
449
|
+
if (!rpcUrl) {
|
|
450
|
+
throw new Error(`no public RPC known for ${CHAIN} (chainId ${chain.id}) — pass --rpc <public-keyless-url> (it is embedded in the public site, so it must not contain a secret key).`);
|
|
451
|
+
}
|
|
452
|
+
// Default the display name from the token's on-chain name (read-only); --name overrides.
|
|
453
|
+
let collectionName = flags.name && flags.name !== 'true' ? flags.name : '';
|
|
454
|
+
if (!collectionName) {
|
|
455
|
+
try {
|
|
456
|
+
collectionName = (await publicClient.readContract({
|
|
457
|
+
address: token,
|
|
458
|
+
abi: oneOfOneImageAbi,
|
|
459
|
+
functionName: 'name',
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
collectionName = 'ABX Collection';
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const art = mintPageArtifact({
|
|
467
|
+
token,
|
|
468
|
+
minter,
|
|
469
|
+
chainId: chain.id,
|
|
470
|
+
chainName: chain.name,
|
|
471
|
+
rpcUrl,
|
|
472
|
+
collectionName,
|
|
473
|
+
explorer: chain.blockExplorers?.default?.url,
|
|
474
|
+
isEdition,
|
|
475
|
+
});
|
|
476
|
+
const dir = flags.dir && flags.dir !== 'true' ? flags.dir : 'mint-page';
|
|
477
|
+
const outDir = resolvePath(dir);
|
|
478
|
+
console.log(bold('\n ABX Self-Host Toolkit — mint page'));
|
|
479
|
+
info(`collection ${bold(collectionName)} ${dim(token)}${isEdition ? dim(` · ${kind.label} (edition)`) : ''}`);
|
|
480
|
+
info(`minter ${minter} ${dim('· ' + chain.name + ' · reads via ' + rpcUrl)}`);
|
|
481
|
+
if (isEdition)
|
|
482
|
+
info(dim('edition purchase shape: token id + quantity, price × qty — no minted-tokens gallery in this v1 page (see the README).'));
|
|
483
|
+
step('Write the mint-page artifact (self-contained Next.js app — reads chain directly, no backend)');
|
|
484
|
+
for (const f of art.files) {
|
|
485
|
+
const dest = joinPath(outDir, f.path);
|
|
486
|
+
mkdirSync(joinPath(dest, '..'), { recursive: true });
|
|
487
|
+
writeFileSync(dest, f.content);
|
|
488
|
+
info(`wrote ${joinPath(dir, f.path)}`);
|
|
489
|
+
}
|
|
490
|
+
step(`Next steps (run from ${bold(dir)}/)`);
|
|
491
|
+
art.steps.forEach((s, i) => console.log(` ${g(`${i + 1}.`)} ${s}`));
|
|
492
|
+
step('Customize — this is a starting point');
|
|
493
|
+
info('Plain React + one CSS file; ask the agent for a bespoke layout per drop. ETH sales + injected wallet by default;');
|
|
494
|
+
info('the README covers RainbowKit (multi-wallet), ERC-20 (add an approve step), and RPC/gallery notes.');
|
|
495
|
+
warn('The RPC in .env.local is PUBLIC (may rate-limit). Never put a secret-keyed RPC in a NEXT_PUBLIC_ var — it ships to the browser.');
|
|
496
|
+
console.log('');
|
|
497
|
+
}
|
|
498
|
+
// ── flags ─────────────────────────────────────────────────────────────────--
|
|
499
|
+
// parseFlags + the Flags type live in flags.ts (unit-testable; repeatable flags like --dep
|
|
500
|
+
// accumulate comma-joined there).
|
|
501
|
+
// ── skill: install the abx agent skill into the user's coding agent ───────────────────────
|
|
502
|
+
// The skill is the agentic half of the toolkit — the CLI is only useful once an agent knows how
|
|
503
|
+
// to drive it. It ships bundled inside the published package (packages/cli/skill/, created at
|
|
504
|
+
// prepack); in dev the same command reads the canonical copy straight from the repo. This is the
|
|
505
|
+
// CANONICAL install path precisely because the bundle is version-locked to the CLI it ships inside
|
|
506
|
+
// (the version lives in SKILL.md frontmatter — see update-check.ts). By default it writes to BOTH
|
|
507
|
+
// `.claude/skills` (Claude Code) and the neutral `.agents/skills` (Cursor · Codex · Gemini ·
|
|
508
|
+
// Copilot all read it), so one command covers the whole ecosystem; `--agent` narrows it. A separate
|
|
509
|
+
// git-based channel, `npx skills add ArtBlocks/abx --skill abx`, is not version-locked to a local
|
|
510
|
+
// CLI — prefer `abx skill install`.
|
|
511
|
+
/** Locate the skill folder: the bundled copy beside the compiled CLI (published), else the
|
|
512
|
+
* canonical repo copy (dev). Returns null if neither is present. */
|
|
513
|
+
export function resolveBundledSkill() {
|
|
514
|
+
const here = fileURLToPath(import.meta.url);
|
|
515
|
+
// `findCliPackageRoot()`, never a counted `..`, so this works from a published install.
|
|
516
|
+
const pkgDir = findCliPackageRoot();
|
|
517
|
+
const bundled = pkgDir ? joinPath(pkgDir, 'skill') : null;
|
|
518
|
+
// Running from SOURCE (`pnpm abx`, tsx on packages/cli/src/**) means we're in the repo working
|
|
519
|
+
// tree, where the canonical skill is the source of truth and `<pkg>/skill` is gitignored PREPACK
|
|
520
|
+
// OUTPUT that may be arbitrarily old. Preferring the bundle there is actively destructive: a
|
|
521
|
+
// leftover `skill/` from a months-old `npm pack` silently overwrote the canonical skill with a
|
|
522
|
+
// copy 8 versions behind (and the drift check then reported the damage as if the user had caused
|
|
523
|
+
// it). In the published layout there is no repo and no canonical copy, so the bundle is correct.
|
|
524
|
+
const root = findRepoRoot();
|
|
525
|
+
const canonical = root ? joinPath(root, '.claude', 'skills', SKILL_DIR_NAME) : null;
|
|
526
|
+
// "Running from source" = this module IS the repo's own CLI source, checked by containment rather
|
|
527
|
+
// than by pattern. Two earlier attempts at this were wrong in opposite directions:
|
|
528
|
+
// - `/[/\\]src[/\\][^/\\]+$/` required the module to sit DIRECTLY in `src/`, so it read false
|
|
529
|
+
// for everything under `src/commands/` — including this file. Combined with the package-root fix
|
|
530
|
+
// above, that would make a dev run prefer `<pkg>/skill`, the gitignored prepack output, over the
|
|
531
|
+
// canonical skill: exactly the "actively destructive" case the note below describes.
|
|
532
|
+
// - matching a bare `/src/` anywhere would read TRUE for any user whose project happens to live
|
|
533
|
+
// under a `src` directory (`~/src/my-drop/node_modules/@artblocks/abx-cli/...`).
|
|
534
|
+
// Containment under the resolved repo's `packages/cli/src` is neither: it is exact, and it is
|
|
535
|
+
// independent of how deep in `src/` this file sits.
|
|
536
|
+
const cliSrc = root ? joinPath(root, 'packages', 'cli', 'src') + sep : null;
|
|
537
|
+
const fromSource = !!cliSrc && here.startsWith(cliSrc);
|
|
538
|
+
if (fromSource && canonical && existsSync(joinPath(canonical, 'SKILL.md')))
|
|
539
|
+
return canonical;
|
|
540
|
+
// Published layout: <pkg>/skill, written by scripts/bundle-skill.mjs at prepack.
|
|
541
|
+
if (bundled && existsSync(joinPath(bundled, 'SKILL.md')))
|
|
542
|
+
return bundled;
|
|
543
|
+
if (canonical && existsSync(joinPath(canonical, 'SKILL.md')))
|
|
544
|
+
return canonical;
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
/** Locate the in-chain renderer Foundry scaffold: bundled beside the CLI (published), else the
|
|
548
|
+
* repo copy (dev). Same layout in both — it lives under the CLI package's `assets/`. */
|
|
549
|
+
export function resolveSolidityScaffold() {
|
|
550
|
+
// Same fix as {resolveBundledSkill}: this had the identical counted-`..` bug, so
|
|
551
|
+
// `abx scaffold-renderer` was broken on every published install too — unreported, found while
|
|
552
|
+
// fixing the skill path.
|
|
553
|
+
const pkgDir = findCliPackageRoot();
|
|
554
|
+
const bundled = pkgDir ? joinPath(pkgDir, 'assets', 'renderer-scaffold') : null;
|
|
555
|
+
if (bundled && existsSync(joinPath(bundled, 'foundry.toml')))
|
|
556
|
+
return bundled;
|
|
557
|
+
const root = findRepoRoot();
|
|
558
|
+
if (root) {
|
|
559
|
+
const canonical = joinPath(root, 'packages', 'cli', 'assets', 'renderer-scaffold');
|
|
560
|
+
if (existsSync(joinPath(canonical, 'foundry.toml')))
|
|
561
|
+
return canonical;
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
/** `abx scaffold-renderer [<dir>]` — write a ready-to-build Foundry project for the in-chain
|
|
566
|
+
* Solidity renderer lane (a seed + PostParam → SVG renderer + coherent traits + tests + a deploy
|
|
567
|
+
* script). abx doesn't compile/deploy Solidity — the creator runs forge, then hands the deployed
|
|
568
|
+
* address to `deploy-code --image-renderer`. Mirrors how deploy-resolver scaffolds an artifact. */
|
|
569
|
+
export function cmdScaffoldRenderer(rest, flags) {
|
|
570
|
+
const positional = positionalArgs(rest);
|
|
571
|
+
return cmdScaffoldSolidity(positional.length ? positional : ['my-renderer'], flags);
|
|
572
|
+
}
|
|
573
|
+
/** `abx scaffold solidity [<dir>]` — one Foundry workspace for ABX's actual Solidity extension
|
|
574
|
+
* surfaces. Contracts stay role-separated: renderers, configure/transfer/augment hooks, and the
|
|
575
|
+
* canonical 721/1155 minters from `abx-contracts`; there is no invented "minter hook" role. */
|
|
576
|
+
export function cmdScaffoldSolidity(rest, flags) {
|
|
577
|
+
const dir = resolvePath(rest[0] ?? 'my-abx-solidity');
|
|
578
|
+
const src = resolveSolidityScaffold();
|
|
579
|
+
if (!src) {
|
|
580
|
+
throw new Error('bundled renderer scaffold not found (expected <pkg>/assets/renderer-scaffold). Reinstall @artblocks/abx-cli.');
|
|
581
|
+
}
|
|
582
|
+
if (existsSync(dir) && readdirSync(dir).length > 0 && flags.force === undefined) {
|
|
583
|
+
throw new Error(`${dir} already exists and is not empty — pass a fresh path, or --force to write into it.`);
|
|
584
|
+
}
|
|
585
|
+
// Copy + assert it landed. Throws rather than printing a success banner over an empty directory —
|
|
586
|
+
// the alpha.9→alpha.14 failure mode. See src/scaffold.ts for why this is not inline.
|
|
587
|
+
copyScaffold(src, dir, SCAFFOLD_ANCHOR);
|
|
588
|
+
ok(`ABX Solidity workspace → ${dir}`);
|
|
589
|
+
step('One workspace, separate deployable roles — use only the extension surfaces your project needs');
|
|
590
|
+
info(`${bold('src/MyRenderer.sol')} + ${bold('MyTraits.sol')} cover on-chain rendering; ${bold('src/MyHooks.sol')} covers configure, transfer, and augment hooks as separate contracts. Canonical 721/1155 fixed-price minters and interfaces come from the pinned ${bold('abx-contracts')} package.`);
|
|
591
|
+
info('build + test + deploy it yourself with Foundry (abx does not compile/deploy Solidity):');
|
|
592
|
+
console.log(` ${g('cd')} ${basename(dir)}`);
|
|
593
|
+
console.log(` ${g('forge soldeer install')} ${dim('# fetch abx-contracts + solady + forge-std (exact-pinned)')}`);
|
|
594
|
+
console.log(` ${g('forge test')} ${dim('# MUST pass — the never-revert cases')}`);
|
|
595
|
+
// `forge test` asserts; it never shows you the art. Preview writes the renderer's actual bytes to
|
|
596
|
+
// preview-out/ so you can open them — the one step between "my tests pass" and "this looks right",
|
|
597
|
+
// and the reason not to discover a broken SVG only after spending testnet gas. Local-EVM only: it
|
|
598
|
+
// proves output, never production gas.
|
|
599
|
+
console.log(` ${g('forge script script/Preview.s.sol')} ${dim('# LOOK at it — writes preview-out/<field>.svg|json')}`);
|
|
600
|
+
console.log(` ${g('forge script script/Deploy.s.sol --rpc-url <rpc> --private-key <key> --broadcast')}`);
|
|
601
|
+
info('then wire only the roles you chose:');
|
|
602
|
+
console.log(` ${g('abx deploy-code --image-renderer <MyRenderer> --attributes-renderer <MyTraits> --onchain-uri --schema palette:HexColor:TokenOwner --name "…" --symbol …')}`);
|
|
603
|
+
console.log(` ${g('abx set-param-hooks <token> --configure <addr> --augment <addr> --transfer <addr>')}`);
|
|
604
|
+
info(`full walkthrough: ${bold(`${basename(dir)}/README.md`)} · extension contracts remain separate so authority is visible and independently omittable.`);
|
|
605
|
+
}
|
|
606
|
+
/** @deprecated Public compatibility alias for the renderer-only command from earlier CLI releases. */
|
|
607
|
+
export const resolveRendererScaffold = resolveSolidityScaffold;
|
|
608
|
+
/** Human label for each skills-parent dir — which agents pick the skill up from there. */
|
|
609
|
+
export const SKILL_PARENT_LABELS = {
|
|
610
|
+
'.claude/skills': 'Claude Code',
|
|
611
|
+
'.agents/skills': 'Cursor · Codex · Gemini · Copilot',
|
|
612
|
+
};
|
|
613
|
+
/** Resolve which skills-parent dirs `install` should write to. No `--agent` → the whole-ecosystem
|
|
614
|
+
* default (Claude Code + the neutral `.agents/skills` everyone else reads). `--agent a,b` narrows
|
|
615
|
+
* to those agents' dirs (de-duped, since several share `.agents/skills`). */
|
|
616
|
+
export function resolveInstallParents(agentFlag) {
|
|
617
|
+
if (!agentFlag || agentFlag === 'true')
|
|
618
|
+
return ['.claude/skills', '.agents/skills'];
|
|
619
|
+
const parents = new Set();
|
|
620
|
+
for (const raw of agentFlag.split(',')) {
|
|
621
|
+
const a = raw.trim().toLowerCase();
|
|
622
|
+
const parent = AGENT_SKILL_PARENTS[a];
|
|
623
|
+
if (!parent) {
|
|
624
|
+
throw new Error(`unknown --agent '${a}'. Use one of: ${Object.keys(AGENT_SKILL_PARENTS).join(', ')} (or omit --agent to install for every agent).`);
|
|
625
|
+
}
|
|
626
|
+
parents.add(parent);
|
|
627
|
+
}
|
|
628
|
+
return [...parents];
|
|
629
|
+
}
|
|
630
|
+
/** Copy the bundled skill folder to `dest`, replacing any prior copy so a re-install after an
|
|
631
|
+
* upgrade never leaves stale reference files behind.
|
|
632
|
+
*
|
|
633
|
+
* Returns `'already-canonical'` when destination IS the source, having done nothing. That case is
|
|
634
|
+
* not hypothetical: in a dev checkout of the abx repo `resolveBundledSkill()` resolves to the
|
|
635
|
+
* canonical `.claude/skills/abx`, which is exactly where a cwd-relative install writes —
|
|
636
|
+
* so the `rmSync` below would delete the canonical skill, and the copy would then have no source.
|
|
637
|
+
* A self-destructing install is a bad way to learn that. */
|
|
638
|
+
export function installSkillTo(src, dest) {
|
|
639
|
+
if (resolvePath(src) === resolvePath(dest))
|
|
640
|
+
return 'already-canonical';
|
|
641
|
+
mkdirSync(joinPath(dest, '..'), { recursive: true });
|
|
642
|
+
rmSync(dest, { recursive: true, force: true });
|
|
643
|
+
cpSync(src, dest, { recursive: true });
|
|
644
|
+
return 'installed';
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Move an old `abx-self-host` skill out of the discovery tree before installing `abx`.
|
|
648
|
+
*
|
|
649
|
+
* Never delete it: users may have customized the copy. A backup outside every `skills/` parent
|
|
650
|
+
* prevents two overlapping skills from triggering while keeping the old bytes recoverable.
|
|
651
|
+
* Return null when the path is absent or does not actually declare the legacy skill name.
|
|
652
|
+
*/
|
|
653
|
+
export function archiveLegacySkillDir(legacyDest, backupRoot) {
|
|
654
|
+
if (!existsSync(legacyDest))
|
|
655
|
+
return null;
|
|
656
|
+
const legacyName = readSkillName(joinPath(legacyDest, 'SKILL.md'));
|
|
657
|
+
if (!legacyName || !LEGACY_SKILL_DIR_NAMES.includes(legacyName))
|
|
658
|
+
return null;
|
|
659
|
+
const version = readSkillVersion(joinPath(legacyDest, 'SKILL.md')) ?? 'unversioned';
|
|
660
|
+
mkdirSync(backupRoot, { recursive: true });
|
|
661
|
+
const stem = `${legacyName}-${version}`;
|
|
662
|
+
let backup = joinPath(backupRoot, stem);
|
|
663
|
+
for (let n = 2; existsSync(backup); n += 1)
|
|
664
|
+
backup = joinPath(backupRoot, `${stem}-${n}`);
|
|
665
|
+
renameSync(legacyDest, backup);
|
|
666
|
+
return backup;
|
|
667
|
+
}
|
|
668
|
+
function archiveLegacySkillsIn(parentDir, backupRoot) {
|
|
669
|
+
for (const legacyName of LEGACY_SKILL_DIR_NAMES) {
|
|
670
|
+
const legacyDest = joinPath(parentDir, legacyName);
|
|
671
|
+
const existed = existsSync(legacyDest);
|
|
672
|
+
const backup = archiveLegacySkillDir(legacyDest, backupRoot);
|
|
673
|
+
if (backup)
|
|
674
|
+
info(`archived legacy ${legacyDest} → ${backup}`);
|
|
675
|
+
else if (existed)
|
|
676
|
+
warn(`left ${legacyDest} in place: its SKILL.md does not declare a recognized legacy ABX skill name; review it manually to avoid duplicate triggers.`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Install the bundled skill into the default per-agent parents (or `~` with `global`), reporting
|
|
681
|
+
* each destination. Shared by `abx skill install` and `abx doctor`'s offer to fix a missing/stale
|
|
682
|
+
* skill — one implementation, so the two can't drift on where the skill lands or what it prints.
|
|
683
|
+
*/
|
|
684
|
+
export function installSkillToDefaults(src, opts = {}) {
|
|
685
|
+
const version = readSkillVersion(joinPath(src, 'SKILL.md')) ?? readCliVersion();
|
|
686
|
+
const base = opts.global ? homedir() : process.cwd();
|
|
687
|
+
const parents = resolveInstallParents(opts.agent);
|
|
688
|
+
ok(`installed the abx skill v${version}${opts.global ? ' (global, ~)' : ''}:`);
|
|
689
|
+
for (const parent of parents) {
|
|
690
|
+
archiveLegacySkillsIn(joinPath(base, parent), joinPath(base, '.abx-skill-backups', parent.replace(/[^a-z0-9]+/gi, '-')));
|
|
691
|
+
const dest = joinPath(base, parent, SKILL_DIR_NAME);
|
|
692
|
+
const outcome = installSkillTo(src, dest);
|
|
693
|
+
const label = SKILL_PARENT_LABELS[parent];
|
|
694
|
+
const note = outcome === 'already-canonical' ? dim(' (already the canonical copy — left as is)') : label ? dim(' → ' + label) : '';
|
|
695
|
+
console.log(` ${g(joinPath(parent, SKILL_DIR_NAME))}${note}`);
|
|
696
|
+
}
|
|
697
|
+
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
698
|
+
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.`);
|
|
699
|
+
return version;
|
|
700
|
+
}
|
|
701
|
+
/** The default skill destinations, as a display string — what doctor's prompt has to name up front. */
|
|
702
|
+
export function defaultSkillDests(opts = {}) {
|
|
703
|
+
const prefix = opts.global ? '~/' : './';
|
|
704
|
+
return resolveInstallParents(opts.agent).map((p) => `${prefix}${p}`).join(' and ');
|
|
705
|
+
}
|
|
706
|
+
export async function cmdSkill(rest, flags) {
|
|
707
|
+
const sub = rest[0] ?? 'install';
|
|
708
|
+
const src = resolveBundledSkill();
|
|
709
|
+
if (!src) {
|
|
710
|
+
throw new Error(`bundled skill not found (expected <pkg>/skill or .claude/skills/${SKILL_DIR_NAME}). ` +
|
|
711
|
+
'Reinstall @artblocks/abx-cli, or install cross-agent with: npx skills add ArtBlocks/abx --skill abx');
|
|
712
|
+
}
|
|
713
|
+
if (sub === 'path') {
|
|
714
|
+
console.log(src);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (sub === 'install') {
|
|
718
|
+
const version = readSkillVersion(joinPath(src, 'SKILL.md')) ?? readCliVersion();
|
|
719
|
+
// Escape hatch: --target <dir> writes the skill folder straight under <dir> (for an agent
|
|
720
|
+
// whose skills dir we don't special-case, or a bespoke location).
|
|
721
|
+
if (typeof flags.target === 'string' && flags.target !== 'true') {
|
|
722
|
+
const target = resolvePath(flags.target);
|
|
723
|
+
archiveLegacySkillsIn(target, joinPath(target, '.abx-skill-backups'));
|
|
724
|
+
const dest = joinPath(target, SKILL_DIR_NAME);
|
|
725
|
+
if (installSkillTo(src, dest) === 'already-canonical') {
|
|
726
|
+
ok(`${dest} is already the canonical skill v${version} — nothing to install.`);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
ok(`installed the abx skill v${version} → ${dest}`);
|
|
730
|
+
info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
installSkillToDefaults(src, { global: flags.global !== undefined, agent: flags.agent });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
throw new Error('usage: abx skill <install|path> [--agent claude|cursor|codex|gemini|copilot] [--global] [--target <dir>]');
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=scaffold.js.map
|