@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.
- package/README.md +11 -1
- package/dist/index.js +224 -14
- 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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
81329
|
-
if (
|
|
81330
|
-
|
|
81331
|
-
|
|
81332
|
-
|
|
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