ymmv-cli 0.8.0 → 0.10.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 +19 -5
- package/dist/cli.js +411 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,10 +9,13 @@ clean page at `ymmv.fyi/<handle>` in about 10 seconds. See a live one:
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npx ymmv-cli # detect your stack, confirm, go live at ymmv.fyi/<you>
|
|
13
|
-
npx ymmv-cli bardisty # view someone's stack in the terminal
|
|
12
|
+
npx ymmv-cli@latest # detect your stack, confirm, go live at ymmv.fyi/<you>
|
|
13
|
+
npx ymmv-cli@latest bardisty # view someone's stack in the terminal
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
(`@latest` makes npx look for the newest release; without a version spec, npx can pin you
|
|
17
|
+
to whatever it downloaded first.)
|
|
18
|
+
|
|
16
19
|
Viewing someone while you're logged in diffs their stack against yours:
|
|
17
20
|
|
|
18
21
|
```
|
|
@@ -33,6 +36,9 @@ Install once for the short `ymmv` command:
|
|
|
33
36
|
npm i -g ymmv-cli
|
|
34
37
|
```
|
|
35
38
|
|
|
39
|
+
When a newer release exists, the CLI mentions it after a command (interactive terminals only)
|
|
40
|
+
and `ymmv update` upgrades a global install in place.
|
|
41
|
+
|
|
36
42
|
First run includes a one-time GitHub sign-in. Works on macOS, Linux, Windows,
|
|
37
43
|
and WSL. Nothing is published until you confirm (detection only pre-fills), and
|
|
38
44
|
`ymmv delete` removes everything. Releases are published from GitHub Actions
|
|
@@ -47,7 +53,10 @@ via npm Trusted Publishing, with provenance.
|
|
|
47
53
|
- `ymmv unset editor` removes one value (`ymmv set editor -` works too); `ymmv unset --extra "Keyboard"` removes an extra
|
|
48
54
|
- `ymmv delete` removes your profile (`ymmv delete -y` skips the confirm, for scripts)
|
|
49
55
|
- `ymmv login` / `ymmv logout` sign in / out
|
|
50
|
-
- `ymmv
|
|
56
|
+
- `ymmv update` updates the CLI to the latest release (runs the matching upgrade for npm, pnpm,
|
|
57
|
+
and bun global installs; via npx it prints the invocation to use instead; anything else gets
|
|
58
|
+
the commands to run by hand)
|
|
59
|
+
- `ymmv version` prints the CLI version (and notes a newer release when one is known)
|
|
51
60
|
|
|
52
61
|
Values are capped at 256 characters and extra labels at 64; a profile holds up
|
|
53
62
|
to 32 extras.
|
|
@@ -59,7 +68,9 @@ Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`. Full c
|
|
|
59
68
|
## Environment variables
|
|
60
69
|
|
|
61
70
|
- `NO_COLOR` disables color output (and `FORCE_COLOR=0`/`false` force-disables it).
|
|
62
|
-
- `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.
|
|
63
74
|
- `YMMV_TOKEN` authenticates without a browser (CI and scripts, below). Takes precedence over
|
|
64
75
|
the stored login and is read-only: the CLI never writes, revokes, or deletes it, and
|
|
65
76
|
`ymmv login` / `ymmv logout` keep acting on the stored login. Viewing (`ymmv <handle>`) also
|
|
@@ -68,6 +79,9 @@ Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`. Full c
|
|
|
68
79
|
- `YMMV_HANDLE` names the GitHub username `YMMV_TOKEN` belongs to. Required for `ymmv -y` and
|
|
69
80
|
`ymmv set`/`unset` under an env token (there is no server lookup for it); ignored without
|
|
70
81
|
`YMMV_TOKEN`.
|
|
82
|
+
- `YMMV_NO_UPDATE_CHECK` disables the startup check for newer releases (the ecosystem-standard
|
|
83
|
+
`NO_UPDATE_NOTIFIER` works too). The check is also off automatically under `CI`, in pipes,
|
|
84
|
+
and in dev builds; it never blocks or fails a command.
|
|
71
85
|
|
|
72
86
|
## Publishing from CI
|
|
73
87
|
|
|
@@ -78,7 +92,7 @@ Every profile is open JSON too: `GET https://ymmv.fyi/api/v1/u/<handle>`. Full c
|
|
|
78
92
|
`~/.config/ymmv/token.json` (Linux), `~/Library/Preferences/ymmv/token.json` (macOS),
|
|
79
93
|
`%APPDATA%\ymmv\Config\token.json` (Windows).
|
|
80
94
|
3. Set it as a CI secret named `YMMV_TOKEN`, and set `YMMV_HANDLE` to your GitHub username.
|
|
81
|
-
4. Run `npx ymmv-cli -y` in the job.
|
|
95
|
+
4. Run `npx ymmv-cli@latest -y` in the job.
|
|
82
96
|
|
|
83
97
|
Two things to know:
|
|
84
98
|
|
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;
|
|
@@ -349,7 +352,8 @@ var CLI_VERBS = [
|
|
|
349
352
|
"view",
|
|
350
353
|
"help",
|
|
351
354
|
"publish",
|
|
352
|
-
"version"
|
|
355
|
+
"version",
|
|
356
|
+
"update"
|
|
353
357
|
];
|
|
354
358
|
var RESERVED = [.../* @__PURE__ */ new Set([...RESERVED_ROUTES, ...CLI_VERBS])];
|
|
355
359
|
var RESERVED_SET = new Set(RESERVED);
|
|
@@ -389,10 +393,7 @@ var CTRL_RE = new RegExp(
|
|
|
389
393
|
`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}-${String.fromCharCode(159)}]`,
|
|
390
394
|
"g"
|
|
391
395
|
);
|
|
392
|
-
var BIDI_RE = new RegExp(
|
|
393
|
-
`[${String.fromCharCode(8234)}-${String.fromCharCode(8238)}${String.fromCharCode(8294)}-${String.fromCharCode(8297)}${String.fromCharCode(8206)}${String.fromCharCode(8207)}]`,
|
|
394
|
-
"g"
|
|
395
|
-
);
|
|
396
|
+
var BIDI_RE = new RegExp("\\p{Bidi_Control}", "gu");
|
|
396
397
|
function sanitizeValue(value) {
|
|
397
398
|
return value.replace(ANSI_RE, "").replace(CTRL_RE, "").replace(BIDI_RE, "");
|
|
398
399
|
}
|
|
@@ -417,10 +418,10 @@ function isHttpUrl(value) {
|
|
|
417
418
|
return HTTP_URL_RE.test(value.trim());
|
|
418
419
|
}
|
|
419
420
|
var NO_OSC8_TERMS = /* @__PURE__ */ new Set(["linux", "dumb"]);
|
|
420
|
-
function link(url, color, term = process.env.TERM) {
|
|
421
|
+
function link(url, color, term = process.env.TERM, label) {
|
|
421
422
|
const clean = sanitizeValue(url).trim();
|
|
422
423
|
if (!color) return clean;
|
|
423
|
-
const text = `${CODES.amber}${displayUrl(clean)}${CODES.reset}`;
|
|
424
|
+
const text = `${CODES.amber}${label ? sanitizeValue(label) : displayUrl(clean)}${CODES.reset}`;
|
|
424
425
|
if (term !== void 0 && NO_OSC8_TERMS.has(term)) return text;
|
|
425
426
|
return `${OSC}8;;${clean}${ST}${text}${OSC}8;;${ST}`;
|
|
426
427
|
}
|
|
@@ -539,11 +540,12 @@ function nudge(color) {
|
|
|
539
540
|
function notFound(handle, color, base) {
|
|
540
541
|
return `
|
|
541
542
|
no ymmv profile for "${sanitizeValue(handle)}" yet.
|
|
542
|
-
publish one at ${link(base, color)} with: npx ymmv-cli`;
|
|
543
|
+
publish one at ${link(base, color)} with: npx ymmv-cli@latest`;
|
|
543
544
|
}
|
|
544
545
|
|
|
545
546
|
// src/http.ts
|
|
546
547
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
548
|
+
var REVOKE_CAP_MS = 5e3;
|
|
547
549
|
var TIMEOUT_TEXT = "request timed out";
|
|
548
550
|
function isTimeoutError(err) {
|
|
549
551
|
return err instanceof Error && (err.name === "TimeoutError" || err.cause instanceof Error && err.cause.name === "TimeoutError");
|
|
@@ -626,9 +628,6 @@ async function safeFetch(url, init, reach, fetchFn = globalThis.fetch) {
|
|
|
626
628
|
}
|
|
627
629
|
}
|
|
628
630
|
|
|
629
|
-
// src/index.ts
|
|
630
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
631
|
-
|
|
632
631
|
// src/config.ts
|
|
633
632
|
function normalizeBase(raw) {
|
|
634
633
|
return raw.replace(/\/+$/, "");
|
|
@@ -685,6 +684,12 @@ async function bodyJson(res) {
|
|
|
685
684
|
return null;
|
|
686
685
|
}
|
|
687
686
|
}
|
|
687
|
+
var MintRejected = class extends Error {
|
|
688
|
+
constructor(msg) {
|
|
689
|
+
super(msg);
|
|
690
|
+
this.name = "MintRejected";
|
|
691
|
+
}
|
|
692
|
+
};
|
|
688
693
|
async function mintYmmvToken(accessToken) {
|
|
689
694
|
const res = await safeFetch(
|
|
690
695
|
`${BASE}/api/v1/auth/token`,
|
|
@@ -726,19 +731,30 @@ async function mintYmmvToken(accessToken) {
|
|
|
726
731
|
throw new Error(`login failed: ${res.status} ${wireText(slug)}`.trim());
|
|
727
732
|
}
|
|
728
733
|
const data = await bodyJson(res);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
734
|
+
const unexpected = `Unexpected response from ${BASE}. Nothing was saved; run \`ymmv login\` again.`;
|
|
735
|
+
if (!data) throw new Error(unexpected);
|
|
736
|
+
const token = typeof data.token === "string" && data.token.length > 0 ? data.token : null;
|
|
737
|
+
const handle = data.handle === null ? null : typeof data.handle === "string" ? sanitizeValue(data.handle) : void 0;
|
|
738
|
+
if (token === null || handle === void 0 || handle !== null && !isValidHandle(handle) || !isGithubId(data.github_id)) {
|
|
739
|
+
let revokeFailed = false;
|
|
740
|
+
if (token !== null) {
|
|
741
|
+
await revokeYmmvToken(token, AbortSignal.timeout(REVOKE_CAP_MS)).catch(() => {
|
|
742
|
+
revokeFailed = true;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
throw new MintRejected(
|
|
746
|
+
revokeFailed ? `${unexpected} The login the server minted could not be revoked.` : unexpected
|
|
732
747
|
);
|
|
733
748
|
}
|
|
734
|
-
return { token
|
|
749
|
+
return { token, handle, github_id: data.github_id };
|
|
735
750
|
}
|
|
736
|
-
async function revokeYmmvToken(token) {
|
|
751
|
+
async function revokeYmmvToken(token, signal) {
|
|
737
752
|
const res = await safeFetch(
|
|
738
753
|
`${BASE}/api/v1/auth/logout`,
|
|
739
754
|
{
|
|
740
755
|
method: "POST",
|
|
741
756
|
headers: { authorization: `Bearer ${token}` },
|
|
757
|
+
signal,
|
|
742
758
|
// Never follow a redirect: a 30x→200 must not read as a successful revoke (which would delete
|
|
743
759
|
// the local file while the server token stays live). Same guard as mint + publish/delete.
|
|
744
760
|
redirect: "manual"
|
|
@@ -771,7 +787,12 @@ async function saveToken(data) {
|
|
|
771
787
|
if (process.platform !== "win32") await chmod(dir, 448).catch(() => {
|
|
772
788
|
});
|
|
773
789
|
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
774
|
-
const stored = {
|
|
790
|
+
const stored = {
|
|
791
|
+
base: BASE,
|
|
792
|
+
token: data.token,
|
|
793
|
+
handle: data.handle,
|
|
794
|
+
github_id: data.github_id
|
|
795
|
+
};
|
|
775
796
|
try {
|
|
776
797
|
await writeFile(tmp, JSON.stringify(stored), { mode: 384 });
|
|
777
798
|
if (process.platform !== "win32") await chmod(tmp, 384);
|
|
@@ -792,10 +813,17 @@ async function readTokenFile() {
|
|
|
792
813
|
}
|
|
793
814
|
async function loadToken() {
|
|
794
815
|
const parsed = await readTokenFile();
|
|
795
|
-
|
|
816
|
+
const rawId = parsed?.github_id;
|
|
817
|
+
const idOk = rawId === void 0 || rawId === null || isGithubId(rawId);
|
|
818
|
+
if (!parsed || parsed.base !== BASE || typeof parsed.token !== "string" || parsed.token === "" || parsed.handle !== null && typeof parsed.handle !== "string" || !idOk) {
|
|
796
819
|
return null;
|
|
797
820
|
}
|
|
798
|
-
return
|
|
821
|
+
return {
|
|
822
|
+
base: parsed.base,
|
|
823
|
+
token: parsed.token,
|
|
824
|
+
handle: parsed.handle === "" ? null : parsed.handle,
|
|
825
|
+
github_id: isGithubId(rawId) ? rawId : null
|
|
826
|
+
};
|
|
799
827
|
}
|
|
800
828
|
async function peekCredential() {
|
|
801
829
|
const parsed = await readTokenFile();
|
|
@@ -804,7 +832,13 @@ async function peekCredential() {
|
|
|
804
832
|
async function loadCredential() {
|
|
805
833
|
const envToken = process.env.YMMV_TOKEN || "";
|
|
806
834
|
if (envToken !== "") {
|
|
807
|
-
return {
|
|
835
|
+
return {
|
|
836
|
+
base: BASE,
|
|
837
|
+
token: envToken,
|
|
838
|
+
handle: process.env.YMMV_HANDLE || null,
|
|
839
|
+
github_id: null,
|
|
840
|
+
source: "env"
|
|
841
|
+
};
|
|
808
842
|
}
|
|
809
843
|
const stored = await loadToken();
|
|
810
844
|
return stored ? { ...stored, source: "file" } : null;
|
|
@@ -952,16 +986,16 @@ ${c.faint}waiting for GitHub approval\u2026 (Ctrl+C to cancel)${c.reset}`
|
|
|
952
986
|
)
|
|
953
987
|
);
|
|
954
988
|
const accessToken = await pollForToken(dc, deps);
|
|
955
|
-
const
|
|
989
|
+
const minted = await mintYmmvToken(accessToken);
|
|
956
990
|
const replaced = await peekCredential();
|
|
957
991
|
try {
|
|
958
|
-
await saveToken(
|
|
992
|
+
await saveToken(minted);
|
|
959
993
|
} catch (e) {
|
|
960
|
-
await revokeYmmvToken(token).catch(() => {
|
|
994
|
+
await revokeYmmvToken(minted.token).catch(() => {
|
|
961
995
|
});
|
|
962
996
|
throw e;
|
|
963
997
|
}
|
|
964
|
-
if (replaced && replaced.base === BASE && replaced.token !== token) {
|
|
998
|
+
if (replaced && replaced.base === BASE && replaced.token !== minted.token) {
|
|
965
999
|
try {
|
|
966
1000
|
await revokeYmmvToken(replaced.token);
|
|
967
1001
|
} catch {
|
|
@@ -970,7 +1004,7 @@ ${c.faint}waiting for GitHub approval\u2026 (Ctrl+C to cancel)${c.reset}`
|
|
|
970
1004
|
}
|
|
971
1005
|
console.log(
|
|
972
1006
|
message(
|
|
973
|
-
handle ? `Logged in as ${handle}.` : "Logged in. No handle bound (your GitHub username is a reserved word)."
|
|
1007
|
+
minted.handle ? `Logged in as ${minted.handle}.` : "Logged in. No handle bound (your GitHub username is a reserved word)."
|
|
974
1008
|
)
|
|
975
1009
|
);
|
|
976
1010
|
}
|
|
@@ -983,18 +1017,30 @@ var PublishRefusal = class extends Error {
|
|
|
983
1017
|
}
|
|
984
1018
|
};
|
|
985
1019
|
var ENV_TOKEN_REJECTED = "The server rejected the token in YMMV_TOKEN (revoked or expired).";
|
|
1020
|
+
var NOT_PERSISTED = "Login did not persist a token. Run `ymmv login`.";
|
|
986
1021
|
async function rateLimitMessage(res) {
|
|
987
1022
|
return withRetryHint(await serverMessage(res) ?? "rate limited, too many requests", res);
|
|
988
1023
|
}
|
|
1024
|
+
async function loginOrRefuse() {
|
|
1025
|
+
try {
|
|
1026
|
+
await login();
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
if (e instanceof MintRejected) throw new PublishRefusal(e.message);
|
|
1029
|
+
throw e;
|
|
1030
|
+
}
|
|
1031
|
+
const fresh = await loadCredential();
|
|
1032
|
+
if (!fresh) throw new PublishRefusal(NOT_PERSISTED);
|
|
1033
|
+
return fresh;
|
|
1034
|
+
}
|
|
989
1035
|
async function ensureLogin() {
|
|
990
1036
|
const existing = await loadCredential();
|
|
991
1037
|
if (existing) return existing;
|
|
992
1038
|
await login();
|
|
993
1039
|
const fresh = await loadCredential();
|
|
994
|
-
if (!fresh) throw new Error(
|
|
1040
|
+
if (!fresh) throw new Error(NOT_PERSISTED);
|
|
995
1041
|
return fresh;
|
|
996
1042
|
}
|
|
997
|
-
async function publishProfile(profile) {
|
|
1043
|
+
async function publishProfile(profile, expected) {
|
|
998
1044
|
const send = (c) => safeFetch(
|
|
999
1045
|
`${BASE}/api/v1/profile`,
|
|
1000
1046
|
{
|
|
@@ -1008,8 +1054,13 @@ async function publishProfile(profile) {
|
|
|
1008
1054
|
},
|
|
1009
1055
|
BASE
|
|
1010
1056
|
);
|
|
1011
|
-
let cred = await
|
|
1012
|
-
if (
|
|
1057
|
+
let cred = await loadCredential();
|
|
1058
|
+
if (!cred) {
|
|
1059
|
+
console.log(message("Not logged in. Logging in to publish."));
|
|
1060
|
+
cred = await loginOrRefuse();
|
|
1061
|
+
}
|
|
1062
|
+
const idDrifted = expected.source === "file" && (expected.github_id === null ? cred.github_id !== null : cred.github_id !== expected.github_id);
|
|
1063
|
+
if ((cred.handle ?? "").toLowerCase() !== profile.handle.toLowerCase() || idDrifted) {
|
|
1013
1064
|
throw new PublishRefusal(
|
|
1014
1065
|
"The stored login changed while this command was running. Re-run it under the current account."
|
|
1015
1066
|
);
|
|
@@ -1022,19 +1073,35 @@ async function publishProfile(profile) {
|
|
|
1022
1073
|
);
|
|
1023
1074
|
}
|
|
1024
1075
|
const was401 = res.status === 401;
|
|
1076
|
+
const before = cred;
|
|
1025
1077
|
console.log(
|
|
1026
1078
|
message(
|
|
1027
1079
|
was401 ? "Session expired. Logging in again to retry the publish." : "The server no longer recognizes your handle. Logging in again to retry the publish."
|
|
1028
1080
|
)
|
|
1029
1081
|
);
|
|
1030
1082
|
if (was401) await deleteToken();
|
|
1031
|
-
await
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
);
|
|
1083
|
+
cred = await loginOrRefuse();
|
|
1084
|
+
const known = before.github_id !== null;
|
|
1085
|
+
const idChanged = known && cred.github_id !== before.github_id;
|
|
1086
|
+
const sameHandle = (cred.handle ?? "").toLowerCase() === profile.handle.toLowerCase();
|
|
1087
|
+
if (!sameHandle || idChanged) {
|
|
1088
|
+
const mine = sanitizeValue(profile.handle);
|
|
1089
|
+
const bound = cred.handle === null ? null : sanitizeValue(cred.handle);
|
|
1090
|
+
const reRun = "Nothing was published. Re-run under the account you meant.";
|
|
1091
|
+
const otherAccount = `The re-login bound a different account ("${bound}", not "${mine}"). ${reRun}`;
|
|
1092
|
+
if (idChanged) {
|
|
1093
|
+
throw new PublishRefusal(
|
|
1094
|
+
sameHandle ? `The handle "${mine}" now belongs to a different GitHub account. ${reRun}` : bound === null ? `The re-login bound a different GitHub account. ${reRun}` : otherAccount
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
if (bound === null) {
|
|
1098
|
+
throw new PublishRefusal(
|
|
1099
|
+
"Your login no longer binds a handle (your GitHub username is a reserved word). Nothing was published."
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const rebound = `Your login now binds "${bound}". Nothing was published. Re-run the command to publish under it.`;
|
|
1103
|
+
if (known) throw new PublishRefusal(rebound);
|
|
1104
|
+
throw new PublishRefusal(was401 ? otherAccount : rebound);
|
|
1038
1105
|
}
|
|
1039
1106
|
res = await send(cred);
|
|
1040
1107
|
if (res.status === 401) throw new PublishRefusal("Authentication failed. Run `ymmv login`.");
|
|
@@ -1578,7 +1645,7 @@ async function publish(io) {
|
|
|
1578
1645
|
}
|
|
1579
1646
|
const entries = assemble();
|
|
1580
1647
|
showCard(entries);
|
|
1581
|
-
printPublished(await publishProfile(newProfile(handle, entries, extras)), color);
|
|
1648
|
+
printPublished(await publishProfile(newProfile(handle, entries, extras), cred), color);
|
|
1582
1649
|
return;
|
|
1583
1650
|
}
|
|
1584
1651
|
try {
|
|
@@ -1594,7 +1661,7 @@ async function publish(io) {
|
|
|
1594
1661
|
);
|
|
1595
1662
|
if (ans === "y") {
|
|
1596
1663
|
try {
|
|
1597
|
-
printPublished(await publishProfile(newProfile(handle, entries, extras)), color);
|
|
1664
|
+
printPublished(await publishProfile(newProfile(handle, entries, extras), cred), color);
|
|
1598
1665
|
return;
|
|
1599
1666
|
} catch (e) {
|
|
1600
1667
|
if (e instanceof PromptAborted || e instanceof PublishRefusal) throw e;
|
|
@@ -1673,7 +1740,7 @@ async function runSet(target) {
|
|
|
1673
1740
|
process.exitCode = 1;
|
|
1674
1741
|
return;
|
|
1675
1742
|
}
|
|
1676
|
-
const res = await publishProfile(newProfile(handle, entries, extras));
|
|
1743
|
+
const res = await publishProfile(newProfile(handle, entries, extras), cred);
|
|
1677
1744
|
const line = target.kind === "curated" ? `Set ${KEY_LABELS[target.key]} = ${target.value}.` : `Set extra ${target.label} = ${target.value}.`;
|
|
1678
1745
|
console.log(message(`${line}${pagePointer(res.handle)}`));
|
|
1679
1746
|
}
|
|
@@ -1696,7 +1763,7 @@ async function runUnset(target) {
|
|
|
1696
1763
|
);
|
|
1697
1764
|
return;
|
|
1698
1765
|
}
|
|
1699
|
-
const res = await publishProfile(newProfile(handle, entries, extras));
|
|
1766
|
+
const res = await publishProfile(newProfile(handle, entries, extras), cred);
|
|
1700
1767
|
const line = target.kind === "curated" ? `Removed ${KEY_LABELS[target.key]} (was "${sanitizeValue(removed.value)}").` : `Removed extra "${sanitizeValue(removed.label)}" (was "${sanitizeValue(removed.value)}").`;
|
|
1701
1768
|
console.log(message(`${line}${pagePointer(res.handle)}`));
|
|
1702
1769
|
}
|
|
@@ -1827,7 +1894,7 @@ function resolveArg(argv) {
|
|
|
1827
1894
|
}
|
|
1828
1895
|
return { kind: "publish", yes: true };
|
|
1829
1896
|
}
|
|
1830
|
-
if (first === "login" || first === "logout") return noArgs(first, rest);
|
|
1897
|
+
if (first === "login" || first === "logout" || first === "update") return noArgs(first, rest);
|
|
1831
1898
|
if (first === "publish") {
|
|
1832
1899
|
return yesOnly("usage: ymmv publish [-y]", rest, (yes) => ({ kind: "publish", yes }));
|
|
1833
1900
|
}
|
|
@@ -1877,6 +1944,274 @@ function reservedError(handle, hintVerbs = false) {
|
|
|
1877
1944
|
};
|
|
1878
1945
|
}
|
|
1879
1946
|
|
|
1947
|
+
// src/update.ts
|
|
1948
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
1949
|
+
import { homedir } from "os";
|
|
1950
|
+
|
|
1951
|
+
// src/update-check.ts
|
|
1952
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1953
|
+
import { readFileSync as readFileSync2, realpathSync } from "fs";
|
|
1954
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1955
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1956
|
+
import envPaths2 from "env-paths";
|
|
1957
|
+
var REGISTRY_URL = "https://registry.npmjs.org/ymmv-cli/latest";
|
|
1958
|
+
var RELEASES_URL = "https://github.com/ymmv-fyi/ymmv/releases";
|
|
1959
|
+
var FETCH_TIMEOUT_MS = 2e3;
|
|
1960
|
+
var FINISH_GRACE_MS = 500;
|
|
1961
|
+
var SUCCESS_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1962
|
+
var FAILURE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
1963
|
+
var VERSION_FRESH_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1964
|
+
function ownVersion() {
|
|
1965
|
+
try {
|
|
1966
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
1967
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
1968
|
+
} catch {
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
function parseTriple(v) {
|
|
1973
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v);
|
|
1974
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
1975
|
+
}
|
|
1976
|
+
function isNewer(latest, current) {
|
|
1977
|
+
const l = parseTriple(latest);
|
|
1978
|
+
const c = parseTriple(current);
|
|
1979
|
+
if (!l || !c) return false;
|
|
1980
|
+
for (let i = 0; i < 3; i++) {
|
|
1981
|
+
if (l[i] !== c[i]) return l[i] > c[i];
|
|
1982
|
+
}
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
function updateCachePath() {
|
|
1986
|
+
return join2(envPaths2("ymmv", { suffix: "" }).config, "update-check.json");
|
|
1987
|
+
}
|
|
1988
|
+
async function readCache(path, now) {
|
|
1989
|
+
try {
|
|
1990
|
+
const parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
1991
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1992
|
+
const c = parsed;
|
|
1993
|
+
if (typeof c.lastCheckedAt !== "number" || typeof c.ok !== "boolean") return null;
|
|
1994
|
+
if (!Number.isFinite(c.lastCheckedAt) || c.lastCheckedAt > now) return null;
|
|
1995
|
+
if (c.latest !== void 0 && typeof c.latest !== "string") return null;
|
|
1996
|
+
if (c.latestAt !== void 0 && (!Number.isFinite(c.latestAt) || c.latestAt > now)) return null;
|
|
1997
|
+
return { lastCheckedAt: c.lastCheckedAt, ok: c.ok, latest: c.latest, latestAt: c.latestAt };
|
|
1998
|
+
} catch {
|
|
1999
|
+
return null;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
async function writeCache(path, cache) {
|
|
2003
|
+
const tmp = `${path}.${randomUUID2()}.tmp`;
|
|
2004
|
+
try {
|
|
2005
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
2006
|
+
await writeFile2(tmp, JSON.stringify(cache));
|
|
2007
|
+
await rename2(tmp, path);
|
|
2008
|
+
} catch {
|
|
2009
|
+
await rm2(tmp, { force: true }).catch(() => {
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function binRealpath() {
|
|
2014
|
+
const raw = process.argv[1];
|
|
2015
|
+
if (!raw) return void 0;
|
|
2016
|
+
try {
|
|
2017
|
+
return realpathSync(raw);
|
|
2018
|
+
} catch {
|
|
2019
|
+
return raw;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
var NPM_PREFIX_ROOTS = /* @__PURE__ */ new Set(["usr", "local", "homebrew", "nodejs", "node", "installation"]);
|
|
2023
|
+
var VERSIONISH_RE = /^v?\d+(\.\d+)*$/;
|
|
2024
|
+
function detectInstallMethod(binPath) {
|
|
2025
|
+
if (!binPath) return "unknown";
|
|
2026
|
+
const segs = binPath.split(/[\\/]+/).filter(Boolean).map((s) => s.toLowerCase());
|
|
2027
|
+
const has = (name) => segs.includes(name);
|
|
2028
|
+
if (has("_npx")) return "ephemeral";
|
|
2029
|
+
if (has("dlx") || segs.some((s) => s.startsWith("dlx-"))) return "ephemeral";
|
|
2030
|
+
if (segs.some((s) => s.startsWith("bunx-"))) return "ephemeral";
|
|
2031
|
+
if (has(".bun") && has("cache")) return "ephemeral";
|
|
2032
|
+
if (has(".bun") && (has("global") || has("bin"))) return "bun-global";
|
|
2033
|
+
if (has(".volta")) return "unknown";
|
|
2034
|
+
if (segs.some((s, i) => s === "pnpm" && segs[i + 1] === "global")) return "pnpm-global";
|
|
2035
|
+
if (has(".pnpm")) return "unknown";
|
|
2036
|
+
const npmGlobal = segs.some(
|
|
2037
|
+
(s, i) => s === "node_modules" && segs[i + 1] === "ymmv-cli" && (segs[i - 1] === "lib" && i >= 2 && (NPM_PREFIX_ROOTS.has(segs[i - 2]) || VERSIONISH_RE.test(segs[i - 2])) || segs[i - 1] === "npm")
|
|
2038
|
+
);
|
|
2039
|
+
if (npmGlobal) return "npm-global";
|
|
2040
|
+
return "unknown";
|
|
2041
|
+
}
|
|
2042
|
+
function npxInvocation(latest) {
|
|
2043
|
+
const triple = latest ? parseTriple(latest) : null;
|
|
2044
|
+
return `npx ymmv-cli@${triple ? triple.join(".") : "latest"}`;
|
|
2045
|
+
}
|
|
2046
|
+
function formatUpdateNotice(current, latest, method, color, term) {
|
|
2047
|
+
const cur = parseTriple(current);
|
|
2048
|
+
const lat = parseTriple(latest);
|
|
2049
|
+
if (!cur || !lat || !isNewer(lat.join("."), cur.join("."))) return null;
|
|
2050
|
+
const latestClean = lat.join(".");
|
|
2051
|
+
const currentClean = cur.join(".");
|
|
2052
|
+
const cmd = method === "ephemeral" ? npxInvocation(latestClean) : "ymmv update";
|
|
2053
|
+
const c = palette(color);
|
|
2054
|
+
const headline = `ymmv-cli ${latestClean} is out`;
|
|
2055
|
+
const linked = color ? link(RELEASES_URL, true, term, headline) : headline;
|
|
2056
|
+
return `${linked} ${c.faint}(you have ${currentClean})${c.reset} \u2192 run ${c.bold}${cmd}${c.reset}`;
|
|
2057
|
+
}
|
|
2058
|
+
function updatesOptedOut(env) {
|
|
2059
|
+
if (env.YMMV_NO_UPDATE_CHECK) return true;
|
|
2060
|
+
if (env.NO_UPDATE_NOTIFIER) return true;
|
|
2061
|
+
if (env.CI && env.CI !== "false") return true;
|
|
2062
|
+
return false;
|
|
2063
|
+
}
|
|
2064
|
+
async function readCachedLatest(deps = {}) {
|
|
2065
|
+
const now = (deps.now ?? Date.now)();
|
|
2066
|
+
const cache = await readCache(deps.cachePath ?? updateCachePath(), now);
|
|
2067
|
+
if (!cache?.latest || cache.latestAt === void 0) return null;
|
|
2068
|
+
if (now - cache.latestAt > VERSION_FRESH_MS) return null;
|
|
2069
|
+
const triple = parseTriple(cache.latest);
|
|
2070
|
+
return triple ? triple.join(".") : null;
|
|
2071
|
+
}
|
|
2072
|
+
var GRACE = /* @__PURE__ */ Symbol("grace");
|
|
2073
|
+
function startUpdateCheck(deps = {}) {
|
|
2074
|
+
const env = deps.env ?? process.env;
|
|
2075
|
+
const nowFn = deps.now ?? Date.now;
|
|
2076
|
+
const stderrIsTTY = deps.stderrIsTTY ?? Boolean(process.stderr.isTTY);
|
|
2077
|
+
const inert = { finish: async () => null, abort: () => {
|
|
2078
|
+
} };
|
|
2079
|
+
if (updatesOptedOut(env)) return inert;
|
|
2080
|
+
if (!stderrIsTTY) return inert;
|
|
2081
|
+
const current = deps.currentVersion !== void 0 ? deps.currentVersion : ownVersion();
|
|
2082
|
+
const currentTriple = current === null ? null : parseTriple(current);
|
|
2083
|
+
if (!currentTriple || currentTriple.join(".") === "0.0.0") return inert;
|
|
2084
|
+
const currentClean = currentTriple.join(".");
|
|
2085
|
+
const cachePath = deps.cachePath ?? updateCachePath();
|
|
2086
|
+
const doFetch = deps.fetch ?? globalThis.fetch;
|
|
2087
|
+
const method = detectInstallMethod(deps.binPath ?? binRealpath());
|
|
2088
|
+
const color = useColor(env, stderrIsTTY);
|
|
2089
|
+
const term = env.TERM;
|
|
2090
|
+
const controller = new AbortController();
|
|
2091
|
+
const notice = (latest) => latest ? formatUpdateNotice(currentClean, latest, method, color, term) : null;
|
|
2092
|
+
const check = (async () => {
|
|
2093
|
+
const cache = await readCache(cachePath, nowFn());
|
|
2094
|
+
if (cache && nowFn() - cache.lastCheckedAt < (cache.ok ? SUCCESS_TTL_MS : FAILURE_TTL_MS)) {
|
|
2095
|
+
return notice(cache.latest);
|
|
2096
|
+
}
|
|
2097
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
2098
|
+
timeout.unref?.();
|
|
2099
|
+
let fetched = null;
|
|
2100
|
+
try {
|
|
2101
|
+
const res = await doFetch(REGISTRY_URL, {
|
|
2102
|
+
signal: controller.signal,
|
|
2103
|
+
headers: { accept: "application/json" }
|
|
2104
|
+
});
|
|
2105
|
+
if (res.ok) {
|
|
2106
|
+
const body = await res.json();
|
|
2107
|
+
if (typeof body.version === "string") fetched = body.version;
|
|
2108
|
+
}
|
|
2109
|
+
} catch {
|
|
2110
|
+
} finally {
|
|
2111
|
+
clearTimeout(timeout);
|
|
2112
|
+
}
|
|
2113
|
+
const triple = fetched === null ? null : parseTriple(fetched);
|
|
2114
|
+
const canonical2 = triple ? triple.join(".") : void 0;
|
|
2115
|
+
const now = nowFn();
|
|
2116
|
+
await writeCache(cachePath, {
|
|
2117
|
+
lastCheckedAt: now,
|
|
2118
|
+
ok: canonical2 !== void 0,
|
|
2119
|
+
latest: canonical2 ?? cache?.latest,
|
|
2120
|
+
latestAt: canonical2 !== void 0 ? now : cache?.latestAt
|
|
2121
|
+
});
|
|
2122
|
+
return notice(canonical2 ?? cache?.latest);
|
|
2123
|
+
})().catch(() => null);
|
|
2124
|
+
return {
|
|
2125
|
+
async finish() {
|
|
2126
|
+
const result = await new Promise((resolve) => {
|
|
2127
|
+
const timer = setTimeout(() => resolve(GRACE), FINISH_GRACE_MS);
|
|
2128
|
+
timer.unref?.();
|
|
2129
|
+
void check.then((v) => {
|
|
2130
|
+
clearTimeout(timer);
|
|
2131
|
+
resolve(v);
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
if (result === GRACE) {
|
|
2135
|
+
controller.abort();
|
|
2136
|
+
return null;
|
|
2137
|
+
}
|
|
2138
|
+
return result;
|
|
2139
|
+
},
|
|
2140
|
+
abort() {
|
|
2141
|
+
controller.abort();
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// src/update.ts
|
|
2147
|
+
var UPGRADE_COMMANDS = {
|
|
2148
|
+
"npm-global": "npm i -g ymmv-cli@latest",
|
|
2149
|
+
"pnpm-global": "pnpm add -g ymmv-cli@latest",
|
|
2150
|
+
"bun-global": "bun add -g ymmv-cli@latest"
|
|
2151
|
+
};
|
|
2152
|
+
var MANUAL_LIST = Object.values(UPGRADE_COMMANDS).map((cmd) => ` ${cmd}`).join("\n");
|
|
2153
|
+
var NOT_FOUND_CODES = /* @__PURE__ */ new Set([127, 9009]);
|
|
2154
|
+
function manualFallback(intro) {
|
|
2155
|
+
return `${intro} Update with the command that matches your setup:
|
|
2156
|
+
${MANUAL_LIST}`;
|
|
2157
|
+
}
|
|
2158
|
+
async function runUpdate(deps = {}) {
|
|
2159
|
+
const method = detectInstallMethod(deps.binPath ?? binRealpath());
|
|
2160
|
+
const c = palette(colorEnabled());
|
|
2161
|
+
if (method === "ephemeral") {
|
|
2162
|
+
const cached = await readCachedLatest({ now: deps.now, cachePath: deps.cachePath });
|
|
2163
|
+
const current = deps.currentVersion !== void 0 ? deps.currentVersion : ownVersion();
|
|
2164
|
+
const pin = cached !== null && current !== null && isNewer(cached, current) ? cached : null;
|
|
2165
|
+
console.log(
|
|
2166
|
+
message(
|
|
2167
|
+
`This run came from a package runner cache (npx-style), so there is no install to update.
|
|
2168
|
+
Next time run: ${c.bold}${npxInvocation(pin)}${c.reset}`
|
|
2169
|
+
)
|
|
2170
|
+
);
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
if (method === "unknown") {
|
|
2174
|
+
console.log(message(manualFallback("Couldn't tell how ymmv-cli was installed.")));
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const cmd = UPGRADE_COMMANDS[method];
|
|
2178
|
+
const doSpawn = deps.spawn ?? nodeSpawn;
|
|
2179
|
+
const failSpawn = () => {
|
|
2180
|
+
console.error(message(manualFallback(`Couldn't run \`${cmd}\`.`)));
|
|
2181
|
+
process.exitCode = 1;
|
|
2182
|
+
};
|
|
2183
|
+
console.log(message(`${c.faint}running${c.reset} ${c.bold}${cmd}${c.reset}`));
|
|
2184
|
+
await new Promise((resolve) => {
|
|
2185
|
+
let child;
|
|
2186
|
+
try {
|
|
2187
|
+
child = doSpawn(cmd, { stdio: "inherit", shell: true, cwd: homedir() });
|
|
2188
|
+
} catch {
|
|
2189
|
+
failSpawn();
|
|
2190
|
+
resolve();
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
let settled = false;
|
|
2194
|
+
const settle = (fn) => {
|
|
2195
|
+
if (settled) return;
|
|
2196
|
+
settled = true;
|
|
2197
|
+
fn();
|
|
2198
|
+
resolve();
|
|
2199
|
+
};
|
|
2200
|
+
child.on("error", () => {
|
|
2201
|
+
settle(failSpawn);
|
|
2202
|
+
});
|
|
2203
|
+
child.on("exit", (code) => {
|
|
2204
|
+
settle(() => {
|
|
2205
|
+
if (code !== null && NOT_FOUND_CODES.has(code)) {
|
|
2206
|
+
failSpawn();
|
|
2207
|
+
} else if (code) {
|
|
2208
|
+
process.exitCode = code;
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
});
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
|
|
1880
2215
|
// src/index.ts
|
|
1881
2216
|
var help = (c) => `${c.bold}ymmv${c.reset}: terminal-native developer tool-stack profiles (ymmv.fyi)
|
|
1882
2217
|
|
|
@@ -1891,6 +2226,7 @@ ${c.faint}Usage:${c.reset}
|
|
|
1891
2226
|
ymmv unset --extra "L" remove a free-form extra
|
|
1892
2227
|
ymmv delete [-y] delete your profile (permanent; -y skips the confirm)
|
|
1893
2228
|
ymmv login | logout GitHub device-flow auth
|
|
2229
|
+
ymmv update update ymmv-cli to the latest release
|
|
1894
2230
|
ymmv help | version
|
|
1895
2231
|
|
|
1896
2232
|
${c.faint}Curated keys:${c.reset} editor, os, shell, prompt, terminal, browser, window-manager,
|
|
@@ -1922,12 +2258,21 @@ async function logout() {
|
|
|
1922
2258
|
console.log(message(revoked ? "Logged out." : "Logged out (no active session on this server)."));
|
|
1923
2259
|
}
|
|
1924
2260
|
function printVersion() {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2261
|
+
const version = ownVersion();
|
|
2262
|
+
console.log(version === null ? "ymmv-cli (version unknown)" : `ymmv-cli ${version}`);
|
|
2263
|
+
}
|
|
2264
|
+
async function printLatestHint() {
|
|
2265
|
+
if (!process.stderr.isTTY) return;
|
|
2266
|
+
if (updatesOptedOut(process.env)) return;
|
|
2267
|
+
const current = ownVersion();
|
|
2268
|
+
const currentTriple = current === null ? null : parseTriple(current);
|
|
2269
|
+
if (!currentTriple || currentTriple.join(".") === "0.0.0") return;
|
|
2270
|
+
const latest = await readCachedLatest();
|
|
2271
|
+
if (latest === null || !isNewer(latest, currentTriple.join("."))) return;
|
|
2272
|
+
const c = palette(useColor(process.env, true));
|
|
2273
|
+
console.error(
|
|
2274
|
+
message(`${c.faint}latest: ${latest}${c.reset} \u2192 run ${c.bold}ymmv update${c.reset}`)
|
|
2275
|
+
);
|
|
1931
2276
|
}
|
|
1932
2277
|
async function interactive(run, yes) {
|
|
1933
2278
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
@@ -1938,9 +2283,12 @@ async function interactive(run, yes) {
|
|
|
1938
2283
|
prompter?.close();
|
|
1939
2284
|
}
|
|
1940
2285
|
}
|
|
2286
|
+
function wantsUpdateCheck(kind) {
|
|
2287
|
+
return kind !== "help" && kind !== "version" && kind !== "error" && kind !== "update";
|
|
2288
|
+
}
|
|
1941
2289
|
async function main(argv) {
|
|
1942
2290
|
const cmd = resolveArg(argv);
|
|
1943
|
-
if (cmd.kind !== "logout") {
|
|
2291
|
+
if (cmd.kind !== "logout" && cmd.kind !== "update") {
|
|
1944
2292
|
const problem = baseProblem() ?? credentialEnvProblem();
|
|
1945
2293
|
if (problem) {
|
|
1946
2294
|
console.error(message(problem));
|
|
@@ -1948,6 +2296,17 @@ async function main(argv) {
|
|
|
1948
2296
|
return;
|
|
1949
2297
|
}
|
|
1950
2298
|
}
|
|
2299
|
+
const update = wantsUpdateCheck(cmd.kind) ? startUpdateCheck() : null;
|
|
2300
|
+
try {
|
|
2301
|
+
await dispatch(cmd);
|
|
2302
|
+
} catch (e) {
|
|
2303
|
+
update?.abort();
|
|
2304
|
+
throw e;
|
|
2305
|
+
}
|
|
2306
|
+
const notice = await update?.finish();
|
|
2307
|
+
if (notice) console.error(message(notice));
|
|
2308
|
+
}
|
|
2309
|
+
async function dispatch(cmd) {
|
|
1951
2310
|
switch (cmd.kind) {
|
|
1952
2311
|
case "publish":
|
|
1953
2312
|
await interactive(publish, cmd.yes);
|
|
@@ -1980,11 +2339,15 @@ async function main(argv) {
|
|
|
1980
2339
|
);
|
|
1981
2340
|
}
|
|
1982
2341
|
break;
|
|
2342
|
+
case "update":
|
|
2343
|
+
await runUpdate();
|
|
2344
|
+
break;
|
|
1983
2345
|
case "help":
|
|
1984
2346
|
console.log(help(palette(colorEnabled())));
|
|
1985
2347
|
break;
|
|
1986
2348
|
case "version":
|
|
1987
2349
|
printVersion();
|
|
2350
|
+
await printLatestHint();
|
|
1988
2351
|
break;
|
|
1989
2352
|
case "error":
|
|
1990
2353
|
console.error(message(cmd.message));
|