From 85f9d3c4e70177e6d09c7df7db701d82f642923d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:54:02 -0500 Subject: [PATCH] Fix non-interactive init to complete with piped stdin --- src/ops.mjs | 47 ++++++++++++++++++--------- test/ops-noninteractive-init.test.mjs | 44 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 test/ops-noninteractive-init.test.mjs diff --git a/src/ops.mjs b/src/ops.mjs index 1dc1409..9383d57 100644 --- a/src/ops.mjs +++ b/src/ops.mjs @@ -186,35 +186,52 @@ function syncAppFiles(torchDir, installDir, verb = 'Copied') { * @returns {Promise<{installDir: string, namespace: string, hashtag: string, relays: string[]}>} - User configuration. */ async function interactiveInit(cwd) { - const rl = readline.createInterface({ input, output }); const currentDirName = path.basename(cwd); + let defaultDir = 'torch'; + if (currentDirName === 'torch') { + defaultDir = '.'; + } + const randomSuffix = crypto.randomBytes(4).toString('hex'); + const defaultNamespace = `torch-${randomSuffix}`; + const defaultHashtag = `${defaultNamespace}-agent-lock`; + + if (!input.isTTY) { + return { + installDir: defaultDir, + namespace: defaultNamespace, + hashtag: defaultHashtag, + relays: DEFAULT_RELAYS, + }; + } + + const rl = readline.createInterface({ input, output }); + const askWithDefault = async (prompt, fallback) => { + try { + const answer = await rl.question(prompt); + return answer.trim() || fallback; + } catch { + // In non-interactive/piped sessions EOF can arrive mid-flow. + // Treat it as "accept defaults" so init still completes deterministically. + return fallback; + } + }; console.log('\nšŸ”„ TORCH Initialization šŸ”„\n'); try { // 1. Install Directory - let defaultDir = 'torch'; - if (currentDirName === 'torch') { - defaultDir = '.'; - } - - const dirAnswer = await rl.question(`Install directory (default: ${defaultDir}): `); - const installDir = dirAnswer.trim() || defaultDir; + const installDir = await askWithDefault(`Install directory (default: ${defaultDir}): `, defaultDir); // 2. Namespace - const randomSuffix = crypto.randomBytes(4).toString('hex'); - const defaultNamespace = `torch-${randomSuffix}`; - const namespaceAnswer = await rl.question(`Nostr Namespace (default: ${defaultNamespace}): `); - const namespace = namespaceAnswer.trim() || defaultNamespace; + const namespace = await askWithDefault(`Nostr Namespace (default: ${defaultNamespace}): `, defaultNamespace); // 3. Hashtag const defaultHashtag = `${namespace}-agent-lock`; - const hashtagAnswer = await rl.question(`Nostr Hashtag (default: ${defaultHashtag}): `); - const hashtag = hashtagAnswer.trim() || defaultHashtag; + const hashtag = await askWithDefault(`Nostr Hashtag (default: ${defaultHashtag}): `, defaultHashtag); // 4. Relays console.log(`\nDefault Relays:\n ${DEFAULT_RELAYS.join('\n ')}`); - const relaysAnswer = await rl.question('Enter relays (comma-separated) or press Enter to use defaults: '); + const relaysAnswer = await askWithDefault('Enter relays (comma-separated) or press Enter to use defaults: ', ''); let relays = DEFAULT_RELAYS; if (relaysAnswer.trim()) { relays = relaysAnswer.split(',').map(r => r.trim()).filter(Boolean); diff --git a/test/ops-noninteractive-init.test.mjs b/test/ops-noninteractive-init.test.mjs new file mode 100644 index 0000000..450df5d --- /dev/null +++ b/test/ops-noninteractive-init.test.mjs @@ -0,0 +1,44 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +function runInitWithSingleNewline(cwd) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [path.resolve('bin/torch-lock.mjs'), 'init', '--force'], { + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.stdin.write('\n'); + child.stdin.end(); + + child.on('close', (code) => { + resolve({ code: code ?? 1, stdout, stderr }); + }); + }); +} + +test('init completes in non-interactive mode when stdin closes early', async () => { + const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'torch-init-noninteractive-')); + await fs.promises.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'tmp-host', version: '1.0.0' }), 'utf8'); + + const result = await runInitWithSingleNewline(tmpDir); + assert.equal(result.code, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`); + + assert.ok(fs.existsSync(path.join(tmpDir, 'torch')), 'torch directory should be created'); + assert.ok(fs.existsSync(path.join(tmpDir, 'torch-config.json')), 'root torch-config.json should be created'); + assert.ok(fs.existsSync(path.join(tmpDir, 'torch', 'src', 'prompts', 'scheduler-flow.md')), 'scheduler flow should be installed'); +});