@online5880/opensession 0.1.8 → 0.1.9
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.ko.md +19 -7
- package/README.md +20 -8
- package/TESTING.ko.md +58 -0
- package/TESTING.md +58 -0
- package/e2e/cli-e2e.test.js +51 -0
- package/e2e/viewer-e2e.test.js +159 -0
- package/package.json +3 -2
- package/site/index.html +137 -12
- package/src/cli.js +20 -9
- package/src/tui.js +17 -12
- package/test/cli-compatibility.test.js +13 -6
- package/session.txt +0 -3
package/README.ko.md
CHANGED
|
@@ -90,13 +90,25 @@ function opss { npx -y @online5880/opensession @args }
|
|
|
90
90
|
opss log --limit 10
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
4. **대시보드 실행**: 원하는 뷰어를 선택해 모니터링하세요.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
4. **대시보드 실행**: 원하는 뷰어를 선택해 모니터링하세요.
|
|
94
|
+
```bash
|
|
95
|
+
opss tui # 터미널 대시보드 (추천)
|
|
96
|
+
opss viewer # 웹 브라우저 뷰어
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 🧪 테스트
|
|
100
|
+
|
|
101
|
+
- 상세 가이드: [TESTING.md](TESTING.md) | [한국어 가이드](TESTING.ko.md)
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm test
|
|
105
|
+
npm run e2e
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `npm test`: `test/*.test.js`에 있는 단위·호환성 테스트를 실행합니다.
|
|
109
|
+
- `npm run e2e`: CLI 명령어 스모크 테스트와 viewer 시작/`/health` 검증 e2e 테스트를 실행합니다.
|
|
110
|
+
|
|
111
|
+
---
|
|
100
112
|
|
|
101
113
|
## 📖 명령어 가이드
|
|
102
114
|
|
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ npm install -g @online5880/opensession
|
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
80
|
-
## 🚀 1-Minute Quickstart
|
|
80
|
+
## 🚀 1-Minute Quickstart
|
|
81
81
|
|
|
82
82
|
1. **Initialize**: Set up your Supabase URL and API Key.
|
|
83
83
|
```bash
|
|
@@ -94,13 +94,25 @@ npm install -g @online5880/opensession
|
|
|
94
94
|
opss log --limit 10
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
4. **Launch Dashboard**: Choose your preferred view.
|
|
98
|
-
```bash
|
|
99
|
-
opss tui # Terminal dashboard (Recommended)
|
|
100
|
-
opss viewer # Web browser viewer
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
4. **Launch Dashboard**: Choose your preferred view.
|
|
98
|
+
```bash
|
|
99
|
+
opss tui # Terminal dashboard (Recommended)
|
|
100
|
+
opss viewer # Web browser viewer
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 🧪 Testing
|
|
104
|
+
|
|
105
|
+
- Detailed guide: [TESTING.md](TESTING.md) | [한국어 가이드](TESTING.ko.md)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm test
|
|
109
|
+
npm run e2e
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `npm test`: Runs unit and compatibility tests under `test/*.test.js`.
|
|
113
|
+
- `npm run e2e`: Runs end-to-end smoke tests for CLI commands and viewer startup/`/health`.
|
|
114
|
+
|
|
115
|
+
---
|
|
104
116
|
|
|
105
117
|
## 📖 Command Reference
|
|
106
118
|
...
|
package/TESTING.ko.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 테스트 가이드
|
|
2
|
+
|
|
3
|
+
**[영어](TESTING.md) | [한국어](TESTING.ko.md)**
|
|
4
|
+
|
|
5
|
+
## 1) 로컬 테스트 구성
|
|
6
|
+
|
|
7
|
+
프로젝트 루트에서 다음 명령을 실행합니다:
|
|
8
|
+
|
|
9
|
+
- `npm test` — 단위/호환성 테스트 실행 (`test/*.test.js`)
|
|
10
|
+
- `npm run e2e` — E2E 스모크 테스트 실행 (`e2e/*.test.js`)
|
|
11
|
+
- `npm run lint` — 런타임 JS 파일 구문 검사
|
|
12
|
+
|
|
13
|
+
## 2) 실행 환경
|
|
14
|
+
|
|
15
|
+
- Node.js 18 이상
|
|
16
|
+
- 네트워크는 원칙적으로 필수가 아닙니다. 테스트는 외부 Supabase를 직접 호출하지 않습니다.
|
|
17
|
+
- 외부 호출이 필요한 별도 스크립트를 직접 실행하지 않는 한 테스트는 임시 파일과 모킹(mock) 기반으로 동작합니다.
|
|
18
|
+
|
|
19
|
+
## 3) 단위/호환성 테스트
|
|
20
|
+
|
|
21
|
+
`npm test`는 다음 파일을 실행합니다:
|
|
22
|
+
|
|
23
|
+
- `test/cli-compatibility.test.js` — CLI 도움말/호환성 플래그 확인
|
|
24
|
+
- `test/config-secrets.test.js` — 비밀값 암호화 저장 및 복호화 동작 확인
|
|
25
|
+
- `test/idempotency.test.js` — 재시도 안전성을 위한 idempotency 유틸 동작 확인
|
|
26
|
+
- `test/supabase-append-event.test.js` — Supabase append 동작과 충돌 처리 검증
|
|
27
|
+
|
|
28
|
+
### 기대 동작
|
|
29
|
+
|
|
30
|
+
- 실패 시 비정상 종료 코드(0 이 아닌 값) 발생
|
|
31
|
+
- 성공 시 표준 Node 테스트 결과가 출력되고 `0` 종료 코드로 종료
|
|
32
|
+
|
|
33
|
+
## 4) E2E 테스트
|
|
34
|
+
|
|
35
|
+
`npm run e2e`는 다음 파일을 실행합니다:
|
|
36
|
+
|
|
37
|
+
- `e2e/cli-e2e.test.js` — 임시 HOME를 이용한 CLI 기본 경로/명령 검증
|
|
38
|
+
- `e2e/viewer-e2e.test.js` — 읽기 전용 뷰어 시작 후 `/health` 응답 검증
|
|
39
|
+
|
|
40
|
+
### 참고
|
|
41
|
+
|
|
42
|
+
- 뷰어 E2E는 임시 포트를 자동으로 할당해 실행합니다.
|
|
43
|
+
- 로컬 환경 부하가 큰 경우 간헐적으로 실패할 수 있으므로 재실행으로 다수 해결됩니다.
|
|
44
|
+
|
|
45
|
+
## 5) 권장 검증 순서
|
|
46
|
+
|
|
47
|
+
변경 후 아래 순서로 실행합니다:
|
|
48
|
+
|
|
49
|
+
1. `npm run lint`
|
|
50
|
+
2. `npm test`
|
|
51
|
+
3. `npm run e2e`
|
|
52
|
+
|
|
53
|
+
세 단계가 모두 통과하면 PR 단계의 기본 검증을 통과한 것으로 봅니다.
|
|
54
|
+
|
|
55
|
+
## 6) 관련 문서
|
|
56
|
+
|
|
57
|
+
- [README.md](README.md)
|
|
58
|
+
- [README.ko.md](README.ko.md)
|
package/TESTING.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
**[English](TESTING.md) | [한국어](TESTING.ko.md)**
|
|
4
|
+
|
|
5
|
+
## 1) Local Test Matrix
|
|
6
|
+
|
|
7
|
+
Run these commands from the repository root:
|
|
8
|
+
|
|
9
|
+
- `npm test` — Unit + compatibility tests (`test/*.test.js`)
|
|
10
|
+
- `npm run e2e` — End-to-end smoke checks (`e2e/*.test.js`)
|
|
11
|
+
- `npm run lint` — Syntax check for runtime JS files
|
|
12
|
+
|
|
13
|
+
## 2) Required Runtime
|
|
14
|
+
|
|
15
|
+
- Node.js 18+
|
|
16
|
+
- Internet access is only needed when you run ad-hoc scripts that call remote services
|
|
17
|
+
- Test suites themselves do **not** require real Supabase or network access; all external interactions in tests are mocked or use local temporary files
|
|
18
|
+
|
|
19
|
+
## 3) Unit and Compatibility Tests
|
|
20
|
+
|
|
21
|
+
`npm test` executes:
|
|
22
|
+
|
|
23
|
+
- `test/cli-compatibility.test.js` — validates CLI command help text and compatibility flags
|
|
24
|
+
- `test/config-secrets.test.js` — verifies secret persistence and decryption logic
|
|
25
|
+
- `test/idempotency.test.js` — checks operation idempotency helper behavior
|
|
26
|
+
- `test/supabase-append-event.test.js` — validates Supabase append behavior with conflict handling
|
|
27
|
+
|
|
28
|
+
### Expected result
|
|
29
|
+
|
|
30
|
+
- Non-zero exit code indicates at least one failing assertion
|
|
31
|
+
- On success, each test file prints its standard Node test summary and exits with `0`
|
|
32
|
+
|
|
33
|
+
## 4) End-to-End Tests
|
|
34
|
+
|
|
35
|
+
`npm run e2e` executes:
|
|
36
|
+
|
|
37
|
+
- `e2e/cli-e2e.test.js` — validates basic CLI command behavior with temporary isolated config paths
|
|
38
|
+
- `e2e/viewer-e2e.test.js` — starts the viewer in read-only mode and verifies `/health` endpoint response
|
|
39
|
+
|
|
40
|
+
### Notes
|
|
41
|
+
|
|
42
|
+
- The viewer e2e test binds to a random free port and tears down the process after verification
|
|
43
|
+
- Failures can occur when the local machine is under heavy resource pressure, usually resolved by re-running the suite
|
|
44
|
+
|
|
45
|
+
## 5) Recommended Validation Flow
|
|
46
|
+
|
|
47
|
+
For every change, we use this order:
|
|
48
|
+
|
|
49
|
+
1. `npm run lint`
|
|
50
|
+
2. `npm test`
|
|
51
|
+
3. `npm run e2e`
|
|
52
|
+
|
|
53
|
+
When all three pass, the repository is ready for PR-ready validation.
|
|
54
|
+
|
|
55
|
+
## 6) Related documentation
|
|
56
|
+
|
|
57
|
+
- [README.md](README.md)
|
|
58
|
+
- [README.ko.md](README.ko.md)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const CLI_PATH = path.resolve(__dirname, '../src/cli.js');
|
|
12
|
+
|
|
13
|
+
function runCli(args, options = {}) {
|
|
14
|
+
return spawnSync(process.execPath, [CLI_PATH, ...args], {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
env: options.env ?? process.env,
|
|
17
|
+
timeout: 5000
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('config-path prints isolated config location with custom HOME', async () => {
|
|
22
|
+
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'opensession-e2e-cli-'));
|
|
23
|
+
const env = {
|
|
24
|
+
...process.env,
|
|
25
|
+
HOME: home,
|
|
26
|
+
USERPROFILE: home
|
|
27
|
+
};
|
|
28
|
+
const expectedPath = path.join(home, '.opensession', 'config.json');
|
|
29
|
+
|
|
30
|
+
const result = runCli(['config-path'], { env });
|
|
31
|
+
|
|
32
|
+
assert.equal(result.status, 0);
|
|
33
|
+
assert.equal(result.stdout.trim(), expectedPath);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('viewer command help is available through command definition', () => {
|
|
37
|
+
const result = runCli(['viewer', '--help']);
|
|
38
|
+
|
|
39
|
+
assert.equal(result.status, 0);
|
|
40
|
+
assert.match(result.stdout, /Run read-only web viewer for projects\/sessions\/events/);
|
|
41
|
+
assert.match(result.stdout, /--host <host>/);
|
|
42
|
+
assert.match(result.stdout, /--port <port>/);
|
|
43
|
+
assert.match(result.stdout, /vw/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('version command returns a semver-compatible value', () => {
|
|
47
|
+
const result = runCli(['--version']);
|
|
48
|
+
|
|
49
|
+
assert.equal(result.status, 0);
|
|
50
|
+
assert.match(result.stdout.trim(), /^\d+\.\d+\.\d+/);
|
|
51
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const CLI_PATH = path.resolve(__dirname, '../src/cli.js');
|
|
13
|
+
|
|
14
|
+
function createTempConfig(home) {
|
|
15
|
+
return fs.mkdir(path.join(home, '.opensession'), { recursive: true }).then(() =>
|
|
16
|
+
fs.writeFile(
|
|
17
|
+
path.join(home, '.opensession', 'config.json'),
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
supabaseUrl: 'https://example.supabase.co',
|
|
20
|
+
supabaseAnonKey: 'test-anon-key'
|
|
21
|
+
})
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function truncateOutput(text, limit = 1200) {
|
|
27
|
+
if (text.length <= limit) {
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
return `...${text.slice(-limit)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatProcessOutput(output) {
|
|
34
|
+
return [
|
|
35
|
+
output.exitCode === null ? '' : `exitCode=${output.exitCode}`,
|
|
36
|
+
output.signal ? `signal=${output.signal}` : '',
|
|
37
|
+
`stdout:\n${truncateOutput(output.stdout || '<empty>')}`,
|
|
38
|
+
`stderr:\n${truncateOutput(output.stderr || '<empty>')}`
|
|
39
|
+
].filter(Boolean).join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function getFreePort() {
|
|
43
|
+
const server = net.createServer();
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
server.once('error', reject);
|
|
46
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const address = server.address();
|
|
50
|
+
const port = typeof address === 'object' && address !== null ? address.port : null;
|
|
51
|
+
await new Promise((resolve) => {
|
|
52
|
+
server.close(resolve);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!port) {
|
|
56
|
+
throw new Error('Failed to allocate ephemeral port for viewer e2e test.');
|
|
57
|
+
}
|
|
58
|
+
return port;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function waitForHealth(url, timeoutMs = 5000, intervalMs = 120, child, output) {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
let lastStatus;
|
|
64
|
+
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
if (child.exitCode !== null) {
|
|
67
|
+
throw new Error(`Viewer exited before health endpoint became available. ${formatProcessOutput(output)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url);
|
|
72
|
+
if (response.status === 200) {
|
|
73
|
+
return response.json();
|
|
74
|
+
}
|
|
75
|
+
lastStatus = response.status;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
lastStatus = error.code ?? error.name ?? 'unknown';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await new Promise((resolve) => {
|
|
81
|
+
setTimeout(resolve, intervalMs);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new Error(`Timed out waiting for health endpoint after ${timeoutMs}ms (last status ${lastStatus}). ${formatProcessOutput(output)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function runViewer({ port, env }) {
|
|
89
|
+
const child = spawn(process.execPath, [CLI_PATH, 'viewer', '--host', '127.0.0.1', '--port', String(port)], {
|
|
90
|
+
env,
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
92
|
+
});
|
|
93
|
+
const output = {
|
|
94
|
+
stdout: '',
|
|
95
|
+
stderr: '',
|
|
96
|
+
exitCode: null,
|
|
97
|
+
signal: null
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
child.stdout?.on('data', (chunk) => {
|
|
101
|
+
output.stdout += String(chunk);
|
|
102
|
+
});
|
|
103
|
+
child.stderr?.on('data', (chunk) => {
|
|
104
|
+
output.stderr += String(chunk);
|
|
105
|
+
});
|
|
106
|
+
child.on('exit', (code, signal) => {
|
|
107
|
+
output.exitCode = code;
|
|
108
|
+
output.signal = signal;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const healthUrl = `http://127.0.0.1:${port}/health`;
|
|
113
|
+
const body = await waitForHealth(healthUrl, 5000, 120, child, output);
|
|
114
|
+
assert.equal(body.ok, true);
|
|
115
|
+
assert.equal(body.mode, 'read-only');
|
|
116
|
+
} finally {
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
if (!child || child.exitCode !== null) {
|
|
119
|
+
resolve();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!child.stdout?.destroyed) {
|
|
124
|
+
child.stdout?.destroy();
|
|
125
|
+
}
|
|
126
|
+
if (!child.stderr?.destroyed) {
|
|
127
|
+
child.stderr?.destroy();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const timeout = setTimeout(() => {
|
|
131
|
+
child.kill('SIGKILL');
|
|
132
|
+
reject(new Error(`Timed out waiting for viewer process to stop after SIGINT. ${formatProcessOutput(output)}`));
|
|
133
|
+
}, 2000);
|
|
134
|
+
|
|
135
|
+
child.once('exit', () => {
|
|
136
|
+
clearTimeout(timeout);
|
|
137
|
+
resolve();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
child.kill('SIGINT');
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
test('viewer command starts and health endpoint returns read-only status', async () => {
|
|
146
|
+
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'opensession-e2e-viewer-'));
|
|
147
|
+
const port = await getFreePort();
|
|
148
|
+
|
|
149
|
+
const env = {
|
|
150
|
+
...process.env,
|
|
151
|
+
HOME: home,
|
|
152
|
+
USERPROFILE: home
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await createTempConfig(home);
|
|
156
|
+
await runViewer({ port, env });
|
|
157
|
+
|
|
158
|
+
await fs.rm(home, { recursive: true, force: true });
|
|
159
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@online5880/opensession",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Session continuity bridge CLI with Supabase backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/cli.js",
|
|
12
12
|
"lint": "sh -c 'for f in src/*.js; do node --check \"$f\"; done'",
|
|
13
|
-
"test": "node --test test/*.test.js"
|
|
13
|
+
"test": "node --test test/*.test.js",
|
|
14
|
+
"e2e": "node --test e2e/*.test.js"
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=18"
|
package/site/index.html
CHANGED
|
@@ -10,18 +10,143 @@
|
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
11
11
|
<style>
|
|
12
12
|
:root {--bg:#080b12;--bg-soft:#0f1420;--panel:#121a28;--panel-2:#182234;--text:#e9f1ff;--muted:#99abc9;--line:#22314a;--brand:#64d6ff;--brand-2:#8c7bff;--ok:#37d67a;}
|
|
13
|
-
*{box-sizing:border-box
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
17
|
+
color: var(--text);
|
|
18
|
+
background:
|
|
19
|
+
radial-gradient(1200px 700px at 85% -10%, rgba(140, 123, 255, .22), transparent 60%),
|
|
20
|
+
radial-gradient(1000px 600px at 10% -10%, rgba(100, 214, 255, .18), transparent 58%),
|
|
21
|
+
var(--bg);
|
|
22
|
+
}
|
|
23
|
+
.container {
|
|
24
|
+
width: min(1120px, 92vw);
|
|
25
|
+
margin: 0 auto;
|
|
26
|
+
padding: 0 8px;
|
|
27
|
+
}
|
|
28
|
+
.topbar {
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
padding: 18px 0;
|
|
34
|
+
border-bottom: 1px solid rgba(255, 255, 255, .06);
|
|
35
|
+
position: sticky;
|
|
36
|
+
top: 0;
|
|
37
|
+
backdrop-filter: blur(10px);
|
|
38
|
+
background: rgba(8, 11, 18, .75);
|
|
39
|
+
}
|
|
40
|
+
.topbar .brand {
|
|
41
|
+
font-weight: 800;
|
|
42
|
+
line-height: 1.2;
|
|
43
|
+
white-space: nowrap;
|
|
44
|
+
}
|
|
45
|
+
.brand span { color: var(--brand); }
|
|
46
|
+
.nav { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 12px 16px; }
|
|
47
|
+
.nav a {
|
|
48
|
+
color: var(--muted);
|
|
49
|
+
text-decoration: none;
|
|
50
|
+
font-size: .95rem;
|
|
51
|
+
}
|
|
52
|
+
.hero { padding: 74px 0 34px; text-align: center; }
|
|
53
|
+
.eyebrow {
|
|
54
|
+
color: var(--brand);
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
letter-spacing: .12em;
|
|
57
|
+
text-transform: uppercase;
|
|
58
|
+
font-size: .78rem;
|
|
59
|
+
}
|
|
60
|
+
h1 {
|
|
61
|
+
margin: 14px auto;
|
|
62
|
+
max-width: 900px;
|
|
63
|
+
font-size: clamp(2rem, 5vw, 3.8rem);
|
|
64
|
+
line-height: 1.08;
|
|
65
|
+
}
|
|
66
|
+
.sub { margin: 0 auto; max-width: 760px; color: var(--muted); font-size: 1.08rem; }
|
|
67
|
+
.cta {
|
|
68
|
+
margin-top: 28px;
|
|
69
|
+
display: flex;
|
|
70
|
+
gap: 12px;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
}
|
|
74
|
+
.btn { padding: 12px 18px; border-radius: 12px; text-decoration: none; font-weight: 600; }
|
|
75
|
+
.btn.primary { color: #06121a; background: linear-gradient(90deg, var(--brand), #7de6ff); }
|
|
76
|
+
.btn.secondary { color: var(--text); border: 1px solid #2a3c59; background: rgba(255, 255, 255, .02); }
|
|
77
|
+
.metrics {
|
|
78
|
+
margin-top: 28px;
|
|
79
|
+
display: grid;
|
|
80
|
+
grid-template-columns: repeat(4, 1fr);
|
|
81
|
+
gap: 10px;
|
|
82
|
+
background: rgba(255, 255, 255, .02);
|
|
83
|
+
border: 1px solid var(--line);
|
|
84
|
+
border-radius: 14px;
|
|
85
|
+
padding: 10px;
|
|
86
|
+
}
|
|
87
|
+
.metric {
|
|
88
|
+
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
|
89
|
+
border-radius: 10px;
|
|
90
|
+
padding: 14px;
|
|
91
|
+
}
|
|
92
|
+
.metric .n { font-size: 1.1rem; font-weight: 800; }
|
|
93
|
+
.metric .l { color: var(--muted); font-size: .85rem; margin-top: 4px; }
|
|
94
|
+
section { padding: 36px 0; }
|
|
95
|
+
h2 { margin: 0 0 14px; font-size: 1.45rem; }
|
|
96
|
+
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
97
|
+
.card {
|
|
98
|
+
border: 1px solid var(--line);
|
|
99
|
+
background: linear-gradient(180deg, var(--panel), #111827);
|
|
100
|
+
border-radius: 14px;
|
|
101
|
+
padding: 16px;
|
|
102
|
+
}
|
|
103
|
+
.card h3 { margin: 0 0 8px; }
|
|
104
|
+
.card p { margin: 0; color: var(--muted); font-size: .93rem; }
|
|
105
|
+
.arch {
|
|
106
|
+
border: 1px solid var(--line);
|
|
107
|
+
border-radius: 14px;
|
|
108
|
+
padding: 16px;
|
|
109
|
+
background: linear-gradient(180deg, #101827, #0d1421);
|
|
110
|
+
}
|
|
111
|
+
.layers { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 12px; }
|
|
112
|
+
.layer { border: 1px solid #2a3e60; border-radius: 10px; padding: 12px; background: #0c1320; }
|
|
113
|
+
.timeline { border-left: 2px solid #2a3b5a; margin-left: 6px; padding-left: 16px; }
|
|
114
|
+
.event { margin: 0 0 14px; }
|
|
115
|
+
.event span { color: var(--muted); display: block; margin-top: 4px; }
|
|
116
|
+
.code {
|
|
117
|
+
border: 1px solid #2a3f60;
|
|
118
|
+
border-radius: 12px;
|
|
119
|
+
background: #08111d;
|
|
120
|
+
overflow: auto;
|
|
121
|
+
font-family: "JetBrains Mono", monospace;
|
|
122
|
+
font-size: .9rem;
|
|
123
|
+
line-height: 1.6;
|
|
124
|
+
padding: 14px;
|
|
125
|
+
}
|
|
126
|
+
.code .ok { color: var(--ok); }
|
|
127
|
+
.code .cmd { color: #8fd0ff; }
|
|
128
|
+
footer {
|
|
129
|
+
padding: 30px 0 42px;
|
|
130
|
+
color: var(--muted);
|
|
131
|
+
font-size: .9rem;
|
|
132
|
+
border-top: 1px solid rgba(255,255,255,.06);
|
|
133
|
+
margin-top: 22px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@media (max-width: 900px) {
|
|
137
|
+
.cards { grid-template-columns: repeat(2, 1fr); }
|
|
138
|
+
.layers { grid-template-columns: repeat(2, 1fr); }
|
|
139
|
+
.metrics { grid-template-columns: repeat(2, 1fr); }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (max-width: 640px) {
|
|
143
|
+
.hero { padding-top: 56px; }
|
|
144
|
+
.topbar { flex-direction: column; align-items: flex-start; }
|
|
145
|
+
.cards { grid-template-columns: 1fr; }
|
|
146
|
+
.layers { grid-template-columns: 1fr; }
|
|
147
|
+
.metrics { grid-template-columns: 1fr; }
|
|
148
|
+
.eyebrow { font-size: .72rem; }
|
|
149
|
+
}
|
|
25
150
|
</style>
|
|
26
151
|
</head>
|
|
27
152
|
<body><div class="container"><div class="topbar"><div class="brand">Open<span>Session</span></div><div class="nav"><a href="#features">Features</a><a href="#docs">Docs</a><a href="#updates">What's New</a></div></div>
|
package/src/cli.js
CHANGED
|
@@ -1280,15 +1280,26 @@ program
|
|
|
1280
1280
|
}
|
|
1281
1281
|
});
|
|
1282
1282
|
|
|
1283
|
-
program
|
|
1284
|
-
.command('tui')
|
|
1285
|
-
.description('Run interactive Terminal UI (TUI) dashboard')
|
|
1286
|
-
.option('--project-key <projectKey>', 'Project key')
|
|
1287
|
-
.
|
|
1288
|
-
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1283
|
+
program
|
|
1284
|
+
.command('tui')
|
|
1285
|
+
.description('Run interactive Terminal UI (TUI) dashboard')
|
|
1286
|
+
.option('--project-key <projectKey>', 'Project key')
|
|
1287
|
+
.option('--project <projectKey>', 'Alias of --project-key')
|
|
1288
|
+
.action(async (options) => {
|
|
1289
|
+
const config = await readConfig();
|
|
1290
|
+
const client = getClient(config);
|
|
1291
|
+
const projectKey = options.projectKey ?? options.project ?? config.defaultProjectKey ?? config.syncStatus?.project;
|
|
1292
|
+
if (!projectKey) {
|
|
1293
|
+
throw new Error('Missing project key. Pass --project-key, sync --project, or run start first.');
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const project = await ensureProject(client, projectKey, projectKey);
|
|
1297
|
+
await startTui(client, {
|
|
1298
|
+
...options,
|
|
1299
|
+
projectKey,
|
|
1300
|
+
projectId: project.id
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1292
1303
|
|
|
1293
1304
|
program
|
|
1294
1305
|
.command('ops')
|
package/src/tui.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import blessed from 'blessed';
|
|
2
2
|
import { listActiveSessions, getSessionEvents, subscribeToSessionEvents } from './supabase.js';
|
|
3
3
|
|
|
4
|
-
export async function startTui(client, options = {}) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
4
|
+
export async function startTui(client, options = {}) {
|
|
5
|
+
const projectId = options.projectId ?? options.project_id;
|
|
6
|
+
if (!projectId) {
|
|
7
|
+
throw new Error('Missing project id. Pass a valid project id when starting TUI.');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const screen = blessed.screen({
|
|
11
|
+
smartCSR: true,
|
|
12
|
+
title: 'OpenSession TUI Dashboard'
|
|
13
|
+
});
|
|
9
14
|
|
|
10
15
|
const layout = blessed.layout({
|
|
11
16
|
parent: screen,
|
|
@@ -91,13 +96,13 @@ export async function startTui(client, options = {}) {
|
|
|
91
96
|
let selectedSessionId = null;
|
|
92
97
|
let unsubscribeRealtime = null;
|
|
93
98
|
|
|
94
|
-
async function refreshSessions() {
|
|
95
|
-
try {
|
|
96
|
-
header.setContent(` Loading sessions... `);
|
|
97
|
-
screen.render();
|
|
98
|
-
|
|
99
|
-
const activeSessions = await listActiveSessions(client,
|
|
100
|
-
sessions = activeSessions || [];
|
|
99
|
+
async function refreshSessions() {
|
|
100
|
+
try {
|
|
101
|
+
header.setContent(` Loading sessions... `);
|
|
102
|
+
screen.render();
|
|
103
|
+
|
|
104
|
+
const activeSessions = await listActiveSessions(client, projectId);
|
|
105
|
+
sessions = activeSessions || [];
|
|
101
106
|
|
|
102
107
|
sessionList.setItems(sessions.map(s => `${s.actor} (${s.id.slice(0, 8)})`));
|
|
103
108
|
header.setContent(` OpenSession TUI Dashboard | Total: ${sessions.length} `);
|
|
@@ -41,12 +41,19 @@ test('approve command help keeps required compatibility flags', () => {
|
|
|
41
41
|
assert.match(result.stdout, /--idempotency-key <idempotencyKey>/);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test('status command help keeps both project-key and project alias', () => {
|
|
45
|
-
const result = runCli(['status', '--help']);
|
|
46
|
-
assert.equal(result.status, 0);
|
|
47
|
-
assert.match(result.stdout, /--project-key <projectKey>/);
|
|
48
|
-
assert.match(result.stdout, /--project <projectKey>/);
|
|
49
|
-
});
|
|
44
|
+
test('status command help keeps both project-key and project alias', () => {
|
|
45
|
+
const result = runCli(['status', '--help']);
|
|
46
|
+
assert.equal(result.status, 0);
|
|
47
|
+
assert.match(result.stdout, /--project-key <projectKey>/);
|
|
48
|
+
assert.match(result.stdout, /--project <projectKey>/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('tui command help keeps project key compatibility flags', () => {
|
|
52
|
+
const result = runCli(['tui', '--help']);
|
|
53
|
+
assert.equal(result.status, 0);
|
|
54
|
+
assert.match(result.stdout, /--project-key <projectKey>/);
|
|
55
|
+
assert.match(result.stdout, /--project <projectKey>/);
|
|
56
|
+
});
|
|
50
57
|
|
|
51
58
|
test('log command help keeps session and limit flags', () => {
|
|
52
59
|
const result = runCli(['log', '--help']);
|
package/session.txt
DELETED