@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.
Files changed (3) hide show
  1. package/README.md +32 -4
  2. package/dist/index.js +2258 -207
  3. 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 path2 = input.path.startsWith("/") ? input.path : `/${input.path}`;
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}${path2}${qs ? `?${qs}` : ""}`;
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 t = v.trim();
87
- return t || void 0;
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 t = aud.trim();
98
- return t ? [t] : void 0;
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 current = readConfig();
278
- const existing = current.tokenList.find((r) => r.token === trimmed);
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 ? current.tokenList.map((r) => r.token === trimmed ? { token: trimmed, label: nextLabel ?? r.label } : r) : [...current.tokenList, { token: trimmed, label: nextLabel }];
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 current = readConfig();
295
- if (!current.tokenList.some((r) => r.token === trimmed)) {
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 = current.tokenList.map((r) => r.token === trimmed ? { token: r.token, label: nextLabel } : r);
299
- writeConfig({ token: current.token, tokenList });
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 current = readConfig();
304
- const tokenList = current.tokenList.filter((r) => r.token !== trimmed);
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 = current.token && tokenList.some((r) => r.token === current.token) ? current.token : tokenList[0].token;
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("No Transcodes token found. Install the CLI (`npm install -g @bigstrider/transcodes-cli`), run `transcodes` to open the dashboard, and paste a token from the Transcodes console (member detail page, https://app.transcodes.io). Non-interactive: `transcodes set <token> -l <label>`.");
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 (URL printed in terminal; default port 3847; --port N / --no-open)"
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 DEFAULT_PORT = 3847;
621
- var HOST = "127.0.0.1";
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((t) => fingerprint(t) === id);
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: 600;
1227
- color: var(--muted);
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: 16px 18px;
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-line {
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: 0; }
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
- <p id="header-token-empty" class="header-token-empty" hidden>Please register a token first</p>
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">Manage Auth</button>
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
- <p class="section-title">Getting Started</p>
1833
- <p class="section-sub">New to Transcodes? Watch the walkthrough, then follow the steps below</p>
1834
- <div class="guide-help">
1835
- <p class="guide-help-line"><strong>For LLM agents</strong> \u2014 prefix prompts with <code class="cli-cmd">/transcodes</code> or <code class="cli-cmd">$transcodes</code> to reduce hallucinations</p>
1836
- <div class="guide-help-examples">
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
- <div class="guide-video">
1842
- <mux-player
1843
- playback-id="${GUIDELINE_MUX_PLAYBACK_ID}"
1844
- stream-type="on-demand"
1845
- accent-color="#5b54e6"
1846
- primary-color="#ffffff"
1847
- metadata-video-title="Transcodes CLI onboarding"
1848
- ></mux-player>
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="list-label">Quick setup</p>
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--console">
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 class="guide-step">
1856
- <span class="guide-step-num">1</span>
1857
- <div class="guide-step-body">
1858
- <p class="guide-step-title">Set up project, auth cluster &amp; member</p>
1859
- <p class="guide-step-desc">Create a project, add an authentication cluster for how members sign in, then invite or add a member so they can be issued an access token</p>
1860
- </div>
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 class="guide-step">
1863
- <span class="guide-step-num">2</span>
1864
- <div class="guide-step-body">
1865
- <p class="guide-step-title">Issue an access token</p>
1866
- <p class="guide-step-desc">Open the member detail page and issue a Member Access Token (MAT) for the agent</p>
1867
- </div>
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 class="guide-step">
1876
- <span class="guide-step-num">3</span>
1877
- <div class="guide-step-body">
1878
- <p class="guide-step-title">Save the token</p>
1879
- <p class="guide-step-desc">Paste the token in the Tokens tab with a label (e.g. <code>transcodes-myapp-dev</code>) \u2014 the plugin reads it from <code>{{HOME_DIR}}/.transcodes/config.json</code></p>
1880
- </div>
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 class="guide-step">
1883
- <span class="guide-step-num">4</span>
1884
- <div class="guide-step-body">
1885
- <p class="guide-step-title">Configure &amp; view RBAC</p>
1886
- <p class="guide-step-desc">In the Transcodes app Role Management, add resources (key, name, description), create roles, and set the permission matrix \u2014 0 deny \xB7 1 allow \xB7 2 step-up MFA. Then open the RBAC tab here to inspect them read-only \u2014 create, update, and delete stay in the app (logo or Transcodes at the top)</p>
1887
- </div>
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
- </ol>
1890
- </section>
1891
-
1892
- <section class="guide-group guide-group--agent">
1893
- <a class="guide-group-label" href="${TRANSCODES_GUARD_REPO_URL}" target="_blank" rel="noopener noreferrer">LLM Agent or Admin Back Office<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>
1894
- <ol class="guide-steps">
1895
- <li class="guide-step">
1896
- <span class="guide-step-num">5</span>
1897
- <div class="guide-step-body">
1898
- <p class="guide-step-title">Add the transcodes-guard plugin</p>
1899
- <p class="guide-step-desc">Install the transcodes-guard plugin on Claude, Codex, Cursor (beta), and Antigravity (beta). For more plugins info, visit the repository</p>
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 class="guide-step">
1909
- <span class="guide-step-num">6</span>
1910
- <div class="guide-step-body">
1911
- <p class="guide-step-title">Prompt-driven permissions</p>
1912
- <p class="guide-step-desc">Once installed, when you give the agent a command, Transcodes AI interprets the prompt using the role, resource, and action settings you configured and determines the permission \u2014 deny, allow, or step-up MFA</p>
1913
- </div>
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 class="guide-step">
1916
- <span class="guide-step-num">7</span>
1917
- <div class="guide-step-body">
1918
- <p class="guide-step-title">Ask the agent for log history</p>
1919
- <p class="guide-step-desc">After setup, ask your installed AI agent \u2014 such as Claude, ChatGPT, or Antigravity \u2014 to pull member audit / security log history. Transcodes AI maps the prompt to the right resource and enforces RBAC</p>
1920
- <div class="guide-help-examples">
1921
- <code class="cli-cmd">/transcodes show log history for the past 2 weeks</code>
1922
- <code class="cli-cmd">/transcodes summarize member audit logs for the past 2 weeks</code>
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
- </div>
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 &lt;token&gt; -l &lt;label&gt;</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">Paste the token from your Transcodes console member detail page</p>
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">Saved to <code>{{HOME_DIR}}/.transcodes/config.json</code><br />Press Ctrl+C in the terminal to stop</p>
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">Token file: <code>{{HOME_DIR}}/.transcodes/config.json</code></p>
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 on the Tokens tab first</p>
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
- <p class="guide-help-heading">How it works</p>
1979
- <p class="guide-help-line">Transcodes AI interprets the prompt and maps it to a resource / action pair. Based on the member's role, it applies deny / allow / step-up from the permission matrix. Resources are matched by name and description \u2014 if nothing matches, it falls back to <code>system</code>. When integrating with your own back office, you can pass <code>resource</code> and <code>action</code> as params to control permissions directly.</p>
1980
- <p class="guide-help-heading">Action Classification</p>
1981
- <ul class="guide-help-list">
1982
- <li><strong>read</strong> \u2014 inspect data without changing it (list, get, query, cat, grep, status, describe)</li>
1983
- <li><strong>update</strong> \u2014 modify existing data (edit, patch, set, rename, move, config change)</li>
1984
- <li><strong>delete</strong> \u2014 remove or destroy data (rm, drop, truncate, purge)</li>
1985
- <li><strong>create</strong> \u2014 any other state-changing action (new records, sends, posts, uploads, installs)</li>
1986
- </ul>
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="https://app.transcodes.io/" target="_blank" rel="noopener noreferrer">Edit RBAC permissions</a></p>
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="https://app.transcodes.io/" target="_blank" rel="noopener noreferrer">Configure RBAC permissions</a></p>
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">Ver ${CLI_VERSION} <code class="cli-cmd">transcodes version</code></p>
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 manageAuthBtn = document.getElementById("manage-auth-btn");
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
- manageAuthBtn.addEventListener("click", async () => {
2174
- manageAuthBtn.disabled = true;
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 openBrowser2(url) {
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 waitForDashboardShutdown(server) {
2598
- return new Promise((resolve) => {
2599
- let shuttingDown = false;
2600
- let forceTimer;
2601
- const cleanup = () => {
2602
- if (forceTimer) clearTimeout(forceTimer);
2603
- process.removeListener("SIGINT", onSignal);
2604
- process.removeListener("SIGTERM", onSignal);
2605
- };
2606
- const finish = () => {
2607
- cleanup();
2608
- resolve();
2609
- };
2610
- const forceExit = () => {
2611
- cleanup();
2612
- process.stderr.write("\nForce stopping dashboard.\n");
2613
- process.exit(0);
2614
- };
2615
- const onSignal = () => {
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
- shuttingDown = true;
2621
- process.stderr.write("\nStopping dashboard\u2026\n");
2622
- forceTimer = setTimeout(forceExit, 1500);
2623
- forceTimer.unref();
2624
- server.closeAllConnections?.();
2625
- server.close((err) => {
2626
- if (err) {
2627
- process.stderr.write(
2628
- `Shutdown error: ${err instanceof Error ? err.message : String(err)}
2629
- `
2630
- );
2631
- }
2632
- finish();
2633
- });
2634
- };
2635
- process.on("SIGINT", onSignal);
2636
- process.on("SIGTERM", onSignal);
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 runDashboard(options) {
2640
- const preferred = options.port ?? DEFAULT_PORT;
2641
- let server;
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 < 10; attempt++) {
3126
+ for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt++) {
2644
3127
  try {
2645
- server = await listen(port);
2646
- break;
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
- if (!server) {
2657
- const last = preferred + 9;
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
- A previous dashboard is probably still running.
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
- const url = `http://${HOST}:${port}/`;
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
- `Transcodes dashboard running at ${url}
2671
- Config file: ${transcodesConfigFile()}
2672
- Press Ctrl+C to stop
3519
+ `${t("dashboardOpened", { url: running.url })}
3520
+ ${t("dashboardHowToUse")}
3521
+ ${t("dashboardStopHint")}
2673
3522
  `
2674
3523
  );
2675
- if (options.open !== false) {
2676
- openBrowser2(url);
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 waitForDashboardShutdown(server);
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
- async function cmdDashboard(args) {
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 runDashboard({ port, open });
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":