From c646685309c94163d3a378e92514da1ad1e05eea Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 11 Aug 2026 10:02:29 +0000 Subject: [PATCH 1/5] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/link-foundation/command-stream/issues/192 --- .gitkeep | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitkeep b/.gitkeep index 3eba852..9d75f85 100644 --- a/.gitkeep +++ b/.gitkeep @@ -1 +1,2 @@ -# .gitkeep file auto-generated at 2026-08-07T19:41:37.524Z for PR creation at branch issue-187-3d458fd12c95 for issue https://github.com/link-foundation/command-stream/issues/187 \ No newline at end of file +# .gitkeep file auto-generated at 2026-08-07T19:41:37.524Z for PR creation at branch issue-187-3d458fd12c95 for issue https://github.com/link-foundation/command-stream/issues/187 +# Updated: 2026-08-11T10:02:29.523Z \ No newline at end of file From 1bb4392d9c5205c8c88a46faa275649ca3f9c037 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 11 Aug 2026 10:07:53 +0000 Subject: [PATCH 2/5] test(js): cover lightweight runner entry --- js/tests/process-runner-entry.test.mjs | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 js/tests/process-runner-entry.test.mjs diff --git a/js/tests/process-runner-entry.test.mjs b/js/tests/process-runner-entry.test.mjs new file mode 100644 index 0000000..2b0372f --- /dev/null +++ b/js/tests/process-runner-entry.test.mjs @@ -0,0 +1,83 @@ +import { expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { dirname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const testDirectory = dirname(fileURLToPath(import.meta.url)); +const packageDirectory = resolve(testDirectory, '..'); +const packageJson = JSON.parse( + readFileSync(resolve(packageDirectory, 'package.json'), 'utf8') +); +const processRunnerExport = packageJson.exports['./process-runner']; +const terminalDependencies = new Set([ + '@resvg/resvg-js', + '@xterm/headless', + 'gifenc', + 'node-pty', +]); + +function collectModuleGraph(entrypoint) { + const pending = [entrypoint]; + const modules = new Set(); + const packages = new Set(); + const importPattern = + /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)['"]([^'"]+)['"]/g; + + while (pending.length > 0) { + const modulePath = pending.pop(); + if (modules.has(modulePath)) { + continue; + } + modules.add(modulePath); + + const source = readFileSync(modulePath, 'utf8'); + for (const match of source.matchAll(importPattern)) { + const specifier = match[1]; + if (specifier.startsWith('.')) { + pending.push(resolve(dirname(modulePath), specifier)); + } else if (!specifier.startsWith('node:')) { + packages.add(specifier); + } + } + } + + return { modules, packages }; +} + +test('exports a fully initialized ProcessRunner subpath', async () => { + expect(processRunnerExport).toBe('./src/process-runner.mjs'); + + const { ProcessRunner } = await import('command-stream/process-runner'); + expect(typeof ProcessRunner.prototype.start).toBe('function'); + + const runner = new ProcessRunner( + { + mode: 'exec', + file: process.execPath, + args: ['-e', "process.stdout.write('lightweight runner')"], + }, + { capture: true, mirror: false, stdin: 'ignore' } + ); + const result = await runner; + + expect(result.code).toBe(0); + expect(result.stdout).toBe('lightweight runner'); +}); + +test('keeps terminal features out of the ProcessRunner module graph', () => { + if (!processRunnerExport) { + throw new Error('command-stream/process-runner is not exported'); + } + + const entrypoint = resolve(packageDirectory, processRunnerExport); + const graph = collectModuleGraph(entrypoint); + const terminalModules = [...graph.modules] + .map((modulePath) => relative(packageDirectory, modulePath)) + .filter((modulePath) => modulePath.includes('terminal-')); + const importedTerminalDependencies = [...graph.packages].filter((specifier) => + terminalDependencies.has(specifier) + ); + + expect(terminalModules).toEqual([]); + expect(importedTerminalDependencies).toEqual([]); +}); From 9ac5b4a8be6effb846dc8c8db2b092118a6a6239 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 11 Aug 2026 10:09:43 +0000 Subject: [PATCH 3/5] feat(js): add lightweight runner export --- js/.changeset/lightweight-process-runner.md | 5 ++++ js/README.md | 18 +++++++++++ js/package.json | 3 +- js/src/$.mjs | 28 +---------------- js/src/process-runner.mjs | 33 +++++++++++++++++++++ 5 files changed, 59 insertions(+), 28 deletions(-) create mode 100644 js/.changeset/lightweight-process-runner.md create mode 100644 js/src/process-runner.mjs diff --git a/js/.changeset/lightweight-process-runner.md b/js/.changeset/lightweight-process-runner.md new file mode 100644 index 0000000..cdc3f5d --- /dev/null +++ b/js/.changeset/lightweight-process-runner.md @@ -0,0 +1,5 @@ +--- +'command-stream': patch +--- + +Add a lightweight `command-stream/process-runner` export that excludes optional PTY and terminal rendering dependencies from its module graph. diff --git a/js/README.md b/js/README.md index b1aeee5..66ebeed 100644 --- a/js/README.md +++ b/js/README.md @@ -156,6 +156,24 @@ npm install command-stream bun add command-stream ``` +### Lightweight ProcessRunner entry point + +Consumers that only need direct process execution can import `ProcessRunner` +without loading the optional PTY, terminal rendering, SVG, or GIF modules: + +```javascript +import { ProcessRunner } from 'command-stream/process-runner'; + +const runner = new ProcessRunner( + { mode: 'exec', file: 'git', args: ['status', '--short'] }, + { mirror: false, capture: true, stdin: 'ignore' } +); +const result = await runner; +``` + +The main `command-stream` entry point remains the supported import for `$` and +terminal capture features. + ## Smart Quoting & Security Command-stream provides intelligent auto-quoting to protect against shell injection while avoiding unnecessary quotes for safe strings: diff --git a/js/package.json b/js/package.json index fc5943f..7410fb4 100644 --- a/js/package.json +++ b/js/package.json @@ -5,7 +5,8 @@ "type": "module", "main": "src/$.mjs", "exports": { - ".": "./src/$.mjs" + ".": "./src/$.mjs", + "./process-runner": "./src/process-runner.mjs" }, "repository": { "type": "git", diff --git a/js/src/$.mjs b/js/src/$.mjs index f726b0b..be160c5 100755 --- a/js/src/$.mjs +++ b/js/src/$.mjs @@ -5,7 +5,6 @@ import { trace } from './$.trace.mjs'; import { globalShellSettings, virtualCommands, - isVirtualCommandsEnabled, enableVirtualCommands as enableVirtualCommandsState, disableVirtualCommands as disableVirtualCommandsState, forceCleanupAll, @@ -25,32 +24,7 @@ import { unrollTerminalFrames, } from './terminal-capture.mjs'; -// Import ProcessRunner base and method modules -import { ProcessRunner } from './$.process-runner-base.mjs'; -import { attachExecutionMethods } from './$.process-runner-execution.mjs'; -import { attachPipelineMethods } from './$.process-runner-pipeline.mjs'; -import { attachOrchestrationMethods } from './$.process-runner-orchestration.mjs'; -import { attachVirtualCommandMethods } from './$.process-runner-virtual.mjs'; -import { attachStreamKillMethods } from './$.process-runner-stream-kill.mjs'; - -// Create dependencies object for method attachment -const deps = { - virtualCommands, - globalShellSettings, - isVirtualCommandsEnabled, -}; - -// Attach all methods to ProcessRunner prototype using mixin pattern -attachExecutionMethods(ProcessRunner, deps); -attachPipelineMethods(ProcessRunner, deps); -attachOrchestrationMethods(ProcessRunner, deps); -attachVirtualCommandMethods(ProcessRunner, deps); -attachStreamKillMethods(ProcessRunner, deps); - -trace( - 'Initialization', - () => 'ProcessRunner methods attached via mixin pattern' -); +import { ProcessRunner } from './process-runner.mjs'; // Public APIs async function sh(commandString, options = {}) { diff --git a/js/src/process-runner.mjs b/js/src/process-runner.mjs new file mode 100644 index 0000000..09ef4cf --- /dev/null +++ b/js/src/process-runner.mjs @@ -0,0 +1,33 @@ +// Lightweight ProcessRunner entry point without terminal capture dependencies + +import { ProcessRunner } from './$.process-runner-base.mjs'; +import { attachExecutionMethods } from './$.process-runner-execution.mjs'; +import { attachOrchestrationMethods } from './$.process-runner-orchestration.mjs'; +import { attachPipelineMethods } from './$.process-runner-pipeline.mjs'; +import { attachStreamKillMethods } from './$.process-runner-stream-kill.mjs'; +import { attachVirtualCommandMethods } from './$.process-runner-virtual.mjs'; +import { + globalShellSettings, + isVirtualCommandsEnabled, + virtualCommands, +} from './$.state.mjs'; +import { trace } from './$.trace.mjs'; + +const dependencies = { + virtualCommands, + globalShellSettings, + isVirtualCommandsEnabled, +}; + +attachExecutionMethods(ProcessRunner, dependencies); +attachPipelineMethods(ProcessRunner, dependencies); +attachOrchestrationMethods(ProcessRunner, dependencies); +attachVirtualCommandMethods(ProcessRunner, dependencies); +attachStreamKillMethods(ProcessRunner, dependencies); + +trace( + 'Initialization', + () => 'ProcessRunner methods attached via mixin pattern' +); + +export { ProcessRunner }; From 100be0723547c94f0db6d02d218242352b00d8b7 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 11 Aug 2026 10:15:05 +0000 Subject: [PATCH 4/5] chore: remove pull request placeholder --- .gitkeep | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 9d75f85..0000000 --- a/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ -# .gitkeep file auto-generated at 2026-08-07T19:41:37.524Z for PR creation at branch issue-187-3d458fd12c95 for issue https://github.com/link-foundation/command-stream/issues/187 -# Updated: 2026-08-11T10:02:29.523Z \ No newline at end of file From 8bad68c2b0ed6f52e40c4507eb348725f4d76838 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 11 Aug 2026 10:18:04 +0000 Subject: [PATCH 5/5] test(js): verify runner mixin initialization --- js/tests/process-runner-entry.test.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/js/tests/process-runner-entry.test.mjs b/js/tests/process-runner-entry.test.mjs index 2b0372f..fa523ba 100644 --- a/js/tests/process-runner-entry.test.mjs +++ b/js/tests/process-runner-entry.test.mjs @@ -48,7 +48,16 @@ test('exports a fully initialized ProcessRunner subpath', async () => { expect(processRunnerExport).toBe('./src/process-runner.mjs'); const { ProcessRunner } = await import('command-stream/process-runner'); - expect(typeof ProcessRunner.prototype.start).toBe('function'); + for (const method of [ + 'start', + '_runPipeline', + 'pipe', + '_runVirtual', + 'stream', + 'kill', + ]) { + expect(typeof ProcessRunner.prototype[method]).toBe('function'); + } const runner = new ProcessRunner( {