@zokugun/exec-lite 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +275 -2
  3. package/lib/cjs/combineSignals.cjs +19 -0
  4. package/lib/cjs/combineSignals.d.cts +1 -0
  5. package/lib/cjs/exec-process.cjs +221 -0
  6. package/lib/cjs/exec-process.d.cts +37 -0
  7. package/lib/cjs/exec.cjs +9 -0
  8. package/lib/cjs/exec.d.cts +2 -0
  9. package/lib/cjs/index.cjs +7 -0
  10. package/lib/cjs/index.d.cts +3 -0
  11. package/lib/cjs/types.cjs +2 -0
  12. package/lib/cjs/types.d.cts +56 -0
  13. package/lib/cjs/utils/command/detectShebang.cjs +36 -0
  14. package/lib/cjs/utils/command/detectShebang.d.cts +1 -0
  15. package/lib/cjs/utils/command/normalizeCommand.cjs +59 -0
  16. package/lib/cjs/utils/command/normalizeCommand.d.cts +6 -0
  17. package/lib/cjs/utils/command/resolveCommand.cjs +43 -0
  18. package/lib/cjs/utils/command/resolveCommand.d.cts +2 -0
  19. package/lib/cjs/utils/env/addNodeBinToPath.cjs +24 -0
  20. package/lib/cjs/utils/env/addNodeBinToPath.d.cts +2 -0
  21. package/lib/cjs/utils/env/computeEnv.cjs +17 -0
  22. package/lib/cjs/utils/env/computeEnv.d.cts +2 -0
  23. package/lib/cjs/utils/env/getPathFromEnv.cjs +22 -0
  24. package/lib/cjs/utils/env/getPathFromEnv.d.cts +2 -0
  25. package/lib/cjs/utils/env/types.cjs +2 -0
  26. package/lib/cjs/utils/env/types.d.cts +5 -0
  27. package/lib/cjs/utils/normalizeOptions.cjs +31 -0
  28. package/lib/cjs/utils/normalizeOptions.d.cts +2 -0
  29. package/lib/cjs/utils/stream/combineStreams.cjs +25 -0
  30. package/lib/cjs/utils/stream/combineStreams.d.cts +2 -0
  31. package/lib/cjs/utils/stream/readStream.cjs +16 -0
  32. package/lib/cjs/utils/stream/readStream.d.cts +5 -0
  33. package/lib/esm/combineSignals.d.mts +1 -0
  34. package/lib/esm/combineSignals.mjs +16 -0
  35. package/lib/esm/exec-process.d.mts +37 -0
  36. package/lib/esm/exec-process.mjs +214 -0
  37. package/lib/esm/exec.d.mts +2 -0
  38. package/lib/esm/exec.mjs +6 -0
  39. package/lib/esm/index.d.mts +3 -0
  40. package/lib/esm/index.mjs +2 -0
  41. package/lib/esm/types.d.mts +56 -0
  42. package/lib/esm/types.mjs +1 -0
  43. package/lib/esm/utils/command/detectShebang.d.mts +1 -0
  44. package/lib/esm/utils/command/detectShebang.mjs +30 -0
  45. package/lib/esm/utils/command/normalizeCommand.d.mts +6 -0
  46. package/lib/esm/utils/command/normalizeCommand.mjs +53 -0
  47. package/lib/esm/utils/command/resolveCommand.d.mts +2 -0
  48. package/lib/esm/utils/command/resolveCommand.mjs +37 -0
  49. package/lib/esm/utils/env/addNodeBinToPath.d.mts +2 -0
  50. package/lib/esm/utils/env/addNodeBinToPath.mjs +18 -0
  51. package/lib/esm/utils/env/computeEnv.d.mts +2 -0
  52. package/lib/esm/utils/env/computeEnv.mjs +14 -0
  53. package/lib/esm/utils/env/getPathFromEnv.d.mts +2 -0
  54. package/lib/esm/utils/env/getPathFromEnv.mjs +19 -0
  55. package/lib/esm/utils/env/types.d.mts +5 -0
  56. package/lib/esm/utils/env/types.mjs +1 -0
  57. package/lib/esm/utils/normalizeOptions.d.mts +2 -0
  58. package/lib/esm/utils/normalizeOptions.mjs +25 -0
  59. package/lib/esm/utils/stream/combineStreams.d.mts +2 -0
  60. package/lib/esm/utils/stream/combineStreams.mjs +22 -0
  61. package/lib/esm/utils/stream/readStream.d.mts +5 -0
  62. package/lib/esm/utils/stream/readStream.mjs +13 -0
  63. package/package.json +123 -5
