ymmv-cli 0.3.0 → 0.5.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 +29 -11
- package/dist/cli.js +505 -160
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,33 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
**Share your dev tool-stack from the terminal.**
|
|
4
4
|
|
|
5
|
-
Editor, OS, shell, terminal,
|
|
6
|
-
page at `ymmv.fyi/<handle>` in about 10 seconds.
|
|
5
|
+
Editor, OS, shell, terminal, theme, AI tool (and more), published to a
|
|
6
|
+
clean page at `ymmv.fyi/<handle>` in about 10 seconds. See a live one:
|
|
7
|
+
[ymmv.fyi/bardisty](https://ymmv.fyi/bardisty).
|
|
8
|
+
|
|
9
|
+

|
|
7
10
|
|
|
8
11
|
```sh
|
|
9
12
|
npx ymmv-cli # detect your stack, confirm, go live at ymmv.fyi/<you>
|
|
10
13
|
npx ymmv-cli bardisty # view someone's stack in the terminal
|
|
11
14
|
```
|
|
12
15
|
|
|
16
|
+
Viewing someone while you're logged in diffs their stack against yours:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
bardisty you
|
|
20
|
+
~ Editor Zed VS Code
|
|
21
|
+
= Shell bash bash
|
|
22
|
+
~ Theme Gruvbox Catppuccin
|
|
23
|
+
~ Font Lilex JetBrains Mono
|
|
24
|
+
|
|
25
|
+
3 differ · 1 shared — your mileage may vary
|
|
26
|
+
```
|
|
27
|
+
|
|
13
28
|
Install once for the short `ymmv` command:
|
|
14
29
|
|
|
15
30
|
```sh
|
|
16
31
|
npm i -g ymmv-cli
|
|
17
32
|
```
|
|
18
33
|
|
|
19
|
-
First run includes a one-time GitHub sign-in
|
|
20
|
-
|
|
34
|
+
First run includes a one-time GitHub sign-in. Works on macOS, Linux, Windows,
|
|
35
|
+
and WSL. Nothing is published until you confirm (detection only pre-fills), and
|
|
36
|
+
`ymmv delete` removes everything. Releases are published from GitHub Actions
|
|
37
|
+
via npm Trusted Publishing, with provenance.
|
|
21
38
|
|
|
22
39
|
## Commands
|
|
23
40
|
|
|
24
|
-
- `ymmv`
|
|
25
|
-
- `ymmv <handle>`
|
|
26
|
-
- `ymmv set editor Neovim`
|
|
27
|
-
- `ymmv set --extra "Keyboard=HHKB"`
|
|
28
|
-
- `ymmv
|
|
29
|
-
- `ymmv
|
|
41
|
+
- `ymmv` detects, confirms, and publishes (re-run any time to update)
|
|
42
|
+
- `ymmv <handle>` views a profile, or diffs it against yours when you're logged in
|
|
43
|
+
- `ymmv set editor Neovim` changes one value
|
|
44
|
+
- `ymmv set --extra "Keyboard=HHKB"` adds a free-form line of your own
|
|
45
|
+
- `ymmv unset editor` removes one value (`ymmv set editor -` works too); `ymmv unset --extra "Keyboard"` removes an extra
|
|
46
|
+
- `ymmv delete` removes your profile
|
|
47
|
+
- `ymmv login` / `ymmv logout` sign in / out
|
|
30
48
|
|
|
31
|
-
Every profile is open JSON too
|
|
49
|
+
Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`.
|
|
32
50
|
|
|
33
51
|
## License
|
|
34
52
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync } from "fs";
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
var BASE = (process.env.YMMV_API ?? "https://ymmv.fyi").replace(/\/+$/, "");
|
|
@@ -44,16 +44,22 @@ async function revokeYmmvToken(token) {
|
|
|
44
44
|
return body.revoked === true;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// src/commands.ts
|
|
48
|
+
import { readFileSync, statSync } from "fs";
|
|
49
|
+
|
|
47
50
|
// ../shared/dist/keys.js
|
|
48
51
|
var CURATED_KEYS = [
|
|
49
52
|
"editor",
|
|
50
53
|
"os",
|
|
51
54
|
"shell",
|
|
55
|
+
"prompt",
|
|
52
56
|
"terminal",
|
|
53
57
|
"browser",
|
|
54
58
|
"window-manager",
|
|
55
59
|
"font",
|
|
60
|
+
"theme",
|
|
56
61
|
"multiplexer",
|
|
62
|
+
"version-manager",
|
|
57
63
|
"dotfiles",
|
|
58
64
|
"ai-tool"
|
|
59
65
|
];
|
|
@@ -61,11 +67,14 @@ var KEY_LABELS = {
|
|
|
61
67
|
editor: "Editor",
|
|
62
68
|
os: "OS",
|
|
63
69
|
shell: "Shell",
|
|
70
|
+
prompt: "Prompt",
|
|
64
71
|
terminal: "Terminal",
|
|
65
72
|
browser: "Browser",
|
|
66
73
|
"window-manager": "Window Manager",
|
|
67
74
|
font: "Font",
|
|
75
|
+
theme: "Theme",
|
|
68
76
|
multiplexer: "Multiplexer",
|
|
77
|
+
"version-manager": "Version Manager",
|
|
69
78
|
dotfiles: "Dotfiles",
|
|
70
79
|
"ai-tool": "AI Tool"
|
|
71
80
|
};
|
|
@@ -122,16 +131,95 @@ var TOOLS = [
|
|
|
122
131
|
{ key: "terminal", canonical: "Hyper", envTokens: ["hyper"] },
|
|
123
132
|
{ key: "terminal", canonical: "Rio", envTokens: ["rio"] },
|
|
124
133
|
{ key: "terminal", canonical: "kitty", envTokens: ["kitty"] },
|
|
134
|
+
// kitty refuses TERM_PROGRAM (its issue #3317); detect.ts probes KITTY_WINDOW_ID instead — token kept for the day that changes
|
|
125
135
|
{ key: "terminal", canonical: "Tabby", envTokens: ["tabby"] },
|
|
126
136
|
{ key: "terminal", canonical: "Warp", envTokens: ["warpterminal"] },
|
|
127
137
|
// `warpterminal` env id ≠ typed "Warp"
|
|
138
|
+
{ key: "terminal", canonical: "GNOME Console", envTokens: ["kgx"] },
|
|
139
|
+
// $TERM_PROGRAM=kgx since GNOME 45
|
|
140
|
+
{ key: "terminal", canonical: "mintty", envTokens: ["mintty"] },
|
|
141
|
+
// Git Bash/MSYS2 console, ≥3.1.5
|
|
142
|
+
{ key: "terminal", canonical: "Zed", envTokens: ["zed"] },
|
|
143
|
+
// Zed's built-in terminal (~0.145+)
|
|
144
|
+
// ── browser (envTokens mirror BROWSER_NAMES: $BROWSER basenames) ──────────
|
|
145
|
+
{ key: "browser", canonical: "Firefox", envTokens: ["firefox"], aliases: ["ff"] },
|
|
146
|
+
// Hyphenated Linux binary names double as diff aliases (kde/plasma precedent): unlike `vi`,
|
|
147
|
+
// each unambiguously names exactly that browser, so a TYPED "google-chrome" should diff-equal
|
|
148
|
+
// a DETECTED "Chrome".
|
|
149
|
+
{
|
|
150
|
+
key: "browser",
|
|
151
|
+
canonical: "Chrome",
|
|
152
|
+
envTokens: ["chrome", "google-chrome"],
|
|
153
|
+
aliases: ["google chrome", "google-chrome"]
|
|
154
|
+
// NOT Chromium — distinct browser
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: "browser",
|
|
158
|
+
canonical: "Edge",
|
|
159
|
+
envTokens: ["msedge", "microsoft-edge"],
|
|
160
|
+
aliases: ["microsoft edge", "microsoft-edge"]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: "browser",
|
|
164
|
+
canonical: "Chromium",
|
|
165
|
+
envTokens: ["chromium", "chromium-browser"],
|
|
166
|
+
aliases: ["chromium-browser"]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: "browser",
|
|
170
|
+
canonical: "Brave",
|
|
171
|
+
envTokens: ["brave", "brave-browser"],
|
|
172
|
+
aliases: ["brave-browser"]
|
|
173
|
+
},
|
|
174
|
+
{ key: "browser", canonical: "LibreWolf", envTokens: ["librewolf"] },
|
|
175
|
+
{
|
|
176
|
+
key: "browser",
|
|
177
|
+
canonical: "Vivaldi",
|
|
178
|
+
envTokens: ["vivaldi", "vivaldi-stable"],
|
|
179
|
+
aliases: ["vivaldi-stable"]
|
|
180
|
+
},
|
|
181
|
+
{ key: "browser", canonical: "Zen", envTokens: ["zen", "zen-browser"], aliases: ["zen-browser"] },
|
|
182
|
+
{ key: "browser", canonical: "qutebrowser", envTokens: ["qutebrowser"] },
|
|
183
|
+
// ── window-manager (envTokens mirror WM_NAMES: XDG_CURRENT_DESKTOP list entries +
|
|
184
|
+
// DESKTOP_SESSION basenames, lowercased). Deliberate: canonical names are what users put on
|
|
185
|
+
// profiles — the DE name (GNOME, KDE Plasma), not its compositor (Mutter, KWin). Compositors
|
|
186
|
+
// with their own socket vars (Hyprland/Sway/i3/niri) are ALSO probed directly in detect.ts;
|
|
187
|
+
// WMs with no env footprint at all (river, bspwm, dwm, Qtile, awesome) have no tokens — a
|
|
188
|
+
// dead token would only inflate the pinned detector fixtures. ──────────
|
|
189
|
+
{ key: "window-manager", canonical: "GNOME", envTokens: ["gnome"] },
|
|
190
|
+
{
|
|
191
|
+
key: "window-manager",
|
|
192
|
+
canonical: "KDE Plasma",
|
|
193
|
+
envTokens: ["kde", "plasma"],
|
|
194
|
+
aliases: ["kde", "plasma"]
|
|
195
|
+
},
|
|
196
|
+
{ key: "window-manager", canonical: "Hyprland", envTokens: ["hyprland"] },
|
|
197
|
+
{ key: "window-manager", canonical: "Sway", envTokens: ["sway"] },
|
|
198
|
+
{ key: "window-manager", canonical: "i3", envTokens: ["i3"] },
|
|
199
|
+
{ key: "window-manager", canonical: "niri", envTokens: ["niri"] },
|
|
200
|
+
{ key: "window-manager", canonical: "COSMIC", envTokens: ["cosmic"] },
|
|
201
|
+
{ key: "window-manager", canonical: "Cinnamon", envTokens: ["cinnamon", "x-cinnamon"] },
|
|
202
|
+
{ key: "window-manager", canonical: "MATE", envTokens: ["mate"] },
|
|
203
|
+
{ key: "window-manager", canonical: "XFCE", envTokens: ["xfce"] },
|
|
204
|
+
{ key: "window-manager", canonical: "LXQt", envTokens: ["lxqt"] },
|
|
205
|
+
{ key: "window-manager", canonical: "Budgie", envTokens: ["budgie"] },
|
|
206
|
+
{ key: "window-manager", canonical: "Pantheon", envTokens: ["pantheon"] },
|
|
207
|
+
{ key: "window-manager", canonical: "Unity", envTokens: ["unity"] },
|
|
208
|
+
// ── prompt (presence-detected in detect.ts — entries exist for diff aliases only; fold
|
|
209
|
+
// already equates case/space variants, so single-word tools need no entry) ──
|
|
210
|
+
{ key: "prompt", canonical: "Powerlevel10k", aliases: ["p10k"] },
|
|
211
|
+
// NOT p9k — distinct predecessor
|
|
212
|
+
{ key: "prompt", canonical: "Oh My Posh", aliases: ["oh-my-posh", "omp"] },
|
|
213
|
+
// hyphens survive fold
|
|
214
|
+
// ── theme (never detected — aliases for spellings fold can't equate: hyphens + the é) ──
|
|
215
|
+
{ key: "theme", canonical: "Ros\xE9 Pine", aliases: ["rose pine", "rose-pine"] },
|
|
216
|
+
{ key: "theme", canonical: "Tokyo Night", aliases: ["tokyo-night"] },
|
|
217
|
+
{ key: "theme", canonical: "One Dark", aliases: ["one-dark"] },
|
|
128
218
|
// ── diff-only synonyms (no detector for these fields) ─────────────────────
|
|
129
|
-
{ key: "browser", canonical: "Firefox", aliases: ["ff"] },
|
|
130
|
-
{ key: "browser", canonical: "Chrome", aliases: ["google chrome"] },
|
|
131
|
-
// NOT Chromium — distinct browser
|
|
132
|
-
{ key: "browser", canonical: "Edge", aliases: ["microsoft edge"] },
|
|
133
219
|
{ key: "os", canonical: "macOS", aliases: ["osx", "os x", "mac"] },
|
|
134
220
|
{ key: "os", canonical: "Windows", aliases: ["win"] },
|
|
221
|
+
{ key: "os", canonical: "Arch Linux", aliases: ["arch"] },
|
|
222
|
+
// os-release NAME= vs what people type
|
|
135
223
|
{ key: "multiplexer", canonical: "GNU Screen", aliases: ["screen"] }
|
|
136
224
|
// matches detector's "GNU Screen"
|
|
137
225
|
];
|
|
@@ -284,7 +372,7 @@ function parseProfile(raw) {
|
|
|
284
372
|
|
|
285
373
|
// ../shared/dist/reserved.js
|
|
286
374
|
var RESERVED_ROUTES = ["api", "login", "logout"];
|
|
287
|
-
var CLI_VERBS = ["login", "logout", "set", "delete", "view", "help"];
|
|
375
|
+
var CLI_VERBS = ["login", "logout", "set", "unset", "delete", "view", "help"];
|
|
288
376
|
var RESERVED = [.../* @__PURE__ */ new Set([...RESERVED_ROUTES, ...CLI_VERBS])];
|
|
289
377
|
var RESERVED_SET = new Set(RESERVED);
|
|
290
378
|
var HANDLE_RE = /^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$/;
|
|
@@ -440,6 +528,136 @@ async function login(deps = {}) {
|
|
|
440
528
|
);
|
|
441
529
|
}
|
|
442
530
|
|
|
531
|
+
// src/render.ts
|
|
532
|
+
var ESC = String.fromCharCode(27);
|
|
533
|
+
var CSI = `${ESC}[`;
|
|
534
|
+
var CODES = {
|
|
535
|
+
amber: `${CSI}93m`,
|
|
536
|
+
// DESIGN: amber == ANSI bright-yellow
|
|
537
|
+
faint: `${CSI}90m`,
|
|
538
|
+
bold: `${CSI}1m`,
|
|
539
|
+
reset: `${CSI}0m`
|
|
540
|
+
};
|
|
541
|
+
var NO_CODES = { amber: "", faint: "", bold: "", reset: "" };
|
|
542
|
+
function palette(color) {
|
|
543
|
+
return color ? CODES : NO_CODES;
|
|
544
|
+
}
|
|
545
|
+
var ESC_INTRODUCERS = `${String.fromCharCode(27)}${String.fromCharCode(155)}`;
|
|
546
|
+
var BEL = String.fromCharCode(7);
|
|
547
|
+
var ANSI_RE = new RegExp(
|
|
548
|
+
`[${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=><~]))`,
|
|
549
|
+
"g"
|
|
550
|
+
);
|
|
551
|
+
var CTRL_RE = new RegExp(
|
|
552
|
+
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
|
|
553
|
+
"g"
|
|
554
|
+
);
|
|
555
|
+
var BIDI_RE = new RegExp(
|
|
556
|
+
`[${String.fromCharCode(8234)}-${String.fromCharCode(8238)}${String.fromCharCode(8294)}-${String.fromCharCode(8297)}${String.fromCharCode(8206)}${String.fromCharCode(8207)}]`,
|
|
557
|
+
"g"
|
|
558
|
+
);
|
|
559
|
+
function sanitizeValue(value) {
|
|
560
|
+
return value.replace(ANSI_RE, "").replace(CTRL_RE, "").replace(BIDI_RE, "");
|
|
561
|
+
}
|
|
562
|
+
function useColor(env, isTTY) {
|
|
563
|
+
if (env.NO_COLOR !== void 0) return false;
|
|
564
|
+
if (env.FORCE_COLOR !== void 0) return env.FORCE_COLOR !== "0";
|
|
565
|
+
return isTTY;
|
|
566
|
+
}
|
|
567
|
+
function orderedEntries(profile) {
|
|
568
|
+
const byKey = new Map(
|
|
569
|
+
(profile.entries ?? []).map((e) => [e.key, e.value])
|
|
570
|
+
);
|
|
571
|
+
return CURATED_KEYS.flatMap((key) => {
|
|
572
|
+
const value = byKey.get(key);
|
|
573
|
+
return value === void 0 ? [] : [{ label: KEY_LABELS[key], value: sanitizeValue(value) }];
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
function renderProfile(profile, opts) {
|
|
577
|
+
const c = palette(opts.color);
|
|
578
|
+
const rows = orderedEntries(profile);
|
|
579
|
+
const extras = (profile.extras ?? []).map((x) => ({
|
|
580
|
+
label: sanitizeValue(x.label),
|
|
581
|
+
value: sanitizeValue(x.value)
|
|
582
|
+
}));
|
|
583
|
+
const labelW = Math.max(
|
|
584
|
+
0,
|
|
585
|
+
...rows.map((r) => r.label.length),
|
|
586
|
+
...extras.map((x) => x.label.length)
|
|
587
|
+
);
|
|
588
|
+
const lines = ["", ` ${c.bold}${sanitizeValue(profile.handle)}${c.reset}`, ""];
|
|
589
|
+
for (const r of rows) {
|
|
590
|
+
lines.push(` ${c.faint}${r.label.padEnd(labelW)}${c.reset} ${r.value}`);
|
|
591
|
+
}
|
|
592
|
+
if (extras.length) {
|
|
593
|
+
lines.push("");
|
|
594
|
+
for (const x of extras) {
|
|
595
|
+
lines.push(` ${c.faint}${x.label.padEnd(labelW)}${c.reset} ${x.value}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
lines.push("", ` ${c.faint}updated ${sanitizeValue(profile.updated_at)}${c.reset}`, "");
|
|
599
|
+
return lines.join("\n");
|
|
600
|
+
}
|
|
601
|
+
var MISSING = "\u2014";
|
|
602
|
+
function extrasBlock(extras, theirsLabel, mineLabel, c) {
|
|
603
|
+
if (!extras.theirs.length && !extras.mine.length) return [];
|
|
604
|
+
const out = ["", ` ${c.faint}extras${c.reset}`];
|
|
605
|
+
const line = (who, label, value) => ` ${c.faint}${who}${c.reset} ${sanitizeValue(label)} = ${sanitizeValue(value)}`;
|
|
606
|
+
for (const x of extras.theirs) out.push(line(theirsLabel, x.label, x.value));
|
|
607
|
+
for (const x of extras.mine) out.push(line(mineLabel, x.label, x.value));
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
function renderDiff(result, opts) {
|
|
611
|
+
const c = palette(opts.color);
|
|
612
|
+
const theirsLabel = sanitizeValue(opts.theirsLabel);
|
|
613
|
+
const mineLabel = sanitizeValue(opts.mineLabel);
|
|
614
|
+
const cells = result.rows.map((r) => ({
|
|
615
|
+
label: r.label,
|
|
616
|
+
theirs: r.theirs === null ? MISSING : sanitizeValue(r.theirs),
|
|
617
|
+
mine: r.mine === null ? MISSING : sanitizeValue(r.mine),
|
|
618
|
+
differ: r.status !== "same"
|
|
619
|
+
}));
|
|
620
|
+
const labelW = Math.max(3, ...cells.map((r) => r.label.length));
|
|
621
|
+
const theirsW = Math.max(theirsLabel.length, ...cells.map((r) => r.theirs.length));
|
|
622
|
+
const lines = [""];
|
|
623
|
+
lines.push(
|
|
624
|
+
` ${c.faint}${"".padEnd(labelW)} ${theirsLabel.padEnd(theirsW)} ${mineLabel}${c.reset}`
|
|
625
|
+
);
|
|
626
|
+
for (const r of cells) {
|
|
627
|
+
if (!opts.color) {
|
|
628
|
+
const sym = r.differ ? "~" : "=";
|
|
629
|
+
lines.push(`${sym} ${r.label.padEnd(labelW)} ${r.theirs.padEnd(theirsW)} ${r.mine}`);
|
|
630
|
+
} else if (r.differ) {
|
|
631
|
+
lines.push(
|
|
632
|
+
`${c.amber}\u2022${c.reset} ${r.label.padEnd(labelW)} ${c.amber}${r.theirs.padEnd(theirsW)}${c.reset} ${c.amber}${r.mine}${c.reset}`
|
|
633
|
+
);
|
|
634
|
+
} else {
|
|
635
|
+
lines.push(
|
|
636
|
+
` ${c.faint}${r.label.padEnd(labelW)} ${r.theirs.padEnd(theirsW)} ${r.mine}${c.reset}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
lines.push(...extrasBlock(result.extras, theirsLabel, mineLabel, c));
|
|
641
|
+
lines.push(
|
|
642
|
+
"",
|
|
643
|
+
` ${c.faint}${result.differ} differ \xB7 ${result.shared} shared \u2014 your mileage may vary${c.reset}`,
|
|
644
|
+
""
|
|
645
|
+
);
|
|
646
|
+
return lines.join("\n");
|
|
647
|
+
}
|
|
648
|
+
function nudge(color) {
|
|
649
|
+
const c = palette(color);
|
|
650
|
+
return `
|
|
651
|
+
${c.amber}publish yours to diff \u2192${c.reset} run ${c.bold}ymmv${c.reset}
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
function notFound(handle) {
|
|
655
|
+
return `
|
|
656
|
+
no ymmv profile for "${sanitizeValue(handle)}" yet.
|
|
657
|
+
publish one at ymmv.fyi with: npx ymmv-cli
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
|
|
443
661
|
// src/api.ts
|
|
444
662
|
async function rateLimitMessage(res) {
|
|
445
663
|
const retry = res.headers.get("retry-after");
|
|
@@ -470,6 +688,11 @@ async function publishProfile(profile) {
|
|
|
470
688
|
// a mutation must never follow a redirect into a false success
|
|
471
689
|
});
|
|
472
690
|
let cred = await ensureLogin();
|
|
691
|
+
if ((cred.handle ?? "").toLowerCase() !== profile.handle.toLowerCase()) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
"the stored login changed while this command was running \u2014 re-run it under the current account."
|
|
694
|
+
);
|
|
695
|
+
}
|
|
473
696
|
let res = await send(cred);
|
|
474
697
|
if (res.status === 401 || res.status === 409) {
|
|
475
698
|
if (res.status === 401) await deleteToken();
|
|
@@ -488,7 +711,8 @@ async function publishProfile(profile) {
|
|
|
488
711
|
throw new Error(`publish failed: ${res.status} ${await res.text()}`);
|
|
489
712
|
}
|
|
490
713
|
const data = await res.json();
|
|
491
|
-
|
|
714
|
+
const shown = typeof data.handle === "string" ? sanitizeValue(data.handle) : profile.handle;
|
|
715
|
+
console.log(`Published ${shown} -> ${BASE}/${shown}`);
|
|
492
716
|
}
|
|
493
717
|
async function fetchProfileJson(handle) {
|
|
494
718
|
const res = await fetch(`${BASE}/api/v1/u/${encodeURIComponent(handle)}`);
|
|
@@ -525,7 +749,7 @@ function firstToken(s) {
|
|
|
525
749
|
return s.trim().split(/\s+/)[0] ?? "";
|
|
526
750
|
}
|
|
527
751
|
var namesFor = (key) => {
|
|
528
|
-
const map =
|
|
752
|
+
const map = /* @__PURE__ */ Object.create(null);
|
|
529
753
|
for (const tool of TOOLS) {
|
|
530
754
|
if (tool.key !== key) continue;
|
|
531
755
|
for (const token of tool.envTokens ?? []) map[token] = tool.canonical;
|
|
@@ -535,15 +759,44 @@ var namesFor = (key) => {
|
|
|
535
759
|
var SHELL_NAMES = namesFor("shell");
|
|
536
760
|
var EDITOR_NAMES = namesFor("editor");
|
|
537
761
|
var TERM_PROGRAMS = namesFor("terminal");
|
|
538
|
-
|
|
539
|
-
|
|
762
|
+
var BROWSER_NAMES = namesFor("browser");
|
|
763
|
+
var WM_NAMES = namesFor("window-manager");
|
|
764
|
+
var TERM_EXACT = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
765
|
+
"xterm-kitty": "kitty",
|
|
766
|
+
"xterm-ghostty": "Ghostty",
|
|
767
|
+
foot: "foot",
|
|
768
|
+
alacritty: "Alacritty"
|
|
769
|
+
});
|
|
770
|
+
var BROWSER_DISPATCHERS = /* @__PURE__ */ new Set([
|
|
771
|
+
"xdg-open",
|
|
772
|
+
"wslview",
|
|
773
|
+
"sensible-browser",
|
|
774
|
+
"x-www-browser",
|
|
775
|
+
"www-browser",
|
|
776
|
+
"open",
|
|
777
|
+
"gio",
|
|
778
|
+
"browser.sh",
|
|
779
|
+
"browser.cmd"
|
|
780
|
+
]);
|
|
781
|
+
function linuxDistro(read) {
|
|
782
|
+
if (!read) return void 0;
|
|
783
|
+
try {
|
|
784
|
+
const line = read("/etc/os-release").split("\n").find((l) => l.startsWith("NAME="));
|
|
785
|
+
const name = line?.slice("NAME=".length).trim().replace(/^["']|["']$/g, "");
|
|
786
|
+
return name?.trim() || void 0;
|
|
787
|
+
} catch {
|
|
788
|
+
return void 0;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function detectOS(env, platform, opts) {
|
|
792
|
+
if (platform === "linux" && env.WSL_DISTRO_NAME) return `${env.WSL_DISTRO_NAME} (WSL)`;
|
|
540
793
|
switch (platform) {
|
|
541
794
|
case "darwin":
|
|
542
795
|
return "macOS";
|
|
543
796
|
case "win32":
|
|
544
797
|
return "Windows";
|
|
545
798
|
case "linux":
|
|
546
|
-
return "Linux";
|
|
799
|
+
return linuxDistro(opts.readTextFile) ?? "Linux";
|
|
547
800
|
default:
|
|
548
801
|
return platform;
|
|
549
802
|
}
|
|
@@ -555,6 +808,9 @@ function detectShell(env, platform) {
|
|
|
555
808
|
return SHELL_NAMES[key] ?? basename(raw);
|
|
556
809
|
}
|
|
557
810
|
if (platform === "win32") {
|
|
811
|
+
const firstSeg = env.PSModulePath?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
812
|
+
if (/[\\/]documents[\\/](windows)?powershell[\\/]modules$/.test(firstSeg)) return "PowerShell";
|
|
813
|
+
if (env.PROMPT) return env.ComSpec ? basename(env.ComSpec) : "cmd";
|
|
558
814
|
if (env.PSModulePath) return "PowerShell";
|
|
559
815
|
if (env.ComSpec) return basename(env.ComSpec);
|
|
560
816
|
}
|
|
@@ -563,18 +819,37 @@ function detectShell(env, platform) {
|
|
|
563
819
|
function detectTerminal(env) {
|
|
564
820
|
const tp = env.TERM_PROGRAM?.trim();
|
|
565
821
|
if (tp && tp.toLowerCase() !== "tmux") {
|
|
822
|
+
if (tp.toLowerCase() === "vscode" && env.CURSOR_TRACE_ID) return "Cursor";
|
|
566
823
|
return TERM_PROGRAMS[tp.toLowerCase()] ?? tp;
|
|
567
824
|
}
|
|
825
|
+
if (env.TERMINAL_EMULATOR === "JetBrains-JediTerm") return "JetBrains";
|
|
568
826
|
if (env.WT_SESSION) return "Windows Terminal";
|
|
569
827
|
if (env.KONSOLE_VERSION) return "Konsole";
|
|
570
828
|
if (env.ALACRITTY_WINDOW_ID || env.ALACRITTY_SOCKET) return "Alacritty";
|
|
829
|
+
if (env.KITTY_WINDOW_ID || env.KITTY_PID) return "kitty";
|
|
830
|
+
if (env.GNOME_TERMINAL_SERVICE || env.GNOME_TERMINAL_SCREEN) return "GNOME Terminal";
|
|
831
|
+
if (env.TILIX_ID) return "Tilix";
|
|
832
|
+
if (env.TERMINATOR_UUID) return "Terminator";
|
|
833
|
+
if (env.ConEmuANSI || env.ConEmuPID) return "ConEmu";
|
|
834
|
+
if (env.XTERM_VERSION) return "xterm";
|
|
835
|
+
const t = env.TERM?.trim().toLowerCase();
|
|
836
|
+
if (t && TERM_EXACT[t]) return TERM_EXACT[t];
|
|
571
837
|
return void 0;
|
|
572
838
|
}
|
|
573
839
|
function detectEditor(env) {
|
|
574
840
|
const raw = env.VISUAL ?? env.EDITOR;
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
|
|
841
|
+
if (raw) {
|
|
842
|
+
const bin = basename(firstToken(raw));
|
|
843
|
+
return EDITOR_NAMES[bin.toLowerCase()] ?? bin;
|
|
844
|
+
}
|
|
845
|
+
if (env.NVIM) return "Neovim";
|
|
846
|
+
if (env.VIM_TERMINAL) return "Vim";
|
|
847
|
+
if (env.INSIDE_EMACS) return "Emacs";
|
|
848
|
+
if (env.CURSOR_TRACE_ID) return "Cursor";
|
|
849
|
+
const tp = env.TERM_PROGRAM?.trim().toLowerCase();
|
|
850
|
+
if (tp === "vscode") return "VS Code";
|
|
851
|
+
if (tp === "zed" || env.ZED_TERM === "true") return "Zed";
|
|
852
|
+
return void 0;
|
|
578
853
|
}
|
|
579
854
|
function detectMultiplexer(env) {
|
|
580
855
|
if (env.TMUX) return "tmux";
|
|
@@ -582,17 +857,92 @@ function detectMultiplexer(env) {
|
|
|
582
857
|
if (env.STY) return "GNU Screen";
|
|
583
858
|
return void 0;
|
|
584
859
|
}
|
|
585
|
-
function
|
|
860
|
+
function detectPrompt(env) {
|
|
861
|
+
if (env.STARSHIP_SHELL || env.STARSHIP_SESSION_KEY) return "Starship";
|
|
862
|
+
if (env.POSH_SHELL || env.POSH_SESSION_ID || env.POSH_THEME) return "Oh My Posh";
|
|
863
|
+
if (env.P9K_TTY || env._P9K_TTY) return "Powerlevel10k";
|
|
864
|
+
if (env.SPACESHIP_VERSION) return "Spaceship";
|
|
865
|
+
if (Object.keys(env).some((k) => k.startsWith("_tide_"))) return "Tide";
|
|
866
|
+
return void 0;
|
|
867
|
+
}
|
|
868
|
+
function detectVersionManager(env) {
|
|
869
|
+
const path = env.PATH ?? "";
|
|
870
|
+
if (env.MISE_SHELL || env.__MISE_ORIG_PATH || /[\\/]mise[\\/]shims/i.test(path)) return "mise";
|
|
871
|
+
if (env.ASDF_DIR || env.ASDF_DATA_DIR || /\.asdf[\\/]shims/i.test(path)) return "asdf";
|
|
872
|
+
if (env.PROTO_HOME) return "proto";
|
|
873
|
+
if (env.VOLTA_HOME) return "Volta";
|
|
874
|
+
if (env.FNM_MULTISHELL_PATH || env.FNM_DIR) return "fnm";
|
|
875
|
+
if (env.NVM_DIR) return "nvm";
|
|
876
|
+
if (env.PYENV_SHELL || env.PYENV_ROOT) return "pyenv";
|
|
877
|
+
if (env.RBENV_SHELL || env.RBENV_ROOT) return "rbenv";
|
|
878
|
+
return void 0;
|
|
879
|
+
}
|
|
880
|
+
function detectWindowManager(env) {
|
|
881
|
+
if (env.HYPRLAND_INSTANCE_SIGNATURE) return "Hyprland";
|
|
882
|
+
if (env.NIRI_SOCKET) return "niri";
|
|
883
|
+
if (env.SWAYSOCK) return "Sway";
|
|
884
|
+
if (env.I3SOCK) return "i3";
|
|
885
|
+
for (const part of env.XDG_CURRENT_DESKTOP?.split(":") ?? []) {
|
|
886
|
+
const hit = WM_NAMES[part.trim().toLowerCase()];
|
|
887
|
+
if (hit) return hit;
|
|
888
|
+
}
|
|
889
|
+
const ds = env.DESKTOP_SESSION;
|
|
890
|
+
if (ds) {
|
|
891
|
+
const hit = WM_NAMES[basename(ds).toLowerCase()];
|
|
892
|
+
if (hit) return hit;
|
|
893
|
+
}
|
|
894
|
+
if (env.KDE_FULL_SESSION || env.KDE_SESSION_VERSION) return "KDE Plasma";
|
|
895
|
+
return void 0;
|
|
896
|
+
}
|
|
897
|
+
function detectBrowser(env) {
|
|
898
|
+
const raw = env.BROWSER;
|
|
899
|
+
if (!raw) return void 0;
|
|
900
|
+
const trimmed = raw.trim();
|
|
901
|
+
if (/^[A-Za-z]:[\\/]/.test(trimmed) || trimmed.includes("\\")) {
|
|
902
|
+
const bin = basename(trimmed);
|
|
903
|
+
if (!bin || BROWSER_DISPATCHERS.has(bin.toLowerCase())) return void 0;
|
|
904
|
+
return BROWSER_NAMES[bin.toLowerCase()] ?? bin;
|
|
905
|
+
}
|
|
906
|
+
for (const candidate of trimmed.split(":")) {
|
|
907
|
+
const bin = basename(firstToken(candidate));
|
|
908
|
+
if (!bin) continue;
|
|
909
|
+
if (BROWSER_DISPATCHERS.has(bin.toLowerCase())) continue;
|
|
910
|
+
const hit = BROWSER_NAMES[bin.toLowerCase()];
|
|
911
|
+
if (hit) return hit;
|
|
912
|
+
if (candidate.startsWith("/") && /\s/.test(candidate.trim())) {
|
|
913
|
+
const whole = basename(candidate.trim().replace(/\s+%[a-zA-Z].*$/, ""));
|
|
914
|
+
if (whole && !whole.includes("/")) return BROWSER_NAMES[whole.toLowerCase()] ?? whole;
|
|
915
|
+
}
|
|
916
|
+
return bin;
|
|
917
|
+
}
|
|
918
|
+
return void 0;
|
|
919
|
+
}
|
|
920
|
+
function detectAiTool(env) {
|
|
921
|
+
if (env.AGENT?.trim().toLowerCase() === "amp") return "Amp";
|
|
922
|
+
if (env.CLAUDECODE) return "Claude Code";
|
|
923
|
+
if (env.CODEX_THREAD_ID) return "Codex";
|
|
924
|
+
if (env.GEMINI_CLI) return "Gemini CLI";
|
|
925
|
+
if (env.OPENCODE) return "opencode";
|
|
926
|
+
if (env.COPILOT_CLI) return "GitHub Copilot";
|
|
927
|
+
if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT) return "Cursor";
|
|
928
|
+
return void 0;
|
|
929
|
+
}
|
|
930
|
+
function detectStack(env, platform, opts = {}) {
|
|
586
931
|
const out = /* @__PURE__ */ new Map();
|
|
587
932
|
const set = (key, value) => {
|
|
588
933
|
if (value?.trim()) out.set(key, value.trim());
|
|
589
934
|
};
|
|
590
935
|
try {
|
|
591
|
-
set("os", detectOS(env, platform));
|
|
936
|
+
set("os", detectOS(env, platform, opts));
|
|
592
937
|
set("shell", detectShell(env, platform));
|
|
938
|
+
set("prompt", detectPrompt(env));
|
|
593
939
|
set("terminal", detectTerminal(env));
|
|
594
940
|
set("editor", detectEditor(env));
|
|
595
941
|
set("multiplexer", detectMultiplexer(env));
|
|
942
|
+
set("version-manager", detectVersionManager(env));
|
|
943
|
+
set("window-manager", detectWindowManager(env));
|
|
944
|
+
set("browser", detectBrowser(env));
|
|
945
|
+
set("ai-tool", detectAiTool(env));
|
|
596
946
|
} catch {
|
|
597
947
|
}
|
|
598
948
|
return out;
|
|
@@ -616,6 +966,9 @@ function entriesFromMap(map) {
|
|
|
616
966
|
return value ? [{ key, value }] : [];
|
|
617
967
|
});
|
|
618
968
|
}
|
|
969
|
+
function unknownEntries(existing) {
|
|
970
|
+
return (existing?.entries ?? []).filter((e) => !isCuratedKey(e.key));
|
|
971
|
+
}
|
|
619
972
|
function applySet(existing, target) {
|
|
620
973
|
const entries = (existing?.entries ?? []).map((e) => ({ ...e }));
|
|
621
974
|
const extras = (existing?.extras ?? []).map((x) => ({ ...x }));
|
|
@@ -625,142 +978,34 @@ function applySet(existing, target) {
|
|
|
625
978
|
if (i >= 0) entries[i] = next;
|
|
626
979
|
else entries.push(next);
|
|
627
980
|
} else {
|
|
628
|
-
const
|
|
981
|
+
const wanted = target.label.trim().toLowerCase();
|
|
982
|
+
const i = extras.findIndex((x) => x.label.trim().toLowerCase() === wanted);
|
|
629
983
|
const next = { label: target.label, value: target.value };
|
|
630
984
|
if (i >= 0) extras[i] = next;
|
|
631
985
|
else extras.push(next);
|
|
632
986
|
}
|
|
633
987
|
return { entries, extras };
|
|
634
988
|
}
|
|
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)} ${c.amber}${r.theirs.padEnd(theirsW)}${c.reset} ${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
|
-
}
|
|
989
|
+
function applyUnset(existing, target) {
|
|
990
|
+
const entries = existing.entries.map((e) => ({ ...e }));
|
|
991
|
+
const extras = existing.extras.map((x) => ({ ...x }));
|
|
992
|
+
if (target.kind === "curated") {
|
|
993
|
+
const hit2 = entries.find((e) => e.key === target.key);
|
|
994
|
+
if (!hit2) return { entries, extras, removed: null };
|
|
995
|
+
return {
|
|
996
|
+
entries: entries.filter((e) => e.key !== target.key),
|
|
997
|
+
extras,
|
|
998
|
+
removed: { label: hit2.key, value: hit2.value }
|
|
999
|
+
};
|
|
744
1000
|
}
|
|
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
|
-
`;
|
|
1001
|
+
const wanted = target.label.trim().toLowerCase();
|
|
1002
|
+
const hit = extras.find((x) => x.label.trim().toLowerCase() === wanted);
|
|
1003
|
+
if (!hit) return { entries, extras, removed: null };
|
|
1004
|
+
return {
|
|
1005
|
+
entries,
|
|
1006
|
+
extras: extras.filter((x) => x.label.trim().toLowerCase() !== wanted),
|
|
1007
|
+
removed: { label: hit.label, value: hit.value }
|
|
1008
|
+
};
|
|
764
1009
|
}
|
|
765
1010
|
|
|
766
1011
|
// src/commands.ts
|
|
@@ -775,6 +1020,13 @@ function requireHandle(cred) {
|
|
|
775
1020
|
process.exitCode = 1;
|
|
776
1021
|
return null;
|
|
777
1022
|
}
|
|
1023
|
+
function assertHandleUnchanged(existing, handle) {
|
|
1024
|
+
if (existing && existing.handle.toLowerCase() !== handle.toLowerCase()) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`this login is bound to "${handle}" but your profile now lives at "${sanitizeValue(existing.handle)}" \u2014 run \`ymmv login\` to refresh, then retry.`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
778
1030
|
function newProfile(handle, entries, extras) {
|
|
779
1031
|
return {
|
|
780
1032
|
schema_version: SCHEMA_VERSION,
|
|
@@ -794,19 +1046,49 @@ async function promptEntries(defaults, prompter) {
|
|
|
794
1046
|
return entriesFromMap(chosen);
|
|
795
1047
|
}
|
|
796
1048
|
async function publish(io) {
|
|
1049
|
+
if (!io.interactive && !io.yes) {
|
|
1050
|
+
console.error("Non-interactive publish needs -y (nothing publishes unconfirmed): ymmv -y");
|
|
1051
|
+
process.exitCode = 1;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
797
1054
|
const cred = await ensureLogin();
|
|
798
1055
|
const handle = requireHandle(cred);
|
|
799
1056
|
if (!handle) return;
|
|
800
|
-
const detected = detectStack(process.env, process.platform
|
|
1057
|
+
const detected = detectStack(process.env, process.platform, {
|
|
1058
|
+
readTextFile: (p) => {
|
|
1059
|
+
const st = statSync(p);
|
|
1060
|
+
if (!st.isFile() || st.size > 64 * 1024) throw new Error("not a readable os-release");
|
|
1061
|
+
return readFileSync(p, "utf8");
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
801
1064
|
const existing = await fetchProfileJson(handle);
|
|
1065
|
+
assertHandleUnchanged(existing, handle);
|
|
802
1066
|
const defaults = buildDefaults(existing, detected);
|
|
1067
|
+
const carried = unknownEntries(existing);
|
|
803
1068
|
let entries = entriesFromMap(defaults);
|
|
804
1069
|
const extras = existing?.extras ?? [];
|
|
805
1070
|
if (io.interactive && io.prompter) {
|
|
806
1071
|
entries = await promptEntries(defaults, io.prompter);
|
|
807
1072
|
}
|
|
1073
|
+
entries = [...entries, ...carried];
|
|
808
1074
|
const profile = newProfile(handle, entries, extras);
|
|
809
1075
|
console.log(renderProfile(profile, { color: colorEnabled() }));
|
|
1076
|
+
if (carried.length > 0) {
|
|
1077
|
+
const s = carried.length === 1 ? "" : "s";
|
|
1078
|
+
console.log(`(+${carried.length} newer field${s} kept as-is \u2014 upgrade ymmv-cli to edit them)`);
|
|
1079
|
+
for (const e of carried) {
|
|
1080
|
+
console.log(` ${sanitizeValue(e.key)} = ${sanitizeValue(e.value)}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const publishedLabels = new Set(
|
|
1084
|
+
entries.filter((e) => isCuratedKey(e.key)).map((e) => KEY_LABELS[e.key].toLowerCase())
|
|
1085
|
+
);
|
|
1086
|
+
for (const x of extras) {
|
|
1087
|
+
if (publishedLabels.has(x.label.trim().toLowerCase())) {
|
|
1088
|
+
const label = sanitizeValue(x.label.trim());
|
|
1089
|
+
console.log(`(extra "${label}" duplicates a curated field \u2014 ymmv unset --extra "${label}")`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
810
1092
|
if (io.interactive && io.prompter && !io.yes) {
|
|
811
1093
|
const go = await io.prompter.confirm(`Publish to ymmv.fyi/${handle}?`, true);
|
|
812
1094
|
if (!go) {
|
|
@@ -845,6 +1127,7 @@ async function runSet(target) {
|
|
|
845
1127
|
const handle = requireHandle(cred);
|
|
846
1128
|
if (!handle) return;
|
|
847
1129
|
const existing = await fetchProfileJson(handle);
|
|
1130
|
+
assertHandleUnchanged(existing, handle);
|
|
848
1131
|
const { entries, extras } = applySet(existing, target);
|
|
849
1132
|
await publishProfile(newProfile(handle, entries, extras));
|
|
850
1133
|
if (target.kind === "curated") {
|
|
@@ -853,6 +1136,32 @@ async function runSet(target) {
|
|
|
853
1136
|
console.log(`Set extra ${target.label} = ${target.value}.`);
|
|
854
1137
|
}
|
|
855
1138
|
}
|
|
1139
|
+
async function runUnset(target) {
|
|
1140
|
+
const cred = await ensureLogin();
|
|
1141
|
+
const handle = requireHandle(cred);
|
|
1142
|
+
if (!handle) return;
|
|
1143
|
+
const existing = await fetchProfileJson(handle);
|
|
1144
|
+
assertHandleUnchanged(existing, handle);
|
|
1145
|
+
if (!existing) {
|
|
1146
|
+
console.log("No profile yet \u2014 run `ymmv` to publish one.");
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const { entries, extras, removed } = applyUnset(existing, target);
|
|
1150
|
+
if (!removed) {
|
|
1151
|
+
console.log(
|
|
1152
|
+
target.kind === "curated" ? `${KEY_LABELS[target.key]} is not set.` : `No extra "${target.label}".`
|
|
1153
|
+
);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
await publishProfile(newProfile(handle, entries, extras));
|
|
1157
|
+
if (target.kind === "curated") {
|
|
1158
|
+
console.log(`Removed ${KEY_LABELS[target.key]} (was "${sanitizeValue(removed.value)}").`);
|
|
1159
|
+
} else {
|
|
1160
|
+
console.log(
|
|
1161
|
+
`Removed extra "${sanitizeValue(removed.label)}" (was "${sanitizeValue(removed.value)}").`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
856
1165
|
async function runDelete(io) {
|
|
857
1166
|
const cred = await ensureLogin();
|
|
858
1167
|
const target = cred.handle ? `ymmv.fyi/${cred.handle}` : "your profile";
|
|
@@ -878,6 +1187,10 @@ async function runDelete(io) {
|
|
|
878
1187
|
// src/prompt.ts
|
|
879
1188
|
import { stdin, stdout } from "process";
|
|
880
1189
|
import { createInterface } from "readline/promises";
|
|
1190
|
+
function promptLine(label, def) {
|
|
1191
|
+
const clean = def ? sanitizeValue(def) : def;
|
|
1192
|
+
return ` ${label}${clean ? ` [${clean}]` : ""}: `;
|
|
1193
|
+
}
|
|
881
1194
|
function makePrompter() {
|
|
882
1195
|
let rl = null;
|
|
883
1196
|
const io = () => {
|
|
@@ -886,8 +1199,9 @@ function makePrompter() {
|
|
|
886
1199
|
};
|
|
887
1200
|
return {
|
|
888
1201
|
async ask(label, def) {
|
|
889
|
-
const
|
|
890
|
-
|
|
1202
|
+
const clean = def ? sanitizeValue(def) : def;
|
|
1203
|
+
const answer = (await io().question(promptLine(label, def))).trim();
|
|
1204
|
+
return answer === "" ? clean ?? "" : answer;
|
|
891
1205
|
},
|
|
892
1206
|
async confirm(question, defYes) {
|
|
893
1207
|
const answer = (await io().question(` ${question} ${defYes ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
|
|
@@ -902,8 +1216,18 @@ function makePrompter() {
|
|
|
902
1216
|
}
|
|
903
1217
|
|
|
904
1218
|
// src/resolve.ts
|
|
905
|
-
var
|
|
906
|
-
var
|
|
1219
|
+
var SET_EXTRA = 'ymmv set --extra "Label=Value"';
|
|
1220
|
+
var UNSET_EXTRA = 'ymmv unset --extra "Label"';
|
|
1221
|
+
var SET_USAGE = `usage: ymmv set <key> <value> | ${SET_EXTRA}`;
|
|
1222
|
+
var EXTRA_USAGE = `usage: ${SET_EXTRA}`;
|
|
1223
|
+
var UNSET_USAGE = `usage: ymmv unset <key> | ${UNSET_EXTRA}`;
|
|
1224
|
+
function invalidKeyError(head, hint) {
|
|
1225
|
+
return {
|
|
1226
|
+
kind: "error",
|
|
1227
|
+
message: `"${head}" is not a curated key. Valid keys: ${CURATED_KEYS.join(", ")}.
|
|
1228
|
+
For anything else, use: ${hint}.`
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
907
1231
|
function hasYes(args) {
|
|
908
1232
|
return args.includes("-y") || args.includes("--yes");
|
|
909
1233
|
}
|
|
@@ -916,20 +1240,34 @@ function parseSet(rest) {
|
|
|
916
1240
|
const label = spec.slice(0, eq).trim();
|
|
917
1241
|
const value2 = spec.slice(eq + 1).trim();
|
|
918
1242
|
if (!label || !value2) return { kind: "error", message: EXTRA_USAGE };
|
|
1243
|
+
if (value2 === "-") return { kind: "unset", target: { kind: "extra", label } };
|
|
919
1244
|
return { kind: "set", target: { kind: "extra", label, value: value2 } };
|
|
920
1245
|
}
|
|
921
1246
|
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
|
-
}
|
|
1247
|
+
if (!isCuratedKey(head)) return invalidKeyError(head, SET_EXTRA);
|
|
929
1248
|
const value = rest.slice(1).join(" ").trim();
|
|
930
1249
|
if (!value) return { kind: "error", message: `usage: ymmv set ${head} <value>` };
|
|
1250
|
+
if (value === "-") return { kind: "unset", target: { kind: "curated", key: head } };
|
|
931
1251
|
return { kind: "set", target: { kind: "curated", key: head, value } };
|
|
932
1252
|
}
|
|
1253
|
+
function parseUnset(rest) {
|
|
1254
|
+
const head = rest[0];
|
|
1255
|
+
if (head === "--extra" || head === "-e") {
|
|
1256
|
+
const label = rest.slice(1).join(" ").trim();
|
|
1257
|
+
if (!label) return { kind: "error", message: `usage: ${UNSET_EXTRA}` };
|
|
1258
|
+
if (label.includes("=")) {
|
|
1259
|
+
return {
|
|
1260
|
+
kind: "error",
|
|
1261
|
+
message: 'unset takes just the label: ymmv unset --extra "Keyboard"'
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
return { kind: "unset", target: { kind: "extra", label } };
|
|
1265
|
+
}
|
|
1266
|
+
if (!head) return { kind: "error", message: UNSET_USAGE };
|
|
1267
|
+
if (!isCuratedKey(head)) return invalidKeyError(head, UNSET_EXTRA);
|
|
1268
|
+
if (rest.length > 1) return { kind: "error", message: `usage: ymmv unset ${head}` };
|
|
1269
|
+
return { kind: "unset", target: { kind: "curated", key: head } };
|
|
1270
|
+
}
|
|
933
1271
|
function resolveArg(argv) {
|
|
934
1272
|
const first = argv[0];
|
|
935
1273
|
if (first === "-h" || first === "--help" || first === "help") return { kind: "help" };
|
|
@@ -940,6 +1278,7 @@ function resolveArg(argv) {
|
|
|
940
1278
|
if (first === "logout") return { kind: "logout" };
|
|
941
1279
|
if (first === "delete") return { kind: "delete", yes: hasYes(argv.slice(1)) };
|
|
942
1280
|
if (first === "set") return parseSet(argv.slice(1));
|
|
1281
|
+
if (first === "unset") return parseUnset(argv.slice(1));
|
|
943
1282
|
if (first === "view") {
|
|
944
1283
|
const handle = argv[1];
|
|
945
1284
|
if (!handle) return { kind: "error", message: "usage: ymmv view <handle>" };
|
|
@@ -965,16 +1304,19 @@ var HELP = `ymmv \u2014 terminal-native developer tool-stack profiles (ymmv.fyi)
|
|
|
965
1304
|
|
|
966
1305
|
Usage:
|
|
967
1306
|
ymmv detect your stack, confirm, and publish your profile
|
|
1307
|
+
ymmv -y publish without prompts (required when stdin isn't a TTY)
|
|
968
1308
|
ymmv <handle> view a profile \u2014 logged in, see the diff vs yours
|
|
969
1309
|
ymmv view <handle> explicit view (when a handle collides with a verb)
|
|
970
1310
|
ymmv set <key> <value> set one curated key
|
|
971
1311
|
ymmv set --extra "L=V" set a free-form extra
|
|
1312
|
+
ymmv unset <key> remove one curated key (ymmv set <key> - works too)
|
|
1313
|
+
ymmv unset --extra "L" remove a free-form extra
|
|
972
1314
|
ymmv delete delete your profile (permanent)
|
|
973
1315
|
ymmv login | logout GitHub device-flow auth
|
|
974
1316
|
ymmv help | --version
|
|
975
1317
|
|
|
976
|
-
Curated keys: editor, os, shell, terminal, browser, window-manager,
|
|
977
|
-
multiplexer, dotfiles, ai-tool
|
|
1318
|
+
Curated keys: editor, os, shell, prompt, terminal, browser, window-manager,
|
|
1319
|
+
font, theme, multiplexer, version-manager, dotfiles, ai-tool
|
|
978
1320
|
|
|
979
1321
|
Respects NO_COLOR. Point YMMV_API at a dev Worker to target one.
|
|
980
1322
|
Publish your own: npx ymmv-cli`;
|
|
@@ -1002,7 +1344,7 @@ async function logout() {
|
|
|
1002
1344
|
}
|
|
1003
1345
|
function printVersion() {
|
|
1004
1346
|
try {
|
|
1005
|
-
const pkg = JSON.parse(
|
|
1347
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
1006
1348
|
console.log(`ymmv-cli ${pkg.version ?? "unknown"}`);
|
|
1007
1349
|
} catch {
|
|
1008
1350
|
console.log("ymmv-cli (version unknown)");
|
|
@@ -1029,6 +1371,9 @@ async function main(argv) {
|
|
|
1029
1371
|
case "set":
|
|
1030
1372
|
await runSet(cmd.target);
|
|
1031
1373
|
break;
|
|
1374
|
+
case "unset":
|
|
1375
|
+
await runUnset(cmd.target);
|
|
1376
|
+
break;
|
|
1032
1377
|
case "delete":
|
|
1033
1378
|
await interactive(runDelete, cmd.yes);
|
|
1034
1379
|
break;
|