@artblocks/abx-cli 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -34,13 +34,14 @@
34
34
  * abx storage backup-key copy the managed Turbo/Arweave key (holds credits) to a safe path
35
35
  * abx status list indexed projects + node info
36
36
  * abx doctor check environment (key, RPC, balance, factory, storage)
37
- * abx skill install install the abx agent skill into your agent (.claude/skills; --global)
37
+ * abx skill install install the version-locked abx agent skill into your agent(s)
38
+ * (default: Claude Code + the neutral .agents/skills; --agent/--global)
38
39
  *
39
40
  * Every write picks a signing lane: default hot (env key signs), `--sign` (a human
40
41
  * approves in their own wallet via a one-shot localhost page), `--unsigned` (print
41
42
  * the tx for a multisig / offline signer). The agent picks the lane; the CLI signs.
42
43
  */
43
- import { appendFileSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
44
+ import { appendFileSync, copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
44
45
  import { randomBytes } from 'node:crypto';
45
46
  import { basename, extname, join as joinPath, resolve as resolvePath } from 'node:path';
46
47
  import { fileURLToPath } from 'node:url';
@@ -54,7 +55,7 @@ import { DEP_RESOLUTION, deploySeedSource, deploySeriesCodeFactory, prepareCodeS
54
55
  import { checkRegistryDeps, dependencySetupCalls, parseDepFlag, resolveDepRegistryPointer } from './deps.js';
55
56
  import { composeParamsKeys, expectedChainComplete, hasOnChainUriLane, onchainUriSetupCalls, onChainUriReport } from './onchain-uri.js';
56
57
  import { parseFlags, unknownFlags } from './flags.js';
57
- import { checkForCliUpdate, compareVersions, installedSkillVersions, readCliVersion, SKILL_VERSION_MARKER } from './update-check.js';
58
+ import { AGENT_SKILL_PARENTS, checkForCliUpdate, compareVersions, installedSkillVersions, readCliVersion, readSkillVersion, SKILL_DIR_NAME, } from './update-check.js';
58
59
  import { analyzeScript, recommendLane } from './inspect.js';
59
60
  import { parseSchemaSpecs, describeSchema } from './schema.js';
60
61
  import { parseSeriesTraits, looksPerTokenAttributes, parseSeriesTraitsById } from './series-traits.js';
@@ -133,7 +134,7 @@ async function maybeNotifyUpdate(flags) {
133
134
  const latest = await checkForCliUpdate(current);
134
135
  if (latest) {
135
136
  console.error(`\n ${c.orange}⚠${c.reset} update available: ${bold('abx')} ${dim(current)} → ${g(latest)}\n` +
136
- ` upgrade: ${g('npm i -g @artblocks/abx-cli@latest')} ${dim('· or invoke:')} ${g('npx abx@latest <command>')}\n` +
137
+ ` upgrade: ${g('npm i -g @artblocks/abx-cli@latest')} ${dim('· or invoke:')} ${g('npx @artblocks/abx-cli@latest <command>')}\n` +
137
138
  ` release notes: https://github.com/ArtBlocks/abx/releases ${dim('· silence: ABX_NO_UPDATE_CHECK=1')}\n`);
138
139
  }
139
140
  }
@@ -4166,99 +4167,109 @@ async function cmdMigrate(address, flags) {
4166
4167
  }
4167
4168
  // ── doctor ────────────────────────────────────────────────────────────────--
4168
4169
  async function cmdDoctor(flags) {
4169
- console.log(bold('\n abx doctor\n'));
4170
- const check = (label, pass, detail = '') => console.log(` ${pass ? g('✓') : `${c.red}✗${c.reset}`} ${label}${detail ? dim(' ' + detail) : ''}`);
4170
+ console.log(bold('\n abx doctor') + dim(` · ${CHAIN}`) + '\n');
4171
+ // Two visual tiers: PASS/FAIL checks (✓/✗) for things that are either working or broken, and an
4172
+ // "Optional" block (·) for path-dependent setup that is fine to be unset. We deliberately do NOT
4173
+ // use ⚠ for "unset but often fine" — that read as noise; ⚠ is reserved for a real gotcha (a
4174
+ // range-capped RPC). Labels are padded so both tiers align.
4175
+ const CONT = ' '.repeat(17); // continuation indent: aligns under a check/opt detail column
4176
+ const check = (label, pass, detail = '') => console.log(` ${pass ? g('✓') : `${c.red}✗${c.reset}`} ${label.padEnd(13)}${detail ? dim(detail) : ''}`);
4177
+ const opt = (label, detail) => console.log(` ${dim('·')} ${label.padEnd(13)}${dim(detail)}`);
4171
4178
  const hasKey = !!(process.env.ABX_DEPLOYER_PK ?? process.env.SEPOLIA_FUNDED_PK ?? process.env.SEPOLIA_WALLET_PK);
4172
- // Not having an env key is NOT fatal: the wallet lane (`--sign`) signs in the user's own wallet,
4173
- // key-free. Only flag it as a soft note + point at the lanes never imply "paste a private key."
4174
- if (hasKey)
4175
- check('signing key in env', true, 'hot lane ready (env key signs unattended)');
4176
- else
4177
- console.log(` ${c.orange}⚠${c.reset} signing key in env${dim(' none fine if you sign in your own wallet (`--sign`; the key never touches the CLI). Only set ABX_DEPLOYER_PK / SEPOLIA_FUNDED_PK / SEPOLIA_WALLET_PK for hot/unattended signing.')}`);
4179
+ // 1. Agent skill FIRST and prominent. The primary way to use abx is to let a coding agent drive
4180
+ // it, so a missing/stale skill is a ✗: not broken infra, but the main UX isn't set up. Its
4181
+ // version lives in SKILL.md frontmatter (version-locked to this CLI). Notify-only — no exit code.
4182
+ const cliVersion = readCliVersion();
4183
+ const skillVersions = installedSkillVersions();
4184
+ const staleSkills = skillVersions.filter((v) => compareVersions(cliVersion, v) > 0);
4185
+ if (skillVersions.length === 0) {
4186
+ check('agent skill', false, `not installed — run ${g('abx skill install')}`);
4187
+ console.log(`${CONT}${dim('(recommended: let a coding agent drive abx)')}`);
4188
+ }
4189
+ else if (staleSkills.length > 0) {
4190
+ check('agent skill', false, `v${staleSkills.join(', v')} behind CLI v${cliVersion} — run ${g('abx skill install')}`);
4191
+ }
4192
+ else {
4193
+ check('agent skill', true, `in sync (v${cliVersion})`);
4194
+ }
4195
+ console.log('');
4196
+ // 2. Core environment (✓/✗). Signing-wallet balances are computed here (they need the RPC) but
4197
+ // printed in the Optional block below, so buffer them.
4198
+ let signingOpt = null;
4199
+ let forOpt = null;
4178
4200
  try {
4179
4201
  const publicClient = makePublicClient({ chainKey: CHAIN });
4180
4202
  const bn = await publicClient.getBlockNumber();
4181
- check(`RPC reachable (${CHAIN})`, true, `head block ${bn}`);
4182
- // RPC endpointsprobe each for reachability, getLogs range, and archive depth,
4183
- // so the toolkit uses (and recommends) the one fit for the job — and says so if none are.
4203
+ // Collapse the RPC report to one line (best endpoint + head), and only add a ⚠ when there is a
4204
+ // genuine problema range-capped-only set that will grind a resolver under load.
4184
4205
  const probes = await probeRpcEndpoints({ chainKey: CHAIN });
4185
4206
  const usable = probes.filter((pr) => pr.verdict !== 'unusable');
4186
4207
  const best = probes.find((pr) => pr.verdict === 'best') ?? usable[0];
4187
- check(`RPC endpoints for ${CHAIN} (${probes.length})`, usable.length > 0, usable.length > 0
4188
- ? `best for reconstruction: ${best.label} (${best.verdict === 'best' ? 'wide range + archive' : 'usable, range-capped'})`
4189
- : 'none usable for reconstruction');
4190
- for (const pr of probes) {
4191
- const glyph = pr.verdict === 'best' ? g('✓') : pr.verdict === 'capped' ? `${c.orange}⚠${c.reset}` : `${c.red}✗${c.reset}`;
4192
- console.log(` ${glyph} ${pr.label} ${dim('— ' + (pr.verdict === 'best' ? 'wide getLogs range + archive' : pr.reason ?? pr.verdict))}`);
4193
- }
4194
- if (usable.length === 0) {
4195
- console.log(` ${c.orange}↳${c.reset} ${dim('research a current free archive RPC with a wide getLogs range and add it to ABX_RPC_URLS — see the skill’s “Choosing an RPC”')}`);
4196
- }
4197
- else if (!probes.some((pr) => pr.verdict === 'best')) {
4198
- // Every usable endpoint is range-capped. Indexing still works (chunking), but a first
4199
- // reconstruction — and a resolver under marketplace load — is slow + rate-limit-prone. That's
4200
- // a serious infra signal, NOT "a paid plan is required": flag it. A normal deploy→index only
4201
- // scans from the deploy block, so this bites first reconstructions and busy resolvers most.
4202
- console.log(` ${c.orange}⚠ every usable RPC is getLogs-range-capped${c.reset} ${dim('— add a wide-range archive endpoint to ABX_RPC_URLS before running a resolver under load; capped ones grind on wide scans (skill → “Choosing an RPC”).')}`);
4203
- }
4204
- if (hasKey) {
4205
- const { account } = makeWalletClient({ chainKey: CHAIN });
4206
- const bal = await publicClient.getBalance({ address: account.address });
4207
- check('deployer funded', bal > 0n, `${account.address} · ${formatEther(bal)} ETH`);
4208
- }
4209
- // Wallet lane has no env key — let a creator preflight THEIR OWN signing wallet's balance,
4210
- // the one gap where an unfunded wallet otherwise only surfaces at the signing step.
4211
- if (flags.for) {
4212
- const bal = await publicClient.getBalance({ address: flags.for });
4213
- check('wallet funded (--for)', bal > 0n, `${flags.for} · ${formatEther(bal)} ETH${bal > 0n ? '' : ` — ${faucetHint(CHAIN)}`}`);
4208
+ if (usable.length > 0) {
4209
+ check('RPC', true, `${best.label} · head ${bn} · ${best.verdict === 'best' ? 'wide range + archive' : 'range-capped'}`);
4210
+ if (!probes.some((pr) => pr.verdict === 'best')) {
4211
+ 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')}`);
4212
+ }
4214
4213
  }
4215
- else if (!hasKey) {
4216
- console.log(` ${dim(' wallet lane: run `abx doctor --for 0x<your wallet>` to check your signing wallet is funded')}`);
4214
+ else {
4215
+ check('RPC', false, `${CHAIN} no endpoint usable for reconstruction; add a wide-range archive RPC to ABX_RPC_URLS`);
4217
4216
  }
4218
4217
  const factory = factoryAddress();
4219
4218
  if (factory) {
4220
4219
  const code = await publicClient.getCode({ address: factory });
4221
- if (!code || code === '0x') {
4222
- check('canonical factory deployed', false, `${factory} — no code on ${CHAIN}; \`abx deploy\` redeploys`);
4223
- }
4224
- else if (await isCurrentFactory(publicClient, factory)) {
4225
- check('canonical factory deployed', true, factory);
4226
- }
4227
- else {
4228
- check('canonical factory deployed', false, `${factory} — older/incompatible version; \`abx deploy\` redeploys`);
4229
- }
4220
+ if (!code || code === '0x')
4221
+ check('factory', false, `${factory} — no code on ${CHAIN}; \`abx deploy\` redeploys`);
4222
+ else if (await isCurrentFactory(publicClient, factory))
4223
+ check('factory', true, factory);
4224
+ else
4225
+ check('factory', false, `${factory} — older/incompatible; \`abx deploy\` redeploys`);
4230
4226
  }
4231
4227
  else {
4232
- check('canonical factory deployed', false, 'none yet — `abx demo` deploys one');
4228
+ check('factory', false, 'none yet — `abx demo` deploys one');
4229
+ }
4230
+ if (hasKey) {
4231
+ const { account } = makeWalletClient({ chainKey: CHAIN });
4232
+ const bal = await publicClient.getBalance({ address: account.address });
4233
+ signingOpt = `env key ${account.address} · ${bal > 0n ? `funded ${formatEther(bal)} ETH` : `empty — fund it (${faucetHint(CHAIN)})`}`;
4234
+ }
4235
+ if (flags.for) {
4236
+ const bal = await publicClient.getBalance({ address: flags.for });
4237
+ forOpt = `${flags.for} · ${bal > 0n ? `funded ${formatEther(bal)} ETH` : `empty — ${faucetHint(CHAIN)}`}`;
4233
4238
  }
4234
4239
  }
4235
4240
  catch (err) {
4236
- check('RPC reachable', false, err.message);
4241
+ check('RPC', false, err.message);
4237
4242
  }
4238
- // public base URL — baked into on-chain URIs at deploy; unset is fine for a demo
4239
- // but a silent footgun for a real launch, so surface it explicitly.
4240
- const baseUrl = process.env.ABX_PUBLIC_BASE_URL;
4241
- if (baseUrl)
4242
- check('public base URL set (baked into on-chain URIs)', true, baseUrl);
4243
- else
4244
- console.log(` ${c.orange}⚠${c.reset} public base URL${dim(' ABX_PUBLIC_BASE_URL unset → resolver-served deploys REFUSE (localhost on-chain resolves for no one). Set it (or --public-base-url) for a hosted resolver. IRRELEVANT if you go fully on-chain: --onchain-image (1/1) or --onchain-uri (code project) bake no resolver base.')}`);
4245
4243
  // storage backend — resolve it (catches missing config), then probe liveness/creds
4246
4244
  const backendId = activeBackendId();
4247
4245
  try {
4248
4246
  const backend = resolveBackend(storageOptions());
4249
4247
  const h = await backend.health?.();
4250
4248
  if (h)
4251
- check(`storage backend '${backend.id}' reachable`, h.ok, h.detail ?? '');
4249
+ check('storage', h.ok, `${backend.id} · ${h.detail ?? ''}`);
4252
4250
  else
4253
- check(`storage backend '${backend.id}' configured`, true);
4251
+ check('storage', true, `${backend.id} · configured`);
4254
4252
  }
4255
4253
  catch (err) {
4256
- check(`storage backend '${backendId}' configured`, false, err.message);
4254
+ check('storage', false, `${backendId} · ${err.message}`);
4257
4255
  }
4258
- // Managed Turbo/Arweave key if it exists, it holds prepaid credits, so remind them to back it up.
4256
+ // 3. Optionalpath-dependent setup. Unset is fine; these say WHEN you'll need each, so an unset
4257
+ // value never reads as a warning.
4258
+ console.log(`\n ${dim('Optional — depends how you deploy:')}`);
4259
+ if (signingOpt)
4260
+ opt('signing', signingOpt);
4261
+ else
4262
+ 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.');
4263
+ if (forOpt)
4264
+ opt('wallet --for', forOpt);
4265
+ const baseUrl = process.env.ABX_PUBLIC_BASE_URL;
4266
+ if (baseUrl)
4267
+ opt('resolver URL', baseUrl);
4268
+ else
4269
+ opt('resolver URL', 'ABX_PUBLIC_BASE_URL unset → needed for off-chain/resolver-served deploys; skip if fully on-chain (--onchain-image / --onchain-uri).');
4259
4270
  if (!process.env.ARWEAVE_JWK && existsSync(arweaveKeyFilePath())) {
4260
4271
  const jwk = loadArweaveJwk();
4261
- console.log(` ${dim(' Turbo/Arweave key ' + (jwk ? arweaveAddress(jwk) + ' ' : '') + 'holds your upload credits — back it up: `abx storage backup-key --out <path>`')}`);
4272
+ opt('arweave key', `${jwk ? arweaveAddress(jwk) + ' ' : ''}holds upload credits — back it up: \`abx storage backup-key --out <path>\``);
4262
4273
  }
4263
4274
  console.log('');
4264
4275
  }
@@ -4558,12 +4569,15 @@ function keepAlive() {
4558
4569
  // ── per-command usage (printed by `<cmd> --help` / `abx help <cmd>`; read-only) ──
4559
4570
  const COMMAND_HELP = {
4560
4571
  skill: `
4561
- ${bold('abx skill')} ${dim('— install the abx agent skill so your coding agent can drive abx')}
4562
- ${g('abx skill install')} copy the skill into ${bold('.claude/skills/abx-self-host')} in the current project
4563
- ${g('--global')} install into your home dir (~/.claude/skills) instead of the project
4564
- ${g('--target <dir>')} install under a different base dir (writes <dir>/.claude/skills/abx-self-host)
4572
+ ${bold('abx skill')} ${dim('— install the version-locked abx agent skill so your coding agent can drive abx')}
4573
+ ${g('abx skill install')} copy the bundled skill (version-locked to this CLI) into your agent(s)
4574
+ ${dim('default: both')} ${bold('.claude/skills')} ${dim('(Claude Code) and')} ${bold('.agents/skills')} ${dim('(Cursor · Codex · Gemini · Copilot)')}
4575
+ ${g('--agent <name>')} only one agent: ${dim('claude | cursor | codex | gemini | copilot')}
4576
+ ${g('--global')} install into your home dir (~) instead of the current project
4577
+ ${g('--target <dir>')} install the skill folder straight under <dir> (writes <dir>/abx-self-host)
4565
4578
  ${g('abx skill path')} print the absolute path to the bundled skill (for ${g('npx skills add <path>')} or manual copy)
4566
- ${dim('Restart your agent after installing so it picks up the skill. Cross-agent install:')} ${g('npx skills add ArtBlocks/abx')}`,
4579
+ ${dim('Restart your agent after installing so it picks up the skill. Once the repo is public, the git-based')}
4580
+ ${dim('cross-agent installer also works (not version-locked):')} ${g('npx skills add ArtBlocks/abx --skill abx-self-host')}`,
4567
4581
  deploy: `
4568
4582
  ${bold('abx deploy')} ${dim('— deploy + index a 1/1 (no server). Sends a tx in the chosen lane.')}
4569
4583
  --image <path> custody your own image (png · jpg · gif · svg · webp); else generative demo art
@@ -5019,7 +5033,7 @@ function help() {
5019
5033
  ${g('abx forget')} <address> drop a project's local registration + projection (on-chain untouched)
5020
5034
  ${g('abx status')} list indexed projects + node info
5021
5035
  ${g('abx doctor')} check environment (key, RPC, balance, factory, storage)
5022
- ${g('abx skill install')} install the abx agent skill into your agent [--global] [--target <dir>] · ${g('abx skill path')} prints the bundled skill
5036
+ ${g('abx skill install')} install the version-locked abx skill into your agent(s) [--agent <name>] [--global] [--target <dir>] · ${g('abx skill path')} prints the bundled skill
5023
5037
  ${g('abx version')} print the installed CLI version
5024
5038
 
5025
5039
  ${dim('Run')} ${g('abx <command> --help')} ${dim('for per-command usage. --help / -h never executes — it only prints usage.')}
@@ -5041,8 +5055,13 @@ function parseSaltFlag(raw) {
5041
5055
  // ── skill: install the abx agent skill into the user's coding agent ───────────────────────
5042
5056
  // The skill is the agentic half of the toolkit — the CLI is only useful once an agent knows how
5043
5057
  // to drive it. It ships bundled inside the published package (packages/cli/skill/, created at
5044
- // prepack); in dev the same command reads the canonical copy straight from the repo. A separate
5045
- // cross-agent channel is `npx skills add ArtBlocks/abx` (the Agent Skills standard, git-based).
5058
+ // prepack); in dev the same command reads the canonical copy straight from the repo. This is the
5059
+ // CANONICAL install path precisely because the bundle is version-locked to the CLI it ships inside
5060
+ // (the version lives in SKILL.md frontmatter — see update-check.ts). By default it writes to BOTH
5061
+ // `.claude/skills` (Claude Code) and the neutral `.agents/skills` (Cursor · Codex · Gemini ·
5062
+ // Copilot all read it), so one command covers the whole ecosystem; `--agent` narrows it. A separate
5063
+ // git-based channel, `npx skills add ArtBlocks/abx --skill abx-self-host`, works once the repo is public but is NOT
5064
+ // version-locked to a local CLI — prefer `abx skill install`.
5046
5065
  /** Locate the skill folder: the bundled copy beside the compiled CLI (published), else the
5047
5066
  * canonical repo copy (dev). Returns null if neither is present. */
5048
5067
  function resolveBundledSkill() {
@@ -5106,37 +5125,71 @@ function cmdScaffoldRenderer(rest, flags) {
5106
5125
  console.log(` ${g('abx deploy-code --image-renderer <MyRenderer> --attributes-renderer <MyTraits> --onchain-uri --schema palette:HexColor:TokenOwner --name "…" --symbol …')}`);
5107
5126
  info(`full walkthrough: ${bold(`${basename(dir)}/README.md`)} · interface + invariants: https://abx.docs.artblocks.io/protocol/renderers/`);
5108
5127
  }
5128
+ /** Human label for each skills-parent dir — which agents pick the skill up from there. */
5129
+ const SKILL_PARENT_LABELS = {
5130
+ '.claude/skills': 'Claude Code',
5131
+ '.agents/skills': 'Cursor · Codex · Gemini · Copilot',
5132
+ };
5133
+ /** Resolve which skills-parent dirs `install` should write to. No `--agent` → the whole-ecosystem
5134
+ * default (Claude Code + the neutral `.agents/skills` everyone else reads). `--agent a,b` narrows
5135
+ * to those agents' dirs (de-duped, since several share `.agents/skills`). */
5136
+ function resolveInstallParents(agentFlag) {
5137
+ if (!agentFlag || agentFlag === 'true')
5138
+ return ['.claude/skills', '.agents/skills'];
5139
+ const parents = new Set();
5140
+ for (const raw of agentFlag.split(',')) {
5141
+ const a = raw.trim().toLowerCase();
5142
+ const parent = AGENT_SKILL_PARENTS[a];
5143
+ if (!parent) {
5144
+ throw new Error(`unknown --agent '${a}'. Use one of: ${Object.keys(AGENT_SKILL_PARENTS).join(', ')} (or omit --agent to install for every agent).`);
5145
+ }
5146
+ parents.add(parent);
5147
+ }
5148
+ return [...parents];
5149
+ }
5150
+ /** Copy the bundled skill folder to `dest`, replacing any prior copy so a re-install after an
5151
+ * upgrade never leaves stale reference files behind. */
5152
+ function installSkillTo(src, dest) {
5153
+ mkdirSync(joinPath(dest, '..'), { recursive: true });
5154
+ rmSync(dest, { recursive: true, force: true });
5155
+ cpSync(src, dest, { recursive: true });
5156
+ }
5109
5157
  async function cmdSkill(rest, flags) {
5110
5158
  const sub = rest[0] ?? 'install';
5111
5159
  const src = resolveBundledSkill();
5112
5160
  if (!src) {
5113
5161
  throw new Error('bundled skill not found (expected <pkg>/skill or .claude/skills/abx-self-host). ' +
5114
- 'Reinstall @artblocks/abx-cli, or install cross-agent with: npx skills add ArtBlocks/abx');
5162
+ 'Reinstall @artblocks/abx-cli, or install cross-agent with: npx skills add ArtBlocks/abx --skill abx-self-host');
5115
5163
  }
5116
5164
  if (sub === 'path') {
5117
5165
  console.log(src);
5118
5166
  return;
5119
5167
  }
5120
5168
  if (sub === 'install') {
5121
- const base = flags.global !== undefined ? homedir() : flags.target ?? process.cwd();
5122
- const destParent = joinPath(base, '.claude', 'skills');
5123
- const dest = joinPath(destParent, 'abx-self-host');
5124
- mkdirSync(destParent, { recursive: true });
5125
- cpSync(src, dest, { recursive: true });
5126
- // Stamp the CLI version so a later run can notice the skill has drifted behind an upgraded CLI
5127
- // (the two are co-versioned). Best-effort — a missing marker just means "no drift nudge".
5128
- try {
5129
- writeFileSync(joinPath(dest, SKILL_VERSION_MARKER), readCliVersion());
5130
- }
5131
- catch {
5132
- /* non-fatal */
5169
+ const version = readSkillVersion(joinPath(src, 'SKILL.md')) ?? readCliVersion();
5170
+ // Escape hatch: --target <dir> writes the skill folder straight under <dir> (for an agent
5171
+ // whose skills dir we don't special-case, or a bespoke location).
5172
+ if (typeof flags.target === 'string' && flags.target !== 'true') {
5173
+ const dest = joinPath(resolvePath(flags.target), SKILL_DIR_NAME);
5174
+ installSkillTo(src, dest);
5175
+ ok(`installed the abx skill v${version} ${dest}`);
5176
+ info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
5177
+ return;
5133
5178
  }
5134
- ok(`installed the abx skill ${dest}`);
5135
- info('restart your agent (Claude Code / Cursor) so it loads the skill, then ask it to launch an NFT with abx.');
5136
- info('cross-agent alternative (git-based): npx skills add ArtBlocks/abx');
5179
+ const base = flags.global !== undefined ? homedir() : process.cwd();
5180
+ const parents = resolveInstallParents(flags.agent);
5181
+ ok(`installed the abx skill v${version}${flags.global !== undefined ? ' (global, ~)' : ''}:`);
5182
+ for (const parent of parents) {
5183
+ const dest = joinPath(base, parent, SKILL_DIR_NAME);
5184
+ installSkillTo(src, dest);
5185
+ const label = SKILL_PARENT_LABELS[parent];
5186
+ console.log(` ${g(joinPath(parent, SKILL_DIR_NAME))}${label ? dim(' → ' + label) : ''}`);
5187
+ }
5188
+ info('restart your agent so it loads the skill, then ask it to launch an NFT with abx.');
5189
+ 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.`);
5137
5190
  return;
5138
5191
  }
5139
- throw new Error('usage: abx skill <install|path> [--global] [--target <dir>]');
5192
+ throw new Error('usage: abx skill <install|path> [--agent claude|cursor|codex|gemini|copilot] [--global] [--target <dir>]');
5140
5193
  }
5141
5194
  main().catch((err) => {
5142
5195
  // Keep the actionable message; strip viem's verbose boilerplate trailer (Docs:/Version: lines) so