command-stream 0.3.1 → 0.4.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.
@@ -0,0 +1,72 @@
1
+ import { trace } from '../$.utils.mjs';
2
+
3
+ export default async function sleep({ args, signal, isCancelled }) {
4
+ const seconds = parseFloat(args[0] || 0);
5
+ trace('VirtualCommand', () => `sleep: starting | ${JSON.stringify({
6
+ seconds,
7
+ hasSignal: !!signal,
8
+ signalAborted: signal?.aborted,
9
+ hasIsCancelled: !!isCancelled
10
+ }, null, 2)}`);
11
+
12
+ if (isNaN(seconds) || seconds < 0) {
13
+ return { stderr: `sleep: invalid time interval '${args[0]}'`, code: 1 };
14
+ }
15
+
16
+ // Use abort signal if available, otherwise use setTimeout
17
+ try {
18
+ await new Promise((resolve, reject) => {
19
+ const timeoutId = setTimeout(resolve, seconds * 1000);
20
+
21
+ // Handle cancellation via signal
22
+ if (signal) {
23
+ trace('VirtualCommand', () => `sleep: setting up abort signal listener | ${JSON.stringify({
24
+ signalAborted: signal.aborted
25
+ }, null, 2)}`);
26
+
27
+ signal.addEventListener('abort', () => {
28
+ trace('VirtualCommand', () => `sleep: abort signal received | ${JSON.stringify({
29
+ seconds,
30
+ signalAborted: signal.aborted
31
+ }, null, 2)}`);
32
+ clearTimeout(timeoutId);
33
+ reject(new Error('Sleep cancelled'));
34
+ });
35
+
36
+ // Check if already aborted
37
+ if (signal.aborted) {
38
+ trace('VirtualCommand', () => `sleep: signal already aborted | ${JSON.stringify({ seconds }, null, 2)}`);
39
+ clearTimeout(timeoutId);
40
+ reject(new Error('Sleep cancelled'));
41
+ return;
42
+ }
43
+ } else {
44
+ trace('VirtualCommand', () => `sleep: no signal provided | ${JSON.stringify({ seconds }, null, 2)}`);
45
+ }
46
+
47
+ // Also check isCancelled periodically for quicker response
48
+ if (isCancelled) {
49
+ trace('VirtualCommand', () => `sleep: setting up isCancelled polling | ${JSON.stringify({ seconds }, null, 2)}`);
50
+ const checkInterval = setInterval(() => {
51
+ if (isCancelled()) {
52
+ trace('VirtualCommand', () => `sleep: isCancelled returned true | ${JSON.stringify({ seconds }, null, 2)}`);
53
+ clearTimeout(timeoutId);
54
+ clearInterval(checkInterval);
55
+ reject(new Error('Sleep cancelled'));
56
+ }
57
+ }, 100);
58
+ }
59
+ });
60
+
61
+ trace('VirtualCommand', () => `sleep: completed naturally | ${JSON.stringify({ seconds }, null, 2)}`);
62
+ return { stdout: '', code: 0 };
63
+ } catch (err) {
64
+ trace('VirtualCommand', () => `sleep: interrupted | ${JSON.stringify({
65
+ seconds,
66
+ error: err.message,
67
+ errorName: err.name
68
+ }, null, 2)}`);
69
+ // Let the ProcessRunner determine the appropriate exit code based on the cancellation signal
70
+ throw err;
71
+ }
72
+ }
@@ -0,0 +1,59 @@
1
+ import fs from 'fs';
2
+ import { VirtualUtils } from '../$.utils.mjs';
3
+
4
+ export default async function test({ args }) {
5
+ if (args.length === 0) {
6
+ return { stdout: '', code: 1 };
7
+ }
8
+
9
+ const operator = args[0];
10
+ const operand = args[1];
11
+
12
+ try {
13
+ switch (operator) {
14
+ case '-e': // File exists
15
+ try {
16
+ fs.statSync(operand);
17
+ return { stdout: '', code: 0 };
18
+ } catch {
19
+ return { stdout: '', code: 1 };
20
+ }
21
+
22
+ case '-f': // Regular file
23
+ try {
24
+ const stats = fs.statSync(operand);
25
+ return { stdout: '', code: stats.isFile() ? 0 : 1 };
26
+ } catch {
27
+ return { stdout: '', code: 1 };
28
+ }
29
+
30
+ case '-d': // Directory
31
+ try {
32
+ const stats = fs.statSync(operand);
33
+ return { stdout: '', code: stats.isDirectory() ? 0 : 1 };
34
+ } catch {
35
+ return { stdout: '', code: 1 };
36
+ }
37
+
38
+ case '-s': // File exists and not empty
39
+ try {
40
+ const stats = fs.statSync(operand);
41
+ return { stdout: '', code: stats.size > 0 ? 0 : 1 };
42
+ } catch {
43
+ return { stdout: '', code: 1 };
44
+ }
45
+
46
+ case '-z': // String is empty
47
+ return { stdout: '', code: (!operand || operand.length === 0) ? 0 : 1 };
48
+
49
+ case '-n': // String is not empty
50
+ return { stdout: '', code: (operand && operand.length > 0) ? 0 : 1 };
51
+
52
+ default:
53
+ // Simple string test (non-empty)
54
+ return { stdout: '', code: (operator && operator.length > 0) ? 0 : 1 };
55
+ }
56
+ } catch (error) {
57
+ return VirtualUtils.error(`test: ${error.message}`);
58
+ }
59
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'fs';
2
+ import { trace, VirtualUtils } from '../$.utils.mjs';
3
+
4
+ export default async function touch({ args, stdin, cwd }) {
5
+ const argError = VirtualUtils.validateArgs(args, 1, 'touch');
6
+ if (argError) return VirtualUtils.missingOperandError('touch', 'touch: missing file operand');
7
+
8
+ try {
9
+ for (const file of args) {
10
+ const resolvedPath = VirtualUtils.resolvePath(file, cwd);
11
+ trace('VirtualCommand', () => `touch: processing | ${JSON.stringify({ file: resolvedPath }, null, 2)}`);
12
+
13
+ const now = new Date();
14
+
15
+ try {
16
+ // Try to update existing file's timestamp
17
+ fs.utimesSync(resolvedPath, now, now);
18
+ trace('VirtualCommand', () => `touch: updated timestamp | ${JSON.stringify({ file: resolvedPath }, null, 2)}`);
19
+ } catch (error) {
20
+ if (error.code === 'ENOENT') {
21
+ // File doesn't exist, create it
22
+ fs.writeFileSync(resolvedPath, '');
23
+ trace('VirtualCommand', () => `touch: created file | ${JSON.stringify({ file: resolvedPath }, null, 2)}`);
24
+ } else {
25
+ throw error;
26
+ }
27
+ }
28
+ }
29
+
30
+ trace('VirtualCommand', () => `touch: success | ${JSON.stringify({ filesTouched: args.length }, null, 2)}`);
31
+ return VirtualUtils.success();
32
+ } catch (error) {
33
+ trace('VirtualCommand', () => `touch: error | ${JSON.stringify({ error: error.message }, null, 2)}`);
34
+ return VirtualUtils.error(`touch: ${error.message}`);
35
+ }
36
+ }
@@ -0,0 +1,5 @@
1
+ import { VirtualUtils } from '../$.utils.mjs';
2
+
3
+ export default async function trueCommand() {
4
+ return VirtualUtils.success();
5
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { VirtualUtils } from '../$.utils.mjs';
4
+
5
+ export default function createWhichCommand(virtualCommands) {
6
+ return async function which({ args }) {
7
+ const argError = VirtualUtils.validateArgs(args, 1, 'which');
8
+ if (argError) return argError;
9
+
10
+ const cmd = args[0];
11
+
12
+ if (virtualCommands.has(cmd)) {
13
+ return VirtualUtils.success(`${cmd}: shell builtin\n`);
14
+ }
15
+
16
+ const paths = (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':');
17
+ const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : [''];
18
+
19
+ for (const pathDir of paths) {
20
+ for (const ext of extensions) {
21
+ const fullPath = path.join(pathDir, cmd + ext);
22
+ try {
23
+ if (fs.statSync(fullPath).isFile()) {
24
+ return VirtualUtils.success(fullPath + '\n');
25
+ }
26
+ } catch { }
27
+ }
28
+ }
29
+
30
+ return VirtualUtils.error(`which: no ${cmd} in PATH`);
31
+ };
32
+ }
@@ -0,0 +1,48 @@
1
+ import { trace } from '../$.utils.mjs';
2
+
3
+ export default async function* yes({ args, stdin, isCancelled, signal, ...rest }) {
4
+ const output = args.length > 0 ? args.join(' ') : 'y';
5
+ trace('VirtualCommand', () => `yes: starting infinite generator | ${JSON.stringify({
6
+ output,
7
+ hasIsCancelled: !!isCancelled,
8
+ hasSignal: !!signal
9
+ }, null, 2)}`);
10
+
11
+ let iteration = 0;
12
+ const MAX_ITERATIONS = 1000000; // Safety limit
13
+
14
+ while (!isCancelled?.() && iteration < MAX_ITERATIONS) {
15
+ trace('VirtualCommand', () => `yes: iteration ${iteration} starting | ${JSON.stringify({
16
+ isCancelled: isCancelled?.(),
17
+ signalAborted: signal?.aborted
18
+ }, null, 2)}`);
19
+
20
+ // Check for abort signal
21
+ if (signal?.aborted) {
22
+ trace('VirtualCommand', () => `yes: aborted via signal | ${JSON.stringify({ iteration }, null, 2)}`);
23
+ break;
24
+ }
25
+
26
+ // Also check rest properties for various cancellation methods
27
+ if (rest.aborted || rest.cancelled || rest.stop) {
28
+ trace('VirtualCommand', () => `yes: stopped via property | ${JSON.stringify({ iteration }, null, 2)}`);
29
+ break;
30
+ }
31
+
32
+ trace('VirtualCommand', () => `yes: yielding output for iteration ${iteration}`);
33
+ yield output + '\n';
34
+
35
+ iteration++;
36
+
37
+ // Yield control after every iteration to allow cancellation
38
+ // This ensures the consumer can break cleanly
39
+ trace('VirtualCommand', () => `yes: yielding control after iteration ${iteration - 1}`);
40
+ await new Promise(resolve => setImmediate(resolve));
41
+ }
42
+
43
+ trace('VirtualCommand', () => `yes: generator completed | ${JSON.stringify({
44
+ iteration,
45
+ wasCancelled: isCancelled?.(),
46
+ wasAborted: signal?.aborted
47
+ }, null, 2)}`);
48
+ }