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.
Files changed (3) hide show
  1. package/README.md +29 -12
  2. package/dist/cli.js +272 -20
  3. 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, browser, font (and more) published to a clean
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
+ ![Running npx ymmv-cli to detect, confirm, and publish a dev stack to a live ymmv.fyi page](https://raw.githubusercontent.com/ymmv-fyi/ymmv/main/docs/demo.gif)
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 (the device flow you know from `gh`
20
- / `npm login`, ~10s).
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` — detect, confirm, and publish (re-run any time to update)
25
- - `ymmv <handle>` view a profile, or diff it against yours when you're logged in
26
- - `ymmv set editor Neovim` change one value
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
29
- - `ymmv delete` remove your profile
30
- - `ymmv login` / `ymmv logout` sign in / out
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 `GET https://ymmv.fyi/api/v1/u/<handle>`.
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
- function detectOS(env, platform) {
675
- if (env.WSL_DISTRO_NAME) return `${env.WSL_DISTRO_NAME} (WSL)`;
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 (!raw) return void 0;
712
- const bin = basename(firstToken(raw));
713
- return EDITOR_NAMES[bin.toLowerCase()] ?? bin;
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 detectStack(env, platform) {
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 answer = (await io().question(` ${label}${def ? ` [${def}]` : ""}: `)).trim();
953
- return answer === "" ? def ?? "" : answer;
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, font,
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(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
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)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ymmv-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Publish and diff terminal-native developer tool-stack profiles at ymmv.fyi.",
5
5
  "type": "module",
6
6
  "bin": {