ymmv-cli 0.11.1 → 0.11.2

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 +2 -2
  2. package/dist/cli.js +103 -41
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,8 +58,8 @@ via npm Trusted Publishing, with provenance.
58
58
  the commands to run by hand)
59
59
  - `ymmv version` prints the CLI version (and notes a newer release when one is known)
60
60
 
61
- Values are capped at 256 characters and extra labels at 64; a profile holds up
62
- to 32 extras.
61
+ Values are capped at 256 characters and extra labels at 64, and each needs at
62
+ least one visible character; a profile holds up to 32 extras.
63
63
 
64
64
  Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`. Full contract
65
65
  (shape, statuses, caching, CORS):
package/dist/cli.js CHANGED
@@ -393,6 +393,12 @@ function isValidHandle(handle) {
393
393
  return handle.length >= 1 && handle.length <= 39 && HANDLE_RE.test(handle);
394
394
  }
395
395
 
396
+ // ../shared/dist/visible.js
397
+ var VISIBLE_RE = /[^\s\p{Default_Ignorable_Code_Point}\p{Cc}]/u;
398
+ function hasVisibleContent(s) {
399
+ return VISIBLE_RE.test(s);
400
+ }
401
+
396
402
  // src/render.ts
397
403
  var ESC = String.fromCharCode(27);
398
404
  var CSI = `${ESC}[`;
@@ -425,6 +431,9 @@ var BIDI_RE = new RegExp("\\p{Bidi_Control}", "gu");
425
431
  function sanitizeValue(value) {
426
432
  return value.replace(ANSI_RE, "").replace(CTRL_RE, "").replace(BIDI_RE, "");
427
433
  }
434
+ function showsVisibleText(value) {
435
+ return hasVisibleContent(sanitizeValue(value));
436
+ }
428
437
  function useColor(env, isTTY) {
429
438
  if (env.NO_COLOR !== void 0) return false;
430
439
  if (env.FORCE_COLOR !== void 0) {
@@ -597,10 +606,16 @@ function causeText(err) {
597
606
  return sanitizeValue(text);
598
607
  }
599
608
  function wireText(text) {
600
- const clean = sanitizeValue(String(text));
601
- return clean.length > 200 ? `${clean.slice(0, 200)}\u2026` : clean;
609
+ return capped(sanitizeValue(String(text)));
610
+ }
611
+ var WIRE_TEXT_CAP = 200;
612
+ function head(clean) {
613
+ return [...clean.slice(0, WIRE_TEXT_CAP * 2)].slice(0, WIRE_TEXT_CAP).join("");
614
+ }
615
+ function capped(clean) {
616
+ const kept = head(clean);
617
+ return kept.length < clean.length ? `${kept}\u2026` : clean;
602
618
  }
603
- var INVISIBLE_RE = new RegExp("\\p{Default_Ignorable_Code_Point}", "gu");
604
619
  async function wireBody(res) {
605
620
  try {
606
621
  return await res.text();
@@ -615,8 +630,8 @@ function wireErrorBody(raw) {
615
630
  const out = {};
616
631
  if (typeof body?.error === "string") out.slug = body.error;
617
632
  if (typeof body?.message === "string") {
618
- const clean = wireText(body.message).trim();
619
- if (clean.replace(INVISIBLE_RE, "").trim()) out.message = clean;
633
+ const clean = sanitizeValue(body.message);
634
+ if (hasVisibleContent(head(clean))) out.message = capped(clean).trim();
620
635
  }
621
636
  return out;
622
637
  } catch {
@@ -1712,7 +1727,31 @@ function pagePointer(handle) {
1712
1727
  const c = palette(color);
1713
1728
  return ` ${c.faint}\u2192 ${color ? displayUrl(BASE) : BASE}/${handle}${c.reset}`;
1714
1729
  }
1715
- async function promptEntries(defaults, prompter) {
1730
+ function writeRuleRefusal(entries, existing) {
1731
+ const saved = savedKeys(existing);
1732
+ for (const { key, value } of entries) {
1733
+ if (!isCuratedKey(key)) continue;
1734
+ const problem = valueProblem(value);
1735
+ if (problem === void 0) continue;
1736
+ const remedy = problem === "invisible" ? "Set one" : "Set a shorter one";
1737
+ const source = saved.has(key) ? "saved" : "detected";
1738
+ const remove = problem === "invisible" && source === "saved" ? `, or remove it: ymmv unset ${key}` : "";
1739
+ return `The ${source} ${KEY_LABELS[key]} value ${ruleClause(problem, value)}. ${remedy}: ymmv set ${key} <value>${remove}.`;
1740
+ }
1741
+ return void 0;
1742
+ }
1743
+ function valueProblem(value) {
1744
+ if (!showsVisibleText(value)) return "invisible";
1745
+ if (value.length > MAX_VALUE) return "over-cap";
1746
+ return void 0;
1747
+ }
1748
+ function ruleClause(problem, value) {
1749
+ return problem === "invisible" ? "has no visible text" : `is ${value.length} characters; the cap is ${MAX_VALUE}`;
1750
+ }
1751
+ function savedKeys(existing) {
1752
+ return new Set((existing?.entries ?? []).map((e) => e.key));
1753
+ }
1754
+ async function promptEntries(defaults, saved, prompter) {
1716
1755
  const c = palette(colorEnabled());
1717
1756
  console.log(message(`${c.faint}Enter to keep, "-" to clear${c.reset}`));
1718
1757
  const chosen = /* @__PURE__ */ new Map();
@@ -1720,13 +1759,15 @@ async function promptEntries(defaults, prompter) {
1720
1759
  for (; ; ) {
1721
1760
  const answer = (await prompter.ask(KEY_LABELS[key], defaults.get(key))).trim();
1722
1761
  const value = answer === "-" ? "" : answer;
1723
- if (value.length > MAX_VALUE) {
1724
- const isDefault = value === (defaults.get(key) ?? "").trim();
1725
- console.log(
1726
- message(
1727
- `${c.faint}${isDefault ? `the saved value is ${value.length} characters; the cap is ${MAX_VALUE}. Type a shorter value or - to clear` : `that value is ${value.length} characters; the cap is ${MAX_VALUE}`}${c.reset}`
1728
- )
1729
- );
1762
+ const rawDefault = defaults.get(key) ?? "";
1763
+ const isDefault = value === sanitizeValue(rawDefault).trim();
1764
+ const emptiedDefault = answer === "" && rawDefault !== "";
1765
+ const problem = emptiedDefault ? "invisible" : value === "" ? void 0 : valueProblem(value);
1766
+ if (problem !== void 0) {
1767
+ const which = `the ${saved.has(key) ? "saved" : "detected"} value`;
1768
+ const clause = ruleClause(problem, value);
1769
+ const note = isDefault ? `${which} ${clause}. Type a ${problem === "invisible" ? "value" : "shorter value"} or - to clear` : `that value ${clause}`;
1770
+ console.log(message(`${c.faint}${note}${c.reset}`));
1730
1771
  continue;
1731
1772
  }
1732
1773
  if (value) chosen.set(key, value);
@@ -1788,26 +1829,23 @@ async function publish(io) {
1788
1829
  let values = defaults;
1789
1830
  const assemble = () => [...entriesFromMap(values), ...carried];
1790
1831
  const edits = /* @__PURE__ */ new Map();
1832
+ let saved = savedKeys(existing);
1791
1833
  const prompt = async (prompter) => {
1792
1834
  const before = values;
1793
- values = await promptEntries(values, prompter);
1835
+ values = await promptEntries(values, saved, prompter);
1794
1836
  for (const key of CURATED_KEYS) {
1795
1837
  const after = values.get(key);
1796
1838
  if (after !== before.get(key)) edits.set(key, after);
1797
1839
  }
1798
1840
  };
1799
1841
  if (!io.interactive || !io.prompter || io.yes) {
1800
- const over = [...values].find(([, v]) => v.length > MAX_VALUE);
1801
- if (over) {
1802
- console.error(
1803
- message(
1804
- `The detected ${KEY_LABELS[over[0]]} value is ${over[1].length} characters; the cap is ${MAX_VALUE}. Set a shorter one: ymmv set ${over[0]} <value>.`
1805
- )
1806
- );
1842
+ const entries = assemble();
1843
+ const refusal = writeRuleRefusal(entries, existing);
1844
+ if (refusal !== void 0) {
1845
+ console.error(message(refusal));
1807
1846
  process.exitCode = 1;
1808
1847
  return;
1809
1848
  }
1810
- const entries = assemble();
1811
1849
  showCard(entries);
1812
1850
  printPublished(
1813
1851
  await publishProfile(newProfile(handle, entries, extras), cred, { ifMatch }),
@@ -1817,7 +1855,9 @@ async function publish(io) {
1817
1855
  }
1818
1856
  try {
1819
1857
  if (!existing) await prompt(io.prompter);
1858
+ const failsRule = ([, v]) => valueProblem(v) !== void 0;
1820
1859
  for (; ; ) {
1860
+ if ([...values].some(failsRule)) await prompt(io.prompter);
1821
1861
  const entries = assemble();
1822
1862
  showCard(entries);
1823
1863
  const ans = await io.prompter.choice(
@@ -1854,6 +1894,7 @@ async function publish(io) {
1854
1894
  else rebased.set(key, value);
1855
1895
  }
1856
1896
  values = rebased;
1897
+ saved = savedKeys(fresh.profile);
1857
1898
  carried = unknownEntries(fresh.profile);
1858
1899
  extras = fresh.profile.extras;
1859
1900
  ifMatch = fresh.etag;
@@ -1954,18 +1995,24 @@ async function runSet(target) {
1954
1995
  process.exitCode = 1;
1955
1996
  return;
1956
1997
  }
1998
+ const refusal = writeRuleRefusal(entries, existing);
1999
+ if (refusal !== void 0) {
2000
+ console.error(message(refusal));
2001
+ process.exitCode = 1;
2002
+ return;
2003
+ }
1957
2004
  const res = await publishProfile(newProfile(handle, entries, extras), cred, {
1958
2005
  ifMatch: own?.etag
1959
2006
  });
1960
- const line = target.kind === "curated" ? `Set ${KEY_LABELS[target.key]} = ${target.value}.` : `Set extra ${target.label} = ${target.value}.`;
2007
+ const line = target.kind === "curated" ? `Set ${KEY_LABELS[target.key]} = ${sanitizeValue(target.value)}.` : `Set extra ${sanitizeValue(target.label)} = ${sanitizeValue(target.value)}.`;
1961
2008
  console.log(message(`${line}${pagePointer(res.handle)}`));
1962
2009
  }
1963
2010
  var PASTE_SAFE_LABEL = /^[\p{L}\p{N} _.+/:-]+$/u;
1964
2011
  function extraHint(existing, label) {
1965
2012
  const eq = label.indexOf("=");
1966
- const head = eq > 0 ? label.slice(0, eq).trim() : "";
1967
- if (!head || !applyUnset(existing, { kind: "extra", label: head }).removed) return "";
1968
- const shown = PASTE_SAFE_LABEL.test(head) ? head : "Label";
2013
+ const head2 = eq > 0 ? label.slice(0, eq).trim() : "";
2014
+ if (!head2 || !applyUnset(existing, { kind: "extra", label: head2 }).removed) return "";
2015
+ const shown = PASTE_SAFE_LABEL.test(head2) ? head2 : "Label";
1969
2016
  return `
1970
2017
  (unset takes just the label: ymmv unset --extra "${shown}")`;
1971
2018
  }
@@ -1986,6 +2033,12 @@ async function runUnset(target) {
1986
2033
  console.log(message(line2));
1987
2034
  return;
1988
2035
  }
2036
+ const refusal = writeRuleRefusal(entries, existing);
2037
+ if (refusal !== void 0) {
2038
+ console.error(message(refusal));
2039
+ process.exitCode = 1;
2040
+ return;
2041
+ }
1989
2042
  const res = await publishProfile(newProfile(handle, entries, extras), cred, {
1990
2043
  ifMatch: own?.etag
1991
2044
  });
@@ -2034,10 +2087,10 @@ var SET_USAGE = `usage: ymmv set <key> <value> | ${SET_EXTRA}`;
2034
2087
  var EXTRA_USAGE = `usage: ${SET_EXTRA}`;
2035
2088
  var UNSET_USAGE = `usage: ymmv unset <key> | ${UNSET_EXTRA}`;
2036
2089
  var VIEW_USAGE = "usage: ymmv view <handle>";
2037
- function invalidKeyError(head, hint) {
2090
+ function invalidKeyError(head2, hint) {
2038
2091
  return {
2039
2092
  kind: "error",
2040
- message: `"${sanitizeValue(head)}" is not a curated key. Valid keys: ${CURATED_KEYS.join(", ")}.
2093
+ message: `"${sanitizeValue(head2)}" is not a curated key. Valid keys: ${CURATED_KEYS.join(", ")}.
2041
2094
  For anything else, use: ${hint}.`
2042
2095
  };
