ymmv-cli 0.4.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 -12
- package/dist/cli.js +272 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,34 +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 unset editor`
|
|
29
|
-
- `ymmv delete`
|
|
30
|
-
- `ymmv login` / `ymmv logout`
|
|
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
|
|
31
48
|
|
|
32
|
-
Every profile is open JSON too
|
|
49
|
+
Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`.
|
|
33
50
|
|
|
34
51
|
## License
|
|
35
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
|
];
|
|
@@ -661,7 +749,7 @@ function firstToken(s) {
|
|
|
661
749
|
return s.trim().split(/\s+/)[0] ?? "";
|
|
662
750
|
}
|
|
663
751
|
var namesFor = (key) => {
|
|
664
|
-
const map =
|
|
752
|
+
const map = /* @__PURE__ */ Object.create(null);
|
|
665
753
|
for (const tool of TOOLS) {
|
|
666
754
|
if (tool.key !== key) continue;
|
|
667
755
|
for (const token of tool.envTokens ?? []) map[token] = tool.canonical;
|
|
@@ -671,15 +759,44 @@ var namesFor = (key) => {
|
|
|
671
759
|
var SHELL_NAMES = namesFor("shell");
|
|
672
760
|
var EDITOR_NAMES = namesFor("editor");
|
|
673
761
|
var TERM_PROGRAMS = namesFor("terminal");
|
|
674
|
-
|
|
675
|
-
|
|
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)`;
|
|
676
793
|
switch (platform) {
|
|
677
794
|
case "darwin":
|
|
678
795
|
return "macOS";
|
|
679
796
|
case "win32":
|
|
680
797
|
return "Windows";
|
|
681
798
|
case "linux":
|
|
682
|
-
return "Linux";
|
|
799
|
+
return linuxDistro(opts.readTextFile) ?? "Linux";
|
|
683
800
|
default:
|
|
684
801
|
return platform;
|
|
685
802
|
}
|
|
@@ -691,6 +808,9 @@ function detectShell(env, platform) {
|
|
|
691
808
|
return SHELL_NAMES[key] ?? basename(raw);
|
|
692
809
|
}
|
|
693
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";
|
|
694
814
|
if (env.PSModulePath) return "PowerShell";
|
|
695
815
|
if (env.ComSpec) return basename(env.ComSpec);
|
|
696
816
|
}
|
|
@@ -699,18 +819,37 @@ function detectShell(env, platform) {
|
|
|
699
819
|
function detectTerminal(env) {
|
|
700
820
|
const tp = env.TERM_PROGRAM?.trim();
|
|
701
821
|
if (tp && tp.toLowerCase() !== "tmux") {
|
|
822
|
+
if (tp.toLowerCase() === "vscode" && env.CURSOR_TRACE_ID) return "Cursor";
|
|
702
823
|
return TERM_PROGRAMS[tp.toLowerCase()] ?? tp;
|
|
703
824
|
}
|
|
825
|
+
if (env.TERMINAL_EMULATOR === "JetBrains-JediTerm") return "JetBrains";
|
|
704
826
|
if (env.WT_SESSION) return "Windows Terminal";
|
|
705
827
|
if (env.KONSOLE_VERSION) return "Konsole";
|
|
706
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];
|
|
707
837
|
return void 0;
|
|
708
838
|
}
|
|
709
839
|
function detectEditor(env) {
|
|
710
840
|
const raw = env.VISUAL ?? env.EDITOR;
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
|
|
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;
|
|
714
853
|
}
|
|
715
854
|
function detectMultiplexer(env) {
|
|
716
855
|
if (env.TMUX) return "tmux";
|
|
@@ -718,17 +857,92 @@ function detectMultiplexer(env) {
|
|
|
718
857
|
if (env.STY) return "GNU Screen";
|
|
719
858
|
return void 0;
|
|
720
859
|
}
|
|
721
|
-
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 = {}) {
|
|
722
931
|
const out = /* @__PURE__ */ new Map();
|
|
723
932
|
const set = (key, value) => {
|
|
724
933
|
if (value?.trim()) out.set(key, value.trim());
|
|
725
934
|
};
|
|
726
935
|
try {
|
|
727
|
-
set("os", detectOS(env, platform));
|
|
936
|
+
set("os", detectOS(env, platform, opts));
|
|
728
937
|
set("shell", detectShell(env, platform));
|
|
938
|
+
set("prompt", detectPrompt(env));
|
|
729
939
|
set("terminal", detectTerminal(env));
|
|
730
940
|
set("editor", detectEditor(env));
|
|
731
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));
|
|
732
946
|
} catch {
|
|
733
947
|
}
|
|
734
948
|
return out;
|
|
@@ -752,6 +966,9 @@ function entriesFromMap(map) {
|
|
|
752
966
|
return value ? [{ key, value }] : [];
|
|
753
967
|
});
|
|
754
968
|
}
|
|
969
|
+
function unknownEntries(existing) {
|
|
970
|
+
return (existing?.entries ?? []).filter((e) => !isCuratedKey(e.key));
|
|
971
|
+
}
|
|
755
972
|
function applySet(existing, target) {
|
|
756
973
|
const entries = (existing?.entries ?? []).map((e) => ({ ...e }));
|
|
757
974
|
const extras = (existing?.extras ?? []).map((x) => ({ ...x }));
|
|
@@ -829,20 +1046,49 @@ async function promptEntries(defaults, prompter) {
|
|
|
829
1046
|
return entriesFromMap(chosen);
|
|
830
1047
|
}
|
|
831
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
|
+
}
|
|
832
1054
|
const cred = await ensureLogin();
|
|
833
1055
|
const handle = requireHandle(cred);
|
|
834
1056
|
if (!handle) return;
|
|
835
|
-
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
|
+
});
|
|
836
1064
|
const existing = await fetchProfileJson(handle);
|
|
837
1065
|
assertHandleUnchanged(existing, handle);
|
|
838
1066
|
const defaults = buildDefaults(existing, detected);
|
|
1067
|
+
const carried = unknownEntries(existing);
|
|
839
1068
|
let entries = entriesFromMap(defaults);
|
|
840
1069
|
const extras = existing?.extras ?? [];
|
|
841
1070
|
if (io.interactive && io.prompter) {
|
|
842
1071
|
entries = await promptEntries(defaults, io.prompter);
|
|
843
1072
|
}
|
|
1073
|
+
entries = [...entries, ...carried];
|
|
844
1074
|
const profile = newProfile(handle, entries, extras);
|
|
845
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
|
+
}
|
|
846
1092
|
if (io.interactive && io.prompter && !io.yes) {
|
|
847
1093
|
const go = await io.prompter.confirm(`Publish to ymmv.fyi/${handle}?`, true);
|
|
848
1094
|
if (!go) {
|
|
@@ -941,6 +1187,10 @@ async function runDelete(io) {
|
|
|
941
1187
|
// src/prompt.ts
|
|
942
1188
|
import { stdin, stdout } from "process";
|
|
943
1189
|
import { createInterface } from "readline/promises";
|
|
1190
|
+
function promptLine(label, def) {
|
|
1191
|
+
const clean = def ? sanitizeValue(def) : def;
|
|
1192
|
+
return ` ${label}${clean ? ` [${clean}]` : ""}: `;
|
|
1193
|
+
}
|
|
944
1194
|
function makePrompter() {
|
|
945
1195
|
let rl = null;
|
|
946
1196
|
const io = () => {
|
|
@@ -949,8 +1199,9 @@ function makePrompter() {
|
|
|
949
1199
|
};
|
|
950
1200
|
return {
|
|
951
1201
|
async ask(label, def) {
|
|
952
|
-
const
|
|
953
|
-
|
|
1202
|
+
const clean = def ? sanitizeValue(def) : def;
|
|
1203
|
+
const answer = (await io().question(promptLine(label, def))).trim();
|
|
1204
|
+
return answer === "" ? clean ?? "" : answer;
|
|
954
1205
|
},
|
|
955
1206
|
async confirm(question, defYes) {
|
|
956
1207
|
const answer = (await io().question(` ${question} ${defYes ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
|
|
@@ -1053,6 +1304,7 @@ var HELP = `ymmv \u2014 terminal-native developer tool-stack profiles (ymmv.fyi)
|
|
|
1053
1304
|
|
|
1054
1305
|
Usage:
|
|
1055
1306
|
ymmv detect your stack, confirm, and publish your profile
|
|
1307
|
+
ymmv -y publish without prompts (required when stdin isn't a TTY)
|
|
1056
1308
|
ymmv <handle> view a profile \u2014 logged in, see the diff vs yours
|
|
1057
1309
|
ymmv view <handle> explicit view (when a handle collides with a verb)
|
|
1058
1310
|
ymmv set <key> <value> set one curated key
|
|
@@ -1063,8 +1315,8 @@ Usage:
|
|
|
1063
1315
|
ymmv login | logout GitHub device-flow auth
|
|
1064
1316
|
ymmv help | --version
|
|
1065
1317
|
|
|
1066
|
-
Curated keys: editor, os, shell, terminal, browser, window-manager,
|
|
1067
|
-
multiplexer, dotfiles, ai-tool
|
|
1318
|
+
Curated keys: editor, os, shell, prompt, terminal, browser, window-manager,
|
|
1319
|
+
font, theme, multiplexer, version-manager, dotfiles, ai-tool
|
|
1068
1320
|
|
|
1069
1321
|
Respects NO_COLOR. Point YMMV_API at a dev Worker to target one.
|
|
1070
1322
|
Publish your own: npx ymmv-cli`;
|
|
@@ -1092,7 +1344,7 @@ async function logout() {
|
|
|
1092
1344
|
}
|
|
1093
1345
|
function printVersion() {
|
|
1094
1346
|
try {
|
|
1095
|
-
const pkg = JSON.parse(
|
|
1347
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
1096
1348
|
console.log(`ymmv-cli ${pkg.version ?? "unknown"}`);
|
|
1097
1349
|
} catch {
|
|
1098
1350
|
console.log("ymmv-cli (version unknown)");
|