Compare commits

...

6 Commits

Author SHA1 Message Date
2896cc396f 1.6.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-17 06:43:36 +00:00
e76ad2fb58 fix(ts/index): Use cli.js as the spawned CLI entry point instead of cli.child.js 2025-10-17 06:43:36 +00:00
6b6ecee0ed 1.6.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-17 06:26:00 +00:00
e5b57c894b fix(plugins): Export child_process.spawn from plugins and use plugins.spawn in spawnPath to remove direct require and unify process spawning 2025-10-17 06:26:00 +00:00
ec2db7af72 1.6.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-17 06:15:33 +00:00
9b5668eccb feat(core): Add spawnPath child-process API with timeout/abort/terminate support, export native types, and expand README 2025-10-17 06:15:33 +00:00
6 changed files with 494 additions and 14 deletions

View File

@@ -1,5 +1,29 @@
# Changelog
## 2025-10-17 - 1.6.2 - fix(ts/index)
Use cli.js as the spawned CLI entry point instead of cli.child.js
- Replace references to ../cli.child.js with ../cli.js in ts/index.ts (runInChildProcess and spawnPath) to ensure child processes spawn the correct CLI entry point.
- This change fixes child process spawning failures caused by referencing a non-existent cli.child.js file.
- Add local .claude/settings.local.json (local runner/editor permissions configuration).
## 2025-10-17 - 1.6.1 - fix(plugins)
Export child_process.spawn from plugins and use plugins.spawn in spawnPath to remove direct require and unify process spawning
- Exported spawn from ts/plugins.ts so native child_process.spawn is available via the plugins module
- Removed require('child_process') from ts/index.ts and switched to plugins.spawn when spawning child processes in spawnPath
- No public API changes; this unifies internal imports and fixes inconsistent spawn usage that could cause runtime issues
## 2025-10-17 - 1.6.0 - feat(core)
Add spawnPath child-process API with timeout/abort/terminate support, export native types, and expand README
- Implement spawnPath(filePath, fromFileUrl?, options?) in ts/index.ts producing an ITsrunChildProcess with childProcess, stdout, stderr, exitCode, kill() and terminate()
- Introduce ISpawnOptions (cwd, env, args, stdio, timeout, signal) and ITsrunChildProcess interfaces for robust process control
- Handle timeouts (auto SIGTERM), AbortSignal cancellation, and graceful terminate() (SIGTERM then SIGKILL after 5s)
- Export Node types ChildProcess and Readable from ts/plugins.ts for improved typings
- Greatly expand README: add badges, table of contents, detailed API docs and examples for runPath, runCli and spawnPath, and troubleshooting guidance
- Add local .claude/settings.local.json (environment/settings file)
## 2025-10-16 - 1.5.0 - feat(core)
Add cwd option and child-process execution for custom working directory; implement signal-forwarding child runner; update docs and bump package version to 1.4.0

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsrun",
"version": "1.5.0",
"version": "1.6.2",
"description": "run typescript programs efficiently",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",

290
readme.md
View File

