ymmv-cli 0.6.0 → 0.6.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 +81 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # ymmv-cli
2
2
 
3
- **Share your dev tool-stack from the terminal.**
3
+ **The tools you actually use. Publish from the CLI, diff against anyone's.**
4
4
 
5
- Editor, OS, shell, terminal, theme, AI tool (and more), published to a
5
+ Editor, OS, shell, terminal, theme (and more), published to a
6
6
  clean page at `ymmv.fyi/<handle>` in about 10 seconds. See a live one:
7
7
  [ymmv.fyi/bardisty](https://ymmv.fyi/bardisty).
8
8
 
package/dist/cli.js CHANGED
@@ -270,8 +270,8 @@ var MAX_PARSE_EXTRAS = 256;
270
270
  var MAX_PARSE_VALUE = 4096;
271
271
  var MAX_PARSE_LABEL = 4096;
272
272
  var ProfileParseError = class extends Error {
273
- constructor(message) {
274
- super(message);
273
+ constructor(message2) {
274
+ super(message2);
275
275
  this.name = "ProfileParseError";
276
276
  }
277
277
  };
@@ -286,8 +286,14 @@ function parseProfile(raw) {
286
286
  }
287
287
  if (typeof raw.handle !== "string")
288
288
  throw new ProfileParseError("handle is not a string");
289
+ if (raw.handle.length > MAX_PARSE_LABEL) {
290
+ throw new ProfileParseError(`handle exceeds ${MAX_PARSE_LABEL} chars`);
291
+ }
289
292
  if (typeof raw.updated_at !== "string")
290
293
  throw new ProfileParseError("updated_at is not a string");
294
+ if (raw.updated_at.length > MAX_PARSE_LABEL) {
295
+ throw new ProfileParseError(`updated_at exceeds ${MAX_PARSE_LABEL} chars`);
296
+ }
291
297
  if (!Array.isArray(raw.entries))
292
298
  throw new ProfileParseError("entries is not an array");
293
299
  if (raw.entries.length > MAX_PARSE_ENTRIES) {
@@ -299,6 +305,9 @@ function parseProfile(raw) {
299
305
  if (!isRecord(entry) || typeof entry.key !== "string" || typeof entry.value !== "string") {
300
306
  throw new ProfileParseError(`entry ${i} is not {key,value} strings`);
301
307
  }
308
+ if (entry.key.length > MAX_PARSE_LABEL) {
309
+ throw new ProfileParseError(`entry ${i} key exceeds ${MAX_PARSE_LABEL} chars`);
310
+ }
302
311
  if (entry.value.length > MAX_PARSE_VALUE) {
303
312
  throw new ProfileParseError(`entry ${i} value exceeds ${MAX_PARSE_VALUE} chars`);
304
313
  }
@@ -330,7 +339,7 @@ function parseProfile(raw) {
330
339
  }
331
340
 
332
341
  // ../shared/dist/reserved.js
333
- var RESERVED_ROUTES = ["api", "login", "logout"];
342
+ var RESERVED_ROUTES = ["404", "api", "login", "logout"];
334
343
  var CLI_VERBS = ["login", "logout", "set", "unset", "delete", "view", "help"];
335
344
  var RESERVED = [.../* @__PURE__ */ new Set([...RESERVED_ROUTES, ...CLI_VERBS])];
336
345
  var RESERVED_SET = new Set(RESERVED);
@@ -353,6 +362,10 @@ var NO_CODES = { amber: "", faint: "", bold: "", reset: "" };
353
362
  function palette(color) {
354
363
  return color ? CODES : NO_CODES;
355
364
  }
365
+ function message(text) {
366
+ return `
367
+ ${text.split(/\r?\n/).map((l) => l ? ` ${l}` : l).join("\n")}`;
368
+ }
356
369
  var ESC_INTRODUCERS = `${String.fromCharCode(27)}${String.fromCharCode(155)}`;
357
370
  var BEL = String.fromCharCode(7);
358
371
  var ANSI_RE = new RegExp(
@@ -449,7 +462,6 @@ function renderProfile(profile, opts) {
449
462
  if (!preview) {
450
463
  lines.push("", ` ${c.faint}updated ${relTime(profile.updated_at, opts.now)}${c.reset}`);
451
464
  }
452
- lines.push("");
453
465
  return lines.join("\n");
454
466
  }
455
467
  var MISSING = "\u2014";
@@ -499,20 +511,18 @@ function renderDiff(result, opts) {
499
511
  }
500
512
  }
501
513
  lines.push(...extrasBlock(result.extras, theirsLabel, mineLabel, c));
502
- lines.push("", ` ${c.faint}${result.differ} differ ${result.shared} shared${c.reset}`, "");
514
+ lines.push("", ` ${c.faint}${result.differ} differ ${result.shared} shared${c.reset}`);
503
515
  return lines.join("\n");
504
516
  }
505
517
  function nudge(color) {
506
518
  const c = palette(color);
507
519
  return `
508
- ${c.amber}publish yours to diff \u2192${c.reset} run ${c.bold}ymmv${c.reset}
509
- `;
520
+ ${c.amber}publish yours to diff \u2192${c.reset} run ${c.bold}ymmv${c.reset}`;
510
521
  }
511
522
  function notFound(handle, color, base) {
512
523
  return `
513
524
  no ymmv profile for "${sanitizeValue(handle)}" yet.
514
- publish one at ${link(base, color)} with: npx ymmv-cli
515
- `;
525
+ publish one at ${link(base, color)} with: npx ymmv-cli`;
516
526
  }
517
527
 
518
528
  // src/http.ts
@@ -574,7 +584,7 @@ async function mintYmmvToken(accessToken) {
574
584
  if (res.status === 429) {
575
585
  const retry = res.headers.get("retry-after");
576
586
  const msg = body.message ? wireText(body.message) : "Too many login attempts. Slow down and try again shortly";
577
- throw new Error(retry ? `${msg} (retry in ${retry}s)` : msg);
587
+ throw new Error(retry && /^\d+$/.test(retry) ? `${msg} (retry in ${retry}s)` : msg);
578
588
  }
579
589
  throw new Error(`login failed: ${res.status} ${wireText(body.error ?? "")}`.trim());
580
590
  }
@@ -754,11 +764,11 @@ async function login(deps = {}) {
754
764
  const c = palette(color);
755
765
  const verifyUri = /^https:\/\/github\.com\//.test(dc.verification_uri) ? link(dc.verification_uri, color) : sanitizeValue(dc.verification_uri);
756
766
  console.log(
757
- `
758
- Open ${verifyUri} and enter code: ${c.bold}${sanitizeValue(dc.user_code)}${c.reset}`
767
+ message(
768
+ `Open ${verifyUri} and enter code: ${c.bold}${sanitizeValue(dc.user_code)}${c.reset}
769
+ ${c.faint}waiting for GitHub approval\u2026 (Ctrl+C to cancel)${c.reset}`
770
+ )
759
771
  );
760
- console.log(` ${c.faint}waiting for GitHub approval\u2026 (Ctrl+C to cancel)${c.reset}
761
- `);
762
772
  const accessToken = await pollForToken(dc, deps);
763
773
  const { token, handle } = await mintYmmvToken(accessToken);
764
774
  try {
@@ -769,8 +779,9 @@ async function login(deps = {}) {
769
779
  throw e;
770
780
  }
771
781
  console.log(
772
- handle ? ` Logged in as ${handle}.
773
- ` : " Logged in. No handle bound (your GitHub username is a reserved word).\n"
782
+ message(
783
+ handle ? `Logged in as ${handle}.` : "Logged in. No handle bound (your GitHub username is a reserved word)."
784
+ )
774
785
  );
775
786
  }
776
787
 
@@ -783,7 +794,7 @@ async function rateLimitMessage(res) {
783
794
  if (typeof body?.message === "string" && body.message) msg = wireText(body.message);
784
795
  } catch {
785
796
  }
786
- return retry ? `${msg} (retry in ${retry}s)` : msg;
797
+ return retry && /^\d+$/.test(retry) ? `${msg} (retry in ${retry}s)` : msg;
787
798
  }
788
799
  async function ensureLogin() {
789
800
  const existing = await loadToken();
@@ -1201,15 +1212,25 @@ function makePrompter() {
1201
1212
  const answer = (await question(promptLine(label, def, color))).trim();
1202
1213
  return answer === "" ? clean ?? "" : answer;
1203
1214
  },
1215
+ // Prompts are output units (render.ts convention): confirm/choice open with the unit's one
1216
+ // blank line here — never in the caller's question string. Field ask()s stay tight: the
1217
+ // 13-key walk is a single unit opened by its hint line.
1204
1218
  async confirm(q, defYes) {
1205
- const answer = (await question(` ${q} ${c.faint}[${defYes ? "Y/n" : "y/N"}]${c.reset} `)).trim().toLowerCase();
1219
+ const answer = (await question(`
1220
+ ${q} ${c.faint}[${defYes ? "Y/n" : "y/N"}]${c.reset} `)).trim().toLowerCase();
1206
1221
  if (answer === "") return defYes;
1207
1222
  return answer === "y" || answer === "yes";
1208
1223
  },
1209
1224
  async choice(q, keys, def, hint) {
1225
+ let prefix = "\n";
1210
1226
  for (; ; ) {
1211
- const hit = matchChoice(await question(` ${q} ${c.faint}[${hint}]${c.reset} `), keys, def);
1227
+ const hit = matchChoice(
1228
+ await question(`${prefix} ${q} ${c.faint}[${hint}]${c.reset} `),
1229
+ keys,
1230
+ def
1231
+ );
1212
1232
  if (hit !== null) return hit;
1233
+ prefix = "";
1213
1234
  }
1214
1235
  },
1215
1236
  close() {
@@ -1223,7 +1244,9 @@ function makePrompter() {
1223
1244
  function requireHandle(cred) {
1224
1245
  if (cred.handle) return cred.handle;
1225
1246
  console.error(
1226
- "Your GitHub username is a reserved word, so no handle is bound. Rename on GitHub, then run `ymmv login` again."
1247
+ message(
1248
+ "Your GitHub username is a reserved word, so no handle is bound. Rename on GitHub, then run `ymmv login` again."
1249
+ )
1227
1250
  );
1228
1251
  process.exitCode = 1;
1229
1252
  return null;
@@ -1245,7 +1268,7 @@ function newProfile(handle, entries, extras) {
1245
1268
  };
1246
1269
  }
1247
1270
  function printPublished(res, color) {
1248
- console.log(`Published ${res.handle} \u2192 ${link(res.url, color)}`);
1271
+ console.log(message(`Published ${res.handle} \u2192 ${link(res.url, color)}`));
1249
1272
  }
1250
1273
  function pagePointer(handle) {
1251
1274
  const color = colorEnabled();
@@ -1254,8 +1277,7 @@ function pagePointer(handle) {
1254
1277
  }
1255
1278
  async function promptEntries(defaults, prompter) {
1256
1279
  const c = palette(colorEnabled());
1257
- console.log(`
1258
- ${c.faint}Enter to keep, "-" to clear${c.reset}`);
1280
+ console.log(message(`${c.faint}Enter to keep, "-" to clear${c.reset}`));
1259
1281
  const chosen = /* @__PURE__ */ new Map();
1260
1282
  for (const key of CURATED_KEYS) {
1261
1283
  const answer = (await prompter.ask(KEY_LABELS[key], defaults.get(key))).trim();
@@ -1266,7 +1288,9 @@ async function promptEntries(defaults, prompter) {
1266
1288
  }
1267
1289
  async function publish(io) {
1268
1290
  if (!io.interactive && !io.yes) {
1269
- console.error("Non-interactive publish needs -y (nothing publishes unconfirmed): ymmv -y");
1291
+ console.error(
1292
+ message("Non-interactive publish needs -y (nothing publishes unconfirmed): ymmv -y")
1293
+ );
1270
1294
  process.exitCode = 1;
1271
1295
  return;
1272
1296
  }
@@ -1291,11 +1315,12 @@ async function publish(io) {
1291
1315
  console.log(
1292
1316
  renderProfile(newProfile(handle, entries, extras), { color, site, mode: "preview" })
1293
1317
  );
1318
+ const notes = [];
1294
1319
  if (carried.length > 0) {
1295
1320
  const s = carried.length === 1 ? "" : "s";
1296
- console.log(`(+${carried.length} newer field${s} kept as-is; upgrade ymmv-cli to edit them)`);
1321
+ notes.push(`(+${carried.length} newer field${s} kept as-is; upgrade ymmv-cli to edit them)`);
1297
1322
  for (const e of carried) {
1298
- console.log(` ${sanitizeValue(e.key)} = ${sanitizeValue(e.value)}`);
1323
+ notes.push(` ${sanitizeValue(e.key)} = ${sanitizeValue(e.value)}`);
1299
1324
  }
1300
1325
  }
1301
1326
  const publishedLabels = new Set(
@@ -1304,9 +1329,10 @@ async function publish(io) {
1304
1329
  for (const x of extras) {
1305
1330
  if (publishedLabels.has(x.label.trim().toLowerCase())) {
1306
1331
  const label = sanitizeValue(x.label.trim());
1307
- console.log(`(extra "${label}" duplicates a curated field; ymmv unset --extra "${label}")`);
1332
+ notes.push(`(extra "${label}" duplicates a curated field; ymmv unset --extra "${label}")`);
1308
1333
  }
1309
1334
  }
1335
+ if (notes.length > 0) console.log(message(notes.join("\n")));
1310
1336
  };
1311
1337
  let values = defaults;
1312
1338
  const assemble = () => [...entriesFromMap(values), ...carried];
@@ -1332,14 +1358,15 @@ async function publish(io) {
1332
1358
  return;
1333
1359
  }
1334
1360
  if (ans === "n") {
1335
- console.log("Aborted. Nothing published.");
1361
+ console.log(message("Aborted. Nothing published."));
1336
1362
  return;
1337
1363
  }
1338
1364
  values = await promptEntries(values, io.prompter);
1339
1365
  }
1340
1366
  } catch (e) {
1341
1367
  if (e instanceof PromptAborted) {
1342
- console.log("\nAborted. Nothing published.");
1368
+ console.log(`
1369
+ ${message("Aborted. Nothing published.")}`);
1343
1370
  process.exitCode = 130;
1344
1371
  return;
1345
1372
  }
@@ -1379,7 +1406,7 @@ async function runSet(target) {
1379
1406
  const { entries, extras } = applySet(existing, target);
1380
1407
  const res = await publishProfile(newProfile(handle, entries, extras));
1381
1408
  const line = target.kind === "curated" ? `Set ${KEY_LABELS[target.key]} = ${target.value}.` : `Set extra ${target.label} = ${target.value}.`;
1382
- console.log(`${line}${pagePointer(res.handle)}`);
1409
+ console.log(message(`${line}${pagePointer(res.handle)}`));
1383
1410
  }
1384
1411
  async function runUnset(target) {
1385
1412
  const cred = await ensureLogin();
@@ -1388,19 +1415,21 @@ async function runUnset(target) {
1388
1415
  const existing = await fetchProfileJson(handle);
1389
1416
  assertHandleUnchanged(existing, handle);
1390
1417
  if (!existing) {
1391
- console.log("No profile yet. Run `ymmv` to publish one.");
1418
+ console.log(message("No profile yet. Run `ymmv` to publish one."));
1392
1419
  return;
1393
1420
  }
1394
1421
  const { entries, extras, removed } = applyUnset(existing, target);
1395
1422
  if (!removed) {
1396
1423
  console.log(
1397
- target.kind === "curated" ? `${KEY_LABELS[target.key]} is not set.` : `No extra "${target.label}".`
1424
+ message(
1425
+ target.kind === "curated" ? `${KEY_LABELS[target.key]} is not set.` : `No extra "${target.label}".`
1426
+ )
1398
1427
  );
1399
1428
  return;
1400
1429
  }
1401
1430
  const res = await publishProfile(newProfile(handle, entries, extras));
1402
1431
  const line = target.kind === "curated" ? `Removed ${KEY_LABELS[target.key]} (was "${sanitizeValue(removed.value)}").` : `Removed extra "${sanitizeValue(removed.label)}" (was "${sanitizeValue(removed.value)}").`;
1403
- console.log(`${line}${pagePointer(res.handle)}`);
1432
+ console.log(message(`${line}${pagePointer(res.handle)}`));
1404
1433
  }
1405
1434
  async function runDelete(io) {
1406
1435
  const cred = await ensureLogin();
@@ -1408,7 +1437,9 @@ async function runDelete(io) {
1408
1437
  if (!io.yes) {
1409
1438
  if (!io.interactive || !io.prompter) {
1410
1439
  console.error(
1411
- `Refusing to delete ${target} without confirmation. Re-run with -y to confirm: ymmv delete -y`
1440
+ message(
1441
+ `Refusing to delete ${target} without confirmation. Re-run with -y to confirm: ymmv delete -y`
1442
+ )
1412
1443
  );
1413
1444
  process.exitCode = 1;
1414
1445
  return;
@@ -1418,20 +1449,21 @@ async function runDelete(io) {
1418
1449
  go = await io.prompter.confirm(`Delete ${target}? This is permanent`, false);
1419
1450
  } catch (e) {
1420
1451
  if (e instanceof PromptAborted) {
1421
- console.log("\nCancelled. Nothing deleted.");
1452
+ console.log(`
1453
+ ${message("Cancelled. Nothing deleted.")}`);
1422
1454
  process.exitCode = 130;
1423
1455
  return;
1424
1456
  }
1425
1457
  throw e;
1426
1458
  }
1427
1459
  if (!go) {
1428
- console.log("Cancelled. Nothing deleted.");
1460
+ console.log(message("Cancelled. Nothing deleted."));
1429
1461
  return;
1430
1462
  }
1431
1463
  }
1432
1464
  await deleteProfile();
1433
1465
  await deleteToken();
1434
- console.log(`Deleted ${target}. Run \`ymmv\` to publish again.`);
1466
+ console.log(message(`Deleted ${target}. Run \`ymmv\` to publish again.`));
1435
1467
  }
1436
1468
 
1437
1469
  // src/resolve.ts
@@ -1541,7 +1573,9 @@ async function logout() {
1541
1573
  if (!stored) {
1542
1574
  const otherBase = await peekBase();
1543
1575
  console.log(
1544
- otherBase && otherBase !== BASE ? `Not logged in to ${BASE} (a token for ${otherBase} exists; set YMMV_API to that to log out of it).` : "Not logged in."
1576
+ message(
1577
+ otherBase && otherBase !== BASE ? `Not logged in to ${BASE} (a token for ${otherBase} exists; set YMMV_API to that to log out of it).` : "Not logged in."
1578
+ )
1545
1579
  );
1546
1580
  return;
1547
1581
  }
@@ -1550,13 +1584,15 @@ async function logout() {
1550
1584
  revoked = await revokeYmmvToken(stored.token);
1551
1585
  } catch {
1552
1586
  console.error(
1553
- "Couldn't reach the server to revoke. Your token is still active. Run `ymmv logout` again when connected."
1587
+ message(
1588
+ "Couldn't reach the server to revoke. Your token is still active. Run `ymmv logout` again when connected."
1589
+ )
1554
1590
  );
1555
1591
  process.exitCode = 1;
1556
1592
  return;
1557
1593
  }
1558
1594
  await deleteToken();
1559
- console.log(revoked ? "Logged out." : "Logged out (no active session on this server).");
1595
+ console.log(message(revoked ? "Logged out." : "Logged out (no active session on this server)."));
1560
1596
  }
1561
1597
  function printVersion() {
1562
1598
  try {
@@ -1596,7 +1632,7 @@ async function main(argv) {
1596
1632
  case "login": {
1597
1633
  await login();
1598
1634
  const c = palette(colorEnabled());
1599
- console.log(` ${c.faint}next: run ymmv to publish your stack${c.reset}`);
1635
+ console.log(message(`${c.faint}next: run ymmv to publish your stack${c.reset}`));
1600
1636
  break;
1601
1637
  }
1602
1638
  case "logout":
@@ -1609,7 +1645,7 @@ async function main(argv) {
1609
1645
  printVersion();
1610
1646
  break;
1611
1647
  case "error":
1612
- console.error(cmd.message);
1648
+ console.error(message(cmd.message));
1613
1649
  process.exitCode = 1;
1614
1650
  break;
1615
1651
  }
@@ -1617,6 +1653,8 @@ async function main(argv) {
1617
1653
 
1618
1654
  // src/cli.ts
1619
1655
  main(process.argv.slice(2)).catch((err) => {
1620
- console.error(err instanceof Error ? err.message : String(err));
1656
+ console.error(message(err instanceof Error ? err.message : String(err)));
1621
1657
  process.exitCode = 1;
1658
+ }).finally(() => {
1659
+ (process.exitCode ? process.stderr : process.stdout).write("\n");
1622
1660
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ymmv-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Publish and diff terminal-native developer tool-stack profiles at ymmv.fyi.",
5
5
  "type": "module",
6
6
  "bin": {