{"id":37600445,"url":"https://github.com/thaitype/shell","last_synced_at":"2026-01-16T10:02:39.991Z","repository":{"id":323022651,"uuid":"1091832389","full_name":"thaitype/shell","owner":"thaitype","description":"A lightweight, type-safe wrapper around execa for running shell commands with flexible output modes and better developer experience.","archived":false,"fork":false,"pushed_at":"2025-11-07T16:35:19.000Z","size":146,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-07T17:29:49.332Z","etag":null,"topics":["cli","command-line","execa","nodejs","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thaitype.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-07T15:30:47.000Z","updated_at":"2025-11-07T17:25:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/thaitype/shell","commit_stats":null,"previous_names":["thaitype/shell"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/thaitype/shell","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thaitype%2Fshell","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thaitype%2Fshell/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thaitype%2Fshell/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thaitype%2Fshell/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thaitype","download_url":"https://codeload.github.com/thaitype/shell/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thaitype%2Fshell/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28478049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","command-line","execa","nodejs","typescript"],"created_at":"2026-01-16T10:02:39.889Z","updated_at":"2026-01-16T10:02:39.961Z","avatar_url":"https://github.com/thaitype.png","language":"TypeScript","readme":"# @thaitype/shell\n\n[![CI](https://github.com/thaitype/shell/actions/workflows/main.yml/badge.svg)](https://github.com/thaitype/shell/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/thaitype/shell/graph/badge.svg?token=TUE7DJ6NKX)](https://codecov.io/gh/thaitype/shell) [![NPM Version](https://img.shields.io/npm/v/@thaitype/shell)](https://www.npmjs.com/package/@thaitype/shell) [![npm downloads](https://img.shields.io/npm/dt/@thaitype/shell)](https://www.npmjs.com/@thaitype/shell)\n\nA lightweight, type-safe wrapper around [execa](https://github.com/sindresorhus/execa) for running shell commands with an elegant fluent API and flexible output modes.\n\n## Why @thaitype/shell?\n\nRunning shell commands in Node.js often involves repetitive boilerplate and dealing with low-level stdio configuration. `@thaitype/shell` provides a modern, fluent API that makes shell scripting in TypeScript/JavaScript feel natural and enjoyable.\n\n**Modern Fluent API:**\n```typescript\nimport { createShell } from '@thaitype/shell';\n\nconst $ = createShell().asFluent();\n\n// Simple and elegant\nconst output = await $('echo hello world');\n\n// Chain operations\nconst lines = await $('ls -la').toLines();\n\n// Parse JSON with validation\nconst pkg = await $('cat package.json').parse(schema);\n\n// Handle errors gracefully\nconst result = await $('some-command').result();\nif (!result.success) {\n  console.error('Failed:', result.stderr);\n}\n```\n\n**Key Features:**\n\n- **Fluent API** - Elegant function call syntax with chainable methods\n- **Type-safe** - Full TypeScript support with automatic type inference\n- **Flexible output modes** - Capture, stream live, or both simultaneously\n- **Schema validation** - Built-in JSON parsing with Standard Schema (Zod, Valibot, etc.)\n- **Smart error handling** - Choose between throwing or non-throwing APIs\n- **Lazy execution** - Commands don't run until consumed\n- **Memoization** - Multiple consumptions share the same execution\n- **Dry-run mode** - Test scripts without executing commands\n- **Verbose logging** - Debug with automatic command logging\n\n## Installation\n\n```bash\nnpm install @thaitype/shell\n# or\npnpm add @thaitype/shell\n# or\nyarn add @thaitype/shell\n# or\nbun add @thaitype/shell\n```\n\n## Compatibility\n\nThis package is **ESM only** and requires:\n\n- **Node.js** \u003e= 20\n- **ESM** module system (not CommonJS)\n\nFollowing the same philosophy as [execa](https://github.com/sindresorhus/execa), this package is pure ESM. Please [read this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) if you need help migrating from CommonJS.\n\n## Quick Start\n\n### Basic Usage - Fluent API\n\n```typescript\nimport { createShell } from '@thaitype/shell';\n\n// Create a fluent shell function\nconst $ = createShell().asFluent();\n\n// Execute and get output\nconst output = await $('echo \"Hello World\"');\nconsole.log(output); // \"Hello World\"\n\n// Use function call syntax\nconst result = await $('ls -la');\nconsole.log(result);\n\n// Array syntax for precise arguments\nconst files = await $(['echo', 'file with spaces.txt']);\n```\n\n## Fluent API Guide\n\nThe fluent API provides an elegant, modern way to run shell commands with powerful features like lazy execution, memoization, and chainable operations.\n\n### Command Execution\n\nExecute commands using string or array syntax:\n\n```typescript\nconst $ = createShell().asFluent();\n\n// String command\nconst output = await $('echo hello');\n\n// Array command (recommended for arguments with spaces)\nconst result = await $(['echo', 'hello world']);\n\n// With options\nconst output = await $('npm run build', { outputMode: 'all' });\n```\n\n### Non-Throwable Execution with `.result()`\n\nHandle failures gracefully without try-catch:\n\n```typescript\nconst $ = createShell().asFluent();\n\nconst result = await $('some-command-that-might-fail').result();\n\nif (!result.success) {\n  console.error(`Command failed with exit code ${result.exitCode}`);\n  console.error(`Error: ${result.stderr}`);\n} else {\n  console.log(`Output: ${result.stdout}`);\n}\n```\n\n### Working with Lines - `.toLines()`\n\nSplit output into an array of lines:\n\n```typescript\nconst $ = createShell().asFluent();\n\n// Get directory listing as lines\nconst files = await $('ls -1 /tmp').toLines();\nfiles.forEach(file =\u003e console.log(`File: ${file}`));\n\n// Read and process file lines\nconst lines = await $('cat /etc/hosts').toLines();\nconst nonEmpty = lines.filter(line =\u003e line.trim() !== '');\n```\n\n### JSON Parsing with Validation - `.parse()`\n\nParse and validate JSON output with Standard Schema:\n\n```typescript\nimport { createShell } from '@thaitype/shell';\nimport { z } from 'zod';\n\nconst $ = createShell().asFluent();\n\n// Define schema\nconst packageSchema = z.object({\n  name: z.string(),\n  version: z.string(),\n  dependencies: z.record(z.string()).optional(),\n});\n\n// Parse and validate (throws on error)\nconst pkg = await $('cat package.json').parse(packageSchema);\nconsole.log(`Package: ${pkg.name}@${pkg.version}`);\n\n// API response example\nconst userSchema = z.object({\n  id: z.number(),\n  username: z.string(),\n  email: z.string().email(),\n});\n\nconst user = await $('curl -s https://api.example.com/user/1').parse(userSchema);\nconsole.log(`User: ${user.username} (${user.email})`);\n```\n\n### Non-Throwable Parsing - `.safeParse()`\n\nParse JSON without throwing exceptions:\n\n```typescript\nconst $ = createShell().asFluent();\n\nconst schema = z.object({\n  status: z.string(),\n  data: z.array(z.any()),\n});\n\nconst result = await $('curl -s https://api.example.com/data').safeParse(schema);\n\nif (result.success) {\n  console.log('Data:', result.data.data);\n} else {\n  console.error('Validation failed:', result.error);\n  // Handle error gracefully - could be:\n  // - Command failed\n  // - Invalid JSON\n  // - Schema validation failed\n}\n```\n\n### Lazy Execution and Memoization\n\nCommands don't execute until consumed, and multiple consumptions share execution:\n\n```typescript\nconst $ = createShell().asFluent();\n\n// Create handle - command hasn't run yet\nconst handle = $('echo expensive operation');\n\n// First consumption - executes command\nconst output1 = await handle;\n\n// Second consumption - reuses first execution\nconst output2 = await handle;\n\n// Works across different methods too\nconst result = await handle.result(); // Still same execution!\n\n// All three share the same memoized result\nconsole.log(output1 === output2); // true\n```\n\n### Output Modes\n\nControl how command output is handled:\n\n```typescript\nconst shell = createShell({ outputMode: 'capture' }); // Default\nconst $ = shell.asFluent();\n\n// Capture mode: Output is captured for programmatic use\nconst output = await $('npm run build');\nconsole.log(output);\n\n// All mode: Both capture AND stream to console\nconst shell2 = createShell({ outputMode: 'all' });\nconst $2 = shell2.asFluent();\n\nconst result = await $2('npm test').result();\n// Test output appears in real-time on console\n// AND is available in result.stdout\n\n// Override mode per command\nconst output2 = await $(['npm', 'install'], { outputMode: 'all' });\n```\n\n**Important:** Fluent API does not support `'live'` mode (streaming only, no capture) because fluent operations require stdout for chaining, parsing, and memoization. Use the traditional Shell API if you need live-only mode.\n\n## Example Use Cases\n\n### 1. Build Script with Progress\n\n```typescript\nimport { createShell } from '@thaitype/shell';\n\nconst shell = createShell({\n  outputMode: 'all',  // Show output + capture\n  verbose: true        // Log commands\n});\n\nconst $ = shell.asFluent();\n\nconsole.log('🏗️  Building project...');\n\n// Clean\nawait $('rm -rf dist');\n\n// Build\nconst buildResult = await $('npm run build').result();\nif (!buildResult.success) {\n  console.error('❌ Build failed!');\n  process.exit(1);\n}\n\n// Test\nawait $('npm test');\n\nconsole.log('✅ Build complete!');\n```\n\n### 2. Git Workflow Helper\n\n```typescript\nimport { createShell } from '@thaitype/shell';\n\nconst $ = createShell().asFluent();\n\n// Get current branch\nconst branch = await $('git rev-parse --abbrev-ref HEAD');\nconsole.log(`Current branch: ${branch}`);\n\n// Check for uncommitted changes\nconst status = await $('git status --porcelain').result();\nif (status.stdout.trim() !== '') {\n  console.log('⚠️  You have uncommitted changes');\n}\n\n// Get recent commits as lines\nconst commits = await $('git log --oneline -5').toLines();\nconsole.log('Recent commits:');\ncommits.forEach(commit =\u003e console.log(`  ${commit}`));\n```\n\n### 3. System Information Gathering\n\n```typescript\nimport { createShell } from '@thaitype/shell';\nimport { z } from 'zod';\n\nconst $ = createShell().asFluent();\n\n// Parse JSON output\nconst pkgSchema = z.object({\n  name: z.string(),\n  version: z.string(),\n  engines: z.object({\n    node: z.string(),\n  }).optional(),\n});\n\nconst pkg = await $('cat package.json').parse(pkgSchema);\n\n// Get Node version\nconst nodeVersion = await $('node --version');\n\n// Get system info as lines\nconst osInfo = await $('uname -a').toLines();\n\nconsole.log(`Project: ${pkg.name}@${pkg.version}`);\nconsole.log(`Node: ${nodeVersion}`);\nconsole.log(`OS: ${osInfo[0]}`);\n```\n\n### 4. Safe Command Execution\n\n```typescript\nimport { createShell } from '@thaitype/shell';\n\nconst $ = createShell().asFluent();\n\nasync function deployApp() {\n  // Test connection\n  const ping = await $('curl -s https://api.example.com/health').result();\n  if (!ping.success) {\n    console.error('❌ API is not reachable');\n    return false;\n  }\n\n  // Run tests\n  const tests = await $('npm test').result();\n  if (!tests.success) {\n    console.error('❌ Tests failed');\n    return false;\n  }\n\n  // Deploy\n  const deploy = await $('npm run deploy').result();\n  if (!deploy.success) {\n    console.error('❌ Deployment failed');\n    console.error(deploy.stderr);\n    return false;\n  }\n\n  console.log('✅ Deployment successful!');\n  return true;\n}\n\nawait deployApp();\n```\n\n### 5. Dry-Run Mode for Testing\n\nTest your automation scripts without actually executing commands:\n\n```typescript\nimport { createShell } from '@thaitype/shell';\n\nconst shell = createShell({\n  dryRun: true,   // Commands logged but not executed\n  verbose: true\n});\n\nconst $ = shell.asFluent();\n\n// These commands will be logged but not executed\nawait $('rm -rf node_modules');\n// Output: $ rm -rf node_modules\n// (nothing is actually deleted)\n\nawait $('git push origin main');\n// Output: $ git push origin main\n// (nothing is actually pushed)\n\nconsole.log('✅ Dry run complete - no actual changes made!');\n```\n\n## Traditional Shell API\n\nFor cases where you need more control or don't want the fluent API, use the traditional methods:\n\n### `shell.run()` - Throws on Error\n\n```typescript\nimport { createShell } from '@thaitype/shell';\n\nconst shell = createShell();\n\ntry {\n  const result = await shell.run('npm test');\n  console.log('Tests passed!', result.stdout);\n} catch (error) {\n  console.error('Tests failed:', error.message);\n}\n```\n\n### `shell.safeRun()` - Never Throws\n\n```typescript\nconst shell = createShell();\n\nconst result = await shell.safeRun('npm test');\n\nif (!result.success) {\n  console.error('Command failed with exit code:', result.exitCode);\n  console.error('Error output:', result.stderr);\n} else {\n  console.log('Success:', result.stdout);\n}\n```\n\n### Output Modes\n\n```typescript\nconst shell = createShell();\n\n// Capture mode (default): Capture output for programmatic use\nconst result1 = await shell.run('ls -la', { outputMode: 'capture' });\nconsole.log('Files:', result1.stdout);\n\n// Live mode: Stream output to console in real-time (no capture)\nawait shell.run('npm test', { outputMode: 'live' });\n// Output appears in real-time, stdout/stderr will be null\n\n// All mode: Both capture AND stream simultaneously\nconst result2 = await shell.run('npm run build', { outputMode: 'all' });\n// Output streams to console AND is captured\nconsole.log('Build output was:', result2.stdout);\n```\n\n### Schema Validation\n\n```typescript\nimport { createShell } from '@thaitype/shell';\nimport { z } from 'zod';\n\nconst shell = createShell();\n\nconst packageSchema = z.object({\n  name: z.string(),\n  version: z.string(),\n});\n\n// Throws on error\nconst pkg = await shell.runParse('cat package.json', packageSchema);\nconsole.log(`${pkg.name}@${pkg.version}`);\n\n// Never throws\nconst result = await shell.safeRunParse('cat package.json', packageSchema);\nif (result.success) {\n  console.log(`${result.data.name}@${result.data.version}`);\n} else {\n  console.error('Validation failed:', result.error);\n}\n```\n\n## API Reference\n\n### Factory Function\n\n#### `createShell(options?)`\n\nCreates a new Shell instance with better type inference (recommended).\n\n```typescript\nconst shell = createShell({\n  outputMode: 'capture',  // 'capture' | 'live' | 'all'\n  dryRun: false,\n  verbose: false,\n  throwMode: 'simple',    // 'simple' | 'raw'\n  logger: {\n    debug: (msg, ctx) =\u003e console.debug(msg),\n    warn: (msg, ctx) =\u003e console.warn(msg),\n  },\n  execaOptions: {\n    env: { NODE_ENV: 'production' },\n    timeout: 30000,\n    cwd: '/app',\n  },\n});\n```\n\n### Fluent API\n\n#### `shell.asFluent()`\n\nReturns a fluent shell function that supports function calls.\n\n```typescript\nconst $ = shell.asFluent();\n\n// Function calls\nawait $('command');\nawait $(['command', 'arg']);\nawait $(command, options);\n```\n\n**Returns:** `DollarFunction` that creates `LazyCommandHandle` instances.\n\n**Throws:** Error if shell has `outputMode: 'live'` (fluent API requires output capture).\n\n#### `LazyCommandHandle`\n\nHandle returned by fluent API with lazy execution and memoization.\n\n**Direct await - Throwable:**\n```typescript\nconst output: string = await $('command');\n```\n\n**Methods:**\n\n- **`.result()`** - Non-throwable execution\n  ```typescript\n  const result = await $('command').result();\n  // result: { success: boolean, stdout: string, stderr: string, exitCode: number | undefined }\n  ```\n\n- **`.toLines()`** - Split output into lines (throws on error)\n  ```typescript\n  const lines: string[] = await $('command').toLines();\n  ```\n\n- **`.parse\u003cT\u003e(schema)`** - Parse and validate JSON (throws on error)\n  ```typescript\n  const data: T = await $('command').parse(schema);\n  ```\n\n- **`.safeParse\u003cT\u003e(schema)`** - Parse and validate JSON (never throws)\n  ```typescript\n  const result = await $('command').safeParse(schema);\n  // result: { success: true, data: T } | { success: false, error: Error[] }\n  ```\n\n### Traditional Shell Methods\n\n#### `shell.run(command, options?)`\n\nExecute command that throws on error.\n\n**Returns:** `Promise\u003cStrictResult\u003e`\n```typescript\n{ stdout: string | null, stderr: string | null }\n```\n\n#### `shell.safeRun(command, options?)`\n\nExecute command that never throws.\n\n**Returns:** `Promise\u003cSafeResult\u003e`\n```typescript\n{\n  stdout: string | null,\n  stderr: string | null,\n  exitCode: number | undefined,\n  success: boolean\n}\n```\n\n#### `shell.runParse(command, schema, options?)`\n\nExecute, parse, and validate JSON (throws on error).\n\n**Returns:** `Promise\u003cT\u003e` (inferred from schema)\n\n#### `shell.safeRunParse(command, schema, options?)`\n\nExecute, parse, and validate JSON (never throws).\n\n**Returns:** `Promise\u003cValidationResult\u003cT\u003e\u003e`\n```typescript\n{ success: true, data: T } | { success: false, error: Error[] }\n```\n\n### Options\n\n#### `ShellOptions`\n\n```typescript\ninterface ShellOptions {\n  outputMode?: 'capture' | 'live' | 'all';  // default: 'capture'\n  dryRun?: boolean;                          // default: false\n  verbose?: boolean;                         // default: false\n  throwMode?: 'simple' | 'raw';              // default: 'simple'\n  logger?: ShellLogger;\n  execaOptions?: ExecaOptions;               // Merged with command options\n}\n```\n\n#### `RunOptions`\n\n```typescript\ninterface RunOptions extends ExecaOptions {\n  outputMode?: 'capture' | 'live' | 'all';\n  verbose?: boolean;\n  dryRun?: boolean;\n}\n```\n\nAll options from [execa](https://github.com/sindresorhus/execa#options) are supported. Shell-level and command-level options are deep merged.\n\n## Advanced Usage\n\n### Custom Logger Integration\n\n```typescript\nimport { createShell } from '@thaitype/shell';\nimport winston from 'winston';\n\nconst logger = winston.createLogger({\n  level: 'info',\n  format: winston.format.json(),\n  transports: [new winston.transports.Console()]\n});\n\nconst shell = createShell({\n  verbose: true,\n  logger: {\n    debug: (message, context) =\u003e {\n      logger.debug(message, {\n        command: context.command,\n        cwd: context.execaOptions.cwd\n      });\n    },\n    warn: (message, context) =\u003e {\n      logger.warn(message, { command: context.command });\n    }\n  }\n});\n\nconst $ = shell.asFluent();\nawait $('npm install');\n// Commands logged via Winston with context\n```\n\n### Deep Merge Options\n\n```typescript\nconst shell = createShell({\n  execaOptions: {\n    env: { API_KEY: 'default', NODE_ENV: 'dev' },\n    timeout: 5000,\n  }\n});\n\nconst $ = shell.asFluent();\n\n// Options are deep merged\nconst result = await $('node script.js', {\n  env: { NODE_ENV: 'prod', EXTRA: 'value' },\n  timeout: 30000,\n});\n\n// Resulting env: { API_KEY: 'default', NODE_ENV: 'prod', EXTRA: 'value' }\n// Resulting timeout: 30000\n```\n\n## License\n\nMIT - see [LICENSE](LICENSE) file for details.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n### Development Setup\n\n1. Clone the repository:\n   ```bash\n   git clone https://github.com/thaitype/shell.git\n   cd shell\n   ```\n\n2. Install dependencies:\n   ```bash\n   pnpm install\n   ```\n\n3. Run tests:\n   ```bash\n   pnpm test\n   ```\n\n4. Build:\n   ```bash\n   pnpm build\n   ```\n\n### Guidelines\n\n- Ensure all tests pass before submitting PR\n- Add tests for new features\n- Follow the existing code style\n- Update documentation as needed\n\n## Author\n\nThada Wangthammang\n\n## Links\n\n- [GitHub Repository](https://github.com/thaitype/shell)\n- [NPM Package](https://www.npmjs.com/package/@thaitype/shell)\n- [Issue Tracker](https://github.com/thaitype/shell/issues)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthaitype%2Fshell","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthaitype%2Fshell","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthaitype%2Fshell/lists"}