@@ -1,9 +1,31 @@
# @git.zone/tsrun
[![npm version](https://img.shields.io/npm/v/@git.zone/tsrun.svg)](https://www.npmjs.com/package/@git.zone/tsrun)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-%3E%3D3.x-blue)](https://www.typescriptlang.org/)
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D16.x-green)](https://nodejs.org/)
> Run TypeScript files instantly, without the compilation hassle ⚡
Execute TypeScript programs on-the-fly with zero configuration. Perfect for scripts, prototyping, and development workflows.
## Table of Contents
- [What is tsrun?](#what-is-tsrun)
- [Installation](#installation)
- [Usage](#usage)
- [CLI Usage](#-cli-usage)
- [Programmatic API](#-programmatic-api)
- [Features](#features)
- [Why tsrun?](#why-tsrun)
- [Common Use Cases](#common-use-cases)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Package Information](#package-information)
- [Requirements](#requirements)
- [Troubleshooting](#troubleshooting)
- [License and Legal Information](#license-and-legal-information)
## What is tsrun?
**tsrun** is a lightweight TypeScript execution tool that lets you run `.ts` files directly—no build step required. It's like running JavaScript with `node`, but for TypeScript. Under the hood, tsrun uses [tsx](https://github.com/esbuild-kit/tsx) for lightning-fast execution while keeping your workflow simple and efficient.
@@ -40,19 +62,56 @@ All arguments are passed through to your TypeScript program, just as if you were
### 💻 Programmatic API
Import tsrun in your code for dynamic TypeScript execution:
tsrun provides three powerful functions for different execution needs:
#### runPath() - Simple Execution
Wait for a script to complete. Perfect for sequential workflows.
```typescript
import { runPath, runCli } from '@git.zone/tsrun';
import { runPath } from '@git.zone/tsrun';
// Run a TypeScript file from an absolute or relative path
await runPath('./scripts/myScript.ts');
// Run a TypeScript file (absolute or relative path)
await runPath('./scripts/build.ts');
// Run with path resolution relative to a file URL
await runPath('./myScript.ts', import.meta.url);
// With path resolution relative to a file URL
await runPath('./build.ts', import.meta.url);
// Run in CLI mode programmatically (respects process.argv)
await runCli('./myScript.ts');
// With custom working directory
await runPath('./build.ts', import.meta.url, { cwd: '/path/to/project' });
```
#### runCli() - CLI Mode
Run with process.argv integration, as if invoked from command line.
```typescript
import { runCli } from '@git.zone/tsrun';
// Respects process.argv for argument passing
await runCli('./script.ts');
```
#### spawnPath() - Advanced Control
Full process control with stdio access, timeouts, and cancellation.
```typescript
import { spawnPath } from '@git.zone/tsrun';
// Returns immediately with process handle
const proc = spawnPath('./task.ts', import.meta.url, {
timeout: 30000,
cwd: '/path/to/project',
env: { NODE_ENV: 'production' },
args: ['--verbose']
});
// Access stdout/stderr streams
proc.stdout?.on('data', (chunk) => console.log(chunk.toString()));
// Wait for completion
const exitCode = await proc.exitCode;
```
## Features
@@ -69,6 +128,8 @@ await runCli('./myScript.ts');
🔀 **Custom Working Directory** - Execute scripts with different cwds for parallel multi-project workflows.
🎛️ **Advanced Process Control** - Full control with spawnPath() for stdio access, timeouts, and cancellation.
## Why tsrun?
Sometimes you just want to run a TypeScript file without setting up a build pipeline, configuring webpack, or waiting for `tsc` to compile. That's where tsrun shines:
@@ -78,6 +139,84 @@ Sometimes you just want to run a TypeScript file without setting up a build pipe
- **Development Workflows**: Integrate TypeScript execution into your tooling
- **CI/CD**: Run TypeScript-based build scripts without pre-compilation
## Common Use Cases
### Development Scripts
```typescript
// scripts/dev-setup.ts
import { runPath } from '@git.zone/tsrun';
console.log('Setting up development environment...');
await runPath('./install-deps.ts', import.meta.url);
await runPath('./init-db.ts', import.meta.url);
await runPath('./seed-data.ts', import.meta.url);
console.log('✓ Development environment ready!');
```
### Multi-Project Builds
```typescript
// build-all-projects.ts
import { runPath } from '@git.zone/tsrun';
const projects = [
'/workspace/frontend',
'/workspace/backend',
'/workspace/shared'
];
await Promise.all(
projects.map(cwd =>
runPath('./build.ts', import.meta.url, { cwd })
)
);
```
### Long-Running Tasks with Monitoring
```typescript
// monitor-task.ts
import { spawnPath } from '@git.zone/tsrun';
const proc = spawnPath('./data-migration.ts', import.meta.url, {
timeout: 300000, // 5 minutes max
env: { LOG_LEVEL: 'verbose' }
});
let lineCount = 0;
proc.stdout?.on('data', (chunk) => {
lineCount++;
if (lineCount % 100 === 0) {
console.log(`Processed ${lineCount} lines...`);
}
});
try {
await proc.exitCode;
console.log('Migration completed successfully!');
} catch (error) {
console.error('Migration failed:', error.message);
process.exit(1);
}
```
## API Reference
Choose the right function for your use case:
| Function | Use When | Returns | Execution Mode |
|----------|----------|---------|----------------|
| `runPath()` | Simple script execution, sequential workflows | Promise (waits) | In-process (or child with cwd) |
| `runCli()` | Need process.argv integration | Promise (waits) | In-process |
| `spawnPath()` | Need process control, stdio access, timeout/cancel | Process handle | Child process |
**Quick decision guide:**
- 🎯 **Need to wait for completion?** → Use `runPath()` or `runCli()`
- 🎛️ **Need to capture output or control process?** → Use `spawnPath()`
-**Running multiple scripts in parallel?** → Use `runPath()` with custom `cwd` or `spawnPath()`
- ⏱️ **Need timeout or cancellation?** → Use `spawnPath()`
## Examples
### Simple Script
@@ -172,6 +311,87 @@ await Promise.all([
- Each child process runs with its own isolated working directory
- Exit codes and signals are properly forwarded
### Advanced Process Control with spawnPath()
For advanced use cases requiring full process control, stdio access, or timeout/cancellation support, use `spawnPath()`. Unlike `runPath()` which waits for completion, `spawnPath()` returns immediately with a process handle.
```typescript
import { spawnPath } from '@git.zone/tsrun';
// Basic spawning with output capture
const proc = spawnPath('./build.ts', import.meta.url);
proc.stdout?.on('data', (chunk) => {
console.log('Output:', chunk.toString());
});
proc.stderr?.on('data', (chunk) => {
console.error('Error:', chunk.toString());
});
const exitCode = await proc.exitCode;
console.log(`Process exited with code ${exitCode}`);
```
**With timeout and custom environment:**
```typescript
const proc = spawnPath('./long-running-task.ts', import.meta.url, {
timeout: 30000, // Kill after 30 seconds
cwd: '/path/to/project',
env: {
NODE_ENV: 'production',
API_KEY: 'secret'
},
args: ['--mode', 'fast']
});
try {
const exitCode = await proc.exitCode;
console.log('Task completed:', exitCode);
} catch (error) {
console.error('Task failed or timed out:', error.message);
}
```
**AbortController integration:**
```typescript
const controller = new AbortController();
const proc = spawnPath('./task.ts', import.meta.url, {
signal: controller.signal
});
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
await proc.exitCode;
} catch (error) {
console.log('Process was aborted');
}
```
**Graceful termination:**
```typescript
const proc = spawnPath('./server.ts', import.meta.url);
// Later: gracefully shut down
// Sends SIGTERM, waits 5s, then SIGKILL if still running
await proc.terminate();
```
**Key differences from runPath():**
| Feature | runPath() | spawnPath() |
|---------|-----------|-------------|
| Returns | Promise (waits) | Process handle (immediate) |
| Default execution | In-process (unless cwd) | Always child process |
| stdio | 'inherit' (transparent) | 'pipe' (capturable) |
| Process control | Limited | Full (streams, signals, timeout) |
| Use case | Simple script execution | Complex process management |
## Package Information
- **npmjs**: [@git.zone/tsrun](https://www.npmjs.com/package/@git.zone/tsrun)
@@ -183,6 +403,60 @@ await Promise.all([
- **Node.js**: >= 16.x
- **TypeScript**: >= 3.x (automatically handled by tsx)
## Troubleshooting
### Common Issues
**"Cannot find module" errors**
Make sure you're using absolute paths or paths relative to `import.meta.url`:
```typescript
// ❌ Wrong - relative to cwd
await runPath('./script.ts');
// ✅ Correct - relative to current file
await runPath('./script.ts', import.meta.url);
```
**Process hangs or doesn't complete**
When using `spawnPath()`, make sure to await the `exitCode` promise:
```typescript
const proc = spawnPath('./script.ts', import.meta.url);
// Don't forget to await!
await proc.exitCode;
```
**Timeout not working**
Timeouts only work with `spawnPath()`, not with `runPath()`:
```typescript
// ❌ Wrong - timeout is ignored
await runPath('./script.ts', import.meta.url, { timeout: 5000 });
// ✅ Correct - use spawnPath for timeout support
const proc = spawnPath('./script.ts', import.meta.url, { timeout: 5000 });
await proc.exitCode;
```
**Environment variables not available**
The `env` option automatically merges with `process.env` - your custom values override parent values:
```typescript
// Parent env is automatically inherited
spawnPath('./script.ts', import.meta.url, {
env: {
CUSTOM_VAR: 'value' // Added to parent env
}
});
// Script will see both process.env AND CUSTOM_VAR
```
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsrun',
version: '1.5.0',
version: '1.6.2',
description: 'run typescript programs efficiently'
}

View File

@@ -5,6 +5,64 @@ export interface IRunOptions {
cwd?: string;
}
export interface ISpawnOptions {
/** Working directory for the child process */
cwd?: string;
/** Environment variables (merged with parent's env) */
env?: Record<string, string>;
/** Additional CLI arguments to pass to the script */
args?: string[];
/**
* Stdio configuration
* - 'pipe': Create pipes for stdin/stdout/stderr (default)
* - 'inherit': Use parent's stdio
* - Array: Custom configuration per stream
*/
stdio?: 'pipe' | 'inherit' | ['pipe' | 'inherit' | 'ignore', 'pipe' | 'inherit' | 'ignore', 'pipe' | 'inherit' | 'ignore'];
/**
* Optional timeout in milliseconds
* If provided, process is automatically killed after timeout
*/
timeout?: number;
/**
* AbortSignal for cancellation support
* Allows external cancellation of the process
*/
signal?: AbortSignal;
}
export interface ITsrunChildProcess {
/** Direct access to Node's ChildProcess object */
childProcess: plugins.ChildProcess;
/** Readable stream for stdout (null if stdio is 'inherit') */
stdout: plugins.Readable | null;
/** Readable stream for stderr (null if stdio is 'inherit') */
stderr: plugins.Readable | null;
/** Promise that resolves with the exit code when process ends */
exitCode: Promise<number>;
/**
* Send signal to process
* Returns true if signal was sent successfully
*/
kill(signal?: NodeJS.Signals): boolean;
/**
* Gracefully terminate the process
* Tries SIGTERM first, waits 5s, then SIGKILL if still running
* Returns a promise that resolves when process is terminated
*/
terminate(): Promise<void>;
}
export const runPath = async (pathArg: string, fromFileUrl?: string, options?: IRunOptions) => {
pathArg = fromFileUrl
? plugins.path.join(plugins.path.dirname(plugins.url.fileURLToPath(fromFileUrl)), pathArg)
@@ -40,13 +98,13 @@ export const runCli = async (pathArg?: string, options?: IRunOptions) => {
const runInChildProcess = async (pathArg: string | undefined, cwd: string): Promise<void> => {
const { spawn } = await import('child_process');
// Resolve cli.child.js relative to this file
const cliChildPath = plugins.path.join(__dirname, '../cli.child.js');
// Resolve cli.js relative to this file
const cliPath = plugins.path.join(__dirname, '../cli.js');
// Build args: [Node flags, entry point, script path, script args]
const args = [
...process.execArgv, // Preserve --inspect, etc.
cliChildPath,
cliPath,
...process.argv.slice(2) // Original CLI args (not spliced)
];
@@ -95,3 +153,123 @@ const runInChildProcess = async (pathArg: string | undefined, cwd: string): Prom
});
});
};
export const spawnPath = (
filePath: string,
fromFileUrl?: string | URL,
options?: ISpawnOptions
): ITsrunChildProcess => {
// 1. Resolve path (similar to runPath)
const resolvedPath = fromFileUrl
? plugins.path.join(
plugins.path.dirname(
plugins.url.fileURLToPath(
typeof fromFileUrl === 'string' ? fromFileUrl : fromFileUrl.href
)
),
filePath
)
: filePath;
// 2. Build spawn args
const cliPath = plugins.path.join(__dirname, '../cli.js');
const args = [
...process.execArgv,
cliPath,
resolvedPath,
...(options?.args || [])
];
// 3. Build spawn options
const spawnOptions = {
cwd: options?.cwd || process.cwd(),
env: { ...process.env, ...options?.env },
stdio: options?.stdio || 'pipe',
shell: false,
windowsHide: false
};
// 4. Spawn child process
const child = plugins.spawn(process.execPath, args, spawnOptions);
// 5. Set up timeout if provided
let timeoutId: NodeJS.Timeout | undefined;
let timeoutTriggered = false;
if (options?.timeout) {
timeoutId = setTimeout(() => {
timeoutTriggered = true;
child.kill('SIGTERM');
}, options.timeout);
}
// 6. Set up AbortSignal if provided
let abortHandler: (() => void) | undefined;
if (options?.signal) {
abortHandler = () => {
child.kill('SIGTERM');
};
options.signal.addEventListener('abort', abortHandler);
}
// 7. Create exitCode promise
const exitCodePromise = new Promise<number>((resolve, reject) => {
child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
if (timeoutId) clearTimeout(timeoutId);
if (abortHandler && options?.signal) {
options.signal.removeEventListener('abort', abortHandler);
}
if (timeoutTriggered) {
reject(new Error(`Process killed: timeout of ${options?.timeout}ms exceeded`));
} else if (options?.signal?.aborted) {
reject(new Error('Process killed: aborted by signal'));
} else if (signal) {
reject(new Error(`Process killed with signal ${signal}`));
} else {
resolve(code || 0);
}
});
child.on('error', (err: Error) => {
if (timeoutId) clearTimeout(timeoutId);
if (abortHandler && options?.signal) {
options.signal.removeEventListener('abort', abortHandler);
}
reject(err);
});
});
// 8. Implement terminate() method
const terminate = async (): Promise<void> => {
return new Promise((resolve) => {
if (child.killed) {
resolve();
return;
}
child.kill('SIGTERM');
const killTimeout = setTimeout(() => {
if (!child.killed) {
child.kill('SIGKILL');
}
}, 5000);
child.on('close', () => {
clearTimeout(killTimeout);
resolve();
});
});
};
// 9. Return ITsrunChildProcess object
return {
childProcess: child,
stdout: child.stdout,
stderr: child.stderr,
exitCode: exitCodePromise,
kill: (signal?: NodeJS.Signals) => child.kill(signal),
terminate
};
};

View File

@@ -1,8 +1,12 @@
// node native
import * as path from 'path';
import * as url from 'url';
import { spawn } from 'child_process';
import type { ChildProcess } from 'child_process';
import type { Readable } from 'stream';
export { path, url };
export { path, url, spawn };
export type { ChildProcess, Readable };
// @pushrocks scope
import * as smartfile from '@push.rocks/smartfile';