@bigstrider/transcodes-cli 0.9.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 +1143 -308
  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 path3 = 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,7 +13,7 @@ async function request(config, input) {
13
13
  }
14
14
  }
15
15
  const qs = params.toString();
16
- const url = `${config.apiBaseV1}${path3}${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"
@@ -60,6 +60,13 @@ async function request(config, input) {
60
60
  }
61
61
  }
62
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
+
63
70
  // ../packages/core/dist/stepup/jwt.js
64
71
  var REQUIRED_AUDIENCE = "transcodes-mcp";
65
72
  function isPlainObject(v) {
@@ -80,8 +87,8 @@ function readString(rec, key) {
80
87
  const v = rec[key];
81
88
  if (typeof v !== "string")
82
89
  return void 0;
83
- const t = v.trim();
84
- return t || void 0;
90
+ const t2 = v.trim();
91
+ return t2 || void 0;
85
92
  }
86
93
  function readNumericDate(rec, key) {
87
94
  const v = rec[key];
@@ -91,8 +98,8 @@ function readNumericDate(rec, key) {
91
98
  function readAudience(rec) {
92
99
  const aud = rec.aud;
93
100
  if (typeof aud === "string") {
94
- const t = aud.trim();
95
- return t ? [t] : void 0;
101
+ const t2 = aud.trim();
102
+ return t2 ? [t2] : void 0;
96
103
  }
97
104
  if (Array.isArray(aud)) {
98
105
  const list = aud.filter((x) => typeof x === "string").map((x) => x.trim()).filter((x) => x.length > 0);
@@ -271,12 +278,12 @@ function writeTokenToFile(token, label) {
271
278
  throw new Error("token is empty");
272
279
  }
273
280
  const nextLabel = normalizeLabel(label);
274
- const current = readConfig();
275
- const existing = current.tokenList.find((r) => r.token === trimmed);
281
+ const current2 = readConfig();
282
+ const existing = current2.tokenList.find((r) => r.token === trimmed);
276
283
  if (!existing && !nextLabel) {
277
284
  throw new Error("label is required");
278
285
  }
279
- 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 }];
280
287
  writeConfig({ token: trimmed, tokenList });
281
288
  }
282
289
  function setActiveToken(token) {
@@ -288,22 +295,22 @@ function setTokenLabel(token, label) {
288
295
  if (!nextLabel) {
289
296
  throw new Error("label is required");
290
297
  }
291
- const current = readConfig();
292
- if (!current.tokenList.some((r) => r.token === trimmed)) {
298
+ const current2 = readConfig();
299
+ if (!current2.tokenList.some((r) => r.token === trimmed)) {
293
300
  throw new Error("token not found");
294
301
  }
295
- const tokenList = current.tokenList.map((r) => r.token === trimmed ? { token: r.token, label: nextLabel } : r);
296
- 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 });
297
304
  }
298
305
  function removeTokenFromFile(token) {
299
306
  const trimmed = token.trim();
300
- const current = readConfig();
301
- const tokenList = current.tokenList.filter((r) => r.token !== trimmed);
307
+ const current2 = readConfig();
308
+ const tokenList = current2.tokenList.filter((r) => r.token !== trimmed);
302
309
  if (tokenList.length === 0) {
303
310
  clearTokenFile();
304
311
  return;
305
312
  }
306
- 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;
307
314
  writeConfig({ token: active, tokenList });
308
315
  }
309
316
  function clearTokenFile() {
@@ -333,7 +340,7 @@ function loadStepupConfig() {
333
340
  }
334
341
  const { token: tokenRaw } = resolveToken();
335
342
  if (!tokenRaw) {
336
- 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>\`.`);
337
344
  }
338
345
  const parsed = parseMemberAccessToken(tokenRaw);
339
346
  for (const w of parsed.warnings) {
@@ -490,11 +497,25 @@ async function openConsoleSession(options) {
490
497
  };
491
498
  }
492
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
+
493
514
  // src/commands.ts