@@ -0,0 +1,56 @@
1
+ import type { ChildProcess, SpawnOptions as ProcessSpawnOptions } from 'node:child_process';
2
+ import type { Result } from '@zokugun/xtry';
3
+ export type ExecFailure = {
4
+ all?: string;
5
+ canceled: boolean;
6
+ command: string;
7
+ durationMs: number;
8
+ error?: Error;
9
+ exitCode?: number;
10
+ pipedFrom?: ExecResult;
11
+ signalCode?: string;
12
+ stderr?: string;
13
+ stdout?: string;
14
+ };
15
+ export type ExecResult = Result<ExecSuccess, ExecFailure>;
16
+ export type ExecSuccess = {
17
+ all: string;
18
+ command: string;
19
+ durationMs: number;
20
+ error?: undefined;
21
+ exitCode: number;
22
+ pipedFrom?: ExecResult;
23
+ stderr: string;
24
+ stdout: string;
25
+ };
26
+ export type KillSignal = Parameters<ChildProcess['kill']>[0];
27
+ export type Options = Omit<ProcessSpawnOptions, 'cwd' | 'env' | 'stdio'> & Readonly<Partial<{
28
+ cwd?: string;
29
+ env: Readonly<Partial<Record<string, string>>>;
30
+ localDir: string;
31
+ preferLocal: boolean;
32
+ signal: AbortSignal;
33
+ stderr: StdioOption;
34
+ stdin: ExecResult | StdinOption | Subprocess;
35
+ stdio: readonly [StdinOption, ...readonly StdioOption[]] | SpawnOptions['stdio'];
36
+ stdout: StdioOption;
37
+ }>>;
38
+ export type SpawnOptions = {
39
+ cwd: string;
40
+ } & Omit<ProcessSpawnOptions, 'cwd'>;
41
+ export type StdinOption = StdioOption | {
42
+ readonly string?: string;
43
+ };
44
+ export type StdioOption = Readonly<Exclude<SpawnOptions['stdio'], undefined>[number]>;
45
+ export type Subprocess = AsyncIterable<string> & PromiseLike<ExecResult> & {
46
+ get aborted(): boolean;
47
+ get error(): Error | undefined;
48
+ get exitCode(): number | undefined;
49
+ get failed(): boolean;
50
+ kill: (signal?: KillSignal) => boolean;
51
+ get killed(): boolean;
52
+ get pid(): number | undefined;
53
+ pipe: (command: string, args?: readonly string[], options?: Partial<Options>) => Subprocess;
54
+ get process(): ChildProcess | undefined;
55
+ get signalCode(): string | undefined;
56
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function detectShebang(file: string): string | null;
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ const SHEBANG_REGEX = /^#!\s*(.+)/;
4
+ const SIZE = 150;
5
+ export function detectShebang(file) {
6
+ const buffer = Buffer.alloc(SIZE);
7
+ let fd = null;
8
+ try {
9
+ fd = fs.openSync(file, 'r');
10
+ fs.readSync(fd, buffer, 0, SIZE, 0);
11
+ }
12
+ catch {
13
+ // do nothing, we'll just assume it's not a shebang
14
+ }
15
+ finally {
16
+ if (fd !== null) {
17
+ fs.closeSync(fd);
18
+ }
19
+ }
20
+ const match = SHEBANG_REGEX.exec(buffer.toString());
21
+ if (match === null) {
22
+ return null;
23
+ }
24
+ const line = match[1].trim();
25
+ const separatorIndex = line.indexOf(' ');
26
+ const path = separatorIndex !== -1 ? line.slice(0, separatorIndex) : line;
27
+ const argument = separatorIndex !== -1 ? line.slice(separatorIndex + 1) : '';
28
+ const binary = basename(path);
29
+ return binary === 'env' ? argument || null : binary;
30
+ }
@@ -0,0 +1,6 @@
1
+ import type { SpawnOptions } from '../../types.mjs';
2
+ export declare function normalizeCommand(command: string, args: readonly string[], options: SpawnOptions): {
3
+ args: readonly string[];
4
+ command: string;
5
+ options: SpawnOptions;
6
+ };
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import { detectShebang } from './detectShebang.mjs';
3
+ import { resolveCommand } from './resolveCommand.mjs';
4
+ const IS_BATCH_FILE_REGEX = /\.(?:bat|cmd)$/i;
5
+ const IS_EXECUTABLE_REGEX = /\.(?:com|exe)$/i;
6
+ const IS_WINDOWS = process.platform === 'win32';
7
+ const META_CHARS_REGEX = /([ !"%&()*,;<>?[\]^`|])/g;
8
+ export function normalizeCommand(command, args, options) {
9
+ if (['node', 'node.exe'].includes(command.toLowerCase())) {
10
+ command = process.execPath;
11
+ args = [...process.execArgv.filter((flag) => !flag.startsWith('--inspect')), ...args];
12
+ }
13
+ if (options.shell === true || !IS_WINDOWS) {
14
+ return {
15
+ command,
16
+ args,
17
+ options,
18
+ };
19
+ }
20
+ let file = resolveCommand(command, options);
21
+ if (file !== null) {
22
+ const shebang = detectShebang(file);
23
+ if (shebang !== null) {
24
+ args = [file, ...args];
25
+ command = shebang;
26
+ file = resolveCommand(command, options);
27
+ }
28
+ }
29
+ if (file !== null && IS_EXECUTABLE_REGEX.test(file)) {
30
+ return {
31
+ command,
32
+ args,
33
+ options,
34
+ };
35
+ }
36
+ const needsDoubleEscape = file !== null && IS_BATCH_FILE_REGEX.test(file);
37
+ const normalizedCommand = path.normalize(command).replace(META_CHARS_REGEX, '^$1');
38
+ const normalizedArgs = [];
39
+ for (let arg of args) {
40
+ arg = arg.replace(/(?=(\\+?))\1(?:"|$)/g, '$1$1\\"');
41
+ arg = `"${arg}"`;
42
+ arg = arg.replace(META_CHARS_REGEX, '^$1');
43
+ if (needsDoubleEscape) {
44
+ arg = arg.replace(META_CHARS_REGEX, '^$1');
45
+ }
46
+ normalizedArgs.push(arg);
47
+ }
48
+ return {
49
+ command: options.env?.comspec ?? 'cmd.exe',
50
+ args: ['/d', '/s', '/c', `"${[normalizedCommand, ...normalizedArgs].join(' ')}"`],
51
+ options: { ...options, windowsVerbatimArguments: true },
52
+ };
53
+ }
@@ -0,0 +1,2 @@
1
+ import type { SpawnOptions } from '../../types.mjs';
2
+ export declare function resolveCommand(command: string, options: SpawnOptions): string | null;
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getPathFromEnv } from '../env/getPathFromEnv.mjs';
4
+ const CACHE = Object.create(null);
5
+ const DEFAULT_PATH_EXT = ['.EXE', '.CMD', '.BAT', '.COM'];
6
+ export function resolveCommand(command, options) {
7
+ const cwd = options.cwd ?? process.cwd();
8
+ const env = options.env ?? process.env;
9
+ const pathEnv = getPathFromEnv(env).value;
10
+ const key = `${command}%%%%${pathEnv}`;
11
+ if (CACHE[key] !== undefined) {
12
+ return CACHE[key];
13
+ }
14
+ const parts = command.includes('/') || command.includes('\\') ? [''] : [cwd, ...pathEnv.split(path.delimiter)];
15
+ let pathExt = env.PATHEXT ? env.PATHEXT.split(path.delimiter) : DEFAULT_PATH_EXT;
16
+ if (command.includes('.') && pathExt[0] !== '') {
17
+ pathExt = ['', ...pathExt];
18
+ }
19
+ for (const part of parts) {
20
+ const unquoted = part.startsWith('"') && part.endsWith('"') && part.length > 1 ? part.slice(1, -1) : part;
21
+ const dest = path.resolve(cwd, unquoted, command);
22
+ for (const ext of [...pathExt, '']) {
23
+ const destWithExt = dest + ext;
24
+ try {
25
+ if (fs.statSync(destWithExt).isFile()) {
26
+ CACHE[key] = destWithExt;
27
+ return destWithExt;
28
+ }
29
+ }
30
+ catch {
31
+ // do nothing, it didn't exist
32
+ }
33
+ }
34
+ }
35
+ CACHE[key] = null;
36
+ return null;
37
+ }
@@ -0,0 +1,2 @@
1
+ import type { EnvPathInfo } from './types.mjs';
2
+ export declare function addNodeBinToPath(cwd: string, { value, key }: EnvPathInfo): EnvPathInfo;
@@ -0,0 +1,18 @@
1
+ import path from 'node:path';
2
+ export function addNodeBinToPath(cwd, { value, key }) {
3
+ const parts = value.split(path.delimiter);
4
+ const nodeBinPaths = [];
5
+ let currentPath = cwd;
6
+ let lastPath;
7
+ do {
8
+ nodeBinPaths.push(path.resolve(currentPath, 'node_modules', '.bin'));
9
+ lastPath = currentPath;
10
+ currentPath = path.dirname(currentPath);
11
+ } while (currentPath !== lastPath);
12
+ nodeBinPaths.push(path.dirname(process.execPath));
13
+ const newValue = nodeBinPaths.concat(parts).join(path.delimiter);
14
+ return {
15
+ key,
16
+ value: newValue,
17
+ };
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { EnvLike } from './types.mjs';
2
+ export declare function computeEnv(env: Readonly<EnvLike> | undefined, preferLocal: boolean, localDir: string): Readonly<EnvLike>;
@@ -0,0 +1,14 @@
1
+ import { addNodeBinToPath } from './addNodeBinToPath.mjs';
2
+ import { getPathFromEnv } from './getPathFromEnv.mjs';
3
+ export function computeEnv(env, preferLocal, localDir) {
4
+ const computedEnv = {
5
+ ...process.env,
6
+ ...env,
7
+ };
8
+ if (!preferLocal) {
9
+ return computedEnv;
10
+ }
11
+ const pathEnvInfo = addNodeBinToPath(localDir, getPathFromEnv(computedEnv));
12
+ computedEnv[pathEnvInfo.key] = pathEnvInfo.value;
13
+ return computedEnv;
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { EnvLike, EnvPathInfo } from './types.mjs';
2
+ export declare function getPathFromEnv(env: EnvLike): EnvPathInfo;
@@ -0,0 +1,19 @@
1
+ export function getPathFromEnv(env) {
2
+ for (const key in env) {
3
+ if (key.toLowerCase() !== 'path') {
4
+ continue;
5
+ }
6
+ const value = env[key];
7
+ if (!value) {
8
+ return {
9
+ key: 'PATH',
10
+ value: '',
11
+ };
12
+ }
13
+ return { key, value };
14
+ }
15
+ return {
16
+ key: 'PATH',
17
+ value: '',
18
+ };
19
+ }
@@ -0,0 +1,5 @@
1
+ export type EnvLike = (typeof process)['env'];
2
+ export type EnvPathInfo = {
3
+ key: string;
4
+ value: string;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Options, SpawnOptions } from '../types.mjs';
2
+ export declare function normalizeOptions(options: Options): SpawnOptions;
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ import { computeEnv } from './env/computeEnv.mjs';
3
+ import { combineSignals } from '../combineSignals.mjs';
4
+ export function normalizeOptions(options) {
5
+ const { cwd, env, preferLocal, signal, stderr, stdin, stdio: stdioOption, stdout, ...restOptions } = options;
6
+ const spawnOptions = restOptions;
7
+ spawnOptions.cwd = path.resolve(cwd ?? process.cwd());
8
+ spawnOptions.env = computeEnv(env, preferLocal ?? false, options.localDir ?? spawnOptions.cwd);
9
+ const signals = [];
10
+ if (options.timeout !== undefined) {
11
+ signals.push(AbortSignal.timeout(options.timeout));
12
+ }
13
+ if (signal !== undefined) {
14
+ signals.push(signal);
15
+ }
16
+ if (signals.length > 0) {
17
+ spawnOptions.signal = combineSignals(signals);
18
+ }
19
+ const stdio = stdioOption ?? [undefined, stdout, stderr];
20
+ if (Array.isArray(stdio) && !(typeof stdin === 'string' || (typeof stdin === 'object' && stdin !== null && 'process' in stdin))) {
21
+ stdio[0] = 'pipe';
22
+ }
23
+ spawnOptions.stdio = stdio;
24
+ return spawnOptions;
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { Readable } from 'node:stream';
2
+ export declare function combineStreams(streams: Readable[]): Readable;
@@ -0,0 +1,22 @@
1
+ import { PassThrough } from 'node:stream';
2
+ import { pipeline } from 'node:stream/promises';
3
+ export function combineStreams(streams) {
4
+ let streamCount = streams.length;
5
+ const combined = new PassThrough();
6
+ if (streamCount === 0) {
7
+ combined.end();
8
+ return combined;
9
+ }
10
+ const maybeEmitEnd = () => {
11
+ streamCount -= 1;
12
+ if (streamCount === 0) {
13
+ combined.end();
14
+ }
15
+ };
16
+ for (const stream of streams) {
17
+ pipeline(stream, combined, { end: false })
18
+ .then(maybeEmitEnd)
19
+ .catch(maybeEmitEnd);
20
+ }
21
+ return combined;
22
+ }
@@ -0,0 +1,5 @@
1
+ import type { Readable } from 'node:stream';
2
+ export type StreamContext = {
3
+ all: string;
4
+ };
5
+ export declare function readStream(stream: Readable, context: StreamContext): Promise<string>;
@@ -0,0 +1,13 @@
1
+ export async function readStream(stream, context) {
2
+ let output = '';
3
+ try {
4
+ for await (const chunk of stream) {
5
+ output += String(chunk);
6
+ context.all += String(chunk);
7
+ }
8
+ }
9
+ catch {
10
+ // suppress errors, child process probably failed to spawn
11
+ }
12
+ return output;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,124 @@
1
1
  {
2
- "name": "@zokugun/exec-lite",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@zokugun/exec-lite",
3
+ "description": "A lightweight tool for running shell commands and reading their output easily.",
4
+ "version": "0.2.0",
5
+ "author": {
6
+ "name": "Baptiste Augrain",
7
+ "email": "daiyam@zokugun.org"
8
+ },
9
+ "license": "MIT",
10
+ "homepage": "https://github.com/zokugun/node-exec-lite",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/zokugun/node-exec-lite.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/zokugun/node-exec-lite/discussions/categories/issue-triage"
17
+ },
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "import": "./lib/esm/index.mjs",
22
+ "require": "./lib/cjs/index.cjs"
23
+ }
24
+ },
25
+ "main": "lib/cjs/index.cjs",
26
+ "module": "lib/esm/index.mjs",
27
+ "typesVersions": {
28
+ "*": {
29
+ ".": [
30
+ "./lib/cjs/index.d.cts"
31
+ ]
32
+ }
33
+ },
34
+ "scripts": {
35
+ "audit:fix": "npm audit fix --min-release-age=0",
36
+ "build": "npm run clean && npm run build:out",
37
+ "build:out": "tsc-leda generate",
38
+ "build:package": "tsc-leda update-package && npm run lint:package",
39
+ "ci:lint": "zizmor .",
40
+ "ci:lint:fix": "zizmor . --fix=all",
41
+ "clean": "rimraf .src .test lib",
42
+ "compile:src": "tsc -p src",
43
+ "compile:test": "tsc -p test",
44
+ "lint": "eslint",
45
+ "lint:all": "npm run lint:package && npm audit && npm run ci:lint",
46
+ "lint:fix": "eslint --fix",
47
+ "lint:package": "fixpack || true",
48
+ "prepack": "npm run build",
49
+ "prepare": "husky; npm run lint:package",
50
+ "test": "vitest run --reporter tree",
51
+ "test:coverage": "vitest run --reporter tree --coverage --coverage.reporter text",
52
+ "test:ui": "vitest --ui --coverage",
53
+ "test:watch": "vitest",
54
+ "update:artifacts": "artifact update",
55
+ "update:ci": "pinact run -min-age 7 -update",
56
+ "update:deps": "taze --all",
57
+ "watch:build": "tsc-watch -p src --onSuccess 'npm run build'",
58
+ "watch:src": "tsc-watch -p src",
59
+ "watch:test": "tsc-watch -p test"
60
+ },
61
+ "dependencies": {
62
+ "@zokugun/xtry": "0.12.2"
63
+ },
64
+ "devDependencies": {
65
+ "@commitlint/cli": "20.5.3",
66
+ "@commitlint/config-conventional": "20.5.3",
67
+ "@types/node": "20.19.43",
68
+ "@vitest/coverage-v8": "4.1.12",
69
+ "@vitest/ui": "4.1.12",
70
+ "@zokugun/eslint-config": "0.2.1",
71
+ "@zokugun/eslint-config-data": "0.2.1",
72
+ "@zokugun/eslint-config-glossary": "0.2.0",
73
+ "@zokugun/eslint-config-js": "0.2.1",
74
+ "@zokugun/eslint-config-md": "0.4.1",
75
+ "@zokugun/eslint-config-nodejs": "0.2.0",
76
+ "@zokugun/eslint-config-style": "0.3.1",
77
+ "@zokugun/eslint-config-test": "0.2.0",
78
+ "@zokugun/eslint-config-ts": "0.3.2",
79
+ "@zokugun/fs-extra-plus": "0.7.0",
80
+ "@zokugun/tsc-leda": "0.9.4",
81
+ "eslint": "10.13.0",
82
+ "fixpack": "4.0.0",
83
+ "get-node": "15.0.4",
84
+ "husky": "9.1.7",
85
+ "lint-staged": "16.4.0",
86
+ "rimraf": "6.1.4",
87
+ "taze": "19.17.2",
88
+ "tsc-watch": "7.2.1",
89
+ "typescript": "5.9.3",
90
+ "vitest": "4.1.12",
91
+ "yaml": "2.9.1"
92
+ },
93
+ "overrides": {
94
+ "@eslint/config-array": {
95
+ "minimatch": "11.0.1"
96
+ },
97
+ "@typescript-eslint/typescript-estree": {
98
+ "minimatch": "11.0.1"
99
+ },
100
+ "eslint": {
101
+ "minimatch": "11.0.1"
102
+ },
103
+ "eslint-plugin-import-x": {
104
+ "minimatch": "11.0.1"
105
+ },
106
+ "micromark-extension-math": {
107
+ "katex": "0.18.2"
108
+ }
109
+ },
110
+ "allowScripts": {
111
+ "fsevents": true,
112
+ "unrs-resolver": true
113
+ },
114
+ "keywords": [
115
+ "child_process",
116
+ "command",
117
+ "exec",
118
+ "execute",
119
+ "process",
120
+ "shell",
121
+ "spawn",
122
+ "subprocess"
123
+ ]
124
+ }