tui: wire approve dispatch in bin entry
phobiCS-tui dispatcher: - --version / -v: print version, exit 0 - approve <existing> <proposed>: read both files, render <Approve>, exit 0 on accept / 1 on reject / 2 on bad args or read error - no args: print 'browser mode coming' placeholder, exit 0 - SIGTERM/SIGINT during approve: unmount + exit 1 Integration tests cover the no-TTY paths (--version, missing file, missing args). Accept/reject keybinds are covered by the ink-testing-library tests in Approve.test.tsx; piping stdin into a no-TTY ink process is flaky on Windows so we don't try to integration-test that path. The shebang line is intentionally absent from the source; the build script prepends one to dist/tui/index.js.
This commit is contained in:
parent
884de61cd7
commit
357c1d11c5
2 changed files with 96 additions and 5 deletions
|
|
@ -1,12 +1,61 @@
|
|||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { Text } from 'ink';
|
||||
import * as fs from 'fs/promises';
|
||||
import { Approve, Decision } from './approve/Approve.js';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
if (argv[0] === '--version' || argv[0] === '-v') {
|
||||
process.stdout.write('phobiCS-tui v0.1.0\n');
|
||||
process.exit(0);
|
||||
async function main(): Promise<number> {
|
||||
if (argv[0] === '--version' || argv[0] === '-v') {
|
||||
process.stdout.write('phobiCS-tui v0.1.0\n');
|
||||
return 0;
|
||||
}
|
||||
if (argv[0] === 'approve') {
|
||||
return runApprove(argv[1], argv[2]);
|
||||
}
|
||||
process.stdout.write('phobiCS-tui — browser mode coming in a later task\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
render(<Text>phobiCS-tui — coming soon</Text>);
|
||||
async function runApprove(oldPath: string | undefined, newPath: string | undefined): Promise<number> {
|
||||
if (!oldPath || !newPath) {
|
||||
process.stderr.write('usage: phobiCS-tui approve <existing> <proposed>\n');
|
||||
return 2;
|
||||
}
|
||||
let oldText: string;
|
||||
let newText: string;
|
||||
try {
|
||||
oldText = await fs.readFile(oldPath, 'utf8');
|
||||
newText = await fs.readFile(newPath, 'utf8');
|
||||
} catch (err) {
|
||||
process.stderr.write(`phobiCS-tui: ${(err as Error).message}\n`);
|
||||
return 2;
|
||||
}
|
||||
|
||||
return new Promise<number>((resolve) => {
|
||||
const onDecision = (d: Decision) => {
|
||||
app.unmount();
|
||||
resolve(d === 'accept' ? 0 : 1);
|
||||
};
|
||||
const fileName = oldPath.split(/[/\\]/).pop() ?? oldPath;
|
||||
const app = render(
|
||||
<Approve fileName={fileName} oldText={oldText} newText={newText} onDecision={onDecision} />
|
||||
);
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
app.unmount();
|
||||
resolve(1);
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
app.unmount();
|
||||
resolve(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.then((code) => process.exit(code))
|
||||
.catch((err) => {
|
||||
process.stderr.write(`phobiCS-tui: ${err}\n`);
|
||||
process.exit(2);
|
||||
});
|
||||
|
|
|
|||
42
tests/tui/index.integration.test.ts
Normal file
42
tests/tui/index.integration.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { spawn } from 'child_process';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
const BIN = path.resolve('dist/tui/index.js');
|
||||
|
||||
beforeAll(async () => {
|
||||
await fs.access(BIN);
|
||||
});
|
||||
|
||||
function run(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(process.execPath, [BIN, ...args], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (d) => (stdout += d.toString()));
|
||||
child.stderr.on('data', (d) => (stderr += d.toString()));
|
||||
child.on('exit', (code) => resolve({ code: code ?? -1, stdout, stderr }));
|
||||
});
|
||||
}
|
||||
|
||||
describe('phobiCS-tui (integration, no-TTY paths only)', () => {
|
||||
it('--version prints version and exits 0', async () => {
|
||||
const r = await run(['--version']);
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toMatch(/phobiCS-tui v\d/);
|
||||
});
|
||||
|
||||
it('approve with missing file exits 2', async () => {
|
||||
const r = await run(['approve', '/nonexistent/old.st', '/nonexistent/new.st']);
|
||||
expect(r.code).toBe(2);
|
||||
});
|
||||
|
||||
it('approve with no args exits 2 with usage on stderr', async () => {
|
||||
const r = await run(['approve']);
|
||||
expect(r.code).toBe(2);
|
||||
expect(r.stderr).toMatch(/usage:/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue