@candledottv/cli 0.11.19 → 0.11.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +11 -1
  2. package/dist/index.js +224 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @candledottv/cli
2
2
 
3
- The Candle CLI (current version 0.11.19): authorize a device from your browser, keep your own keys
3
+ The Candle CLI (current version 0.11.20): authorize a device from your browser, keep your own keys
4
4
  in an encrypted vault on this machine, hand an agent a TEE wallet it can trade, and manage API
5
5
  keys, wallets, and setup health from the terminal. Zero runtime dependencies; the whole thing is
6
6
  one self-contained `dist/index.js` that runs under plain Node.
@@ -8,6 +8,16 @@ one self-contained `dist/index.js` that runs under plain Node.
8
8
  The full custody guide is [CLI custody](https://docs.candle.tv/developers/cli-custody), and every
9
9
  command and flag is in the [Candle CLI reference](https://docs.candle.tv/developers/cli).
10
10
 
11
+ `candle vault rename --pairs-from <file> [--dry-run]` renames 1–256 vault or external keys under
12
+ one unlock and one commit. Use `<label|address|id> <new-label>` lines or CSV columns `from,to`
13
+ (picked by name; quote names with spaces or commas). Blank lines and `#` comments are skipped.
14
+ Every finding refuses the whole file before any write; swaps within the batch work. `--dry-run`
15
+ unlocks and prints the plan without writing; `--json` returns
16
+ `{ ok, renamed: [{ id, address, role, from, to }] }` (plus `dryRun: true` for a dry run).
17
+ Single and batch rename require new labels of at most 64 characters, without edge whitespace,
18
+ control characters or a leading `-`; names are never trimmed. Existing longer labels still
19
+ resolve as targets. A terminal is required. TEE wallets are refused by `vault rename`.
20
+
11
21
  ## Quick start
12
22
 
13
23
  ```
package/dist/index.js CHANGED
@@ -1319,6 +1319,7 @@ var init_errors = __esm(() => {
1319
1319
  "VAULT_LABEL_TAKEN",
1320
1320
  "VAULT_LABEL_UNCHANGED",
1321
1321
  "VAULT_RENAME_ROLE_REFUSED",
1322
+ "VAULT_RENAME_BATCH_REFUSED",
1322
1323
  "PROMOTE_BATCH_REFUSED",
1323
1324
  "WALLET_LIMIT_REACHED",
1324
1325
  "TIER_REQUIRED",
@@ -63166,7 +63167,7 @@ init_render();
63166
63167
  init_secret_store();
63167
63168
 
63168
63169
  // src/version.ts
63169
- var CLI_VERSION = "0.11.19";
63170
+ var CLI_VERSION = "0.11.20";
63170
63171
 
63171
63172
  // src/commands/auth.ts
63172
63173
  init_keys_embedded_wallet();
@@ -64067,8 +64068,12 @@ var HELP = {
64067
64068
  description: "Derive the next Solana key (m/44'/501'/n'/0') or EVM key (m/44'/60'/n'/0/0), or n of them under one unlock; the name must be free"
64068
64069
  },
64069
64070
  {
64070
- invocation: "rename <label|address> <new-label> [--id <entry-id>]",
64071
- description: "Rename one key. The address, the derivation and the key blob do not change"
64071
+ invocation: "rename <label|address|id> <new-label> [--id <entry-id>]",
64072
+ description: "Rename one vault or external key. New names: at most 64 characters, no edge whitespace. The address, the derivation and the key blob do not change"
64073
+ },
64074
+ {
64075
+ invocation: " rename --pairs-from <file> [--dry-run]",
64076
+ description: "Rename 1–256 vault or external keys under one unlock and one commit. All findings refuse the whole file; swaps are allowed. --dry-run prints the plan and writes nothing; --json returns the renamed rows"
64072
64077
  },
64073
64078
  { invocation: "phrase show", description: "Show the 24-word recovery phrase (terminal only)" },
64074
64079
  {
@@ -64149,7 +64154,11 @@ var HELP = {
64149
64154
  },
64150
64155
  {
64151
64156
  invocation: "--pairs-from <file>",
64152
- description: "promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
64157
+ description: "rename: one '<label|address|id> <new-label>' per line, or CSV columns from,to (1–256 rows; blank lines and # comments skipped). No trimming of names; use CSV for names with spaces. promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
64158
+ },
64159
+ {
64160
+ invocation: "--dry-run",
64161
+ description: "rename --pairs-from: unlock, check every row, print the plan, and write nothing"
64153
64162
  },
64154
64163
  {
64155
64164
  invocation: "--to-key <prefix|label>",
@@ -64163,6 +64172,7 @@ var HELP = {
64163
64172
  "candle vault transfer 0x000000000000000000000000000000000000dEaD --amount 0.5 --asset USDG --from hood-cold",
64164
64173
  "candle vault list cn-s",
64165
64174
  "candle vault rename key-7 treasury-cold",
64175
+ "candle vault rename --pairs-from ./rename-plan.csv --dry-run",
64166
64176
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
64167
64177
  "candle vault promote-batch --pairs-from ./promote-plan.csv",
64168
64178
  "candle vault promote-batch --pairs-from ./promote-plan.csv --to-key tr-01",
@@ -81289,6 +81299,10 @@ function validateLabel(label) {
81289
81299
  if (label.startsWith("-")) {
81290
81300
  return `A key's label cannot begin with "-": it would be read as a flag everywhere a label is typed.`;
81291
81301
  }
81302
+ if (label !== label.trim())
81303
+ return "A key's label cannot begin or end with whitespace.";
81304
+ if (label.length > 64)
81305
+ return "A key's label cannot exceed 64 characters.";
81292
81306
  return;
81293
81307
  }
81294
81308
  function resolveRenameTarget(index, old, id) {
@@ -81306,30 +81320,165 @@ function resolveRenameTarget(index, old, id) {
81306
81320
  return { kind: "found", entry: byAddress[0], by: "address" };
81307
81321
  if (byAddress.length > 1)
81308
81322
  return { kind: "ambiguous", by: "address", candidates: byAddress };
81323
+ const byId = index.entries.find((entry) => entry.id === old);
81324
+ if (byId !== undefined)
81325
+ return { kind: "found", entry: byId, by: "id" };
81309
81326
  return { kind: "none" };
81310
81327
  }
81311
81328
 
81329
+ // src/vault/rename-batch.ts
81330
+ var MAX_RENAME_BATCH = 256;
81331
+ function csvCells(text) {
81332
+ const cells = [];
81333
+ let cell = "";
81334
+ let quoted = false;
81335
+ let closed = false;
81336
+ for (let i = 0;i < text.length; i++) {
81337
+ const char = text[i];
81338
+ if (quoted) {
81339
+ if (char === '"') {
81340
+ if (text[i + 1] === '"') {
81341
+ cell += '"';
81342
+ i++;
81343
+ } else {
81344
+ quoted = false;
81345
+ closed = true;
81346
+ }
81347
+ } else
81348
+ cell += char;
81349
+ } else if (char === ",") {
81350
+ cells.push(cell);
81351
+ cell = "";
81352
+ closed = false;
81353
+ } else if (char === '"' && cell === "" && !closed)
81354
+ quoted = true;
81355
+ else if (char === '"' || closed)
81356
+ return;
81357
+ else
81358
+ cell += char;
81359
+ }
81360
+ if (quoted)
81361
+ return;
81362
+ cells.push(cell);
81363
+ return cells;
81364
+ }
81365
+ function parseRenamePairs(contents) {
81366
+ const lines = contents.split(/\r?\n/).map((text, at) => ({ text, line: at + 1 })).filter(({ text }) => text.trim() !== "" && !/^\s*#/.test(text));
81367
+ const rows = [];
81368
+ const findings = [];
81369
+ const header = csvCells(lines[0]?.text ?? "")?.map((cell) => cell.trim()) ?? [];
81370
+ const csv = header.length > 1 && (header.includes("from") || header.includes("to"));
81371
+ if (csv && (header.filter((name2) => name2 === "from").length !== 1 || header.filter((name2) => name2 === "to").length !== 1)) {
81372
+ return {
81373
+ rows,
81374
+ findings: [{ line: lines[0]?.line ?? 0, problem: "A CSV needs exactly one from and one to column." }]
81375
+ };
81376
+ }
81377
+ const data = csv ? lines.slice(1) : lines;
81378
+ if (data.length < 1 || data.length > MAX_RENAME_BATCH) {
81379
+ findings.push({ line: 0, problem: `A rename file must hold 1 to ${MAX_RENAME_BATCH} rows; found ${data.length}.` });
81380
+ }
81381
+ for (const { text, line } of data) {
81382
+ let from;
81383
+ let to;
81384
+ if (csv) {
81385
+ const cells = csvCells(text);
81386
+ if (cells === undefined || cells.length !== header.length) {
81387
+ findings.push({ line, problem: "Malformed CSV row or field count does not match the header." });
81388
+ continue;
81389
+ }
81390
+ from = cells[header.indexOf("from")];
81391
+ to = cells[header.indexOf("to")];
81392
+ } else {
81393
+ const match = /^(\S+)[ \t]+(\S+)([ \t]*)$/.exec(text);
81394
+ if (match !== null)
81395
+ [from, to] = [match[1], `${match[2]}${match[3]}`];
81396
+ }
81397
+ if (from === undefined || from === "" || to === undefined) {
81398
+ findings.push({ line, problem: 'A row needs "<label|address|id> <new-label>" (CSV columns: from,to).' });
81399
+ continue;
81400
+ }
81401
+ rows.push({ line, from, to });
81402
+ const invalid = validateLabel(to);
81403
+ if (invalid !== undefined)
81404
+ findings.push({ line, problem: invalid });
81405
+ }
81406
+ for (const field of ["from", "to"]) {
81407
+ const seen = new Map;
81408
+ for (const row of rows) {
81409
+ const earlier = seen.get(row[field]);
81410
+ if (earlier !== undefined)
81411
+ findings.push({ line: row.line, problem: `Duplicate ${field} ${row[field]} (line ${earlier}).` });
81412
+ else
81413
+ seen.set(row[field], row.line);
81414
+ }
81415
+ }
81416
+ return { rows, findings };
81417
+ }
81418
+ function planBatchRename(index, rows) {
81419
+ const plan = [];
81420
+ const findings = [];
81421
+ const seen = new Map;
81422
+ for (const row of rows) {
81423
+ const match = resolveRenameTarget(index, row.from);
81424
+ if (match.kind !== "found") {
81425
+ findings.push({
81426
+ line: row.line,
81427
+ problem: match.kind === "none" ? `No key matches ${row.from}.` : `${row.from} is ambiguous; use an id. Candidates: ${match.candidates.map((entry2) => `${entry2.address} (${entry2.id})`).join(", ")}.`
81428
+ });
81429
+ continue;
81430
+ }
81431
+ const entry = match.entry;
81432
+ const earlier = seen.get(entry.id);
81433
+ if (earlier !== undefined)
81434
+ findings.push({ line: row.line, problem: `Same key as line ${earlier}, named by another handle.` });
81435
+ else
81436
+ seen.set(entry.id, row.line);
81437
+ if (entry.role !== "vault" && entry.role !== "external") {
81438
+ findings.push({
81439
+ line: row.line,
81440
+ problem: `${entry.label} is a TEE wallet; vault rename only changes vault or external keys.`
81441
+ });
81442
+ }
81443
+ if (entry.label === row.to)
81444
+ findings.push({ line: row.line, problem: `${row.to} is already this key's label.` });
81445
+ plan.push({ ...row, entry });
81446
+ }
81447
+ const targets = new Set(plan.map(({ entry }) => entry.id));
81448
+ for (const row of plan) {
81449
+ if (index.entries.some((entry) => entry.label === row.to && !targets.has(entry.id))) {
81450
+ findings.push({ line: row.line, problem: `New name ${row.to} is taken by a key outside the batch.` });
81451
+ }
81452
+ }
81453
+ return { plan, findings };
81454
+ }
81455
+
81312
81456
  // src/commands/vault-rename.ts
81313
81457
  init_store();
81314
81458
  init_vault_support();
81315
- var RENAME_USAGE = "Usage: candle vault rename <label|address> <new-label> [--id <entry-id>]";
81459
+ var RENAME_USAGE = "Usage: candle vault rename <label|address|id> <new-label> [--id <entry-id>] | --pairs-from <file> [--dry-run]";
81316
81460
  var SAME_STRING_LINE = "The two arguments are the same string; nothing to do.";
81317
81461
  async function vaultRename(args, ctx) {
81318
81462
  const parsed = parseArgs(args, {
81319
- valueFlags: ["--keystore", "--id"],
81320
- booleanFlags: ["--accept-older-copy"],
81321
- pathFlags: ["--keystore"]
81463
+ valueFlags: ["--keystore", "--id", "--pairs-from"],
81464
+ booleanFlags: ["--accept-older-copy", "--dry-run"],
81465
+ pathFlags: ["--keystore", "--pairs-from"]
81322
81466
  });
81323
81467
  if ("error" in parsed)
81324
81468
  return usage(ctx, parsed.error);
81325
81469
  const [old, next, extra] = parsed.positionals;
81326
- if (old === undefined || next === undefined || extra !== undefined)
81470
+ const pairsFile = parsed.values["--pairs-from"];
81471
+ const batch = pairsFile !== undefined;
81472
+ if (batch ? parsed.positionals.length > 0 || parsed.values["--id"] !== undefined : old === undefined || next === undefined || extra !== undefined || parsed.booleans.has("--dry-run")) {
81327
81473
  return usage(ctx, RENAME_USAGE);
81328
- const invalid = validateLabel(next);
81329
- if (invalid !== undefined)
81330
- return usage(ctx, invalid);
81331
- if (old === next)
81332
- return usage(ctx, SAME_STRING_LINE);
81474
+ }
81475
+ if (!batch && next !== undefined) {
81476
+ const invalid = validateLabel(next);
81477
+ if (invalid !== undefined)
81478
+ return usage(ctx, invalid);
81479
+ if (old === next)
81480
+ return usage(ctx, SAME_STRING_LINE);
81481
+ }
81333
81482
  const id = parsed.values["--id"];
81334
81483
  if (!refuseEnvPassphrase(ctx))
81335
81484
  return 1;
@@ -81340,12 +81489,60 @@ async function vaultRename(args, ctx) {
81340
81489
  if ("error" in resolvedVault)
81341
81490
  return usage(ctx, resolvedVault.error);
81342
81491
  const path = resolvedVault.path;
81492
+ let rows = [];
81493
+ if (pairsFile !== undefined) {
81494
+ let contents;
81495
+ try {
81496
+ contents = await deps.readFile(pairsFile);
81497
+ } catch (error) {
81498
+ return usage(ctx, `Could not read --pairs-from: ${error instanceof Error ? error.message : error}`);
81499
+ }
81500
+ const parsedFile = parseRenamePairs(contents);
81501
+ if (parsedFile.findings.length > 0)
81502
+ return batchRefusal(ctx, parsedFile.findings, 2);
81503
+ rows = parsedFile.rows;
81504
+ }
81343
81505
  return runVaultCommand(ctx, async ({ hold }) => {
81344
81506
  const raw = await requireVaultRaw(ctx, resolvedVault);
81345
81507
  const opened = await unlockInteractively(ctx, path, raw, {
81346
81508
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
81347
81509
  });
81348
81510
  const vault = hold(opened.vault);
81511
+ if (batch) {
81512
+ const { plan, findings } = planBatchRename(vault.index, rows);
81513
+ if (findings.length > 0)
81514
+ return batchRefusal(ctx, findings, 1);
81515
+ const renamed = plan.map(({ entry: entry2, to }) => ({
81516
+ id: entry2.id,
81517
+ address: entry2.address,
81518
+ role: entry2.role,
81519
+ from: entry2.label,
81520
+ to
81521
+ }));
81522
+ const dryRun = parsed.booleans.has("--dry-run");
81523
+ if (!dryRun) {
81524
+ const names = new Map(renamed.map(({ id: id2, to }) => [id2, to]));
81525
+ await commitVault(vault, {
81526
+ index: {
81527
+ hd: vault.index.hd,
81528
+ entries: vault.index.entries.map((entry2) => names.has(entry2.id) ? { ...entry2, label: names.get(entry2.id) } : entry2)
81529
+ }
81530
+ }, deps);
81531
+ }
81532
+ if (ctx.json)
81533
+ writeJson(deps, { ok: true, renamed, ...dryRun ? { dryRun: true } : {} });
81534
+ else {
81535
+ deps.stdout.write(dryRun ? `Dry run: nothing was written.
81536
+ ` : `Renamed ${renamed.length} keys in one commit.
81537
+ `);
81538
+ for (const row of renamed)
81539
+ deps.stdout.write(` ${row.from} -> ${row.to} ${row.address} (${row.id}, ${row.role})
81540
+ `);
81541
+ }
81542
+ return 0;
81543
+ }
81544
+ if (old === undefined || next === undefined)
81545
+ return usage(ctx, RENAME_USAGE);
81349
81546
  const entry = resolveTarget(vault.index, old, id);
81350
81547
  if (entry.role === "tee-wallet") {
81351
81548
  throw new VaultError("VAULT_RENAME_ROLE_REFUSED", `${entry.label} is a TEE wallet. Its label was sent to Candle when it was enabled and \`candle wallets\` lists that copy, so renaming it here would give one wallet two names and nothing reconciles them.`, {
@@ -81386,6 +81583,19 @@ async function vaultRename(args, ctx) {
81386
81583
  return 0;
81387
81584
  });
81388
81585
  }
81586
+ function batchRefusal(ctx, findings, exit) {
81587
+ const message = "Batch rename refused. Nothing was written.";
81588
+ if (ctx.json)
81589
+ writeJson(ctx.deps, { ok: false, code: "VAULT_RENAME_BATCH_REFUSED", message, findings });
81590
+ else {
81591
+ ctx.deps.stderr.write(`${message}
81592
+ `);
81593
+ for (const finding of findings)
81594
+ ctx.deps.stderr.write(` ${finding.line === 0 ? "file" : `line ${finding.line}`}: ${finding.problem}
81595
+ `);
81596
+ }
81597
+ return exit;
81598
+ }
81389
81599
  function resolveTarget(index, old, id) {
81390
81600
  const match = resolveRenameTarget(index, old, id);
81391
81601
  if (match.kind === "found")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.19",
3
+ "version": "0.11.20",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {