Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions bin/qc.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
#!/usr/bin/env node
// qc: qrypt.chat in the terminal. `qc` opens the chat client; `qc --help` lists the rest.
import { readFileSync } from 'node:fs';
import { errorLine, main } from '../src/cli/commands.js';
import { errorLine, exitWhenFlushed, main } from '../src/cli/commands.js';

const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));

// main resolves when the command is done (the TUI quit, stdin closed under
// mcp); exit then, or the SSE socket and timers would hold the process open.
// mcp); exit then, or the SSE socket and timers would hold the process open,
// but only once stdout has drained (see exit.js).
main(process.argv.slice(2), { version: pkg.version }).then(
() => process.exit(0),
() => exitWhenFlushed(0),
(err) => {
process.stderr.write(`${errorLine(err)}\n`);
process.exit(1);
exitWhenFlushed(1);
},
);
6 changes: 3 additions & 3 deletions packages/qryptchat/bin/qc.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env node
// qc: qrypt.chat in the terminal. `qc` opens the chat client; `qc --help` lists the rest.
import { readFileSync } from 'node:fs';
import { errorLine, main } from '../dist/commands.js';
import { errorLine, exitWhenFlushed, main } from '../dist/commands.js';

const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));

main(process.argv.slice(2), { version: pkg.version }).then(
() => process.exit(0),
() => exitWhenFlushed(0),
(err) => {
process.stderr.write(`${errorLine(err)}\n`);
process.exit(1);
exitWhenFlushed(1);
},
);
2 changes: 1 addition & 1 deletion packages/qryptchat/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/qryptchat",
"version": "0.6.3",
"version": "0.6.4",
"description": "qc: qrypt.chat in your terminal. A full-screen end-to-end encrypted chat client (ML-KEM-1024), plus a scriptable CLI and an MCP server.",
"type": "module",
"bin": {
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { login } from './login.js';
import { joinAsAgent } from './agent.js';
import { em, fail, info, mark, ok, warn, who } from './style.js';
import { notify } from './notify.js';
export { exitWhenFlushed } from './exit.js';

/** An error as qc prints it: the error icon and the message. */
export const errorLine = (err) => `qc: ${fail(err?.message ?? String(err))}`;
Expand Down
10 changes: 10 additions & 0 deletions src/cli/exit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// process.exit() does not wait for stdout. Into a pipe (qc read --json | jq, a
// script's subprocess) the write is asynchronous, so anything past the 64 KB
// pipe buffer was dropped and the reader got truncated JSON. Exit once both
// streams have finished; the timer only guards a reader that never reads.
// end(), not write('', cb): under Bun an empty write calls back before the
// earlier data has drained.
export function exitWhenFlushed(code, { timeoutMs = 30_000 } = {}) {
setTimeout(() => process.exit(code), timeoutMs);
process.stdout.end(() => process.stderr.end(() => process.exit(code)));
}
35 changes: 35 additions & 0 deletions tests/cli/exit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { describe, expect, it } from 'vitest';

// vitest runs from the repo root; import.meta.url is http:// under jsdom.
const exitJs = pathToFileURL(resolve('src/cli/exit.js')).href;
const SIZE = 1024 * 1024;

// Run a child that writes SIZE bytes to a piped stdout, then exits the given way.
// `node` is Bun under `bun --bun vitest`, so this covers both runtimes between
// a local `node` run and CI.
function run(exit) {
const script = `import { exitWhenFlushed } from ${JSON.stringify(exitJs)};
process.stdout.write('x'.repeat(${SIZE}));
${exit}`;
const r = spawnSync('node', ['--input-type=module', '-e', script], { maxBuffer: SIZE * 2 });
return r;
}

describe('exitWhenFlushed', () => {
it('delivers all of a large piped stdout before exiting', () => {
const r = run('exitWhenFlushed(0);');
expect(r.status).toBe(0);
expect(r.stdout.length).toBe(SIZE);
});

it('keeps the exit code', () => {
expect(run('exitWhenFlushed(3);').status).toBe(3);
});

it('is needed: a bare process.exit truncates at the pipe buffer', () => {
expect(run('process.exit(0);').stdout.length).toBeLessThan(SIZE);
});
});
Loading