ai-cli-online
Advanced tools
| declare const router: import("express-serve-static-core").Router; | ||
| export default router; |
| import { Router } from 'express'; | ||
| import { execFile as execFileCb } from 'child_process'; | ||
| import { promisify } from 'util'; | ||
| import { resolveSession } from '../middleware/auth.js'; | ||
| import { getCwd } from '../tmux.js'; | ||
| const execFile = promisify(execFileCb); | ||
| const EXEC_TIMEOUT = 10000; | ||
| const router = Router(); | ||
| // Git log with optional file filter | ||
| router.get('/api/sessions/:sessionId/git-log', async (req, res) => { | ||
| const sessionName = resolveSession(req, res); | ||
| if (!sessionName) | ||
| return; | ||
| const page = Math.max(1, parseInt(req.query.page, 10) || 1); | ||
| const limit = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 30)); | ||
| const file = req.query.file; | ||
| if (file && (file.includes('..') || file.startsWith('/'))) { | ||
| res.status(400).json({ error: 'Invalid file path' }); | ||
| return; | ||
| } | ||
| const all = req.query.all === 'true'; | ||
| const branch = req.query.branch; | ||
| if (branch && !/^[\w\-\/.]+$/.test(branch)) { | ||
| res.status(400).json({ error: 'Invalid branch name' }); | ||
| return; | ||
| } | ||
| const skip = (page - 1) * limit; | ||
| let cwd; | ||
| try { | ||
| cwd = await getCwd(sessionName); | ||
| } | ||
| catch { | ||
| res.status(404).json({ error: 'Session not found' }); | ||
| return; | ||
| } | ||
| try { | ||
| // Use a separator that won't appear in commit messages | ||
| const SEP = '---GIT-LOG-SEP---'; | ||
| const format = `${SEP}%n%H%n%h%n%P%n%D%n%s%n%an%n%aI`; | ||
| const args = ['log', '--topo-order', `--pretty=format:${format}`, '--numstat', `--skip=${skip}`, `-${limit + 1}`]; | ||
| if (all) | ||
| args.splice(1, 0, '--all'); | ||
| if (branch && !all) | ||
| args.push(branch); | ||
| if (file) { | ||
| args.push('--', file); | ||
| } | ||
| const { stdout } = await execFile('git', args, { cwd, timeout: EXEC_TIMEOUT }); | ||
| const commits = []; | ||
| const blocks = stdout.split(SEP).filter((b) => b.trim()); | ||
| for (const block of blocks) { | ||
| // Don't filter empty lines — %P and %D may be empty (root commits, no refs) | ||
| // Block starts with \n from format, so drop the leading empty entry | ||
| const rawLines = block.split('\n'); | ||
| // Drop leading empty line from format separator | ||
| if (rawLines[0] === '') | ||
| rawLines.shift(); | ||
| // First 7 lines are fixed fields; remaining are numstat file lines | ||
| if (rawLines.length < 7) | ||
| continue; | ||
| const [hash, shortHash, parentLine, refLine, message, author, date, ...fileLines] = rawLines; | ||
| const parents = parentLine.trim() ? parentLine.trim().split(' ') : []; | ||
| const refs = []; | ||
| if (refLine.trim()) { | ||
| for (const raw of refLine.split(',')) { | ||
| const part = raw.trim(); | ||
| if (!part) | ||
| continue; | ||
| if (part.startsWith('HEAD -> ')) { | ||
| refs.push({ type: 'head', name: part.slice(8) }); | ||
| } | ||
| else if (part === 'HEAD') { | ||
| refs.push({ type: 'head', name: 'HEAD' }); | ||
| } | ||
| else if (part.startsWith('tag: ')) { | ||
| refs.push({ type: 'tag', name: part.slice(5) }); | ||
| } | ||
| else if (part.includes('/')) { | ||
| refs.push({ type: 'remote', name: part }); | ||
| } | ||
| else { | ||
| refs.push({ type: 'branch', name: part }); | ||
| } | ||
| } | ||
| } | ||
| const files = []; | ||
| for (const fl of fileLines) { | ||
| const match = fl.match(/^(\d+|-)\t(\d+|-)\t(.+)$/); | ||
| if (match) { | ||
| files.push({ | ||
| additions: match[1] === '-' ? 0 : parseInt(match[1], 10), | ||
| deletions: match[2] === '-' ? 0 : parseInt(match[2], 10), | ||
| path: match[3], | ||
| }); | ||
| } | ||
| } | ||
| commits.push({ hash, shortHash, parents, refs, message, author, date, files }); | ||
| } | ||
| const hasMore = commits.length > limit; | ||
| if (hasMore) | ||
| commits.pop(); | ||
| res.json({ commits, hasMore }); | ||
| } | ||
| catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| if (msg.includes('not a git repository')) { | ||
| res.json({ commits: [], hasMore: false, error: 'Not a git repository' }); | ||
| } | ||
| else { | ||
| console.error('[api:git-log]', msg); | ||
| res.status(500).json({ error: 'Failed to get git log' }); | ||
| } | ||
| } | ||
| }); | ||
| // Git diff for a specific commit | ||
| router.get('/api/sessions/:sessionId/git-diff', async (req, res) => { | ||
| const sessionName = resolveSession(req, res); | ||
| if (!sessionName) | ||
| return; | ||
| const commit = req.query.commit; | ||
| if (!commit || !/^[a-f0-9]{7,40}$/.test(commit)) { | ||
| res.status(400).json({ error: 'Invalid commit hash' }); | ||
| return; | ||
| } | ||
| const file = req.query.file; | ||
| if (file && (file.includes('..') || file.startsWith('/'))) { | ||
| res.status(400).json({ error: 'Invalid file path' }); | ||
| return; | ||
| } | ||
| let cwd; | ||
| try { | ||
| cwd = await getCwd(sessionName); | ||
| } | ||
| catch { | ||
| res.status(404).json({ error: 'Session not found' }); | ||
| return; | ||
| } | ||
| try { | ||
| // Check if it's the root commit (no parent) | ||
| let args; | ||
| try { | ||
| await execFile('git', ['rev-parse', `${commit}~1`], { cwd, timeout: EXEC_TIMEOUT }); | ||
| args = ['diff', `${commit}~1`, commit]; | ||
| } | ||
| catch { | ||
| // Root commit | ||
| args = ['diff', '--root', commit]; | ||
| } | ||
| if (file) { | ||
| args.push('--', file); | ||
| } | ||
| const { stdout } = await execFile('git', args, { cwd, timeout: EXEC_TIMEOUT }); | ||
| res.json({ diff: stdout }); | ||
| } | ||
| catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| console.error('[api:git-diff]', msg); | ||
| res.status(500).json({ error: 'Failed to get diff' }); | ||
| } | ||
| }); | ||
| // Git branches list | ||
| router.get('/api/sessions/:sessionId/git-branches', async (req, res) => { | ||
| const sessionName = resolveSession(req, res); | ||
| if (!sessionName) | ||
| return; | ||
| let cwd; | ||
| try { | ||
| cwd = await getCwd(sessionName); | ||
| } | ||
| catch { | ||
| res.status(404).json({ error: 'Session not found' }); | ||
| return; | ||
| } | ||
| try { | ||
| const { stdout } = await execFile('git', ['branch', '-a', '--no-color'], { cwd, timeout: EXEC_TIMEOUT }); | ||
| const branches = []; | ||
| let current = ''; | ||
| for (const line of stdout.split('\n')) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed) | ||
| continue; | ||
| if (trimmed.startsWith('* ')) { | ||
| current = trimmed.slice(2); | ||
| branches.push(current); | ||
| } | ||
| else { | ||
| branches.push(trimmed); | ||
| } | ||
| } | ||
| res.json({ current, branches }); | ||
| } | ||
| catch (err) { | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| if (msg.includes('not a git repository')) { | ||
| res.json({ current: '', branches: [] }); | ||
| } | ||
| else { | ||
| console.error('[api:git-branches]', msg); | ||
| res.status(500).json({ error: 'Failed to get branches' }); | ||
| } | ||
| } | ||
| }); | ||
| export default router; |
| export {}; |
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
| import express from 'express'; | ||
| import request from 'supertest'; | ||
| // --------------------------------------------------------------------------- | ||
| // Mocks — vi.mock is hoisted, so use vi.hoisted for shared state | ||
| // --------------------------------------------------------------------------- | ||
| const { mockExecFile, mockResolveSession, mockGetCwd } = vi.hoisted(() => ({ | ||
| mockExecFile: vi.fn(), | ||
| mockResolveSession: vi.fn(() => 'mock-session'), | ||
| mockGetCwd: vi.fn(), | ||
| })); | ||
| vi.mock('../middleware/auth.js', () => ({ | ||
| resolveSession: mockResolveSession, | ||
| })); | ||
| vi.mock('../tmux.js', () => ({ | ||
| getCwd: mockGetCwd, | ||
| })); | ||
| vi.mock('util', async () => { | ||
| const actual = await vi.importActual('util'); | ||
| return { ...actual, promisify: () => mockExecFile }; | ||
| }); | ||
| import gitRouter from './git.js'; | ||
| // --------------------------------------------------------------------------- | ||
| // App setup | ||
| // --------------------------------------------------------------------------- | ||
| function createApp() { | ||
| const app = express(); | ||
| app.use(express.json()); | ||
| app.use(gitRouter); | ||
| return app; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Tests | ||
| // --------------------------------------------------------------------------- | ||
| describe('GET /api/sessions/:sessionId/git-log', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockResolveSession.mockReturnValue('mock-session'); | ||
| }); | ||
| it('returns 401 when resolveSession fails', async () => { | ||
| mockResolveSession.mockImplementation((_req, res) => { | ||
| res.status(401).json({ error: 'Unauthorized' }); | ||
| return null; | ||
| }); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-log'); | ||
| expect(res.status).toBe(401); | ||
| }); | ||
| it('returns 404 when session/cwd not found', async () => { | ||
| mockGetCwd.mockRejectedValue(new Error('no session')); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-log'); | ||
| expect(res.status).toBe(404); | ||
| expect(res.body.error).toBe('Session not found'); | ||
| }); | ||
| it('returns commits on success', async () => { | ||
| mockGetCwd.mockResolvedValue('/home/user/project'); | ||
| const SEP = '---GIT-LOG-SEP---'; | ||
| const gitOutput = [ | ||
| `${SEP}`, | ||
| 'abc1234567890abcdef1234567890abcdef123456', | ||
| 'abc1234', | ||
| 'Initial commit', | ||
| 'TestUser', | ||
| '2024-01-01T00:00:00+00:00', | ||
| '10\t2\tREADME.md', | ||
| ].join('\n'); | ||
| mockExecFile.mockResolvedValue({ stdout: gitOutput, stderr: '' }); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-log'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.commits).toHaveLength(1); | ||
| expect(res.body.commits[0]).toMatchObject({ | ||
| hash: 'abc1234567890abcdef1234567890abcdef123456', | ||
| shortHash: 'abc1234', | ||
| message: 'Initial commit', | ||
| author: 'TestUser', | ||
| }); | ||
| expect(res.body.commits[0].files).toHaveLength(1); | ||
| expect(res.body.commits[0].files[0]).toEqual({ | ||
| path: 'README.md', | ||
| additions: 10, | ||
| deletions: 2, | ||
| }); | ||
| expect(res.body.hasMore).toBe(false); | ||
| }); | ||
| it('returns empty for non-git repository', async () => { | ||
| mockGetCwd.mockResolvedValue('/tmp'); | ||
| mockExecFile.mockRejectedValue(new Error('fatal: not a git repository')); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-log'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.commits).toEqual([]); | ||
| expect(res.body.hasMore).toBe(false); | ||
| expect(res.body.error).toBe('Not a git repository'); | ||
| }); | ||
| it('supports file filter query param', async () => { | ||
| mockGetCwd.mockResolvedValue('/home/user/project'); | ||
| mockExecFile.mockResolvedValue({ stdout: '', stderr: '' }); | ||
| const app = createApp(); | ||
| await request(app).get('/api/sessions/t1/git-log?file=src/index.ts'); | ||
| expect(mockExecFile).toHaveBeenCalled(); | ||
| const callArgs = mockExecFile.mock.calls[0]; | ||
| const args = callArgs[1]; | ||
| expect(args).toContain('--'); | ||
| expect(args).toContain('src/index.ts'); | ||
| }); | ||
| it('supports pagination and hasMore', async () => { | ||
| mockGetCwd.mockResolvedValue('/home/user/project'); | ||
| const SEP = '---GIT-LOG-SEP---'; | ||
| const gitOutput = [ | ||
| `${SEP}`, 'hash1', 'sh1', 'msg1', 'Author', '2024-01-01T00:00:00+00:00', | ||
| `${SEP}`, 'hash2', 'sh2', 'msg2', 'Author', '2024-01-02T00:00:00+00:00', | ||
| ].join('\n'); | ||
| mockExecFile.mockResolvedValue({ stdout: gitOutput, stderr: '' }); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-log?limit=1'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.commits).toHaveLength(1); | ||
| expect(res.body.hasMore).toBe(true); | ||
| }); | ||
| }); | ||
| describe('GET /api/sessions/:sessionId/git-diff', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockResolveSession.mockReturnValue('mock-session'); | ||
| }); | ||
| it('returns 400 for missing commit', async () => { | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-diff'); | ||
| expect(res.status).toBe(400); | ||
| expect(res.body.error).toBe('Invalid commit hash'); | ||
| }); | ||
| it('returns 400 for invalid commit hash', async () => { | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-diff?commit=ZZZZ'); | ||
| expect(res.status).toBe(400); | ||
| }); | ||
| it('returns diff on success', async () => { | ||
| mockGetCwd.mockResolvedValue('/home/user/project'); | ||
| const diffOutput = `diff --git a/file.ts b/file.ts | ||
| --- a/file.ts | ||
| +++ b/file.ts | ||
| @@ -1,3 +1,4 @@ | ||
| line1 | ||
| +added line | ||
| line2 | ||
| line3`; | ||
| mockExecFile | ||
| .mockResolvedValueOnce({ stdout: 'parenthash', stderr: '' }) | ||
| .mockResolvedValueOnce({ stdout: diffOutput, stderr: '' }); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-diff?commit=abc1234'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.diff).toContain('+added line'); | ||
| }); | ||
| it('handles root commit fallback', async () => { | ||
| mockGetCwd.mockResolvedValue('/home/user/project'); | ||
| mockExecFile | ||
| .mockRejectedValueOnce(new Error('unknown revision')) | ||
| .mockResolvedValueOnce({ stdout: 'root diff output', stderr: '' }); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/git-diff?commit=abc1234'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.diff).toBe('root diff output'); | ||
| const secondCall = mockExecFile.mock.calls[1]; | ||
| const args = secondCall[1]; | ||
| expect(args).toContain('--root'); | ||
| }); | ||
| }); |
| export {}; |
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
| import express from 'express'; | ||
| import request from 'supertest'; | ||
| import sessionsRouter from './sessions.js'; | ||
| // --------------------------------------------------------------------------- | ||
| // Mocks | ||
| // --------------------------------------------------------------------------- | ||
| vi.mock('../middleware/auth.js', () => ({ | ||
| extractToken: vi.fn(() => 'test-token'), | ||
| checkAuth: vi.fn(() => true), | ||
| resolveSession: vi.fn(() => 'mock-session'), | ||
| })); | ||
| vi.mock('../tmux.js', () => ({ | ||
| listSessions: vi.fn(), | ||
| killSession: vi.fn(), | ||
| buildSessionName: vi.fn((_token, sessionId) => `mock_${sessionId}`), | ||
| getCwd: vi.fn(), | ||
| getPaneCommand: vi.fn(), | ||
| isValidSessionId: vi.fn((id) => /^[\w-]+$/.test(id)), | ||
| })); | ||
| vi.mock('../websocket.js', () => ({ | ||
| getActiveSessionNames: vi.fn(() => new Set(['mock_t1'])), | ||
| })); | ||
| vi.mock('../db.js', () => ({ | ||
| deleteDraft: vi.fn(), | ||
| })); | ||
| import { checkAuth, resolveSession } from '../middleware/auth.js'; | ||
| import { listSessions, killSession, getCwd, getPaneCommand } from '../tmux.js'; | ||
| import { deleteDraft } from '../db.js'; | ||
| const mockCheckAuth = vi.mocked(checkAuth); | ||
| const mockResolveSession = vi.mocked(resolveSession); | ||
| const mockListSessions = vi.mocked(listSessions); | ||
| const mockKillSession = vi.mocked(killSession); | ||
| const mockGetCwd = vi.mocked(getCwd); | ||
| const mockGetPaneCommand = vi.mocked(getPaneCommand); | ||
| const mockDeleteDraft = vi.mocked(deleteDraft); | ||
| // --------------------------------------------------------------------------- | ||
| // App setup | ||
| // --------------------------------------------------------------------------- | ||
| function createApp() { | ||
| const app = express(); | ||
| app.use(express.json()); | ||
| app.use(sessionsRouter); | ||
| return app; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Tests | ||
| // --------------------------------------------------------------------------- | ||
| describe('GET /api/sessions', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockCheckAuth.mockReturnValue(true); | ||
| }); | ||
| it('returns 401 when auth fails', async () => { | ||
| mockCheckAuth.mockImplementation((_req, res) => { | ||
| res.status(401).json({ error: 'Unauthorized' }); | ||
| return false; | ||
| }); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions'); | ||
| expect(res.status).toBe(401); | ||
| }); | ||
| it('returns sessions with active status', async () => { | ||
| mockListSessions.mockResolvedValue([ | ||
| { sessionId: 't1', sessionName: 'mock_t1', createdAt: 1000 }, | ||
| { sessionId: 't2', sessionName: 'mock_t2', createdAt: 2000 }, | ||
| ]); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body).toHaveLength(2); | ||
| expect(res.body[0]).toMatchObject({ | ||
| sessionId: 't1', | ||
| sessionName: 'mock_t1', | ||
| active: true, | ||
| }); | ||
| expect(res.body[1]).toMatchObject({ | ||
| sessionId: 't2', | ||
| active: false, | ||
| }); | ||
| }); | ||
| }); | ||
| describe('DELETE /api/sessions/:sessionId', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockCheckAuth.mockReturnValue(true); | ||
| mockKillSession.mockResolvedValue(undefined); | ||
| }); | ||
| it('returns 400 for invalid sessionId', async () => { | ||
| const app = createApp(); | ||
| const res = await request(app).delete('/api/sessions/invalid!id'); | ||
| expect(res.status).toBe(400); | ||
| }); | ||
| it('kills session and deletes draft', async () => { | ||
| const app = createApp(); | ||
| const res = await request(app).delete('/api/sessions/t1'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.ok).toBe(true); | ||
| expect(mockKillSession).toHaveBeenCalledWith('mock_t1'); | ||
| expect(mockDeleteDraft).toHaveBeenCalledWith('mock_t1'); | ||
| }); | ||
| }); | ||
| describe('GET /api/sessions/:sessionId/cwd', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockResolveSession.mockReturnValue('mock-session'); | ||
| }); | ||
| it('returns cwd on success', async () => { | ||
| mockGetCwd.mockResolvedValue('/home/user/project'); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/cwd'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.cwd).toBe('/home/user/project'); | ||
| }); | ||
| it('returns 404 when session not found', async () => { | ||
| mockGetCwd.mockRejectedValue(new Error('session not found')); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/cwd'); | ||
| expect(res.status).toBe(404); | ||
| }); | ||
| }); | ||
| describe('GET /api/sessions/:sessionId/pane-command', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockResolveSession.mockReturnValue('mock-session'); | ||
| }); | ||
| it('returns command on success', async () => { | ||
| mockGetPaneCommand.mockResolvedValue('claude'); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/pane-command'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.command).toBe('claude'); | ||
| }); | ||
| it('returns empty string on error', async () => { | ||
| mockGetPaneCommand.mockRejectedValue(new Error('fail')); | ||
| const app = createApp(); | ||
| const res = await request(app).get('/api/sessions/t1/pane-command'); | ||
| expect(res.status).toBe(200); | ||
| expect(res.body.command).toBe(''); | ||
| }); | ||
| }); |
Sorry, the diff of this file is too big to display
+6
-3
| { | ||
| "name": "ai-cli-online", | ||
| "version": "3.0.13", | ||
| "version": "3.0.15", | ||
| "description": "AI-Cli Online - Web Terminal for Claude Code via xterm.js + tmux", | ||
@@ -51,8 +51,11 @@ "license": "MIT", | ||
| "build": "npm run build --workspace=shared && npm run build --workspace=server && npm run build --workspace=web", | ||
| "start": "npm run start --workspace=server" | ||
| "start": "npm run start --workspace=server", | ||
| "test": "npm run test --workspace=server && npm run test --workspace=web", | ||
| "test:watch": "npm run test:watch --workspace=server" | ||
| }, | ||
| "devDependencies": { | ||
| "concurrently": "^8.2.2", | ||
| "typescript": "^5.9.3" | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^2.1.9" | ||
| } | ||
| } |
@@ -21,2 +21,3 @@ import express from 'express'; | ||
| import settingsRouter from './routes/settings.js'; | ||
| import gitRouter from './routes/git.js'; | ||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
@@ -103,2 +104,3 @@ config(); | ||
| app.use(settingsRouter); | ||
| app.use(gitRouter); | ||
| // --- Static files --- | ||
@@ -105,0 +107,0 @@ const webDistPath = join(__dirname, '../../web/dist'); |
| { | ||
| "name": "ai-cli-online-server", | ||
| "version": "3.0.13", | ||
| "version": "3.0.15", | ||
| "description": "CLI-Online Backend Server", | ||
@@ -10,3 +10,4 @@ "main": "dist/index.js", | ||
| "build": "tsc", | ||
| "start": "node dist/index.js" | ||
| "start": "node dist/index.js", | ||
| "test": "npx vitest run" | ||
| }, | ||
@@ -31,6 +32,9 @@ "dependencies": { | ||
| "@types/node": "^20.19.33", | ||
| "@types/supertest": "^6.0.3", | ||
| "@types/ws": "^8.5.10", | ||
| "supertest": "^7.2.2", | ||
| "tsx": "^4.7.0", | ||
| "typescript": "^5.9.3" | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^2.1.9" | ||
| } | ||
| } |
| { | ||
| "name": "ai-cli-online-shared", | ||
| "version": "3.0.13", | ||
| "version": "3.0.15", | ||
| "description": "Shared types for CLI-Online", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -13,3 +13,3 @@ <!DOCTYPE html> | ||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-webfont@1.7.0/lxgwwenkaimono-bold.css" /> | ||
| <script type="module" crossorigin src="/assets/index-DtWYEMhe.js"></script> | ||
| <script type="module" crossorigin src="/assets/index-BxwAIZoA.js"></script> | ||
| <link rel="modulepreload" crossorigin href="/assets/react-vendor-BCIvbQoU.js"> | ||
@@ -16,0 +16,0 @@ <link rel="modulepreload" crossorigin href="/assets/terminal-DnNpv9tw.js"> |
+8
-3
| { | ||
| "name": "ai-cli-online-web", | ||
| "version": "3.0.13", | ||
| "version": "3.0.15", | ||
| "description": "CLI-Online Web Frontend", | ||
@@ -9,3 +9,4 @@ "type": "module", | ||
| "build": "tsc && vite build", | ||
| "preview": "vite preview" | ||
| "preview": "vite preview", | ||
| "test": "npx vitest run" | ||
| }, | ||
@@ -28,2 +29,4 @@ "dependencies": { | ||
| "devDependencies": { | ||
| "@testing-library/jest-dom": "^6.9.1", | ||
| "@testing-library/react": "^16.3.2", | ||
| "@types/react": "^18.2.43", | ||
@@ -33,5 +36,7 @@ "@types/react-dom": "^18.3.7", | ||
| "@vitejs/plugin-react": "^4.2.1", | ||
| "jsdom": "^28.1.0", | ||
| "typescript": "^5.9.3", | ||
| "vite": "^5.0.8" | ||
| "vite": "^5.0.8", | ||
| "vitest": "^2.1.9" | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 14 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 14 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
1172876
2.88%60
11.11%6561
9.46%3
50%80
3.9%20
5.26%