@bigstrider/transcodes-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/dist/index.js +989 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,12 @@ The plugins and their hooks authenticate to the Transcodes backend with a member
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
# no install needed
|
|
11
|
-
npx @bigstrider/transcodes-cli
|
|
10
|
+
# no install needed — opens the dashboard
|
|
11
|
+
npx @bigstrider/transcodes-cli
|
|
12
12
|
|
|
13
13
|
# or global
|
|
14
14
|
npm install -g @bigstrider/transcodes-cli
|
|
15
|
-
transcodes
|
|
15
|
+
transcodes
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Works the same on macOS, Linux, and Windows (Node ≥ 20).
|
|
@@ -21,10 +21,25 @@ Works the same on macOS, Linux, and Windows (Node ≥ 20).
|
|
|
21
21
|
|
|
22
22
|
| Command | What it does |
|
|
23
23
|
|---------|--------------|
|
|
24
|
-
| `transcodes
|
|
25
|
-
| `transcodes
|
|
24
|
+
| `transcodes` | Opens the dashboard at `http://127.0.0.1:3847/` to paste, save, switch, label, or delete tokens (accepts `--port N` / `--no-open`). |
|
|
25
|
+
| `transcodes set <token> -l <label>` | Validates the JWT and saves it (label required) to `~/.transcodes/config.json` (dir `0700`, file `0600`), making it active. |
|
|
26
|
+
| `transcodes tokens` | Lists all saved tokens; the active one is marked with `*`. |
|
|
26
27
|
| `transcodes status` | Shows the active token source (env vs file) and its expiry. |
|
|
27
|
-
| `transcodes
|
|
28
|
+
| `transcodes reset` | Deletes all saved tokens. |
|
|
29
|
+
| `transcodes help` | Shows the full command list and usage. |
|
|
30
|
+
|
|
31
|
+
### Dashboard
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @bigstrider/transcodes-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Starts a small localhost server (127.0.0.1 only), opens your browser, and lets you save, switch, rename, or delete tokens without pasting them on the command line. Multiple tokens are kept in `~/.transcodes/config.json` under `token_list`, each with a label; the active one is stored as `token`.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
|
|
41
|
+
- `--port N` — bind to a specific port (default `3847`; increments if busy)
|
|
42
|
+
- `--no-open` — do not open the browser automatically
|
|
28
43
|
|
|
29
44
|
## Token precedence
|
|
30
45
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/dashboard.ts
|
|
4
|
+
import {
|
|
5
|
+
createServer
|
|
6
|
+
} from "http";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
|
|
3
10
|
// ../stepup-core/dist/jwt.js
|
|
4
11
|
var REQUIRED_AUDIENCE = "transcodes-mcp";
|
|
5
12
|
function isPlainObject(v) {
|
|
@@ -106,38 +113,130 @@ function transcodesConfigDir() {
|
|
|
106
113
|
function transcodesConfigFile() {
|
|
107
114
|
return path.join(transcodesConfigDir(), CONFIG_FILE_NAME);
|
|
108
115
|
}
|
|
109
|
-
function
|
|
116
|
+
function normalizeToken(v) {
|
|
117
|
+
if (typeof v !== "string")
|
|
118
|
+
return null;
|
|
119
|
+
const trimmed = v.trim();
|
|
120
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
121
|
+
}
|
|
122
|
+
function normalizeLabel(v) {
|
|
123
|
+
if (typeof v !== "string")
|
|
124
|
+
return null;
|
|
125
|
+
const trimmed = v.trim();
|
|
126
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
127
|
+
}
|
|
128
|
+
function normalizeRecord(item) {
|
|
129
|
+
if (typeof item === "string") {
|
|
130
|
+
const token = normalizeToken(item);
|
|
131
|
+
return token ? { token, label: null } : null;
|
|
132
|
+
}
|
|
133
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
134
|
+
const obj = item;
|
|
135
|
+
const token = normalizeToken(obj.token);
|
|
136
|
+
if (!token)
|
|
137
|
+
return null;
|
|
138
|
+
return { token, label: normalizeLabel(obj.label) };
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function readConfig() {
|
|
110
143
|
let raw;
|
|
111
144
|
try {
|
|
112
145
|
raw = readFileSync(transcodesConfigFile(), "utf8");
|
|
113
146
|
} catch {
|
|
114
|
-
return null;
|
|
147
|
+
return { token: null, tokenList: [] };
|
|
115
148
|
}
|
|
116
149
|
let parsed;
|
|
117
150
|
try {
|
|
118
151
|
parsed = JSON.parse(raw);
|
|
119
152
|
} catch {
|
|
120
|
-
return null;
|
|
153
|
+
return { token: null, tokenList: [] };
|
|
121
154
|
}
|
|
122
155
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
123
|
-
return null;
|
|
156
|
+
return { token: null, tokenList: [] };
|
|
124
157
|
}
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
158
|
+
const obj = parsed;
|
|
159
|
+
const list = [];
|
|
160
|
+
const seen = /* @__PURE__ */ new Set();
|
|
161
|
+
const push = (rec) => {
|
|
162
|
+
if (!rec)
|
|
163
|
+
return;
|
|
164
|
+
const existing = list.find((r) => r.token === rec.token);
|
|
165
|
+
if (existing) {
|
|
166
|
+
if (!existing.label && rec.label)
|
|
167
|
+
existing.label = rec.label;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
seen.add(rec.token);
|
|
171
|
+
list.push(rec);
|
|
172
|
+
};
|
|
173
|
+
if (Array.isArray(obj.token_list)) {
|
|
174
|
+
for (const item of obj.token_list)
|
|
175
|
+
push(normalizeRecord(item));
|
|
176
|
+
}
|
|
177
|
+
const active = normalizeToken(obj.token);
|
|
178
|
+
if (active)
|
|
179
|
+
push({ token: active, label: null });
|
|
180
|
+
return {
|
|
181
|
+
token: active ?? (list.length > 0 ? list[0].token : null),
|
|
182
|
+
tokenList: list
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function writeConfig(config) {
|
|
186
|
+
const dir = transcodesConfigDir();
|
|
187
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
188
|
+
const token_list = config.tokenList.map((r) => r.label ? { token: r.token, label: r.label } : { token: r.token });
|
|
189
|
+
writeFileSync(transcodesConfigFile(), JSON.stringify({ token: config.token, token_list }), { mode: 384 });
|
|
190
|
+
}
|
|
191
|
+
function readTokenFromFile() {
|
|
192
|
+
return readConfig().token;
|
|
193
|
+
}
|
|
194
|
+
function readTokenList() {
|
|
195
|
+
return readConfig().tokenList.map((r) => r.token);
|
|
130
196
|
}
|
|
131
|
-
function
|
|
197
|
+
function readTokenRecords() {
|
|
198
|
+
return readConfig().tokenList;
|
|
199
|
+
}
|
|
200
|
+
function writeTokenToFile(token, label) {
|
|
132
201
|
const trimmed = token.trim();
|
|
133
202
|
if (!trimmed) {
|
|
134
203
|
throw new Error("token is empty");
|
|
135
204
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
205
|
+
const nextLabel = normalizeLabel(label);
|
|
206
|
+
const current = readConfig();
|
|
207
|
+
const existing = current.tokenList.find((r) => r.token === trimmed);
|
|
208
|
+
if (!existing && !nextLabel) {
|
|
209
|
+
throw new Error("label is required");
|
|
210
|
+
}
|
|
211
|
+
const tokenList = existing ? current.tokenList.map((r) => r.token === trimmed ? { token: trimmed, label: nextLabel ?? r.label } : r) : [...current.tokenList, { token: trimmed, label: nextLabel }];
|
|
212
|
+
writeConfig({ token: trimmed, tokenList });
|
|
213
|
+
}
|
|
214
|
+
function setActiveToken(token) {
|
|
215
|
+
writeTokenToFile(token);
|
|
216
|
+
}
|
|
217
|
+
function setTokenLabel(token, label) {
|
|
218
|
+
const trimmed = token.trim();
|
|
219
|
+
const nextLabel = normalizeLabel(label);
|
|
220
|
+
if (!nextLabel) {
|
|
221
|
+
throw new Error("label is required");
|
|
222
|
+
}
|
|
223
|
+
const current = readConfig();
|
|
224
|
+
if (!current.tokenList.some((r) => r.token === trimmed)) {
|
|
225
|
+
throw new Error("token not found");
|
|
226
|
+
}
|
|
227
|
+
const tokenList = current.tokenList.map((r) => r.token === trimmed ? { token: r.token, label: nextLabel } : r);
|
|
228
|
+
writeConfig({ token: current.token, tokenList });
|
|
229
|
+
}
|
|
230
|
+
function removeTokenFromFile(token) {
|
|
231
|
+
const trimmed = token.trim();
|
|
232
|
+
const current = readConfig();
|
|
233
|
+
const tokenList = current.tokenList.filter((r) => r.token !== trimmed);
|
|
234
|
+
if (tokenList.length === 0) {
|
|
235
|
+
clearTokenFile();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const active = current.token && tokenList.some((r) => r.token === current.token) ? current.token : tokenList[0].token;
|
|
239
|
+
writeConfig({ token: active, tokenList });
|
|
141
240
|
}
|
|
142
241
|
function clearTokenFile() {
|
|
143
242
|
try {
|
|
@@ -157,14 +256,800 @@ function resolveToken() {
|
|
|
157
256
|
return { token: null, source: "none" };
|
|
158
257
|
}
|
|
159
258
|
|
|
259
|
+
// src/logo.ts
|
|
260
|
+
var LOGO_DATA_URI = "data:image/png;base64,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";
|
|
261
|
+
|
|
262
|
+
// src/dashboard.ts
|
|
263
|
+
var DEFAULT_PORT = 3847;
|
|
264
|
+
var HOST = "127.0.0.1";
|
|
265
|
+
function fingerprint(token) {
|
|
266
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
267
|
+
}
|
|
268
|
+
function readJsonBody(req) {
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
const chunks = [];
|
|
271
|
+
req.on("data", (c) => chunks.push(c));
|
|
272
|
+
req.on("end", () => {
|
|
273
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
274
|
+
if (!raw.trim()) {
|
|
275
|
+
resolve({});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
resolve(JSON.parse(raw));
|
|
280
|
+
} catch {
|
|
281
|
+
reject(new Error("invalid JSON body"));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
req.on("error", reject);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function buildStatus() {
|
|
288
|
+
const configPath = transcodesConfigFile();
|
|
289
|
+
const records = readTokenRecords();
|
|
290
|
+
const active = readTokenFromFile();
|
|
291
|
+
const envOverridesFile = Boolean(
|
|
292
|
+
process.env.TRANSCODES_TOKEN?.trim() && active
|
|
293
|
+
);
|
|
294
|
+
const tokens = records.map(({ token, label }) => {
|
|
295
|
+
const entry = {
|
|
296
|
+
id: fingerprint(token),
|
|
297
|
+
active: token === active
|
|
298
|
+
};
|
|
299
|
+
if (label) entry.label = label;
|
|
300
|
+
try {
|
|
301
|
+
const parsed = parseMemberAccessToken(token);
|
|
302
|
+
entry.projectId = parsed.claims.projectId;
|
|
303
|
+
entry.organizationId = parsed.claims.organizationId;
|
|
304
|
+
entry.expiresAt = new Date(parsed.claims.exp * 1e3).toISOString();
|
|
305
|
+
if (parsed.warnings.length > 0) entry.warnings = [...parsed.warnings];
|
|
306
|
+
} catch (err) {
|
|
307
|
+
entry.warnings = [err instanceof Error ? err.message : String(err)];
|
|
308
|
+
}
|
|
309
|
+
return entry;
|
|
310
|
+
});
|
|
311
|
+
return { configPath, envOverridesFile, tokens };
|
|
312
|
+
}
|
|
313
|
+
function tokenById(id) {
|
|
314
|
+
return readTokenList().find((t) => fingerprint(t) === id);
|
|
315
|
+
}
|
|
316
|
+
function sendJson(res, status, body) {
|
|
317
|
+
res.writeHead(status, {
|
|
318
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
319
|
+
"Cache-Control": "no-store"
|
|
320
|
+
});
|
|
321
|
+
res.end(JSON.stringify(body));
|
|
322
|
+
}
|
|
323
|
+
function dashboardHtml() {
|
|
324
|
+
return `<!DOCTYPE html>
|
|
325
|
+
<html lang="en">
|
|
326
|
+
<head>
|
|
327
|
+
<meta charset="utf-8" />
|
|
328
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
329
|
+
<title>Transcodes \u2014 Token</title>
|
|
330
|
+
<style>
|
|
331
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
332
|
+
:root {
|
|
333
|
+
--bg: #f4f4f6;
|
|
334
|
+
--card: #ffffff;
|
|
335
|
+
--line: #ececf0;
|
|
336
|
+
--ink: #16161a;
|
|
337
|
+
--muted: #8a8a94;
|
|
338
|
+
--accent: #5b54e6;
|
|
339
|
+
--accent-soft: #eeedfb;
|
|
340
|
+
}
|
|
341
|
+
body {
|
|
342
|
+
margin: 0;
|
|
343
|
+
min-height: 100vh;
|
|
344
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
345
|
+
background: var(--bg);
|
|
346
|
+
color: var(--ink);
|
|
347
|
+
display: flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
justify-content: center;
|
|
350
|
+
padding: 32px;
|
|
351
|
+
-webkit-font-smoothing: antialiased;
|
|
352
|
+
}
|
|
353
|
+
.card {
|
|
354
|
+
width: 100%;
|
|
355
|
+
max-width: 460px;
|
|
356
|
+
background: var(--card);
|
|
357
|
+
border-radius: 24px;
|
|
358
|
+
padding: 34px;
|
|
359
|
+
box-shadow: 0 1px 2px rgba(16, 16, 26, 0.04), 0 12px 40px rgba(16, 16, 26, 0.06);
|
|
360
|
+
}
|
|
361
|
+
.header {
|
|
362
|
+
display: flex;
|
|
363
|
+
align-items: center;
|
|
364
|
+
gap: 16px;
|
|
365
|
+
padding-bottom: 24px;
|
|
366
|
+
border-bottom: 1px solid var(--line);
|
|
367
|
+
}
|
|
368
|
+
.avatar {
|
|
369
|
+
width: 56px;
|
|
370
|
+
height: 56px;
|
|
371
|
+
border-radius: 14px;
|
|
372
|
+
flex-shrink: 0;
|
|
373
|
+
object-fit: contain;
|
|
374
|
+
background: #f4f4f6;
|
|
375
|
+
padding: 8px;
|
|
376
|
+
}
|
|
377
|
+
.header h1 {
|
|
378
|
+
margin: 0;
|
|
379
|
+
font-size: 21px;
|
|
380
|
+
font-weight: 700;
|
|
381
|
+
letter-spacing: -0.02em;
|
|
382
|
+
}
|
|
383
|
+
.header p {
|
|
384
|
+
margin: 5px 0 0;
|
|
385
|
+
font-size: 14px;
|
|
386
|
+
color: var(--muted);
|
|
387
|
+
}
|
|
388
|
+
.tabs {
|
|
389
|
+
display: flex;
|
|
390
|
+
gap: 4px;
|
|
391
|
+
margin-top: 22px;
|
|
392
|
+
padding: 4px;
|
|
393
|
+
background: #f4f4f6;
|
|
394
|
+
border-radius: 13px;
|
|
395
|
+
}
|
|
396
|
+
.tab {
|
|
397
|
+
flex: 1;
|
|
398
|
+
padding: 9px 12px;
|
|
399
|
+
font-size: 13.5px;
|
|
400
|
+
font-weight: 600;
|
|
401
|
+
color: var(--muted);
|
|
402
|
+
background: transparent;
|
|
403
|
+
border: none;
|
|
404
|
+
border-radius: 9px;
|
|
405
|
+
cursor: pointer;
|
|
406
|
+
transition: background 0.15s, color 0.15s;
|
|
407
|
+
}
|
|
408
|
+
.tab:hover { color: var(--ink); }
|
|
409
|
+
.tab.active {
|
|
410
|
+
background: #fff;
|
|
411
|
+
color: var(--ink);
|
|
412
|
+
box-shadow: 0 1px 2px rgba(16, 16, 26, 0.08);
|
|
413
|
+
}
|
|
414
|
+
.panel { display: none; padding-top: 26px; }
|
|
415
|
+
.panel.active { display: block; }
|
|
416
|
+
.section-title {
|
|
417
|
+
font-size: 17px;
|
|
418
|
+
font-weight: 700;
|
|
419
|
+
margin: 0 0 4px;
|
|
420
|
+
letter-spacing: -0.01em;
|
|
421
|
+
}
|
|
422
|
+
.section-sub {
|
|
423
|
+
font-size: 14px;
|
|
424
|
+
color: var(--muted);
|
|
425
|
+
margin: 0 0 20px;
|
|
426
|
+
}
|
|
427
|
+
textarea {
|
|
428
|
+
width: 100%;
|
|
429
|
+
min-height: 92px;
|
|
430
|
+
padding: 14px 16px;
|
|
431
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
432
|
+
font-size: 12.5px;
|
|
433
|
+
line-height: 1.5;
|
|
434
|
+
color: var(--ink);
|
|
435
|
+
background: #fbfbfc;
|
|
436
|
+
border: 1px solid var(--line);
|
|
437
|
+
border-radius: 14px;
|
|
438
|
+
resize: vertical;
|
|
439
|
+
outline: none;
|
|
440
|
+
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
|
|
441
|
+
}
|
|
442
|
+
textarea:focus {
|
|
443
|
+
background: #fff;
|
|
444
|
+
border-color: var(--accent);
|
|
445
|
+
box-shadow: 0 0 0 4px rgba(91, 84, 230, 0.12);
|
|
446
|
+
}
|
|
447
|
+
textarea::placeholder { color: #b9b9c2; }
|
|
448
|
+
.label-input {
|
|
449
|
+
width: 100%;
|
|
450
|
+
margin-top: 12px;
|
|
451
|
+
padding: 12px 16px;
|
|
452
|
+
font-size: 13.5px;
|
|
453
|
+
color: var(--ink);
|
|
454
|
+
background: #fbfbfc;
|
|
455
|
+
border: 1px solid var(--line);
|
|
456
|
+
border-radius: 14px;
|
|
457
|
+
outline: none;
|
|
458
|
+
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
|
|
459
|
+
}
|
|
460
|
+
.label-input:focus {
|
|
461
|
+
background: #fff;
|
|
462
|
+
border-color: var(--accent);
|
|
463
|
+
box-shadow: 0 0 0 4px rgba(91, 84, 230, 0.12);
|
|
464
|
+
}
|
|
465
|
+
.label-input::placeholder { color: #b9b9c2; }
|
|
466
|
+
.actions {
|
|
467
|
+
display: flex;
|
|
468
|
+
gap: 12px;
|
|
469
|
+
margin-top: 18px;
|
|
470
|
+
}
|
|
471
|
+
button {
|
|
472
|
+
flex: 1;
|
|
473
|
+
padding: 13px 18px;
|
|
474
|
+
font-size: 15px;
|
|
475
|
+
font-weight: 600;
|
|
476
|
+
border-radius: 14px;
|
|
477
|
+
border: none;
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
transition: background 0.15s, opacity 0.15s, transform 0.05s;
|
|
480
|
+
}
|
|
481
|
+
button:active:not(:disabled) { transform: translateY(1px); }
|
|
482
|
+
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
483
|
+
.btn-primary { background: var(--accent); color: #fff; }
|
|
484
|
+
.btn-primary:hover:not(:disabled) { background: #4a43d4; }
|
|
485
|
+
.btn-secondary {
|
|
486
|
+
background: #f4f4f6;
|
|
487
|
+
color: #5a5a64;
|
|
488
|
+
}
|
|
489
|
+
.btn-secondary:hover:not(:disabled) { background: #ececf0; }
|
|
490
|
+
.list-label {
|
|
491
|
+
margin: 26px 0 10px;
|
|
492
|
+
font-size: 13px;
|
|
493
|
+
font-weight: 600;
|
|
494
|
+
color: var(--muted);
|
|
495
|
+
letter-spacing: 0.01em;
|
|
496
|
+
}
|
|
497
|
+
.token-list { display: flex; flex-direction: column; gap: 10px; }
|
|
498
|
+
.token-empty {
|
|
499
|
+
padding: 16px 18px;
|
|
500
|
+
background: #fbfbfc;
|
|
501
|
+
border: 1px dashed var(--line);
|
|
502
|
+
border-radius: 16px;
|
|
503
|
+
font-size: 13.5px;
|
|
504
|
+
color: var(--muted);
|
|
505
|
+
text-align: center;
|
|
506
|
+
}
|
|
507
|
+
.token-row {
|
|
508
|
+
display: flex;
|
|
509
|
+
flex-direction: column;
|
|
510
|
+
gap: 14px;
|
|
511
|
+
padding: 16px;
|
|
512
|
+
background: #fbfbfc;
|
|
513
|
+
border: 1px solid var(--line);
|
|
514
|
+
border-radius: 16px;
|
|
515
|
+
transition: border-color 0.15s, background 0.15s;
|
|
516
|
+
}
|
|
517
|
+
.token-row.active {
|
|
518
|
+
border-color: var(--accent);
|
|
519
|
+
background: var(--accent-soft);
|
|
520
|
+
}
|
|
521
|
+
.token-top { display: flex; align-items: center; gap: 14px; }
|
|
522
|
+
.radio {
|
|
523
|
+
width: 18px;
|
|
524
|
+
height: 18px;
|
|
525
|
+
border-radius: 50%;
|
|
526
|
+
border: 2px solid #d0d0d8;
|
|
527
|
+
flex-shrink: 0;
|
|
528
|
+
position: relative;
|
|
529
|
+
transition: border-color 0.15s;
|
|
530
|
+
}
|
|
531
|
+
.token-row.active .radio { border-color: var(--accent); }
|
|
532
|
+
.token-row.active .radio::after {
|
|
533
|
+
content: "";
|
|
534
|
+
position: absolute;
|
|
535
|
+
inset: 3px;
|
|
536
|
+
border-radius: 50%;
|
|
537
|
+
background: var(--accent);
|
|
538
|
+
}
|
|
539
|
+
.token-info { flex: 1; min-width: 0; line-height: 1.45; }
|
|
540
|
+
.token-info .label {
|
|
541
|
+
font-size: 14.5px;
|
|
542
|
+
font-weight: 700;
|
|
543
|
+
color: var(--ink);
|
|
544
|
+
margin-bottom: 4px;
|
|
545
|
+
}
|
|
546
|
+
.token-info .field { font-size: 13px; color: #4a4a52; }
|
|
547
|
+
.token-info .field .k { color: var(--muted); }
|
|
548
|
+
.token-info .field code {
|
|
549
|
+
font-size: 12px;
|
|
550
|
+
color: var(--ink);
|
|
551
|
+
background: #fff;
|
|
552
|
+
border: 1px solid var(--line);
|
|
553
|
+
padding: 1px 7px;
|
|
554
|
+
border-radius: 6px;
|
|
555
|
+
}
|
|
556
|
+
.token-row.active .token-info .field code { background: #fff; }
|
|
557
|
+
.token-info .warn { font-size: 12px; color: #c0392f; margin-top: 2px; }
|
|
558
|
+
.token-actions {
|
|
559
|
+
display: flex;
|
|
560
|
+
gap: 8px;
|
|
561
|
+
padding-top: 12px;
|
|
562
|
+
border-top: 1px solid var(--line);
|
|
563
|
+
}
|
|
564
|
+
.token-row.active .token-actions { border-top-color: rgba(91, 84, 230, 0.18); }
|
|
565
|
+
.token-actions button {
|
|
566
|
+
flex: 1;
|
|
567
|
+
padding: 9px 12px;
|
|
568
|
+
font-size: 12px;
|
|
569
|
+
font-weight: 600;
|
|
570
|
+
letter-spacing: 0.03em;
|
|
571
|
+
border-radius: 10px;
|
|
572
|
+
border: 1px solid var(--line);
|
|
573
|
+
background: #fff;
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.15s;
|
|
576
|
+
}
|
|
577
|
+
.btn-set { color: var(--accent); }
|
|
578
|
+
.btn-set:hover:not(:disabled) { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
579
|
+
.btn-set:disabled { opacity: 0.45; cursor: default; }
|
|
580
|
+
.btn-del { color: #c0392f; }
|
|
581
|
+
.btn-del:hover { background: #c0392f; color: #fff; border-color: #c0392f; }
|
|
582
|
+
.btn-edit, .btn-cancel { color: #5a5a64; }
|
|
583
|
+
.btn-edit:hover, .btn-cancel:hover { background: #ececf0; color: var(--ink); border-color: #dcdce2; }
|
|
584
|
+
.label-edit {
|
|
585
|
+
width: 100%;
|
|
586
|
+
padding: 9px 12px;
|
|
587
|
+
font-size: 13.5px;
|
|
588
|
+
font-weight: 600;
|
|
589
|
+
color: var(--ink);
|
|
590
|
+
background: #fff;
|
|
591
|
+
border: 1px solid var(--accent);
|
|
592
|
+
border-radius: 9px;
|
|
593
|
+
outline: none;
|
|
594
|
+
box-shadow: 0 0 0 4px rgba(91, 84, 230, 0.12);
|
|
595
|
+
}
|
|
596
|
+
.cmd-list { display: flex; flex-direction: column; gap: 10px; }
|
|
597
|
+
.cmd {
|
|
598
|
+
padding: 14px 16px;
|
|
599
|
+
background: #fbfbfc;
|
|
600
|
+
border: 1px solid var(--line);
|
|
601
|
+
border-radius: 14px;
|
|
602
|
+
}
|
|
603
|
+
.cmd code {
|
|
604
|
+
display: inline-block;
|
|
605
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
606
|
+
font-size: 12.5px;
|
|
607
|
+
color: var(--accent);
|
|
608
|
+
background: var(--accent-soft);
|
|
609
|
+
padding: 3px 9px;
|
|
610
|
+
border-radius: 7px;
|
|
611
|
+
}
|
|
612
|
+
.cmd .cmd-desc {
|
|
613
|
+
display: block;
|
|
614
|
+
margin-top: 8px;
|
|
615
|
+
font-size: 13px;
|
|
616
|
+
color: var(--muted);
|
|
617
|
+
line-height: 1.5;
|
|
618
|
+
}
|
|
619
|
+
.toast {
|
|
620
|
+
margin-top: 14px;
|
|
621
|
+
padding: 12px 16px;
|
|
622
|
+
border-radius: 12px;
|
|
623
|
+
font-size: 13.5px;
|
|
624
|
+
font-weight: 500;
|
|
625
|
+
display: none;
|
|
626
|
+
}
|
|
627
|
+
.toast.show { display: block; }
|
|
628
|
+
.toast.success { background: #effaf2; color: #1a7f45; }
|
|
629
|
+
.toast.error { background: #fdf0f0; color: #c0392f; }
|
|
630
|
+
.hint {
|
|
631
|
+
margin: 18px 0 0;
|
|
632
|
+
font-size: 12.5px;
|
|
633
|
+
color: #b9b9c2;
|
|
634
|
+
text-align: center;
|
|
635
|
+
line-height: 1.6;
|
|
636
|
+
}
|
|
637
|
+
.hint code {
|
|
638
|
+
font-size: 11.5px;
|
|
639
|
+
background: #f4f4f6;
|
|
640
|
+
padding: 2px 7px;
|
|
641
|
+
border-radius: 6px;
|
|
642
|
+
color: #8a8a94;
|
|
643
|
+
}
|
|
644
|
+
</style>
|
|
645
|
+
</head>
|
|
646
|
+
<body>
|
|
647
|
+
<div class="card">
|
|
648
|
+
<div class="header">
|
|
649
|
+
<img class="avatar" src="${LOGO_DATA_URI}" alt="Transcodes" />
|
|
650
|
+
<div>
|
|
651
|
+
<h1>Transcodes</h1>
|
|
652
|
+
<p>CLI Dashboard</p>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
<div class="tabs">
|
|
656
|
+
<button type="button" class="tab active" data-tab="tokens">Tokens</button>
|
|
657
|
+
<button type="button" class="tab" data-tab="cli">CLI Commands</button>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
<div class="panel active" id="panel-tokens">
|
|
661
|
+
<p class="section-title">MCP Agent Token</p>
|
|
662
|
+
<p class="section-sub">Paste the token from your Transcodes console member detail page</p>
|
|
663
|
+
<textarea id="token" placeholder="eyJhbGciOi\u2026" spellcheck="false" autocomplete="off"></textarea>
|
|
664
|
+
<input type="text" id="label" class="label-input" placeholder="Label (required) \u2014 e.g. transcodes-{project_name}-{env}" autocomplete="off" required />
|
|
665
|
+
<div class="actions">
|
|
666
|
+
<button type="button" class="btn-primary" id="save">Save</button>
|
|
667
|
+
<button type="button" class="btn-secondary" id="clear">Clear</button>
|
|
668
|
+
</div>
|
|
669
|
+
<div id="toast" class="toast"></div>
|
|
670
|
+
<p class="list-label">Saved tokens</p>
|
|
671
|
+
<div class="token-list" id="token-list"></div>
|
|
672
|
+
<p class="hint">Saved to <code>{{HOME_DIR}}/.transcodes/config.json</code><br />Press Ctrl+C in the terminal to stop</p>
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
<div class="panel" id="panel-cli">
|
|
676
|
+
<p class="section-title">CLI Commands</p>
|
|
677
|
+
<p class="section-sub">Run these from your terminal \u2014 the dashboard wraps the same actions</p>
|
|
678
|
+
<div class="cmd-list">
|
|
679
|
+
<div class="cmd"><code>transcodes</code><span class="cmd-desc">Open this dashboard (default, same as transcodes dashboard)</span></div>
|
|
680
|
+
<div class="cmd"><code>transcodes set <token> -l <label></code><span class="cmd-desc">Validate and save a token with a label, then make it active</span></div>
|
|
681
|
+
<div class="cmd"><code>transcodes tokens</code><span class="cmd-desc">List all saved tokens (active one marked with *)</span></div>
|
|
682
|
+
<div class="cmd"><code>transcodes reset</code><span class="cmd-desc">Remove all saved tokens</span></div>
|
|
683
|
+
<div class="cmd"><code>transcodes help</code><span class="cmd-desc">Show the full command list and how to use each one</span></div>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
<script>
|
|
688
|
+
const tokenEl = document.getElementById("token");
|
|
689
|
+
const labelEl = document.getElementById("label");
|
|
690
|
+
const toastEl = document.getElementById("toast");
|
|
691
|
+
const listEl = document.getElementById("token-list");
|
|
692
|
+
const saveBtn = document.getElementById("save");
|
|
693
|
+
const clearBtn = document.getElementById("clear");
|
|
694
|
+
|
|
695
|
+
document.querySelectorAll(".tab").forEach((tab) => {
|
|
696
|
+
tab.addEventListener("click", () => {
|
|
697
|
+
const name = tab.getAttribute("data-tab");
|
|
698
|
+
document.querySelectorAll(".tab").forEach((t) =>
|
|
699
|
+
t.classList.toggle("active", t === tab));
|
|
700
|
+
document.querySelectorAll(".panel").forEach((p) =>
|
|
701
|
+
p.classList.toggle("active", p.id === "panel-" + name));
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
function showToast(msg, kind) {
|
|
706
|
+
toastEl.textContent = msg;
|
|
707
|
+
toastEl.className = "toast show " + (kind || "success");
|
|
708
|
+
setTimeout(() => toastEl.classList.remove("show"), 4000);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function esc(s) {
|
|
712
|
+
return String(s).replace(/[&<>"]/g, (c) =>
|
|
713
|
+
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let lastStatus = { tokens: [] };
|
|
717
|
+
let editingId = null;
|
|
718
|
+
|
|
719
|
+
function renderTokens(s) {
|
|
720
|
+
if (!s.tokens || s.tokens.length === 0) {
|
|
721
|
+
listEl.innerHTML = '<div class="token-empty">No tokens saved yet \u2014 paste one above and press Save</div>';
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
listEl.innerHTML = s.tokens.map((t) => {
|
|
726
|
+
const editing = t.id === editingId;
|
|
727
|
+
const project = t.projectId
|
|
728
|
+
? '<div class="field"><span class="k">Project Id</span> <code>' + esc(t.projectId) + '</code></div>'
|
|
729
|
+
: '';
|
|
730
|
+
const org = t.organizationId
|
|
731
|
+
? '<div class="field"><span class="k">Organization Id</span> <code>' + esc(t.organizationId) + '</code></div>'
|
|
732
|
+
: '';
|
|
733
|
+
const warn = t.warnings && t.warnings.length
|
|
734
|
+
? '<div class="warn">' + esc(t.warnings.join("; ")) + '</div>'
|
|
735
|
+
: '';
|
|
736
|
+
const labelBlock = editing
|
|
737
|
+
? '<input type="text" class="label-edit" data-edit-input="' + t.id + '" value="' + esc(t.label || "") + '" placeholder="Label" />'
|
|
738
|
+
: (t.label ? '<div class="label">' + esc(t.label) + '</div>' : '');
|
|
739
|
+
const actions = editing
|
|
740
|
+
? '<button type="button" class="btn-set" data-save-label="' + t.id + '">SAVE</button>' +
|
|
741
|
+
'<button type="button" class="btn-cancel" data-cancel-edit="1">CANCEL</button>'
|
|
742
|
+
: '<button type="button" class="btn-edit" data-edit="' + t.id + '">EDIT</button>' +
|
|
743
|
+
'<button type="button" class="btn-set" data-set="' + t.id + '"' + (t.active ? " disabled" : "") + '>' +
|
|
744
|
+
(t.active ? "DEFAULT" : "SET DEFAULT") +
|
|
745
|
+
'</button>' +
|
|
746
|
+
'<button type="button" class="btn-del" data-del="' + t.id + '">DELETE</button>';
|
|
747
|
+
return (
|
|
748
|
+
'<div class="token-row' + (t.active ? " active" : "") + '" data-id="' + t.id + '">' +
|
|
749
|
+
'<div class="token-top">' +
|
|
750
|
+
'<span class="radio"></span>' +
|
|
751
|
+
'<div class="token-info">' + labelBlock + org + project + warn + '</div>' +
|
|
752
|
+
'</div>' +
|
|
753
|
+
'<div class="token-actions">' + actions + '</div>' +
|
|
754
|
+
'</div>'
|
|
755
|
+
);
|
|
756
|
+
}).join("");
|
|
757
|
+
|
|
758
|
+
if (editingId) {
|
|
759
|
+
const el = listEl.querySelector('[data-edit-input="' + editingId + '"]');
|
|
760
|
+
if (el) { el.focus(); el.select(); }
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function refresh() {
|
|
765
|
+
const res = await fetch("/api/status");
|
|
766
|
+
lastStatus = await res.json();
|
|
767
|
+
renderTokens(lastStatus);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function saveLabel(id) {
|
|
771
|
+
const input = listEl.querySelector('[data-edit-input="' + id + '"]');
|
|
772
|
+
const label = input ? input.value.trim() : "";
|
|
773
|
+
if (!label) {
|
|
774
|
+
showToast("Label cannot be empty", "error");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
const res = await fetch("/api/label", {
|
|
779
|
+
method: "POST",
|
|
780
|
+
headers: { "Content-Type": "application/json" },
|
|
781
|
+
body: JSON.stringify({ id, label }),
|
|
782
|
+
});
|
|
783
|
+
const data = await res.json();
|
|
784
|
+
if (!res.ok) throw new Error(data.error || "Rename failed");
|
|
785
|
+
editingId = null;
|
|
786
|
+
showToast("Label updated", "success");
|
|
787
|
+
refresh();
|
|
788
|
+
} catch (e) {
|
|
789
|
+
showToast(e.message || "Rename failed", "error");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function setDefault(id) {
|
|
794
|
+
try {
|
|
795
|
+
const res = await fetch("/api/select", {
|
|
796
|
+
method: "POST",
|
|
797
|
+
headers: { "Content-Type": "application/json" },
|
|
798
|
+
body: JSON.stringify({ id }),
|
|
799
|
+
});
|
|
800
|
+
const data = await res.json();
|
|
801
|
+
if (!res.ok) throw new Error(data.error || "Set default failed");
|
|
802
|
+
showToast("Default token updated", "success");
|
|
803
|
+
refresh();
|
|
804
|
+
} catch (e) {
|
|
805
|
+
showToast(e.message || "Set default failed", "error");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function removeToken(id) {
|
|
810
|
+
if (!confirm("Delete this token from the saved list?")) return;
|
|
811
|
+
try {
|
|
812
|
+
const res = await fetch("/api/token", {
|
|
813
|
+
method: "DELETE",
|
|
814
|
+
headers: { "Content-Type": "application/json" },
|
|
815
|
+
body: JSON.stringify({ id }),
|
|
816
|
+
});
|
|
817
|
+
const data = await res.json();
|
|
818
|
+
if (!res.ok) throw new Error(data.error || "Delete failed");
|
|
819
|
+
showToast("Token deleted", "success");
|
|
820
|
+
refresh();
|
|
821
|
+
} catch (e) {
|
|
822
|
+
showToast(e.message || "Delete failed", "error");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
listEl.addEventListener("click", (e) => {
|
|
827
|
+
const editId = e.target.getAttribute("data-edit");
|
|
828
|
+
if (editId) { editingId = editId; renderTokens(lastStatus); return; }
|
|
829
|
+
const saveId = e.target.getAttribute("data-save-label");
|
|
830
|
+
if (saveId) { saveLabel(saveId); return; }
|
|
831
|
+
if (e.target.getAttribute("data-cancel-edit")) {
|
|
832
|
+
editingId = null; renderTokens(lastStatus); return;
|
|
833
|
+
}
|
|
834
|
+
const setId = e.target.getAttribute("data-set");
|
|
835
|
+
if (setId) { setDefault(setId); return; }
|
|
836
|
+
const delId = e.target.getAttribute("data-del");
|
|
837
|
+
if (delId) { removeToken(delId); return; }
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
listEl.addEventListener("keydown", (e) => {
|
|
841
|
+
const input = e.target.closest(".label-edit");
|
|
842
|
+
if (!input) return;
|
|
843
|
+
if (e.key === "Enter") { e.preventDefault(); saveLabel(editingId); }
|
|
844
|
+
else if (e.key === "Escape") { editingId = null; renderTokens(lastStatus); }
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
saveBtn.addEventListener("click", async () => {
|
|
848
|
+
const token = tokenEl.value.trim();
|
|
849
|
+
const label = labelEl.value.trim();
|
|
850
|
+
if (!token) {
|
|
851
|
+
showToast("Paste a token first", "error");
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (!label) {
|
|
855
|
+
showToast("Add a label first", "error");
|
|
856
|
+
labelEl.focus();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
saveBtn.disabled = true;
|
|
860
|
+
try {
|
|
861
|
+
const res = await fetch("/api/token", {
|
|
862
|
+
method: "POST",
|
|
863
|
+
headers: { "Content-Type": "application/json" },
|
|
864
|
+
body: JSON.stringify({ token, label }),
|
|
865
|
+
});
|
|
866
|
+
const data = await res.json();
|
|
867
|
+
if (!res.ok) throw new Error(data.error || "Save failed");
|
|
868
|
+
tokenEl.value = "";
|
|
869
|
+
labelEl.value = "";
|
|
870
|
+
showToast("Token saved", "success");
|
|
871
|
+
refresh();
|
|
872
|
+
} catch (e) {
|
|
873
|
+
showToast(e.message || "Save failed", "error");
|
|
874
|
+
} finally {
|
|
875
|
+
saveBtn.disabled = false;
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
clearBtn.addEventListener("click", () => {
|
|
880
|
+
tokenEl.value = "";
|
|
881
|
+
labelEl.value = "";
|
|
882
|
+
tokenEl.focus();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
refresh();
|
|
886
|
+
</script>
|
|
887
|
+
</body>
|
|
888
|
+
</html>`;
|
|
889
|
+
}
|
|
890
|
+
function openBrowser(url) {
|
|
891
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
892
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
893
|
+
try {
|
|
894
|
+
const child = spawn(opener, args, { stdio: "ignore", detached: true });
|
|
895
|
+
child.on("error", () => {
|
|
896
|
+
});
|
|
897
|
+
child.unref();
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function listen(port) {
|
|
902
|
+
return new Promise((resolve, reject) => {
|
|
903
|
+
const server = createServer(async (req, res) => {
|
|
904
|
+
const url = req.url ?? "/";
|
|
905
|
+
const method = req.method ?? "GET";
|
|
906
|
+
try {
|
|
907
|
+
if (method === "GET" && (url === "/" || url === "/index.html")) {
|
|
908
|
+
res.writeHead(200, {
|
|
909
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
910
|
+
"Cache-Control": "no-store"
|
|
911
|
+
});
|
|
912
|
+
res.end(dashboardHtml());
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (method === "GET" && url === "/api/status") {
|
|
916
|
+
sendJson(res, 200, buildStatus());
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (method === "POST" && url === "/api/token") {
|
|
920
|
+
const body = await readJsonBody(req);
|
|
921
|
+
const token = typeof body.token === "string" ? body.token.trim() : "";
|
|
922
|
+
const label = typeof body.label === "string" ? body.label.trim() : "";
|
|
923
|
+
if (!token) {
|
|
924
|
+
sendJson(res, 400, { error: "token is required" });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (!label) {
|
|
928
|
+
sendJson(res, 400, { error: "label is required" });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
parseMemberAccessToken(token);
|
|
933
|
+
} catch (err) {
|
|
934
|
+
sendJson(res, 400, {
|
|
935
|
+
error: err instanceof Error ? err.message : String(err)
|
|
936
|
+
});
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
writeTokenToFile(token, label);
|
|
940
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (method === "POST" && url === "/api/label") {
|
|
944
|
+
const body = await readJsonBody(req);
|
|
945
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
946
|
+
const label = typeof body.label === "string" ? body.label.trim() : "";
|
|
947
|
+
const token = id ? tokenById(id) : void 0;
|
|
948
|
+
if (!token) {
|
|
949
|
+
sendJson(res, 404, { error: "token not found" });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (!label) {
|
|
953
|
+
sendJson(res, 400, { error: "label is required" });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
setTokenLabel(token, label);
|
|
957
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (method === "POST" && url === "/api/select") {
|
|
961
|
+
const body = await readJsonBody(req);
|
|
962
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
963
|
+
const token = id ? tokenById(id) : void 0;
|
|
964
|
+
if (!token) {
|
|
965
|
+
sendJson(res, 404, { error: "token not found" });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
setActiveToken(token);
|
|
969
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (method === "DELETE" && url === "/api/token") {
|
|
973
|
+
const body = await readJsonBody(req);
|
|
974
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
975
|
+
const token = id ? tokenById(id) : void 0;
|
|
976
|
+
if (!token) {
|
|
977
|
+
sendJson(res, 404, { error: "token not found" });
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
removeTokenFromFile(token);
|
|
981
|
+
sendJson(res, 200, { ok: true, ...buildStatus() });
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
sendJson(res, 404, { error: "not found" });
|
|
985
|
+
} catch (err) {
|
|
986
|
+
sendJson(res, 500, {
|
|
987
|
+
error: err instanceof Error ? err.message : String(err)
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
server.on("error", reject);
|
|
992
|
+
server.listen(port, HOST, () => resolve(server));
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
async function runDashboard(options) {
|
|
996
|
+
const preferred = options.port ?? DEFAULT_PORT;
|
|
997
|
+
let server;
|
|
998
|
+
let port = preferred;
|
|
999
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
1000
|
+
try {
|
|
1001
|
+
server = await listen(port);
|
|
1002
|
+
break;
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
const code = err.code;
|
|
1005
|
+
if (code === "EADDRINUSE") {
|
|
1006
|
+
port += 1;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
throw err;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (!server) {
|
|
1013
|
+
const last = preferred + 9;
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
`could not find a free port in ${preferred}-${last} (all in use).
|
|
1016
|
+
A previous dashboard is probably still running.
|
|
1017
|
+
Tip: if you stopped one with Ctrl+Z it is only suspended (still alive) \u2014 use Ctrl+C to stop it.
|
|
1018
|
+
Free the ports and retry:
|
|
1019
|
+
macOS/Linux: lsof -ti tcp:${preferred}-${last} | xargs kill -9
|
|
1020
|
+
any platform: npx kill-port ${preferred} ${preferred + 1} # repeat per port
|
|
1021
|
+
Or choose another port: transcodes --port <N>`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
const url = `http://${HOST}:${port}/`;
|
|
1025
|
+
process.stdout.write(
|
|
1026
|
+
`Transcodes dashboard running at ${url}
|
|
1027
|
+
Config file: ${transcodesConfigFile()}
|
|
1028
|
+
Press Ctrl+C to stop
|
|
1029
|
+
`
|
|
1030
|
+
);
|
|
1031
|
+
if (options.open !== false) {
|
|
1032
|
+
openBrowser(url);
|
|
1033
|
+
}
|
|
1034
|
+
await new Promise((resolve) => {
|
|
1035
|
+
const onSignal = () => {
|
|
1036
|
+
server.close(() => resolve());
|
|
1037
|
+
};
|
|
1038
|
+
process.on("SIGINT", onSignal);
|
|
1039
|
+
process.on("SIGTERM", onSignal);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
160
1043
|
// src/index.ts
|
|
161
1044
|
var USAGE = `transcodes \u2014 ai-action-tracker token manager
|
|
162
1045
|
|
|
163
1046
|
Usage:
|
|
164
|
-
transcodes
|
|
165
|
-
transcodes
|
|
166
|
-
transcodes
|
|
167
|
-
transcodes
|
|
1047
|
+
transcodes Open the dashboard at http://127.0.0.1:3847/ (add --port N or --no-open)
|
|
1048
|
+
transcodes set <token> -l <label> Save your Transcodes member token (label required) to ${transcodesConfigFile()}
|
|
1049
|
+
transcodes reset Remove all saved tokens
|
|
1050
|
+
transcodes status Show where the active token comes from
|
|
1051
|
+
transcodes tokens List all saved tokens (active one marked with *)
|
|
1052
|
+
transcodes help Show this message
|
|
168
1053
|
|
|
169
1054
|
The token is read by the ai-action-tracker plugins/hooks with precedence:
|
|
170
1055
|
1. TRANSCODES_TOKEN environment variable (overrides everything)
|
|
@@ -185,11 +1070,27 @@ function expiryLine(token) {
|
|
|
185
1070
|
return `unable to decode token: ${err instanceof Error ? err.message : String(err)}`;
|
|
186
1071
|
}
|
|
187
1072
|
}
|
|
188
|
-
function
|
|
1073
|
+
function cmdSet(args) {
|
|
1074
|
+
let token;
|
|
1075
|
+
let label;
|
|
1076
|
+
for (let i = 0; i < args.length; i++) {
|
|
1077
|
+
const arg = args[i];
|
|
1078
|
+
if (arg === "-l" || arg === "--label") {
|
|
1079
|
+
label = args[++i];
|
|
1080
|
+
} else if (token === void 0) {
|
|
1081
|
+
token = arg;
|
|
1082
|
+
} else {
|
|
1083
|
+
fail(`unexpected argument "${arg}". Usage: transcodes set <token> -l <label>`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
189
1086
|
if (!token || !token.trim()) {
|
|
190
|
-
fail("missing token. Usage: transcodes
|
|
1087
|
+
fail("missing token. Usage: transcodes set <token> -l <label>");
|
|
1088
|
+
}
|
|
1089
|
+
if (!label || !label.trim()) {
|
|
1090
|
+
fail("missing label. Usage: transcodes set <token> -l <label>");
|
|
191
1091
|
}
|
|
192
1092
|
const trimmed = token.trim();
|
|
1093
|
+
const trimmedLabel = label.trim();
|
|
193
1094
|
try {
|
|
194
1095
|
parseMemberAccessToken(trimmed);
|
|
195
1096
|
} catch (err) {
|
|
@@ -198,7 +1099,7 @@ function cmdLogin(token) {
|
|
|
198
1099
|
);
|
|
199
1100
|
}
|
|
200
1101
|
try {
|
|
201
|
-
writeTokenToFile(trimmed);
|
|
1102
|
+
writeTokenToFile(trimmed, trimmedLabel);
|
|
202
1103
|
} catch (err) {
|
|
203
1104
|
fail(
|
|
204
1105
|
`could not write token file: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -206,20 +1107,20 @@ function cmdLogin(token) {
|
|
|
206
1107
|
}
|
|
207
1108
|
process.stdout.write(
|
|
208
1109
|
`Saved to ${transcodesConfigFile()}
|
|
209
|
-
${expiryLine(trimmed)}
|
|
1110
|
+
label=${trimmedLabel} ${expiryLine(trimmed)}
|
|
210
1111
|
`
|
|
211
1112
|
);
|
|
212
1113
|
}
|
|
213
|
-
function
|
|
1114
|
+
function cmdReset() {
|
|
214
1115
|
clearTokenFile();
|
|
215
|
-
process.stdout.write(`Removed ${transcodesConfigFile()}
|
|
1116
|
+
process.stdout.write(`Removed all saved tokens (${transcodesConfigFile()})
|
|
216
1117
|
`);
|
|
217
1118
|
}
|
|
218
1119
|
function cmdStatus() {
|
|
219
1120
|
const { token, source } = resolveToken();
|
|
220
1121
|
if (source === "none" || !token) {
|
|
221
1122
|
process.stdout.write(
|
|
222
|
-
"No token configured. Run `transcodes
|
|
1123
|
+
"No token configured. Run `transcodes set <token> -l <label>` or `transcodes` to set one.\n"
|
|
223
1124
|
);
|
|
224
1125
|
return;
|
|
225
1126
|
}
|
|
@@ -228,25 +1129,82 @@ function cmdStatus() {
|
|
|
228
1129
|
${expiryLine(token)}
|
|
229
1130
|
`);
|
|
230
1131
|
}
|
|
1132
|
+
function cmdTokens() {
|
|
1133
|
+
const records = readTokenRecords();
|
|
1134
|
+
if (records.length === 0) {
|
|
1135
|
+
process.stdout.write(
|
|
1136
|
+
"No tokens saved. Run `transcodes set <token> -l <label>` or `transcodes` to add one.\n"
|
|
1137
|
+
);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const active = readTokenFromFile();
|
|
1141
|
+
process.stdout.write(`Saved tokens (${transcodesConfigFile()}):
|
|
1142
|
+
`);
|
|
1143
|
+
for (const { token, label } of records) {
|
|
1144
|
+
const marker = token === active ? "*" : " ";
|
|
1145
|
+
process.stdout.write(` ${marker} ${label ?? "(no label)"}
|
|
1146
|
+
`);
|
|
1147
|
+
process.stdout.write(` ${expiryLine(token)}
|
|
1148
|
+
`);
|
|
1149
|
+
}
|
|
1150
|
+
const envToken = process.env.TRANSCODES_TOKEN?.trim();
|
|
1151
|
+
if (envToken) {
|
|
1152
|
+
process.stdout.write(
|
|
1153
|
+
"\nNote: TRANSCODES_TOKEN is set and overrides the active selection above.\n"
|
|
1154
|
+
);
|
|
1155
|
+
} else {
|
|
1156
|
+
process.stdout.write("\n* = active token used by the plugins/hooks.\n");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
async function cmdDashboard(args) {
|
|
1160
|
+
let port;
|
|
1161
|
+
let open = true;
|
|
1162
|
+
for (let i = 0; i < args.length; i++) {
|
|
1163
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
1164
|
+
port = Number(args[++i]);
|
|
1165
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1166
|
+
fail("--port must be an integer between 1 and 65535");
|
|
1167
|
+
}
|
|
1168
|
+
} else if (args[i] === "--no-open") {
|
|
1169
|
+
open = false;
|
|
1170
|
+
} else {
|
|
1171
|
+
fail(`unknown flag "${args[i]}". Usage: transcodes [--port N] [--no-open]`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
try {
|
|
1175
|
+
await runDashboard({ port, open });
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
231
1180
|
function main() {
|
|
232
1181
|
const [command, ...rest] = process.argv.slice(2);
|
|
233
1182
|
switch (command) {
|
|
234
|
-
case "
|
|
235
|
-
|
|
1183
|
+
case "set":
|
|
1184
|
+
cmdSet(rest);
|
|
236
1185
|
break;
|
|
237
|
-
case "
|
|
238
|
-
|
|
1186
|
+
case "reset":
|
|
1187
|
+
cmdReset();
|
|
239
1188
|
break;
|
|
240
1189
|
case "status":
|
|
241
1190
|
cmdStatus();
|
|
242
1191
|
break;
|
|
1192
|
+
case "tokens":
|
|
1193
|
+
cmdTokens();
|
|
1194
|
+
break;
|
|
243
1195
|
case "help":
|
|
244
1196
|
case "--help":
|
|
245
1197
|
case "-h":
|
|
246
|
-
case void 0:
|
|
247
1198
|
process.stdout.write(USAGE);
|
|
248
1199
|
break;
|
|
1200
|
+
case void 0:
|
|
1201
|
+
void cmdDashboard([]);
|
|
1202
|
+
break;
|
|
249
1203
|
default:
|
|
1204
|
+
if (command.startsWith("-")) {
|
|
1205
|
+
void cmdDashboard([command, ...rest]);
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
250
1208
|
fail(`unknown command "${command}". Run \`transcodes help\`.`);
|
|
251
1209
|
}
|
|
252
1210
|
}
|