2043
2096
  }
@@ -2061,9 +2114,15 @@ function valueCapError(value) {
2061
2114
  message: `That value is ${value.length} characters; the cap is ${MAX_VALUE}.`
2062
2115
  };
2063
2116
  }
2117
+ function labelInvisibleError() {
2118
+ return { kind: "error", message: "That label has no visible text." };
2119
+ }
2120
+ function valueInvisibleError() {
2121
+ return { kind: "error", message: "That value has no visible text." };
2122
+ }
2064
2123
  function parseSet(rest) {
2065
- const head = rest[0];
2066
- if (head === "--extra" || head === "-e") {
2124
+ const head2 = rest[0];
2125
+ if (head2 === "--extra" || head2 === "-e") {
2067
2126
  const spec = rest.slice(1).join(" ").trim();
2068
2127
  const eq = spec.indexOf("=");
2069
2128
  if (eq <= 0) return { kind: "error", message: EXTRA_USAGE };
@@ -2071,30 +2130,33 @@ function parseSet(rest) {
2071
2130
  const value2 = spec.slice(eq + 1).trim();
2072
2131
  if (!label || !value2) return { kind: "error", message: EXTRA_USAGE };
2073
2132
  if (value2 === "-") return { kind: "unset", target: { kind: "extra", label } };
2133
+ if (!showsVisibleText(label)) return labelInvisibleError();
2134
+ if (!showsVisibleText(value2)) return valueInvisibleError();
2074
2135
  if (label.length > MAX_LABEL) return labelCapError(label);
2075
2136
  if (value2.length > MAX_VALUE) return valueCapError(value2);
2076
2137
  return { kind: "set", target: { kind: "extra", label, value: value2 } };
2077
2138
  }
2078
- if (!head) return { kind: "error", message: SET_USAGE };
2079
- if (!isCuratedKey(head)) return invalidKeyError(head, SET_EXTRA);
2139
+ if (!head2) return { kind: "error", message: SET_USAGE };
2140
+ if (!isCuratedKey(head2)) return invalidKeyError(head2, SET_EXTRA);
2080
2141
  const value = rest.slice(1).join(" ").trim();
2081
- if (!value) return { kind: "error", message: `usage: ymmv set ${head} <value>` };
2082
- if (value === "-") return { kind: "unset", target: { kind: "curated", key: head } };
2142
+ if (!value) return { kind: "error", message: `usage: ymmv set ${head2} <value>` };
2143
+ if (value === "-") return { kind: "unset", target: { kind: "curated", key: head2 } };
2144
+ if (!showsVisibleText(value)) return valueInvisibleError();
2083
2145
  if (value.length > MAX_VALUE) return valueCapError(value);
2084
- return { kind: "set", target: { kind: "curated", key: head, value } };
2146
+ return { kind: "set", target: { kind: "curated", key: head2, value } };
2085
2147
  }
2086
2148
  function parseUnset(rest) {
2087
- const head = rest[0];
2088
- if (head === "--extra" || head === "-e") {
2149
+ const head2 = rest[0];
2150
+ if (head2 === "--extra" || head2 === "-e") {
2089
2151
  const label = rest.slice(1).join(" ").trim();
2090
2152
  if (!label) return { kind: "error", message: `usage: ${UNSET_EXTRA}` };
2091
2153
  if (label.length > MAX_LABEL) return labelCapError(label);
2092
2154
  return { kind: "unset", target: { kind: "extra", label } };
2093
2155
  }
2094
- if (!head) return { kind: "error", message: UNSET_USAGE };
2095
- if (!isCuratedKey(head)) return invalidKeyError(head, UNSET_EXTRA);
2096
- if (rest.length > 1) return { kind: "error", message: `usage: ymmv unset ${head}` };
2097
- return { kind: "unset", target: { kind: "curated", key: head } };
2156
+ if (!head2) return { kind: "error", message: UNSET_USAGE };
2157
+ if (!isCuratedKey(head2)) return invalidKeyError(head2, UNSET_EXTRA);
2158
+ if (rest.length > 1) return { kind: "error", message: `usage: ymmv unset ${head2}` };
2159
+ return { kind: "unset", target: { kind: "curated", key: head2 } };
2098
2160
  }
2099
2161
  function resolveArg(argv) {
2100
2162
  const first = argv[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ymmv-cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Publish and diff terminal-native developer tool-stack profiles at ymmv.fyi.",
5
5
  "type": "module",
6
6
  "bin": {