ymmv-cli 0.11.0 → 0.11.1
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 +5 -3
- package/dist/cli.js +180 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,9 +69,11 @@ Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`. Full c
|
|
|
69
69
|
|
|
70
70
|
- `NO_COLOR` disables color output (and `FORCE_COLOR=0`/`false` force-disables it).
|
|
71
71
|
- `YMMV_API` points the CLI at a different Worker (development/staging). Bare origin only. That
|
|
72
|
-
Worker must be deployed with or after this CLI release: `ymmv login` requires
|
|
73
|
-
login response carries and
|
|
74
|
-
|
|
72
|
+
Worker must be deployed with or after this CLI release: `ymmv login` requires every field the
|
|
73
|
+
login response carries (the account id, and on a re-login the server's answer to retiring the
|
|
74
|
+
previous token) and refuses (revoking what it minted) otherwise, `YMMV_TOKEN` needs the
|
|
75
|
+
server's account lookup, and `ymmv`, `ymmv set`, and `ymmv unset` need the profile lookup they
|
|
76
|
+
read from before writing.
|
|
75
77
|
- `YMMV_TOKEN` authenticates without a browser (CI and scripts, below). Takes precedence over
|
|
76
78
|
the stored login and is read-only: the CLI never writes, revokes, or deletes it, and
|
|
77
79
|
`ymmv login` / `ymmv logout` keep acting on the stored login. The CLI asks the server which
|
package/dist/cli.js
CHANGED
|
@@ -257,6 +257,34 @@ function diff(mine, theirs) {
|
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// ../shared/dist/display-url.js
|
|
261
|
+
var PARSER_STRIPS = /[\t\n\r]/g;
|
|
262
|
+
var HTTP_RE = /^https?:[/\\]*([^/?#\\]*)([/?#\\][\s\S]*)?$/i;
|
|
263
|
+
function trimC0(s) {
|
|
264
|
+
let start = 0;
|
|
265
|
+
let end = s.length;
|
|
266
|
+
while (start < end && s.charCodeAt(start) <= 32)
|
|
267
|
+
start++;
|
|
268
|
+
while (end > start && s.charCodeAt(end - 1) <= 32)
|
|
269
|
+
end--;
|
|
270
|
+
return s.slice(start, end);
|
|
271
|
+
}
|
|
272
|
+
function displayUrl(value) {
|
|
273
|
+
const raw = value.trim();
|
|
274
|
+
const input = trimC0(raw.replace(PARSER_STRIPS, ""));
|
|
275
|
+
const m = HTTP_RE.exec(input);
|
|
276
|
+
if (!m)
|
|
277
|
+
return raw;
|
|
278
|
+
let url;
|
|
279
|
+
try {
|
|
280
|
+
url = new URL(input);
|
|
281
|
+
} catch {
|
|
282
|
+
return raw;
|
|
283
|
+
}
|
|
284
|
+
const prefix = url.protocol === "http:" ? "http://" : "";
|
|
285
|
+
return `${prefix}${url.host}${m[2] ?? ""}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
260
288
|
// ../shared/dist/github.js
|
|
261
289
|
var GITHUB_CLIENT_ID = "Ov23liMoD29eizQcN1KZ";
|
|
262
290
|
function isGithubId(x) {
|
|
@@ -410,12 +438,10 @@ function colorEnabled() {
|
|
|
410
438
|
}
|
|
411
439
|
var OSC = `${ESC}]`;
|
|
412
440
|
var ST = `${ESC}\\`;
|
|
413
|
-
function displayUrl(value) {
|
|
414
|
-
return value.trim().replace(/^https:\/\/(?=.)/i, "");
|
|
415
|
-
}
|
|
416
441
|
var HTTP_URL_RE = /^https?:\/\/\S+$/i;
|
|
417
442
|
function isHttpUrl(value) {
|
|
418
|
-
|
|
443
|
+
const t = value.trim();
|
|
444
|
+
return HTTP_URL_RE.test(t) && URL.canParse(t);
|
|
419
445
|
}
|
|
420
446
|
var NO_OSC8_TERMS = /* @__PURE__ */ new Set(["linux", "dumb"]);
|
|
421
447
|
function link(url, color, term = process.env.TERM, label) {
|
|
@@ -695,19 +721,23 @@ function parseIdentity(data) {
|
|
|
695
721
|
}
|
|
696
722
|
return { github_id: id, handle };
|
|
697
723
|
}
|
|
724
|
+
function serverBehindHint() {
|
|
725
|
+
return process.env.YMMV_API ? "Point YMMV_API at an up-to-date server." : "The server is behind this CLI release; try again later.";
|
|
726
|
+
}
|
|
698
727
|
var MintRejected = class extends Error {
|
|
699
728
|
constructor(msg) {
|
|
700
729
|
super(msg);
|
|
701
730
|
this.name = "MintRejected";
|
|
702
731
|
}
|
|
703
732
|
};
|
|
704
|
-
async function mintYmmvToken(accessToken) {
|
|
733
|
+
async function mintYmmvToken(accessToken, revoke) {
|
|
705
734
|
const res = await safeFetch(
|
|
706
735
|
`${BASE}/api/v1/auth/token`,
|
|
707
736
|
{
|
|
708
737
|
method: "POST",
|
|
709
738
|
headers: { "content-type": "application/json" },
|
|
710
|
-
|
|
739
|
+
// JSON.stringify drops an undefined `revoke`: no key on the wire when there is nothing to retire.
|
|
740
|
+
body: JSON.stringify({ access_token: accessToken, revoke }),
|
|
711
741
|
// Never follow a redirect: a 30x must fail (the existing `!res.ok` guard rejects the resulting
|
|
712
742
|
// opaqueredirect), not re-POST the GitHub access_token to the redirect target or read a
|
|
713
743
|
// redirected 200 as a successful mint. Mirrors publish/delete in api.ts.
|
|
@@ -742,23 +772,29 @@ async function mintYmmvToken(accessToken) {
|
|
|
742
772
|
throw new Error(`login failed: ${res.status} ${wireText(slug)}`.trim());
|
|
743
773
|
}
|
|
744
774
|
const data = await bodyJson(res);
|
|
745
|
-
const unexpected = `Unexpected response from ${BASE}. Nothing was saved; run
|
|
775
|
+
const unexpected = `Unexpected response from ${BASE}. Nothing was saved` + (revoke === void 0 ? "" : " (the previous login on this machine may have been signed out)") + "; run `ymmv login` again.";
|
|
746
776
|
if (!data) throw new Error(unexpected);
|
|
747
777
|
const token = typeof data.token === "string" && data.token.length > 0 ? data.token : null;
|
|
748
778
|
const identity = parseIdentity(data);
|
|
749
|
-
|
|
779
|
+
const revoked = typeof data.revoked === "boolean" ? data.revoked : void 0;
|
|
780
|
+
const behind = token !== null && identity !== null && revoke !== void 0 && revoked === void 0 ? `${BASE} did not retire the previous login. ${serverBehindHint()} Nothing was saved; to log in anyway, run \`ymmv logout\` first.` : null;
|
|
781
|
+
if (token === null || identity === null || behind !== null) {
|
|
750
782
|
let revokeFailed = false;
|
|
751
783
|
if (token !== null) {
|
|
752
784
|
await revokeYmmvToken(token, AbortSignal.timeout(REVOKE_CAP_MS)).catch(() => {
|
|
753
785
|
revokeFailed = true;
|
|
754
786
|
});
|
|
755
787
|
}
|
|
788
|
+
const refusal = behind ?? unexpected;
|
|
756
789
|
throw new MintRejected(
|
|
757
|
-
revokeFailed ? `${
|
|
790
|
+
revokeFailed ? `${refusal} The login the server minted could not be revoked.` : refusal
|
|
758
791
|
);
|
|
759
792
|
}
|
|
760
793
|
return { token, ...identity };
|
|
761
794
|
}
|
|
795
|
+
function missingRouteError(missing) {
|
|
796
|
+
return new Error(`${BASE} has no ${missing}. ${serverBehindHint()}`);
|
|
797
|
+
}
|
|
762
798
|
async function fetchWhoami(token) {
|
|
763
799
|
const res = await safeFetch(
|
|
764
800
|
`${BASE}/api/v1/auth/whoami`,
|
|
@@ -773,9 +809,7 @@ async function fetchWhoami(token) {
|
|
|
773
809
|
if (!res.ok) {
|
|
774
810
|
if (res.status === 401) throw new Error(ENV_TOKEN_REJECTED_MINT_AGAIN);
|
|
775
811
|
if (res.status === 404) {
|
|
776
|
-
throw
|
|
777
|
-
`${BASE} has no identity lookup for YMMV_TOKEN. ${process.env.YMMV_API ? "Point YMMV_API at an up-to-date server." : "The server is behind this CLI release; try again later."}`
|
|
778
|
-
);
|
|
812
|
+
throw missingRouteError("identity lookup for YMMV_TOKEN");
|
|
779
813
|
}
|
|
780
814
|
throw new Error(
|
|
781
815
|
withRetryHint(
|
|
@@ -996,6 +1030,9 @@ async function pollForToken(dc, deps = {}) {
|
|
|
996
1030
|
}
|
|
997
1031
|
throw new Error("Device code expired. Run `ymmv login` again.");
|
|
998
1032
|
}
|
|
1033
|
+
function retirable(cred) {
|
|
1034
|
+
return cred != null && cred.base === BASE && cred.token.trim() !== "";
|
|
1035
|
+
}
|
|
999
1036
|
async function login(deps = {}) {
|
|
1000
1037
|
if (!process.stdin.isTTY) {
|
|
1001
1038
|
throw new Error(
|
|
@@ -1028,18 +1065,23 @@ ${c.faint}waiting for GitHub approval\u2026 (Ctrl+C to cancel)${c.reset}`
|
|
|
1028
1065
|
)
|
|
1029
1066
|
);
|
|
1030
1067
|
const accessToken = await pollForToken(dc, deps);
|
|
1031
|
-
const
|
|
1032
|
-
const
|
|
1068
|
+
const before = await peekCredential();
|
|
1069
|
+
const revoke = retirable(before) ? before.token : void 0;
|
|
1070
|
+
const minted = await mintYmmvToken(accessToken, revoke);
|
|
1071
|
+
const after = await peekCredential();
|
|
1033
1072
|
try {
|
|
1034
1073
|
await saveToken(minted);
|
|
1035
1074
|
} catch (e) {
|
|
1036
1075
|
await revokeYmmvToken(minted.token).catch(() => {
|
|
1076
|
+
console.error(
|
|
1077
|
+
message(`${c.faint}(the login the server minted could not be revoked)${c.reset}`)
|
|
1078
|
+
);
|
|
1037
1079
|
});
|
|
1038
1080
|
throw e;
|
|
1039
1081
|
}
|
|
1040
|
-
if (
|
|
1082
|
+
if (retirable(after) && after.token !== revoke && after.token !== minted.token) {
|
|
1041
1083
|
try {
|
|
1042
|
-
await revokeYmmvToken(
|
|
1084
|
+
await revokeYmmvToken(after.token);
|
|
1043
1085
|
} catch {
|
|
1044
1086
|
console.error(message(`${c.faint}(couldn't revoke the previous session's token)${c.reset}`));
|
|
1045
1087
|
}
|
|
@@ -1058,6 +1100,12 @@ var PublishRefusal = class extends Error {
|
|
|
1058
1100
|
this.name = "PublishRefusal";
|
|
1059
1101
|
}
|
|
1060
1102
|
};
|
|
1103
|
+
var ProfileChanged = class extends PublishRefusal {
|
|
1104
|
+
constructor() {
|
|
1105
|
+
super("Your profile changed since this command read it. Re-run the command.");
|
|
1106
|
+
this.name = "ProfileChanged";
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1061
1109
|
var NOT_PERSISTED = "Login did not persist a token. Run `ymmv login`.";
|
|
1062
1110
|
function assertVerified(cred) {
|
|
1063
1111
|
if (cred.source === "env" && cred.github_id === null) {
|
|
@@ -1099,12 +1147,16 @@ async function ensureLogin() {
|
|
|
1099
1147
|
if (!fresh) throw new Error(NOT_PERSISTED);
|
|
1100
1148
|
return fresh;
|
|
1101
1149
|
}
|
|
1102
|
-
async function publishProfile(profile, expected) {
|
|
1150
|
+
async function publishProfile(profile, expected, opts = {}) {
|
|
1103
1151
|
const send = (c) => safeFetch(
|
|
1104
1152
|
`${BASE}/api/v1/profile`,
|
|
1105
1153
|
{
|
|
1106
1154
|
method: "POST",
|
|
1107
|
-
headers: {
|
|
1155
|
+
headers: {
|
|
1156
|
+
"content-type": "application/json",
|
|
1157
|
+
authorization: `Bearer ${c.token}`,
|
|
1158
|
+
...opts.ifMatch ? { "if-match": opts.ifMatch } : {}
|
|
1159
|
+
},
|
|
1108
1160
|
// Send the login-bound handle, never a caller-guessed one — the official client never claims
|
|
1109
1161
|
// a handle it doesn't own.
|
|
1110
1162
|
body: JSON.stringify({ ...profile, handle: c.handle ?? profile.handle }),
|
|
@@ -1182,6 +1234,7 @@ async function publishProfile(profile, expected) {
|
|
|
1182
1234
|
);
|
|
1183
1235
|
}
|
|
1184
1236
|
}
|
|
1237
|
+
if (res.status === 412) throw new ProfileChanged();
|
|
1185
1238
|
if (res.status === 429) throw new Error(await rateLimitMessage(res));
|
|
1186
1239
|
if (!res.ok) {
|
|
1187
1240
|
const raw = await wireBody(res);
|
|
@@ -1207,6 +1260,40 @@ async function fetchProfileJson(handle) {
|
|
|
1207
1260
|
}
|
|
1208
1261
|
return parseProfile(await res.json());
|
|
1209
1262
|
}
|
|
1263
|
+
var HEADER_SAFE_TAG = /^[!#-~]{1,128}$/;
|
|
1264
|
+
async function fetchOwnProfile(cred) {
|
|
1265
|
+
assertVerified(cred);
|
|
1266
|
+
const res = await safeFetch(
|
|
1267
|
+
`${BASE}/api/v1/profile`,
|
|
1268
|
+
{
|
|
1269
|
+
headers: { authorization: `Bearer ${cred.token}` },
|
|
1270
|
+
// Never follow a redirect: the bearer must not travel to a redirect target, and a 30x→200
|
|
1271
|
+
// must not read as a profile. Same guard as whoami, mint, logout, publish, and delete.
|
|
1272
|
+
redirect: "manual"
|
|
1273
|
+
},
|
|
1274
|
+
BASE
|
|
1275
|
+
);
|
|
1276
|
+
if (res.status === 401) {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
cred.source === "env" ? ENV_TOKEN_REJECTED_MINT_AGAIN : "Session expired. Run `ymmv login`, then re-run the command."
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
if (res.status === 404) {
|
|
1282
|
+
const { slug } = wireErrorBody(await wireBody(res));
|
|
1283
|
+
if (slug === "not_found") return null;
|
|
1284
|
+
throw missingRouteError("profile lookup");
|
|
1285
|
+
}
|
|
1286
|
+
if (res.status === 429) throw new Error(await rateLimitMessage(res));
|
|
1287
|
+
if (!res.ok) {
|
|
1288
|
+
const raw = await wireBody(res);
|
|
1289
|
+
throw new Error(withRetryHint(`fetch failed: ${res.status} ${wireText(raw)}`, res));
|
|
1290
|
+
}
|
|
1291
|
+
const profile = parseProfile(await res.json());
|
|
1292
|
+
if (!HEADER_SAFE_TAG.test(profile.updated_at)) {
|
|
1293
|
+
throw new Error(`Unexpected response from ${BASE}. Check YMMV_API, or try again shortly.`);
|
|
1294
|
+
}
|
|
1295
|
+
return { profile, etag: `"${profile.updated_at}"` };
|
|
1296
|
+
}
|
|
1210
1297
|
async function deleteProfile(cred) {
|
|
1211
1298
|
assertVerified(cred);
|
|
1212
1299
|
const res = await safeFetch(
|
|
@@ -1666,11 +1753,13 @@ async function publish(io) {
|
|
|
1666
1753
|
return readFileSync(p, "utf8");
|
|
1667
1754
|
}
|
|
1668
1755
|
});
|
|
1669
|
-
const
|
|
1756
|
+
const own = await fetchOwnProfile(cred);
|
|
1757
|
+
const existing = own?.profile ?? null;
|
|
1670
1758
|
assertHandleUnchanged(existing, cred, handle);
|
|
1671
1759
|
const defaults = buildDefaults(existing, detected);
|
|
1672
|
-
|
|
1673
|
-
|
|
1760
|
+
let carried = unknownEntries(existing);
|
|
1761
|
+
let extras = existing?.extras ?? [];
|
|
1762
|
+
let ifMatch = own?.etag;
|
|
1674
1763
|
const color = colorEnabled();
|
|
1675
1764
|
const site = displayUrl(BASE);
|
|
1676
1765
|
const showCard = (entries) => {
|
|
@@ -1698,6 +1787,15 @@ async function publish(io) {
|
|
|
1698
1787
|
};
|
|
1699
1788
|
let values = defaults;
|
|
1700
1789
|
const assemble = () => [...entriesFromMap(values), ...carried];
|
|
1790
|
+
const edits = /* @__PURE__ */ new Map();
|
|
1791
|
+
const prompt = async (prompter) => {
|
|
1792
|
+
const before = values;
|
|
1793
|
+
values = await promptEntries(values, prompter);
|
|
1794
|
+
for (const key of CURATED_KEYS) {
|
|
1795
|
+
const after = values.get(key);
|
|
1796
|
+
if (after !== before.get(key)) edits.set(key, after);
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1701
1799
|
if (!io.interactive || !io.prompter || io.yes) {
|
|
1702
1800
|
const over = [...values].find(([, v]) => v.length > MAX_VALUE);
|
|
1703
1801
|
if (over) {
|
|
@@ -1711,11 +1809,14 @@ async function publish(io) {
|
|
|
1711
1809
|
}
|
|
1712
1810
|
const entries = assemble();
|
|
1713
1811
|
showCard(entries);
|
|
1714
|
-
printPublished(
|
|
1812
|
+
printPublished(
|
|
1813
|
+
await publishProfile(newProfile(handle, entries, extras), cred, { ifMatch }),
|
|
1814
|
+
color
|
|
1815
|
+
);
|
|
1715
1816
|
return;
|
|
1716
1817
|
}
|
|
1717
1818
|
try {
|
|
1718
|
-
if (!existing)
|
|
1819
|
+
if (!existing) await prompt(io.prompter);
|
|
1719
1820
|
for (; ; ) {
|
|
1720
1821
|
const entries = assemble();
|
|
1721
1822
|
showCard(entries);
|
|
@@ -1727,9 +1828,37 @@ async function publish(io) {
|
|
|
1727
1828
|
);
|
|
1728
1829
|
if (ans === "y") {
|
|
1729
1830
|
try {
|
|
1730
|
-
printPublished(
|
|
1831
|
+
printPublished(
|
|
1832
|
+
await publishProfile(newProfile(handle, entries, extras), cred, { ifMatch }),
|
|
1833
|
+
color
|
|
1834
|
+
);
|
|
1731
1835
|
return;
|
|
1732
1836
|
} catch (e) {
|
|
1837
|
+
if (e instanceof ProfileChanged) {
|
|
1838
|
+
console.error(
|
|
1839
|
+
message(
|
|
1840
|
+
"Your profile changed since this command read it. Reloaded the current version; your answers are kept."
|
|
1841
|
+
)
|
|
1842
|
+
);
|
|
1843
|
+
const liveCred = cred.source === "env" ? cred : await loadCredential() ?? cred;
|
|
1844
|
+
const fresh = await fetchOwnProfile(liveCred);
|
|
1845
|
+
if (!fresh) {
|
|
1846
|
+
throw new PublishRefusal(
|
|
1847
|
+
"Your profile was deleted since this command read it. Nothing was published. Run `ymmv` again to recreate it."
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
assertHandleUnchanged(fresh.profile, cred, handle);
|
|
1851
|
+
const rebased = buildDefaults(fresh.profile, detected);
|
|
1852
|
+
for (const [key, value] of edits) {
|
|
1853
|
+
if (value === void 0) rebased.delete(key);
|
|
1854
|
+
else rebased.set(key, value);
|
|
1855
|
+
}
|
|
1856
|
+
values = rebased;
|
|
1857
|
+
carried = unknownEntries(fresh.profile);
|
|
1858
|
+
extras = fresh.profile.extras;
|
|
1859
|
+
ifMatch = fresh.etag;
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1733
1862
|
if (e instanceof PromptAborted || e instanceof PublishRefusal) throw e;
|
|
1734
1863
|
const ambiguous = e instanceof NetworkError || isTimeoutError(e);
|
|
1735
1864
|
console.error(
|
|
@@ -1745,7 +1874,7 @@ ${ambiguous ? "The publish may not have completed. Your answers are kept." : "No
|
|
|
1745
1874
|
console.log(message("Aborted. Nothing published."));
|
|
1746
1875
|
return;
|
|
1747
1876
|
}
|
|
1748
|
-
|
|
1877
|
+
await prompt(io.prompter);
|
|
1749
1878
|
}
|
|
1750
1879
|
} catch (e) {
|
|
1751
1880
|
if (e instanceof PromptAborted) {
|
|
@@ -1812,7 +1941,8 @@ async function runSet(target) {
|
|
|
1812
1941
|
const cred = await ensureLogin();
|
|
1813
1942
|
const handle = requireHandle(cred);
|
|
1814
1943
|
if (!handle) return;
|
|
1815
|
-
const
|
|
1944
|
+
const own = await fetchOwnProfile(cred);
|
|
1945
|
+
const existing = own?.profile ?? null;
|
|
1816
1946
|
assertHandleUnchanged(existing, cred, handle);
|
|
1817
1947
|
const { entries, extras } = applySet(existing, target);
|
|
1818
1948
|
if (target.kind === "extra" && extras.length > MAX_EXTRAS) {
|
|
@@ -1824,15 +1954,27 @@ async function runSet(target) {
|
|
|
1824
1954
|
process.exitCode = 1;
|
|
1825
1955
|
return;
|
|
1826
1956
|
}
|
|
1827
|
-
const res = await publishProfile(newProfile(handle, entries, extras), cred
|
|
1957
|
+
const res = await publishProfile(newProfile(handle, entries, extras), cred, {
|
|
1958
|
+
ifMatch: own?.etag
|
|
1959
|
+
});
|
|
1828
1960
|
const line = target.kind === "curated" ? `Set ${KEY_LABELS[target.key]} = ${target.value}.` : `Set extra ${target.label} = ${target.value}.`;
|
|
1829
1961
|
console.log(message(`${line}${pagePointer(res.handle)}`));
|
|
1830
1962
|
}
|
|
1963
|
+
var PASTE_SAFE_LABEL = /^[\p{L}\p{N} _.+/:-]+$/u;
|
|
1964
|
+
function extraHint(existing, label) {
|
|
1965
|
+
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";
|
|
1969
|
+
return `
|
|
1970
|
+
(unset takes just the label: ymmv unset --extra "${shown}")`;
|
|
1971
|
+
}
|
|
1831
1972
|
async function runUnset(target) {
|
|
1832
1973
|
const cred = await ensureLogin();
|
|
1833
1974
|
const handle = requireHandle(cred);
|
|
1834
1975
|
if (!handle) return;
|
|
1835
|
-
const
|
|
1976
|
+
const own = await fetchOwnProfile(cred);
|
|
1977
|
+
const existing = own?.profile ?? null;
|
|
1836
1978
|
assertHandleUnchanged(existing, cred, handle);
|
|
1837
1979
|
if (!existing) {
|
|
1838
1980
|
console.log(message("No profile yet. Run `ymmv` to publish one."));
|
|
@@ -1840,14 +1982,13 @@ async function runUnset(target) {
|
|
|
1840
1982
|
}
|
|
1841
1983
|
const { entries, extras, removed } = applyUnset(existing, target);
|
|
1842
1984
|
if (!removed) {
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
target.kind === "curated" ? `${KEY_LABELS[target.key]} is not set.` : `No extra "${target.label}".`
|
|
1846
|
-
)
|
|
1847
|
-
);
|
|
1985
|
+
const line2 = target.kind === "curated" ? `${KEY_LABELS[target.key]} is not set.` : `No extra "${sanitizeValue(target.label)}".${extraHint(existing, target.label)}`;
|
|
1986
|
+
console.log(message(line2));
|
|
1848
1987
|
return;
|
|
1849
1988
|
}
|
|
1850
|
-
const res = await publishProfile(newProfile(handle, entries, extras), cred
|
|
1989
|
+
const res = await publishProfile(newProfile(handle, entries, extras), cred, {
|
|
1990
|
+
ifMatch: own?.etag
|
|
1991
|
+
});
|
|
1851
1992
|
const line = target.kind === "curated" ? `Removed ${KEY_LABELS[target.key]} (was "${sanitizeValue(removed.value)}").` : `Removed extra "${sanitizeValue(removed.label)}" (was "${sanitizeValue(removed.value)}").`;
|
|
1852
1993
|
console.log(message(`${line}${pagePointer(res.handle)}`));
|
|
1853
1994
|
}
|
|
@@ -1947,12 +2088,7 @@ function parseUnset(rest) {
|
|
|
1947
2088
|
if (head === "--extra" || head === "-e") {
|
|
1948
2089
|
const label = rest.slice(1).join(" ").trim();
|
|
1949
2090
|
if (!label) return { kind: "error", message: `usage: ${UNSET_EXTRA}` };
|
|
1950
|
-
if (label.
|
|
1951
|
-
return {
|
|
1952
|
-
kind: "error",
|
|
1953
|
-
message: 'unset takes just the label: ymmv unset --extra "Keyboard"'
|
|
1954
|
-
};
|
|
1955
|
-
}
|
|
2091
|
+
if (label.length > MAX_LABEL) return labelCapError(label);
|
|
1956
2092
|
return { kind: "unset", target: { kind: "extra", label } };
|
|
1957
2093
|
}
|
|
1958
2094
|
if (!head) return { kind: "error", message: UNSET_USAGE };
|
|
@@ -2317,7 +2453,9 @@ ${c.faint}Curated keys:${c.reset} editor, os, shell, prompt, terminal, browser,
|
|
|
2317
2453
|
font, theme, multiplexer, version-manager, dotfiles, ai-tool`;
|
|
2318
2454
|
async function logout() {
|
|
2319
2455
|
const stored = await loadToken();
|
|
2320
|
-
|
|
2456
|
+
const leftover = stored ? null : await peekCredential();
|
|
2457
|
+
const token = stored?.token ?? (retirable(leftover) ? leftover.token : null);
|
|
2458
|
+
if (token === null) {
|
|
2321
2459
|
const otherBase = await peekBase();
|
|
2322
2460
|
console.log(
|
|
2323
2461
|
message(
|
|
@@ -2328,7 +2466,7 @@ async function logout() {
|
|
|
2328
2466
|
}
|
|
2329
2467
|
let revoked;
|
|
2330
2468
|
try {
|
|
2331
|
-
revoked = await revokeYmmvToken(
|
|
2469
|
+
revoked = await revokeYmmvToken(token);
|
|
2332
2470
|
} catch (e) {
|
|
2333
2471
|
console.error(
|
|
2334
2472
|
message(
|