@online5880/opensession 0.1.7 → 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 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
- ```bash
95
- opss tui # 터미널 대시보드 (추천)
96
- opss viewer # 웹 브라우저 뷰어
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,21 +94,28 @@ 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
-
107
- | Command | Alias | Description |
108
- | :--- | :--- | :--- |
109
- | `init` | `setup` | Initialize Supabase connection and local config. |
110
- | `start` | `st` | Create a new session and start the timeline. |
111
- | `resume` | `rs` | Resume an existing session with idempotency protection. |
118
+ ...
112
119
  | `tui` | - | **(New)** Launch the interactive Terminal UI dashboard. |
113
120
  | `viewer` | `vw` | Run a local read-only web viewer server. |
114
121
  | `status` | `ps` | Check CLI version and active session status. |
@@ -116,6 +123,17 @@ npm install -g @online5880/opensession
116
123
 
117
124
  ---
118
125
 
126
+ ## ⌨️ TUI 인터랙티브 조작법
127
+
128
+ TUI 대시보드(`opss tui`)는 실시간 이벤트 모니터링을 위해 다음 조작이 필요합니다:
129
+
130
+ - **세션 선택**: `[↑ / ↓]` 화살표 키로 이동 후 **`[Enter]`**를 눌러 선택하세요. 선택된 세션부터 실시간 스트리밍이 시작됩니다.
131
+ - **새로고침**: `[R]` 키를 눌러 전체 세션 목록을 새로고침합니다.
132
+ - **종료**: `[Q]` 또는 `[Esc]` 키를 눌러 종료합니다.
133
+
134
+ ---
135
+
136
+
119
137
  ## 🏗️ Architecture
120
138
 
121
139
  OpenSession acts as a high-reliability bridge between agent runtimes and persistent storage.
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.7",
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} body{margin:0;font-family:Inter,system-ui,-apple-system,sans-serif;color:var(--text);background:radial-gradient(1200px 700px at 85% -10%,rgba(140,123,255,.22),transparent 60%),radial-gradient(1000px 600px at 10% -10%,rgba(100,214,255,.18),transparent 58%),var(--bg)}
14
- .container{width:min(1120px,92vw);margin:0 auto}.topbar{display:flex;justify-content:space-between;align-items:center;padding:18px 0;border-bottom:1px solid rgba(255,255,255,.06);position:sticky;top:0;backdrop-filter:blur(10px);background:rgba(8,11,18,.75)}
15
- .brand{font-weight:800}.brand span{color:var(--brand)}.nav a{color:var(--muted);text-decoration:none;margin-left:18px;font-size:.95rem}.hero{padding:74px 0 34px;text-align:center}
16
- .eyebrow{color:var(--brand);font-weight:600;letter-spacing:.12em;text-transform:uppercase;font-size:.78rem}h1{margin:14px auto;max-width:900px;font-size:clamp(2rem,5vw,3.8rem);line-height:1.08}.sub{margin:0 auto;max-width:760px;color:var(--muted);font-size:1.08rem}
17
- .cta{margin-top:28px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}.btn{padding:12px 18px;border-radius:12px;text-decoration:none;font-weight:600}.btn.primary{color:#06121a;background:linear-gradient(90deg,var(--brand),#7de6ff)}.btn.secondary{color:var(--text);border:1px solid #2a3c59;background:rgba(255,255,255,.02)}
18
- .metrics{margin-top:28px;display:grid;grid-template-columns:repeat(4,1fr);gap:10px;background:rgba(255,255,255,.02);border:1px solid var(--line);border-radius:14px;padding:10px}.metric{background:linear-gradient(180deg,var(--panel),var(--panel-2));border-radius:10px;padding:14px}.metric .n{font-size:1.1rem;font-weight:800}.metric .l{color:var(--muted);font-size:.85rem;margin-top:4px}
19
- section{padding:36px 0}h2{margin:0 0 14px;font-size:1.45rem}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.card{border:1px solid var(--line);background:linear-gradient(180deg,var(--panel),#111827);border-radius:14px;padding:16px}.card h3{margin:0 0 8px}.card p{margin:0;color:var(--muted);font-size:.93rem}
20
- .arch{border:1px solid var(--line);border-radius:14px;padding:16px;background:linear-gradient(180deg,#101827,#0d1421)}.layers{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:12px}.layer{border:1px solid #2a3e60;border-radius:10px;padding:12px;background:#0c1320}
21
- .timeline{border-left:2px solid #2a3b5a;margin-left:6px;padding-left:16px}.event{margin:0 0 14px}.event span{color:var(--muted);display:block;margin-top:4px}
22
- .code{border:1px solid #2a3f60;border-radius:12px;background:#08111d;overflow:auto;font-family:"JetBrains Mono",monospace;font-size:.9rem;line-height:1.6;padding:14px}.code .ok{color:var(--ok)}.code .cmd{color:#8fd0ff}
23
- footer{padding:30px 0 42px;color:var(--muted);font-size:.9rem;border-top:1px solid rgba(255,255,255,.06);margin-top:22px}
24
- @media (max-width:900px){.cards{grid-template-columns:1fr}.layers{grid-template-columns:1fr 1fr}.metrics{grid-template-columns:1fr 1fr}}
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
- .action(async (options) => {
1288
- const config = await readConfig();
1289
- const client = getClient(config);
1290
- await startTui(client, options);
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 screen = blessed.screen({
6
- smartCSR: true,
7
- title: 'OpenSession TUI Dashboard'
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, options.projectKey ?? options.project);
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
@@ -1,3 +0,0 @@
1
- Session started: 883d114c-ce11-4f6b-a36a-74fac5dc2bfc
2
- Project: test-all
3
- Actor: e2e-user