ymmv-cli 0.2.0 → 0.4.0
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 +1 -0
- package/dist/cli.js +233 -140
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ First run includes a one-time GitHub sign-in (the device flow you know from `gh`
|
|
|
25
25
|
- `ymmv <handle>` — view a profile, or diff it against yours when you're logged in
|
|
26
26
|
- `ymmv set editor Neovim` — change one value
|
|
27
27
|
- `ymmv set --extra "Keyboard=HHKB"` — add a free-form line of your own
|
|
28
|
+
- `ymmv unset editor` — remove one value (`ymmv set editor -` works too); `ymmv unset --extra "Keyboard"` removes an extra
|
|
28
29
|
- `ymmv delete` — remove your profile
|
|
29
30
|
- `ymmv login` / `ymmv logout` — sign in / out
|
|
30
31
|
|
package/dist/cli.js
CHANGED
|
@@ -284,7 +284,7 @@ function parseProfile(raw) {
|
|
|
284
284
|
|
|
285
285
|
// ../shared/dist/reserved.js
|
|
286
286
|
var RESERVED_ROUTES = ["api", "login", "logout"];
|
|
287
|
-
var CLI_VERBS = ["login", "logout", "set", "delete", "view", "help"];
|
|
287
|
+
var CLI_VERBS = ["login", "logout", "set", "unset", "delete", "view", "help"];
|
|
288
288
|
var RESERVED = [.../* @__PURE__ */ new Set([...RESERVED_ROUTES, ...CLI_VERBS])];
|
|
289
289
|
var RESERVED_SET = new Set(RESERVED);
|
|
290
290
|
var HANDLE_RE = /^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$/;
|
|
@@ -440,6 +440,136 @@ async function login(deps = {}) {
|
|
|
440
440
|
);
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// src/render.ts
|
|
444
|
+
var ESC = String.fromCharCode(27);
|
|
445
|
+
var CSI = `${ESC}[`;
|
|
446
|
+
var CODES = {
|
|
447
|
+
amber: `${CSI}93m`,
|
|
448
|
+
// DESIGN: amber == ANSI bright-yellow
|
|
449
|
+
faint: `${CSI}90m`,
|
|
450
|
+
bold: `${CSI}1m`,
|
|
451
|
+
reset: `${CSI}0m`
|
|
452
|
+
};
|
|
453
|
+
var NO_CODES = { amber: "", faint: "", bold: "", reset: "" };
|
|
454
|
+
function palette(color) {
|
|
455
|
+
return color ? CODES : NO_CODES;
|
|
456
|
+
}
|
|
457
|
+
var ESC_INTRODUCERS = `${String.fromCharCode(27)}${String.fromCharCode(155)}`;
|
|
458
|
+
var BEL = String.fromCharCode(7);
|
|
459
|
+
var ANSI_RE = new RegExp(
|
|
460
|
+
`[${ESC_INTRODUCERS}][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d/#&.:=?%@~_]*)*)?${BEL})|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))`,
|
|
461
|
+
"g"
|
|
462
|
+
);
|
|
463
|
+
var CTRL_RE = new RegExp(
|
|
464
|
+
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
|
|
465
|
+
"g"
|
|
466
|
+
);
|
|
467
|
+
var BIDI_RE = new RegExp(
|
|
468
|
+
`[${String.fromCharCode(8234)}-${String.fromCharCode(8238)}${String.fromCharCode(8294)}-${String.fromCharCode(8297)}${String.fromCharCode(8206)}${String.fromCharCode(8207)}]`,
|
|
469
|
+
"g"
|
|
470
|
+
);
|
|
471
|
+
function sanitizeValue(value) {
|
|
472
|
+
return value.replace(ANSI_RE, "").replace(CTRL_RE, "").replace(BIDI_RE, "");
|
|
473
|
+
}
|
|
474
|
+
function useColor(env, isTTY) {
|
|
475
|
+
if (env.NO_COLOR !== void 0) return false;
|
|
476
|
+
if (env.FORCE_COLOR !== void 0) return env.FORCE_COLOR !== "0";
|
|
477
|
+
return isTTY;
|
|
478
|
+
}
|
|
479
|
+
function orderedEntries(profile) {
|
|
480
|
+
const byKey = new Map(
|
|
481
|
+
(profile.entries ?? []).map((e) => [e.key, e.value])
|
|
482
|
+
);
|
|
483
|
+
return CURATED_KEYS.flatMap((key) => {
|
|
484
|
+
const value = byKey.get(key);
|
|
485
|
+
return value === void 0 ? [] : [{ label: KEY_LABELS[key], value: sanitizeValue(value) }];
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
function renderProfile(profile, opts) {
|
|
489
|
+
const c = palette(opts.color);
|
|
490
|
+
const rows = orderedEntries(profile);
|
|
491
|
+
const extras = (profile.extras ?? []).map((x) => ({
|
|
492
|
+
label: sanitizeValue(x.label),
|
|
493
|
+
value: sanitizeValue(x.value)
|
|
494
|
+
}));
|
|
495
|
+
const labelW = Math.max(
|
|
496
|
+
0,
|
|
497
|
+
...rows.map((r) => r.label.length),
|
|
498
|
+
...extras.map((x) => x.label.length)
|
|
499
|
+
);
|
|
500
|
+
const lines = ["", ` ${c.bold}${sanitizeValue(profile.handle)}${c.reset}`, ""];
|
|
501
|
+
for (const r of rows) {
|
|
502
|
+
lines.push(` ${c.faint}${r.label.padEnd(labelW)}${c.reset} ${r.value}`);
|
|
503
|
+
}
|
|
504
|
+
if (extras.length) {
|
|
505
|
+
lines.push("");
|
|
506
|
+
for (const x of extras) {
|
|
507
|
+
lines.push(` ${c.faint}${x.label.padEnd(labelW)}${c.reset} ${x.value}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
lines.push("", ` ${c.faint}updated ${sanitizeValue(profile.updated_at)}${c.reset}`, "");
|
|
511
|
+
return lines.join("\n");
|
|
512
|
+
}
|
|
513
|
+
var MISSING = "\u2014";
|
|
514
|
+
function extrasBlock(extras, theirsLabel, mineLabel, c) {
|
|
515
|
+
if (!extras.theirs.length && !extras.mine.length) return [];
|
|
516
|
+
const out = ["", ` ${c.faint}extras${c.reset}`];
|
|
517
|
+
const line = (who, label, value) => ` ${c.faint}${who}${c.reset} ${sanitizeValue(label)} = ${sanitizeValue(value)}`;
|
|
518
|
+
for (const x of extras.theirs) out.push(line(theirsLabel, x.label, x.value));
|
|
519
|
+
for (const x of extras.mine) out.push(line(mineLabel, x.label, x.value));
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
function renderDiff(result, opts) {
|
|
523
|
+
const c = palette(opts.color);
|
|
524
|
+
const theirsLabel = sanitizeValue(opts.theirsLabel);
|
|
525
|
+
const mineLabel = sanitizeValue(opts.mineLabel);
|
|
526
|
+
const cells = result.rows.map((r) => ({
|
|
527
|
+
label: r.label,
|
|
528
|
+
theirs: r.theirs === null ? MISSING : sanitizeValue(r.theirs),
|
|
529
|
+
mine: r.mine === null ? MISSING : sanitizeValue(r.mine),
|
|
530
|
+
differ: r.status !== "same"
|
|
531
|
+
}));
|
|
532
|
+
const labelW = Math.max(3, ...cells.map((r) => r.label.length));
|
|
533
|
+
const theirsW = Math.max(theirsLabel.length, ...cells.map((r) => r.theirs.length));
|
|
534
|
+
const lines = [""];
|
|
535
|
+
lines.push(
|
|
536
|
+
` ${c.faint}${"".padEnd(labelW)} ${theirsLabel.padEnd(theirsW)} ${mineLabel}${c.reset}`
|
|
537
|
+
);
|
|
538
|
+
for (const r of cells) {
|
|
539
|
+
if (!opts.color) {
|
|
540
|
+
const sym = r.differ ? "~" : "=";
|
|
541
|
+
lines.push(`${sym} ${r.label.padEnd(labelW)} ${r.theirs.padEnd(theirsW)} ${r.mine}`);
|
|
542
|
+
} else if (r.differ) {
|
|
543
|
+
lines.push(
|
|
544
|
+
`${c.amber}\u2022${c.reset} ${r.label.padEnd(labelW)} ${c.amber}${r.theirs.padEnd(theirsW)}${c.reset} ${c.amber}${r.mine}${c.reset}`
|
|
545
|
+
);
|
|
546
|
+
} else {
|
|
547
|
+
lines.push(
|
|
548
|
+
` ${c.faint}${r.label.padEnd(labelW)} ${r.theirs.padEnd(theirsW)} ${r.mine}${c.reset}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
lines.push(...extrasBlock(result.extras, theirsLabel, mineLabel, c));
|
|
553
|
+
lines.push(
|
|
554
|
+
"",
|
|
555
|
+
` ${c.faint}${result.differ} differ \xB7 ${result.shared} shared \u2014 your mileage may vary${c.reset}`,
|
|
556
|
+
""
|
|
557
|
+
);
|
|
558
|
+
return lines.join("\n");
|
|
559
|
+
}
|
|
560
|
+
function nudge(color) {
|
|
561
|
+
const c = palette(color);
|
|
562
|
+
return `
|
|
563
|
+
${c.amber}publish yours to diff \u2192${c.reset} run ${c.bold}ymmv${c.reset}
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
function notFound(handle) {
|
|
567
|
+
return `
|
|
568
|
+
no ymmv profile for "${sanitizeValue(handle)}" yet.
|
|
569
|
+
publish one at ymmv.fyi with: npx ymmv-cli
|
|
570
|
+
`;
|
|
571
|
+
}
|
|
572
|
+
|
|
443
573
|
// src/api.ts
|
|
444
574
|
async function rateLimitMessage(res) {
|
|
445
575
|
const retry = res.headers.get("retry-after");
|
|
@@ -470,6 +600,11 @@ async function publishProfile(profile) {
|
|
|
470
600
|
// a mutation must never follow a redirect into a false success
|
|
471
601
|
});
|
|
472
602
|
let cred = await ensureLogin();
|
|
603
|
+
if ((cred.handle ?? "").toLowerCase() !== profile.handle.toLowerCase()) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
"the stored login changed while this command was running \u2014 re-run it under the current account."
|
|
606
|
+
);
|
|
607
|
+
}
|
|
473
608
|
let res = await send(cred);
|
|
474
609
|
if (res.status === 401 || res.status === 409) {
|
|
475
610
|
if (res.status === 401) await deleteToken();
|
|
@@ -488,7 +623,8 @@ async function publishProfile(profile) {
|
|
|
488
623
|
throw new Error(`publish failed: ${res.status} ${await res.text()}`);
|
|
489
624
|
}
|
|
490
625
|
const data = await res.json();
|
|
491
|
-
|
|
626
|
+
const shown = typeof data.handle === "string" ? sanitizeValue(data.handle) : profile.handle;
|
|
627
|
+
console.log(`Published ${shown} -> ${BASE}/${shown}`);
|
|
492
628
|
}
|
|
493
629
|
async function fetchProfileJson(handle) {
|
|
494
630
|
const res = await fetch(`${BASE}/api/v1/u/${encodeURIComponent(handle)}`);
|
|
@@ -625,142 +761,34 @@ function applySet(existing, target) {
|
|
|
625
761
|
if (i >= 0) entries[i] = next;
|
|
626
762
|
else entries.push(next);
|
|
627
763
|
} else {
|
|
628
|
-
const
|
|
764
|
+
const wanted = target.label.trim().toLowerCase();
|
|
765
|
+
const i = extras.findIndex((x) => x.label.trim().toLowerCase() === wanted);
|
|
629
766
|
const next = { label: target.label, value: target.value };
|
|
630
767
|
if (i >= 0) extras[i] = next;
|
|
631
768
|
else extras.push(next);
|
|
632
769
|
}
|
|
633
770
|
return { entries, extras };
|
|
634
771
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
};
|
|
646
|
-
var NO_CODES = { amber: "", faint: "", bold: "", reset: "" };
|
|
647
|
-
function palette(color) {
|
|
648
|
-
return color ? CODES : NO_CODES;
|
|
649
|
-
}
|
|
650
|
-
var ESC_INTRODUCERS = `${String.fromCharCode(27)}${String.fromCharCode(155)}`;
|
|
651
|
-
var BEL = String.fromCharCode(7);
|
|
652
|
-
var ANSI_RE = new RegExp(
|
|
653
|
-
`[${ESC_INTRODUCERS}][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d/#&.:=?%@~_]*)*)?${BEL})|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))`,
|
|
654
|
-
"g"
|
|
655
|
-
);
|
|
656
|
-
var CTRL_RE = new RegExp(
|
|
657
|
-
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
|
|
658
|
-
"g"
|
|
659
|
-
);
|
|
660
|
-
var BIDI_RE = new RegExp(
|
|
661
|
-
`[${String.fromCharCode(8234)}-${String.fromCharCode(8238)}${String.fromCharCode(8294)}-${String.fromCharCode(8297)}${String.fromCharCode(8206)}${String.fromCharCode(8207)}]`,
|
|
662
|
-
"g"
|
|
663
|
-
);
|
|
664
|
-
function sanitizeValue(value) {
|
|
665
|
-
return value.replace(ANSI_RE, "").replace(CTRL_RE, "").replace(BIDI_RE, "");
|
|
666
|
-
}
|
|
667
|
-
function useColor(env, isTTY) {
|
|
668
|
-
if (env.NO_COLOR !== void 0) return false;
|
|
669
|
-
if (env.FORCE_COLOR !== void 0) return env.FORCE_COLOR !== "0";
|
|
670
|
-
return isTTY;
|
|
671
|
-
}
|
|
672
|
-
function orderedEntries(profile) {
|
|
673
|
-
const byKey = new Map(
|
|
674
|
-
(profile.entries ?? []).map((e) => [e.key, e.value])
|
|
675
|
-
);
|
|
676
|
-
return CURATED_KEYS.flatMap((key) => {
|
|
677
|
-
const value = byKey.get(key);
|
|
678
|
-
return value === void 0 ? [] : [{ label: KEY_LABELS[key], value: sanitizeValue(value) }];
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
function renderProfile(profile, opts) {
|
|
682
|
-
const c = palette(opts.color);
|
|
683
|
-
const rows = orderedEntries(profile);
|
|
684
|
-
const extras = (profile.extras ?? []).map((x) => ({
|
|
685
|
-
label: sanitizeValue(x.label),
|
|
686
|
-
value: sanitizeValue(x.value)
|
|
687
|
-
}));
|
|
688
|
-
const labelW = Math.max(
|
|
689
|
-
0,
|
|
690
|
-
...rows.map((r) => r.label.length),
|
|
691
|
-
...extras.map((x) => x.label.length)
|
|
692
|
-
);
|
|
693
|
-
const lines = ["", ` ${c.bold}${sanitizeValue(profile.handle)}${c.reset}`, ""];
|
|
694
|
-
for (const r of rows) {
|
|
695
|
-
lines.push(` ${c.faint}${r.label.padEnd(labelW)}${c.reset} ${r.value}`);
|
|
696
|
-
}
|
|
697
|
-
if (extras.length) {
|
|
698
|
-
lines.push("");
|
|
699
|
-
for (const x of extras) {
|
|
700
|
-
lines.push(` ${c.faint}${x.label.padEnd(labelW)}${c.reset} ${x.value}`);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
lines.push("", ` ${c.faint}updated ${sanitizeValue(profile.updated_at)}${c.reset}`, "");
|
|
704
|
-
return lines.join("\n");
|
|
705
|
-
}
|
|
706
|
-
var MISSING = "\u2014";
|
|
707
|
-
function extrasBlock(extras, theirsLabel, mineLabel, c) {
|
|
708
|
-
if (!extras.theirs.length && !extras.mine.length) return [];
|
|
709
|
-
const out = ["", ` ${c.faint}extras${c.reset}`];
|
|
710
|
-
const line = (who, label, value) => ` ${c.faint}${who}${c.reset} ${sanitizeValue(label)} = ${sanitizeValue(value)}`;
|
|
711
|
-
for (const x of extras.theirs) out.push(line(theirsLabel, x.label, x.value));
|
|
712
|
-
for (const x of extras.mine) out.push(line(mineLabel, x.label, x.value));
|
|
713
|
-
return out;
|
|
714
|
-
}
|
|
715
|
-
function renderDiff(result, opts) {
|
|
716
|
-
const c = palette(opts.color);
|
|
717
|
-
const theirsLabel = sanitizeValue(opts.theirsLabel);
|
|
718
|
-
const mineLabel = sanitizeValue(opts.mineLabel);
|
|
719
|
-
const cells = result.rows.map((r) => ({
|
|
720
|
-
label: r.label,
|
|
721
|
-
theirs: r.theirs === null ? MISSING : sanitizeValue(r.theirs),
|
|
722
|
-
mine: r.mine === null ? MISSING : sanitizeValue(r.mine),
|
|
723
|
-
differ: r.status !== "same"
|
|
724
|
-
}));
|
|
725
|
-
const labelW = Math.max(3, ...cells.map((r) => r.label.length));
|
|
726
|
-
const theirsW = Math.max(theirsLabel.length, ...cells.map((r) => r.theirs.length));
|
|
727
|
-
const lines = [""];
|
|
728
|
-
lines.push(
|
|
729
|
-
` ${c.faint}${"".padEnd(labelW)} ${theirsLabel.padEnd(theirsW)} ${mineLabel}${c.reset}`
|
|
730
|
-
);
|
|
731
|
-
for (const r of cells) {
|
|
732
|
-
if (!opts.color) {
|
|
733
|
-
const sym = r.differ ? "~" : "=";
|
|
734
|
-
lines.push(`${sym} ${r.label.padEnd(labelW)} ${r.theirs.padEnd(theirsW)} ${r.mine}`);
|
|
735
|
-
} else if (r.differ) {
|
|
736
|
-
lines.push(
|
|
737
|
-
`${c.amber}\u2022${c.reset} ${r.label.padEnd(labelW)} ${r.theirs.padEnd(theirsW)} ${c.amber}${r.mine}${c.reset}`
|
|
738
|
-
);
|
|
739
|
-
} else {
|
|
740
|
-
lines.push(
|
|
741
|
-
` ${c.faint}${r.label.padEnd(labelW)} ${r.theirs.padEnd(theirsW)} ${r.mine}${c.reset}`
|
|
742
|
-
);
|
|
743
|
-
}
|
|
772
|
+
function applyUnset(existing, target) {
|
|
773
|
+
const entries = existing.entries.map((e) => ({ ...e }));
|
|
774
|
+
const extras = existing.extras.map((x) => ({ ...x }));
|
|
775
|
+
if (target.kind === "curated") {
|
|
776
|
+
const hit2 = entries.find((e) => e.key === target.key);
|
|
777
|
+
if (!hit2) return { entries, extras, removed: null };
|
|
778
|
+
return {
|
|
779
|
+
entries: entries.filter((e) => e.key !== target.key),
|
|
780
|
+
extras,
|
|
781
|
+
removed: { label: hit2.key, value: hit2.value }
|
|
782
|
+
};
|
|
744
783
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
function nudge(color) {
|
|
754
|
-
const c = palette(color);
|
|
755
|
-
return `
|
|
756
|
-
${c.amber}publish yours to diff \u2192${c.reset} run ${c.bold}ymmv${c.reset}
|
|
757
|
-
`;
|
|
758
|
-
}
|
|
759
|
-
function notFound(handle) {
|
|
760
|
-
return `
|
|
761
|
-
no ymmv profile for "${sanitizeValue(handle)}" yet.
|
|
762
|
-
publish one at ymmv.fyi with: npx ymmv-cli
|
|
763
|
-
`;
|
|
784
|
+
const wanted = target.label.trim().toLowerCase();
|
|
785
|
+
const hit = extras.find((x) => x.label.trim().toLowerCase() === wanted);
|
|
786
|
+
if (!hit) return { entries, extras, removed: null };
|
|
787
|
+
return {
|
|
788
|
+
entries,
|
|
789
|
+
extras: extras.filter((x) => x.label.trim().toLowerCase() !== wanted),
|
|
790
|
+
removed: { label: hit.label, value: hit.value }
|
|
791
|
+
};
|
|
764
792
|
}
|
|
765
793
|
|
|
766
794
|
// src/commands.ts
|
|
@@ -775,6 +803,13 @@ function requireHandle(cred) {
|
|
|
775
803
|
process.exitCode = 1;
|
|
776
804
|
return null;
|
|
777
805
|
}
|
|
806
|
+
function assertHandleUnchanged(existing, handle) {
|
|
807
|
+
if (existing && existing.handle.toLowerCase() !== handle.toLowerCase()) {
|
|
808
|
+
throw new Error(
|
|
809
|
+
`this login is bound to "${handle}" but your profile now lives at "${sanitizeValue(existing.handle)}" \u2014 run \`ymmv login\` to refresh, then retry.`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
778
813
|
function newProfile(handle, entries, extras) {
|
|
779
814
|
return {
|
|
780
815
|
schema_version: SCHEMA_VERSION,
|
|
@@ -799,6 +834,7 @@ async function publish(io) {
|
|
|
799
834
|
if (!handle) return;
|
|
800
835
|
const detected = detectStack(process.env, process.platform);
|
|
801
836
|
const existing = await fetchProfileJson(handle);
|
|
837
|
+
assertHandleUnchanged(existing, handle);
|
|
802
838
|
const defaults = buildDefaults(existing, detected);
|
|
803
839
|
let entries = entriesFromMap(defaults);
|
|
804
840
|
const extras = existing?.extras ?? [];
|
|
@@ -845,6 +881,7 @@ async function runSet(target) {
|
|
|
845
881
|
const handle = requireHandle(cred);
|
|
846
882
|
if (!handle) return;
|
|
847
883
|
const existing = await fetchProfileJson(handle);
|
|
884
|
+
assertHandleUnchanged(existing, handle);
|
|
848
885
|
const { entries, extras } = applySet(existing, target);
|
|
849
886
|
await publishProfile(newProfile(handle, entries, extras));
|
|
850
887
|
if (target.kind === "curated") {
|
|
@@ -853,6 +890,32 @@ async function runSet(target) {
|
|
|
853
890
|
console.log(`Set extra ${target.label} = ${target.value}.`);
|
|
854
891
|
}
|
|
855
892
|
}
|
|
893
|
+
async function runUnset(target) {
|
|
894
|
+
const cred = await ensureLogin();
|
|
895
|
+
const handle = requireHandle(cred);
|
|
896
|
+
if (!handle) return;
|
|
897
|
+
const existing = await fetchProfileJson(handle);
|
|
898
|
+
assertHandleUnchanged(existing, handle);
|
|
899
|
+
if (!existing) {
|
|
900
|
+
console.log("No profile yet \u2014 run `ymmv` to publish one.");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const { entries, extras, removed } = applyUnset(existing, target);
|
|
904
|
+
if (!removed) {
|
|
905
|
+
console.log(
|
|
906
|
+
target.kind === "curated" ? `${KEY_LABELS[target.key]} is not set.` : `No extra "${target.label}".`
|
|
907
|
+
);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
await publishProfile(newProfile(handle, entries, extras));
|
|
911
|
+
if (target.kind === "curated") {
|
|
912
|
+
console.log(`Removed ${KEY_LABELS[target.key]} (was "${sanitizeValue(removed.value)}").`);
|
|
913
|
+
} else {
|
|
914
|
+
console.log(
|
|
915
|
+
`Removed extra "${sanitizeValue(removed.label)}" (was "${sanitizeValue(removed.value)}").`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
856
919
|
async function runDelete(io) {
|
|
857
920
|
const cred = await ensureLogin();
|
|
858
921
|
const target = cred.handle ? `ymmv.fyi/${cred.handle}` : "your profile";
|
|
@@ -902,8 +965,18 @@ function makePrompter() {
|
|
|
902
965
|
}
|
|
903
966
|
|
|
904
967
|
// src/resolve.ts
|
|
905
|
-
var
|
|
906
|
-
var
|
|
968
|
+
var SET_EXTRA = 'ymmv set --extra "Label=Value"';
|
|
969
|
+
var UNSET_EXTRA = 'ymmv unset --extra "Label"';
|
|
970
|
+
var SET_USAGE = `usage: ymmv set <key> <value> | ${SET_EXTRA}`;
|
|
971
|
+
var EXTRA_USAGE = `usage: ${SET_EXTRA}`;
|
|
972
|
+
var UNSET_USAGE = `usage: ymmv unset <key> | ${UNSET_EXTRA}`;
|
|
973
|
+
function invalidKeyError(head, hint) {
|
|
974
|
+
return {
|
|
975
|
+
kind: "error",
|
|
976
|
+
message: `"${head}" is not a curated key. Valid keys: ${CURATED_KEYS.join(", ")}.
|
|
977
|
+
For anything else, use: ${hint}.`
|
|
978
|
+
};
|
|
979
|
+
}
|
|
907
980
|
function hasYes(args) {
|
|
908
981
|
return args.includes("-y") || args.includes("--yes");
|
|
909
982
|
}
|
|
@@ -916,20 +989,34 @@ function parseSet(rest) {
|
|
|
916
989
|
const label = spec.slice(0, eq).trim();
|
|
917
990
|
const value2 = spec.slice(eq + 1).trim();
|
|
918
991
|
if (!label || !value2) return { kind: "error", message: EXTRA_USAGE };
|
|
992
|
+
if (value2 === "-") return { kind: "unset", target: { kind: "extra", label } };
|
|
919
993
|
return { kind: "set", target: { kind: "extra", label, value: value2 } };
|
|
920
994
|
}
|
|
921
995
|
if (!head) return { kind: "error", message: SET_USAGE };
|
|
922
|
-
if (!isCuratedKey(head))
|
|
923
|
-
return {
|
|
924
|
-
kind: "error",
|
|
925
|
-
message: `"${head}" is not a curated key. Valid keys: ${CURATED_KEYS.join(", ")}.
|
|
926
|
-
For anything else, use: ymmv set --extra "Label=Value".`
|
|
927
|
-
};
|
|
928
|
-
}
|
|
996
|
+
if (!isCuratedKey(head)) return invalidKeyError(head, SET_EXTRA);
|
|
929
997
|
const value = rest.slice(1).join(" ").trim();
|
|
930
998
|
if (!value) return { kind: "error", message: `usage: ymmv set ${head} <value>` };
|
|
999
|
+
if (value === "-") return { kind: "unset", target: { kind: "curated", key: head } };
|
|
931
1000
|
return { kind: "set", target: { kind: "curated", key: head, value } };
|
|
932
1001
|
}
|
|
1002
|
+
function parseUnset(rest) {
|
|
1003
|
+
const head = rest[0];
|
|
1004
|
+
if (head === "--extra" || head === "-e") {
|
|
1005
|
+
const label = rest.slice(1).join(" ").trim();
|
|
1006
|
+
if (!label) return { kind: "error", message: `usage: ${UNSET_EXTRA}` };
|
|
1007
|
+
if (label.includes("=")) {
|
|
1008
|
+
return {
|
|
1009
|
+
kind: "error",
|
|
1010
|
+
message: 'unset takes just the label: ymmv unset --extra "Keyboard"'
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
return { kind: "unset", target: { kind: "extra", label } };
|
|
1014
|
+
}
|
|
1015
|
+
if (!head) return { kind: "error", message: UNSET_USAGE };
|
|
1016
|
+
if (!isCuratedKey(head)) return invalidKeyError(head, UNSET_EXTRA);
|
|
1017
|
+
if (rest.length > 1) return { kind: "error", message: `usage: ymmv unset ${head}` };
|
|
1018
|
+
return { kind: "unset", target: { kind: "curated", key: head } };
|
|
1019
|
+
}
|
|
933
1020
|
function resolveArg(argv) {
|
|
934
1021
|
const first = argv[0];
|
|
935
1022
|
if (first === "-h" || first === "--help" || first === "help") return { kind: "help" };
|
|
@@ -940,6 +1027,7 @@ function resolveArg(argv) {
|
|
|
940
1027
|
if (first === "logout") return { kind: "logout" };
|
|
941
1028
|
if (first === "delete") return { kind: "delete", yes: hasYes(argv.slice(1)) };
|
|
942
1029
|
if (first === "set") return parseSet(argv.slice(1));
|
|
1030
|
+
if (first === "unset") return parseUnset(argv.slice(1));
|
|
943
1031
|
if (first === "view") {
|
|
944
1032
|
const handle = argv[1];
|
|
945
1033
|
if (!handle) return { kind: "error", message: "usage: ymmv view <handle>" };
|
|
@@ -969,6 +1057,8 @@ Usage:
|
|
|
969
1057
|
ymmv view <handle> explicit view (when a handle collides with a verb)
|
|
970
1058
|
ymmv set <key> <value> set one curated key
|
|
971
1059
|
ymmv set --extra "L=V" set a free-form extra
|
|
1060
|
+
ymmv unset <key> remove one curated key (ymmv set <key> - works too)
|
|
1061
|
+
ymmv unset --extra "L" remove a free-form extra
|
|
972
1062
|
ymmv delete delete your profile (permanent)
|
|
973
1063
|
ymmv login | logout GitHub device-flow auth
|
|
974
1064
|
ymmv help | --version
|
|
@@ -1029,6 +1119,9 @@ async function main(argv) {
|
|
|
1029
1119
|
case "set":
|
|
1030
1120
|
await runSet(cmd.target);
|
|
1031
1121
|
break;
|
|
1122
|
+
case "unset":
|
|
1123
|
+
await runUnset(cmd.target);
|
|
1124
|
+
break;
|
|
1032
1125
|
case "delete":
|
|
1033
1126
|
await interactive(runDelete, cmd.yes);
|
|
1034
1127
|
break;
|