494
515
  var CLI_COMMAND_SPECS = [
495
516
  {
496
517
  usage: "transcodes install",
497
- description: "Guided setup: pick host plugins to install, save a token, then open the dashboard (--all / <platforms>)"
518
+ description: "Guided setup: pick host plugins to install, sign in, then open the dashboard (--all / <platforms>)"
498
519
  },
499
520
  {
500
521
  usage: "transcodes update",
@@ -502,7 +523,15 @@ var CLI_COMMAND_SPECS = [
502
523
  },
503
524
  {
504
525
  usage: "transcodes",
505
- 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)"
506
535
  },
507
536
  {
508
537
  usage: "transcodes set <token> -l <label>",
@@ -561,6 +590,20 @@ function renderCliCommandsHtml() {
561
590
  ).join("\n ");
562
591
  }
563
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
+
564
607
  // src/dashboard.ts
565
608
  import { execFile, spawn as spawn2 } from "child_process";
566
609
  import { createHash } from "crypto";
@@ -569,9 +612,6 @@ import {
569
612
  } from "http";
570
613
  import { promisify } from "util";
571
614
 
572
- // ../packages/core/dist/contract/messages.js
573
- var TRANSCODES_GUARD_REPO_URL = "https://github.com/transcodings/transcodes-guard";
574
-
575
615
  // src/logo.ts
576
616
  var LOGO_DATA_URI = "data:image/png;base64,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";
577
617
 
@@ -623,11 +663,16 @@ var CLI_PACKAGE_NAME = pkg.name;
623
663
  var CLI_VERSION = pkg.version;
624
664
 
625
665
  // src/dashboard.ts
626
- var DEFAULT_PORT = 3847;
627
- var HOST = "127.0.0.1";
628
- var PORT_ATTEMPTS = 10;
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";
629
673
  var GUIDELINE_MUX_PLAYBACK_ID = "h1vOCPmFDA02fGhcout01FZWD4lpKNTzLFk7vybxvrc3M";
630
674
  var APP_ORG_URL = "https://app.transcodes.io/en/org";
675
+ var APP_HOME_URL = "https://app.transcodes.io";
631
676
  var execFileAsync = promisify(execFile);
632
677
  function fingerprint(token) {
633
678
  return createHash("sha256").update(token).digest("hex").slice(0, 12);
@@ -708,7 +753,7 @@ async function buildStatus() {
708
753
  };
709
754
  }
710
755
  function tokenById(id) {
711
- return readTokenList().find((t) => fingerprint(t) === id);
756
+ return readTokenList().find((t2) => fingerprint(t2) === id);
712
757
  }
713
758
  function sendJson(res, status, body) {
714
759
  res.writeHead(status, {
@@ -912,6 +957,28 @@ function dashboardHtml() {
912
957
  opacity: 0.55;
913
958
  cursor: not-allowed;
914
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; }
915
982
  .header-profile-actions {
916
983
  flex-shrink: 0;
917
984
  display: flex;
@@ -988,6 +1055,18 @@ function dashboardHtml() {
988
1055
  margin: 0 0 4px;
989
1056
  letter-spacing: -0.01em;
990
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; }
991
1070
  .section-sub {
992
1071
  font-size: var(--text-base);
993
1072
  color: var(--muted);
@@ -1560,7 +1639,7 @@ function dashboardHtml() {
1560
1639
  margin: 0;
1561
1640
  }
1562
1641
  .guide-video-wrap {
1563
- margin: 0 0 22px;
1642
+ margin: 0;
1564
1643
  }
1565
1644
  .guide-video-toggle {
1566
1645
  display: inline-flex;
@@ -1586,7 +1665,7 @@ function dashboardHtml() {
1586
1665
  background: var(--accent-soft);
1587
1666
  }
1588
1667
  .guide-video {
1589
- margin: 10px 0 0;
1668
+ margin: 0 0 22px;
1590
1669
  border-radius: 14px;
1591
1670
  overflow: hidden;
1592
1671
  border: 1px solid var(--line);
@@ -1787,13 +1866,49 @@ function dashboardHtml() {
1787
1866
  }
1788
1867
  .guide-help {
1789
1868
  margin: 0 0 18px;
1790
- padding: 16px 18px;
1869
+ padding: 0;
1791
1870
  background: var(--accent-soft);
1792
1871
  border: 1px solid rgba(91, 84, 230, 0.18);
1793
1872
  border-radius: 14px;
1873
+ overflow: hidden;
1794
1874
  }
1795
- .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 {
1796
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;
1797
1912
  font-size: var(--text-sm);
1798
1913
  color: #4a4a52;
1799
1914
  line-height: 1.6;
@@ -1813,7 +1928,7 @@ function dashboardHtml() {
1813
1928
  color: #4a4a52;
1814
1929
  line-height: 1.6;
1815
1930
  }
1816
- .guide-help-heading:first-child { margin-top: 0; }
1931
+ .guide-help-body > .guide-help-heading:first-child { margin-top: 12px; }
1817
1932
  .guide-help-list {
1818
1933
  margin: 6px 0 0;
1819
1934
  padding-left: 18px;
@@ -1979,11 +2094,10 @@ function dashboardHtml() {
1979
2094
  </div>
1980
2095
  </div>
1981
2096
  <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>
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>
1984
2099
  <div class="header-token-empty-cmds">
1985
- <code>npm install -g @bigstrider/transcodes-cli</code>
1986
- <code>transcodes install</code>
2100
+ <code>transcodes login</code>
1987
2101
  </div>
1988
2102
  </div>
1989
2103
  <div class="header-profile" id="header-profile" hidden>
@@ -2010,12 +2124,14 @@ function dashboardHtml() {
2010
2124
  </div>
2011
2125
 
2012
2126
  <div class="panel active" id="panel-guideline">
2013
- <p class="section-title">Getting Started</p>
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">
2127
+ <div class="section-title-row">
2128
+ <p class="section-title">Getting Started</p>
2016
2129
  <button type="button" class="guide-video-toggle" id="guide-video-toggle" aria-expanded="false" aria-controls="guide-video">
2017
2130
  Watch Intro Video
2018
2131
  </button>
2132
+ </div>
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">
2019
2135
  <div class="guide-video" id="guide-video" hidden>
2020
2136
  <mux-player
2021
2137
  playback-id="${GUIDELINE_MUX_PLAYBACK_ID}"
@@ -2026,41 +2142,51 @@ function dashboardHtml() {
2026
2142
  ></mux-player>
2027
2143
  </div>
2028
2144
  </div>
2029
- <p class="list-label">Quick Demo</p>
2145
+ <p class="section-title section-title--spaced">Quick Demo</p>
2030
2146
  <p class="guide-prefix-note">Try the quick demo to see how Transcodes works</p>
2031
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>
2032
2148
  <div class="guide-groups">
2033
2149
  <section class="guide-group guide-group--panel">
2034
2150
  <ol class="guide-steps">
2035
- <li class="guide-step">
2036
- <span class="guide-step-num">1</span>
2037
- <div class="guide-step-body">
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>
2040
- </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>
2041
2162
  </li>
2042
- <li class="guide-step">
2043
- <span class="guide-step-num">2</span>
2044
- <div class="guide-step-body">
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>
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>
2055
2181
  </div>
2056
2182
  </div>
2057
- </div>
2183
+ </details>
2058
2184
  </li>
2059
2185
  </ol>
2060
2186
  </section>
2061
2187
  </div>
2062
2188
 
2063
- <p class="list-label">Steps</p>
2189
+ <p class="section-title section-title--spaced">Steps</p>
2064
2190
  <div class="guide-groups">
2065
2191
  <section class="guide-group guide-group--panel">
2066
2192
  <ol class="guide-steps">
@@ -2166,8 +2292,14 @@ function dashboardHtml() {
2166
2292
  <p class="cli-map-row cli-map-row--section">
2167
2293
  <span class="cli-map-label cli-map-label--title">MCP Access Token</span>
2168
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>
2169
2301
  </p>
2170
- <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>
2171
2303
  <textarea id="token" placeholder="eyJhbGciOi\u2026" spellcheck="false" autocomplete="off"></textarea>
2172
2304
  <input type="text" id="label" class="label-input" placeholder="Label (required) \u2014 e.g. transcodes-{project_name}-{env}" autocomplete="off" required />
2173
2305
  <div class="actions">
@@ -2190,7 +2322,7 @@ function dashboardHtml() {
2190
2322
  </div>
2191
2323
  <button type="button" class="btn-danger" id="reset-all">Reset all</button>
2192
2324
  </div>
2193
- <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>
2194
2326
  </div>
2195
2327
 
2196
2328
  <div class="panel" id="panel-cli">
@@ -2199,21 +2331,40 @@ function dashboardHtml() {
2199
2331
  <div class="cmd-list">
2200
2332
  ${renderCliCommandsHtml()}
2201
2333
  </div>
2202
- <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>
2203
2335
  </div>
2204
2336
 
2205
2337
  <div class="panel" id="panel-rbac">
2206
2338
  <p id="rbac-token-warning" class="rbac-token-warning" hidden>Save a Transcodes token(MAT) first</p>
2207
2339
  <div class="guide-help">
2208
- <p class="guide-help-heading">How it works</p>
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>
2210
- <p class="guide-help-heading">Action Classification</p>
2211
- <ul class="guide-help-list">
2212
- <li><strong>read</strong> \u2014 inspect data without changing it (list, get, query, cat, grep, status, describe)</li>
2213
- <li><strong>update</strong> \u2014 modify existing data (edit, patch, set, rename, move, config change)</li>
2214
- <li><strong>delete</strong> \u2014 remove or destroy data (rm, drop, truncate, purge)</li>
2215
- <li><strong>create</strong> \u2014 any other state-changing action (new records, sends, posts, uploads, installs)</li>
2216
- </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>
2217
2368
  </div>
2218
2369
  <div class="rbac-legend">
2219
2370
  <p class="rbac-legend-title">Permission matrix</p>
@@ -2281,6 +2432,7 @@ function dashboardHtml() {
2281
2432
  const headerProfileNameEl = document.getElementById("header-profile-name");
2282
2433
  const headerProfileMetaEl = document.getElementById("header-profile-meta");
2283
2434
  const APP_ORG_URL = ${JSON.stringify(APP_ORG_URL)};
2435
+ const APP_HOME_URL = ${JSON.stringify(APP_HOME_URL)};
2284
2436
 
2285
2437
  let lastStatus = { tokens: [], activeMember: null };
2286
2438
  let editingId = null;
@@ -2289,12 +2441,33 @@ function dashboardHtml() {
2289
2441
  return Array.isArray(s.tokens) && s.tokens.length > 0;
2290
2442
  }
2291
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
+
2292
2462
  function updateAppDeepLinks(s) {
2463
+ const oid = s && s.activeMember && s.activeMember.organizationId;
2293
2464
  const pid = s && s.activeMember && s.activeMember.projectId;
2294
2465
  document.querySelectorAll("[data-app-tab]").forEach((a) => {
2295
2466
  const tab = a.getAttribute("data-app-tab");
2296
- if (pid && tab) {
2297
- a.href = APP_ORG_URL + "?pid=" + encodeURIComponent(pid) + "&tab=" + encodeURIComponent(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);
2298
2471
  } else {
2299
2472
  a.href = APP_ORG_URL;
2300
2473
  }
@@ -2329,6 +2502,19 @@ function dashboardHtml() {
2329
2502
  }
2330
2503
  }
2331
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
+
2332
2518
  const guideVideoToggle = document.getElementById("guide-video-toggle");
2333
2519
  const guideVideo = document.getElementById("guide-video");
2334
2520
  if (guideVideoToggle && guideVideo) {
@@ -2455,6 +2641,11 @@ function dashboardHtml() {
2455
2641
  btn.addEventListener("click", () => { openConsole(); });
2456
2642
  });
2457
2643
 
2644
+ const manageTokensBtn = document.getElementById("manage-tokens-btn");
2645
+ if (manageTokensBtn) {
2646
+ manageTokensBtn.addEventListener("click", () => { openManageTokens(); });
2647
+ }
2648
+
2458
2649
  async function saveLabel(id) {
2459
2650
  const input = listEl.querySelector('[data-edit-input="' + id + '"]');
2460
2651
  const label = input ? input.value.trim() : "";
@@ -2719,7 +2910,7 @@ function dashboardHtml() {
2719
2910
  </body>
2720
2911
  </html>`;
2721
2912
  }
2722
- function openBrowser2(url) {
2913
+ function openDashboardBrowser(url) {
2723
2914
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2724
2915
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
2725
2916
  try {
@@ -2742,6 +2933,16 @@ function listen(port) {
2742
2933
  return;
2743
2934
  }
2744
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
+ }
2745
2946
  if (method === "GET" && (url === "/" || url === "/index.html")) {
2746
2947
  res.writeHead(200, {
2747
2948
  "Content-Type": "text/html; charset=utf-8",
@@ -2864,48 +3065,6 @@ function listen(port) {
2864
3065
  server.listen(port, HOST, () => resolve(server));
2865
3066
  });
2866
3067
  }
2867
- function waitForDashboardShutdown(server) {
2868
- return new Promise((resolve) => {
2869
- let shuttingDown = false;
2870
- let forceTimer;
2871
- const cleanup = () => {
2872
- if (forceTimer) clearTimeout(forceTimer);
2873
- process.removeListener("SIGINT", onSignal);
2874
- process.removeListener("SIGTERM", onSignal);
2875
- };
2876
- const finish = () => {
2877
- cleanup();
2878
- resolve();
2879
- };
2880
- const forceExit = () => {
2881
- cleanup();
2882
- process.stderr.write("\nForce stopping dashboard.\n");
2883
- process.exit(0);
2884
- };
2885
- const onSignal = () => {
2886
- if (shuttingDown) {
2887
- forceExit();
2888
- return;
2889
- }
2890
- shuttingDown = true;
2891
- process.stderr.write("\nStopping dashboard\u2026\n");
2892
- forceTimer = setTimeout(forceExit, 1500);
2893
- forceTimer.unref();
2894
- server.closeAllConnections?.();
2895
- server.close((err) => {
2896
- if (err) {
2897
- process.stderr.write(
2898
- `Shutdown error: ${err instanceof Error ? err.message : String(err)}
2899
- `
2900
- );
2901
- }
2902
- finish();
2903
- });
2904
- };
2905
- process.on("SIGINT", onSignal);
2906
- process.on("SIGTERM", onSignal);
2907
- });
2908
- }
2909
3068
  async function pidsListeningOnPorts(from, to) {
2910
3069
  const pids = /* @__PURE__ */ new Set();
2911
3070
  if (process.platform === "win32") {
@@ -2979,7 +3138,7 @@ async function tryBindPortRange(preferred) {
2979
3138
  }
2980
3139
  return null;
2981
3140
  }
2982
- async function runDashboard(options) {
3141
+ async function serveDashboardHttp(options) {
2983
3142
  const preferred = options.port ?? DEFAULT_PORT;
2984
3143
  const last = preferred + PORT_ATTEMPTS - 1;
2985
3144
  let bound = await tryBindPortRange(preferred);
@@ -2990,39 +3149,591 @@ async function runDashboard(options) {
2990
3149
  if (!bound) {
2991
3150
  throw new Error(
2992
3151
  `could not find a free port in ${preferred}-${last} (all in use).
2993
- A previous dashboard is probably still running.
2994
- Tip: Ctrl+Z only suspends the process (port stays taken) \u2014 run \`kill %1\` or \`fg\` then Ctrl+C.
2995
- Free the ports and retry:
3152
+ Stop a previous dashboard with: transcodes stop
2996
3153
  macOS/Linux: lsof -ti tcp:${preferred}-${last} | xargs kill -9
2997
- any platform: npx kill-port ${preferred} ${preferred + 1} # repeat per port
2998
3154
  Or choose another port: transcodes --port <N>`
2999
3155
  );
3000
3156
  }
3001
- const { server, port } = bound;
3002
- 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
+ }
3003
3518
  process.stdout.write(
3004
- `Transcodes dashboard running at ${url}
3005
- Config file: ${transcodesConfigFile()}
3006
- Press Ctrl+C to stop
3519
+ `${t("dashboardOpened", { url: running.url })}
3520
+ ${t("dashboardHowToUse")}
3521
+ ${t("dashboardStopHint")}
3007
3522
  `
3008
3523
  );
3009
- if (options.open !== false) {
3010
- openBrowser2(url);
3524
+ if (open) {
3525
+ openDashboardBrowser(running.url);
3011
3526
  }
3012
- await waitForDashboardShutdown(server);
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
+ );
3013
3538
  }
3014
3539
 
3015
3540
  // 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";
3541
+ import { spawn as spawn5 } from "child_process";
3542
+ import fs2 from "fs";
3543
+ import os5 from "os";
3544
+ import path5 from "path";
3020
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
+ `);
3716
+ }
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
3021
3733
  var REPO_SLUG = "transcodings/transcodes-guard";
3022
3734
  var REPO_GIT_URL = "https://github.com/transcodings/transcodes-guard.git";
3023
3735
  var MARKETPLACE = "bigstrider";
3024
3736
  var PLUGIN_NAME = "transcodes-guard";
3025
- var APP_CONSOLE_URL = "https://app.transcodes.io";
3026
3737
  var MIN_NODE_MAJOR = 20;
3027
3738
  var PLATFORMS = [
3028
3739
  { id: "claude", label: "Claude Code" },
@@ -3045,6 +3756,36 @@ function log(line = "") {
3045
3756
  function clearScreen() {
3046
3757
  process.stdout.write("\x1B[2J\x1B[H");
3047
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
+ }
3048
3789
  function isTty() {
3049
3790
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
3050
3791
  }
@@ -3052,7 +3793,7 @@ function openUrl(url) {
3052
3793
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
3053
3794
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
3054
3795
  try {
3055
- const child = spawn3(opener, args, { stdio: "ignore", detached: true });
3796
+ const child = spawn5(opener, args, { stdio: "ignore", detached: true });
3056
3797
  child.on("error", () => {
3057
3798
  });
3058
3799
  child.unref();
@@ -3060,25 +3801,25 @@ function openUrl(url) {
3060
3801
  }
3061
3802
  }
3062
3803
  function refreshPathHints() {
3063
- const home = os2.homedir();
3804
+ const home = os5.homedir();
3064
3805
  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"),
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"),
3071
3812
  "/usr/local/bin",
3072
3813
  "/opt/homebrew/bin"
3073
3814
  ];
3074
- const current = process.env.PATH ?? "";
3075
- const parts = current.split(path2.delimiter).filter(Boolean);
3815
+ const current2 = process.env.PATH ?? "";
3816
+ const parts = current2.split(path5.delimiter).filter(Boolean);
3076
3817
  const merged = [...extras.filter((p) => !parts.includes(p)), ...parts];
3077
- process.env.PATH = merged.join(path2.delimiter);
3818
+ process.env.PATH = merged.join(path5.delimiter);
3078
3819
  }
3079
3820
  function commandExists(cmd) {
3080
3821
  return new Promise((resolve) => {
3081
- const child = process.platform === "win32" ? spawn3("where", [cmd], { stdio: "ignore" }) : spawn3("sh", ["-c", `command -v ${JSON.stringify(cmd)}`], {
3822
+ const child = process.platform === "win32" ? spawn5("where", [cmd], { stdio: "ignore" }) : spawn5("sh", ["-c", `command -v ${JSON.stringify(cmd)}`], {
3082
3823
  stdio: "ignore"
3083
3824
  });
3084
3825
  child.on("error", () => resolve(false));
@@ -3095,7 +3836,7 @@ function run(cmd, args) {
3095
3836
  return new Promise((resolve) => {
3096
3837
  log(`
3097
3838
  $ ${cmd} ${args.join(" ")}`);
3098
- const child = spawn3(cmd, args, { stdio: "inherit", env: process.env });
3839
+ const child = spawn5(cmd, args, { stdio: "inherit", env: process.env });
3099
3840
  child.on("error", (err) => {
3100
3841
  log(` (failed to start: ${err.message})`);
3101
3842
  resolve(1);
@@ -3107,7 +3848,7 @@ function runShell(script) {
3107
3848
  return new Promise((resolve) => {
3108
3849
  log(`
3109
3850
  $ ${script}`);
3110
- const child = spawn3("sh", ["-c", script], {
3851
+ const child = spawn5("sh", ["-c", script], {
3111
3852
  stdio: "inherit",
3112
3853
  env: process.env
3113
3854
  });
@@ -3169,20 +3910,20 @@ async function ensureNode() {
3169
3910
  installed = await runShell(nvmInstall) === 0;
3170
3911
  }
3171
3912
  refreshPathHints();
3172
- const nvmDefaultBin = path2.join(os2.homedir(), ".nvm", "versions", "node");
3173
- if (fs.existsSync(nvmDefaultBin)) {
3913
+ const nvmDefaultBin = path5.join(os5.homedir(), ".nvm", "versions", "node");
3914
+ if (fs2.existsSync(nvmDefaultBin)) {
3174
3915
  try {
3175
- const versions = fs.readdirSync(nvmDefaultBin).filter((v) => v.startsWith("v")).sort().reverse();
3916
+ const versions = fs2.readdirSync(nvmDefaultBin).filter((v) => v.startsWith("v")).sort().reverse();
3176
3917
  if (versions[0]) {
3177
- const bin = path2.join(nvmDefaultBin, versions[0], "bin");
3178
- process.env.PATH = `${bin}${path2.delimiter}${process.env.PATH ?? ""}`;
3918
+ const bin = path5.join(nvmDefaultBin, versions[0], "bin");
3919
+ process.env.PATH = `${bin}${path5.delimiter}${process.env.PATH ?? ""}`;
3179
3920
  }
3180
3921
  } catch {
3181
3922
  }
3182
3923
  }
3183
3924
  if (await commandExists("node")) {
3184
3925
  const ver = await new Promise((resolve) => {
3185
- const child = spawn3("node", ["-v"], {
3926
+ const child = spawn5("node", ["-v"], {
3186
3927
  stdio: ["ignore", "pipe", "ignore"]
3187
3928
  });
3188
3929
  let out = "";
@@ -3288,70 +4029,159 @@ function promptLine(question) {
3288
4029
  }
3289
4030
  function safeExists(p) {
3290
4031
  try {
3291
- return fs.existsSync(p);
4032
+ return fs2.existsSync(p);
3292
4033
  } catch {
3293
4034
  return false;
3294
4035
  }
3295
4036
  }
3296
4037
  function dirHasMatch(dir, needles) {
3297
4038
  try {
3298
- return fs.readdirSync(dir).some((entry) => needles.some((n) => entry.includes(n)));
4039
+ return fs2.readdirSync(dir).some((entry) => needles.some((n) => entry.includes(n)));
3299
4040
  } catch {
3300
4041
  return false;
3301
4042
  }
3302
4043
  }
3303
4044
  function fileHas(file, needle) {
3304
4045
  try {
3305
- return fs.readFileSync(file, "utf8").includes(needle);
4046
+ return fs2.readFileSync(file, "utf8").includes(needle);
3306
4047
  } catch {
3307
4048
  return false;
3308
4049
  }
3309
4050
  }
3310
4051
  function isPluginInstalled(id) {
3311
- const home = os2.homedir();
4052
+ const home = os5.homedir();
3312
4053
  switch (id) {
3313
4054
  case "cursor":
3314
4055
  return safeExists(
3315
- path2.join(home, ".cursor", "plugins", "local", "transcodes-guard")
4056
+ path5.join(home, ".cursor", "plugins", "local", "transcodes-guard")
3316
4057
  );
3317
4058
  case "antigravity":
3318
4059
  return safeExists(
3319
- path2.join(home, ".gemini", "config", "plugins", "transcodes-guard")
4060
+ path5.join(home, ".gemini", "config", "plugins", "transcodes-guard")
3320
4061
  );
3321
4062
  case "claude":
3322
- return dirHasMatch(path2.join(home, ".claude", "plugins"), [
4063
+ return dirHasMatch(path5.join(home, ".claude", "plugins"), [
3323
4064
  "transcodes-guard",
3324
4065
  "bigstrider"
3325
- ]) || dirHasMatch(path2.join(home, ".claude", "plugins", "marketplaces"), [
4066
+ ]) || dirHasMatch(path5.join(home, ".claude", "plugins", "marketplaces"), [
3326
4067
  "bigstrider",
3327
4068
  "transcodes-guard"
3328
4069
  ]) || fileHas(
3329
- path2.join(home, ".claude", "settings.json"),
4070
+ path5.join(home, ".claude", "settings.json"),
3330
4071
  "transcodes-guard"
3331
- ) || fileHas(path2.join(home, ".claude.json"), "transcodes-guard");
4072
+ ) || fileHas(path5.join(home, ".claude.json"), "transcodes-guard");
3332
4073
  case "codex":
3333
- return dirHasMatch(path2.join(home, ".codex", "plugins"), [
4074
+ return dirHasMatch(path5.join(home, ".codex", "plugins"), [
3334
4075
  "transcodes-guard",
3335
4076
  "bigstrider"
3336
- ]) || fileHas(path2.join(home, ".codex", "config.toml"), "transcodes-guard") || fileHas(path2.join(home, ".codex", "config.json"), "transcodes-guard");
4077
+ ]) || fileHas(path5.join(home, ".codex", "config.toml"), "transcodes-guard") || fileHas(path5.join(home, ".codex", "config.json"), "transcodes-guard");
3337
4078
  }
3338
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
+ }
3339
4169
  function renderMenu() {
3340
4170
  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)");
4171
+ log(t("platformTitle"));
4172
+ log(t("platformHint1"));
4173
+ log(installedAppsHint());
3344
4174
  log("");
3345
4175
  PLATFORMS.forEach((p, i) => {
3346
- const mark = isPluginInstalled(p.id) ? " [Installed \u2713]" : "";
3347
- log(` ${i + 1}. ${p.label}${mark}`);
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}`);
3348
4180
  });
3349
- log(` ${PLATFORMS.length + 1}. Next Step \u2192`);
4181
+ log(` ${PLATFORMS.length + 1}. ${t("nextStep")}`);
3350
4182
  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
- );
4183
+ log(t("platformNumberedHint"));
4184
+ log(t("platformNumberedNext", { n: String(PLATFORMS.length + 1) }));
3355
4185
  }
3356
4186
  async function promptMenu() {
3357
4187
  const nextNum = String(PLATFORMS.length + 1);
@@ -3394,27 +4224,22 @@ function arrowSelect(checked) {
3394
4224
  const { stdin, stdout } = process;
3395
4225
  const draw = () => {
3396
4226
  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)");
4227
+ lines.push(t("platformTitle"));
4228
+ lines.push(t("platformHint1"));
4229
+ lines.push(installedAppsHint());
3400
4230
  lines.push("");
3401
4231
  PLATFORMS.forEach((p, i) => {
3402
4232
  const pointer = cursor === i ? "\u276F" : " ";
3403
- const box = checked.has(p.id) ? "\u25C9" : "\u25EF";
3404
- const mark = isPluginInstalled(p.id) ? " [Installed \u2713]" : "";
4233
+ const onDevice = isHostAppInstalled(p.id);
4234
+ const box = checked.has(p.id) ? "\u25CF" : "\u25EF";
4235
+ const mark = onDevice ? ` ${t("installed")}` : "";
3405
4236
  lines.push(`${pointer} ${box} ${p.label}${mark}`);
3406
4237
  });
3407
4238
  const nextPointer = cursor === PLATFORMS.length ? "\u276F" : " ";
3408
- lines.push(`${nextPointer} Next Step \u2192`);
4239
+ lines.push(`${nextPointer} ${t("nextStep")}`);
3409
4240
  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;
4241
+ lines.push(t("platformKeys"));
4242
+ rendered = redrawBlock(stdout, lines, rendered);
3418
4243
  };
3419
4244
  const cleanup = () => {
3420
4245
  stdout.write("\x1B[?25h");
@@ -3480,11 +4305,8 @@ function arrowChoose(title, options, defaultIndex = 0) {
3480
4305
  lines.push(`${pointer} ${radio} ${opt}`);
3481
4306
  });
3482
4307
  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;
4308
+ lines.push(t("chooseKeys"));
4309
+ rendered = redrawBlock(stdout, lines, rendered);
3488
4310
  };
3489
4311
  const cleanup = () => {
3490
4312
  stdout.write("\x1B[?25h");
@@ -3521,24 +4343,6 @@ function arrowChoose(title, options, defaultIndex = 0) {
3521
4343
  draw();
3522
4344
  });
3523
4345
  }
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
4346
  async function runInstalls(ids) {
3543
4347
  const clonedRepoDir = await cloneRepoIfNeeded(ids);
3544
4348
  const results = /* @__PURE__ */ new Map();
@@ -3551,10 +4355,11 @@ async function runInstalls(ids) {
3551
4355
  }
3552
4356
  } finally {
3553
4357
  if (clonedRepoDir) {
3554
- fs.rmSync(clonedRepoDir, { recursive: true, force: true });
4358
+ fs2.rmSync(clonedRepoDir, { recursive: true, force: true });
3555
4359
  }
3556
4360
  }
3557
- log("\n\u2500\u2500 Install summary \u2500\u2500");
4361
+ log(`
4362
+ ${t("installSummary")}`);
3558
4363
  for (const id of ids) {
3559
4364
  const label = PLATFORMS.find((p) => p.id === id)?.label ?? id;
3560
4365
  log(` ${results.get(id) ? "\u2713" : "\u2717"} ${label}`);
@@ -3591,8 +4396,8 @@ async function installPlatform(id, clonedRepoDir) {
3591
4396
  log(` Skipped ${platform?.label ?? id} \u2014 repo clone unavailable.`);
3592
4397
  return false;
3593
4398
  }
3594
- const installer = path2.join(clonedRepoDir, platform.installerRel);
3595
- if (!fs.existsSync(installer)) {
4399
+ const installer = path5.join(clonedRepoDir, platform.installerRel);
4400
+ if (!fs2.existsSync(installer)) {
3596
4401
  log(` Skipped ${platform.label} \u2014 installer missing at ${installer}.`);
3597
4402
  return false;
3598
4403
  }
@@ -3608,92 +4413,75 @@ async function cloneRepoIfNeeded(platforms) {
3608
4413
  log("Install git and re-run, or use the one-liners from the README.");
3609
4414
  return null;
3610
4415
  }
3611
- const tmpDir = fs.mkdtempSync(path2.join(os2.tmpdir(), "tg-install-"));
4416
+ const tmpDir = fs2.mkdtempSync(path5.join(os5.tmpdir(), "tg-install-"));
3612
4417
  const ok = await run("git", ["clone", "--depth", "1", REPO_GIT_URL, tmpDir]) === 0;
3613
4418
  if (!ok) {
3614
- fs.rmSync(tmpDir, { recursive: true, force: true });
4419
+ fs2.rmSync(tmpDir, { recursive: true, force: true });
3615
4420
  log("\nRepo clone failed \u2014 skipping Cursor / Antigravity.");
3616
4421
  return null;
3617
4422
  }
3618
4423
  return tmpDir;
3619
4424
  }
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
4425
  function printSetupComplete() {
3655
4426
  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");
4427
+ log(t("congratsBar"));
4428
+ log(` ${t("congrats1")}`);
4429
+ log(` ${t("congrats2")}`);
4430
+ log(t("congratsBar"));
3660
4431
  log("");
3661
- log("Plugins are installed and your MAT token is saved.");
4432
+ log(t("congratsBody"));
3662
4433
  log("");
3663
4434
  }
3664
4435
  async function tokenFlow() {
3665
4436
  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.");
4437
+ try {
4438
+ await cmdLogin([]);
3673
4439
  return true;
4440
+ } catch (error) {
4441
+ log(` ${error instanceof Error ? error.message : String(error)}`);
4442
+ return false;
3674
4443
  }
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);
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");
3688
4460
  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.");
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");
3695
4484
  }
3696
- return registerToken();
3697
4485
  }
3698
4486
  function parseSelection(args) {
3699
4487
  const ids = [];
@@ -3713,7 +4501,9 @@ function parseSelection(args) {
3713
4501
  return explicit ? ids : "interactive";
3714
4502
  }
3715
4503
  async function cmdInstall(args) {
3716
- log("transcodes install \u2014 set up plugins, token, and dashboard.\n");
4504
+ await promptLocale();
4505
+ log(`${t("installBanner")}
4506
+ `);
3717
4507
  if (!await ensureNode()) {
3718
4508
  process.exit(1);
3719
4509
  }
@@ -3722,19 +4512,17 @@ async function cmdInstall(args) {
3722
4512
  let openDashboard = false;
3723
4513
  if (selection === "interactive") {
3724
4514
  if (!isTty()) {
3725
- log(
3726
- "Non-interactive shell detected. Specify platforms explicitly, e.g.:"
3727
- );
4515
+ log(t("nonInteractive"));
3728
4516
  log(" transcodes install --all");
3729
4517
  log(" transcodes install claude codex cursor antigravity");
3730
4518
  process.exit(1);
3731
4519
  }
3732
4520
  const useArrows = supportsArrowSelect();
3733
- const checked = /* @__PURE__ */ new Set();
3734
- let firstMenu = true;
4521
+ const checked = new Set(
4522
+ PLATFORMS.filter((p) => isHostAppInstalled(p.id)).map((p) => p.id)
4523
+ );
3735
4524
  for (; ; ) {
3736
- if (!firstMenu) clearScreen();
3737
- firstMenu = false;
4525
+ clearScreen();
3738
4526
  let choice;
3739
4527
  if (useArrows) {
3740
4528
  choice = await arrowSelect(checked);
@@ -3744,9 +4532,7 @@ async function cmdInstall(args) {
3744
4532
  }
3745
4533
  if (choice.kind === "cancel") {
3746
4534
  log("");
3747
- log(
3748
- "If you want to manage tokens directly, please type `transcodes` to open the dashboard"
3749
- );
4535
+ log(t("cancelHint"));
3750
4536
  process.exit(0);
3751
4537
  }
3752
4538
  if (choice.kind === "next") {
@@ -3754,7 +4540,7 @@ async function cmdInstall(args) {
3754
4540
  break;
3755
4541
  }
3756
4542
  if (choice.ids.length === 0) {
3757
- log(" Nothing selected.");
4543
+ log(` ${t("nothingSelected")}`);
3758
4544
  continue;
3759
4545
  }
3760
4546
  checked.clear();
@@ -3763,12 +4549,14 @@ async function cmdInstall(args) {
3763
4549
  }
3764
4550
  } else {
3765
4551
  if (selection.length === 0) {
3766
- log("No platforms selected \u2014 nothing to install.");
4552
+ log(t("noPlatforms"));
3767
4553
  process.exit(0);
3768
4554
  }
3769
4555
  log(
3770
4556
  `
3771
- Installing: ${selection.map((id) => PLATFORMS.find((p) => p.id === id)?.label ?? id).join(", ")}`
4557
+ ${t("installing", {
4558
+ list: selection.map((id) => PLATFORMS.find((p) => p.id === id)?.label ?? id).join(", ")
4559
+ })}`
3772
4560
  );
3773
4561
  await runInstalls(selection);
3774
4562
  clearScreen();
@@ -3778,8 +4566,8 @@ Installing: ${selection.map((id) => PLATFORMS.find((p) => p.id === id)?.label ??
3778
4566
  process.exit(0);
3779
4567
  }
3780
4568
  printSetupComplete();
3781
- await promptLine("Press ENTER to open the transcodes CLI dashboard \u2026 ");
3782
- await runDashboard({});
4569
+ await promptLine(t("pressEnterDashboard"));
4570
+ await ensureDashboard({});
3783
4571
  process.exit(0);
3784
4572
  }
3785
4573
  function installedPlatforms() {
@@ -3866,7 +4654,7 @@ async function cmdUpdate(args) {
3866
4654
  }
3867
4655
  } finally {
3868
4656
  if (clonedRepoDir) {
3869
- fs.rmSync(clonedRepoDir, { recursive: true, force: true });
4657
+ fs2.rmSync(clonedRepoDir, { recursive: true, force: true });
3870
4658
  }
3871
4659
  }
3872
4660
  }
@@ -4017,9 +4805,10 @@ async function cmdConsole(args) {
4017
4805
  }
4018
4806
  process.exit(0);
4019
4807
  }
4020
- async function cmdDashboard(args) {
4808
+ function parseDashboardFlags(args, usage) {
4021
4809
  let port;
4022
4810
  let open = true;
4811
+ let daemon = false;
4023
4812
  for (let i = 0; i < args.length; i++) {
4024
4813
  if (args[i] === "--port" && args[i + 1]) {
4025
4814
  port = Number(args[++i]);
@@ -4028,14 +4817,49 @@ async function cmdDashboard(args) {
4028
4817
  }
4029
4818
  } else if (args[i] === "--no-open") {
4030
4819
  open = false;
4820
+ } else if (args[i] === "--daemon") {
4821
+ daemon = true;
4031
4822
  } else {
4032
- fail(
4033
- `unknown flag "${args[i]}". Usage: transcodes [--port N] [--no-open]`
4034
- );
4823
+ fail(`unknown flag "${args[i]}". Usage: ${usage}`);
4824
+ }
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
+ }
4836
+ try {
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;
4035
4853
  }
4854
+ await ensureDashboard({ port, open });
4855
+ } catch (err) {
4856
+ fail(err instanceof Error ? err.message : String(err));
4036
4857
  }
4858
+ process.exit(0);
4859
+ }
4860
+ async function cmdStop() {
4037
4861
  try {
4038
- await runDashboard({ port, open });
4862
+ await stopDashboard();
4039
4863
  } catch (err) {
4040
4864
  fail(err instanceof Error ? err.message : String(err));
4041
4865
  }
@@ -4062,9 +4886,20 @@ function main() {
4062
4886
  case "tokens":
4063
4887
  cmdTokens();
4064
4888
  break;
4889
+ case "login":
4890
+ void cmdLogin(rest).catch((error) => {
4891
+ fail(error instanceof Error ? error.message : String(error));
4892
+ });
4893
+ break;
4065
4894
  case "console":
4066
4895
  void cmdConsole(rest);
4067
4896
  break;
4897
+ case "dashboard":
4898
+ void cmdDashboardSub(rest);
4899
+ break;
4900
+ case "stop":
4901
+ void cmdStop();
4902
+ break;
4068
4903
  case "version":
4069
4904
  case "--version":
4070
4905
  case "-V":