ymmv-cli 0.9.0 → 0.11.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 +20 -11
- package/dist/cli.js +200 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,15 +68,20 @@ Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`. Full c
|
|
|
68
68
|
## Environment variables
|
|
69
69
|
|
|
70
70
|
- `NO_COLOR` disables color output (and `FORCE_COLOR=0`/`false` force-disables it).
|
|
71
|
-
- `YMMV_API` points the CLI at a different Worker (development/staging). Bare origin only.
|
|
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 the account id the
|
|
73
|
+
login response carries and refuses (revoking what it minted) otherwise, and `YMMV_TOKEN` needs
|
|
74
|
+
the server's account lookup.
|
|
72
75
|
- `YMMV_TOKEN` authenticates without a browser (CI and scripts, below). Takes precedence over
|
|
73
76
|
the stored login and is read-only: the CLI never writes, revokes, or deletes it, and
|
|
74
|
-
`ymmv login` / `ymmv logout` keep acting on the stored login.
|
|
75
|
-
|
|
76
|
-
`YMMV_API` selects, so set the
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`
|
|
77
|
+
`ymmv login` / `ymmv logout` keep acting on the stored login. The CLI asks the server which
|
|
78
|
+
account the token belongs to, so every command, including the you-side of a `ymmv <handle>`
|
|
79
|
+
diff, runs as that account. The token is sent to the server `YMMV_API` selects, so set the
|
|
80
|
+
two together.
|
|
81
|
+
- `YMMV_HANDLE` is optional. When set, it must name the GitHub username `YMMV_TOKEN` belongs
|
|
82
|
+
to: if the server reports a different account, `ymmv -y`, `ymmv set`/`unset`, and
|
|
83
|
+
`ymmv delete` refuse before sending anything, and `ymmv <handle>` shows the profile without
|
|
84
|
+
a diff. Ignored without `YMMV_TOKEN`.
|
|
80
85
|
- `YMMV_NO_UPDATE_CHECK` disables the startup check for newer releases (the ecosystem-standard
|
|
81
86
|
`NO_UPDATE_NOTIFIER` works too). The check is also off automatically under `CI`, in pipes,
|
|
82
87
|
and in dev builds; it never blocks or fails a command.
|
|
@@ -89,7 +94,8 @@ Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`. Full c
|
|
|
89
94
|
2. Copy the `token` value from the token file:
|
|
90
95
|
`~/.config/ymmv/token.json` (Linux), `~/Library/Preferences/ymmv/token.json` (macOS),
|
|
91
96
|
`%APPDATA%\ymmv\Config\token.json` (Windows).
|
|
92
|
-
3. Set it as a CI secret named `YMMV_TOKEN
|
|
97
|
+
3. Set it as a CI secret named `YMMV_TOKEN`. Setting `YMMV_HANDLE` to your GitHub username is
|
|
98
|
+
optional, and makes the job fail if the secret ever holds another account's token.
|
|
93
99
|
4. Run `npx ymmv-cli@latest -y` in the job.
|
|
94
100
|
|
|
95
101
|
Two things to know:
|
|
@@ -98,9 +104,12 @@ Two things to know:
|
|
|
98
104
|
runs on. Values you already published always win, but curated keys you have never set get the
|
|
99
105
|
CI runner's detected values (its OS, shell, and so on). For targeted updates from CI, prefer
|
|
100
106
|
`ymmv set <key> <value>`.
|
|
101
|
-
- A rejected or revoked `YMMV_TOKEN` fails
|
|
102
|
-
to an interactive login, and the stored login
|
|
103
|
-
|
|
107
|
+
- A rejected or revoked `YMMV_TOKEN` fails `ymmv -y`, `ymmv set`/`unset`, and `ymmv delete` with
|
|
108
|
+
an error naming the variable; nothing falls back to an interactive login, and the stored login
|
|
109
|
+
file on the runner (if any) is left untouched. `ymmv <handle>` still shows the profile, with the
|
|
110
|
+
reason there is no diff on stderr, and exits 0.
|
|
111
|
+
`ymmv delete` acts on the account the token is bound to and names that account's page when
|
|
112
|
+
it asks for confirmation.
|
|
104
113
|
|
|
105
114
|
## License
|
|
106
115
|
|
package/dist/cli.js
CHANGED
|
@@ -259,6 +259,9 @@ function diff(mine, theirs) {
|
|
|
259
259
|
|
|
260
260
|
// ../shared/dist/github.js
|
|
261
261
|
var GITHUB_CLIENT_ID = "Ov23liMoD29eizQcN1KZ";
|
|
262
|
+
function isGithubId(x) {
|
|
263
|
+
return typeof x === "number" && Number.isSafeInteger(x) && x > 0;
|
|
264
|
+
}
|
|
262
265
|
|
|
263
266
|
// ../shared/dist/types.js
|
|
264
267
|
var SCHEMA_VERSION = 1;
|
|
@@ -390,10 +393,7 @@ var CTRL_RE = new RegExp(
|
|
|
390
393
|
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
|
|
391
394
|
"g"
|
|
392
395
|
);
|
|
393
|
-
var BIDI_RE = new RegExp(
|
|
394
|
-
`[${String.fromCharCode(8234)}-${String.fromCharCode(8238)}${String.fromCharCode(8294)}-${String.fromCharCode(8297)}${String.fromCharCode(8206)}${String.fromCharCode(8207)}]`,
|
|
395
|
-
"g"
|
|
396
|
-
);
|
|
396
|
+
var BIDI_RE = new RegExp("\\p{Bidi_Control}", "gu");
|
|
397
397
|
function sanitizeValue(value) {
|
|
398
398
|
return value.replace(ANSI_RE, "").replace(CTRL_RE, "").replace(BIDI_RE, "");
|
|
399
399
|
}
|
|
@@ -545,6 +545,7 @@ function notFound(handle, color, base) {
|
|
|
545
545
|
|
|
546
546
|
// src/http.ts
|
|
547
547
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
548
|
+
var REVOKE_CAP_MS = 5e3;
|
|
548
549
|
var TIMEOUT_TEXT = "request timed out";
|
|
549
550
|
function isTimeoutError(err) {
|
|
550
551
|
return err instanceof Error && (err.name === "TimeoutError" || err.cause instanceof Error && err.cause.name === "TimeoutError");
|
|
@@ -683,6 +684,23 @@ async function bodyJson(res) {
|
|
|
683
684
|
return null;
|
|
684
685
|
}
|
|
685
686
|
}
|
|
687
|
+
var ENV_TOKEN_REJECTED = "The server rejected the token in YMMV_TOKEN (invalid or revoked).";
|
|
688
|
+
var ENV_TOKEN_REJECTED_MINT_AGAIN = `${ENV_TOKEN_REJECTED} Mint a new one with \`ymmv login\` on an interactive machine and update YMMV_TOKEN.`;
|
|
689
|
+
function parseIdentity(data) {
|
|
690
|
+
if (typeof data !== "object" || data === null) return null;
|
|
691
|
+
const { handle: rawHandle, github_id: id } = data;
|
|
692
|
+
const handle = rawHandle === null ? null : typeof rawHandle === "string" ? sanitizeValue(rawHandle) : void 0;
|
|
693
|
+
if (handle === void 0 || handle !== null && !isValidHandle(handle) || !isGithubId(id)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
return { github_id: id, handle };
|
|
697
|
+
}
|
|
698
|
+
var MintRejected = class extends Error {
|
|
699
|
+
constructor(msg) {
|
|
700
|
+
super(msg);
|
|
701
|
+
this.name = "MintRejected";
|
|
702
|
+
}
|
|
703
|
+
};
|
|
686
704
|
async function mintYmmvToken(accessToken) {
|
|
687
705
|
const res = await safeFetch(
|
|
688
706
|
`${BASE}/api/v1/auth/token`,
|
|
@@ -724,19 +742,61 @@ async function mintYmmvToken(accessToken) {
|
|
|
724
742
|
throw new Error(`login failed: ${res.status} ${wireText(slug)}`.trim());
|
|
725
743
|
}
|
|
726
744
|
const data = await bodyJson(res);
|
|
727
|
-
|
|
745
|
+
const unexpected = `Unexpected response from ${BASE}. Nothing was saved; run \`ymmv login\` again.`;
|
|
746
|
+
if (!data) throw new Error(unexpected);
|
|
747
|
+
const token = typeof data.token === "string" && data.token.length > 0 ? data.token : null;
|
|
748
|
+
const identity = parseIdentity(data);
|
|
749
|
+
if (token === null || identity === null) {
|
|
750
|
+
let revokeFailed = false;
|
|
751
|
+
if (token !== null) {
|
|
752
|
+
await revokeYmmvToken(token, AbortSignal.timeout(REVOKE_CAP_MS)).catch(() => {
|
|
753
|
+
revokeFailed = true;
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
throw new MintRejected(
|
|
757
|
+
revokeFailed ? `${unexpected} The login the server minted could not be revoked.` : unexpected
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
return { token, ...identity };
|
|
761
|
+
}
|
|
762
|
+
async function fetchWhoami(token) {
|
|
763
|
+
const res = await safeFetch(
|
|
764
|
+
`${BASE}/api/v1/auth/whoami`,
|
|
765
|
+
{
|
|
766
|
+
headers: { authorization: `Bearer ${token}` },
|
|
767
|
+
// Never follow a redirect: the bearer must not travel to a redirect target, and a 30x→200
|
|
768
|
+
// must not read as a verified identity. Same guard as mint, logout, publish, and delete.
|
|
769
|
+
redirect: "manual"
|
|
770
|
+
},
|
|
771
|
+
BASE
|
|
772
|
+
);
|
|
773
|
+
if (!res.ok) {
|
|
774
|
+
if (res.status === 401) throw new Error(ENV_TOKEN_REJECTED_MINT_AGAIN);
|
|
775
|
+
if (res.status === 404) {
|
|
776
|
+
throw new Error(
|
|
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
|
+
);
|
|
779
|
+
}
|
|
728
780
|
throw new Error(
|
|
729
|
-
|
|
781
|
+
withRetryHint(
|
|
782
|
+
await serverMessage(res) ?? (res.status === 429 ? "rate limited, too many requests" : `The server couldn't look up your token (${res.status}). Try again shortly.`),
|
|
783
|
+
res
|
|
784
|
+
)
|
|
730
785
|
);
|
|
731
786
|
}
|
|
732
|
-
|
|
787
|
+
const identity = parseIdentity(await bodyJson(res));
|
|
788
|
+
if (!identity) {
|
|
789
|
+
throw new Error(`Unexpected response from ${BASE}. Check YMMV_API, or try again shortly.`);
|
|
790
|
+
}
|
|
791
|
+
return identity;
|
|
733
792
|
}
|
|
734
|
-
async function revokeYmmvToken(token) {
|
|
793
|
+
async function revokeYmmvToken(token, signal) {
|
|
735
794
|
const res = await safeFetch(
|
|
736
795
|
`${BASE}/api/v1/auth/logout`,
|
|
737
796
|
{
|
|
738
797
|
method: "POST",
|
|
739
798
|
headers: { authorization: `Bearer ${token}` },
|
|
799
|
+
signal,
|
|
740
800
|
// Never follow a redirect: a 30x→200 must not read as a successful revoke (which would delete
|
|
741
801
|
// the local file while the server token stays live). Same guard as mint + publish/delete.
|
|
742
802
|
redirect: "manual"
|
|
@@ -769,7 +829,12 @@ async function saveToken(data) {
|
|
|
769
829
|
if (process.platform !== "win32") await chmod(dir, 448).catch(() => {
|
|
770
830
|
});
|
|
771
831
|
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
772
|
-
const stored = {
|
|
832
|
+
const stored = {
|
|
833
|
+
base: BASE,
|
|
834
|
+
token: data.token,
|
|
835
|
+
handle: data.handle,
|
|
836
|
+
github_id: data.github_id
|
|
837
|
+
};
|
|
773
838
|
try {
|
|
774
839
|
await writeFile(tmp, JSON.stringify(stored), { mode: 384 });
|
|
775
840
|
if (process.platform !== "win32") await chmod(tmp, 384);
|
|
@@ -790,10 +855,17 @@ async function readTokenFile() {
|
|
|
790
855
|
}
|
|
791
856
|
async function loadToken() {
|
|
792
857
|
const parsed = await readTokenFile();
|
|
793
|
-
|
|
858
|
+
const rawId = parsed?.github_id;
|
|
859
|
+
const idOk = rawId === void 0 || rawId === null || isGithubId(rawId);
|
|
860
|
+
if (!parsed || parsed.base !== BASE || typeof parsed.token !== "string" || parsed.token === "" || parsed.handle !== null && typeof parsed.handle !== "string" || !idOk) {
|
|
794
861
|
return null;
|
|
795
862
|
}
|
|
796
|
-
return
|
|
863
|
+
return {
|
|
864
|
+
base: parsed.base,
|
|
865
|
+
token: parsed.token,
|
|
866
|
+
handle: parsed.handle === "" ? null : parsed.handle,
|
|
867
|
+
github_id: isGithubId(rawId) ? rawId : null
|
|
868
|
+
};
|
|
797
869
|
}
|
|
798
870
|
async function peekCredential() {
|
|
799
871
|
const parsed = await readTokenFile();
|
|
@@ -802,7 +874,13 @@ async function peekCredential() {
|
|
|
802
874
|
async function loadCredential() {
|
|
803
875
|
const envToken = process.env.YMMV_TOKEN || "";
|
|
804
876
|
if (envToken !== "") {
|
|
805
|
-
return {
|
|
877
|
+
return {
|
|
878
|
+
base: BASE,
|
|
879
|
+
token: envToken,
|
|
880
|
+
handle: process.env.YMMV_HANDLE || null,
|
|
881
|
+
github_id: null,
|
|
882
|
+
source: "env"
|
|
883
|
+
};
|
|
806
884
|
}
|
|
807
885
|
const stored = await loadToken();
|
|
808
886
|
return stored ? { ...stored, source: "file" } : null;
|
|
@@ -950,16 +1028,16 @@ ${c.faint}waiting for GitHub approval\u2026 (Ctrl+C to cancel)${c.reset}`
|
|
|
950
1028
|
)
|
|
951
1029
|
);
|
|
952
1030
|
const accessToken = await pollForToken(dc, deps);
|
|
953
|
-
const
|
|
1031
|
+
const minted = await mintYmmvToken(accessToken);
|
|
954
1032
|
const replaced = await peekCredential();
|
|
955
1033
|
try {
|
|
956
|
-
await saveToken(
|
|
1034
|
+
await saveToken(minted);
|
|
957
1035
|
} catch (e) {
|
|
958
|
-
await revokeYmmvToken(token).catch(() => {
|
|
1036
|
+
await revokeYmmvToken(minted.token).catch(() => {
|
|
959
1037
|
});
|
|
960
1038
|
throw e;
|
|
961
1039
|
}
|
|
962
|
-
if (replaced && replaced.base === BASE && replaced.token !== token) {
|
|
1040
|
+
if (replaced && replaced.base === BASE && replaced.token !== minted.token) {
|
|
963
1041
|
try {
|
|
964
1042
|
await revokeYmmvToken(replaced.token);
|
|
965
1043
|
} catch {
|
|
@@ -968,7 +1046,7 @@ ${c.faint}waiting for GitHub approval\u2026 (Ctrl+C to cancel)${c.reset}`
|
|
|
968
1046
|
}
|
|
969
1047
|
console.log(
|
|
970
1048
|
message(
|
|
971
|
-
handle ? `Logged in as ${handle}.` : "Logged in. No handle bound (your GitHub username is a reserved word)."
|
|
1049
|
+
minted.handle ? `Logged in as ${minted.handle}.` : "Logged in. No handle bound (your GitHub username is a reserved word)."
|
|
972
1050
|
)
|
|
973
1051
|
);
|
|
974
1052
|
}
|
|
@@ -980,19 +1058,48 @@ var PublishRefusal = class extends Error {
|
|
|
980
1058
|
this.name = "PublishRefusal";
|
|
981
1059
|
}
|
|
982
1060
|
};
|
|
983
|
-
var
|
|
1061
|
+
var NOT_PERSISTED = "Login did not persist a token. Run `ymmv login`.";
|
|
1062
|
+
function assertVerified(cred) {
|
|
1063
|
+
if (cred.source === "env" && cred.github_id === null) {
|
|
1064
|
+
throw new PublishRefusal(
|
|
1065
|
+
"Internal error: the YMMV_TOKEN credential was not verified. This is a ymmv-cli bug; please report it at https://github.com/ymmv-fyi/ymmv/issues."
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
984
1069
|
async function rateLimitMessage(res) {
|
|
985
1070
|
return withRetryHint(await serverMessage(res) ?? "rate limited, too many requests", res);
|
|
986
1071
|
}
|
|
1072
|
+
async function loginOrRefuse() {
|
|
1073
|
+
try {
|
|
1074
|
+
await login();
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
if (e instanceof MintRejected) throw new PublishRefusal(e.message);
|
|
1077
|
+
throw e;
|
|
1078
|
+
}
|
|
1079
|
+
const fresh = await loadCredential();
|
|
1080
|
+
if (!fresh) throw new PublishRefusal(NOT_PERSISTED);
|
|
1081
|
+
return fresh;
|
|
1082
|
+
}
|
|
1083
|
+
async function verifyEnvCredential(cred) {
|
|
1084
|
+
const identity = await fetchWhoami(cred.token);
|
|
1085
|
+
const claimed = cred.handle;
|
|
1086
|
+
if (claimed !== null && claimed.toLowerCase() !== identity.handle?.toLowerCase()) {
|
|
1087
|
+
const shown = `YMMV_HANDLE is "${sanitizeValue(claimed)}"`;
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
identity.handle === null ? `${shown} but the YMMV_TOKEN account has no handle bound.` : `${shown} but YMMV_TOKEN belongs to "${identity.handle}". Fix or unset YMMV_HANDLE.`
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
return { ...cred, handle: identity.handle, github_id: identity.github_id };
|
|
1093
|
+
}
|
|
987
1094
|
async function ensureLogin() {
|
|
988
1095
|
const existing = await loadCredential();
|
|
989
|
-
if (existing) return existing;
|
|
1096
|
+
if (existing) return existing.source === "env" ? verifyEnvCredential(existing) : existing;
|
|
990
1097
|
await login();
|
|
991
1098
|
const fresh = await loadCredential();
|
|
992
|
-
if (!fresh) throw new Error(
|
|
1099
|
+
if (!fresh) throw new Error(NOT_PERSISTED);
|
|
993
1100
|
return fresh;
|
|
994
1101
|
}
|
|
995
|
-
async function publishProfile(profile) {
|
|
1102
|
+
async function publishProfile(profile, expected) {
|
|
996
1103
|
const send = (c) => safeFetch(
|
|
997
1104
|
`${BASE}/api/v1/profile`,
|
|
998
1105
|
{
|
|
@@ -1006,8 +1113,15 @@ async function publishProfile(profile) {
|
|
|
1006
1113
|
},
|
|
1007
1114
|
BASE
|
|
1008
1115
|
);
|
|
1009
|
-
|
|
1010
|
-
|
|
1116
|
+
assertVerified(expected);
|
|
1117
|
+
let cred = expected.source === "env" ? expected : await loadCredential();
|
|
1118
|
+
if (!cred) {
|
|
1119
|
+
console.log(message("Not logged in. Logging in to publish."));
|
|
1120
|
+
cred = await loginOrRefuse();
|
|
1121
|
+
}
|
|
1122
|
+
assertVerified(cred);
|
|
1123
|
+
const idDrifted = expected.source === "file" && (expected.github_id === null ? cred.github_id !== null : cred.github_id !== expected.github_id);
|
|
1124
|
+
if ((cred.handle ?? "").toLowerCase() !== profile.handle.toLowerCase() || idDrifted) {
|
|
1011
1125
|
throw new PublishRefusal(
|
|
1012
1126
|
"The stored login changed while this command was running. Re-run it under the current account."
|
|
1013
1127
|
);
|
|
@@ -1016,23 +1130,43 @@ async function publishProfile(profile) {
|
|
|
1016
1130
|
if (res.status === 401 || res.status === 409) {
|
|
1017
1131
|
if (cred.source === "env") {
|
|
1018
1132
|
throw new PublishRefusal(
|
|
1019
|
-
res.status === 401 ?
|
|
1133
|
+
res.status === 401 ? ENV_TOKEN_REJECTED_MINT_AGAIN : (
|
|
1134
|
+
// The handle came from whoami moments ago, so a 409 means the bind changed in between
|
|
1135
|
+
// (a re-login on another machine after a GitHub rename). A fresh run looks it up again.
|
|
1136
|
+
"The server no longer accepts this handle for the YMMV_TOKEN account. Re-run the command."
|
|
1137
|
+
)
|
|
1020
1138
|
);
|
|
1021
1139
|
}
|
|
1022
1140
|
const was401 = res.status === 401;
|
|
1141
|
+
const before = cred;
|
|
1023
1142
|
console.log(
|
|
1024
1143
|
message(
|
|
1025
1144
|
was401 ? "Session expired. Logging in again to retry the publish." : "The server no longer recognizes your handle. Logging in again to retry the publish."
|
|
1026
1145
|
)
|
|
1027
1146
|
);
|
|
1028
1147
|
if (was401) await deleteToken();
|
|
1029
|
-
await
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
);
|
|
1148
|
+
cred = await loginOrRefuse();
|
|
1149
|
+
const known = before.github_id !== null;
|
|
1150
|
+
const idChanged = known && cred.github_id !== before.github_id;
|
|
1151
|
+
const sameHandle = (cred.handle ?? "").toLowerCase() === profile.handle.toLowerCase();
|
|
1152
|
+
if (!sameHandle || idChanged) {
|
|
1153
|
+
const mine = sanitizeValue(profile.handle);
|
|
1154
|
+
const bound = cred.handle === null ? null : sanitizeValue(cred.handle);
|
|
1155
|
+
const reRun = "Nothing was published. Re-run under the account you meant.";
|
|
1156
|
+
const otherAccount = `The re-login bound a different account ("${bound}", not "${mine}"). ${reRun}`;
|
|
1157
|
+
if (idChanged) {
|
|
1158
|
+
throw new PublishRefusal(
|
|
1159
|
+
sameHandle ? `The handle "${mine}" now belongs to a different GitHub account. ${reRun}` : bound === null ? `The re-login bound a different GitHub account. ${reRun}` : otherAccount
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
if (bound === null) {
|
|
1163
|
+
throw new PublishRefusal(
|
|
1164
|
+
"Your login no longer binds a handle (your GitHub username is a reserved word). Nothing was published."
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const rebound = `Your login now binds "${bound}". Nothing was published. Re-run the command to publish under it.`;
|
|
1168
|
+
if (known) throw new PublishRefusal(rebound);
|
|
1169
|
+
throw new PublishRefusal(was401 ? otherAccount : rebound);
|
|
1036
1170
|
}
|
|
1037
1171
|
res = await send(cred);
|
|
1038
1172
|
if (res.status === 401) throw new PublishRefusal("Authentication failed. Run `ymmv login`.");
|
|
@@ -1074,6 +1208,7 @@ async function fetchProfileJson(handle) {
|
|
|
1074
1208
|
return parseProfile(await res.json());
|
|
1075
1209
|
}
|
|
1076
1210
|
async function deleteProfile(cred) {
|
|
1211
|
+
assertVerified(cred);
|
|
1077
1212
|
const res = await safeFetch(
|
|
1078
1213
|
`${BASE}/api/v1/profile`,
|
|
1079
1214
|
{
|
|
@@ -1460,16 +1595,16 @@ function requireHandle(cred) {
|
|
|
1460
1595
|
if (cred.handle) return cred.handle;
|
|
1461
1596
|
console.error(
|
|
1462
1597
|
message(
|
|
1463
|
-
cred.source === "env" ? "
|
|
1598
|
+
cred.source === "env" ? "The account behind YMMV_TOKEN has no handle bound. Run `ymmv login` on an interactive machine, as the same GitHub account, to rebind it. If that GitHub username is a reserved word, rename on GitHub first." : "Your GitHub username is a reserved word, so no handle is bound. Rename on GitHub, then run `ymmv login` again."
|
|
1464
1599
|
)
|
|
1465
1600
|
);
|
|
1466
1601
|
process.exitCode = 1;
|
|
1467
1602
|
return null;
|
|
1468
1603
|
}
|
|
1469
|
-
function assertHandleUnchanged(existing, handle) {
|
|
1604
|
+
function assertHandleUnchanged(existing, cred, handle) {
|
|
1470
1605
|
if (existing && existing.handle.toLowerCase() !== handle.toLowerCase()) {
|
|
1471
1606
|
throw new Error(
|
|
1472
|
-
`This login is bound to "${handle}" but your profile now lives at "${sanitizeValue(existing.handle)}". Run
|
|
1607
|
+
`This login is bound to "${handle}" but your profile now lives at "${sanitizeValue(existing.handle)}". ${cred.source === "env" ? "Re-run the command." : "Run `ymmv login` to refresh, then retry."}`
|
|
1473
1608
|
);
|
|
1474
1609
|
}
|
|
1475
1610
|
}
|
|
@@ -1532,7 +1667,7 @@ async function publish(io) {
|
|
|
1532
1667
|
}
|
|
1533
1668
|
});
|
|
1534
1669
|
const existing = await fetchProfileJson(handle);
|
|
1535
|
-
assertHandleUnchanged(existing, handle);
|
|
1670
|
+
assertHandleUnchanged(existing, cred, handle);
|
|
1536
1671
|
const defaults = buildDefaults(existing, detected);
|
|
1537
1672
|
const carried = unknownEntries(existing);
|
|
1538
1673
|
const extras = existing?.extras ?? [];
|
|
@@ -1576,7 +1711,7 @@ async function publish(io) {
|
|
|
1576
1711
|
}
|
|
1577
1712
|
const entries = assemble();
|
|
1578
1713
|
showCard(entries);
|
|
1579
|
-
printPublished(await publishProfile(newProfile(handle, entries, extras)), color);
|
|
1714
|
+
printPublished(await publishProfile(newProfile(handle, entries, extras), cred), color);
|
|
1580
1715
|
return;
|
|
1581
1716
|
}
|
|
1582
1717
|
try {
|
|
@@ -1592,7 +1727,7 @@ async function publish(io) {
|
|
|
1592
1727
|
);
|
|
1593
1728
|
if (ans === "y") {
|
|
1594
1729
|
try {
|
|
1595
|
-
printPublished(await publishProfile(newProfile(handle, entries, extras)), color);
|
|
1730
|
+
printPublished(await publishProfile(newProfile(handle, entries, extras), cred), color);
|
|
1596
1731
|
return;
|
|
1597
1732
|
} catch (e) {
|
|
1598
1733
|
if (e instanceof PromptAborted || e instanceof PublishRefusal) throw e;
|
|
@@ -1629,7 +1764,26 @@ async function view(handle) {
|
|
|
1629
1764
|
console.log(notFound(handle, c, BASE));
|
|
1630
1765
|
return;
|
|
1631
1766
|
}
|
|
1632
|
-
const
|
|
1767
|
+
const plainCard = (note, wrap = true) => {
|
|
1768
|
+
console.log(renderProfile(theirs, { color: c, site: displayUrl(BASE) }));
|
|
1769
|
+
if (note) {
|
|
1770
|
+
const codes = palette(c);
|
|
1771
|
+
console.error(message(`${codes.faint}${wrap ? `(${note})` : note}${codes.reset}`));
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
let cred = await loadCredential();
|
|
1775
|
+
if (cred?.source === "env") {
|
|
1776
|
+
try {
|
|
1777
|
+
cred = await verifyEnvCredential(cred);
|
|
1778
|
+
} catch (e) {
|
|
1779
|
+
plainCard(`No diff: ${displayError(e)}`, false);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (cred.handle === null) {
|
|
1783
|
+
plainCard("no diff: the account behind YMMV_TOKEN has no handle bound");
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1633
1787
|
if (cred?.handle) {
|
|
1634
1788
|
let mineFailed = false;
|
|
1635
1789
|
const mine = await fetchProfileJson(cred.handle).catch(() => {
|
|
@@ -1643,24 +1797,23 @@ async function view(handle) {
|
|
|
1643
1797
|
return;
|
|
1644
1798
|
}
|
|
1645
1799
|
if (!mine) {
|
|
1646
|
-
console.log(renderProfile(theirs, { color: c, site: displayUrl(BASE) }));
|
|
1647
1800
|
if (mineFailed) {
|
|
1648
|
-
|
|
1649
|
-
console.error(message(`${codes.faint}(couldn't load your profile to diff)${codes.reset}`));
|
|
1801
|
+
plainCard("couldn't load your profile to diff");
|
|
1650
1802
|
} else {
|
|
1803
|
+
plainCard();
|
|
1651
1804
|
console.log(nudge(c));
|
|
1652
1805
|
}
|
|
1653
1806
|
return;
|
|
1654
1807
|
}
|
|
1655
1808
|
}
|
|
1656
|
-
|
|
1809
|
+
plainCard();
|
|
1657
1810
|
}
|
|
1658
1811
|
async function runSet(target) {
|
|
1659
1812
|
const cred = await ensureLogin();
|
|
1660
1813
|
const handle = requireHandle(cred);
|
|
1661
1814
|
if (!handle) return;
|
|
1662
1815
|
const existing = await fetchProfileJson(handle);
|
|
1663
|
-
assertHandleUnchanged(existing, handle);
|
|
1816
|
+
assertHandleUnchanged(existing, cred, handle);
|
|
1664
1817
|
const { entries, extras } = applySet(existing, target);
|
|
1665
1818
|
if (target.kind === "extra" && extras.length > MAX_EXTRAS) {
|
|
1666
1819
|
console.error(
|
|
@@ -1671,7 +1824,7 @@ async function runSet(target) {
|
|
|
1671
1824
|
process.exitCode = 1;
|
|
1672
1825
|
return;
|
|
1673
1826
|
}
|
|
1674
|
-
const res = await publishProfile(newProfile(handle, entries, extras));
|
|
1827
|
+
const res = await publishProfile(newProfile(handle, entries, extras), cred);
|
|
1675
1828
|
const line = target.kind === "curated" ? `Set ${KEY_LABELS[target.key]} = ${target.value}.` : `Set extra ${target.label} = ${target.value}.`;
|
|
1676
1829
|
console.log(message(`${line}${pagePointer(res.handle)}`));
|
|
1677
1830
|
}
|
|
@@ -1680,7 +1833,7 @@ async function runUnset(target) {
|
|
|
1680
1833
|
const handle = requireHandle(cred);
|
|
1681
1834
|
if (!handle) return;
|
|
1682
1835
|
const existing = await fetchProfileJson(handle);
|
|
1683
|
-
assertHandleUnchanged(existing, handle);
|
|
1836
|
+
assertHandleUnchanged(existing, cred, handle);
|
|
1684
1837
|
if (!existing) {
|
|
1685
1838
|
console.log(message("No profile yet. Run `ymmv` to publish one."));
|
|
1686
1839
|
return;
|
|
@@ -1694,13 +1847,13 @@ async function runUnset(target) {
|
|
|
1694
1847
|
);
|
|
1695
1848
|
return;
|
|
1696
1849
|
}
|
|
1697
|
-
const res = await publishProfile(newProfile(handle, entries, extras));
|
|
1850
|
+
const res = await publishProfile(newProfile(handle, entries, extras), cred);
|
|
1698
1851
|
const line = target.kind === "curated" ? `Removed ${KEY_LABELS[target.key]} (was "${sanitizeValue(removed.value)}").` : `Removed extra "${sanitizeValue(removed.label)}" (was "${sanitizeValue(removed.value)}").`;
|
|
1699
1852
|
console.log(message(`${line}${pagePointer(res.handle)}`));
|
|
1700
1853
|
}
|
|
1701
1854
|
async function runDelete(io) {
|
|
1702
1855
|
const cred = await ensureLogin();
|
|
1703
|
-
const target = cred.
|
|
1856
|
+
const target = cred.handle ? `${displayUrl(BASE)}/${sanitizeValue(cred.handle)}` : cred.source === "env" ? "the profile bound to YMMV_TOKEN" : "your profile";
|
|
1704
1857
|
if (!io.yes) {
|
|
1705
1858
|
if (!io.interactive || !io.prompter) {
|
|
1706
1859
|
console.error(
|