@gjsify/child_process 0.1.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.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @gjsify/child_process
2
+
3
+ GJS implementation of the Node.js `child_process` module using Gio.Subprocess. Supports exec, execSync, spawn, and spawnSync.
4
+
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @gjsify/child_process
11
+ # or
12
+ yarn add @gjsify/child_process
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { execSync, spawn } from '@gjsify/child_process';
19
+
20
+ const output = execSync('echo hello').toString();
21
+ const child = spawn('ls', ['-la']);
22
+ ```
23
+
24
+ ## License
25
+
26
+ MIT
@@ -0,0 +1,301 @@
1
+ import Gio from "@girs/gio-2.0";
2
+ import GLib from "@girs/glib-2.0";
3
+ import { EventEmitter } from "node:events";
4
+ import { Buffer } from "node:buffer";
5
+ import { gbytesToUint8Array, deferEmit } from "@gjsify/utils";
6
+ const _activeProcesses = /* @__PURE__ */ new Set();
7
+ class ChildProcess extends EventEmitter {
8
+ pid;
9
+ exitCode = null;
10
+ signalCode = null;
11
+ killed = false;
12
+ connected = false;
13
+ stdin = null;
14
+ stdout = null;
15
+ stderr = null;
16
+ _subprocess = null;
17
+ /** @internal Set the underlying Gio.Subprocess and extract PID. */
18
+ _setSubprocess(proc) {
19
+ this._subprocess = proc;
20
+ const pid = proc.get_identifier();
21
+ if (pid) this.pid = parseInt(pid, 10);
22
+ }
23
+ /** Send a signal to the child process. */
24
+ kill(signal) {
25
+ if (!this._subprocess) return false;
26
+ try {
27
+ if (signal === "SIGKILL" || signal === 9) {
28
+ this._subprocess.force_exit();
29
+ } else {
30
+ this._subprocess.send_signal(typeof signal === "number" ? signal : 15);
31
+ }
32
+ this.killed = true;
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ ref() {
39
+ return this;
40
+ }
41
+ unref() {
42
+ return this;
43
+ }
44
+ }
45
+ function _spawnSubprocess(argv, flags, options) {
46
+ const launcher = new Gio.SubprocessLauncher({ flags });
47
+ if (options?.cwd) {
48
+ launcher.set_cwd(options.cwd);
49
+ }
50
+ if (options?.env) {
51
+ for (const [key, value] of Object.entries(options.env)) {
52
+ launcher.setenv(key, value, true);
53
+ }
54
+ }
55
+ return launcher.spawnv(argv);
56
+ }
57
+ function execSync(command, options) {
58
+ const encoding = options?.encoding;
59
+ const input = options?.input;
60
+ const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
61
+ const shell = typeof options?.shell === "string" ? options.shell : "/bin/sh";
62
+ const proc = _spawnSubprocess([shell, "-c", command], flags, options);
63
+ const stdinBytes = input ? new GLib.Bytes(typeof input === "string" ? new TextEncoder().encode(input) : input) : null;
64
+ const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
65
+ const status = proc.get_exit_status();
66
+ if (status !== 0) {
67
+ const stderrStr = stderrBytes ? new TextDecoder().decode(gbytesToUint8Array(stderrBytes)) : "";
68
+ const error = new Error(`Command failed: ${command}
69
+ ${stderrStr}`);
70
+ error.status = status;
71
+ error.stderr = stderrStr;
72
+ error.stdout = stdoutBytes ? new TextDecoder().decode(gbytesToUint8Array(stdoutBytes)) : "";
73
+ throw error;
74
+ }
75
+ if (!stdoutBytes) return encoding && encoding !== "buffer" ? "" : Buffer.alloc(0);
76
+ const data = gbytesToUint8Array(stdoutBytes);
77
+ if (encoding && encoding !== "buffer") {
78
+ return new TextDecoder().decode(data);
79
+ }
80
+ return Buffer.from(data);
81
+ }
82
+ function _exec(command, options, callback) {
83
+ if (typeof options === "function") {
84
+ callback = options;
85
+ options = {};
86
+ }
87
+ const opts = options || {};
88
+ const child = new ChildProcess();
89
+ const shell = typeof opts.shell === "string" ? opts.shell : "/bin/sh";
90
+ const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE;
91
+ try {
92
+ const proc = _spawnSubprocess([shell, "-c", command], flags, opts);
93
+ child._setSubprocess(proc);
94
+ proc.communicate_utf8_async(null, null, (_source, result) => {
95
+ try {
96
+ const [, stdout, stderr] = proc.communicate_utf8_finish(result);
97
+ const exitStatus = proc.get_exit_status();
98
+ child.exitCode = exitStatus;
99
+ if (exitStatus !== 0) {
100
+ const error = new Error(`Command failed: ${command}`);
101
+ error.code = exitStatus;
102
+ error.killed = child.killed;
103
+ error.stdout = stdout || "";
104
+ error.stderr = stderr || "";
105
+ if (callback) callback(error, stdout || "", stderr || "");
106
+ } else {
107
+ if (callback) callback(null, stdout || "", stderr || "");
108
+ }
109
+ child.emit("close", exitStatus, null);
110
+ child.emit("exit", exitStatus, null);
111
+ } catch (err) {
112
+ const error = err instanceof Error ? err : new Error(String(err));
113
+ if (callback) callback(error, "", "");
114
+ child.emit("error", error);
115
+ }
116
+ });
117
+ } catch (err) {
118
+ const error = err instanceof Error ? err : new Error(String(err));
119
+ setTimeout(() => {
120
+ if (callback) callback(error, "", "");
121
+ child.emit("error", error);
122
+ }, 0);
123
+ }
124
+ return child;
125
+ }
126
+ function execFile(file, args, options, callback) {
127
+ let _args = [];
128
+ let _opts = {};
129
+ let _callback;
130
+ if (typeof args === "function") {
131
+ _callback = args;
132
+ } else if (Array.isArray(args)) {
133
+ _args = args;
134
+ if (typeof options === "function") {
135
+ _callback = options;
136
+ } else {
137
+ _opts = options || {};
138
+ _callback = callback;
139
+ }
140
+ }
141
+ const child = new ChildProcess();
142
+ const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE;
143
+ try {
144
+ const proc = _spawnSubprocess([file, ..._args], flags, _opts);
145
+ child._setSubprocess(proc);
146
+ proc.communicate_utf8_async(null, null, (_source, result) => {
147
+ try {
148
+ const [, stdout, stderr] = proc.communicate_utf8_finish(result);
149
+ const exitStatus = proc.get_exit_status();
150
+ child.exitCode = exitStatus;
151
+ if (exitStatus !== 0) {
152
+ const error = new Error(`Command failed: ${file}`);
153
+ error.code = exitStatus;
154
+ error.stdout = stdout || "";
155
+ error.stderr = stderr || "";
156
+ if (_callback) _callback(error, stdout || "", stderr || "");
157
+ } else {
158
+ if (_callback) _callback(null, stdout || "", stderr || "");
159
+ }
160
+ child.emit("close", exitStatus, null);
161
+ child.emit("exit", exitStatus, null);
162
+ } catch (err) {
163
+ const error = err instanceof Error ? err : new Error(String(err));
164
+ if (_callback) _callback(error, "", "");
165
+ child.emit("error", error);
166
+ }
167
+ });
168
+ } catch (err) {
169
+ const error = err instanceof Error ? err : new Error(String(err));
170
+ setTimeout(() => {
171
+ if (_callback) _callback(error, "", "");
172
+ child.emit("error", error);
173
+ }, 0);
174
+ }
175
+ return child;
176
+ }
177
+ function execFileSync(file, args, options) {
178
+ const _args = args || [];
179
+ const encoding = options?.encoding;
180
+ const input = options?.input;
181
+ const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
182
+ const proc = _spawnSubprocess([file, ..._args], flags, options);
183
+ const stdinBytes = input ? new GLib.Bytes(typeof input === "string" ? new TextEncoder().encode(input) : input) : null;
184
+ const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
185
+ const status = proc.get_exit_status();
186
+ if (status !== 0) {
187
+ const stderrStr = stderrBytes ? new TextDecoder().decode(gbytesToUint8Array(stderrBytes)) : "";
188
+ const error = new Error(`Command failed: ${file} ${_args.join(" ")}`);
189
+ error.status = status;
190
+ error.stderr = stderrStr;
191
+ throw error;
192
+ }
193
+ if (!stdoutBytes) return encoding && encoding !== "buffer" ? "" : Buffer.alloc(0);
194
+ const data = gbytesToUint8Array(stdoutBytes);
195
+ if (encoding && encoding !== "buffer") {
196
+ return new TextDecoder().decode(data);
197
+ }
198
+ return Buffer.from(data);
199
+ }
200
+ function spawn(command, args, options) {
201
+ const _args = args || [];
202
+ const child = new ChildProcess();
203
+ const useShell = options?.shell;
204
+ let argv;
205
+ if (useShell) {
206
+ const shell = typeof useShell === "string" ? useShell : "/bin/sh";
207
+ const fullCmd = [command, ..._args].join(" ");
208
+ argv = [shell, "-c", fullCmd];
209
+ } else {
210
+ argv = [command, ..._args];
211
+ }
212
+ const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE | Gio.SubprocessFlags.STDIN_PIPE;
213
+ try {
214
+ const proc = _spawnSubprocess(argv, flags, options);
215
+ child._setSubprocess(proc);
216
+ _activeProcesses.add(child);
217
+ proc.wait_async(null, (_source, result) => {
218
+ try {
219
+ proc.wait_finish(result);
220
+ const exitStatus = proc.get_if_exited() ? proc.get_exit_status() : null;
221
+ const signal = proc.get_if_signaled() ? "SIGTERM" : null;
222
+ child.exitCode = exitStatus;
223
+ child.signalCode = signal;
224
+ child.emit("exit", exitStatus, signal);
225
+ child.emit("close", exitStatus, signal);
226
+ } catch (err) {
227
+ child.emit("error", err instanceof Error ? err : new Error(String(err)));
228
+ }
229
+ _activeProcesses.delete(child);
230
+ });
231
+ deferEmit(child, "spawn");
232
+ } catch (err) {
233
+ deferEmit(child, "error", err instanceof Error ? err : new Error(String(err)));
234
+ }
235
+ return child;
236
+ }
237
+ function spawnSync(command, args, options) {
238
+ const _args = args || [];
239
+ const useShell = options?.shell;
240
+ const input = options?.input;
241
+ let argv;
242
+ if (useShell) {
243
+ const shell = typeof useShell === "string" ? useShell : "/bin/sh";
244
+ const fullCmd = [command, ..._args].join(" ");
245
+ argv = [shell, "-c", fullCmd];
246
+ } else {
247
+ argv = [command, ..._args];
248
+ }
249
+ const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
250
+ try {
251
+ const proc = _spawnSubprocess(argv, flags, options);
252
+ const pid = proc.get_identifier();
253
+ const stdinBytes = input ? new GLib.Bytes(typeof input === "string" ? new TextEncoder().encode(input) : input) : null;
254
+ const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
255
+ const stdoutBuf = stdoutBytes ? Buffer.from(gbytesToUint8Array(stdoutBytes)) : Buffer.alloc(0);
256
+ const stderrBuf = stderrBytes ? Buffer.from(gbytesToUint8Array(stderrBytes)) : Buffer.alloc(0);
257
+ const encoding = options?.encoding;
258
+ const stdoutData = encoding && encoding !== "buffer" ? new TextDecoder().decode(stdoutBuf) : stdoutBuf;
259
+ const stderrData = encoding && encoding !== "buffer" ? new TextDecoder().decode(stderrBuf) : stderrBuf;
260
+ const status = proc.get_if_exited() ? proc.get_exit_status() : null;
261
+ const signal = proc.get_if_signaled() ? "SIGTERM" : null;
262
+ return {
263
+ pid: pid ? parseInt(pid, 10) : 0,
264
+ output: [null, stdoutData, stderrData],
265
+ stdout: stdoutData,
266
+ stderr: stderrData,
267
+ status,
268
+ signal
269
+ };
270
+ } catch (err) {
271
+ const empty = options?.encoding && options.encoding !== "buffer" ? "" : Buffer.alloc(0);
272
+ return {
273
+ pid: 0,
274
+ output: [null, empty, empty],
275
+ stdout: empty,
276
+ stderr: empty,
277
+ status: null,
278
+ signal: null,
279
+ error: err instanceof Error ? err : new Error(String(err))
280
+ };
281
+ }
282
+ }
283
+ var index_default = {
284
+ ChildProcess,
285
+ exec: _exec,
286
+ execSync,
287
+ execFile,
288
+ execFileSync,
289
+ spawn,
290
+ spawnSync
291
+ };
292
+ export {
293
+ ChildProcess,
294
+ index_default as default,
295
+ _exec as exec,
296
+ execFile,
297
+ execFileSync,
298
+ execSync,
299
+ spawn,
300
+ spawnSync
301
+ };
@@ -0,0 +1,96 @@
1
+ import Gio from '@girs/gio-2.0';
2
+ import { EventEmitter } from 'node:events';
3
+ import { Buffer } from 'node:buffer';
4
+ import type { Readable, Writable } from 'node:stream';
5
+ export interface ExecOptions {
6
+ cwd?: string;
7
+ env?: Record<string, string>;
8
+ encoding?: BufferEncoding | 'buffer';
9
+ shell?: string | boolean;
10
+ timeout?: number;
11
+ maxBuffer?: number;
12
+ killSignal?: string | number;
13
+ uid?: number;
14
+ gid?: number;
15
+ windowsHide?: boolean;
16
+ }
17
+ export interface ExecSyncOptions {
18
+ cwd?: string;
19
+ env?: Record<string, string>;
20
+ encoding?: BufferEncoding | 'buffer';
21
+ shell?: string | boolean;
22
+ timeout?: number;
23
+ maxBuffer?: number;
24
+ killSignal?: string | number;
25
+ stdio?: string | string[];
26
+ input?: string | Buffer | Uint8Array;
27
+ }
28
+ export interface SpawnOptions {
29
+ cwd?: string;
30
+ env?: Record<string, string>;
31
+ stdio?: string | string[];
32
+ shell?: string | boolean;
33
+ timeout?: number;
34
+ killSignal?: string | number;
35
+ }
36
+ export interface SpawnSyncResult {
37
+ pid: number;
38
+ output: (Buffer | string | null)[];
39
+ stdout: Buffer | string;
40
+ stderr: Buffer | string;
41
+ status: number | null;
42
+ signal: string | null;
43
+ error?: Error;
44
+ }
45
+ /**
46
+ * ChildProcess — EventEmitter wrapping Gio.Subprocess.
47
+ */
48
+ export declare class ChildProcess extends EventEmitter {
49
+ pid?: number;
50
+ exitCode: number | null;
51
+ signalCode: string | null;
52
+ killed: boolean;
53
+ connected: boolean;
54
+ stdin: Writable | null;
55
+ stdout: Readable | null;
56
+ stderr: Readable | null;
57
+ private _subprocess;
58
+ /** @internal Set the underlying Gio.Subprocess and extract PID. */
59
+ _setSubprocess(proc: Gio.Subprocess): void;
60
+ /** Send a signal to the child process. */
61
+ kill(signal?: string | number): boolean;
62
+ ref(): this;
63
+ unref(): this;
64
+ }
65
+ export declare function execSync(command: string, options?: ExecSyncOptions): Buffer | string;
66
+ /**
67
+ * Execute a command in a shell (async with callback).
68
+ */
69
+ declare function _exec(command: string, options?: ExecOptions | ((error: Error | null, stdout: string, stderr: string) => void), callback?: (error: Error | null, stdout: string, stderr: string) => void): ChildProcess;
70
+ export { _exec as exec };
71
+ /**
72
+ * Execute a file directly without shell (async).
73
+ */
74
+ export declare function execFile(file: string, args?: string[] | ((error: Error | null, stdout: string, stderr: string) => void), options?: ExecOptions | ((error: Error | null, stdout: string, stderr: string) => void), callback?: (error: Error | null, stdout: string, stderr: string) => void): ChildProcess;
75
+ /**
76
+ * Execute a file directly without shell (sync).
77
+ */
78
+ export declare function execFileSync(file: string, args?: string[], options?: ExecSyncOptions): Buffer | string;
79
+ /**
80
+ * Spawn a new process (async, with event-based API).
81
+ */
82
+ export declare function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess;
83
+ /**
84
+ * Spawn a new process (sync).
85
+ */
86
+ export declare function spawnSync(command: string, args?: string[], options?: ExecSyncOptions): SpawnSyncResult;
87
+ declare const _default: {
88
+ ChildProcess: typeof ChildProcess;
89
+ exec: typeof _exec;
90
+ execSync: typeof execSync;
91
+ execFile: typeof execFile;
92
+ execFileSync: typeof execFileSync;
93
+ spawn: typeof spawn;
94
+ spawnSync: typeof spawnSync;
95
+ };
96
+ export default _default;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@gjsify/child_process",
3
+ "version": "0.1.0",
4
+ "description": "Node.js child_process module for Gjs",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
16
+ "check": "tsc --noEmit",
17
+ "build": "yarn build:gjsify && yarn build:types",
18
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
19
+ "build:types": "tsc",
20
+ "build:test": "yarn build:test:gjs && yarn build:test:node",
21
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
22
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
23
+ "test": "yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
24
+ "test:gjs": "gjs -m test.gjs.mjs",
25
+ "test:node": "node test.node.mjs"
26
+ },
27
+ "keywords": [
28
+ "gjs",
29
+ "node",
30
+ "child_process"
31
+ ],
32
+ "devDependencies": {
33
+ "@gjsify/cli": "^0.1.0",
34
+ "@gjsify/unit": "^0.1.0",
35
+ "@types/node": "^25.5.0",
36
+ "typescript": "^6.0.2"
37
+ },
38
+ "dependencies": {
39
+ "@girs/gio-2.0": "^2.88.0-4.0.0-beta.42",
40
+ "@girs/glib-2.0": "^2.88.0-4.0.0-beta.42",
41
+ "@gjsify/buffer": "^0.1.0",
42
+ "@gjsify/events": "^0.1.0",
43
+ "@gjsify/utils": "^0.1.0"
44
+ }
45
+ }