@bigstrider/transcodes-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 +32 -4
- package/dist/index.js +2258 -207
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ../packages/core/dist/stepup/client.js
|
|
4
4
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
5
5
|
async function request(config, input) {
|
|
6
|
-
const
|
|
6
|
+
const path6 = input.path.startsWith("/") ? input.path : `/${input.path}`;
|
|
7
7
|
const params = new URLSearchParams();
|
|
8
8
|
if (input.query) {
|
|
9
9
|
for (const [k, v] of Object.entries(input.query)) {
|
|
@@ -13,14 +13,11 @@ async function request(config, input) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
const qs = params.toString();
|
|
16
|
-
const url = `${config.apiBaseV1}${
|
|
16
|
+
const url = `${config.apiBaseV1}${path6}${qs ? `?${qs}` : ""}`;
|
|
17
17
|
const headers = {
|
|
18
18
|
"x-transcodes-token": config.token,
|
|
19
19
|
Accept: "application/json"
|
|
20
20
|
};
|
|
21
|
-
if (input.stepUpSid) {
|
|
22
|
-
headers["X-Step-Up-Session-Id"] = input.stepUpSid;
|
|
23
|
-
}
|
|
24
21
|
let body;
|
|
25
22
|
const sendsBody = input.method !== "GET" && !input.omitBody;
|
|
26
23
|
if (sendsBody) {
|
|
@@ -63,6 +60,13 @@ async function request(config, input) {
|
|
|
63
60
|
}
|
|
64
61
|
}
|
|
65
62
|
|
|
63
|
+
// ../packages/core/dist/contract/messages.js
|
|
64
|
+
var TRANSCODES_GUARD_REPO_URL = "https://github.com/transcodings/transcodes-guard";
|
|
65
|
+
var CLI_INSTALL_SH_URL = "https://raw.githubusercontent.com/transcodings/transcodes-guard/prod/cli/install.sh";
|
|
66
|
+
var CLI_INSTALL_PS1_URL = "https://raw.githubusercontent.com/transcodings/transcodes-guard/prod/cli/install.ps1";
|
|
67
|
+
var CLI_INSTALL_HINT_UNIX = `curl -fsSL ${CLI_INSTALL_SH_URL} | bash && transcodes install`;
|
|
68
|
+
var CLI_INSTALL_HINT_WIN = `irm ${CLI_INSTALL_PS1_URL} | iex; transcodes install`;
|
|
69
|
+
|
|
66
70
|
// ../packages/core/dist/stepup/jwt.js
|
|
67
71
|
var REQUIRED_AUDIENCE = "transcodes-mcp";
|
|
68
72
|
function isPlainObject(v) {
|
|
@@ -83,8 +87,8 @@ function readString(rec, key) {
|
|
|
83
87
|
const v = rec[key];
|
|
84
88
|
if (typeof v !== "string")
|
|
85
89
|
return void 0;
|
|
86
|
-
const
|
|
87
|
-
return
|
|
90
|
+
const t2 = v.trim();
|
|
91
|
+
return t2 || void 0;
|
|
88
92
|
}
|
|
89
93
|
function readNumericDate(rec, key) {
|
|
90
94
|
const v = rec[key];
|
|
@@ -94,8 +98,8 @@ function readNumericDate(rec, key) {
|
|
|
94
98
|
function readAudience(rec) {
|
|
95
99
|
const aud = rec.aud;
|
|
96
100
|
if (typeof aud === "string") {
|
|
97
|
-
const
|
|
98
|
-
return
|
|
101
|
+
const t2 = aud.trim();
|
|
102
|
+
return t2 ? [t2] : void 0;
|
|
99
103
|
}
|
|
100
104
|
if (Array.isArray(aud)) {
|
|
101
105
|
const list = aud.filter((x) => typeof x === "string").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
@@ -274,12 +278,12 @@ function writeTokenToFile(token, label) {
|
|
|
274
278
|
throw new Error("token is empty");
|
|
275
279
|
}
|
|
276
280
|
const nextLabel = normalizeLabel(label);
|
|
277
|
-
const
|
|
278
|
-
const existing =
|
|
281
|
+
const current2 = readConfig();
|
|
282
|
+
const existing = current2.tokenList.find((r) => r.token === trimmed);
|
|
279
283
|
if (!existing && !nextLabel) {
|
|
280
284
|
throw new Error("label is required");
|
|
281
285
|
}
|
|
282
|
-
const tokenList = existing ?
|
|
286
|
+
const tokenList = existing ? current2.tokenList.map((r) => r.token === trimmed ? { token: trimmed, label: nextLabel ?? r.label } : r) : [...current2.tokenList, { token: trimmed, label: nextLabel }];
|
|
283
287
|
writeConfig({ token: trimmed, tokenList });
|
|
284
288
|
}
|
|
285
289
|
function setActiveToken(token) {
|
|
@@ -291,22 +295,22 @@ function setTokenLabel(token, label) {
|
|
|
291
295
|
if (!nextLabel) {
|
|
292
296
|
throw new Error("label is required");
|
|
293
297
|
}
|
|
294
|
-
const
|
|
295
|
-
if (!
|
|
298
|
+
const current2 = readConfig();
|
|
299
|
+
if (!current2.tokenList.some((r) => r.token === trimmed)) {
|
|
296
300
|
throw new Error("token not found");
|
|
297
301
|
}
|
|
298
|
-
const tokenList =
|
|
299
|
-
writeConfig({ token:
|
|
302
|
+
const tokenList = current2.tokenList.map((r) => r.token === trimmed ? { token: r.token, label: nextLabel } : r);
|
|
303
|
+
writeConfig({ token: current2.token, tokenList });
|
|
300
304
|
}
|
|
301
305
|
function removeTokenFromFile(token) {
|
|
302
306
|
const trimmed = token.trim();
|
|
303
|
-
const
|
|
304
|
-
const tokenList =
|
|
307
|
+
const current2 = readConfig();
|
|
308
|
+
const tokenList = current2.tokenList.filter((r) => r.token !== trimmed);
|
|
305
309
|
if (tokenList.length === 0) {
|
|
306
310
|
clearTokenFile();
|
|
307
311
|
return;
|
|
308
312
|
}
|
|
309
|
-
const active =
|
|
313
|
+
const active = current2.token && tokenList.some((r) => r.token === current2.token) ? current2.token : tokenList[0].token;
|
|
310
314
|
writeConfig({ token: active, tokenList });
|
|
311
315
|
}
|
|
312
316
|
function clearTokenFile() {
|
|
@@ -336,7 +340,7 @@ function loadStepupConfig() {
|
|
|
336
340
|
}
|
|
337
341
|
const { token: tokenRaw } = resolveToken();
|
|
338
342
|
if (!tokenRaw) {
|
|
339
|
-
throw new Error(
|
|
343
|
+
throw new Error(`No Transcodes token found. Install the CLI (macOS/Linux: \`${CLI_INSTALL_HINT_UNIX}\`; Windows: \`${CLI_INSTALL_HINT_WIN}\`), then run \`transcodes login\` to sign in and choose an organization. Manual fallback: \`transcodes set <token> -l <label>\`.`);
|
|
340
344
|
}
|
|
341
345
|
const parsed = parseMemberAccessToken(tokenRaw);
|
|
342
346
|
for (const w of parsed.warnings) {
|
|
@@ -493,11 +497,41 @@ async function openConsoleSession(options) {
|
|
|
493
497
|
};
|
|
494
498
|
}
|
|
495
499
|
|
|
500
|
+
// ../packages/core/dist/paths/index.js
|
|
501
|
+
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, renameSync } from "fs";
|
|
502
|
+
import os2 from "os";
|
|
503
|
+
import path2 from "path";
|
|
504
|
+
function transcodesDir() {
|
|
505
|
+
return path2.join(os2.homedir(), ".transcodes");
|
|
506
|
+
}
|
|
507
|
+
function stateDir() {
|
|
508
|
+
return path2.join(transcodesDir(), "state");
|
|
509
|
+
}
|
|
510
|
+
function dataDir() {
|
|
511
|
+
return stateDir();
|
|
512
|
+
}
|
|
513
|
+
|
|
496
514
|
// src/commands.ts
|
|
497
515
|
var CLI_COMMAND_SPECS = [
|
|
516
|
+
{
|
|
517
|
+
usage: "transcodes install",
|
|
518
|
+
description: "Guided setup: pick host plugins to install, sign in, then open the dashboard (--all / <platforms>)"
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
usage: "transcodes update",
|
|
522
|
+
description: "Update installed host plugins in place and bump this CLI from npm (--cli-only / --plugins-only / --all)"
|
|
523
|
+
},
|
|
498
524
|
{
|
|
499
525
|
usage: "transcodes",
|
|
500
|
-
description: "Open the local dashboard (
|
|
526
|
+
description: "Open the local dashboard in the background (reuses a running server; default port 3847; --port N / --no-open)"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
usage: "transcodes stop",
|
|
530
|
+
description: "Stop the background local dashboard daemon"
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
usage: "transcodes login",
|
|
534
|
+
description: "Sign in with Google in your browser, choose an organization, and save the issued member token (-l <label> / --no-open)"
|
|
501
535
|
},
|
|
502
536
|
{
|
|
503
537
|
usage: "transcodes set <token> -l <label>",
|
|
@@ -556,15 +590,27 @@ function renderCliCommandsHtml() {
|
|
|
556
590
|
).join("\n ");
|
|
557
591
|
}
|
|
558
592
|
|
|
593
|
+
// src/dashboard-lifecycle.ts
|
|
594
|
+
import { execFile as execFile2, spawn as spawn3 } from "child_process";
|
|
595
|
+
import {
|
|
596
|
+
closeSync,
|
|
597
|
+
mkdirSync as mkdirSync3,
|
|
598
|
+
openSync,
|
|
599
|
+
readFileSync as readFileSync2,
|
|
600
|
+
unlinkSync,
|
|
601
|
+
writeFileSync as writeFileSync2
|
|
602
|
+
} from "fs";
|
|
603
|
+
import http from "http";
|
|
604
|
+
import path4 from "path";
|
|
605
|
+
import { promisify as promisify2 } from "util";
|
|
606
|
+
|
|
559
607
|
// src/dashboard.ts
|
|
560
|
-
import { spawn as spawn2 } from "child_process";
|
|
608
|
+
import { execFile, spawn as spawn2 } from "child_process";
|
|
561
609
|
import { createHash } from "crypto";
|
|
562
610
|
import {
|
|
563
611
|
createServer
|
|
564
612
|
} from "http";
|
|
565
|
-
|
|
566
|
-
// ../packages/core/dist/contract/messages.js
|
|
567
|
-
var TRANSCODES_GUARD_REPO_URL = "https://github.com/transcodings/transcodes-guard";
|
|
613
|
+
import { promisify } from "util";
|
|
568
614
|
|
|
569
615
|
// src/logo.ts
|
|
570
616
|
var LOGO_DATA_URI = "data:image/png;base64,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";
|
|
@@ -617,9 +663,17 @@ var CLI_PACKAGE_NAME = pkg.name;
|
|
|
617
663
|
var CLI_VERSION = pkg.version;
|
|
618
664
|
|
|
619
665
|
// src/dashboard.ts
|
|
620
|
-
var
|
|
621
|
-
var
|
|
666
|
+
var DEFAULT_DASHBOARD_PORT = 3847;
|
|
667
|
+
var DASHBOARD_HOST = "127.0.0.1";
|
|
668
|
+
var DASHBOARD_PORT_ATTEMPTS = 10;
|
|
669
|
+
var DEFAULT_PORT = DEFAULT_DASHBOARD_PORT;
|
|
670
|
+
var HOST = DASHBOARD_HOST;
|
|
671
|
+
var PORT_ATTEMPTS = DASHBOARD_PORT_ATTEMPTS;
|
|
672
|
+
var DASHBOARD_HEALTH_MARKER = "transcodes-dashboard";
|
|
622
673
|
var GUIDELINE_MUX_PLAYBACK_ID = "h1vOCPmFDA02fGhcout01FZWD4lpKNTzLFk7vybxvrc3M";
|
|
674
|
+
var APP_ORG_URL = "https://app.transcodes.io/en/org";
|
|
675
|
+
var APP_HOME_URL = "https://app.transcodes.io";
|
|
676
|
+
var execFileAsync = promisify(execFile);
|
|
623
677
|
function fingerprint(token) {
|
|
624
678
|
return createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
625
679
|
}
|
|
@@ -699,7 +753,7 @@ async function buildStatus() {
|
|
|
699
753
|
};
|
|
700
754
|
}
|
|
701
755
|
function tokenById(id) {
|
|
702
|
-
return readTokenList().find((
|
|
756
|
+
return readTokenList().find((t2) => fingerprint(t2) === id);
|
|
703
757
|
}
|
|
704
758
|
function sendJson(res, status, body) {
|
|
705
759
|
res.writeHead(status, {
|
|
@@ -827,6 +881,24 @@ function dashboardHtml() {
|
|
|
827
881
|
font-weight: 600;
|
|
828
882
|
line-height: 1.45;
|
|
829
883
|
}
|
|
884
|
+
.header-token-empty-title { margin: 0; }
|
|
885
|
+
.header-token-empty-cmds {
|
|
886
|
+
margin: 10px 0 0;
|
|
887
|
+
display: flex;
|
|
888
|
+
flex-direction: column;
|
|
889
|
+
align-items: flex-start;
|
|
890
|
+
gap: 6px;
|
|
891
|
+
}
|
|
892
|
+
.header-token-empty-cmds code {
|
|
893
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
894
|
+
font-size: var(--text-2xs);
|
|
895
|
+
font-weight: 600;
|
|
896
|
+
color: var(--accent);
|
|
897
|
+
background: #fff;
|
|
898
|
+
border: 1px solid #f5c6cb;
|
|
899
|
+
padding: 3px 9px;
|
|
900
|
+
border-radius: 7px;
|
|
901
|
+
}
|
|
830
902
|
.header-profile {
|
|
831
903
|
display: flex;
|
|
832
904
|
align-items: center;
|
|
@@ -862,6 +934,9 @@ function dashboardHtml() {
|
|
|
862
934
|
}
|
|
863
935
|
.btn-manage-auth {
|
|
864
936
|
flex-shrink: 0;
|
|
937
|
+
display: inline-flex;
|
|
938
|
+
align-items: center;
|
|
939
|
+
gap: 6px;
|
|
865
940
|
border: none;
|
|
866
941
|
border-radius: 10px;
|
|
867
942
|
padding: 10px 16px;
|
|
@@ -872,11 +947,38 @@ function dashboardHtml() {
|
|
|
872
947
|
cursor: pointer;
|
|
873
948
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
874
949
|
}
|
|
950
|
+
.btn-manage-auth svg {
|
|
951
|
+
width: 20px;
|
|
952
|
+
height: 20px;
|
|
953
|
+
flex-shrink: 0;
|
|
954
|
+
}
|
|
875
955
|
.btn-manage-auth:hover:not(:disabled) { opacity: 0.92; }
|
|
876
956
|
.btn-manage-auth:disabled {
|
|
877
957
|
opacity: 0.55;
|
|
878
958
|
cursor: not-allowed;
|
|
879
959
|
}
|
|
960
|
+
.btn-manage-tokens {
|
|
961
|
+
margin-left: auto;
|
|
962
|
+
flex-shrink: 0;
|
|
963
|
+
display: inline-flex;
|
|
964
|
+
align-items: center;
|
|
965
|
+
gap: 6px;
|
|
966
|
+
border: none;
|
|
967
|
+
border-radius: 10px;
|
|
968
|
+
padding: 10px 16px;
|
|
969
|
+
font-size: var(--text-sm);
|
|
970
|
+
font-weight: 600;
|
|
971
|
+
color: #fff;
|
|
972
|
+
background: var(--accent);
|
|
973
|
+
cursor: pointer;
|
|
974
|
+
transition: opacity 0.15s ease;
|
|
975
|
+
}
|
|
976
|
+
.btn-manage-tokens svg {
|
|
977
|
+
width: 20px;
|
|
978
|
+
height: 20px;
|
|
979
|
+
flex-shrink: 0;
|
|
980
|
+
}
|
|
981
|
+
.btn-manage-tokens:hover { opacity: 0.92; }
|
|
880
982
|
.header-profile-actions {
|
|
881
983
|
flex-shrink: 0;
|
|
882
984
|
display: flex;
|
|
@@ -953,6 +1055,18 @@ function dashboardHtml() {
|
|
|
953
1055
|
margin: 0 0 4px;
|
|
954
1056
|
letter-spacing: -0.01em;
|
|
955
1057
|
}
|
|
1058
|
+
.section-title--spaced {
|
|
1059
|
+
margin: 26px 0 4px;
|
|
1060
|
+
}
|
|
1061
|
+
.section-title-row {
|
|
1062
|
+
display: flex;
|
|
1063
|
+
align-items: center;
|
|
1064
|
+
justify-content: space-between;
|
|
1065
|
+
gap: 12px;
|
|
1066
|
+
margin: 0 0 4px;
|
|
1067
|
+
}
|
|
1068
|
+
.section-title-row .section-title { margin: 0; }
|
|
1069
|
+
.section-title-row .guide-video-toggle { flex-shrink: 0; }
|
|
956
1070
|
.section-sub {
|
|
957
1071
|
font-size: var(--text-base);
|
|
958
1072
|
color: var(--muted);
|
|
@@ -1223,8 +1337,8 @@ function dashboardHtml() {
|
|
|
1223
1337
|
.list-label {
|
|
1224
1338
|
margin: 26px 0 10px;
|
|
1225
1339
|
font-size: var(--text-sm);
|
|
1226
|
-
font-weight:
|
|
1227
|
-
color: var(--
|
|
1340
|
+
font-weight: 700;
|
|
1341
|
+
color: var(--ink);
|
|
1228
1342
|
letter-spacing: 0.01em;
|
|
1229
1343
|
}
|
|
1230
1344
|
.token-list { display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -1524,6 +1638,32 @@ function dashboardHtml() {
|
|
|
1524
1638
|
line-height: 1.5;
|
|
1525
1639
|
margin: 0;
|
|
1526
1640
|
}
|
|
1641
|
+
.guide-video-wrap {
|
|
1642
|
+
margin: 0;
|
|
1643
|
+
}
|
|
1644
|
+
.guide-video-toggle {
|
|
1645
|
+
display: inline-flex;
|
|
1646
|
+
align-items: center;
|
|
1647
|
+
gap: 6px;
|
|
1648
|
+
border: 1px solid var(--line);
|
|
1649
|
+
border-radius: 999px;
|
|
1650
|
+
padding: 6px 12px;
|
|
1651
|
+
font-size: var(--text-xs);
|
|
1652
|
+
font-weight: 600;
|
|
1653
|
+
color: var(--ink);
|
|
1654
|
+
background: #fff;
|
|
1655
|
+
cursor: pointer;
|
|
1656
|
+
transition: background 0.15s ease, border-color 0.15s ease;
|
|
1657
|
+
}
|
|
1658
|
+
.guide-video-toggle:hover {
|
|
1659
|
+
background: #f7f7f9;
|
|
1660
|
+
border-color: #d8d8de;
|
|
1661
|
+
}
|
|
1662
|
+
.guide-video-toggle[aria-expanded="true"] {
|
|
1663
|
+
color: var(--accent);
|
|
1664
|
+
border-color: rgba(91, 84, 230, 0.35);
|
|
1665
|
+
background: var(--accent-soft);
|
|
1666
|
+
}
|
|
1527
1667
|
.guide-video {
|
|
1528
1668
|
margin: 0 0 22px;
|
|
1529
1669
|
border-radius: 14px;
|
|
@@ -1533,6 +1673,7 @@ function dashboardHtml() {
|
|
|
1533
1673
|
aspect-ratio: 16 / 9;
|
|
1534
1674
|
box-shadow: 0 8px 28px rgba(22, 22, 26, 0.08);
|
|
1535
1675
|
}
|
|
1676
|
+
.guide-video[hidden] { display: none !important; }
|
|
1536
1677
|
.guide-video mux-player {
|
|
1537
1678
|
width: 100%;
|
|
1538
1679
|
height: 100%;
|
|
@@ -1598,6 +1739,37 @@ function dashboardHtml() {
|
|
|
1598
1739
|
border: 1px solid var(--line);
|
|
1599
1740
|
border-radius: 14px;
|
|
1600
1741
|
}
|
|
1742
|
+
.guide-step--accordion {
|
|
1743
|
+
display: block;
|
|
1744
|
+
padding: 0;
|
|
1745
|
+
overflow: hidden;
|
|
1746
|
+
}
|
|
1747
|
+
.guide-step-summary {
|
|
1748
|
+
display: flex;
|
|
1749
|
+
align-items: center;
|
|
1750
|
+
gap: 14px;
|
|
1751
|
+
padding: 14px 16px;
|
|
1752
|
+
cursor: pointer;
|
|
1753
|
+
list-style: none;
|
|
1754
|
+
user-select: none;
|
|
1755
|
+
}
|
|
1756
|
+
.guide-step-summary::-webkit-details-marker { display: none; }
|
|
1757
|
+
.guide-step-summary::marker { content: ''; }
|
|
1758
|
+
.guide-step-summary:hover { background: #f7f7f9; }
|
|
1759
|
+
.guide-step--accordion[open] .guide-step-summary {
|
|
1760
|
+
border-bottom: 1px solid var(--line);
|
|
1761
|
+
}
|
|
1762
|
+
.guide-step-chevron {
|
|
1763
|
+
margin-left: auto;
|
|
1764
|
+
flex-shrink: 0;
|
|
1765
|
+
width: 16px;
|
|
1766
|
+
height: 16px;
|
|
1767
|
+
color: var(--muted);
|
|
1768
|
+
transition: transform 0.15s ease;
|
|
1769
|
+
}
|
|
1770
|
+
.guide-step--accordion[open] .guide-step-chevron {
|
|
1771
|
+
transform: rotate(180deg);
|
|
1772
|
+
}
|
|
1601
1773
|
.guide-step-num {
|
|
1602
1774
|
flex: 0 0 28px;
|
|
1603
1775
|
width: 28px;
|
|
@@ -1612,18 +1784,35 @@ function dashboardHtml() {
|
|
|
1612
1784
|
justify-content: center;
|
|
1613
1785
|
}
|
|
1614
1786
|
.guide-step-body { min-width: 0; }
|
|
1787
|
+
.guide-step--accordion > .guide-step-body {
|
|
1788
|
+
padding: 12px 16px 14px 58px;
|
|
1789
|
+
}
|
|
1615
1790
|
.guide-step-title {
|
|
1616
1791
|
font-size: var(--text-sm);
|
|
1617
1792
|
font-weight: 700;
|
|
1618
1793
|
color: var(--ink);
|
|
1619
1794
|
margin: 0 0 4px;
|
|
1620
1795
|
}
|
|
1796
|
+
.guide-step-summary .guide-step-title { margin: 0; flex: 1; min-width: 0; }
|
|
1621
1797
|
.guide-step-desc {
|
|
1622
1798
|
font-size: var(--text-sm);
|
|
1623
1799
|
color: var(--muted);
|
|
1624
1800
|
line-height: 1.5;
|
|
1625
1801
|
margin: 0;
|
|
1626
1802
|
}
|
|
1803
|
+
.guide-step-desc a,
|
|
1804
|
+
.guide-console-link {
|
|
1805
|
+
color: var(--accent);
|
|
1806
|
+
font-weight: 600;
|
|
1807
|
+
text-decoration: underline;
|
|
1808
|
+
background: none;
|
|
1809
|
+
border: none;
|
|
1810
|
+
padding: 0;
|
|
1811
|
+
font: inherit;
|
|
1812
|
+
cursor: pointer;
|
|
1813
|
+
}
|
|
1814
|
+
.guide-step-desc a:hover,
|
|
1815
|
+
.guide-console-link:hover { opacity: 0.88; }
|
|
1627
1816
|
.guide-step-desc code {
|
|
1628
1817
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
1629
1818
|
font-size: var(--text-2xs);
|
|
@@ -1677,13 +1866,49 @@ function dashboardHtml() {
|
|
|
1677
1866
|
}
|
|
1678
1867
|
.guide-help {
|
|
1679
1868
|
margin: 0 0 18px;
|
|
1680
|
-
padding:
|
|
1869
|
+
padding: 0;
|
|
1681
1870
|
background: var(--accent-soft);
|
|
1682
1871
|
border: 1px solid rgba(91, 84, 230, 0.18);
|
|
1683
1872
|
border-radius: 14px;
|
|
1873
|
+
overflow: hidden;
|
|
1684
1874
|
}
|
|
1685
|
-
.guide-help-
|
|
1875
|
+
.guide-help-accordion { margin: 0; }
|
|
1876
|
+
.guide-help-summary {
|
|
1877
|
+
display: flex;
|
|
1878
|
+
align-items: center;
|
|
1879
|
+
gap: 10px;
|
|
1880
|
+
padding: 14px 18px;
|
|
1881
|
+
cursor: pointer;
|
|
1882
|
+
list-style: none;
|
|
1883
|
+
user-select: none;
|
|
1884
|
+
}
|
|
1885
|
+
.guide-help-summary::-webkit-details-marker { display: none; }
|
|
1886
|
+
.guide-help-summary::marker { content: ''; }
|
|
1887
|
+
.guide-help-summary:hover { background: rgba(91, 84, 230, 0.06); }
|
|
1888
|
+
.guide-help-summary .guide-help-heading {
|
|
1686
1889
|
margin: 0;
|
|
1890
|
+
flex: 1;
|
|
1891
|
+
min-width: 0;
|
|
1892
|
+
font-size: var(--text-sm);
|
|
1893
|
+
font-weight: 700;
|
|
1894
|
+
color: var(--ink);
|
|
1895
|
+
}
|
|
1896
|
+
.guide-help-chevron {
|
|
1897
|
+
width: 18px;
|
|
1898
|
+
height: 18px;
|
|
1899
|
+
flex-shrink: 0;
|
|
1900
|
+
color: var(--muted);
|
|
1901
|
+
transition: transform 0.15s ease;
|
|
1902
|
+
}
|
|
1903
|
+
.guide-help-accordion[open] .guide-help-chevron {
|
|
1904
|
+
transform: rotate(180deg);
|
|
1905
|
+
}
|
|
1906
|
+
.guide-help-body {
|
|
1907
|
+
padding: 0 18px 16px;
|
|
1908
|
+
border-top: 1px solid rgba(91, 84, 230, 0.12);
|
|
1909
|
+
}
|
|
1910
|
+
.guide-help-line {
|
|
1911
|
+
margin: 12px 0 0;
|
|
1687
1912
|
font-size: var(--text-sm);
|
|
1688
1913
|
color: #4a4a52;
|
|
1689
1914
|
line-height: 1.6;
|
|
@@ -1703,7 +1928,7 @@ function dashboardHtml() {
|
|
|
1703
1928
|
color: #4a4a52;
|
|
1704
1929
|
line-height: 1.6;
|
|
1705
1930
|
}
|
|
1706
|
-
.guide-help-heading:first-child { margin-top:
|
|
1931
|
+
.guide-help-body > .guide-help-heading:first-child { margin-top: 12px; }
|
|
1707
1932
|
.guide-help-list {
|
|
1708
1933
|
margin: 6px 0 0;
|
|
1709
1934
|
padding-left: 18px;
|
|
@@ -1717,12 +1942,56 @@ function dashboardHtml() {
|
|
|
1717
1942
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
1718
1943
|
font-size: var(--text-xs);
|
|
1719
1944
|
}
|
|
1945
|
+
.guide-prefix-note {
|
|
1946
|
+
margin: 0 0 14px;
|
|
1947
|
+
font-size: var(--text-sm);
|
|
1948
|
+
color: var(--muted);
|
|
1949
|
+
line-height: 1.6;
|
|
1950
|
+
}
|
|
1951
|
+
.guide-prefix-note code.cli-cmd { white-space: nowrap; }
|
|
1720
1952
|
.guide-help-examples {
|
|
1721
1953
|
margin: 10px 0 0;
|
|
1722
1954
|
display: flex;
|
|
1723
1955
|
flex-direction: column;
|
|
1724
1956
|
gap: 8px;
|
|
1725
1957
|
}
|
|
1958
|
+
.guide-cmd-row {
|
|
1959
|
+
display: flex;
|
|
1960
|
+
flex-direction: column;
|
|
1961
|
+
gap: 4px;
|
|
1962
|
+
}
|
|
1963
|
+
.guide-cmd-hosts {
|
|
1964
|
+
font-size: var(--text-2xs);
|
|
1965
|
+
font-weight: 600;
|
|
1966
|
+
color: var(--muted);
|
|
1967
|
+
text-transform: uppercase;
|
|
1968
|
+
letter-spacing: 0.04em;
|
|
1969
|
+
}
|
|
1970
|
+
.guide-classify-list {
|
|
1971
|
+
margin: 10px 0 0;
|
|
1972
|
+
padding: 0;
|
|
1973
|
+
list-style: none;
|
|
1974
|
+
display: flex;
|
|
1975
|
+
flex-direction: column;
|
|
1976
|
+
gap: 8px;
|
|
1977
|
+
}
|
|
1978
|
+
.guide-classify-list li {
|
|
1979
|
+
display: flex;
|
|
1980
|
+
align-items: center;
|
|
1981
|
+
flex-wrap: wrap;
|
|
1982
|
+
gap: 8px;
|
|
1983
|
+
font-size: var(--text-sm);
|
|
1984
|
+
color: var(--muted);
|
|
1985
|
+
line-height: 1.5;
|
|
1986
|
+
}
|
|
1987
|
+
.guide-classify-prompt { color: var(--ink); }
|
|
1988
|
+
.guide-classify-arrow { color: var(--muted); }
|
|
1989
|
+
.guide-classify-note {
|
|
1990
|
+
margin: 10px 0 0;
|
|
1991
|
+
font-size: var(--text-sm);
|
|
1992
|
+
color: var(--muted);
|
|
1993
|
+
line-height: 1.55;
|
|
1994
|
+
}
|
|
1726
1995
|
.guide-help-examples code.cli-cmd {
|
|
1727
1996
|
display: block;
|
|
1728
1997
|
width: fit-content;
|
|
@@ -1782,12 +2051,27 @@ function dashboardHtml() {
|
|
|
1782
2051
|
display: flex;
|
|
1783
2052
|
align-items: center;
|
|
1784
2053
|
justify-content: center;
|
|
2054
|
+
flex-wrap: wrap;
|
|
1785
2055
|
gap: 8px;
|
|
1786
2056
|
font-size: var(--text-2xs);
|
|
1787
2057
|
color: var(--muted);
|
|
1788
2058
|
text-align: center;
|
|
1789
2059
|
line-height: 1.6;
|
|
1790
2060
|
}
|
|
2061
|
+
.dashboard-footer a {
|
|
2062
|
+
display: inline-flex;
|
|
2063
|
+
align-items: center;
|
|
2064
|
+
gap: 5px;
|
|
2065
|
+
color: var(--accent);
|
|
2066
|
+
font-weight: 600;
|
|
2067
|
+
text-decoration: none;
|
|
2068
|
+
}
|
|
2069
|
+
.dashboard-footer a:hover { text-decoration: underline; }
|
|
2070
|
+
.dashboard-footer-github-icon {
|
|
2071
|
+
width: 14px;
|
|
2072
|
+
height: 14px;
|
|
2073
|
+
flex-shrink: 0;
|
|
2074
|
+
}
|
|
1791
2075
|
</style>
|
|
1792
2076
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@mux/mux-player"></script>
|
|
1793
2077
|
</head>
|
|
@@ -1809,14 +2093,25 @@ function dashboardHtml() {
|
|
|
1809
2093
|
<p class="header-tagline">Manage credentials and view RBAC from one panel \u2014 no CLI typing required</p>
|
|
1810
2094
|
</div>
|
|
1811
2095
|
</div>
|
|
1812
|
-
<
|
|
2096
|
+
<div id="header-token-empty" class="header-token-empty" hidden>
|
|
2097
|
+
<p class="header-token-empty-title">No Transcodes CLI authorization found</p>
|
|
2098
|
+
<p class="header-token-empty-title">Sign in with Google and choose an organization:</p>
|
|
2099
|
+
<div class="header-token-empty-cmds">
|
|
2100
|
+
<code>transcodes login</code>
|
|
2101
|
+
</div>
|
|
2102
|
+
</div>
|
|
1813
2103
|
<div class="header-profile" id="header-profile" hidden>
|
|
1814
2104
|
<div class="header-profile-info">
|
|
1815
2105
|
<div class="header-profile-name" id="header-profile-name"></div>
|
|
1816
2106
|
<div class="header-profile-meta" id="header-profile-meta"></div>
|
|
1817
2107
|
</div>
|
|
1818
2108
|
<div class="header-profile-actions">
|
|
1819
|
-
<button type="button" class="btn-manage-auth" id="manage-auth-btn"
|
|
2109
|
+
<button type="button" class="btn-manage-auth" id="manage-auth-btn" data-console-open aria-label="Open Transcodes console">
|
|
2110
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
2111
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.6 9.75m6.633-4.596a18.666 18.666 0 0 1-2.485 5.33" />
|
|
2112
|
+
</svg>
|
|
2113
|
+
Console
|
|
2114
|
+
</button>
|
|
1820
2115
|
<p class="header-profile-cli-hint"><code>transcodes console</code></p>
|
|
1821
2116
|
</div>
|
|
1822
2117
|
</div>
|
|
@@ -1829,99 +2124,160 @@ function dashboardHtml() {
|
|
|
1829
2124
|
</div>
|
|
1830
2125
|
|
|
1831
2126
|
<div class="panel active" id="panel-guideline">
|
|
1832
|
-
<
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
<code class="cli-cmd">/transcodes add signin / stepup / console in our admin dashboard</code>
|
|
1838
|
-
<code class="cli-cmd">/transcodes summarize member audit logs for the past 2 weeks</code>
|
|
1839
|
-
</div>
|
|
2127
|
+
<div class="section-title-row">
|
|
2128
|
+
<p class="section-title">Getting Started</p>
|
|
2129
|
+
<button type="button" class="guide-video-toggle" id="guide-video-toggle" aria-expanded="false" aria-controls="guide-video">
|
|
2130
|
+
Watch Intro Video
|
|
2131
|
+
</button>
|
|
1840
2132
|
</div>
|
|
1841
|
-
<
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2133
|
+
<p class="section-sub">Transcodes CLI Dashboard,Set it up visually \u2014 no terminal required. New to Transcodes? Watch the video.</p>
|
|
2134
|
+
<div class="guide-video-wrap">
|
|
2135
|
+
<div class="guide-video" id="guide-video" hidden>
|
|
2136
|
+
<mux-player
|
|
2137
|
+
playback-id="${GUIDELINE_MUX_PLAYBACK_ID}"
|
|
2138
|
+
stream-type="on-demand"
|
|
2139
|
+
accent-color="#5b54e6"
|
|
2140
|
+
primary-color="#ffffff"
|
|
2141
|
+
metadata-video-title="Transcodes CLI onboarding"
|
|
2142
|
+
></mux-player>
|
|
2143
|
+
</div>
|
|
1849
2144
|
</div>
|
|
1850
|
-
<p class="
|
|
2145
|
+
<p class="section-title section-title--spaced">Quick Demo</p>
|
|
2146
|
+
<p class="guide-prefix-note">Try the quick demo to see how Transcodes works</p>
|
|
2147
|
+
<p class="guide-prefix-note">Prefix prompts with <code class="cli-cmd">/transcodes</code> on Claude, Cursor, and Antigravity \u2014 use <code class="cli-cmd">$transcodes</code> on ChatGPT (Codex).</p>
|
|
1851
2148
|
<div class="guide-groups">
|
|
1852
|
-
<section class="guide-group guide-group--
|
|
1853
|
-
<a class="guide-group-label" href="https://app.transcodes.io/" target="_blank" rel="noopener noreferrer">Transcodes Console<span class="guide-group-link-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span></a>
|
|
2149
|
+
<section class="guide-group guide-group--panel">
|
|
1854
2150
|
<ol class="guide-steps">
|
|
1855
|
-
<li
|
|
1856
|
-
<
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2151
|
+
<li>
|
|
2152
|
+
<details class="guide-step guide-step--accordion">
|
|
2153
|
+
<summary class="guide-step-summary">
|
|
2154
|
+
<span class="guide-step-num">1</span>
|
|
2155
|
+
<span class="guide-step-title">Open your CLI or desktop app (Claude, Cursor, Antigravity, ChatGPT)</span>
|
|
2156
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2157
|
+
</summary>
|
|
2158
|
+
<div class="guide-step-body">
|
|
2159
|
+
<p class="guide-step-desc">If it's already running, restart it so the latest plugin and token are loaded</p>
|
|
2160
|
+
</div>
|
|
2161
|
+
</details>
|
|
1861
2162
|
</li>
|
|
1862
|
-
<li
|
|
1863
|
-
<
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2163
|
+
<li>
|
|
2164
|
+
<details class="guide-step guide-step--accordion">
|
|
2165
|
+
<summary class="guide-step-summary">
|
|
2166
|
+
<span class="guide-step-num">2</span>
|
|
2167
|
+
<span class="guide-step-title">Trigger a step-up test</span>
|
|
2168
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2169
|
+
</summary>
|
|
2170
|
+
<div class="guide-step-body">
|
|
2171
|
+
<p class="guide-step-desc">Ask the agent to run a protected action \u2014 it should prompt for step-up authentication</p>
|
|
2172
|
+
<div class="guide-help-examples">
|
|
2173
|
+
<div class="guide-cmd-row">
|
|
2174
|
+
<span class="guide-cmd-hosts">Claude \xB7 Cursor \xB7 Antigravity</span>
|
|
2175
|
+
<code class="cli-cmd">/transcodes open step-up authentication for testing</code>
|
|
2176
|
+
</div>
|
|
2177
|
+
<div class="guide-cmd-row">
|
|
2178
|
+
<span class="guide-cmd-hosts">ChatGPT (Codex)</span>
|
|
2179
|
+
<code class="cli-cmd">$transcodes open step-up authentication for testing</code>
|
|
2180
|
+
</div>
|
|
2181
|
+
</div>
|
|
2182
|
+
</div>
|
|
2183
|
+
</details>
|
|
1868
2184
|
</li>
|
|
1869
2185
|
</ol>
|
|
1870
2186
|
</section>
|
|
2187
|
+
</div>
|
|
1871
2188
|
|
|
2189
|
+
<p class="section-title section-title--spaced">Steps</p>
|
|
2190
|
+
<div class="guide-groups">
|
|
1872
2191
|
<section class="guide-group guide-group--panel">
|
|
1873
|
-
<p class="guide-group-label">This CLI panel</p>
|
|
1874
2192
|
<ol class="guide-steps">
|
|
1875
|
-
<li
|
|
1876
|
-
<
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2193
|
+
<li>
|
|
2194
|
+
<details class="guide-step guide-step--accordion">
|
|
2195
|
+
<summary class="guide-step-summary">
|
|
2196
|
+
<span class="guide-step-num">1</span>
|
|
2197
|
+
<span class="guide-step-title">Configure RBAC in the Transcodes app</span>
|
|
2198
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2199
|
+
</summary>
|
|
2200
|
+
<div class="guide-step-body">
|
|
2201
|
+
<p class="guide-step-desc">Set up resources, actions, and roles in the <a href="${APP_ORG_URL}" data-app-tab="rbac" target="_blank" rel="noopener noreferrer">Transcodes app</a>. You can review the resulting RBAC permissions read-only in this CLI panel's RBAC tab</p>
|
|
2202
|
+
</div>
|
|
2203
|
+
</details>
|
|
1881
2204
|
</li>
|
|
1882
|
-
<li
|
|
1883
|
-
<
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2205
|
+
<li>
|
|
2206
|
+
<details class="guide-step guide-step--accordion">
|
|
2207
|
+
<summary class="guide-step-summary">
|
|
2208
|
+
<span class="guide-step-num">2</span>
|
|
2209
|
+
<span class="guide-step-title">Register a passkey or biometrics</span>
|
|
2210
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2211
|
+
</summary>
|
|
2212
|
+
<div class="guide-step-body">
|
|
2213
|
+
<p class="guide-step-desc">Click the <button type="button" class="guide-console-link" data-console-open>Console</button> button (or run <code class="cli-cmd">transcodes console</code> in your terminal), sign in, then register a passkey or biometrics \u2014 these are used for step-up authentication</p>
|
|
2214
|
+
</div>
|
|
2215
|
+
</details>
|
|
1888
2216
|
</li>
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
<p class="guide-step-opensource">
|
|
1901
|
-
<a class="guide-opensource-link" href="${TRANSCODES_GUARD_REPO_URL}" target="_blank" rel="noopener noreferrer" aria-label="transcodes-guard on GitHub">
|
|
1902
|
-
<svg class="guide-opensource-icon" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
|
1903
|
-
We support Open Source, <span class="guide-opensource-accent">More Info</span>
|
|
1904
|
-
</a>
|
|
1905
|
-
</p>
|
|
1906
|
-
</div>
|
|
2217
|
+
<li>
|
|
2218
|
+
<details class="guide-step guide-step--accordion">
|
|
2219
|
+
<summary class="guide-step-summary">
|
|
2220
|
+
<span class="guide-step-num">3</span>
|
|
2221
|
+
<span class="guide-step-title">Open your CLI or desktop app (Claude, Cursor, Antigravity, ChatGPT)</span>
|
|
2222
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2223
|
+
</summary>
|
|
2224
|
+
<div class="guide-step-body">
|
|
2225
|
+
<p class="guide-step-desc">If it's already running, restart it so the latest plugin and token are loaded</p>
|
|
2226
|
+
</div>
|
|
2227
|
+
</details>
|
|
1907
2228
|
</li>
|
|
1908
|
-
<li
|
|
1909
|
-
<
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2229
|
+
<li>
|
|
2230
|
+
<details class="guide-step guide-step--accordion">
|
|
2231
|
+
<summary class="guide-step-summary">
|
|
2232
|
+
<span class="guide-step-num">4</span>
|
|
2233
|
+
<span class="guide-step-title">Run an action from the agent</span>
|
|
2234
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2235
|
+
</summary>
|
|
2236
|
+
<div class="guide-step-body">
|
|
2237
|
+
<p class="guide-step-desc">Try an action in your CLI or desktop app. Transcodes classifies each prompt into a <code>resource:action</code> pair, then applies the permission matrix \u2014 <strong>0 deny</strong> \xB7 <strong>1 allow (pass)</strong> \xB7 <strong>2 step-up MFA</strong>.</p>
|
|
2238
|
+
<ul class="guide-classify-list">
|
|
2239
|
+
<li><span class="guide-classify-prompt">"Create a Google Calendar event"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">google:create</code></li>
|
|
2240
|
+
<li><span class="guide-classify-prompt">"Change James's role in Transcodes"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">transcodes:update</code></li>
|
|
2241
|
+
<li><span class="guide-classify-prompt">"Delete files on my computer"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">system:delete</code></li>
|
|
2242
|
+
</ul>
|
|
2243
|
+
<p class="guide-classify-note">No matching resource? It falls back to <code class="cli-cmd">system</code> automatically \u2014 e.g. if there's no <code>google</code> resource, "Create a Google Calendar event" becomes <code class="cli-cmd">system:create</code>.</p>
|
|
2244
|
+
</div>
|
|
2245
|
+
</details>
|
|
2246
|
+
</li>
|
|
2247
|
+
<li>
|
|
2248
|
+
<details class="guide-step guide-step--accordion">
|
|
2249
|
+
<summary class="guide-step-summary">
|
|
2250
|
+
<span class="guide-step-num">5</span>
|
|
2251
|
+
<span class="guide-step-title">Pull the audit log report</span>
|
|
2252
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2253
|
+
</summary>
|
|
2254
|
+
<div class="guide-step-body">
|
|
2255
|
+
<p class="guide-step-desc">Ask the agent for a member audit / security log report \u2014 Transcodes maps the prompt to the right resource and enforces RBAC</p>
|
|
2256
|
+
<p class="guide-step-desc">Every action is logged \u2014 whether it was 0 deny, 1 allow, or 2 step-up MFA.</p>
|
|
2257
|
+
<div class="guide-help-examples">
|
|
2258
|
+
<div class="guide-cmd-row">
|
|
2259
|
+
<span class="guide-cmd-hosts">Claude \xB7 Cursor \xB7 Antigravity</span>
|
|
2260
|
+
<code class="cli-cmd">/transcodes show audit logs report</code>
|
|
2261
|
+
</div>
|
|
2262
|
+
<div class="guide-cmd-row">
|
|
2263
|
+
<span class="guide-cmd-hosts">ChatGPT (Codex)</span>
|
|
2264
|
+
<code class="cli-cmd">$transcodes show audit logs report</code>
|
|
2265
|
+
</div>
|
|
2266
|
+
</div>
|
|
2267
|
+
</div>
|
|
2268
|
+
</details>
|
|
1914
2269
|
</li>
|
|
1915
|
-
<li
|
|
1916
|
-
<
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2270
|
+
<li>
|
|
2271
|
+
<details class="guide-step guide-step--accordion">
|
|
2272
|
+
<summary class="guide-step-summary">
|
|
2273
|
+
<span class="guide-step-num">6</span>
|
|
2274
|
+
<span class="guide-step-title">Receive step-up links and notifications on Slack or Discord</span>
|
|
2275
|
+
<svg class="guide-step-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2276
|
+
</summary>
|
|
2277
|
+
<div class="guide-step-body">
|
|
2278
|
+
<p class="guide-step-desc">Want step-up links delivered to Slack or Discord? Go to <a href="${APP_ORG_URL}" data-app-tab="settings" target="_blank" rel="noopener noreferrer">Transcodes Settings</a> and set up a webhook. (More channels coming soon.)</p>
|
|
1923
2279
|
</div>
|
|
1924
|
-
</
|
|
2280
|
+
</details>
|
|
1925
2281
|
</li>
|
|
1926
2282
|
</ol>
|
|
1927
2283
|
</section>
|
|
@@ -1936,8 +2292,14 @@ function dashboardHtml() {
|
|
|
1936
2292
|
<p class="cli-map-row cli-map-row--section">
|
|
1937
2293
|
<span class="cli-map-label cli-map-label--title">MCP Access Token</span>
|
|
1938
2294
|
<code>transcodes set <token> -l <label></code>
|
|
2295
|
+
<button type="button" class="btn-manage-tokens" id="manage-tokens-btn" aria-label="Open Transcodes members page">
|
|
2296
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
2297
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
|
|
2298
|
+
</svg>
|
|
2299
|
+
Manage Tokens
|
|
2300
|
+
</button>
|
|
1939
2301
|
</p>
|
|
1940
|
-
<p class="section-sub">
|
|
2302
|
+
<p class="section-sub">Browser login is recommended: <code>transcodes login</code>. Manual token entry remains available for scripts and recovery.</p>
|
|
1941
2303
|
<textarea id="token" placeholder="eyJhbGciOi\u2026" spellcheck="false" autocomplete="off"></textarea>
|
|
1942
2304
|
<input type="text" id="label" class="label-input" placeholder="Label (required) \u2014 e.g. transcodes-{project_name}-{env}" autocomplete="off" required />
|
|
1943
2305
|
<div class="actions">
|
|
@@ -1960,7 +2322,7 @@ function dashboardHtml() {
|
|
|
1960
2322
|
</div>
|
|
1961
2323
|
<button type="button" class="btn-danger" id="reset-all">Reset all</button>
|
|
1962
2324
|
</div>
|
|
1963
|
-
<p class="hint">
|
|
2325
|
+
<p class="hint">Please read the Guideline tab to learn how to use Transcodes CLI.<br />Stop the background dashboard with <code>transcodes stop</code>.</p>
|
|
1964
2326
|
</div>
|
|
1965
2327
|
|
|
1966
2328
|
<div class="panel" id="panel-cli">
|
|
@@ -1969,21 +2331,40 @@ function dashboardHtml() {
|
|
|
1969
2331
|
<div class="cmd-list">
|
|
1970
2332
|
${renderCliCommandsHtml()}
|
|
1971
2333
|
</div>
|
|
1972
|
-
<p class="hint">
|
|
2334
|
+
<p class="hint">Please read the Guideline tab first if you are new to Transcodes CLI.</p>
|
|
1973
2335
|
</div>
|
|
1974
2336
|
|
|
1975
2337
|
<div class="panel" id="panel-rbac">
|
|
1976
|
-
<p id="rbac-token-warning" class="rbac-token-warning" hidden>Save a token
|
|
2338
|
+
<p id="rbac-token-warning" class="rbac-token-warning" hidden>Save a Transcodes token(MAT) first</p>
|
|
1977
2339
|
<div class="guide-help">
|
|
1978
|
-
<
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
<
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2340
|
+
<details class="guide-help-accordion">
|
|
2341
|
+
<summary class="guide-help-summary">
|
|
2342
|
+
<span class="guide-help-heading">How it works</span>
|
|
2343
|
+
<svg class="guide-help-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
|
|
2344
|
+
</summary>
|
|
2345
|
+
<div class="guide-help-body">
|
|
2346
|
+
<p class="guide-help-line">Try an action in your CLI or desktop app. Transcodes classifies each prompt into a <code class="cli-cmd">resource:action</code> pair, then applies the permission matrix \u2014 <strong>0 deny</strong> \xB7 <strong>1 allow (pass)</strong> \xB7 <strong>2 step-up MFA</strong>.</p>
|
|
2347
|
+
<p class="guide-help-line">Resources are matched by <strong>name</strong> and <strong>description</strong>. If nothing matches, it falls back to <code class="cli-cmd">system</code>. Write a concrete description (what the resource covers \u2014 e.g. tools, domains, verbs) so the classifier can match accurately. When integrating with your own back office, you can pass <code>resource</code> and <code>action</code> as params to control permissions directly.</p>
|
|
2348
|
+
<p class="guide-help-heading">Action Classification</p>
|
|
2349
|
+
<ul class="guide-help-list">
|
|
2350
|
+
<li><strong>read</strong> \u2014 inspect data without changing it (list, get, query, cat, grep, status, describe)</li>
|
|
2351
|
+
<li><strong>update</strong> \u2014 modify existing data (edit, patch, set, rename, move, config change)</li>
|
|
2352
|
+
<li><strong>delete</strong> \u2014 remove or destroy data (rm, drop, truncate, purge). Also used for destructive / irreversible mutations (force-push, reset --hard, DROP/TRUNCATE, prod overwrite)</li>
|
|
2353
|
+
<li><strong>create</strong> \u2014 any other state-changing action (new records, sends, posts, uploads, installs)</li>
|
|
2354
|
+
</ul>
|
|
2355
|
+
<p class="guide-help-heading">Examples</p>
|
|
2356
|
+
<ul class="guide-classify-list">
|
|
2357
|
+
<li><span class="guide-classify-prompt">"Create a Google Calendar event"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">google:create</code> <span class="guide-classify-arrow">if <code>google</code> is registered</span> \xB7 otherwise <code class="cli-cmd">system:create</code></li>
|
|
2358
|
+
<li><span class="guide-classify-prompt">"Post a message to #eng on Slack"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">slack:create</code> <span class="guide-classify-arrow">if <code>slack</code> is registered</span> \xB7 otherwise <code class="cli-cmd">system:create</code></li>
|
|
2359
|
+
<li><span class="guide-classify-prompt">"Change James's role in Transcodes"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">transcodes:update</code></li>
|
|
2360
|
+
<li><span class="guide-classify-prompt">"Retire a Transcodes member"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">transcodes:delete</code></li>
|
|
2361
|
+
<li><span class="guide-classify-prompt">"Push this branch to GitHub"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">github:update</code> <span class="guide-classify-arrow">if <code>github</code> is registered</span> \xB7 otherwise <code class="cli-cmd">system:update</code></li>
|
|
2362
|
+
<li><span class="guide-classify-prompt">"Delete files on my computer"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">system:delete</code></li>
|
|
2363
|
+
<li><span class="guide-classify-prompt">"Bash/Shell cat .env" / "read ~/.ssh/id_rsa"</span> <span class="guide-classify-arrow">\u2192</span> <code class="cli-cmd">system:update</code></li>
|
|
2364
|
+
</ul>
|
|
2365
|
+
<p class="guide-classify-note">Registered resources only. Name/description must match \u2014 if the project has no <code>google</code> / <code>slack</code> / <code>github</code> resource, that prompt is classified under <code class="cli-cmd">system</code> instead.</p>
|
|
2366
|
+
</div>
|
|
2367
|
+
</details>
|
|
1987
2368
|
</div>
|
|
1988
2369
|
<div class="rbac-legend">
|
|
1989
2370
|
<p class="rbac-legend-title">Permission matrix</p>
|
|
@@ -1998,7 +2379,7 @@ function dashboardHtml() {
|
|
|
1998
2379
|
<button type="button" class="tab" data-rbac="roles" role="tab">Roles</button>
|
|
1999
2380
|
</div>
|
|
2000
2381
|
<div class="rbac-pane active" id="rbac-pane-resources">
|
|
2001
|
-
<p class="section-sub">Configure resources, roles, and permissions. <a href="
|
|
2382
|
+
<p class="section-sub">Configure resources, roles, and permissions. <a href="${APP_ORG_URL}" data-app-tab="rbac" target="_blank" rel="noopener noreferrer">Edit RBAC Permissions</a></p>
|
|
2002
2383
|
<div class="rbac-table-wrap">
|
|
2003
2384
|
<table class="rbac-table" id="resources-table">
|
|
2004
2385
|
<thead><tr><th>Resource</th><th>Description</th></tr></thead>
|
|
@@ -2007,7 +2388,7 @@ function dashboardHtml() {
|
|
|
2007
2388
|
</div>
|
|
2008
2389
|
</div>
|
|
2009
2390
|
<div class="rbac-pane" id="rbac-pane-roles">
|
|
2010
|
-
<p class="section-sub">Select a role to view its permission matrix \xB7 <a href="
|
|
2391
|
+
<p class="section-sub">Select a role to view its permission matrix \xB7 <a href="${APP_ORG_URL}" data-app-tab="rbac" target="_blank" rel="noopener noreferrer">Edit RBAC Permissions</a></p>
|
|
2011
2392
|
<div class="role-picker" id="role-picker"></div>
|
|
2012
2393
|
<div id="matrix-wrap" hidden>
|
|
2013
2394
|
<p class="list-label" id="matrix-role-label"></p>
|
|
@@ -2031,7 +2412,14 @@ function dashboardHtml() {
|
|
|
2031
2412
|
</div>
|
|
2032
2413
|
</div>
|
|
2033
2414
|
|
|
2034
|
-
<p class="dashboard-footer">
|
|
2415
|
+
<p class="dashboard-footer">
|
|
2416
|
+
Ver ${CLI_VERSION} <code class="cli-cmd">transcodes version</code>
|
|
2417
|
+
<span aria-hidden="true">\xB7</span>
|
|
2418
|
+
<a href="${TRANSCODES_GUARD_REPO_URL}" target="_blank" rel="noopener noreferrer">
|
|
2419
|
+
<svg class="dashboard-footer-github-icon" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
|
2420
|
+
GitHub
|
|
2421
|
+
</a>
|
|
2422
|
+
</p>
|
|
2035
2423
|
<script>
|
|
2036
2424
|
const tokenEl = document.getElementById("token");
|
|
2037
2425
|
const labelEl = document.getElementById("label");
|
|
@@ -2043,7 +2431,8 @@ function dashboardHtml() {
|
|
|
2043
2431
|
const headerProfileEl = document.getElementById("header-profile");
|
|
2044
2432
|
const headerProfileNameEl = document.getElementById("header-profile-name");
|
|
2045
2433
|
const headerProfileMetaEl = document.getElementById("header-profile-meta");
|
|
2046
|
-
const
|
|
2434
|
+
const APP_ORG_URL = ${JSON.stringify(APP_ORG_URL)};
|
|
2435
|
+
const APP_HOME_URL = ${JSON.stringify(APP_HOME_URL)};
|
|
2047
2436
|
|
|
2048
2437
|
let lastStatus = { tokens: [], activeMember: null };
|
|
2049
2438
|
let editingId = null;
|
|
@@ -2052,14 +2441,92 @@ function dashboardHtml() {
|
|
|
2052
2441
|
return Array.isArray(s.tokens) && s.tokens.length > 0;
|
|
2053
2442
|
}
|
|
2054
2443
|
|
|
2444
|
+
function appTabToSegment(tab) {
|
|
2445
|
+
if (tab === "rbac") return "permissions";
|
|
2446
|
+
if (tab === "settings" || tab === "logs" || tab === "members") return tab;
|
|
2447
|
+
return "members";
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
function appWorkspaceUrl(organizationId, projectId, tab) {
|
|
2451
|
+
return (
|
|
2452
|
+
APP_ORG_URL +
|
|
2453
|
+
"/" +
|
|
2454
|
+
encodeURIComponent(organizationId) +
|
|
2455
|
+
"/project/" +
|
|
2456
|
+
encodeURIComponent(projectId) +
|
|
2457
|
+
"/" +
|
|
2458
|
+
encodeURIComponent(appTabToSegment(tab))
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function updateAppDeepLinks(s) {
|
|
2463
|
+
const oid = s && s.activeMember && s.activeMember.organizationId;
|
|
2464
|
+
const pid = s && s.activeMember && s.activeMember.projectId;
|
|
2465
|
+
document.querySelectorAll("[data-app-tab]").forEach((a) => {
|
|
2466
|
+
const tab = a.getAttribute("data-app-tab");
|
|
2467
|
+
if (oid && pid && tab) {
|
|
2468
|
+
a.href = appWorkspaceUrl(oid, pid, tab);
|
|
2469
|
+
} else if (oid) {
|
|
2470
|
+
a.href = APP_ORG_URL + "/" + encodeURIComponent(oid);
|
|
2471
|
+
} else {
|
|
2472
|
+
a.href = APP_ORG_URL;
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2055
2477
|
function updateTokenEmptyState() {
|
|
2056
2478
|
const empty = !hasSavedTokens(lastStatus);
|
|
2057
2479
|
headerTokenEmptyEl.hidden = !empty;
|
|
2058
2480
|
if (empty) {
|
|
2059
2481
|
headerProfileEl.hidden = true;
|
|
2482
|
+
updateAppDeepLinks(lastStatus);
|
|
2060
2483
|
return;
|
|
2061
2484
|
}
|
|
2062
2485
|
renderHeaderProfile(lastStatus);
|
|
2486
|
+
updateAppDeepLinks(lastStatus);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
async function openConsole() {
|
|
2490
|
+
const consoleBtns = document.querySelectorAll("[data-console-open]");
|
|
2491
|
+
consoleBtns.forEach((btn) => { btn.disabled = true; });
|
|
2492
|
+
try {
|
|
2493
|
+
const res = await fetch("/api/console/open", { method: "POST" });
|
|
2494
|
+
const data = await res.json();
|
|
2495
|
+
if (!res.ok) throw new Error(data.error || "Could not open auth settings");
|
|
2496
|
+
if (data.browserUrl) window.open(data.browserUrl, "_blank", "noopener,noreferrer");
|
|
2497
|
+
showToast("Opening auth settings in your browser", "success");
|
|
2498
|
+
} catch (e) {
|
|
2499
|
+
showToast(e.message || "Failed to open auth settings", "error");
|
|
2500
|
+
} finally {
|
|
2501
|
+
consoleBtns.forEach((btn) => { btn.disabled = false; });
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function openManageTokens() {
|
|
2506
|
+
const member = lastStatus && lastStatus.activeMember;
|
|
2507
|
+
const oid = member && member.organizationId;
|
|
2508
|
+
const pid = member && member.projectId;
|
|
2509
|
+
const url =
|
|
2510
|
+
oid && pid
|
|
2511
|
+
? appWorkspaceUrl(oid, pid, "members")
|
|
2512
|
+
: oid
|
|
2513
|
+
? APP_ORG_URL + "/" + encodeURIComponent(oid)
|
|
2514
|
+
: APP_HOME_URL;
|
|
2515
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
const guideVideoToggle = document.getElementById("guide-video-toggle");
|
|
2519
|
+
const guideVideo = document.getElementById("guide-video");
|
|
2520
|
+
if (guideVideoToggle && guideVideo) {
|
|
2521
|
+
guideVideoToggle.addEventListener("click", () => {
|
|
2522
|
+
const open = guideVideoToggle.getAttribute("aria-expanded") === "true";
|
|
2523
|
+
const next = !open;
|
|
2524
|
+
guideVideoToggle.setAttribute("aria-expanded", String(next));
|
|
2525
|
+
guideVideo.hidden = !next;
|
|
2526
|
+
guideVideoToggle.textContent = next
|
|
2527
|
+
? "Hide Intro Video"
|
|
2528
|
+
: "Watch Intro Video";
|
|
2529
|
+
});
|
|
2063
2530
|
}
|
|
2064
2531
|
|
|
2065
2532
|
document.querySelectorAll(".tab[data-tab]").forEach((tab) => {
|
|
@@ -2170,21 +2637,15 @@ function dashboardHtml() {
|
|
|
2170
2637
|
renderTokens(lastStatus);
|
|
2171
2638
|
}
|
|
2172
2639
|
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
try {
|
|
2176
|
-
const res = await fetch("/api/console/open", { method: "POST" });
|
|
2177
|
-
const data = await res.json();
|
|
2178
|
-
if (!res.ok) throw new Error(data.error || "Could not open auth settings");
|
|
2179
|
-
if (data.browserUrl) window.open(data.browserUrl, "_blank", "noopener,noreferrer");
|
|
2180
|
-
showToast("Opening auth settings in your browser", "success");
|
|
2181
|
-
} catch (e) {
|
|
2182
|
-
showToast(e.message || "Failed to open auth settings", "error");
|
|
2183
|
-
} finally {
|
|
2184
|
-
manageAuthBtn.disabled = false;
|
|
2185
|
-
}
|
|
2640
|
+
document.querySelectorAll("[data-console-open]").forEach((btn) => {
|
|
2641
|
+
btn.addEventListener("click", () => { openConsole(); });
|
|
2186
2642
|
});
|
|
2187
2643
|
|
|
2644
|
+
const manageTokensBtn = document.getElementById("manage-tokens-btn");
|
|
2645
|
+
if (manageTokensBtn) {
|
|
2646
|
+
manageTokensBtn.addEventListener("click", () => { openManageTokens(); });
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2188
2649
|
async function saveLabel(id) {
|
|
2189
2650
|
const input = listEl.querySelector('[data-edit-input="' + id + '"]');
|
|
2190
2651
|
const label = input ? input.value.trim() : "";
|
|
@@ -2449,7 +2910,7 @@ function dashboardHtml() {
|
|
|
2449
2910
|
</body>
|
|
2450
2911
|
</html>`;
|
|
2451
2912
|
}
|
|
2452
|
-
function
|
|
2913
|
+
function openDashboardBrowser(url) {
|
|
2453
2914
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2454
2915
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2455
2916
|
try {
|
|
@@ -2472,6 +2933,16 @@ function listen(port) {
|
|
|
2472
2933
|
return;
|
|
2473
2934
|
}
|
|
2474
2935
|
try {
|
|
2936
|
+
if (method === "GET" && url === "/health") {
|
|
2937
|
+
res.writeHead(200, {
|
|
2938
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
2939
|
+
"Cache-Control": "no-store",
|
|
2940
|
+
Connection: "close",
|
|
2941
|
+
"X-Transcodes-Dashboard": DASHBOARD_HEALTH_MARKER
|
|
2942
|
+
});
|
|
2943
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2475
2946
|
if (method === "GET" && (url === "/" || url === "/index.html")) {
|
|
2476
2947
|
res.writeHead(200, {
|
|
2477
2948
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -2594,56 +3065,68 @@ function listen(port) {
|
|
|
2594
3065
|
server.listen(port, HOST, () => resolve(server));
|
|
2595
3066
|
});
|
|
2596
3067
|
}
|
|
2597
|
-
function
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
let
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
if (shuttingDown) {
|
|
2617
|
-
forceExit();
|
|
2618
|
-
return;
|
|
3068
|
+
async function pidsListeningOnPorts(from, to) {
|
|
3069
|
+
const pids = /* @__PURE__ */ new Set();
|
|
3070
|
+
if (process.platform === "win32") {
|
|
3071
|
+
for (let port = from; port <= to; port++) {
|
|
3072
|
+
try {
|
|
3073
|
+
const { stdout } = await execFileAsync(
|
|
3074
|
+
"powershell",
|
|
3075
|
+
[
|
|
3076
|
+
"-NoProfile",
|
|
3077
|
+
"-Command",
|
|
3078
|
+
`(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue).OwningProcess`
|
|
3079
|
+
],
|
|
3080
|
+
{ timeout: 5e3 }
|
|
3081
|
+
);
|
|
3082
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
3083
|
+
const pid = Number(line.trim());
|
|
3084
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
3085
|
+
}
|
|
3086
|
+
} catch {
|
|
2619
3087
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
3088
|
+
}
|
|
3089
|
+
return [...pids];
|
|
3090
|
+
}
|
|
3091
|
+
for (let port = from; port <= to; port++) {
|
|
3092
|
+
try {
|
|
3093
|
+
const { stdout } = await execFileAsync(
|
|
3094
|
+
"lsof",
|
|
3095
|
+
["-ti", `tcp:${port}`, "-sTCP:LISTEN"],
|
|
3096
|
+
{ timeout: 5e3 }
|
|
3097
|
+
);
|
|
3098
|
+
for (const token of stdout.trim().split(/\s+/)) {
|
|
3099
|
+
const pid = Number(token);
|
|
3100
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
3101
|
+
}
|
|
3102
|
+
} catch {
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
return [...pids];
|
|
2638
3106
|
}
|
|
2639
|
-
async function
|
|
2640
|
-
|
|
2641
|
-
|
|
3107
|
+
async function freePortRange(from, to) {
|
|
3108
|
+
process.stdout.write(`Freeing ports ${from}\u2013${to} \u2026
|
|
3109
|
+
`);
|
|
3110
|
+
const pids = await pidsListeningOnPorts(from, to);
|
|
3111
|
+
if (pids.length === 0) {
|
|
3112
|
+
process.stdout.write(" (no listeners found)\n");
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
for (const pid of pids) {
|
|
3116
|
+
try {
|
|
3117
|
+
process.kill(pid, "SIGKILL");
|
|
3118
|
+
process.stdout.write(` killed pid ${pid}
|
|
3119
|
+
`);
|
|
3120
|
+
} catch {
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
async function tryBindPortRange(preferred) {
|
|
2642
3125
|
let port = preferred;
|
|
2643
|
-
for (let attempt = 0; attempt <
|
|
3126
|
+
for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt++) {
|
|
2644
3127
|
try {
|
|
2645
|
-
server = await listen(port);
|
|
2646
|
-
|
|
3128
|
+
const server = await listen(port);
|
|
3129
|
+
return { server, port };
|
|
2647
3130
|
} catch (err) {
|
|
2648
3131
|
const code = err.code;
|
|
2649
3132
|
if (code === "EADDRINUSE") {
|
|
@@ -2653,29 +3136,1544 @@ async function runDashboard(options) {
|
|
|
2653
3136
|
throw err;
|
|
2654
3137
|
}
|
|
2655
3138
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
async function serveDashboardHttp(options) {
|
|
3142
|
+
const preferred = options.port ?? DEFAULT_PORT;
|
|
3143
|
+
const last = preferred + PORT_ATTEMPTS - 1;
|
|
3144
|
+
let bound = await tryBindPortRange(preferred);
|
|
3145
|
+
if (!bound) {
|
|
3146
|
+
await freePortRange(preferred, last);
|
|
3147
|
+
bound = await tryBindPortRange(preferred);
|
|
3148
|
+
}
|
|
3149
|
+
if (!bound) {
|
|
2658
3150
|
throw new Error(
|
|
2659
3151
|
`could not find a free port in ${preferred}-${last} (all in use).
|
|
2660
|
-
|
|
2661
|
-
Tip: Ctrl+Z only suspends the process (port stays taken) \u2014 run \`kill %1\` or \`fg\` then Ctrl+C.
|
|
2662
|
-
Free the ports and retry:
|
|
3152
|
+
Stop a previous dashboard with: transcodes stop
|
|
2663
3153
|
macOS/Linux: lsof -ti tcp:${preferred}-${last} | xargs kill -9
|
|
2664
|
-
any platform: npx kill-port ${preferred} ${preferred + 1} # repeat per port
|
|
2665
3154
|
Or choose another port: transcodes --port <N>`
|
|
2666
3155
|
);
|
|
2667
3156
|
}
|
|
2668
|
-
|
|
3157
|
+
return bound;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
// src/i18n.ts
|
|
3161
|
+
import fs from "fs";
|
|
3162
|
+
import os3 from "os";
|
|
3163
|
+
import path3 from "path";
|
|
3164
|
+
var LOCALE_FILE = path3.join(os3.homedir(), ".transcodes", "locale");
|
|
3165
|
+
var current = "en";
|
|
3166
|
+
var MESSAGES = {
|
|
3167
|
+
en: {
|
|
3168
|
+
installBanner: "transcodes install \u2014 set up plugins and dashboard.",
|
|
3169
|
+
langTitle: "Select language / \uC5B8\uC5B4\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
|
|
3170
|
+
chooseKeys: "\u2191/\u2193 move \xB7 enter select",
|
|
3171
|
+
platformTitle: "Please select platforms that you want to install:",
|
|
3172
|
+
platformHint1: "Installing a plugin enables both its CLI and desktop app.",
|
|
3173
|
+
platformInstalledApps: "Currently installed applications: {list}",
|
|
3174
|
+
platformInstalledNone: "No applications are installed yet.",
|
|
3175
|
+
nextStep: "Next Step \u2192",
|
|
3176
|
+
platformKeys: "\u2191/\u2193 move \xB7 space select \xB7 a all/none \xB7 enter confirm \xB7 q quit/exit",
|
|
3177
|
+
platformNumberedHint: 'Enter numbers to install (e.g. 1,2), "all" or Enter to install all.',
|
|
3178
|
+
platformNumberedNext: 'Type {n} (or "next") for Next Step, "exit"/"q" to quit.',
|
|
3179
|
+
installed: "[Installed \u2713]",
|
|
3180
|
+
nothingSelected: "Nothing selected.",
|
|
3181
|
+
noPlatforms: "No platforms selected \u2014 nothing to install.",
|
|
3182
|
+
installing: "Installing: {list}",
|
|
3183
|
+
cancelHint: "To open the dashboard later, type `transcodes`",
|
|
3184
|
+
nonInteractive: "Non-interactive shell detected. Specify platforms explicitly, e.g.:",
|
|
3185
|
+
loginPleaseSignIn: "Please sign in to Transcodes in your browser first.",
|
|
3186
|
+
loginWaiting: "Waiting for authorization to finish\u2026",
|
|
3187
|
+
loginBrowserOpenFailed: "Could not open the browser automatically. Run `transcodes login` again and complete sign-in when the browser opens.",
|
|
3188
|
+
loginNoOpenHint: "Browser open was skipped (--no-open). Run `transcodes login` without --no-open to continue.",
|
|
3189
|
+
tokenWhy: "The MAT is the ID card / key attached to every request to Transcodes. It verifies your identity so you can use Transcodes.",
|
|
3190
|
+
tokenQuestion: "Do you have a Member Access Token (MAT) to access Transcodes?",
|
|
3191
|
+
tokenYes: "Yes",
|
|
3192
|
+
tokenNo: "No",
|
|
3193
|
+
tokenSkip: "Skip \u2014 token already configured",
|
|
3194
|
+
tokenChoose123: "Please choose 1, 2, or 3.",
|
|
3195
|
+
tokenSkipLog: "Skipping token setup \u2014 using the token already configured.",
|
|
3196
|
+
tokenNoIntro1: "No token yet \u2014 open the Transcodes console to create a project and issue a MAT.",
|
|
3197
|
+
tokenNoIntro2: "When you return, paste the token here (or run `transcodes install` again).",
|
|
3198
|
+
tokenPressEnterConsole: "Press ENTER to open the console ({url}) \u2026",
|
|
3199
|
+
tokenOpening: "Opening {url} \u2026",
|
|
3200
|
+
tokenSteps1: "1) After sign-in, create a new project on the dashboard.",
|
|
3201
|
+
tokenSteps2: '2) Open the "Members & Tokens" tab and add a new member.',
|
|
3202
|
+
tokenSteps3: "3) Generate a new token for that member, or ask your manager to get a token.",
|
|
3203
|
+
tokenSteps4: "4) Paste it here.",
|
|
3204
|
+
registerTitle: "\u2500\u2500 Register MAT token \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
3205
|
+
pasteToken: "Paste your token: ",
|
|
3206
|
+
tokenRequired: "Token is required.",
|
|
3207
|
+
tokenRejected: "Token rejected: {err}",
|
|
3208
|
+
labelPrompt: "Label (e.g. transcodes-myproject-prod): ",
|
|
3209
|
+
labelRequired: "Label is required.",
|
|
3210
|
+
tokenSaved: "Saved token to {path} (label={label}).",
|
|
3211
|
+
tokenTooMany: "Too many invalid attempts \u2014 run `transcodes install` again later.",
|
|
3212
|
+
congratsBar: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
3213
|
+
congrats1: "Authenticated successfully.",
|
|
3214
|
+
congrats2: "You are ready to use Transcodes.",
|
|
3215
|
+
congratsBody: "Plugins are installed and authenticated successfully",
|
|
3216
|
+
pressEnterDashboard: "Press ENTER to open the transcodes CLI dashboard \u2026 ",
|
|
3217
|
+
installSummary: "\u2500\u2500 Install summary \u2500\u2500",
|
|
3218
|
+
dashboardOpened: "Transcodes dashboard is open: {url}",
|
|
3219
|
+
dashboardHowToUse: "Please read how to use Transcodes in the CLI dashboard.",
|
|
3220
|
+
dashboardStopHint: "Running in the background \u2014 stop with: transcodes stop",
|
|
3221
|
+
dashboardStopped: "Transcodes dashboard stopped.",
|
|
3222
|
+
dashboardNotRunning: "Transcodes dashboard is not running.",
|
|
3223
|
+
loginTokenSaved: "Login complete. Your Transcodes access is ready."
|
|
3224
|
+
},
|
|
3225
|
+
ko: {
|
|
3226
|
+
installBanner: "transcodes install \u2014 \uD50C\uB7EC\uADF8\uC778\uACFC \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.",
|
|
3227
|
+
langTitle: "Select language / \uC5B8\uC5B4\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
|
|
3228
|
+
chooseKeys: "\u2191/\u2193 \uC774\uB3D9 \xB7 enter \uC120\uD0DD",
|
|
3229
|
+
platformTitle: "\uC124\uCE58\uD560 \uD50C\uB7AB\uD3FC\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
|
|
3230
|
+
platformHint1: "\uD50C\uB7EC\uADF8\uC778 \uC124\uCE58 \uC2DC CLI\xB7\uB370\uC2A4\uD06C\uD1B1 \uC571\uC5D0 \uBAA8\uB450 \uC801\uC6A9\uB429\uB2C8\uB2E4.",
|
|
3231
|
+
platformInstalledApps: "\uD604\uC7AC \uC124\uCE58\uB418\uC5B4 \uC788\uB294 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158: {list}",
|
|
3232
|
+
platformInstalledNone: "\uC544\uC9C1 \uC124\uCE58\uB41C Application\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3233
|
+
nextStep: "\uB2E4\uC74C \uB2E8\uACC4 \u2192",
|
|
3234
|
+
platformKeys: "\u2191/\u2193 \uC774\uB3D9 \xB7 space \uC120\uD0DD \xB7 a \uC804\uCCB4 \xB7 enter \uD655\uC778 \xB7 q \uC885\uB8CC",
|
|
3235
|
+
platformNumberedHint: '\uC124\uCE58\uD560 \uBC88\uD638\uB97C \uC785\uB825\uD558\uC138\uC694 (\uC608: 1,2). "all" \uB610\uB294 Enter\uBA74 \uC804\uCCB4 \uC124\uCE58.',
|
|
3236
|
+
platformNumberedNext: '{n} (\uB610\uB294 "next")\uB294 \uB2E4\uC74C \uB2E8\uACC4, "exit"/"q"\uB294 \uC885\uB8CC\uC785\uB2C8\uB2E4.',
|
|
3237
|
+
installed: "[\uC124\uCE58\uB428 \u2713]",
|
|
3238
|
+
nothingSelected: "\uC120\uD0DD\uB41C \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3239
|
+
noPlatforms: "\uC120\uD0DD\uB41C \uD50C\uB7AB\uD3FC\uC774 \uC5C6\uC5B4 \uC124\uCE58\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4.",
|
|
3240
|
+
installing: "\uC124\uCE58 \uC911: {list}",
|
|
3241
|
+
cancelHint: "\uB098\uC911\uC5D0 \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC5F4\uB824\uBA74 `transcodes`\uB97C \uC785\uB825\uD558\uC138\uC694",
|
|
3242
|
+
nonInteractive: "\uBE44\uB300\uD654\uD615 \uC178\uC785\uB2C8\uB2E4. \uD50C\uB7AB\uD3FC\uC744 \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694. \uC608:",
|
|
3243
|
+
loginPleaseSignIn: "\uBA3C\uC800 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C Transcodes\uC5D0 \uB85C\uADF8\uC778\uD574 \uC8FC\uC138\uC694.",
|
|
3244
|
+
loginWaiting: "\uC2B9\uC778 \uC644\uB8CC\uB97C \uAE30\uB2E4\uB9AC\uB294 \uC911\u2026",
|
|
3245
|
+
loginBrowserOpenFailed: "\uBE0C\uB77C\uC6B0\uC800\uB97C \uC790\uB3D9\uC73C\uB85C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. `transcodes login`\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD55C \uB4A4, \uC5F4\uB9B0 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778\uC744 \uC644\uB8CC\uD574 \uC8FC\uC138\uC694.",
|
|
3246
|
+
loginNoOpenHint: "\uBE0C\uB77C\uC6B0\uC800 \uC790\uB3D9 \uC5F4\uAE30\uAC00 \uAC74\uB108\uB6F0\uC5B4\uC84C\uC2B5\uB2C8\uB2E4 (--no-open). `--no-open` \uC5C6\uC774 `transcodes login`\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.",
|
|
3247
|
+
tokenWhy: "MAT\uB294 Transcodes\uC5D0 \uC694\uCCAD\uD560 \uB54C \uAC19\uC774 \uCCA8\uBD80\uB418\uB294 ID Card / Key\uC785\uB2C8\uB2E4. Transcodes\uB97C \uC774\uC6A9\uD558\uAE30 \uC704\uD574 \uC2E0\uC6D0\uC744 \uD655\uC778\uD558\uB294 \uB370 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
3248
|
+
tokenQuestion: "Transcodes \uC5D0 \uC811\uC18D\uD558\uAE30 \uC704\uD55C Member Access Token(MAT)\uC774 \uC788\uB098\uC694?",
|
|
3249
|
+
tokenYes: "\uC608",
|
|
3250
|
+
tokenNo: "\uC544\uB2C8\uC624",
|
|
3251
|
+
tokenSkip: "\uAC74\uB108\uB6F0\uAE30 \u2014 \uD1A0\uD070\uC774 \uC774\uBBF8 \uC124\uC815\uB418\uC5B4 \uC788\uC74C",
|
|
3252
|
+
tokenChoose123: "1, 2, \uB610\uB294 3\uC744 \uC120\uD0DD\uD558\uC138\uC694.",
|
|
3253
|
+
tokenSkipLog: "\uD1A0\uD070 \uC124\uC815\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4 \u2014 \uC774\uBBF8 \uC124\uC815\uB41C \uD1A0\uD070\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
|
|
3254
|
+
tokenNoIntro1: "\uD1A0\uD070\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 Transcodes \uCF58\uC194\uC5D0\uC11C \uD504\uB85C\uC81D\uD2B8\uB97C \uB9CC\uB4E4\uACE0 MAT\uB97C \uBC1C\uAE09\uD558\uC138\uC694.",
|
|
3255
|
+
tokenNoIntro2: "\uB3CC\uC544\uC628 \uB4A4 \uC5EC\uAE30\uC5D0 \uD1A0\uD070\uC744 \uBD99\uC5EC\uB123\uAC70\uB098 `transcodes install`\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
3256
|
+
tokenPressEnterConsole: "ENTER\uB97C \uB204\uB974\uBA74 \uCF58\uC194\uC744 \uC5FD\uB2C8\uB2E4 ({url}) \u2026",
|
|
3257
|
+
tokenOpening: "{url} \uC5EC\uB294 \uC911 \u2026",
|
|
3258
|
+
tokenSteps1: "1) \uB85C\uADF8\uC778 \uD6C4 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uC0C8 \uD504\uB85C\uC81D\uD2B8\uB97C \uB9CC\uB4DC\uC138\uC694.",
|
|
3259
|
+
tokenSteps2: '2) "Members & Tokens" \uD0ED\uC5D0\uC11C \uBA64\uBC84\uB97C \uCD94\uAC00\uD558\uC138\uC694.',
|
|
3260
|
+
tokenSteps3: "3) \uD574\uB2F9 \uBA64\uBC84\uC758 \uD1A0\uD070\uC744 \uBC1C\uAE09\uD558\uAC70\uB098, \uAD00\uB9AC\uC790\uC5D0\uAC8C \uD1A0\uD070\uC744 \uC694\uCCAD\uD558\uC138\uC694.",
|
|
3261
|
+
tokenSteps4: "4) \uC5EC\uAE30\uC5D0 \uBD99\uC5EC\uB123\uC73C\uC138\uC694.",
|
|
3262
|
+
registerTitle: "\u2500\u2500 MAT \uD1A0\uD070 \uB4F1\uB85D \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
3263
|
+
pasteToken: "\uD1A0\uD070\uC744 \uBD99\uC5EC\uB123\uC73C\uC138\uC694: ",
|
|
3264
|
+
tokenRequired: "\uD1A0\uD070\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
3265
|
+
tokenRejected: "\uD1A0\uD070\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4: {err}",
|
|
3266
|
+
labelPrompt: "\uB77C\uBCA8 (\uC608: transcodes-myproject-prod): ",
|
|
3267
|
+
labelRequired: "\uB77C\uBCA8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
|
|
3268
|
+
tokenSaved: "\uD1A0\uD070\uC744 {path}\uC5D0 \uC800\uC7A5\uD588\uC2B5\uB2C8\uB2E4 (label={label}).",
|
|
3269
|
+
tokenTooMany: "\uC798\uBABB\uB41C \uC785\uB825\uC774 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4 \u2014 \uB098\uC911\uC5D0 `transcodes install`\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
|
|
3270
|
+
congratsBar: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
3271
|
+
congrats1: "Authenticated successfully.",
|
|
3272
|
+
congrats2: "\uC774\uC81C Transcodes\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
3273
|
+
congratsBody: "\uC124\uC815\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
3274
|
+
pressEnterDashboard: "ENTER\uB97C \uB204\uB974\uBA74 transcodes CLI \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC5FD\uB2C8\uB2E4 \u2026 ",
|
|
3275
|
+
installSummary: "\u2500\u2500 \uC124\uCE58 \uC694\uC57D \u2500\u2500",
|
|
3276
|
+
dashboardOpened: "Transcodes \uB300\uC2DC\uBCF4\uB4DC\uAC00 \uC5F4\uB838\uC2B5\uB2C8\uB2E4: {url}",
|
|
3277
|
+
dashboardHowToUse: "CLI \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uC0AC\uC6A9 \uBC29\uBC95\uC744 \uC77D\uC5B4\uBCF4\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4.",
|
|
3278
|
+
dashboardStopHint: "\uBC31\uADF8\uB77C\uC6B4\uB4DC\uC5D0\uC11C \uC2E4\uD589 \uC911 \u2014 \uC885\uB8CC: transcodes stop",
|
|
3279
|
+
dashboardStopped: "Transcodes \uB300\uC2DC\uBCF4\uB4DC\uB97C \uC885\uB8CC\uD588\uC2B5\uB2C8\uB2E4.",
|
|
3280
|
+
dashboardNotRunning: "\uC2E4\uD589 \uC911\uC778 Transcodes \uB300\uC2DC\uBCF4\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3281
|
+
loginTokenSaved: "\uB85C\uADF8\uC778\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774\uC81C Transcodes\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
|
|
3282
|
+
}
|
|
3283
|
+
};
|
|
3284
|
+
function setLocale(locale) {
|
|
3285
|
+
current = locale;
|
|
3286
|
+
try {
|
|
3287
|
+
const dir = path3.dirname(LOCALE_FILE);
|
|
3288
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3289
|
+
fs.writeFileSync(LOCALE_FILE, `${locale}
|
|
3290
|
+
`, { mode: 384 });
|
|
3291
|
+
} catch {
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
function readSavedLocale() {
|
|
3295
|
+
try {
|
|
3296
|
+
const raw = fs.readFileSync(LOCALE_FILE, "utf8").trim().toLowerCase();
|
|
3297
|
+
if (raw === "en" || raw === "ko") return raw;
|
|
3298
|
+
} catch {
|
|
3299
|
+
}
|
|
3300
|
+
return null;
|
|
3301
|
+
}
|
|
3302
|
+
function t(key, vars) {
|
|
3303
|
+
let s = MESSAGES[current][key] ?? MESSAGES.en[key];
|
|
3304
|
+
if (vars) {
|
|
3305
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
3306
|
+
s = s.replaceAll(`{${k}}`, String(v));
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
return s;
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// src/dashboard-lifecycle.ts
|
|
3313
|
+
var DASHBOARD_MARKER = "transcodes-dashboard";
|
|
3314
|
+
var HEALTH_PATH = "/health";
|
|
3315
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3316
|
+
function pidFilePath() {
|
|
3317
|
+
return path4.join(dataDir(), "dashboard.pid");
|
|
3318
|
+
}
|
|
3319
|
+
function sleep(ms) {
|
|
3320
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3321
|
+
}
|
|
3322
|
+
function readPidRecord() {
|
|
3323
|
+
try {
|
|
3324
|
+
const raw = readFileSync2(pidFilePath(), "utf8");
|
|
3325
|
+
const parsed = JSON.parse(raw);
|
|
3326
|
+
if (typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 && typeof parsed.port === "number" && Number.isInteger(parsed.port) && parsed.port > 0) {
|
|
3327
|
+
return { pid: parsed.pid, port: parsed.port };
|
|
3328
|
+
}
|
|
3329
|
+
} catch {
|
|
3330
|
+
}
|
|
3331
|
+
return null;
|
|
3332
|
+
}
|
|
3333
|
+
function writePidRecord(record) {
|
|
3334
|
+
mkdirSync3(dataDir(), { recursive: true });
|
|
3335
|
+
writeFileSync2(pidFilePath(), `${JSON.stringify(record)}
|
|
3336
|
+
`, { mode: 384 });
|
|
3337
|
+
}
|
|
3338
|
+
function clearPidRecord() {
|
|
3339
|
+
try {
|
|
3340
|
+
unlinkSync(pidFilePath());
|
|
3341
|
+
} catch {
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
function isProcessAlive(pid) {
|
|
3345
|
+
try {
|
|
3346
|
+
process.kill(pid, 0);
|
|
3347
|
+
return true;
|
|
3348
|
+
} catch {
|
|
3349
|
+
return false;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
function probeHealth(port) {
|
|
3353
|
+
return new Promise((resolve) => {
|
|
3354
|
+
const req = http.get(
|
|
3355
|
+
{
|
|
3356
|
+
host: DASHBOARD_HOST,
|
|
3357
|
+
port,
|
|
3358
|
+
path: HEALTH_PATH,
|
|
3359
|
+
timeout: 500
|
|
3360
|
+
},
|
|
3361
|
+
(res) => {
|
|
3362
|
+
const marker = res.headers["x-transcodes-dashboard"];
|
|
3363
|
+
res.resume();
|
|
3364
|
+
resolve(res.statusCode === 200 && marker === DASHBOARD_MARKER);
|
|
3365
|
+
}
|
|
3366
|
+
);
|
|
3367
|
+
req.on("error", () => resolve(false));
|
|
3368
|
+
req.on("timeout", () => {
|
|
3369
|
+
req.destroy();
|
|
3370
|
+
resolve(false);
|
|
3371
|
+
});
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
async function findRunningDashboard(preferred) {
|
|
3375
|
+
const record = readPidRecord();
|
|
3376
|
+
if (record && isProcessAlive(record.pid)) {
|
|
3377
|
+
if (await probeHealth(record.port)) {
|
|
3378
|
+
return {
|
|
3379
|
+
port: record.port,
|
|
3380
|
+
url: `http://${DASHBOARD_HOST}:${record.port}/`
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
for (let i = 0; i < DASHBOARD_PORT_ATTEMPTS; i++) {
|
|
3385
|
+
const port = preferred + i;
|
|
3386
|
+
if (await probeHealth(port)) {
|
|
3387
|
+
return { port, url: `http://${DASHBOARD_HOST}:${port}/` };
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
return null;
|
|
3391
|
+
}
|
|
3392
|
+
function spawnDaemon(port) {
|
|
3393
|
+
const entry = process.argv[1];
|
|
3394
|
+
if (!entry) {
|
|
3395
|
+
throw new Error("cannot resolve CLI entry path to spawn dashboard daemon");
|
|
3396
|
+
}
|
|
3397
|
+
mkdirSync3(dataDir(), { recursive: true });
|
|
3398
|
+
const logPath = path4.join(dataDir(), "dashboard.log");
|
|
3399
|
+
const logFd = openSync(logPath, "a");
|
|
3400
|
+
try {
|
|
3401
|
+
const child = spawn3(
|
|
3402
|
+
process.execPath,
|
|
3403
|
+
[entry, "dashboard", "--daemon", "--port", String(port)],
|
|
3404
|
+
{
|
|
3405
|
+
detached: true,
|
|
3406
|
+
stdio: ["ignore", logFd, logFd],
|
|
3407
|
+
env: process.env
|
|
3408
|
+
}
|
|
3409
|
+
);
|
|
3410
|
+
child.unref();
|
|
3411
|
+
} finally {
|
|
3412
|
+
try {
|
|
3413
|
+
closeSync(logFd);
|
|
3414
|
+
} catch {
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
async function killPortListener(port) {
|
|
3419
|
+
try {
|
|
3420
|
+
if (process.platform === "win32") {
|
|
3421
|
+
await execFileAsync2(
|
|
3422
|
+
"powershell",
|
|
3423
|
+
[
|
|
3424
|
+
"-NoProfile",
|
|
3425
|
+
"-Command",
|
|
3426
|
+
`$p=(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue).OwningProcess; if($p){ Stop-Process -Id $p -Force }`
|
|
3427
|
+
],
|
|
3428
|
+
{ timeout: 5e3 }
|
|
3429
|
+
);
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
const { stdout } = await execFileAsync2(
|
|
3433
|
+
"lsof",
|
|
3434
|
+
["-ti", `tcp:${port}`, "-sTCP:LISTEN"],
|
|
3435
|
+
{ timeout: 5e3 }
|
|
3436
|
+
);
|
|
3437
|
+
for (const token of stdout.trim().split(/\s+/)) {
|
|
3438
|
+
const pid = Number(token);
|
|
3439
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
3440
|
+
try {
|
|
3441
|
+
process.kill(pid, "SIGKILL");
|
|
3442
|
+
} catch {
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
} catch {
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
async function serveDashboard(options) {
|
|
3450
|
+
const preferred = options.port ?? DEFAULT_DASHBOARD_PORT;
|
|
3451
|
+
const bound = await serveDashboardHttp({ port: preferred });
|
|
3452
|
+
writePidRecord({ pid: process.pid, port: bound.port });
|
|
3453
|
+
const cleanup = () => clearPidRecord();
|
|
3454
|
+
process.once("exit", cleanup);
|
|
3455
|
+
await new Promise((resolve) => {
|
|
3456
|
+
let shuttingDown = false;
|
|
3457
|
+
const onSignal = () => {
|
|
3458
|
+
if (shuttingDown) {
|
|
3459
|
+
cleanup();
|
|
3460
|
+
process.exit(0);
|
|
3461
|
+
}
|
|
3462
|
+
shuttingDown = true;
|
|
3463
|
+
bound.server.closeAllConnections?.();
|
|
3464
|
+
bound.server.close(() => {
|
|
3465
|
+
cleanup();
|
|
3466
|
+
resolve();
|
|
3467
|
+
});
|
|
3468
|
+
setTimeout(() => {
|
|
3469
|
+
cleanup();
|
|
3470
|
+
process.exit(0);
|
|
3471
|
+
}, 1500).unref();
|
|
3472
|
+
};
|
|
3473
|
+
process.on("SIGINT", onSignal);
|
|
3474
|
+
process.on("SIGTERM", onSignal);
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
async function killRunningDashboard(preferred) {
|
|
3478
|
+
const record = readPidRecord();
|
|
3479
|
+
if (record && isProcessAlive(record.pid)) {
|
|
3480
|
+
try {
|
|
3481
|
+
process.kill(record.pid, "SIGTERM");
|
|
3482
|
+
for (let i = 0; i < 20; i++) {
|
|
3483
|
+
await sleep(100);
|
|
3484
|
+
if (!isProcessAlive(record.pid)) break;
|
|
3485
|
+
}
|
|
3486
|
+
if (isProcessAlive(record.pid)) {
|
|
3487
|
+
process.kill(record.pid, "SIGKILL");
|
|
3488
|
+
}
|
|
3489
|
+
} catch {
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
for (let i = 0; i < DASHBOARD_PORT_ATTEMPTS; i++) {
|
|
3493
|
+
const port = preferred + i;
|
|
3494
|
+
if (!await probeHealth(port)) continue;
|
|
3495
|
+
await killPortListener(port);
|
|
3496
|
+
}
|
|
3497
|
+
clearPidRecord();
|
|
3498
|
+
}
|
|
3499
|
+
async function ensureDashboard(options) {
|
|
3500
|
+
const preferred = options.port ?? DEFAULT_DASHBOARD_PORT;
|
|
3501
|
+
const open = options.open !== false;
|
|
3502
|
+
await killRunningDashboard(preferred);
|
|
3503
|
+
spawnDaemon(preferred);
|
|
3504
|
+
let running = null;
|
|
3505
|
+
for (let i = 0; i < 50; i++) {
|
|
3506
|
+
await sleep(100);
|
|
3507
|
+
running = await findRunningDashboard(preferred);
|
|
3508
|
+
if (running) break;
|
|
3509
|
+
}
|
|
3510
|
+
if (!running) {
|
|
3511
|
+
throw new Error(
|
|
3512
|
+
`could not start dashboard on ${DASHBOARD_HOST}:${preferred}.
|
|
3513
|
+
Check ${path4.join(dataDir(), "dashboard.log")} or run:
|
|
3514
|
+
transcodes dashboard --daemon --port ${preferred}
|
|
3515
|
+
Stop a stuck instance with: transcodes stop`
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
2669
3518
|
process.stdout.write(
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
3519
|
+
`${t("dashboardOpened", { url: running.url })}
|
|
3520
|
+
${t("dashboardHowToUse")}
|
|
3521
|
+
${t("dashboardStopHint")}
|
|
2673
3522
|
`
|
|
2674
3523
|
);
|
|
2675
|
-
if (
|
|
2676
|
-
|
|
3524
|
+
if (open) {
|
|
3525
|
+
openDashboardBrowser(running.url);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
async function stopDashboard() {
|
|
3529
|
+
const preferred = readPidRecord()?.port ?? DEFAULT_DASHBOARD_PORT;
|
|
3530
|
+
const record = readPidRecord();
|
|
3531
|
+
const wasRunning = record !== null && isProcessAlive(record.pid) || await findRunningDashboard(preferred) !== null;
|
|
3532
|
+
await killRunningDashboard(preferred);
|
|
3533
|
+
process.stdout.write(
|
|
3534
|
+
wasRunning ? `${t("dashboardStopped")}
|
|
3535
|
+
` : `${t("dashboardNotRunning")}
|
|
3536
|
+
`
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// src/install.ts
|
|
3541
|
+
import { spawn as spawn5 } from "child_process";
|
|
3542
|
+
import fs2 from "fs";
|
|
3543
|
+
import os5 from "os";
|
|
3544
|
+
import path5 from "path";
|
|
3545
|
+
import { createInterface, moveCursor } from "readline";
|
|
3546
|
+
|
|
3547
|
+
// src/login.ts
|
|
3548
|
+
import { spawn as spawn4 } from "child_process";
|
|
3549
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
3550
|
+
import os4 from "os";
|
|
3551
|
+
var REQUEST_TIMEOUT_MS2 = 3e4;
|
|
3552
|
+
var TransientPollError = class extends Error {
|
|
3553
|
+
constructor(message) {
|
|
3554
|
+
super(message);
|
|
3555
|
+
this.name = "TransientPollError";
|
|
3556
|
+
}
|
|
3557
|
+
};
|
|
3558
|
+
var FatalPollError = class extends Error {
|
|
3559
|
+
constructor(message) {
|
|
3560
|
+
super(message);
|
|
3561
|
+
this.name = "FatalPollError";
|
|
3562
|
+
}
|
|
3563
|
+
};
|
|
3564
|
+
function parseOptions(args) {
|
|
3565
|
+
let label = `transcodes-${os4.hostname() || "cli"}`;
|
|
3566
|
+
let open = true;
|
|
3567
|
+
for (let i = 0; i < args.length; i++) {
|
|
3568
|
+
const arg = args[i];
|
|
3569
|
+
if (arg === "-l" || arg === "--label") {
|
|
3570
|
+
const value = args[++i]?.trim();
|
|
3571
|
+
if (!value) {
|
|
3572
|
+
throw new Error("missing label. Usage: transcodes login [-l <label>]");
|
|
3573
|
+
}
|
|
3574
|
+
label = value;
|
|
3575
|
+
} else if (arg === "--no-open") {
|
|
3576
|
+
open = false;
|
|
3577
|
+
} else {
|
|
3578
|
+
throw new Error(
|
|
3579
|
+
`unknown flag "${arg}". Usage: transcodes login [-l <label>] [--no-open]`
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
return { label, open };
|
|
3584
|
+
}
|
|
3585
|
+
function openBrowser2(url) {
|
|
3586
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
3587
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
3588
|
+
try {
|
|
3589
|
+
const child = spawn4(opener, args, { stdio: "ignore", detached: true });
|
|
3590
|
+
child.on("error", () => {
|
|
3591
|
+
});
|
|
3592
|
+
child.unref();
|
|
3593
|
+
return true;
|
|
3594
|
+
} catch {
|
|
3595
|
+
return false;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
function apiBaseV1() {
|
|
3599
|
+
const backend = process.env.TRANSCODES_BACKEND_URL?.trim() || DEFAULT_BACKEND_URL;
|
|
3600
|
+
return `${backend.replace(/\/$/, "")}/v1`;
|
|
3601
|
+
}
|
|
3602
|
+
async function postJson(path6, body) {
|
|
3603
|
+
const controller = new AbortController();
|
|
3604
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
3605
|
+
try {
|
|
3606
|
+
const response = await fetch(`${apiBaseV1()}${path6}`, {
|
|
3607
|
+
method: "POST",
|
|
3608
|
+
headers: {
|
|
3609
|
+
Accept: "application/json",
|
|
3610
|
+
"Content-Type": "application/json"
|
|
3611
|
+
},
|
|
3612
|
+
body: JSON.stringify(body),
|
|
3613
|
+
signal: controller.signal
|
|
3614
|
+
});
|
|
3615
|
+
let envelope;
|
|
3616
|
+
try {
|
|
3617
|
+
envelope = await response.json();
|
|
3618
|
+
} catch {
|
|
3619
|
+
throw new TransientPollError(
|
|
3620
|
+
`CLI authorization response was not JSON (${response.status})`
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
return { status: response.status, envelope };
|
|
3624
|
+
} catch (error) {
|
|
3625
|
+
if (error instanceof TransientPollError) throw error;
|
|
3626
|
+
const aborted = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
3627
|
+
throw new TransientPollError(
|
|
3628
|
+
aborted ? "CLI authorization request timed out" : error instanceof Error ? error.message : "CLI authorization request failed"
|
|
3629
|
+
);
|
|
3630
|
+
} finally {
|
|
3631
|
+
clearTimeout(timer);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
async function createSession(body) {
|
|
3635
|
+
const { status, envelope } = await postJson(
|
|
3636
|
+
"/auth/temp-session/cli/session",
|
|
3637
|
+
body
|
|
3638
|
+
);
|
|
3639
|
+
const value = envelope.payload?.[0];
|
|
3640
|
+
if (status < 200 || status >= 300 || !value) {
|
|
3641
|
+
throw new Error(
|
|
3642
|
+
envelope.message || envelope.error || `CLI authorization request failed (${status})`
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
return value;
|
|
3646
|
+
}
|
|
3647
|
+
async function pollToken(params) {
|
|
3648
|
+
const { status, envelope } = await postJson(
|
|
3649
|
+
`/auth/temp-session/cli/session/${encodeURIComponent(params.sid)}/token`,
|
|
3650
|
+
{ code_verifier: params.verifier }
|
|
3651
|
+
);
|
|
3652
|
+
if (status === 404) {
|
|
3653
|
+
throw new FatalPollError(
|
|
3654
|
+
envelope.message || envelope.error || "CLI authorization session expired or was already used"
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
if (status === 403) {
|
|
3658
|
+
throw new FatalPollError(
|
|
3659
|
+
envelope.message || envelope.error || "CLI authorization verifier was rejected"
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
if (status >= 500 || status === 0) {
|
|
3663
|
+
throw new TransientPollError(
|
|
3664
|
+
envelope.message || envelope.error || `CLI authorization temporary failure (${status})`
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
const value = envelope.payload?.[0];
|
|
3668
|
+
if (status < 200 || status >= 300 || !value) {
|
|
3669
|
+
throw new FatalPollError(
|
|
3670
|
+
envelope.message || envelope.error || `CLI authorization request failed (${status})`
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3673
|
+
return value;
|
|
3674
|
+
}
|
|
3675
|
+
function delay(ms) {
|
|
3676
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3677
|
+
}
|
|
3678
|
+
async function waitForToken(params) {
|
|
3679
|
+
const deadline = new Date(params.expiresAt).getTime();
|
|
3680
|
+
if (!Number.isFinite(deadline)) {
|
|
3681
|
+
throw new Error("CLI authorization session has an invalid expiry");
|
|
3682
|
+
}
|
|
3683
|
+
while (Date.now() < deadline) {
|
|
3684
|
+
try {
|
|
3685
|
+
const result = await pollToken({
|
|
3686
|
+
sid: params.sid,
|
|
3687
|
+
verifier: params.verifier
|
|
3688
|
+
});
|
|
3689
|
+
if (result.status === "authorized" && result.token) return result;
|
|
3690
|
+
} catch (error) {
|
|
3691
|
+
if (error instanceof FatalPollError) throw error;
|
|
3692
|
+
}
|
|
3693
|
+
await delay(Math.max(1, params.interval) * 1e3);
|
|
3694
|
+
}
|
|
3695
|
+
throw new Error("CLI authorization timed out");
|
|
3696
|
+
}
|
|
3697
|
+
async function cmdLogin(args) {
|
|
3698
|
+
const options = parseOptions(args);
|
|
3699
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
3700
|
+
const challenge = createHash2("sha256").update(verifier).digest("base64url");
|
|
3701
|
+
const session = await createSession({
|
|
3702
|
+
code_challenge: challenge,
|
|
3703
|
+
label: options.label
|
|
3704
|
+
});
|
|
3705
|
+
process.stdout.write(`${t("loginPleaseSignIn")}
|
|
3706
|
+
`);
|
|
3707
|
+
if (!options.open) {
|
|
3708
|
+
process.stdout.write(`${t("loginNoOpenHint")}
|
|
3709
|
+
`);
|
|
3710
|
+
} else if (!openBrowser2(session.url)) {
|
|
3711
|
+
process.stdout.write(`${t("loginBrowserOpenFailed")}
|
|
3712
|
+
`);
|
|
3713
|
+
} else {
|
|
3714
|
+
process.stdout.write(`${t("loginWaiting")}
|
|
3715
|
+
`);
|
|
2677
3716
|
}
|
|
2678
|
-
await
|
|
3717
|
+
const result = await waitForToken({
|
|
3718
|
+
sid: session.sid,
|
|
3719
|
+
verifier,
|
|
3720
|
+
expiresAt: session.expires_at,
|
|
3721
|
+
interval: session.interval
|
|
3722
|
+
});
|
|
3723
|
+
const token = result.token;
|
|
3724
|
+
if (!token) throw new Error("CLI authorization completed without a token");
|
|
3725
|
+
const label = result.label || options.label;
|
|
3726
|
+
parseMemberAccessToken(token);
|
|
3727
|
+
writeTokenToFile(token, label);
|
|
3728
|
+
process.stdout.write(`${t("loginTokenSaved")}
|
|
3729
|
+
`);
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
// src/install.ts
|
|
3733
|
+
var REPO_SLUG = "transcodings/transcodes-guard";
|
|
3734
|
+
var REPO_GIT_URL = "https://github.com/transcodings/transcodes-guard.git";
|
|
3735
|
+
var MARKETPLACE = "bigstrider";
|
|
3736
|
+
var PLUGIN_NAME = "transcodes-guard";
|
|
3737
|
+
var MIN_NODE_MAJOR = 20;
|
|
3738
|
+
var PLATFORMS = [
|
|
3739
|
+
{ id: "claude", label: "Claude Code" },
|
|
3740
|
+
{ id: "codex", label: "ChatGPT (Codex)" },
|
|
3741
|
+
{
|
|
3742
|
+
id: "cursor",
|
|
3743
|
+
label: "Cursor",
|
|
3744
|
+
installerRel: "plugins/cursor/install.mjs"
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
id: "antigravity",
|
|
3748
|
+
label: "Antigravity",
|
|
3749
|
+
installerRel: "plugins/antigravity/install.mjs"
|
|
3750
|
+
}
|
|
3751
|
+
];
|
|
3752
|
+
function log(line = "") {
|
|
3753
|
+
process.stdout.write(`${line}
|
|
3754
|
+
`);
|
|
3755
|
+
}
|
|
3756
|
+
function clearScreen() {
|
|
3757
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
3758
|
+
}
|
|
3759
|
+
function displayWidth(s) {
|
|
3760
|
+
let w = 0;
|
|
3761
|
+
for (const ch of s) {
|
|
3762
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
3763
|
+
if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510) {
|
|
3764
|
+
w += 2;
|
|
3765
|
+
} else {
|
|
3766
|
+
w += 1;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
return w;
|
|
3770
|
+
}
|
|
3771
|
+
function physicalRowCount(lines) {
|
|
3772
|
+
const cols = Math.max(1, process.stdout.columns || 80);
|
|
3773
|
+
let rows = 0;
|
|
3774
|
+
for (const line of lines) {
|
|
3775
|
+
const w = Math.max(1, displayWidth(line));
|
|
3776
|
+
rows += Math.ceil(w / cols);
|
|
3777
|
+
}
|
|
3778
|
+
return rows;
|
|
3779
|
+
}
|
|
3780
|
+
function redrawBlock(stdout, lines, previousPhysicalRows) {
|
|
3781
|
+
if (previousPhysicalRows > 0) {
|
|
3782
|
+
moveCursor(stdout, 0, -previousPhysicalRows);
|
|
3783
|
+
stdout.write("\x1B[J");
|
|
3784
|
+
}
|
|
3785
|
+
stdout.write(`${lines.map((line) => `${line}\x1B[K`).join("\n")}
|
|
3786
|
+
`);
|
|
3787
|
+
return physicalRowCount(lines);
|
|
3788
|
+
}
|
|
3789
|
+
function isTty() {
|
|
3790
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3791
|
+
}
|
|
3792
|
+
function openUrl(url) {
|
|
3793
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
3794
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
3795
|
+
try {
|
|
3796
|
+
const child = spawn5(opener, args, { stdio: "ignore", detached: true });
|
|
3797
|
+
child.on("error", () => {
|
|
3798
|
+
});
|
|
3799
|
+
child.unref();
|
|
3800
|
+
} catch {
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
function refreshPathHints() {
|
|
3804
|
+
const home = os5.homedir();
|
|
3805
|
+
const extras = [
|
|
3806
|
+
path5.join(home, ".local", "bin"),
|
|
3807
|
+
path5.join(home, ".claude", "bin"),
|
|
3808
|
+
path5.join(home, ".codex", "bin"),
|
|
3809
|
+
path5.join(home, ".cursor", "bin"),
|
|
3810
|
+
path5.join(home, ".gemini", "bin"),
|
|
3811
|
+
path5.join(home, "bin"),
|
|
3812
|
+
"/usr/local/bin",
|
|
3813
|
+
"/opt/homebrew/bin"
|
|
3814
|
+
];
|
|
3815
|
+
const current2 = process.env.PATH ?? "";
|
|
3816
|
+
const parts = current2.split(path5.delimiter).filter(Boolean);
|
|
3817
|
+
const merged = [...extras.filter((p) => !parts.includes(p)), ...parts];
|
|
3818
|
+
process.env.PATH = merged.join(path5.delimiter);
|
|
3819
|
+
}
|
|
3820
|
+
function commandExists(cmd) {
|
|
3821
|
+
return new Promise((resolve) => {
|
|
3822
|
+
const child = process.platform === "win32" ? spawn5("where", [cmd], { stdio: "ignore" }) : spawn5("sh", ["-c", `command -v ${JSON.stringify(cmd)}`], {
|
|
3823
|
+
stdio: "ignore"
|
|
3824
|
+
});
|
|
3825
|
+
child.on("error", () => resolve(false));
|
|
3826
|
+
child.on("close", (code) => resolve(code === 0));
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
async function anyCommandExists(cmds) {
|
|
3830
|
+
for (const cmd of cmds) {
|
|
3831
|
+
if (await commandExists(cmd)) return cmd;
|
|
3832
|
+
}
|
|
3833
|
+
return null;
|
|
3834
|
+
}
|
|
3835
|
+
function run(cmd, args) {
|
|
3836
|
+
return new Promise((resolve) => {
|
|
3837
|
+
log(`
|
|
3838
|
+
$ ${cmd} ${args.join(" ")}`);
|
|
3839
|
+
const child = spawn5(cmd, args, { stdio: "inherit", env: process.env });
|
|
3840
|
+
child.on("error", (err) => {
|
|
3841
|
+
log(` (failed to start: ${err.message})`);
|
|
3842
|
+
resolve(1);
|
|
3843
|
+
});
|
|
3844
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
function runShell(script) {
|
|
3848
|
+
return new Promise((resolve) => {
|
|
3849
|
+
log(`
|
|
3850
|
+
$ ${script}`);
|
|
3851
|
+
const child = spawn5("sh", ["-c", script], {
|
|
3852
|
+
stdio: "inherit",
|
|
3853
|
+
env: process.env
|
|
3854
|
+
});
|
|
3855
|
+
child.on("error", (err) => {
|
|
3856
|
+
log(` (failed to start: ${err.message})`);
|
|
3857
|
+
resolve(1);
|
|
3858
|
+
});
|
|
3859
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
async function runAll(steps) {
|
|
3863
|
+
for (const step of steps) {
|
|
3864
|
+
const code = await run(step.cmd, step.args);
|
|
3865
|
+
if (code !== 0) return false;
|
|
3866
|
+
}
|
|
3867
|
+
return true;
|
|
3868
|
+
}
|
|
3869
|
+
function nodeMajor(version = process.versions.node) {
|
|
3870
|
+
const major = Number(version.split(".")[0]);
|
|
3871
|
+
return Number.isFinite(major) ? major : 0;
|
|
3872
|
+
}
|
|
3873
|
+
async function ensureNode() {
|
|
3874
|
+
log("\u2500\u2500 Prerequisites \u2500\u2500");
|
|
3875
|
+
const major = nodeMajor();
|
|
3876
|
+
const onPath = await commandExists("node");
|
|
3877
|
+
if (major >= MIN_NODE_MAJOR && onPath) {
|
|
3878
|
+
log(` \u2713 Node.js v${process.versions.node} (>= ${MIN_NODE_MAJOR})`);
|
|
3879
|
+
return true;
|
|
3880
|
+
}
|
|
3881
|
+
if (major >= MIN_NODE_MAJOR && !onPath) {
|
|
3882
|
+
log(
|
|
3883
|
+
` ! Node.js v${process.versions.node} is running this CLI but \`node\` is not on PATH.`
|
|
3884
|
+
);
|
|
3885
|
+
log(" Prepending common bin dirs and continuing\u2026");
|
|
3886
|
+
refreshPathHints();
|
|
3887
|
+
if (await commandExists("node")) {
|
|
3888
|
+
log(" \u2713 Node.js now on PATH");
|
|
3889
|
+
return true;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
log(
|
|
3893
|
+
major > 0 && major < MIN_NODE_MAJOR ? ` Node.js v${process.versions.node} is below ${MIN_NODE_MAJOR} \u2014 installing LTS\u2026` : " Node.js not found \u2014 installing LTS\u2026"
|
|
3894
|
+
);
|
|
3895
|
+
let installed = false;
|
|
3896
|
+
if (process.platform === "darwin" && await commandExists("brew")) {
|
|
3897
|
+
installed = await run("brew", ["install", "node"]) === 0;
|
|
3898
|
+
}
|
|
3899
|
+
if (!installed && await commandExists("nvm")) {
|
|
3900
|
+
installed = await runShell("nvm install --lts && nvm use --lts") === 0;
|
|
3901
|
+
}
|
|
3902
|
+
if (!installed) {
|
|
3903
|
+
const nvmInstall = [
|
|
3904
|
+
"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash",
|
|
3905
|
+
'export NVM_DIR="$HOME/.nvm"',
|
|
3906
|
+
'[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"',
|
|
3907
|
+
"nvm install --lts",
|
|
3908
|
+
'nvm alias default "lts/*"'
|
|
3909
|
+
].join(" && ");
|
|
3910
|
+
installed = await runShell(nvmInstall) === 0;
|
|
3911
|
+
}
|
|
3912
|
+
refreshPathHints();
|
|
3913
|
+
const nvmDefaultBin = path5.join(os5.homedir(), ".nvm", "versions", "node");
|
|
3914
|
+
if (fs2.existsSync(nvmDefaultBin)) {
|
|
3915
|
+
try {
|
|
3916
|
+
const versions = fs2.readdirSync(nvmDefaultBin).filter((v) => v.startsWith("v")).sort().reverse();
|
|
3917
|
+
if (versions[0]) {
|
|
3918
|
+
const bin = path5.join(nvmDefaultBin, versions[0], "bin");
|
|
3919
|
+
process.env.PATH = `${bin}${path5.delimiter}${process.env.PATH ?? ""}`;
|
|
3920
|
+
}
|
|
3921
|
+
} catch {
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
if (await commandExists("node")) {
|
|
3925
|
+
const ver = await new Promise((resolve) => {
|
|
3926
|
+
const child = spawn5("node", ["-v"], {
|
|
3927
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3928
|
+
});
|
|
3929
|
+
let out = "";
|
|
3930
|
+
child.stdout?.on("data", (c) => {
|
|
3931
|
+
out += c.toString();
|
|
3932
|
+
});
|
|
3933
|
+
child.on("close", () => resolve(out.trim().replace(/^v/, "")));
|
|
3934
|
+
child.on("error", () => resolve(""));
|
|
3935
|
+
});
|
|
3936
|
+
const installedMajor = nodeMajor(ver || "0");
|
|
3937
|
+
if (installedMajor >= MIN_NODE_MAJOR) {
|
|
3938
|
+
log(` \u2713 Node.js v${ver} installed`);
|
|
3939
|
+
return true;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
log("");
|
|
3943
|
+
log("Could not install Node.js LTS automatically.");
|
|
3944
|
+
log(
|
|
3945
|
+
`Install Node.js ${MIN_NODE_MAJOR}+ from https://nodejs.org then re-run:`
|
|
3946
|
+
);
|
|
3947
|
+
log(" transcodes install");
|
|
3948
|
+
openUrl("https://nodejs.org");
|
|
3949
|
+
return false;
|
|
3950
|
+
}
|
|
3951
|
+
var HOST_CLIS = {
|
|
3952
|
+
claude: {
|
|
3953
|
+
binaries: ["claude"],
|
|
3954
|
+
label: "Claude Code CLI (`claude`)",
|
|
3955
|
+
install: async () => {
|
|
3956
|
+
if (process.platform === "win32") {
|
|
3957
|
+
return await run("npm", ["install", "-g", "@anthropic-ai/claude-code"]) === 0;
|
|
3958
|
+
}
|
|
3959
|
+
const native = await runShell("curl -fsSL https://claude.ai/install.sh | bash") === 0;
|
|
3960
|
+
if (native) return true;
|
|
3961
|
+
return await run("npm", ["install", "-g", "@anthropic-ai/claude-code"]) === 0;
|
|
3962
|
+
}
|
|
3963
|
+
},
|
|
3964
|
+
codex: {
|
|
3965
|
+
binaries: ["codex"],
|
|
3966
|
+
label: "Codex CLI (`codex`)",
|
|
3967
|
+
install: async () => {
|
|
3968
|
+
if (process.platform !== "win32") {
|
|
3969
|
+
const native = await runShell(
|
|
3970
|
+
"curl -fsSL https://chatgpt.com/codex/install.sh | sh"
|
|
3971
|
+
) === 0;
|
|
3972
|
+
if (native) return true;
|
|
3973
|
+
}
|
|
3974
|
+
return await run("npm", ["install", "-g", "@openai/codex"]) === 0;
|
|
3975
|
+
}
|
|
3976
|
+
},
|
|
3977
|
+
cursor: {
|
|
3978
|
+
// Docs: `agent` is primary; `cursor-agent` is the backward-compatible name.
|
|
3979
|
+
binaries: ["cursor-agent", "agent"],
|
|
3980
|
+
label: "Cursor Agent CLI (`cursor-agent` / `agent`)",
|
|
3981
|
+
install: async () => {
|
|
3982
|
+
if (process.platform === "win32") {
|
|
3983
|
+
return await runShell(
|
|
3984
|
+
`powershell -NoProfile -Command "irm 'https://cursor.com/install?win32=true' | iex"`
|
|
3985
|
+
) === 0;
|
|
3986
|
+
}
|
|
3987
|
+
return await runShell("curl https://cursor.com/install -fsS | bash") === 0;
|
|
3988
|
+
}
|
|
3989
|
+
},
|
|
3990
|
+
antigravity: {
|
|
3991
|
+
binaries: ["agy"],
|
|
3992
|
+
label: "Antigravity CLI (`agy`)",
|
|
3993
|
+
install: async () => await runShell(
|
|
3994
|
+
"curl -fsSL https://antigravity.google/cli/install.sh | bash"
|
|
3995
|
+
) === 0
|
|
3996
|
+
}
|
|
3997
|
+
};
|
|
3998
|
+
async function ensureHostCli(id) {
|
|
3999
|
+
const spec = HOST_CLIS[id];
|
|
4000
|
+
refreshPathHints();
|
|
4001
|
+
const found = await anyCommandExists(spec.binaries);
|
|
4002
|
+
if (found) {
|
|
4003
|
+
log(` \u2713 ${spec.label} found (\`${found}\`)`);
|
|
4004
|
+
return true;
|
|
4005
|
+
}
|
|
4006
|
+
log(` ${spec.label} not found \u2014 installing\u2026`);
|
|
4007
|
+
const ok = await spec.install();
|
|
4008
|
+
refreshPathHints();
|
|
4009
|
+
const after = await anyCommandExists(spec.binaries);
|
|
4010
|
+
if (ok && after) {
|
|
4011
|
+
log(` \u2713 ${spec.label} installed (\`${after}\`)`);
|
|
4012
|
+
return true;
|
|
4013
|
+
}
|
|
4014
|
+
log(` \u2717 Failed to install ${spec.label}.`);
|
|
4015
|
+
log(" Install it manually, then re-run `transcodes install`.");
|
|
4016
|
+
return false;
|
|
4017
|
+
}
|
|
4018
|
+
function promptLine(question) {
|
|
4019
|
+
return new Promise((resolve) => {
|
|
4020
|
+
const rl = createInterface({
|
|
4021
|
+
input: process.stdin,
|
|
4022
|
+
output: process.stdout
|
|
4023
|
+
});
|
|
4024
|
+
rl.question(question, (answer) => {
|
|
4025
|
+
rl.close();
|
|
4026
|
+
resolve(answer);
|
|
4027
|
+
});
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
function safeExists(p) {
|
|
4031
|
+
try {
|
|
4032
|
+
return fs2.existsSync(p);
|
|
4033
|
+
} catch {
|
|
4034
|
+
return false;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
function dirHasMatch(dir, needles) {
|
|
4038
|
+
try {
|
|
4039
|
+
return fs2.readdirSync(dir).some((entry) => needles.some((n) => entry.includes(n)));
|
|
4040
|
+
} catch {
|
|
4041
|
+
return false;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
function fileHas(file, needle) {
|
|
4045
|
+
try {
|
|
4046
|
+
return fs2.readFileSync(file, "utf8").includes(needle);
|
|
4047
|
+
} catch {
|
|
4048
|
+
return false;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
function isPluginInstalled(id) {
|
|
4052
|
+
const home = os5.homedir();
|
|
4053
|
+
switch (id) {
|
|
4054
|
+
case "cursor":
|
|
4055
|
+
return safeExists(
|
|
4056
|
+
path5.join(home, ".cursor", "plugins", "local", "transcodes-guard")
|
|
4057
|
+
);
|
|
4058
|
+
case "antigravity":
|
|
4059
|
+
return safeExists(
|
|
4060
|
+
path5.join(home, ".gemini", "config", "plugins", "transcodes-guard")
|
|
4061
|
+
);
|
|
4062
|
+
case "claude":
|
|
4063
|
+
return dirHasMatch(path5.join(home, ".claude", "plugins"), [
|
|
4064
|
+
"transcodes-guard",
|
|
4065
|
+
"bigstrider"
|
|
4066
|
+
]) || dirHasMatch(path5.join(home, ".claude", "plugins", "marketplaces"), [
|
|
4067
|
+
"bigstrider",
|
|
4068
|
+
"transcodes-guard"
|
|
4069
|
+
]) || fileHas(
|
|
4070
|
+
path5.join(home, ".claude", "settings.json"),
|
|
4071
|
+
"transcodes-guard"
|
|
4072
|
+
) || fileHas(path5.join(home, ".claude.json"), "transcodes-guard");
|
|
4073
|
+
case "codex":
|
|
4074
|
+
return dirHasMatch(path5.join(home, ".codex", "plugins"), [
|
|
4075
|
+
"transcodes-guard",
|
|
4076
|
+
"bigstrider"
|
|
4077
|
+
]) || fileHas(path5.join(home, ".codex", "config.toml"), "transcodes-guard") || fileHas(path5.join(home, ".codex", "config.json"), "transcodes-guard");
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
function commandExistsSync(cmd) {
|
|
4081
|
+
const pathEnv = process.env.PATH ?? "";
|
|
4082
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ".ps1", ""] : [""];
|
|
4083
|
+
for (const dir of pathEnv.split(path5.delimiter)) {
|
|
4084
|
+
if (!dir) continue;
|
|
4085
|
+
for (const ext of exts) {
|
|
4086
|
+
if (safeExists(path5.join(dir, `${cmd}${ext}`))) return true;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
return false;
|
|
4090
|
+
}
|
|
4091
|
+
function hostDesktopPresent(id) {
|
|
4092
|
+
const home = os5.homedir();
|
|
4093
|
+
if (process.platform === "darwin") {
|
|
4094
|
+
const candidates = {
|
|
4095
|
+
claude: [
|
|
4096
|
+
"/Applications/Claude.app",
|
|
4097
|
+
"/Applications/Claude Code.app",
|
|
4098
|
+
path5.join(home, "Applications", "Claude.app"),
|
|
4099
|
+
path5.join(home, "Applications", "Claude Code.app")
|
|
4100
|
+
],
|
|
4101
|
+
cursor: [
|
|
4102
|
+
"/Applications/Cursor.app",
|
|
4103
|
+
path5.join(home, "Applications", "Cursor.app")
|
|
4104
|
+
],
|
|
4105
|
+
codex: [
|
|
4106
|
+
"/Applications/ChatGPT.app",
|
|
4107
|
+
"/Applications/Codex.app",
|
|
4108
|
+
path5.join(home, "Applications", "ChatGPT.app"),
|
|
4109
|
+
path5.join(home, "Applications", "Codex.app")
|
|
4110
|
+
],
|
|
4111
|
+
antigravity: [
|
|
4112
|
+
"/Applications/Antigravity.app",
|
|
4113
|
+
"/Applications/Google Antigravity.app",
|
|
4114
|
+
path5.join(home, "Applications", "Antigravity.app"),
|
|
4115
|
+
path5.join(home, "Applications", "Google Antigravity.app")
|
|
4116
|
+
]
|
|
4117
|
+
};
|
|
4118
|
+
return candidates[id].some(safeExists);
|
|
4119
|
+
}
|
|
4120
|
+
if (process.platform === "win32") {
|
|
4121
|
+
const local = process.env.LOCALAPPDATA ?? "";
|
|
4122
|
+
const prog = process.env.PROGRAMFILES ?? "C:\\Program Files";
|
|
4123
|
+
const candidates = {
|
|
4124
|
+
claude: [
|
|
4125
|
+
path5.join(local, "Programs", "Claude", "Claude.exe"),
|
|
4126
|
+
path5.join(local, "AnthropicClaude", "claude.exe")
|
|
4127
|
+
],
|
|
4128
|
+
cursor: [
|
|
4129
|
+
path5.join(local, "Programs", "cursor", "Cursor.exe"),
|
|
4130
|
+
path5.join(prog, "Cursor", "Cursor.exe")
|
|
4131
|
+
],
|
|
4132
|
+
codex: [
|
|
4133
|
+
path5.join(local, "Programs", "ChatGPT", "ChatGPT.exe"),
|
|
4134
|
+
path5.join(local, "Programs", "codex", "Codex.exe")
|
|
4135
|
+
],
|
|
4136
|
+
antigravity: [
|
|
4137
|
+
path5.join(local, "Programs", "Antigravity", "Antigravity.exe"),
|
|
4138
|
+
path5.join(local, "Programs", "Google Antigravity", "Antigravity.exe")
|
|
4139
|
+
]
|
|
4140
|
+
};
|
|
4141
|
+
return candidates[id].some(safeExists);
|
|
4142
|
+
}
|
|
4143
|
+
return false;
|
|
4144
|
+
}
|
|
4145
|
+
function isHostAppInstalled(id) {
|
|
4146
|
+
refreshPathHints();
|
|
4147
|
+
if (HOST_CLIS[id].binaries.some(commandExistsSync)) return true;
|
|
4148
|
+
return hostDesktopPresent(id);
|
|
4149
|
+
}
|
|
4150
|
+
function platformShortName(id) {
|
|
4151
|
+
switch (id) {
|
|
4152
|
+
case "claude":
|
|
4153
|
+
return "Claude";
|
|
4154
|
+
case "codex":
|
|
4155
|
+
return "ChatGPT";
|
|
4156
|
+
case "cursor":
|
|
4157
|
+
return "Cursor";
|
|
4158
|
+
case "antigravity":
|
|
4159
|
+
return "Antigravity";
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
function installedAppsHint() {
|
|
4163
|
+
const names = PLATFORMS.filter((p) => isHostAppInstalled(p.id)).map(
|
|
4164
|
+
(p) => platformShortName(p.id)
|
|
4165
|
+
);
|
|
4166
|
+
if (names.length === 0) return t("platformInstalledNone");
|
|
4167
|
+
return t("platformInstalledApps", { list: names.join(", ") });
|
|
4168
|
+
}
|
|
4169
|
+
function renderMenu() {
|
|
4170
|
+
log("");
|
|
4171
|
+
log(t("platformTitle"));
|
|
4172
|
+
log(t("platformHint1"));
|
|
4173
|
+
log(installedAppsHint());
|
|
4174
|
+
log("");
|
|
4175
|
+
PLATFORMS.forEach((p, i) => {
|
|
4176
|
+
const onDevice = isHostAppInstalled(p.id);
|
|
4177
|
+
const dot = onDevice ? "\u25CF" : "\u25CB";
|
|
4178
|
+
const mark = onDevice ? ` ${t("installed")}` : "";
|
|
4179
|
+
log(` ${i + 1}. ${dot} ${p.label}${mark}`);
|
|
4180
|
+
});
|
|
4181
|
+
log(` ${PLATFORMS.length + 1}. ${t("nextStep")}`);
|
|
4182
|
+
log("");
|
|
4183
|
+
log(t("platformNumberedHint"));
|
|
4184
|
+
log(t("platformNumberedNext", { n: String(PLATFORMS.length + 1) }));
|
|
4185
|
+
}
|
|
4186
|
+
async function promptMenu() {
|
|
4187
|
+
const nextNum = String(PLATFORMS.length + 1);
|
|
4188
|
+
for (; ; ) {
|
|
4189
|
+
const answer = (await promptLine("> ")).trim().toLowerCase();
|
|
4190
|
+
if (answer === "exit" || answer === "q") return { kind: "cancel" };
|
|
4191
|
+
if (answer === "next" || answer === "n" || answer === nextNum) {
|
|
4192
|
+
return { kind: "next" };
|
|
4193
|
+
}
|
|
4194
|
+
if (answer === "" || answer === "all" || answer === "a") {
|
|
4195
|
+
return { kind: "install", ids: PLATFORMS.map((p) => p.id) };
|
|
4196
|
+
}
|
|
4197
|
+
const tokens = answer.split(/[\s,]+/).filter(Boolean);
|
|
4198
|
+
const ids = [];
|
|
4199
|
+
let invalid = false;
|
|
4200
|
+
for (const token of tokens) {
|
|
4201
|
+
const num = Number(token);
|
|
4202
|
+
if (!Number.isInteger(num) || num < 1 || num > PLATFORMS.length) {
|
|
4203
|
+
log(
|
|
4204
|
+
` Invalid choice "${token}". Use 1\u2013${PLATFORMS.length}, ${nextNum} (Next Step), all, or exit.`
|
|
4205
|
+
);
|
|
4206
|
+
invalid = true;
|
|
4207
|
+
break;
|
|
4208
|
+
}
|
|
4209
|
+
const pid = PLATFORMS[num - 1].id;
|
|
4210
|
+
if (!ids.includes(pid)) ids.push(pid);
|
|
4211
|
+
}
|
|
4212
|
+
if (invalid) continue;
|
|
4213
|
+
return { kind: "install", ids };
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
function supportsArrowSelect() {
|
|
4217
|
+
return isTty() && typeof process.stdin.setRawMode === "function";
|
|
4218
|
+
}
|
|
4219
|
+
function arrowSelect(checked) {
|
|
4220
|
+
return new Promise((resolve) => {
|
|
4221
|
+
const rowCount = PLATFORMS.length + 1;
|
|
4222
|
+
let cursor = 0;
|
|
4223
|
+
let rendered = 0;
|
|
4224
|
+
const { stdin, stdout } = process;
|
|
4225
|
+
const draw = () => {
|
|
4226
|
+
const lines = [];
|
|
4227
|
+
lines.push(t("platformTitle"));
|
|
4228
|
+
lines.push(t("platformHint1"));
|
|
4229
|
+
lines.push(installedAppsHint());
|
|
4230
|
+
lines.push("");
|
|
4231
|
+
PLATFORMS.forEach((p, i) => {
|
|
4232
|
+
const pointer = cursor === i ? "\u276F" : " ";
|
|
4233
|
+
const onDevice = isHostAppInstalled(p.id);
|
|
4234
|
+
const box = checked.has(p.id) ? "\u25CF" : "\u25EF";
|
|
4235
|
+
const mark = onDevice ? ` ${t("installed")}` : "";
|
|
4236
|
+
lines.push(`${pointer} ${box} ${p.label}${mark}`);
|
|
4237
|
+
});
|
|
4238
|
+
const nextPointer = cursor === PLATFORMS.length ? "\u276F" : " ";
|
|
4239
|
+
lines.push(`${nextPointer} ${t("nextStep")}`);
|
|
4240
|
+
lines.push("");
|
|
4241
|
+
lines.push(t("platformKeys"));
|
|
4242
|
+
rendered = redrawBlock(stdout, lines, rendered);
|
|
4243
|
+
};
|
|
4244
|
+
const cleanup = () => {
|
|
4245
|
+
stdout.write("\x1B[?25h");
|
|
4246
|
+
stdin.setRawMode?.(false);
|
|
4247
|
+
stdin.pause();
|
|
4248
|
+
stdin.removeListener("data", onData);
|
|
4249
|
+
};
|
|
4250
|
+
const onData = (buf) => {
|
|
4251
|
+
const key = buf.toString();
|
|
4252
|
+
if (key === "" || key === "q") {
|
|
4253
|
+
cleanup();
|
|
4254
|
+
resolve({ kind: "cancel" });
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
if (key === "\r" || key === "\n") {
|
|
4258
|
+
cleanup();
|
|
4259
|
+
if (cursor === PLATFORMS.length) {
|
|
4260
|
+
resolve({ kind: "next" });
|
|
4261
|
+
} else {
|
|
4262
|
+
resolve({
|
|
4263
|
+
kind: "install",
|
|
4264
|
+
ids: PLATFORMS.filter((p) => checked.has(p.id)).map((p) => p.id)
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
if (key === " ") {
|
|
4270
|
+
if (cursor < PLATFORMS.length) {
|
|
4271
|
+
const id = PLATFORMS[cursor].id;
|
|
4272
|
+
if (checked.has(id)) checked.delete(id);
|
|
4273
|
+
else checked.add(id);
|
|
4274
|
+
}
|
|
4275
|
+
} else if (key === "a") {
|
|
4276
|
+
if (PLATFORMS.every((p) => checked.has(p.id))) checked.clear();
|
|
4277
|
+
else for (const p of PLATFORMS) checked.add(p.id);
|
|
4278
|
+
} else if (key === "\x1B[A" || key === "k") {
|
|
4279
|
+
cursor = (cursor - 1 + rowCount) % rowCount;
|
|
4280
|
+
} else if (key === "\x1B[B" || key === "j") {
|
|
4281
|
+
cursor = (cursor + 1) % rowCount;
|
|
4282
|
+
} else {
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
draw();
|
|
4286
|
+
};
|
|
4287
|
+
stdin.setRawMode?.(true);
|
|
4288
|
+
stdin.resume();
|
|
4289
|
+
stdin.setEncoding("utf8");
|
|
4290
|
+
stdin.on("data", onData);
|
|
4291
|
+
stdout.write("\x1B[?25l");
|
|
4292
|
+
draw();
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
function arrowChoose(title, options, defaultIndex = 0) {
|
|
4296
|
+
return new Promise((resolve) => {
|
|
4297
|
+
let cursor = Math.max(0, Math.min(defaultIndex, options.length - 1));
|
|
4298
|
+
let rendered = 0;
|
|
4299
|
+
const { stdin, stdout } = process;
|
|
4300
|
+
const draw = () => {
|
|
4301
|
+
const lines = [title];
|
|
4302
|
+
options.forEach((opt, i) => {
|
|
4303
|
+
const pointer = cursor === i ? "\u276F" : " ";
|
|
4304
|
+
const radio = cursor === i ? "\u25C9" : "\u25EF";
|
|
4305
|
+
lines.push(`${pointer} ${radio} ${opt}`);
|
|
4306
|
+
});
|
|
4307
|
+
lines.push("");
|
|
4308
|
+
lines.push(t("chooseKeys"));
|
|
4309
|
+
rendered = redrawBlock(stdout, lines, rendered);
|
|
4310
|
+
};
|
|
4311
|
+
const cleanup = () => {
|
|
4312
|
+
stdout.write("\x1B[?25h");
|
|
4313
|
+
stdin.setRawMode?.(false);
|
|
4314
|
+
stdin.pause();
|
|
4315
|
+
stdin.removeListener("data", onData);
|
|
4316
|
+
};
|
|
4317
|
+
const onData = (buf) => {
|
|
4318
|
+
const key = buf.toString();
|
|
4319
|
+
if (key === "") {
|
|
4320
|
+
cleanup();
|
|
4321
|
+
resolve(-1);
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4324
|
+
if (key === "\r" || key === "\n") {
|
|
4325
|
+
cleanup();
|
|
4326
|
+
resolve(cursor);
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
if (key === "\x1B[A" || key === "k") {
|
|
4330
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
4331
|
+
} else if (key === "\x1B[B" || key === "j") {
|
|
4332
|
+
cursor = (cursor + 1) % options.length;
|
|
4333
|
+
} else {
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
draw();
|
|
4337
|
+
};
|
|
4338
|
+
stdin.setRawMode?.(true);
|
|
4339
|
+
stdin.resume();
|
|
4340
|
+
stdin.setEncoding("utf8");
|
|
4341
|
+
stdin.on("data", onData);
|
|
4342
|
+
stdout.write("\x1B[?25l");
|
|
4343
|
+
draw();
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
async function runInstalls(ids) {
|
|
4347
|
+
const clonedRepoDir = await cloneRepoIfNeeded(ids);
|
|
4348
|
+
const results = /* @__PURE__ */ new Map();
|
|
4349
|
+
try {
|
|
4350
|
+
for (const id of ids) {
|
|
4351
|
+
const label = PLATFORMS.find((p) => p.id === id)?.label ?? id;
|
|
4352
|
+
log(`
|
|
4353
|
+
\u2500\u2500 ${label} \u2500\u2500`);
|
|
4354
|
+
results.set(id, await installPlatform(id, clonedRepoDir));
|
|
4355
|
+
}
|
|
4356
|
+
} finally {
|
|
4357
|
+
if (clonedRepoDir) {
|
|
4358
|
+
fs2.rmSync(clonedRepoDir, { recursive: true, force: true });
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
log(`
|
|
4362
|
+
${t("installSummary")}`);
|
|
4363
|
+
for (const id of ids) {
|
|
4364
|
+
const label = PLATFORMS.find((p) => p.id === id)?.label ?? id;
|
|
4365
|
+
log(` ${results.get(id) ? "\u2713" : "\u2717"} ${label}`);
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
async function installPlatform(id, clonedRepoDir) {
|
|
4369
|
+
if (!await ensureHostCli(id)) return false;
|
|
4370
|
+
if (id === "claude") {
|
|
4371
|
+
return runAll([
|
|
4372
|
+
{ cmd: "claude", args: ["plugin", "marketplace", "add", REPO_SLUG] },
|
|
4373
|
+
{
|
|
4374
|
+
cmd: "claude",
|
|
4375
|
+
args: [
|
|
4376
|
+
"plugin",
|
|
4377
|
+
"install",
|
|
4378
|
+
`${PLUGIN_NAME}@${MARKETPLACE}`,
|
|
4379
|
+
"--scope",
|
|
4380
|
+
"user"
|
|
4381
|
+
]
|
|
4382
|
+
}
|
|
4383
|
+
]);
|
|
4384
|
+
}
|
|
4385
|
+
if (id === "codex") {
|
|
4386
|
+
return runAll([
|
|
4387
|
+
{ cmd: "codex", args: ["plugin", "marketplace", "add", REPO_SLUG] },
|
|
4388
|
+
{
|
|
4389
|
+
cmd: "codex",
|
|
4390
|
+
args: ["plugin", "add", `${PLUGIN_NAME}@${MARKETPLACE}`]
|
|
4391
|
+
}
|
|
4392
|
+
]);
|
|
4393
|
+
}
|
|
4394
|
+
const platform = PLATFORMS.find((p) => p.id === id);
|
|
4395
|
+
if (!clonedRepoDir || !platform?.installerRel) {
|
|
4396
|
+
log(` Skipped ${platform?.label ?? id} \u2014 repo clone unavailable.`);
|
|
4397
|
+
return false;
|
|
4398
|
+
}
|
|
4399
|
+
const installer = path5.join(clonedRepoDir, platform.installerRel);
|
|
4400
|
+
if (!fs2.existsSync(installer)) {
|
|
4401
|
+
log(` Skipped ${platform.label} \u2014 installer missing at ${installer}.`);
|
|
4402
|
+
return false;
|
|
4403
|
+
}
|
|
4404
|
+
return await run("node", [installer]) === 0;
|
|
4405
|
+
}
|
|
4406
|
+
async function cloneRepoIfNeeded(platforms) {
|
|
4407
|
+
const needsClone = platforms.some(
|
|
4408
|
+
(id) => PLATFORMS.find((p) => p.id === id)?.installerRel
|
|
4409
|
+
);
|
|
4410
|
+
if (!needsClone) return null;
|
|
4411
|
+
if (!await commandExists("git")) {
|
|
4412
|
+
log("\n`git` not found on PATH \u2014 cannot install Cursor / Antigravity.");
|
|
4413
|
+
log("Install git and re-run, or use the one-liners from the README.");
|
|
4414
|
+
return null;
|
|
4415
|
+
}
|
|
4416
|
+
const tmpDir = fs2.mkdtempSync(path5.join(os5.tmpdir(), "tg-install-"));
|
|
4417
|
+
const ok = await run("git", ["clone", "--depth", "1", REPO_GIT_URL, tmpDir]) === 0;
|
|
4418
|
+
if (!ok) {
|
|
4419
|
+
fs2.rmSync(tmpDir, { recursive: true, force: true });
|
|
4420
|
+
log("\nRepo clone failed \u2014 skipping Cursor / Antigravity.");
|
|
4421
|
+
return null;
|
|
4422
|
+
}
|
|
4423
|
+
return tmpDir;
|
|
4424
|
+
}
|
|
4425
|
+
function printSetupComplete() {
|
|
4426
|
+
clearScreen();
|
|
4427
|
+
log(t("congratsBar"));
|
|
4428
|
+
log(` ${t("congrats1")}`);
|
|
4429
|
+
log(` ${t("congrats2")}`);
|
|
4430
|
+
log(t("congratsBar"));
|
|
4431
|
+
log("");
|
|
4432
|
+
log(t("congratsBody"));
|
|
4433
|
+
log("");
|
|
4434
|
+
}
|
|
4435
|
+
async function tokenFlow() {
|
|
4436
|
+
log("");
|
|
4437
|
+
try {
|
|
4438
|
+
await cmdLogin([]);
|
|
4439
|
+
return true;
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
log(` ${error instanceof Error ? error.message : String(error)}`);
|
|
4442
|
+
return false;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
async function promptLocale() {
|
|
4446
|
+
if (!isTty()) {
|
|
4447
|
+
setLocale(readSavedLocale() ?? "en");
|
|
4448
|
+
return;
|
|
4449
|
+
}
|
|
4450
|
+
const saved = readSavedLocale();
|
|
4451
|
+
const defaultIndex = saved === "ko" ? 1 : 0;
|
|
4452
|
+
const options = ["English", "\uD55C\uAD6D\uC5B4"];
|
|
4453
|
+
if (supportsArrowSelect()) {
|
|
4454
|
+
const idx = await arrowChoose(t("langTitle"), [...options], defaultIndex);
|
|
4455
|
+
if (idx < 0) {
|
|
4456
|
+
setLocale(saved ?? "en");
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
setLocale(idx === 1 ? "ko" : "en");
|
|
4460
|
+
log("");
|
|
4461
|
+
return;
|
|
4462
|
+
}
|
|
4463
|
+
for (; ; ) {
|
|
4464
|
+
log(t("langTitle"));
|
|
4465
|
+
log(" 1. English");
|
|
4466
|
+
log(" 2. \uD55C\uAD6D\uC5B4");
|
|
4467
|
+
const answer = (await promptLine("> ")).trim().toLowerCase();
|
|
4468
|
+
if (answer === "1" || answer === "en" || answer === "english") {
|
|
4469
|
+
setLocale("en");
|
|
4470
|
+
log("");
|
|
4471
|
+
return;
|
|
4472
|
+
}
|
|
4473
|
+
if (answer === "2" || answer === "ko" || answer === "kr" || answer === "\uD55C\uAD6D\uC5B4") {
|
|
4474
|
+
setLocale("ko");
|
|
4475
|
+
log("");
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
if (answer === "" && saved) {
|
|
4479
|
+
setLocale(saved);
|
|
4480
|
+
log("");
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
log(" 1 or 2 / 1 \uB610\uB294 2");
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
function parseSelection(args) {
|
|
4487
|
+
const ids = [];
|
|
4488
|
+
let explicit = false;
|
|
4489
|
+
for (const arg of args) {
|
|
4490
|
+
if (arg === "--all") {
|
|
4491
|
+
return PLATFORMS.map((p) => p.id);
|
|
4492
|
+
}
|
|
4493
|
+
const match = PLATFORMS.find(
|
|
4494
|
+
(p) => p.id === arg || arg === "chatgpt" && p.id === "codex"
|
|
4495
|
+
);
|
|
4496
|
+
if (match) {
|
|
4497
|
+
explicit = true;
|
|
4498
|
+
if (!ids.includes(match.id)) ids.push(match.id);
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
return explicit ? ids : "interactive";
|
|
4502
|
+
}
|
|
4503
|
+
async function cmdInstall(args) {
|
|
4504
|
+
await promptLocale();
|
|
4505
|
+
log(`${t("installBanner")}
|
|
4506
|
+
`);
|
|
4507
|
+
if (!await ensureNode()) {
|
|
4508
|
+
process.exit(1);
|
|
4509
|
+
}
|
|
4510
|
+
log("");
|
|
4511
|
+
const selection = parseSelection(args);
|
|
4512
|
+
let openDashboard = false;
|
|
4513
|
+
if (selection === "interactive") {
|
|
4514
|
+
if (!isTty()) {
|
|
4515
|
+
log(t("nonInteractive"));
|
|
4516
|
+
log(" transcodes install --all");
|
|
4517
|
+
log(" transcodes install claude codex cursor antigravity");
|
|
4518
|
+
process.exit(1);
|
|
4519
|
+
}
|
|
4520
|
+
const useArrows = supportsArrowSelect();
|
|
4521
|
+
const checked = new Set(
|
|
4522
|
+
PLATFORMS.filter((p) => isHostAppInstalled(p.id)).map((p) => p.id)
|
|
4523
|
+
);
|
|
4524
|
+
for (; ; ) {
|
|
4525
|
+
clearScreen();
|
|
4526
|
+
let choice;
|
|
4527
|
+
if (useArrows) {
|
|
4528
|
+
choice = await arrowSelect(checked);
|
|
4529
|
+
} else {
|
|
4530
|
+
renderMenu();
|
|
4531
|
+
choice = await promptMenu();
|
|
4532
|
+
}
|
|
4533
|
+
if (choice.kind === "cancel") {
|
|
4534
|
+
log("");
|
|
4535
|
+
log(t("cancelHint"));
|
|
4536
|
+
process.exit(0);
|
|
4537
|
+
}
|
|
4538
|
+
if (choice.kind === "next") {
|
|
4539
|
+
clearScreen();
|
|
4540
|
+
break;
|
|
4541
|
+
}
|
|
4542
|
+
if (choice.ids.length === 0) {
|
|
4543
|
+
log(` ${t("nothingSelected")}`);
|
|
4544
|
+
continue;
|
|
4545
|
+
}
|
|
4546
|
+
checked.clear();
|
|
4547
|
+
for (const id of choice.ids) checked.add(id);
|
|
4548
|
+
await runInstalls(choice.ids);
|
|
4549
|
+
}
|
|
4550
|
+
} else {
|
|
4551
|
+
if (selection.length === 0) {
|
|
4552
|
+
log(t("noPlatforms"));
|
|
4553
|
+
process.exit(0);
|
|
4554
|
+
}
|
|
4555
|
+
log(
|
|
4556
|
+
`
|
|
4557
|
+
${t("installing", {
|
|
4558
|
+
list: selection.map((id) => PLATFORMS.find((p) => p.id === id)?.label ?? id).join(", ")
|
|
4559
|
+
})}`
|
|
4560
|
+
);
|
|
4561
|
+
await runInstalls(selection);
|
|
4562
|
+
clearScreen();
|
|
4563
|
+
}
|
|
4564
|
+
openDashboard = await tokenFlow();
|
|
4565
|
+
if (!openDashboard) {
|
|
4566
|
+
process.exit(0);
|
|
4567
|
+
}
|
|
4568
|
+
printSetupComplete();
|
|
4569
|
+
await promptLine(t("pressEnterDashboard"));
|
|
4570
|
+
await ensureDashboard({});
|
|
4571
|
+
process.exit(0);
|
|
4572
|
+
}
|
|
4573
|
+
function installedPlatforms() {
|
|
4574
|
+
return PLATFORMS.filter((p) => isPluginInstalled(p.id)).map((p) => p.id);
|
|
4575
|
+
}
|
|
4576
|
+
async function updateCliPackage() {
|
|
4577
|
+
log(`
|
|
4578
|
+
\u2500\u2500 ${CLI_PACKAGE_NAME} (CLI) \u2500\u2500`);
|
|
4579
|
+
log(` Current version: ${CLI_VERSION}`);
|
|
4580
|
+
if (!await commandExists("npm")) {
|
|
4581
|
+
log(" \u2717 `npm` not found on PATH \u2014 cannot update the CLI.");
|
|
4582
|
+
log(
|
|
4583
|
+
` Install Node.js / npm, then: npm install -g ${CLI_PACKAGE_NAME}@latest`
|
|
4584
|
+
);
|
|
4585
|
+
return false;
|
|
4586
|
+
}
|
|
4587
|
+
const ok = await run("npm", ["install", "-g", `${CLI_PACKAGE_NAME}@latest`]) === 0;
|
|
4588
|
+
if (ok) {
|
|
4589
|
+
log(" \u2713 CLI updated (run `transcodes version` to confirm).");
|
|
4590
|
+
} else {
|
|
4591
|
+
log(" \u2717 CLI update failed.");
|
|
4592
|
+
}
|
|
4593
|
+
return ok;
|
|
4594
|
+
}
|
|
4595
|
+
async function cmdUpdate(args) {
|
|
4596
|
+
log("transcodes update \u2014 refresh plugins and CLI.\n");
|
|
4597
|
+
if (!await ensureNode()) {
|
|
4598
|
+
process.exit(1);
|
|
4599
|
+
}
|
|
4600
|
+
let cliOnly = false;
|
|
4601
|
+
let pluginsOnly = false;
|
|
4602
|
+
let forceAll = false;
|
|
4603
|
+
const platformArgs = [];
|
|
4604
|
+
for (const arg of args) {
|
|
4605
|
+
if (arg === "--cli-only") cliOnly = true;
|
|
4606
|
+
else if (arg === "--plugins-only") pluginsOnly = true;
|
|
4607
|
+
else if (arg === "--all") forceAll = true;
|
|
4608
|
+
else if (arg.startsWith("-")) {
|
|
4609
|
+
log(`Unknown flag "${arg}".`);
|
|
4610
|
+
log(
|
|
4611
|
+
"Usage: transcodes update [--cli-only|--plugins-only|--all] [claude|codex|cursor|antigravity \u2026]"
|
|
4612
|
+
);
|
|
4613
|
+
process.exit(1);
|
|
4614
|
+
} else {
|
|
4615
|
+
platformArgs.push(arg);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
if (cliOnly && pluginsOnly) {
|
|
4619
|
+
log("Cannot combine --cli-only and --plugins-only.");
|
|
4620
|
+
process.exit(1);
|
|
4621
|
+
}
|
|
4622
|
+
const results = [];
|
|
4623
|
+
if (!cliOnly) {
|
|
4624
|
+
let targets;
|
|
4625
|
+
if (forceAll) {
|
|
4626
|
+
targets = PLATFORMS.map((p) => p.id);
|
|
4627
|
+
} else if (platformArgs.length > 0) {
|
|
4628
|
+
const parsed = parseSelection(platformArgs);
|
|
4629
|
+
if (parsed === "interactive" || parsed.length === 0) {
|
|
4630
|
+
log(
|
|
4631
|
+
"No valid platforms. Use: claude, codex, cursor, antigravity (or chatgpt)."
|
|
4632
|
+
);
|
|
4633
|
+
process.exit(1);
|
|
4634
|
+
}
|
|
4635
|
+
targets = parsed;
|
|
4636
|
+
} else {
|
|
4637
|
+
targets = installedPlatforms();
|
|
4638
|
+
}
|
|
4639
|
+
if (targets.length === 0) {
|
|
4640
|
+
log("No installed plugins detected.");
|
|
4641
|
+
log(" Run `transcodes install` first, or `transcodes update --all`.");
|
|
4642
|
+
} else {
|
|
4643
|
+
log(
|
|
4644
|
+
`Updating plugins: ${targets.map((id) => PLATFORMS.find((p) => p.id === id)?.label ?? id).join(", ")}`
|
|
4645
|
+
);
|
|
4646
|
+
const clonedRepoDir = await cloneRepoIfNeeded(targets);
|
|
4647
|
+
try {
|
|
4648
|
+
for (const id of targets) {
|
|
4649
|
+
const label = PLATFORMS.find((p) => p.id === id)?.label ?? id;
|
|
4650
|
+
log(`
|
|
4651
|
+
\u2500\u2500 ${label} \u2500\u2500`);
|
|
4652
|
+
const ok = await installPlatform(id, clonedRepoDir);
|
|
4653
|
+
results.push({ name: label, ok });
|
|
4654
|
+
}
|
|
4655
|
+
} finally {
|
|
4656
|
+
if (clonedRepoDir) {
|
|
4657
|
+
fs2.rmSync(clonedRepoDir, { recursive: true, force: true });
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
if (!pluginsOnly) {
|
|
4663
|
+
const ok = await updateCliPackage();
|
|
4664
|
+
results.push({ name: `${CLI_PACKAGE_NAME} (CLI)`, ok });
|
|
4665
|
+
}
|
|
4666
|
+
log("\n\u2500\u2500 Update summary \u2500\u2500");
|
|
4667
|
+
if (results.length === 0) {
|
|
4668
|
+
log(" (nothing to update)");
|
|
4669
|
+
} else {
|
|
4670
|
+
for (const r of results) {
|
|
4671
|
+
log(` ${r.ok ? "\u2713" : "\u2717"} ${r.name}`);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
log("");
|
|
4675
|
+
log("Done. Restart your host apps/CLIs so they pick up the new plugin.");
|
|
4676
|
+
process.exit(results.some((r) => !r.ok) ? 1 : 0);
|
|
2679
4677
|
}
|
|
2680
4678
|
|
|
2681
4679
|
// src/index.ts
|
|
@@ -2807,9 +4805,10 @@ async function cmdConsole(args) {
|
|
|
2807
4805
|
}
|
|
2808
4806
|
process.exit(0);
|
|
2809
4807
|
}
|
|
2810
|
-
|
|
4808
|
+
function parseDashboardFlags(args, usage) {
|
|
2811
4809
|
let port;
|
|
2812
4810
|
let open = true;
|
|
4811
|
+
let daemon = false;
|
|
2813
4812
|
for (let i = 0; i < args.length; i++) {
|
|
2814
4813
|
if (args[i] === "--port" && args[i + 1]) {
|
|
2815
4814
|
port = Number(args[++i]);
|
|
@@ -2818,14 +4817,49 @@ async function cmdDashboard(args) {
|
|
|
2818
4817
|
}
|
|
2819
4818
|
} else if (args[i] === "--no-open") {
|
|
2820
4819
|
open = false;
|
|
4820
|
+
} else if (args[i] === "--daemon") {
|
|
4821
|
+
daemon = true;
|
|
2821
4822
|
} else {
|
|
2822
|
-
fail(
|
|
2823
|
-
`unknown flag "${args[i]}". Usage: transcodes [--port N] [--no-open]`
|
|
2824
|
-
);
|
|
4823
|
+
fail(`unknown flag "${args[i]}". Usage: ${usage}`);
|
|
2825
4824
|
}
|
|
2826
4825
|
}
|
|
4826
|
+
return { port, open, daemon };
|
|
4827
|
+
}
|
|
4828
|
+
async function cmdDashboard(args) {
|
|
4829
|
+
const { port, open, daemon } = parseDashboardFlags(
|
|
4830
|
+
args,
|
|
4831
|
+
"transcodes [--port N] [--no-open]"
|
|
4832
|
+
);
|
|
4833
|
+
if (daemon) {
|
|
4834
|
+
fail("use `transcodes dashboard --daemon` to run the server process");
|
|
4835
|
+
}
|
|
2827
4836
|
try {
|
|
2828
|
-
await
|
|
4837
|
+
await ensureDashboard({ port, open });
|
|
4838
|
+
} catch (err) {
|
|
4839
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
4840
|
+
}
|
|
4841
|
+
process.exit(0);
|
|
4842
|
+
}
|
|
4843
|
+
async function cmdDashboardSub(args) {
|
|
4844
|
+
const { port, open, daemon } = parseDashboardFlags(
|
|
4845
|
+
args,
|
|
4846
|
+
"transcodes dashboard [--daemon] [--port N] [--no-open]"
|
|
4847
|
+
);
|
|
4848
|
+
try {
|
|
4849
|
+
if (daemon) {
|
|
4850
|
+
await serveDashboard({ port });
|
|
4851
|
+
process.exit(0);
|
|
4852
|
+
return;
|
|
4853
|
+
}
|
|
4854
|
+
await ensureDashboard({ port, open });
|
|
4855
|
+
} catch (err) {
|
|
4856
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
4857
|
+
}
|
|
4858
|
+
process.exit(0);
|
|
4859
|
+
}
|
|
4860
|
+
async function cmdStop() {
|
|
4861
|
+
try {
|
|
4862
|
+
await stopDashboard();
|
|
2829
4863
|
} catch (err) {
|
|
2830
4864
|
fail(err instanceof Error ? err.message : String(err));
|
|
2831
4865
|
}
|
|
@@ -2834,6 +4868,12 @@ async function cmdDashboard(args) {
|
|
|
2834
4868
|
function main() {
|
|
2835
4869
|
const [command, ...rest] = process.argv.slice(2);
|
|
2836
4870
|
switch (command) {
|
|
4871
|
+
case "install":
|
|
4872
|
+
void cmdInstall(rest);
|
|
4873
|
+
break;
|
|
4874
|
+
case "update":
|
|
4875
|
+
void cmdUpdate(rest);
|
|
4876
|
+
break;
|
|
2837
4877
|
case "set":
|
|
2838
4878
|
cmdSet(rest);
|
|
2839
4879
|
break;
|
|
@@ -2846,9 +4886,20 @@ function main() {
|
|
|
2846
4886
|
case "tokens":
|
|
2847
4887
|
cmdTokens();
|
|
2848
4888
|
break;
|
|
4889
|
+
case "login":
|
|
4890
|
+
void cmdLogin(rest).catch((error) => {
|
|
4891
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
4892
|
+
});
|
|
4893
|
+
break;
|
|
2849
4894
|
case "console":
|
|
2850
4895
|
void cmdConsole(rest);
|
|
2851
4896
|
break;
|
|
4897
|
+
case "dashboard":
|
|
4898
|
+
void cmdDashboardSub(rest);
|
|
4899
|
+
break;
|
|
4900
|
+
case "stop":
|
|
4901
|
+
void cmdStop();
|
|
4902
|
+
break;
|
|
2852
4903
|
case "version":
|
|
2853
4904
|
case "--version":
|
|
2854
4905
|
case "-V":
|