@bigstrider/transcodes-cli 0.7.0 → 0.9.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 (2) hide show
  1. package/dist/index.js +1330 -118
  2. package/package.json +1 -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 path3 = 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}${path3}${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) {
@@ -495,6 +492,14 @@ async function openConsoleSession(options) {
495
492
 
496
493
  // src/commands.ts
497
494
  var CLI_COMMAND_SPECS = [
495
+ {
496
+ usage: "transcodes install",
497
+ description: "Guided setup: pick host plugins to install, save a token, then open the dashboard (--all / <platforms>)"
498
+ },
499
+ {
500
+ usage: "transcodes update",
501
+ description: "Update installed host plugins in place and bump this CLI from npm (--cli-only / --plugins-only / --all)"
502
+ },
498
503
  {
499
504
  usage: "transcodes",
500
505
  description: "Open the local dashboard (URL printed in terminal; default port 3847; --port N / --no-open)"
@@ -557,11 +562,12 @@ function renderCliCommandsHtml() {
557
562
  }
558
563
 
559
564
  // src/dashboard.ts
560
- import { spawn as spawn2 } from "child_process";
565
+ import { execFile, spawn as spawn2 } from "child_process";
561
566
  import { createHash } from "crypto";
562
567
  import {
563
568
  createServer
564
569
  } from "http";
570
+ import { promisify } from "util";
565
571
 
566
572
  // ../packages/core/dist/contract/messages.js
567
573
  var TRANSCODES_GUARD_REPO_URL = "https://github.com/transcodings/transcodes-guard";
@@ -619,7 +625,10 @@ var CLI_VERSION = pkg.version;
619
625
  // src/dashboard.ts
620
626
  var DEFAULT_PORT = 3847;
621
627
  var HOST = "127.0.0.1";
622
- var GUIDELINE_MUX_PLAYBACK_ID = "01Q02da00mR6xobESjn2KHOz31eGX3OUS01xoc025RKQxsR4";
628
+ var PORT_ATTEMPTS = 10;
629
+ var GUIDELINE_MUX_PLAYBACK_ID = "h1vOCPmFDA02fGhcout01FZWD4lpKNTzLFk7vybxvrc3M";
630
+ var APP_ORG_URL = "https://app.transcodes.io/en/org";
631
+ var execFileAsync = promisify(execFile);
623
632
  function fingerprint(token) {
624
633
  return createHash("sha256").update(token).digest("hex").slice(0, 12);
625
634
  }
@@ -827,6 +836,24 @@ function dashboardHtml() {
827
836
  font-weight: 600;
828
837
  line-height: 1.45;
829
838
  }
839
+ .header-token-empty-title { margin: 0; }
840
+ .header-token-empty-cmds {
841
+ margin: 10px 0 0;
842
+ display: flex;
843
+ flex-direction: column;
844
+ align-items: flex-start;
845
+ gap: 6px;
846
+ }
847
+ .header-token-empty-cmds code {
848
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
849
+ font-size: var(--text-2xs);
850
+ font-weight: 600;
851
+ color: var(--accent);
852
+ background: #fff;
853
+ border: 1px solid #f5c6cb;
854
+ padding: 3px 9px;
855
+ border-radius: 7px;
856
+ }
830
857
  .header-profile {
831
858
  display: flex;
832
859
  align-items: center;
@@ -862,6 +889,9 @@ function dashboardHtml() {
862
889
  }
863
890
  .btn-manage-auth {
864
891
  flex-shrink: 0;
892
+ display: inline-flex;
893
+ align-items: center;
894
+ gap: 6px;
865
895
  border: none;
866
896
  border-radius: 10px;
867
897
  padding: 10px 16px;
@@ -872,6 +902,11 @@ function dashboardHtml() {
872
902
  cursor: pointer;
873
903
  transition: opacity 0.15s ease, transform 0.15s ease;
874
904
  }
905
+ .btn-manage-auth svg {
906
+ width: 20px;
907
+ height: 20px;
908
+ flex-shrink: 0;
909
+ }
875
910
  .btn-manage-auth:hover:not(:disabled) { opacity: 0.92; }
876
911
  .btn-manage-auth:disabled {
877
912
  opacity: 0.55;
@@ -958,6 +993,12 @@ function dashboardHtml() {
958
993
  color: var(--muted);
959
994
  margin: 0 0 16px;
960
995
  }
996
+ .section-sub a {
997
+ color: var(--accent);
998
+ font-weight: 600;
999
+ text-decoration: none;
1000
+ }
1001
+ .section-sub a:hover { text-decoration: underline; }
961
1002
  .cli-map-row {
962
1003
  display: flex;
963
1004
  align-items: center;
@@ -1217,8 +1258,8 @@ function dashboardHtml() {
1217
1258
  .list-label {
1218
1259
  margin: 26px 0 10px;
1219
1260
  font-size: var(--text-sm);
1220
- font-weight: 600;
1221
- color: var(--muted);
1261
+ font-weight: 700;
1262
+ color: var(--ink);
1222
1263
  letter-spacing: 0.01em;
1223
1264
  }
1224
1265
  .token-list { display: flex; flex-direction: column; gap: 10px; }
@@ -1518,8 +1559,34 @@ function dashboardHtml() {
1518
1559
  line-height: 1.5;
1519
1560
  margin: 0;
1520
1561
  }
1521
- .guide-video {
1562
+ .guide-video-wrap {
1522
1563
  margin: 0 0 22px;
1564
+ }
1565
+ .guide-video-toggle {
1566
+ display: inline-flex;
1567
+ align-items: center;
1568
+ gap: 6px;
1569
+ border: 1px solid var(--line);
1570
+ border-radius: 999px;
1571
+ padding: 6px 12px;
1572
+ font-size: var(--text-xs);
1573
+ font-weight: 600;
1574
+ color: var(--ink);
1575
+ background: #fff;
1576
+ cursor: pointer;
1577
+ transition: background 0.15s ease, border-color 0.15s ease;
1578
+ }
1579
+ .guide-video-toggle:hover {
1580
+ background: #f7f7f9;
1581
+ border-color: #d8d8de;
1582
+ }
1583
+ .guide-video-toggle[aria-expanded="true"] {
1584
+ color: var(--accent);
1585
+ border-color: rgba(91, 84, 230, 0.35);
1586
+ background: var(--accent-soft);
1587
+ }
1588
+ .guide-video {
1589
+ margin: 10px 0 0;
1523
1590
  border-radius: 14px;
1524
1591
  overflow: hidden;
1525
1592
  border: 1px solid var(--line);
@@ -1527,6 +1594,7 @@ function dashboardHtml() {
1527
1594
  aspect-ratio: 16 / 9;
1528
1595
  box-shadow: 0 8px 28px rgba(22, 22, 26, 0.08);
1529
1596
  }
1597
+ .guide-video[hidden] { display: none !important; }
1530
1598
  .guide-video mux-player {
1531
1599
  width: 100%;
1532
1600
  height: 100%;
@@ -1592,6 +1660,37 @@ function dashboardHtml() {
1592
1660
  border: 1px solid var(--line);
1593
1661
  border-radius: 14px;
1594
1662
  }
1663
+ .guide-step--accordion {
1664
+ display: block;
1665
+ padding: 0;
1666
+ overflow: hidden;
1667
+ }
1668
+ .guide-step-summary {
1669
+ display: flex;
1670
+ align-items: center;
1671
+ gap: 14px;
1672
+ padding: 14px 16px;
1673
+ cursor: pointer;
1674
+ list-style: none;
1675
+ user-select: none;
1676
+ }
1677
+ .guide-step-summary::-webkit-details-marker { display: none; }
1678
+ .guide-step-summary::marker { content: ''; }
1679
+ .guide-step-summary:hover { background: #f7f7f9; }
1680
+ .guide-step--accordion[open] .guide-step-summary {
1681
+ border-bottom: 1px solid var(--line);
1682
+ }
1683
+ .guide-step-chevron {
1684
+ margin-left: auto;
1685
+ flex-shrink: 0;
1686
+ width: 16px;
1687
+ height: 16px;
1688
+ color: var(--muted);
1689
+ transition: transform 0.15s ease;
1690
+ }
1691
+ .guide-step--accordion[open] .guide-step-chevron {
1692
+ transform: rotate(180deg);
1693
+ }
1595
1694
  .guide-step-num {
1596
1695
  flex: 0 0 28px;
1597
1696
  width: 28px;
@@ -1606,18 +1705,35 @@ function dashboardHtml() {
1606
1705
  justify-content: center;
1607
1706
  }
1608
1707
  .guide-step-body { min-width: 0; }
1708
+ .guide-step--accordion > .guide-step-body {
1709
+ padding: 12px 16px 14px 58px;
1710
+ }
1609
1711
  .guide-step-title {
1610
1712
  font-size: var(--text-sm);
1611
1713
  font-weight: 700;
1612
1714
  color: var(--ink);
1613
1715
  margin: 0 0 4px;
1614
1716
  }
1717
+ .guide-step-summary .guide-step-title { margin: 0; flex: 1; min-width: 0; }
1615
1718
  .guide-step-desc {
1616
1719
  font-size: var(--text-sm);
1617
1720
  color: var(--muted);
1618
1721
  line-height: 1.5;
1619
1722
  margin: 0;
1620
1723
  }
1724
+ .guide-step-desc a,
1725
+ .guide-console-link {
1726
+ color: var(--accent);
1727
+ font-weight: 600;
1728
+ text-decoration: underline;
1729
+ background: none;
1730
+ border: none;
1731
+ padding: 0;
1732
+ font: inherit;
1733
+ cursor: pointer;
1734
+ }
1735
+ .guide-step-desc a:hover,
1736
+ .guide-console-link:hover { opacity: 0.88; }
1621
1737
  .guide-step-desc code {
1622
1738
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1623
1739
  font-size: var(--text-2xs);
@@ -1711,12 +1827,56 @@ function dashboardHtml() {
1711
1827
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1712
1828
  font-size: var(--text-xs);
1713
1829
  }
1830
+ .guide-prefix-note {
1831
+ margin: 0 0 14px;
1832
+ font-size: var(--text-sm);
1833
+ color: var(--muted);
1834
+ line-height: 1.6;
1835
+ }
1836
+ .guide-prefix-note code.cli-cmd { white-space: nowrap; }
1714
1837
  .guide-help-examples {
1715
1838
  margin: 10px 0 0;
1716
1839
  display: flex;
1717
1840
  flex-direction: column;
1718
1841
  gap: 8px;
1719
1842
  }
1843
+ .guide-cmd-row {
1844
+ display: flex;
1845
+ flex-direction: column;
1846
+ gap: 4px;
1847
+ }
1848
+ .guide-cmd-hosts {
1849
+ font-size: var(--text-2xs);
1850
+ font-weight: 600;
1851
+ color: var(--muted);
1852
+ text-transform: uppercase;
1853
+ letter-spacing: 0.04em;
1854
+ }
1855
+ .guide-classify-list {
1856
+ margin: 10px 0 0;
1857
+ padding: 0;
1858
+ list-style: none;
1859
+ display: flex;
1860
+ flex-direction: column;
1861
+ gap: 8px;
1862
+ }
1863
+ .guide-classify-list li {
1864
+ display: flex;
1865
+ align-items: center;
1866
+ flex-wrap: wrap;
1867
+ gap: 8px;
1868
+ font-size: var(--text-sm);
1869
+ color: var(--muted);
1870
+ line-height: 1.5;
1871
+ }
1872
+ .guide-classify-prompt { color: var(--ink); }
1873
+ .guide-classify-arrow { color: var(--muted); }
1874
+ .guide-classify-note {
1875
+ margin: 10px 0 0;
1876
+ font-size: var(--text-sm);
1877
+ color: var(--muted);
1878
+ line-height: 1.55;
1879
+ }
1720
1880
  .guide-help-examples code.cli-cmd {
1721
1881
  display: block;
1722
1882
  width: fit-content;
@@ -1776,12 +1936,27 @@ function dashboardHtml() {
1776
1936
  display: flex;
1777
1937
  align-items: center;
1778
1938
  justify-content: center;
1939
+ flex-wrap: wrap;
1779
1940
  gap: 8px;
1780
1941
  font-size: var(--text-2xs);
1781
1942
  color: var(--muted);
1782
1943
  text-align: center;
1783
1944
  line-height: 1.6;
1784
1945
  }
1946
+ .dashboard-footer a {
1947
+ display: inline-flex;
1948
+ align-items: center;
1949
+ gap: 5px;
1950
+ color: var(--accent);
1951
+ font-weight: 600;
1952
+ text-decoration: none;
1953
+ }
1954
+ .dashboard-footer a:hover { text-decoration: underline; }
1955
+ .dashboard-footer-github-icon {
1956
+ width: 14px;
1957
+ height: 14px;
1958
+ flex-shrink: 0;
1959
+ }
1785
1960
  </style>
1786
1961
  <script type="module" src="https://cdn.jsdelivr.net/npm/@mux/mux-player"></script>
1787
1962
  </head>
@@ -1803,14 +1978,26 @@ function dashboardHtml() {
1803
1978
  <p class="header-tagline">Manage credentials and view RBAC from one panel \u2014 no CLI typing required</p>
1804
1979
  </div>
1805
1980
  </div>
1806
- <p id="header-token-empty" class="header-token-empty" hidden>Please register a token first</p>
1981
+ <div id="header-token-empty" class="header-token-empty" hidden>
1982
+ <p class="header-token-empty-title">Invalid Token Configuration. Please register a Transcodes Member Access Token</p>
1983
+ <p class="header-token-empty-title">To get started, run the following commands:</p>
1984
+ <div class="header-token-empty-cmds">
1985
+ <code>npm install -g @bigstrider/transcodes-cli</code>
1986
+ <code>transcodes install</code>
1987
+ </div>
1988
+ </div>
1807
1989
  <div class="header-profile" id="header-profile" hidden>
1808
1990
  <div class="header-profile-info">
1809
1991
  <div class="header-profile-name" id="header-profile-name"></div>
1810
1992
  <div class="header-profile-meta" id="header-profile-meta"></div>
1811
1993
  </div>
1812
1994
  <div class="header-profile-actions">
1813
- <button type="button" class="btn-manage-auth" id="manage-auth-btn">Manage Auth</button>
1995
+ <button type="button" class="btn-manage-auth" id="manage-auth-btn" data-console-open aria-label="Open Transcodes console">
1996
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
1997
+ <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" />
1998
+ </svg>
1999
+ Console
2000
+ </button>
1814
2001
  <p class="header-profile-cli-hint"><code>transcodes console</code></p>
1815
2002
  </div>
1816
2003
  </div>
@@ -1824,108 +2011,147 @@ function dashboardHtml() {
1824
2011
 
1825
2012
  <div class="panel active" id="panel-guideline">
1826
2013
  <p class="section-title">Getting Started</p>
1827
- <p class="section-sub">New to Transcodes? Watch the walkthrough, then follow the steps below</p>
1828
- <div class="guide-help">
1829
- <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>
1830
- <div class="guide-help-examples">
1831
- <code class="cli-cmd">/transcodes add signin / stepup / console in our admin dashboard</code>
1832
- <code class="cli-cmd">/transcodes summarize member audit logs for the past 2 weeks</code>
2014
+ <p class="section-sub">Transcodes CLI Dashboard,Set it up visually \u2014 no terminal required. New to Transcodes? Watch the video.</p>
2015
+ <div class="guide-video-wrap">
2016
+ <button type="button" class="guide-video-toggle" id="guide-video-toggle" aria-expanded="false" aria-controls="guide-video">
2017
+ Watch Intro Video
2018
+ </button>
2019
+ <div class="guide-video" id="guide-video" hidden>
2020
+ <mux-player
2021
+ playback-id="${GUIDELINE_MUX_PLAYBACK_ID}"
2022
+ stream-type="on-demand"
2023
+ accent-color="#5b54e6"
2024
+ primary-color="#ffffff"
2025
+ metadata-video-title="Transcodes CLI onboarding"
2026
+ ></mux-player>
1833
2027
  </div>
1834
2028
  </div>
1835
- <div class="guide-video">
1836
- <mux-player
1837
- playback-id="${GUIDELINE_MUX_PLAYBACK_ID}"
1838
- stream-type="on-demand"
1839
- accent-color="#5b54e6"
1840
- primary-color="#ffffff"
1841
- metadata-video-title="Transcodes CLI onboarding"
1842
- ></mux-player>
1843
- </div>
1844
- <p class="list-label">Quick setup</p>
2029
+ <p class="list-label">Quick Demo</p>
2030
+ <p class="guide-prefix-note">Try the quick demo to see how Transcodes works</p>
2031
+ <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>
1845
2032
  <div class="guide-groups">
1846
- <section class="guide-group guide-group--console">
1847
- <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>
2033
+ <section class="guide-group guide-group--panel">
1848
2034
  <ol class="guide-steps">
1849
2035
  <li class="guide-step">
1850
2036
  <span class="guide-step-num">1</span>
1851
2037
  <div class="guide-step-body">
1852
- <p class="guide-step-title">Create a project</p>
1853
- <p class="guide-step-desc">Create a new project for your app</p>
2038
+ <p class="guide-step-title">Open your CLI or desktop app (Claude, Cursor, Antigravity, ChatGPT)</p>
2039
+ <p class="guide-step-desc">If it's already running, restart it so the latest plugin and token are loaded</p>
1854
2040
  </div>
1855
2041
  </li>
1856
2042
  <li class="guide-step">
1857
2043
  <span class="guide-step-num">2</span>
1858
2044
  <div class="guide-step-body">
1859
- <p class="guide-step-title">Create an authentication cluster</p>
1860
- <p class="guide-step-desc">Add an authentication cluster to define how members sign in and authenticate</p>
1861
- </div>
1862
- </li>
1863
- <li class="guide-step">
1864
- <span class="guide-step-num">3</span>
1865
- <div class="guide-step-body">
1866
- <p class="guide-step-title">Add a member</p>
1867
- <p class="guide-step-desc">Invite or add a member so they can be issued an access token</p>
1868
- </div>
1869
- </li>
1870
- <li class="guide-step">
1871
- <span class="guide-step-num">4</span>
1872
- <div class="guide-step-body">
1873
- <p class="guide-step-title">Issue an access token</p>
1874
- <p class="guide-step-desc">Open the member detail page and issue a Member Access Token (MAT) for the agent</p>
1875
- </div>
1876
- </li>
1877
- <li class="guide-step">
1878
- <span class="guide-step-num">5</span>
1879
- <div class="guide-step-body">
1880
- <p class="guide-step-title">Configure RBAC</p>
1881
- <p class="guide-step-desc">In 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</p>
2045
+ <p class="guide-step-title">Trigger a step-up test</p>
2046
+ <p class="guide-step-desc">Ask the agent to run a protected action \u2014 it should prompt for step-up authentication</p>
2047
+ <div class="guide-help-examples">
2048
+ <div class="guide-cmd-row">
2049
+ <span class="guide-cmd-hosts">Claude \xB7 Cursor \xB7 Antigravity</span>
2050
+ <code class="cli-cmd">/transcodes open step-up authentication for testing</code>
2051
+ </div>
2052
+ <div class="guide-cmd-row">
2053
+ <span class="guide-cmd-hosts">ChatGPT (Codex)</span>
2054
+ <code class="cli-cmd">$transcodes open step-up authentication for testing</code>
2055
+ </div>
2056
+ </div>
1882
2057
  </div>
1883
2058
  </li>
1884
2059
  </ol>
1885
2060
  </section>
2061
+ </div>
1886
2062
 
2063
+ <p class="list-label">Steps</p>
2064
+ <div class="guide-groups">
1887
2065
  <section class="guide-group guide-group--panel">
1888
- <p class="guide-group-label">This CLI panel</p>
1889
2066
  <ol class="guide-steps">
1890
- <li class="guide-step">
1891
- <span class="guide-step-num">6</span>
1892
- <div class="guide-step-body">
1893
- <p class="guide-step-title">Save the token</p>
1894
- <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>
1895
- </div>
2067
+ <li>
2068
+ <details class="guide-step guide-step--accordion">
2069
+ <summary class="guide-step-summary">
2070
+ <span class="guide-step-num">1</span>
2071
+ <span class="guide-step-title">Configure RBAC in the Transcodes app</span>
2072
+ <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>
2073
+ </summary>
2074
+ <div class="guide-step-body">
2075
+ <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>
2076
+ </div>
2077
+ </details>
1896
2078
  </li>
1897
- <li class="guide-step">
1898
- <span class="guide-step-num">7</span>
1899
- <div class="guide-step-body">
1900
- <p class="guide-step-title">View RBAC (read-only)</p>
1901
- <p class="guide-step-desc">Open the RBAC tab to inspect resources and role permission matrices. Create, update, and delete stay in the Transcodes app \u2014 click the logo or Transcodes at the top</p>
1902
- </div>
2079
+ <li>
2080
+ <details class="guide-step guide-step--accordion">
2081
+ <summary class="guide-step-summary">
2082
+ <span class="guide-step-num">2</span>
2083
+ <span class="guide-step-title">Register a passkey or biometrics</span>
2084
+ <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>
2085
+ </summary>
2086
+ <div class="guide-step-body">
2087
+ <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>
2088
+ </div>
2089
+ </details>
1903
2090
  </li>
1904
- </ol>
1905
- </section>
1906
-
1907
- <section class="guide-group guide-group--agent">
1908
- <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>
1909
- <ol class="guide-steps">
1910
- <li class="guide-step">
1911
- <span class="guide-step-num">8</span>
1912
- <div class="guide-step-body">
1913
- <p class="guide-step-title">Add the transcodes-guard plugin</p>
1914
- <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>
1915
- <p class="guide-step-opensource">
1916
- <a class="guide-opensource-link" href="${TRANSCODES_GUARD_REPO_URL}" target="_blank" rel="noopener noreferrer" aria-label="transcodes-guard on GitHub">
1917
- <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>
1918
- We support Open Source, <span class="guide-opensource-accent">More Info</span>
1919
- </a>
1920
- </p>
1921
- </div>
2091
+ <li>
2092
+ <details class="guide-step guide-step--accordion">
2093
+ <summary class="guide-step-summary">
2094
+ <span class="guide-step-num">3</span>
2095
+ <span class="guide-step-title">Open your CLI or desktop app (Claude, Cursor, Antigravity, ChatGPT)</span>
2096
+ <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>
2097
+ </summary>
2098
+ <div class="guide-step-body">
2099
+ <p class="guide-step-desc">If it's already running, restart it so the latest plugin and token are loaded</p>
2100
+ </div>
2101
+ </details>
1922
2102
  </li>
1923
- <li class="guide-step">
1924
- <span class="guide-step-num">9</span>
1925
- <div class="guide-step-body">
1926
- <p class="guide-step-title">Prompt-driven permissions</p>
1927
- <p class="guide-step-desc">Once installed, Transcodes AI interprets the prompts you send using the role, resource, and action settings you configured and enforces permissions accordingly</p>
1928
- </div>
2103
+ <li>
2104
+ <details class="guide-step guide-step--accordion">
2105
+ <summary class="guide-step-summary">
2106
+ <span class="guide-step-num">4</span>
2107
+ <span class="guide-step-title">Run an action from the agent</span>
2108
+ <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>
2109
+ </summary>
2110
+ <div class="guide-step-body">
2111
+ <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>
2112
+ <ul class="guide-classify-list">
2113
+ <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>
2114
+ <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>
2115
+ <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>
2116
+ </ul>
2117
+ <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>
2118
+ </div>
2119
+ </details>
2120
+ </li>
2121
+ <li>
2122
+ <details class="guide-step guide-step--accordion">
2123
+ <summary class="guide-step-summary">
2124
+ <span class="guide-step-num">5</span>
2125
+ <span class="guide-step-title">Pull the audit log report</span>
2126
+ <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>
2127
+ </summary>
2128
+ <div class="guide-step-body">
2129
+ <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>
2130
+ <p class="guide-step-desc">Every action is logged \u2014 whether it was 0 deny, 1 allow, or 2 step-up MFA.</p>
2131
+ <div class="guide-help-examples">
2132
+ <div class="guide-cmd-row">
2133
+ <span class="guide-cmd-hosts">Claude \xB7 Cursor \xB7 Antigravity</span>
2134
+ <code class="cli-cmd">/transcodes show audit logs report</code>
2135
+ </div>
2136
+ <div class="guide-cmd-row">
2137
+ <span class="guide-cmd-hosts">ChatGPT (Codex)</span>
2138
+ <code class="cli-cmd">$transcodes show audit logs report</code>
2139
+ </div>
2140
+ </div>
2141
+ </div>
2142
+ </details>
2143
+ </li>
2144
+ <li>
2145
+ <details class="guide-step guide-step--accordion">
2146
+ <summary class="guide-step-summary">
2147
+ <span class="guide-step-num">6</span>
2148
+ <span class="guide-step-title">Receive step-up links and notifications on Slack or Discord</span>
2149
+ <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>
2150
+ </summary>
2151
+ <div class="guide-step-body">
2152
+ <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>
2153
+ </div>
2154
+ </details>
1929
2155
  </li>
1930
2156
  </ol>
1931
2157
  </section>
@@ -1977,7 +2203,7 @@ function dashboardHtml() {
1977
2203
  </div>
1978
2204
 
1979
2205
  <div class="panel" id="panel-rbac">
1980
- <p id="rbac-token-warning" class="rbac-token-warning" hidden>Save a token on the Tokens tab first</p>
2206
+ <p id="rbac-token-warning" class="rbac-token-warning" hidden>Save a Transcodes token(MAT) first</p>
1981
2207
  <div class="guide-help">
1982
2208
  <p class="guide-help-heading">How it works</p>
1983
2209
  <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>
@@ -2002,7 +2228,7 @@ function dashboardHtml() {
2002
2228
  <button type="button" class="tab" data-rbac="roles" role="tab">Roles</button>
2003
2229
  </div>
2004
2230
  <div class="rbac-pane active" id="rbac-pane-resources">
2005
- <p class="section-sub">Configure resources, roles, and permissions directly in the Transcodes app. Click the logo or Transcodes at the top.</p>
2231
+ <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>
2006
2232
  <div class="rbac-table-wrap">
2007
2233
  <table class="rbac-table" id="resources-table">
2008
2234
  <thead><tr><th>Resource</th><th>Description</th></tr></thead>
@@ -2011,7 +2237,7 @@ function dashboardHtml() {
2011
2237
  </div>
2012
2238
  </div>
2013
2239
  <div class="rbac-pane" id="rbac-pane-roles">
2014
- <p class="section-sub">Select a role to view its permission matrix</p>
2240
+ <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>
2015
2241
  <div class="role-picker" id="role-picker"></div>
2016
2242
  <div id="matrix-wrap" hidden>
2017
2243
  <p class="list-label" id="matrix-role-label"></p>
@@ -2035,7 +2261,14 @@ function dashboardHtml() {
2035
2261
  </div>
2036
2262
  </div>
2037
2263
 
2038
- <p class="dashboard-footer">Ver ${CLI_VERSION} <code class="cli-cmd">transcodes version</code></p>
2264
+ <p class="dashboard-footer">
2265
+ Ver ${CLI_VERSION} <code class="cli-cmd">transcodes version</code>
2266
+ <span aria-hidden="true">\xB7</span>
2267
+ <a href="${TRANSCODES_GUARD_REPO_URL}" target="_blank" rel="noopener noreferrer">
2268
+ <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>
2269
+ GitHub
2270
+ </a>
2271
+ </p>
2039
2272
  <script>
2040
2273
  const tokenEl = document.getElementById("token");
2041
2274
  const labelEl = document.getElementById("label");
@@ -2047,7 +2280,7 @@ function dashboardHtml() {
2047
2280
  const headerProfileEl = document.getElementById("header-profile");
2048
2281
  const headerProfileNameEl = document.getElementById("header-profile-name");
2049
2282
  const headerProfileMetaEl = document.getElementById("header-profile-meta");
2050
- const manageAuthBtn = document.getElementById("manage-auth-btn");
2283
+ const APP_ORG_URL = ${JSON.stringify(APP_ORG_URL)};
2051
2284
 
2052
2285
  let lastStatus = { tokens: [], activeMember: null };
2053
2286
  let editingId = null;
@@ -2056,14 +2289,58 @@ function dashboardHtml() {
2056
2289
  return Array.isArray(s.tokens) && s.tokens.length > 0;
2057
2290
  }
2058
2291
 
2292
+ function updateAppDeepLinks(s) {
2293
+ const pid = s && s.activeMember && s.activeMember.projectId;
2294
+ document.querySelectorAll("[data-app-tab]").forEach((a) => {
2295
+ const tab = a.getAttribute("data-app-tab");
2296
+ if (pid && tab) {
2297
+ a.href = APP_ORG_URL + "?pid=" + encodeURIComponent(pid) + "&tab=" + encodeURIComponent(tab);
2298
+ } else {
2299
+ a.href = APP_ORG_URL;
2300
+ }
2301
+ });
2302
+ }
2303
+
2059
2304
  function updateTokenEmptyState() {
2060
2305
  const empty = !hasSavedTokens(lastStatus);
2061
2306
  headerTokenEmptyEl.hidden = !empty;
2062
2307
  if (empty) {
2063
2308
  headerProfileEl.hidden = true;
2309
+ updateAppDeepLinks(lastStatus);
2064
2310
  return;
2065
2311
  }
2066
2312
  renderHeaderProfile(lastStatus);
2313
+ updateAppDeepLinks(lastStatus);
2314
+ }
2315
+
2316
+ async function openConsole() {
2317
+ const consoleBtns = document.querySelectorAll("[data-console-open]");
2318
+ consoleBtns.forEach((btn) => { btn.disabled = true; });
2319
+ try {
2320
+ const res = await fetch("/api/console/open", { method: "POST" });
2321
+ const data = await res.json();
2322
+ if (!res.ok) throw new Error(data.error || "Could not open auth settings");
2323
+ if (data.browserUrl) window.open(data.browserUrl, "_blank", "noopener,noreferrer");
2324
+ showToast("Opening auth settings in your browser", "success");
2325
+ } catch (e) {
2326
+ showToast(e.message || "Failed to open auth settings", "error");
2327
+ } finally {
2328
+ consoleBtns.forEach((btn) => { btn.disabled = false; });
2329
+ }
2330
+ }
2331
+
2332
+ const guideVideoToggle = document.getElementById("guide-video-toggle");
2333
+ const guideVideo = document.getElementById("guide-video");
2334
+ if (guideVideoToggle && guideVideo) {
2335
+ guideVideoToggle.addEventListener("click", () => {
2336
+ const open = guideVideoToggle.getAttribute("aria-expanded") === "true";
2337
+ const next = !open;
2338
+ guideVideoToggle.setAttribute("aria-expanded", String(next));
2339
+ guideVideo.hidden = !next;
2340
+ guideVideoToggle.textContent = next
2341
+ ? "Hide Intro Video"
2342
+ : "Watch Intro Video";
2343
+ });
2067
2344
  }
2068
2345
 
2069
2346
  document.querySelectorAll(".tab[data-tab]").forEach((tab) => {
@@ -2174,19 +2451,8 @@ function dashboardHtml() {
2174
2451
  renderTokens(lastStatus);
2175
2452
  }
2176
2453
 
2177
- manageAuthBtn.addEventListener("click", async () => {
2178
- manageAuthBtn.disabled = true;
2179
- try {
2180
- const res = await fetch("/api/console/open", { method: "POST" });
2181
- const data = await res.json();
2182
- if (!res.ok) throw new Error(data.error || "Could not open auth settings");
2183
- if (data.browserUrl) window.open(data.browserUrl, "_blank", "noopener,noreferrer");
2184
- showToast("Opening auth settings in your browser", "success");
2185
- } catch (e) {
2186
- showToast(e.message || "Failed to open auth settings", "error");
2187
- } finally {
2188
- manageAuthBtn.disabled = false;
2189
- }
2454
+ document.querySelectorAll("[data-console-open]").forEach((btn) => {
2455
+ btn.addEventListener("click", () => { openConsole(); });
2190
2456
  });
2191
2457
 
2192
2458
  async function saveLabel(id) {
@@ -2640,14 +2906,68 @@ function waitForDashboardShutdown(server) {
2640
2906
  process.on("SIGTERM", onSignal);
2641
2907
  });
2642
2908
  }
2643
- async function runDashboard(options) {
2644
- const preferred = options.port ?? DEFAULT_PORT;
2645
- let server;
2909
+ async function pidsListeningOnPorts(from, to) {
2910
+ const pids = /* @__PURE__ */ new Set();
2911
+ if (process.platform === "win32") {
2912
+ for (let port = from; port <= to; port++) {
2913
+ try {
2914
+ const { stdout } = await execFileAsync(
2915
+ "powershell",
2916
+ [
2917
+ "-NoProfile",
2918
+ "-Command",
2919
+ `(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue).OwningProcess`
2920
+ ],
2921
+ { timeout: 5e3 }
2922
+ );
2923
+ for (const line of stdout.split(/\r?\n/)) {
2924
+ const pid = Number(line.trim());
2925
+ if (Number.isInteger(pid) && pid > 0) pids.add(pid);
2926
+ }
2927
+ } catch {
2928
+ }
2929
+ }
2930
+ return [...pids];
2931
+ }
2932
+ for (let port = from; port <= to; port++) {
2933
+ try {
2934
+ const { stdout } = await execFileAsync(
2935
+ "lsof",
2936
+ ["-ti", `tcp:${port}`, "-sTCP:LISTEN"],
2937
+ { timeout: 5e3 }
2938
+ );
2939
+ for (const token of stdout.trim().split(/\s+/)) {
2940
+ const pid = Number(token);
2941
+ if (Number.isInteger(pid) && pid > 0) pids.add(pid);
2942
+ }
2943
+ } catch {
2944
+ }
2945
+ }
2946
+ return [...pids];
2947
+ }
2948
+ async function freePortRange(from, to) {
2949
+ process.stdout.write(`Freeing ports ${from}\u2013${to} \u2026
2950
+ `);
2951
+ const pids = await pidsListeningOnPorts(from, to);
2952
+ if (pids.length === 0) {
2953
+ process.stdout.write(" (no listeners found)\n");
2954
+ return;
2955
+ }
2956
+ for (const pid of pids) {
2957
+ try {
2958
+ process.kill(pid, "SIGKILL");
2959
+ process.stdout.write(` killed pid ${pid}
2960
+ `);
2961
+ } catch {
2962
+ }
2963
+ }
2964
+ }
2965
+ async function tryBindPortRange(preferred) {
2646
2966
  let port = preferred;
2647
- for (let attempt = 0; attempt < 10; attempt++) {
2967
+ for (let attempt = 0; attempt < PORT_ATTEMPTS; attempt++) {
2648
2968
  try {
2649
- server = await listen(port);
2650
- break;
2969
+ const server = await listen(port);
2970
+ return { server, port };
2651
2971
  } catch (err) {
2652
2972
  const code = err.code;
2653
2973
  if (code === "EADDRINUSE") {
@@ -2657,8 +2977,17 @@ async function runDashboard(options) {
2657
2977
  throw err;
2658
2978
  }
2659
2979
  }
2660
- if (!server) {
2661
- const last = preferred + 9;
2980
+ return null;
2981
+ }
2982
+ async function runDashboard(options) {
2983
+ const preferred = options.port ?? DEFAULT_PORT;
2984
+ const last = preferred + PORT_ATTEMPTS - 1;
2985
+ let bound = await tryBindPortRange(preferred);
2986
+ if (!bound) {
2987
+ await freePortRange(preferred, last);
2988
+ bound = await tryBindPortRange(preferred);
2989
+ }
2990
+ if (!bound) {
2662
2991
  throw new Error(
2663
2992
  `could not find a free port in ${preferred}-${last} (all in use).
2664
2993
  A previous dashboard is probably still running.
@@ -2669,6 +2998,7 @@ async function runDashboard(options) {
2669
2998
  Or choose another port: transcodes --port <N>`
2670
2999
  );
2671
3000
  }
3001
+ const { server, port } = bound;
2672
3002
  const url = `http://${HOST}:${port}/`;
2673
3003
  process.stdout.write(
2674
3004
  `Transcodes dashboard running at ${url}
@@ -2682,6 +3012,882 @@ async function runDashboard(options) {
2682
3012
  await waitForDashboardShutdown(server);
2683
3013
  }
2684
3014
 
3015
+ // src/install.ts
3016
+ import { spawn as spawn3 } from "child_process";
3017
+ import fs from "fs";
3018
+ import os2 from "os";
3019
+ import path2 from "path";
3020
+ import { createInterface, moveCursor } from "readline";
3021
+ var REPO_SLUG = "transcodings/transcodes-guard";
3022
+ var REPO_GIT_URL = "https://github.com/transcodings/transcodes-guard.git";
3023
+ var MARKETPLACE = "bigstrider";
3024
+ var PLUGIN_NAME = "transcodes-guard";
3025
+ var APP_CONSOLE_URL = "https://app.transcodes.io";
3026
+ var MIN_NODE_MAJOR = 20;
3027
+ var PLATFORMS = [
3028
+ { id: "claude", label: "Claude Code" },
3029
+ { id: "codex", label: "ChatGPT (Codex)" },
3030
+ {
3031
+ id: "cursor",
3032
+ label: "Cursor",
3033
+ installerRel: "plugins/cursor/install.mjs"
3034
+ },
3035
+ {
3036
+ id: "antigravity",
3037
+ label: "Antigravity",
3038
+ installerRel: "plugins/antigravity/install.mjs"
3039
+ }
3040
+ ];
3041
+ function log(line = "") {
3042
+ process.stdout.write(`${line}
3043
+ `);
3044
+ }
3045
+ function clearScreen() {
3046
+ process.stdout.write("\x1B[2J\x1B[H");
3047
+ }
3048
+ function isTty() {
3049
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
3050
+ }
3051
+ function openUrl(url) {
3052
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
3053
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
3054
+ try {
3055
+ const child = spawn3(opener, args, { stdio: "ignore", detached: true });
3056
+ child.on("error", () => {
3057
+ });
3058
+ child.unref();
3059
+ } catch {
3060
+ }
3061
+ }
3062
+ function refreshPathHints() {
3063
+ const home = os2.homedir();
3064
+ const extras = [
3065
+ path2.join(home, ".local", "bin"),
3066
+ path2.join(home, ".claude", "bin"),
3067
+ path2.join(home, ".codex", "bin"),
3068
+ path2.join(home, ".cursor", "bin"),
3069
+ path2.join(home, ".gemini", "bin"),
3070
+ path2.join(home, "bin"),
3071
+ "/usr/local/bin",
3072
+ "/opt/homebrew/bin"
3073
+ ];
3074
+ const current = process.env.PATH ?? "";
3075
+ const parts = current.split(path2.delimiter).filter(Boolean);
3076
+ const merged = [...extras.filter((p) => !parts.includes(p)), ...parts];
3077
+ process.env.PATH = merged.join(path2.delimiter);
3078
+ }
3079
+ function commandExists(cmd) {
3080
+ return new Promise((resolve) => {
3081
+ const child = process.platform === "win32" ? spawn3("where", [cmd], { stdio: "ignore" }) : spawn3("sh", ["-c", `command -v ${JSON.stringify(cmd)}`], {
3082
+ stdio: "ignore"
3083
+ });
3084
+ child.on("error", () => resolve(false));
3085
+ child.on("close", (code) => resolve(code === 0));
3086
+ });
3087
+ }
3088
+ async function anyCommandExists(cmds) {
3089
+ for (const cmd of cmds) {
3090
+ if (await commandExists(cmd)) return cmd;
3091
+ }
3092
+ return null;
3093
+ }
3094
+ function run(cmd, args) {
3095
+ return new Promise((resolve) => {
3096
+ log(`
3097
+ $ ${cmd} ${args.join(" ")}`);
3098
+ const child = spawn3(cmd, args, { stdio: "inherit", env: process.env });
3099
+ child.on("error", (err) => {
3100
+ log(` (failed to start: ${err.message})`);
3101
+ resolve(1);
3102
+ });
3103
+ child.on("close", (code) => resolve(code ?? 1));
3104
+ });
3105
+ }
3106
+ function runShell(script) {
3107
+ return new Promise((resolve) => {
3108
+ log(`
3109
+ $ ${script}`);
3110
+ const child = spawn3("sh", ["-c", script], {
3111
+ stdio: "inherit",
3112
+ env: process.env
3113
+ });
3114
+ child.on("error", (err) => {
3115
+ log(` (failed to start: ${err.message})`);
3116
+ resolve(1);
3117
+ });
3118
+ child.on("close", (code) => resolve(code ?? 1));
3119
+ });
3120
+ }
3121
+ async function runAll(steps) {
3122
+ for (const step of steps) {
3123
+ const code = await run(step.cmd, step.args);
3124
+ if (code !== 0) return false;
3125
+ }
3126
+ return true;
3127
+ }
3128
+ function nodeMajor(version = process.versions.node) {
3129
+ const major = Number(version.split(".")[0]);
3130
+ return Number.isFinite(major) ? major : 0;
3131
+ }
3132
+ async function ensureNode() {
3133
+ log("\u2500\u2500 Prerequisites \u2500\u2500");
3134
+ const major = nodeMajor();
3135
+ const onPath = await commandExists("node");
3136
+ if (major >= MIN_NODE_MAJOR && onPath) {
3137
+ log(` \u2713 Node.js v${process.versions.node} (>= ${MIN_NODE_MAJOR})`);
3138
+ return true;
3139
+ }
3140
+ if (major >= MIN_NODE_MAJOR && !onPath) {
3141
+ log(
3142
+ ` ! Node.js v${process.versions.node} is running this CLI but \`node\` is not on PATH.`
3143
+ );
3144
+ log(" Prepending common bin dirs and continuing\u2026");
3145
+ refreshPathHints();
3146
+ if (await commandExists("node")) {
3147
+ log(" \u2713 Node.js now on PATH");
3148
+ return true;
3149
+ }
3150
+ }
3151
+ log(
3152
+ 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"
3153
+ );
3154
+ let installed = false;
3155
+ if (process.platform === "darwin" && await commandExists("brew")) {
3156
+ installed = await run("brew", ["install", "node"]) === 0;
3157
+ }
3158
+ if (!installed && await commandExists("nvm")) {
3159
+ installed = await runShell("nvm install --lts && nvm use --lts") === 0;
3160
+ }
3161
+ if (!installed) {
3162
+ const nvmInstall = [
3163
+ "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash",
3164
+ 'export NVM_DIR="$HOME/.nvm"',
3165
+ '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"',
3166
+ "nvm install --lts",
3167
+ 'nvm alias default "lts/*"'
3168
+ ].join(" && ");
3169
+ installed = await runShell(nvmInstall) === 0;
3170
+ }
3171
+ refreshPathHints();
3172
+ const nvmDefaultBin = path2.join(os2.homedir(), ".nvm", "versions", "node");
3173
+ if (fs.existsSync(nvmDefaultBin)) {
3174
+ try {
3175
+ const versions = fs.readdirSync(nvmDefaultBin).filter((v) => v.startsWith("v")).sort().reverse();
3176
+ if (versions[0]) {
3177
+ const bin = path2.join(nvmDefaultBin, versions[0], "bin");
3178
+ process.env.PATH = `${bin}${path2.delimiter}${process.env.PATH ?? ""}`;
3179
+ }
3180
+ } catch {
3181
+ }
3182
+ }
3183
+ if (await commandExists("node")) {
3184
+ const ver = await new Promise((resolve) => {
3185
+ const child = spawn3("node", ["-v"], {
3186
+ stdio: ["ignore", "pipe", "ignore"]
3187
+ });
3188
+ let out = "";
3189
+ child.stdout?.on("data", (c) => {
3190
+ out += c.toString();
3191
+ });
3192
+ child.on("close", () => resolve(out.trim().replace(/^v/, "")));
3193
+ child.on("error", () => resolve(""));
3194
+ });
3195
+ const installedMajor = nodeMajor(ver || "0");
3196
+ if (installedMajor >= MIN_NODE_MAJOR) {
3197
+ log(` \u2713 Node.js v${ver} installed`);
3198
+ return true;
3199
+ }
3200
+ }
3201
+ log("");
3202
+ log("Could not install Node.js LTS automatically.");
3203
+ log(
3204
+ `Install Node.js ${MIN_NODE_MAJOR}+ from https://nodejs.org then re-run:`
3205
+ );
3206
+ log(" transcodes install");
3207
+ openUrl("https://nodejs.org");
3208
+ return false;
3209
+ }
3210
+ var HOST_CLIS = {
3211
+ claude: {
3212
+ binaries: ["claude"],
3213
+ label: "Claude Code CLI (`claude`)",
3214
+ install: async () => {
3215
+ if (process.platform === "win32") {
3216
+ return await run("npm", ["install", "-g", "@anthropic-ai/claude-code"]) === 0;
3217
+ }
3218
+ const native = await runShell("curl -fsSL https://claude.ai/install.sh | bash") === 0;
3219
+ if (native) return true;
3220
+ return await run("npm", ["install", "-g", "@anthropic-ai/claude-code"]) === 0;
3221
+ }
3222
+ },
3223
+ codex: {
3224
+ binaries: ["codex"],
3225
+ label: "Codex CLI (`codex`)",
3226
+ install: async () => {
3227
+ if (process.platform !== "win32") {
3228
+ const native = await runShell(
3229
+ "curl -fsSL https://chatgpt.com/codex/install.sh | sh"
3230
+ ) === 0;
3231
+ if (native) return true;
3232
+ }
3233
+ return await run("npm", ["install", "-g", "@openai/codex"]) === 0;
3234
+ }
3235
+ },
3236
+ cursor: {
3237
+ // Docs: `agent` is primary; `cursor-agent` is the backward-compatible name.
3238
+ binaries: ["cursor-agent", "agent"],
3239
+ label: "Cursor Agent CLI (`cursor-agent` / `agent`)",
3240
+ install: async () => {
3241
+ if (process.platform === "win32") {
3242
+ return await runShell(
3243
+ `powershell -NoProfile -Command "irm 'https://cursor.com/install?win32=true' | iex"`
3244
+ ) === 0;
3245
+ }
3246
+ return await runShell("curl https://cursor.com/install -fsS | bash") === 0;
3247
+ }
3248
+ },
3249
+ antigravity: {
3250
+ binaries: ["agy"],
3251
+ label: "Antigravity CLI (`agy`)",
3252
+ install: async () => await runShell(
3253
+ "curl -fsSL https://antigravity.google/cli/install.sh | bash"
3254
+ ) === 0
3255
+ }
3256
+ };
3257
+ async function ensureHostCli(id) {
3258
+ const spec = HOST_CLIS[id];
3259
+ refreshPathHints();
3260
+ const found = await anyCommandExists(spec.binaries);
3261
+ if (found) {
3262
+ log(` \u2713 ${spec.label} found (\`${found}\`)`);
3263
+ return true;
3264
+ }
3265
+ log(` ${spec.label} not found \u2014 installing\u2026`);
3266
+ const ok = await spec.install();
3267
+ refreshPathHints();
3268
+ const after = await anyCommandExists(spec.binaries);
3269
+ if (ok && after) {
3270
+ log(` \u2713 ${spec.label} installed (\`${after}\`)`);
3271
+ return true;
3272
+ }
3273
+ log(` \u2717 Failed to install ${spec.label}.`);
3274
+ log(" Install it manually, then re-run `transcodes install`.");
3275
+ return false;
3276
+ }
3277
+ function promptLine(question) {
3278
+ return new Promise((resolve) => {
3279
+ const rl = createInterface({
3280
+ input: process.stdin,
3281
+ output: process.stdout
3282
+ });
3283
+ rl.question(question, (answer) => {
3284
+ rl.close();
3285
+ resolve(answer);
3286
+ });
3287
+ });
3288
+ }
3289
+ function safeExists(p) {
3290
+ try {
3291
+ return fs.existsSync(p);
3292
+ } catch {
3293
+ return false;
3294
+ }
3295
+ }
3296
+ function dirHasMatch(dir, needles) {
3297
+ try {
3298
+ return fs.readdirSync(dir).some((entry) => needles.some((n) => entry.includes(n)));
3299
+ } catch {
3300
+ return false;
3301
+ }
3302
+ }
3303
+ function fileHas(file, needle) {
3304
+ try {
3305
+ return fs.readFileSync(file, "utf8").includes(needle);
3306
+ } catch {
3307
+ return false;
3308
+ }
3309
+ }
3310
+ function isPluginInstalled(id) {
3311
+ const home = os2.homedir();
3312
+ switch (id) {
3313
+ case "cursor":
3314
+ return safeExists(
3315
+ path2.join(home, ".cursor", "plugins", "local", "transcodes-guard")
3316
+ );
3317
+ case "antigravity":
3318
+ return safeExists(
3319
+ path2.join(home, ".gemini", "config", "plugins", "transcodes-guard")
3320
+ );
3321
+ case "claude":
3322
+ return dirHasMatch(path2.join(home, ".claude", "plugins"), [
3323
+ "transcodes-guard",
3324
+ "bigstrider"
3325
+ ]) || dirHasMatch(path2.join(home, ".claude", "plugins", "marketplaces"), [
3326
+ "bigstrider",
3327
+ "transcodes-guard"
3328
+ ]) || fileHas(
3329
+ path2.join(home, ".claude", "settings.json"),
3330
+ "transcodes-guard"
3331
+ ) || fileHas(path2.join(home, ".claude.json"), "transcodes-guard");
3332
+ case "codex":
3333
+ return dirHasMatch(path2.join(home, ".codex", "plugins"), [
3334
+ "transcodes-guard",
3335
+ "bigstrider"
3336
+ ]) || fileHas(path2.join(home, ".codex", "config.toml"), "transcodes-guard") || fileHas(path2.join(home, ".codex", "config.json"), "transcodes-guard");
3337
+ }
3338
+ }
3339
+ function renderMenu() {
3340
+ log("");
3341
+ log("Please select platforms that you want to install:");
3342
+ log("Installing a plugin enables both its CLI and desktop app.");
3343
+ log("(Claude only supports the Claude Code tab)");
3344
+ log("");
3345
+ PLATFORMS.forEach((p, i) => {
3346
+ const mark = isPluginInstalled(p.id) ? " [Installed \u2713]" : "";
3347
+ log(` ${i + 1}. ${p.label}${mark}`);
3348
+ });
3349
+ log(` ${PLATFORMS.length + 1}. Next Step \u2192`);
3350
+ log("");
3351
+ log('Enter numbers to install (e.g. 1,2), "all" or Enter to install all.');
3352
+ log(
3353
+ `Type ${PLATFORMS.length + 1} (or "next") for Next Step, "exit"/"q" to quit.`
3354
+ );
3355
+ }
3356
+ async function promptMenu() {
3357
+ const nextNum = String(PLATFORMS.length + 1);
3358
+ for (; ; ) {
3359
+ const answer = (await promptLine("> ")).trim().toLowerCase();
3360
+ if (answer === "exit" || answer === "q") return { kind: "cancel" };
3361
+ if (answer === "next" || answer === "n" || answer === nextNum) {
3362
+ return { kind: "next" };
3363
+ }
3364
+ if (answer === "" || answer === "all" || answer === "a") {
3365
+ return { kind: "install", ids: PLATFORMS.map((p) => p.id) };
3366
+ }
3367
+ const tokens = answer.split(/[\s,]+/).filter(Boolean);
3368
+ const ids = [];
3369
+ let invalid = false;
3370
+ for (const token of tokens) {
3371
+ const num = Number(token);
3372
+ if (!Number.isInteger(num) || num < 1 || num > PLATFORMS.length) {
3373
+ log(
3374
+ ` Invalid choice "${token}". Use 1\u2013${PLATFORMS.length}, ${nextNum} (Next Step), all, or exit.`
3375
+ );
3376
+ invalid = true;
3377
+ break;
3378
+ }
3379
+ const pid = PLATFORMS[num - 1].id;
3380
+ if (!ids.includes(pid)) ids.push(pid);
3381
+ }
3382
+ if (invalid) continue;
3383
+ return { kind: "install", ids };
3384
+ }
3385
+ }
3386
+ function supportsArrowSelect() {
3387
+ return isTty() && typeof process.stdin.setRawMode === "function";
3388
+ }
3389
+ function arrowSelect(checked) {
3390
+ return new Promise((resolve) => {
3391
+ const rowCount = PLATFORMS.length + 1;
3392
+ let cursor = 0;
3393
+ let rendered = 0;
3394
+ const { stdin, stdout } = process;
3395
+ const draw = () => {
3396
+ const lines = [];
3397
+ lines.push("Please select platforms that you want to install:");
3398
+ lines.push("Installing a plugin enables both its CLI and desktop app.");
3399
+ lines.push("(Claude only supports the Claude Code tab)");
3400
+ lines.push("");
3401
+ PLATFORMS.forEach((p, i) => {
3402
+ const pointer = cursor === i ? "\u276F" : " ";
3403
+ const box = checked.has(p.id) ? "\u25C9" : "\u25EF";
3404
+ const mark = isPluginInstalled(p.id) ? " [Installed \u2713]" : "";
3405
+ lines.push(`${pointer} ${box} ${p.label}${mark}`);
3406
+ });
3407
+ const nextPointer = cursor === PLATFORMS.length ? "\u276F" : " ";
3408
+ lines.push(`${nextPointer} Next Step \u2192`);
3409
+ lines.push("");
3410
+ lines.push(
3411
+ "\u2191/\u2193 move \xB7 space select \xB7 a all/none \xB7 enter confirm \xB7 q quit/exit"
3412
+ );
3413
+ if (rendered > 0) moveCursor(stdout, 0, -rendered);
3414
+ const frame = lines.map((line) => `${line}\x1B[K`).join("\n");
3415
+ stdout.write(`${frame}
3416
+ `);
3417
+ rendered = lines.length;
3418
+ };
3419
+ const cleanup = () => {
3420
+ stdout.write("\x1B[?25h");
3421
+ stdin.setRawMode?.(false);
3422
+ stdin.pause();
3423
+ stdin.removeListener("data", onData);
3424
+ };
3425
+ const onData = (buf) => {
3426
+ const key = buf.toString();
3427
+ if (key === "" || key === "q") {
3428
+ cleanup();
3429
+ resolve({ kind: "cancel" });
3430
+ return;
3431
+ }
3432
+ if (key === "\r" || key === "\n") {
3433
+ cleanup();
3434
+ if (cursor === PLATFORMS.length) {
3435
+ resolve({ kind: "next" });
3436
+ } else {
3437
+ resolve({
3438
+ kind: "install",
3439
+ ids: PLATFORMS.filter((p) => checked.has(p.id)).map((p) => p.id)
3440
+ });
3441
+ }
3442
+ return;
3443
+ }
3444
+ if (key === " ") {
3445
+ if (cursor < PLATFORMS.length) {
3446
+ const id = PLATFORMS[cursor].id;
3447
+ if (checked.has(id)) checked.delete(id);
3448
+ else checked.add(id);
3449
+ }
3450
+ } else if (key === "a") {
3451
+ if (PLATFORMS.every((p) => checked.has(p.id))) checked.clear();
3452
+ else for (const p of PLATFORMS) checked.add(p.id);
3453
+ } else if (key === "\x1B[A" || key === "k") {
3454
+ cursor = (cursor - 1 + rowCount) % rowCount;
3455
+ } else if (key === "\x1B[B" || key === "j") {
3456
+ cursor = (cursor + 1) % rowCount;
3457
+ } else {
3458
+ return;
3459
+ }
3460
+ draw();
3461
+ };
3462
+ stdin.setRawMode?.(true);
3463
+ stdin.resume();
3464
+ stdin.setEncoding("utf8");
3465
+ stdin.on("data", onData);
3466
+ stdout.write("\x1B[?25l");
3467
+ draw();
3468
+ });
3469
+ }
3470
+ function arrowChoose(title, options, defaultIndex = 0) {
3471
+ return new Promise((resolve) => {
3472
+ let cursor = Math.max(0, Math.min(defaultIndex, options.length - 1));
3473
+ let rendered = 0;
3474
+ const { stdin, stdout } = process;
3475
+ const draw = () => {
3476
+ const lines = [title];
3477
+ options.forEach((opt, i) => {
3478
+ const pointer = cursor === i ? "\u276F" : " ";
3479
+ const radio = cursor === i ? "\u25C9" : "\u25EF";
3480
+ lines.push(`${pointer} ${radio} ${opt}`);
3481
+ });
3482
+ lines.push("");
3483
+ lines.push("\u2191/\u2193 move \xB7 enter select");
3484
+ if (rendered > 0) moveCursor(stdout, 0, -rendered);
3485
+ stdout.write(`${lines.map((line) => `${line}\x1B[K`).join("\n")}
3486
+ `);
3487
+ rendered = lines.length;
3488
+ };
3489
+ const cleanup = () => {
3490
+ stdout.write("\x1B[?25h");
3491
+ stdin.setRawMode?.(false);
3492
+ stdin.pause();
3493
+ stdin.removeListener("data", onData);
3494
+ };
3495
+ const onData = (buf) => {
3496
+ const key = buf.toString();
3497
+ if (key === "") {
3498
+ cleanup();
3499
+ resolve(-1);
3500
+ return;
3501
+ }
3502
+ if (key === "\r" || key === "\n") {
3503
+ cleanup();
3504
+ resolve(cursor);
3505
+ return;
3506
+ }
3507
+ if (key === "\x1B[A" || key === "k") {
3508
+ cursor = (cursor - 1 + options.length) % options.length;
3509
+ } else if (key === "\x1B[B" || key === "j") {
3510
+ cursor = (cursor + 1) % options.length;
3511
+ } else {
3512
+ return;
3513
+ }
3514
+ draw();
3515
+ };
3516
+ stdin.setRawMode?.(true);
3517
+ stdin.resume();
3518
+ stdin.setEncoding("utf8");
3519
+ stdin.on("data", onData);
3520
+ stdout.write("\x1B[?25l");
3521
+ draw();
3522
+ });
3523
+ }
3524
+ async function chooseTokenAction(question) {
3525
+ const options = ["Yes", "No", "Skip \u2014 token already configured"];
3526
+ if (supportsArrowSelect()) {
3527
+ const idx = await arrowChoose(question, options, 0);
3528
+ return idx === 2 ? "skip" : idx === 0 ? "yes" : "no";
3529
+ }
3530
+ for (; ; ) {
3531
+ log(question);
3532
+ for (let i = 0; i < options.length; i++) {
3533
+ log(` ${i + 1}. ${options[i]}`);
3534
+ }
3535
+ const answer = (await promptLine("> ")).trim().toLowerCase();
3536
+ if (answer === "1" || answer === "y" || answer === "yes") return "yes";
3537
+ if (answer === "2" || answer === "n" || answer === "no") return "no";
3538
+ if (answer === "3" || answer === "skip" || answer === "s") return "skip";
3539
+ log(" Please choose 1, 2, or 3.");
3540
+ }
3541
+ }
3542
+ async function runInstalls(ids) {
3543
+ const clonedRepoDir = await cloneRepoIfNeeded(ids);
3544
+ const results = /* @__PURE__ */ new Map();
3545
+ try {
3546
+ for (const id of ids) {
3547
+ const label = PLATFORMS.find((p) => p.id === id)?.label ?? id;
3548
+ log(`
3549
+ \u2500\u2500 ${label} \u2500\u2500`);
3550
+ results.set(id, await installPlatform(id, clonedRepoDir));
3551
+ }
3552
+ } finally {
3553
+ if (clonedRepoDir) {
3554
+ fs.rmSync(clonedRepoDir, { recursive: true, force: true });
3555
+ }
3556
+ }
3557
+ log("\n\u2500\u2500 Install summary \u2500\u2500");
3558
+ for (const id of ids) {
3559
+ const label = PLATFORMS.find((p) => p.id === id)?.label ?? id;
3560
+ log(` ${results.get(id) ? "\u2713" : "\u2717"} ${label}`);
3561
+ }
3562
+ }
3563
+ async function installPlatform(id, clonedRepoDir) {
3564
+ if (!await ensureHostCli(id)) return false;
3565
+ if (id === "claude") {
3566
+ return runAll([
3567
+ { cmd: "claude", args: ["plugin", "marketplace", "add", REPO_SLUG] },
3568
+ {
3569
+ cmd: "claude",
3570
+ args: [
3571
+ "plugin",
3572
+ "install",
3573
+ `${PLUGIN_NAME}@${MARKETPLACE}`,
3574
+ "--scope",
3575
+ "user"
3576
+ ]
3577
+ }
3578
+ ]);
3579
+ }
3580
+ if (id === "codex") {
3581
+ return runAll([
3582
+ { cmd: "codex", args: ["plugin", "marketplace", "add", REPO_SLUG] },
3583
+ {
3584
+ cmd: "codex",
3585
+ args: ["plugin", "add", `${PLUGIN_NAME}@${MARKETPLACE}`]
3586
+ }
3587
+ ]);
3588
+ }
3589
+ const platform = PLATFORMS.find((p) => p.id === id);
3590
+ if (!clonedRepoDir || !platform?.installerRel) {
3591
+ log(` Skipped ${platform?.label ?? id} \u2014 repo clone unavailable.`);
3592
+ return false;
3593
+ }
3594
+ const installer = path2.join(clonedRepoDir, platform.installerRel);
3595
+ if (!fs.existsSync(installer)) {
3596
+ log(` Skipped ${platform.label} \u2014 installer missing at ${installer}.`);
3597
+ return false;
3598
+ }
3599
+ return await run("node", [installer]) === 0;
3600
+ }
3601
+ async function cloneRepoIfNeeded(platforms) {
3602
+ const needsClone = platforms.some(
3603
+ (id) => PLATFORMS.find((p) => p.id === id)?.installerRel
3604
+ );
3605
+ if (!needsClone) return null;
3606
+ if (!await commandExists("git")) {
3607
+ log("\n`git` not found on PATH \u2014 cannot install Cursor / Antigravity.");
3608
+ log("Install git and re-run, or use the one-liners from the README.");
3609
+ return null;
3610
+ }
3611
+ const tmpDir = fs.mkdtempSync(path2.join(os2.tmpdir(), "tg-install-"));
3612
+ const ok = await run("git", ["clone", "--depth", "1", REPO_GIT_URL, tmpDir]) === 0;
3613
+ if (!ok) {
3614
+ fs.rmSync(tmpDir, { recursive: true, force: true });
3615
+ log("\nRepo clone failed \u2014 skipping Cursor / Antigravity.");
3616
+ return null;
3617
+ }
3618
+ return tmpDir;
3619
+ }
3620
+ async function registerToken() {
3621
+ log("");
3622
+ log("\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");
3623
+ for (let attempt = 0; attempt < 3; attempt++) {
3624
+ const token = (await promptLine("Paste your token: ")).trim();
3625
+ if (!token) {
3626
+ log(" Token is required.");
3627
+ continue;
3628
+ }
3629
+ try {
3630
+ parseMemberAccessToken(token);
3631
+ } catch (err) {
3632
+ log(
3633
+ ` Token rejected: ${err instanceof Error ? err.message : String(err)}`
3634
+ );
3635
+ continue;
3636
+ }
3637
+ let label = "";
3638
+ while (!label) {
3639
+ label = (await promptLine("Label (e.g. transcodes-myproject-prod): ")).trim();
3640
+ if (!label) log(" Label is required.");
3641
+ }
3642
+ writeTokenToFile(token, label);
3643
+ log(`
3644
+ Saved token to ${transcodesConfigFile()} (label=${label}).`);
3645
+ log("");
3646
+ log(
3647
+ "Please restart your CLI or desktop app to activate transcodes plugins."
3648
+ );
3649
+ return true;
3650
+ }
3651
+ log("\nToo many invalid attempts \u2014 run `transcodes install` again later.");
3652
+ return false;
3653
+ }
3654
+ function printSetupComplete() {
3655
+ clearScreen();
3656
+ log("\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");
3657
+ log(" Congratulations!");
3658
+ log(" All setup is completed.");
3659
+ log("\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");
3660
+ log("");
3661
+ log("Plugins are installed and your MAT token is saved.");
3662
+ log("");
3663
+ }
3664
+ async function tokenFlow() {
3665
+ log("");
3666
+ log("\u2500\u2500 Token setup \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");
3667
+ const choice = await chooseTokenAction(
3668
+ "Do you have a Transcodes project MAT (Member Access Token)?"
3669
+ );
3670
+ if (choice === "skip") {
3671
+ log("");
3672
+ log("Skipping token setup \u2014 using the token already configured.");
3673
+ return true;
3674
+ }
3675
+ if (choice === "no") {
3676
+ log("");
3677
+ log(
3678
+ "You can create a MAT (Member Access Token) in the Transcodes console."
3679
+ );
3680
+ log(
3681
+ "If you don't have access, ask your project administrator to issue one for you."
3682
+ );
3683
+ log("");
3684
+ await promptLine(`Press ENTER to open the console (${APP_CONSOLE_URL}) \u2026 `);
3685
+ log(`
3686
+ Opening ${APP_CONSOLE_URL} \u2026`);
3687
+ openUrl(APP_CONSOLE_URL);
3688
+ log("");
3689
+ log(" 1) After sign-in, create a new project on the dashboard.");
3690
+ log(' 2) Open the "Members & Tokens" tab and add a new member.');
3691
+ log(
3692
+ " 3) Generate a new token for that member, or ask your manager to get a token."
3693
+ );
3694
+ log(" 4) Paste it here.");
3695
+ }
3696
+ return registerToken();
3697
+ }
3698
+ function parseSelection(args) {
3699
+ const ids = [];
3700
+ let explicit = false;
3701
+ for (const arg of args) {
3702
+ if (arg === "--all") {
3703
+ return PLATFORMS.map((p) => p.id);
3704
+ }
3705
+ const match = PLATFORMS.find(
3706
+ (p) => p.id === arg || arg === "chatgpt" && p.id === "codex"
3707
+ );
3708
+ if (match) {
3709
+ explicit = true;
3710
+ if (!ids.includes(match.id)) ids.push(match.id);
3711
+ }
3712
+ }
3713
+ return explicit ? ids : "interactive";
3714
+ }
3715
+ async function cmdInstall(args) {
3716
+ log("transcodes install \u2014 set up plugins, token, and dashboard.\n");
3717
+ if (!await ensureNode()) {
3718
+ process.exit(1);
3719
+ }
3720
+ log("");
3721
+ const selection = parseSelection(args);
3722
+ let openDashboard = false;
3723
+ if (selection === "interactive") {
3724
+ if (!isTty()) {
3725
+ log(
3726
+ "Non-interactive shell detected. Specify platforms explicitly, e.g.:"
3727
+ );
3728
+ log(" transcodes install --all");
3729
+ log(" transcodes install claude codex cursor antigravity");
3730
+ process.exit(1);
3731
+ }
3732
+ const useArrows = supportsArrowSelect();
3733
+ const checked = /* @__PURE__ */ new Set();
3734
+ let firstMenu = true;
3735
+ for (; ; ) {
3736
+ if (!firstMenu) clearScreen();
3737
+ firstMenu = false;
3738
+ let choice;
3739
+ if (useArrows) {
3740
+ choice = await arrowSelect(checked);
3741
+ } else {
3742
+ renderMenu();
3743
+ choice = await promptMenu();
3744
+ }
3745
+ if (choice.kind === "cancel") {
3746
+ log("");
3747
+ log(
3748
+ "If you want to manage tokens directly, please type `transcodes` to open the dashboard"
3749
+ );
3750
+ process.exit(0);
3751
+ }
3752
+ if (choice.kind === "next") {
3753
+ clearScreen();
3754
+ break;
3755
+ }
3756
+ if (choice.ids.length === 0) {
3757
+ log(" Nothing selected.");
3758
+ continue;
3759
+ }
3760
+ checked.clear();
3761
+ for (const id of choice.ids) checked.add(id);
3762
+ await runInstalls(choice.ids);
3763
+ }
3764
+ } else {
3765
+ if (selection.length === 0) {
3766
+ log("No platforms selected \u2014 nothing to install.");
3767
+ process.exit(0);
3768
+ }
3769
+ log(
3770
+ `
3771
+ Installing: ${selection.map((id) => PLATFORMS.find((p) => p.id === id)?.label ?? id).join(", ")}`
3772
+ );
3773
+ await runInstalls(selection);
3774
+ clearScreen();
3775
+ }
3776
+ openDashboard = await tokenFlow();
3777
+ if (!openDashboard) {
3778
+ process.exit(0);
3779
+ }
3780
+ printSetupComplete();
3781
+ await promptLine("Press ENTER to open the transcodes CLI dashboard \u2026 ");
3782
+ await runDashboard({});
3783
+ process.exit(0);
3784
+ }
3785
+ function installedPlatforms() {
3786
+ return PLATFORMS.filter((p) => isPluginInstalled(p.id)).map((p) => p.id);
3787
+ }
3788
+ async function updateCliPackage() {
3789
+ log(`
3790
+ \u2500\u2500 ${CLI_PACKAGE_NAME} (CLI) \u2500\u2500`);
3791
+ log(` Current version: ${CLI_VERSION}`);
3792
+ if (!await commandExists("npm")) {
3793
+ log(" \u2717 `npm` not found on PATH \u2014 cannot update the CLI.");
3794
+ log(
3795
+ ` Install Node.js / npm, then: npm install -g ${CLI_PACKAGE_NAME}@latest`
3796
+ );
3797
+ return false;
3798
+ }
3799
+ const ok = await run("npm", ["install", "-g", `${CLI_PACKAGE_NAME}@latest`]) === 0;
3800
+ if (ok) {
3801
+ log(" \u2713 CLI updated (run `transcodes version` to confirm).");
3802
+ } else {
3803
+ log(" \u2717 CLI update failed.");
3804
+ }
3805
+ return ok;
3806
+ }
3807
+ async function cmdUpdate(args) {
3808
+ log("transcodes update \u2014 refresh plugins and CLI.\n");
3809
+ if (!await ensureNode()) {
3810
+ process.exit(1);
3811
+ }
3812
+ let cliOnly = false;
3813
+ let pluginsOnly = false;
3814
+ let forceAll = false;
3815
+ const platformArgs = [];
3816
+ for (const arg of args) {
3817
+ if (arg === "--cli-only") cliOnly = true;
3818
+ else if (arg === "--plugins-only") pluginsOnly = true;
3819
+ else if (arg === "--all") forceAll = true;
3820
+ else if (arg.startsWith("-")) {
3821
+ log(`Unknown flag "${arg}".`);
3822
+ log(
3823
+ "Usage: transcodes update [--cli-only|--plugins-only|--all] [claude|codex|cursor|antigravity \u2026]"
3824
+ );
3825
+ process.exit(1);
3826
+ } else {
3827
+ platformArgs.push(arg);
3828
+ }
3829
+ }
3830
+ if (cliOnly && pluginsOnly) {
3831
+ log("Cannot combine --cli-only and --plugins-only.");
3832
+ process.exit(1);
3833
+ }
3834
+ const results = [];
3835
+ if (!cliOnly) {
3836
+ let targets;
3837
+ if (forceAll) {
3838
+ targets = PLATFORMS.map((p) => p.id);
3839
+ } else if (platformArgs.length > 0) {
3840
+ const parsed = parseSelection(platformArgs);
3841
+ if (parsed === "interactive" || parsed.length === 0) {
3842
+ log(
3843
+ "No valid platforms. Use: claude, codex, cursor, antigravity (or chatgpt)."
3844
+ );
3845
+ process.exit(1);
3846
+ }
3847
+ targets = parsed;
3848
+ } else {
3849
+ targets = installedPlatforms();
3850
+ }
3851
+ if (targets.length === 0) {
3852
+ log("No installed plugins detected.");
3853
+ log(" Run `transcodes install` first, or `transcodes update --all`.");
3854
+ } else {
3855
+ log(
3856
+ `Updating plugins: ${targets.map((id) => PLATFORMS.find((p) => p.id === id)?.label ?? id).join(", ")}`
3857
+ );
3858
+ const clonedRepoDir = await cloneRepoIfNeeded(targets);
3859
+ try {
3860
+ for (const id of targets) {
3861
+ const label = PLATFORMS.find((p) => p.id === id)?.label ?? id;
3862
+ log(`
3863
+ \u2500\u2500 ${label} \u2500\u2500`);
3864
+ const ok = await installPlatform(id, clonedRepoDir);
3865
+ results.push({ name: label, ok });
3866
+ }
3867
+ } finally {
3868
+ if (clonedRepoDir) {
3869
+ fs.rmSync(clonedRepoDir, { recursive: true, force: true });
3870
+ }
3871
+ }
3872
+ }
3873
+ }
3874
+ if (!pluginsOnly) {
3875
+ const ok = await updateCliPackage();
3876
+ results.push({ name: `${CLI_PACKAGE_NAME} (CLI)`, ok });
3877
+ }
3878
+ log("\n\u2500\u2500 Update summary \u2500\u2500");
3879
+ if (results.length === 0) {
3880
+ log(" (nothing to update)");
3881
+ } else {
3882
+ for (const r of results) {
3883
+ log(` ${r.ok ? "\u2713" : "\u2717"} ${r.name}`);
3884
+ }
3885
+ }
3886
+ log("");
3887
+ log("Done. Restart your host apps/CLIs so they pick up the new plugin.");
3888
+ process.exit(results.some((r) => !r.ok) ? 1 : 0);
3889
+ }
3890
+
2685
3891
  // src/index.ts
2686
3892
  function fail(message) {
2687
3893
  process.stderr.write(`transcodes: ${message}
@@ -2838,6 +4044,12 @@ async function cmdDashboard(args) {
2838
4044
  function main() {
2839
4045
  const [command, ...rest] = process.argv.slice(2);
2840
4046
  switch (command) {
4047
+ case "install":
4048
+ void cmdInstall(rest);
4049
+ break;
4050
+ case "update":
4051
+ void cmdUpdate(rest);
4052
+ break;
2841
4053
  case "set":
2842
4054
  cmdSet(rest);
2843
4055
  break;