github-issue-tower-defence-management 1.93.0 → 1.94.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/.github/workflows/console-ui.yml +3 -0
- package/.github/workflows/publish.yml +4 -0
- package/CHANGELOG.md +14 -0
- package/bin/adapter/entry-points/cli/index.js +4 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleServer.js +40 -1
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-C5nxEEOu.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DoJ05EuW.js +101 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/verifyConsoleUiDist.mjs +96 -0
- package/src/adapter/entry-points/cli/index.ts +9 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +181 -0
- package/src/adapter/entry-points/console/consoleServer.ts +50 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts +3 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BJixkRxv.css +0 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BSNMvjcB.js +0 -100
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DoJ05EuW.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C5nxEEOu.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
const sourceDir = path.join(
|
|
9
|
+
repoRoot,
|
|
10
|
+
'src',
|
|
11
|
+
'adapter',
|
|
12
|
+
'entry-points',
|
|
13
|
+
'console',
|
|
14
|
+
'ui-dist',
|
|
15
|
+
);
|
|
16
|
+
const targetDir = path.join(
|
|
17
|
+
repoRoot,
|
|
18
|
+
'bin',
|
|
19
|
+
'adapter',
|
|
20
|
+
'entry-points',
|
|
21
|
+
'console',
|
|
22
|
+
'ui-dist',
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const collectRelativeFiles = (baseDir) => {
|
|
26
|
+
const walk = (currentDir) => {
|
|
27
|
+
const entries = readdirSync(currentDir);
|
|
28
|
+
return entries.flatMap((entry) => {
|
|
29
|
+
const absolutePath = path.join(currentDir, entry);
|
|
30
|
+
if (statSync(absolutePath).isDirectory()) {
|
|
31
|
+
return walk(absolutePath);
|
|
32
|
+
}
|
|
33
|
+
return [path.relative(baseDir, absolutePath)];
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
return walk(baseDir).sort();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (!existsSync(sourceDir)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Console UI source build output not found at ${sourceDir}. Run "npm run build:console-ui" first.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!existsSync(targetDir)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Served console UI bundle not found at ${targetDir}. Run "node scripts/copyConsoleUiDist.mjs" to regenerate it.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sourceFiles = collectRelativeFiles(sourceDir);
|
|
52
|
+
const targetFiles = collectRelativeFiles(targetDir);
|
|
53
|
+
|
|
54
|
+
const mismatches = [];
|
|
55
|
+
|
|
56
|
+
const sourceFileSet = new Set(sourceFiles);
|
|
57
|
+
const targetFileSet = new Set(targetFiles);
|
|
58
|
+
|
|
59
|
+
for (const relativePath of sourceFiles) {
|
|
60
|
+
if (!targetFileSet.has(relativePath)) {
|
|
61
|
+
mismatches.push(`missing in served bundle: ${relativePath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const relativePath of targetFiles) {
|
|
66
|
+
if (!sourceFileSet.has(relativePath)) {
|
|
67
|
+
mismatches.push(`unexpected in served bundle: ${relativePath}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const relativePath of sourceFiles) {
|
|
72
|
+
if (!targetFileSet.has(relativePath)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const sourceContent = readFileSync(path.join(sourceDir, relativePath));
|
|
76
|
+
const targetContent = readFileSync(path.join(targetDir, relativePath));
|
|
77
|
+
if (!sourceContent.equals(targetContent)) {
|
|
78
|
+
mismatches.push(`content differs: ${relativePath}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (mismatches.length > 0) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
[
|
|
85
|
+
'Served console UI bundle is stale and does not match the source build.',
|
|
86
|
+
`Source: ${sourceDir}`,
|
|
87
|
+
`Served: ${targetDir}`,
|
|
88
|
+
'Run "npm run build:console-ui && node scripts/copyConsoleUiDist.mjs" and commit the result.',
|
|
89
|
+
...mismatches.map((entry) => ` - ${entry}`),
|
|
90
|
+
].join('\n'),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.stdout.write(
|
|
95
|
+
`Served console UI bundle matches source build (${sourceFiles.length} files).\n`,
|
|
96
|
+
);
|
|
@@ -76,11 +76,14 @@ type ServeConsoleOptions = {
|
|
|
76
76
|
port?: string;
|
|
77
77
|
consoleDataOutputDir?: string;
|
|
78
78
|
inTmuxDataDir?: string;
|
|
79
|
+
dashboardDir?: string;
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
const DEFAULT_IN_TMUX_DATA_DIR =
|
|
82
83
|
'/home/hiromi/0_workspaces/workspace1/jsonpub/in-tmux-by-human';
|
|
83
84
|
|
|
85
|
+
const DEFAULT_DASHBOARD_DIR = '/home/hiromi/0_workspaces/workspace1/jsonpub';
|
|
86
|
+
|
|
84
87
|
type SelectOauthTokenOptions = {
|
|
85
88
|
tokenListJsonPath?: string;
|
|
86
89
|
cacheDir?: string;
|
|
@@ -599,6 +602,10 @@ program
|
|
|
599
602
|
'--inTmuxDataDir <path>',
|
|
600
603
|
`Directory containing the flat in-tmux-by-human static JSON files served at /in-tmux-by-human/*.json (default: ${DEFAULT_IN_TMUX_DATA_DIR})`,
|
|
601
604
|
)
|
|
605
|
+
.option(
|
|
606
|
+
'--dashboardDir <path>',
|
|
607
|
+
`Directory containing the dashboard HTML fragment tdpm.txt served unauthenticated at /tdpm.txt (default: ${DEFAULT_DASHBOARD_DIR})`,
|
|
608
|
+
)
|
|
602
609
|
.action(async (options: ServeConsoleOptions) => {
|
|
603
610
|
const config = loadConfigFile(options.configFilePath);
|
|
604
611
|
|
|
@@ -701,12 +708,14 @@ program
|
|
|
701
708
|
const uiDistDir = path.join(__dirname, '..', 'console', 'ui-dist');
|
|
702
709
|
const consoleDataOutputDir = options.consoleDataOutputDir ?? null;
|
|
703
710
|
const inTmuxDataDir = options.inTmuxDataDir ?? DEFAULT_IN_TMUX_DATA_DIR;
|
|
711
|
+
const dashboardDir = options.dashboardDir ?? DEFAULT_DASHBOARD_DIR;
|
|
704
712
|
|
|
705
713
|
await startConsoleServer({
|
|
706
714
|
accessToken,
|
|
707
715
|
uiDistDir,
|
|
708
716
|
consoleDataOutputDir,
|
|
709
717
|
inTmuxDataDir,
|
|
718
|
+
dashboardDir,
|
|
710
719
|
issueRepository,
|
|
711
720
|
resolveProject,
|
|
712
721
|
issueTitleStateCache: new IssueTitleStateCache(),
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isConsoleAppRoute,
|
|
13
13
|
extractProvidedToken,
|
|
14
14
|
resolveFlatInTmuxFilePath,
|
|
15
|
+
resolveDashboardFilePath,
|
|
15
16
|
startConsoleServer,
|
|
16
17
|
} from './consoleServer';
|
|
17
18
|
import { IssueTitleStateCache } from './consoleReadApi';
|
|
@@ -177,6 +178,7 @@ describe('consoleServer integration', () => {
|
|
|
177
178
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
178
179
|
consoleDataOutputDir: null,
|
|
179
180
|
inTmuxDataDir: null,
|
|
181
|
+
dashboardDir: null,
|
|
180
182
|
port: 0,
|
|
181
183
|
});
|
|
182
184
|
const address = server.address();
|
|
@@ -193,6 +195,7 @@ describe('consoleServer integration', () => {
|
|
|
193
195
|
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
194
196
|
consoleDataOutputDir: null,
|
|
195
197
|
inTmuxDataDir: null,
|
|
198
|
+
dashboardDir: null,
|
|
196
199
|
port: 0,
|
|
197
200
|
});
|
|
198
201
|
try {
|
|
@@ -218,6 +221,7 @@ describe('consoleServer integration', () => {
|
|
|
218
221
|
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
219
222
|
consoleDataOutputDir: null,
|
|
220
223
|
inTmuxDataDir: null,
|
|
224
|
+
dashboardDir: null,
|
|
221
225
|
port: 0,
|
|
222
226
|
});
|
|
223
227
|
try {
|
|
@@ -247,6 +251,7 @@ describe('consoleServer integration', () => {
|
|
|
247
251
|
uiDistDir,
|
|
248
252
|
consoleDataOutputDir: null,
|
|
249
253
|
inTmuxDataDir: null,
|
|
254
|
+
dashboardDir: null,
|
|
250
255
|
port: 0,
|
|
251
256
|
});
|
|
252
257
|
try {
|
|
@@ -278,6 +283,7 @@ describe('consoleServer integration', () => {
|
|
|
278
283
|
uiDistDir,
|
|
279
284
|
consoleDataOutputDir: null,
|
|
280
285
|
inTmuxDataDir: null,
|
|
286
|
+
dashboardDir: null,
|
|
281
287
|
port: 0,
|
|
282
288
|
});
|
|
283
289
|
try {
|
|
@@ -306,6 +312,7 @@ describe('consoleServer integration', () => {
|
|
|
306
312
|
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
307
313
|
consoleDataOutputDir: null,
|
|
308
314
|
inTmuxDataDir: null,
|
|
315
|
+
dashboardDir: null,
|
|
309
316
|
port: 0,
|
|
310
317
|
});
|
|
311
318
|
try {
|
|
@@ -328,6 +335,7 @@ describe('consoleServer integration', () => {
|
|
|
328
335
|
uiDistDir,
|
|
329
336
|
consoleDataOutputDir: null,
|
|
330
337
|
inTmuxDataDir: null,
|
|
338
|
+
dashboardDir: null,
|
|
331
339
|
port: 0,
|
|
332
340
|
});
|
|
333
341
|
try {
|
|
@@ -353,6 +361,7 @@ describe('consoleServer integration', () => {
|
|
|
353
361
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
354
362
|
consoleDataOutputDir: null,
|
|
355
363
|
inTmuxDataDir: null,
|
|
364
|
+
dashboardDir: null,
|
|
356
365
|
port: 0,
|
|
357
366
|
});
|
|
358
367
|
try {
|
|
@@ -490,6 +499,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
490
499
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
491
500
|
consoleDataOutputDir: dataDir,
|
|
492
501
|
inTmuxDataDir: null,
|
|
502
|
+
dashboardDir: null,
|
|
493
503
|
port: 0,
|
|
494
504
|
});
|
|
495
505
|
try {
|
|
@@ -526,6 +536,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
526
536
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
527
537
|
consoleDataOutputDir: null,
|
|
528
538
|
inTmuxDataDir: null,
|
|
539
|
+
dashboardDir: null,
|
|
529
540
|
issueRepository,
|
|
530
541
|
issueTitleStateCache: new IssueTitleStateCache(),
|
|
531
542
|
port: 0,
|
|
@@ -559,6 +570,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
559
570
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
560
571
|
consoleDataOutputDir: null,
|
|
561
572
|
inTmuxDataDir: null,
|
|
573
|
+
dashboardDir: null,
|
|
562
574
|
issueRepository,
|
|
563
575
|
resolveProject: async (pjcode) =>
|
|
564
576
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
@@ -607,6 +619,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
607
619
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
608
620
|
consoleDataOutputDir: dataDir,
|
|
609
621
|
inTmuxDataDir: null,
|
|
622
|
+
dashboardDir: null,
|
|
610
623
|
issueRepository,
|
|
611
624
|
resolveProject: async (pjcode) =>
|
|
612
625
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
@@ -645,6 +658,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
645
658
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
646
659
|
consoleDataOutputDir: null,
|
|
647
660
|
inTmuxDataDir: null,
|
|
661
|
+
dashboardDir: null,
|
|
648
662
|
issueRepository,
|
|
649
663
|
resolveProject: async (pjcode) =>
|
|
650
664
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
@@ -695,6 +709,7 @@ describe('consoleServer new routes integration', () => {
|
|
|
695
709
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
696
710
|
consoleDataOutputDir: null,
|
|
697
711
|
inTmuxDataDir: null,
|
|
712
|
+
dashboardDir: null,
|
|
698
713
|
port: 0,
|
|
699
714
|
});
|
|
700
715
|
try {
|
|
@@ -823,6 +838,7 @@ describe('consoleServer flat in-tmux-by-human route integration', () => {
|
|
|
823
838
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
824
839
|
consoleDataOutputDir: null,
|
|
825
840
|
inTmuxDataDir,
|
|
841
|
+
dashboardDir: null,
|
|
826
842
|
port: 0,
|
|
827
843
|
});
|
|
828
844
|
return { server, tmpDir, inTmuxDataDir };
|
|
@@ -936,6 +952,7 @@ describe('consoleServer flat in-tmux-by-human route integration', () => {
|
|
|
936
952
|
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
937
953
|
consoleDataOutputDir: null,
|
|
938
954
|
inTmuxDataDir: null,
|
|
955
|
+
dashboardDir: null,
|
|
939
956
|
port: 0,
|
|
940
957
|
});
|
|
941
958
|
try {
|
|
@@ -950,3 +967,167 @@ describe('consoleServer flat in-tmux-by-human route integration', () => {
|
|
|
950
967
|
}
|
|
951
968
|
});
|
|
952
969
|
});
|
|
970
|
+
|
|
971
|
+
describe('resolveDashboardFilePath', () => {
|
|
972
|
+
const baseDir = path.join(os.tmpdir(), 'dashboard-data');
|
|
973
|
+
|
|
974
|
+
it('resolves /tdpm.txt under the dashboard dir', () => {
|
|
975
|
+
expect(resolveDashboardFilePath(baseDir, '/tdpm.txt')).toBe(
|
|
976
|
+
path.join(path.resolve(baseDir), 'tdpm.txt'),
|
|
977
|
+
);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it('returns null for any other path', () => {
|
|
981
|
+
expect(resolveDashboardFilePath(baseDir, '/tdpm.html')).toBeNull();
|
|
982
|
+
expect(resolveDashboardFilePath(baseDir, '/other.txt')).toBeNull();
|
|
983
|
+
expect(resolveDashboardFilePath(baseDir, '/')).toBeNull();
|
|
984
|
+
expect(resolveDashboardFilePath(baseDir, '/sub/tdpm.txt')).toBeNull();
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
describe('consoleServer dashboard /tdpm.txt route integration', () => {
|
|
989
|
+
const testToken = 'integration-test-token-value';
|
|
990
|
+
|
|
991
|
+
const requestServer = (
|
|
992
|
+
server: http.Server,
|
|
993
|
+
requestPath: string,
|
|
994
|
+
method = 'GET',
|
|
995
|
+
): Promise<{
|
|
996
|
+
statusCode: number;
|
|
997
|
+
body: string;
|
|
998
|
+
cacheControl: string | undefined;
|
|
999
|
+
contentType: string | undefined;
|
|
1000
|
+
contentLength: string | undefined;
|
|
1001
|
+
transferEncoding: string | undefined;
|
|
1002
|
+
}> => {
|
|
1003
|
+
const address = server.address();
|
|
1004
|
+
if (address === null || typeof address === 'string') {
|
|
1005
|
+
throw new Error('server is not listening on a TCP port');
|
|
1006
|
+
}
|
|
1007
|
+
const port = address.port;
|
|
1008
|
+
return new Promise((resolve, reject) => {
|
|
1009
|
+
const httpRequest = http.request(
|
|
1010
|
+
{ host: '127.0.0.1', port, path: requestPath, method },
|
|
1011
|
+
(response) => {
|
|
1012
|
+
const chunks: Uint8Array[] = [];
|
|
1013
|
+
response.on('data', (chunk: Uint8Array) => chunks.push(chunk));
|
|
1014
|
+
response.on('end', () => {
|
|
1015
|
+
resolve({
|
|
1016
|
+
statusCode: response.statusCode ?? 0,
|
|
1017
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
1018
|
+
cacheControl: response.headers['cache-control'],
|
|
1019
|
+
contentType: response.headers['content-type'],
|
|
1020
|
+
contentLength: response.headers['content-length'],
|
|
1021
|
+
transferEncoding: response.headers['transfer-encoding'],
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
},
|
|
1025
|
+
);
|
|
1026
|
+
httpRequest.on('error', reject);
|
|
1027
|
+
httpRequest.end();
|
|
1028
|
+
});
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const closeServer = (server: http.Server): Promise<void> =>
|
|
1032
|
+
new Promise((resolve, reject) => {
|
|
1033
|
+
server.close((error) => {
|
|
1034
|
+
if (error) {
|
|
1035
|
+
reject(error);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
resolve();
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
const dashboardRaw =
|
|
1043
|
+
'<tt>MEM 30%</tt><br>\n<tt>pj unr tdo</tt><br>\n';
|
|
1044
|
+
|
|
1045
|
+
const startWithDashboard = async (): Promise<{
|
|
1046
|
+
server: http.Server;
|
|
1047
|
+
tmpDir: string;
|
|
1048
|
+
}> => {
|
|
1049
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1050
|
+
fs.writeFileSync(path.join(tmpDir, 'tdpm.txt'), dashboardRaw);
|
|
1051
|
+
fs.writeFileSync(path.join(tmpDir, 'secret.txt'), 'secret content');
|
|
1052
|
+
const server = await startConsoleServer({
|
|
1053
|
+
accessToken: testToken,
|
|
1054
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1055
|
+
consoleDataOutputDir: null,
|
|
1056
|
+
inTmuxDataDir: null,
|
|
1057
|
+
dashboardDir: tmpDir,
|
|
1058
|
+
port: 0,
|
|
1059
|
+
});
|
|
1060
|
+
return { server, tmpDir };
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
it('serves /tdpm.txt without a token, byte-identical, as text/html with an explicit Content-Length and no chunked encoding', async () => {
|
|
1064
|
+
const { server, tmpDir } = await startWithDashboard();
|
|
1065
|
+
try {
|
|
1066
|
+
const response = await requestServer(server, '/tdpm.txt');
|
|
1067
|
+
expect(response.statusCode).toBe(200);
|
|
1068
|
+
expect(response.body).toBe(dashboardRaw);
|
|
1069
|
+
expect(response.contentType).toBe('text/html; charset=utf-8');
|
|
1070
|
+
expect(response.contentLength).toBe(
|
|
1071
|
+
String(Buffer.byteLength(dashboardRaw)),
|
|
1072
|
+
);
|
|
1073
|
+
expect(response.transferEncoding).toBeUndefined();
|
|
1074
|
+
expect(response.cacheControl).toBe('no-store');
|
|
1075
|
+
} finally {
|
|
1076
|
+
await closeServer(server);
|
|
1077
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('returns 404 for /tdpm.txt when the file is absent', async () => {
|
|
1082
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1083
|
+
const server = await startConsoleServer({
|
|
1084
|
+
accessToken: testToken,
|
|
1085
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1086
|
+
consoleDataOutputDir: null,
|
|
1087
|
+
inTmuxDataDir: null,
|
|
1088
|
+
dashboardDir: tmpDir,
|
|
1089
|
+
port: 0,
|
|
1090
|
+
});
|
|
1091
|
+
try {
|
|
1092
|
+
const response = await requestServer(server, '/tdpm.txt');
|
|
1093
|
+
expect(response.statusCode).toBe(404);
|
|
1094
|
+
} finally {
|
|
1095
|
+
await closeServer(server);
|
|
1096
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it('returns 404 for /tdpm.txt when dashboardDir is null', async () => {
|
|
1101
|
+
const { server, tmpDir } = await (async () => {
|
|
1102
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1103
|
+
fs.writeFileSync(path.join(dir, 'tdpm.txt'), dashboardRaw);
|
|
1104
|
+
const srv = await startConsoleServer({
|
|
1105
|
+
accessToken: testToken,
|
|
1106
|
+
uiDistDir: path.join(dir, 'ui-dist'),
|
|
1107
|
+
consoleDataOutputDir: null,
|
|
1108
|
+
inTmuxDataDir: null,
|
|
1109
|
+
dashboardDir: null,
|
|
1110
|
+
port: 0,
|
|
1111
|
+
});
|
|
1112
|
+
return { server: srv, tmpDir: dir };
|
|
1113
|
+
})();
|
|
1114
|
+
try {
|
|
1115
|
+
const response = await requestServer(server, '/tdpm.txt');
|
|
1116
|
+
expect(response.statusCode).toBe(404);
|
|
1117
|
+
} finally {
|
|
1118
|
+
await closeServer(server);
|
|
1119
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('rejects a non-GET method on /tdpm.txt with 404', async () => {
|
|
1124
|
+
const { server, tmpDir } = await startWithDashboard();
|
|
1125
|
+
try {
|
|
1126
|
+
const response = await requestServer(server, '/tdpm.txt', 'POST');
|
|
1127
|
+
expect(response.statusCode).toBe(404);
|
|
1128
|
+
} finally {
|
|
1129
|
+
await closeServer(server);
|
|
1130
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
@@ -155,6 +155,7 @@ export type ConsoleServerOptions = {
|
|
|
155
155
|
uiDistDir: string;
|
|
156
156
|
consoleDataOutputDir: string | null;
|
|
157
157
|
inTmuxDataDir: string | null;
|
|
158
|
+
dashboardDir: string | null;
|
|
158
159
|
issueRepository?: IssueRepository | null;
|
|
159
160
|
resolveProject?: ConsoleProjectResolver | null;
|
|
160
161
|
issueTitleStateCache?: IssueTitleStateCache | null;
|
|
@@ -164,6 +165,26 @@ const FLAT_IN_TMUX_PREFIX = '/in-tmux-by-human/';
|
|
|
164
165
|
|
|
165
166
|
const FLAT_IN_TMUX_FILE = /^[A-Za-z0-9._-]+\.json$/;
|
|
166
167
|
|
|
168
|
+
export const DASHBOARD_REQUEST_PATH = '/tdpm.txt';
|
|
169
|
+
|
|
170
|
+
const DASHBOARD_FILE_NAME = 'tdpm.txt';
|
|
171
|
+
|
|
172
|
+
export const resolveDashboardFilePath = (
|
|
173
|
+
dashboardDir: string,
|
|
174
|
+
requestPath: string,
|
|
175
|
+
): string | null => {
|
|
176
|
+
if (requestPath !== DASHBOARD_REQUEST_PATH) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const candidate = path.join(dashboardDir, DASHBOARD_FILE_NAME);
|
|
180
|
+
const resolvedRoot = path.resolve(dashboardDir);
|
|
181
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
182
|
+
if (resolvedCandidate !== path.join(resolvedRoot, DASHBOARD_FILE_NAME)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return resolvedCandidate;
|
|
186
|
+
};
|
|
187
|
+
|
|
167
188
|
export const resolveFlatInTmuxFilePath = (
|
|
168
189
|
inTmuxDataDir: string,
|
|
169
190
|
requestPath: string,
|
|
@@ -442,6 +463,35 @@ export const handleConsoleRequest = async (
|
|
|
442
463
|
return;
|
|
443
464
|
}
|
|
444
465
|
|
|
466
|
+
if (requestPath === DASHBOARD_REQUEST_PATH) {
|
|
467
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
468
|
+
if (method !== 'GET') {
|
|
469
|
+
sendNotFound(response);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (options.dashboardDir === null) {
|
|
473
|
+
sendNotFound(response);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const dashboardFilePath = resolveDashboardFilePath(
|
|
477
|
+
options.dashboardDir,
|
|
478
|
+
requestPath,
|
|
479
|
+
);
|
|
480
|
+
const dashboardContent =
|
|
481
|
+
dashboardFilePath === null ? null : readStaticFile(dashboardFilePath);
|
|
482
|
+
if (dashboardContent === null) {
|
|
483
|
+
sendNotFound(response);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
response.writeHead(200, {
|
|
487
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
488
|
+
'Cache-Control': 'no-store',
|
|
489
|
+
'Content-Length': String(dashboardContent.length),
|
|
490
|
+
});
|
|
491
|
+
response.end(dashboardContent);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
445
495
|
if (requiresToken(requestPath)) {
|
|
446
496
|
const providedToken = extractProvidedToken(
|
|
447
497
|
requestUrl.searchParams.get('k'),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,UAAU,EACV,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAkGzB,eAAO,MAAM,OAAO,SAAgB,CAAC"}
|
|
@@ -14,10 +14,13 @@ export type ConsoleServerOptions = {
|
|
|
14
14
|
uiDistDir: string;
|
|
15
15
|
consoleDataOutputDir: string | null;
|
|
16
16
|
inTmuxDataDir: string | null;
|
|
17
|
+
dashboardDir: string | null;
|
|
17
18
|
issueRepository?: IssueRepository | null;
|
|
18
19
|
resolveProject?: ConsoleProjectResolver | null;
|
|
19
20
|
issueTitleStateCache?: IssueTitleStateCache | null;
|
|
20
21
|
};
|
|
22
|
+
export declare const DASHBOARD_REQUEST_PATH = "/tdpm.txt";
|
|
23
|
+
export declare const resolveDashboardFilePath: (dashboardDir: string, requestPath: string) => string | null;
|
|
21
24
|
export declare const resolveFlatInTmuxFilePath: (inTmuxDataDir: string, requestPath: string) => string | null;
|
|
22
25
|
export declare const handleConsoleRequest: (options: ConsoleServerOptions, request: http.IncomingMessage, response: http.ServerResponse) => Promise<void>;
|
|
23
26
|
export declare const createConsoleServer: (options: ConsoleServerOptions) => http.Server;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consoleServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/consoleServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6DAA6D,CAAC;AAM9F,OAAO,EACL,oBAAoB,EAOrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,sBAAsB,EAKvB,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAmCjD,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGiB,CAAC;AAEtE,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGrB,CAAC;AAIhC,eAAO,MAAM,iBAAiB,GAAI,aAAa,MAAM,KAAG,OAmBvD,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,eAAe,MAAM,EACrB,eAAe,MAAM,GAAG,IAAI,KAC3B,OAAoE,CAAC;AAExE,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EACpC,aAAa,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,KACzC,MAAM,GAAG,IAQX,CAAC;AAuCF,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,cAAc,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC/C,oBAAoB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACpD,CAAC;AAMF,eAAO,MAAM,yBAAyB,GACpC,eAAe,MAAM,EACrB,aAAa,MAAM,KAClB,MAAM,GAAG,IAeX,CAAC;AAuPF,eAAO,MAAM,oBAAoB,GAC/B,SAAS,oBAAoB,EAC7B,SAAS,IAAI,CAAC,eAAe,EAC7B,UAAU,IAAI,CAAC,cAAc,KAC5B,OAAO,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"consoleServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/consoleServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6DAA6D,CAAC;AAM9F,OAAO,EACL,oBAAoB,EAOrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,sBAAsB,EAKvB,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAmCjD,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGiB,CAAC;AAEtE,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,OAGrB,CAAC;AAIhC,eAAO,MAAM,iBAAiB,GAAI,aAAa,MAAM,KAAG,OAmBvD,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,eAAe,MAAM,EACrB,eAAe,MAAM,GAAG,IAAI,KAC3B,OAAoE,CAAC;AAExE,eAAO,MAAM,oBAAoB,GAC/B,YAAY,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EACpC,aAAa,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,KACzC,MAAM,GAAG,IAQX,CAAC;AAuCF,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,cAAc,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC/C,oBAAoB,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACpD,CAAC;AAMF,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAIlD,eAAO,MAAM,wBAAwB,GACnC,cAAc,MAAM,EACpB,aAAa,MAAM,KAClB,MAAM,GAAG,IAWX,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,eAAe,MAAM,EACrB,aAAa,MAAM,KAClB,MAAM,GAAG,IAeX,CAAC;AAuPF,eAAO,MAAM,oBAAoB,GAC/B,SAAS,oBAAoB,EAC7B,SAAS,IAAI,CAAC,eAAe,EAC7B,UAAU,IAAI,CAAC,cAAc,KAC5B,OAAO,CAAC,IAAI,CAiFd,CAAC;AAcF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,KAC5B,IAAI,CAAC,MAMJ,CAAC;AAEL,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG;IAC7D,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,SAAS,yBAAyB,KACjC,OAAO,CAAC,IAAI,CAAC,MAAM,CAQlB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-outline-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-input:#e5e5e5;--color-ring:#0a0a0a;--color-background:#fff;--color-foreground:#0a0a0a;--color-primary:#171717;--color-primary-foreground:#fafafa;--color-secondary:#f5f5f5;--color-secondary-foreground:#171717;--color-accent:#f5f5f5;--color-accent-foreground:#171717}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.relative{position:relative}.static{position:static}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-input{border-color:var(--color-input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background)}.bg-primary{background-color:var(--color-primary)}.bg-secondary{background-color:var(--color-secondary)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-foreground{color:var(--color-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-secondary-foreground{color:var(--color-secondary-foreground)}.lowercase{text-transform:lowercase}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-primary\/90:hover{background-color:#171717e6}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--color-primary) 90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:#f5f5f5cc}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--color-secondary) 80%,transparent)}}.hover\:text-accent-foreground:hover{color:var(--color-accent-foreground)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}}body{color:#e6edf3;background-color:#0d1117;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.console-app{flex-direction:column;max-width:920px;margin:0 auto;display:flex}.console-tabbar{background:#161b22;border-bottom:2px solid #30363d;align-items:stretch;gap:0;min-height:42px;padding:0 8px;display:flex}.console-tab{color:#8b949e;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-2px;padding:10px 14px;font-size:13px;font-weight:500;line-height:1.2;text-decoration:none;display:inline-flex}.console-tab:hover{color:#e6edf3}.console-tab[data-active=true]{color:#e6edf3;border-bottom-color:#2f81f7;font-weight:700}.console-tab-badge{text-align:center;color:#e6edf3;background:#484f58;border-radius:20px;min-width:20px;padding:1px 7px;font-size:11px;font-weight:700;line-height:1.5}.console-tab-badge[data-zero=true]{color:#8b949e;background:#30363d}.console-tab-pjname{color:#8b949e;align-self:center;margin-left:auto;padding:0 8px;font-size:11.5px}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:11px}.console-tab-count-heading{color:#8b949e;margin:0;padding:12px 18px 0;font-size:13px}.console-list{margin:0;padding:12px 18px 18px;list-style:none}.console-list-group{list-style:none}.console-item-row .console-item-icon{flex:none;margin-top:3px}.console-group-header{background:#0b0f14;border-bottom:1px solid #21262d;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-storytag{align-items:center;gap:8px;font-size:13px;font-weight:700;display:inline-flex}.console-story-dot{border-radius:999px;width:10px;height:10px;display:inline-block}.console-group-count{color:#8b949e;font-size:12px}.console-item-row{color:#e6edf3;text-align:left;cursor:pointer;background:#161b22;border:1px solid #30363d;border-radius:8px;align-items:flex-start;gap:14px;width:100%;margin-bottom:10px;padding:12px 16px;display:flex}.console-item-row:hover{background:#1a2029;border-color:#484f58}.console-item-row[data-active=true]{background:#1a2029;border-color:#4493f8}.console-item-meta{flex:1;min-width:0}.console-item-title{font-size:14.5px;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:12.5px;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:11px;display:inline-block}.console-item-createdat{color:#8b949e;cursor:help}.console-list-message{color:#8b949e;padding:16px;font-size:14px}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:14px}.console-list-error,.console-comment-error,.console-files-error,.console-commits-error,.console-detail-body-error{color:#f85149}.console-detail{flex-direction:column;gap:12px;padding:16px;display:flex}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:20px;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:13px}.console-detail-subbar{align-items:center;gap:12px;font-size:13px;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:12px;display:inline-block}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:12px}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-panel-toggle{color:#e6edf3;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;font-size:14px;font-weight:600;display:inline-flex}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:14px;line-height:1.5}.console-mermaid-error{color:#f85149;font-size:13px}.console-comment{border-top:1px solid #21262d;padding-top:8px}.console-comment-header{color:#8b949e;gap:8px;font-size:12px;display:flex}.console-comment-author{color:#e6edf3;font-weight:600}.console-files,.console-commits{margin:0;padding:0;list-style:none}.console-file,.console-commit{align-items:center;gap:8px;padding:4px 0;font-size:13px;display:flex}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:11px}.console-file-path,.console-commit-message{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-add,.console-pr-add{color:#3fb950}.console-file-del,.console-pr-del{color:#f85149}.console-commit-sha{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-actionbar{z-index:100;background:#161b22;border-top:2px solid #30363d;padding:10px 16px;position:fixed;bottom:0;left:0;right:0}.console-operation-bar{flex-direction:column;gap:8px;max-width:920px;margin:0 auto;display:flex}.console-op-group{flex-wrap:wrap;gap:8px;display:flex}.console-op-group-review{justify-content:flex-end;align-items:center;gap:10px}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-op-button:hover{border-color:#484f58}.console-op-button-approve{color:#fff;background:#238636;border-color:#2ea043}.console-op-button-reject{color:#ffd166;background:#7d5000;border-color:#a06800}.console-op-button-wrong{color:#f85149;background:#3a1518;border-color:#f85149}.console-op-button-unneeded{color:#aab0b8;background:#2a2d31;border-color:#6e7681}.console-op-button-snooze{color:#79c0ff;background:#1c2b4a;border-color:#4493f8}.console-pr-section{border:1px solid #30363d;border-radius:8px;flex-direction:column;gap:10px;padding:12px;display:flex}.console-pr-statbar{color:#8b949e;gap:12px;font-size:12px;display:flex}.console-detail-screen{padding-bottom:140px}.console-panel-open-link{color:#4493f8;font-size:13px;font-weight:400}.console-composer{margin-top:4px}.console-composer-toggle{color:#8b949e;cursor:pointer;background:0 0;border:none;padding:2px 0;font-size:12.5px}.console-composer-toggle:hover{color:#e6edf3}.console-composer-posted{flex-direction:column;gap:8px;margin-top:8px;display:flex}.console-composer-form{margin-top:8px}.console-composer-input{box-sizing:border-box;color:#e6edf3;width:100%;font:inherit;resize:vertical;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:8px;font-size:14px}.console-composer-row{align-items:center;gap:8px;margin-top:6px;display:flex}.console-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #2ea043;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:12px}.console-composer-error{color:#f85149}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|