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,81 @@
1
+ import path from 'path';
2
+
3
+ const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true';
4
+
5
+ export function trace(category, messageOrFunc) {
6
+ if (!VERBOSE) return;
7
+ const message = typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
8
+ const timestamp = new Date().toISOString();
9
+ console.error(`[TRACE ${timestamp}] [${category}] ${message}`);
10
+ }
11
+
12
+ export const VirtualUtils = {
13
+ /**
14
+ * Create standardized error response for missing operands
15
+ */
16
+ missingOperandError(commandName, customMessage = null) {
17
+ const message = customMessage || `${commandName}: missing operand`;
18
+ return { stderr: message, code: 1 };
19
+ },
20
+
21
+ /**
22
+ * Create standardized error response for invalid arguments
23
+ */
24
+ invalidArgumentError(commandName, message) {
25
+ return { stderr: `${commandName}: ${message}`, code: 1 };
26
+ },
27
+
28
+ /**
29
+ * Create standardized success response
30
+ */
31
+ success(stdout = '', code = 0) {
32
+ return { stdout, stderr: '', code };
33
+ },
34
+
35
+ /**
36
+ * Create standardized error response
37
+ */
38
+ error(stderr = '', code = 1) {
39
+ return { stdout: '', stderr, code };
40
+ },
41
+
42
+ /**
43
+ * Validate that command has required number of arguments
44
+ */
45
+ validateArgs(args, minCount, commandName) {
46
+ if (args.length < minCount) {
47
+ if (minCount === 1) {
48
+ return this.missingOperandError(commandName);
49
+ } else {
50
+ return this.invalidArgumentError(commandName, `requires at least ${minCount} arguments`);
51
+ }
52
+ }
53
+ return null; // No error
54
+ },
55
+
56
+ /**
57
+ * Resolve file path with optional cwd parameter
58
+ */
59
+ resolvePath(filePath, cwd = null) {
60
+ const basePath = cwd || process.cwd();
61
+ return path.isAbsolute(filePath) ? filePath : path.resolve(basePath, filePath);
62
+ },
63
+
64
+ /**
65
+ * Safe file system operation wrapper
66
+ */
67
+ async safeFsOperation(operation, errorPrefix) {
68
+ try {
69
+ return await operation();
70
+ } catch (error) {
71
+ return this.error(`${errorPrefix}: ${error.message}`);
72
+ }
73
+ },
74
+
75
+ /**
76
+ * Create async wrapper for Promise-based operations
77
+ */
78
+ createAsyncWrapper(promiseFactory) {
79
+ return new Promise(promiseFactory);
80
+ }
81
+ };
@@ -0,0 +1,19 @@
1
+ import path from 'path';
2
+ import { VirtualUtils } from '../$.utils.mjs';
3
+
4
+ export default async function basename({ args }) {
5
+ const argError = VirtualUtils.validateArgs(args, 1, 'basename');
6
+ if (argError) return argError;
7
+
8
+ const filePath = args[0];
9
+ const suffix = args[1];
10
+
11
+ let result = path.basename(filePath);
12
+
13
+ // Remove suffix if provided and it matches
14
+ if (suffix && result.endsWith(suffix)) {
15
+ result = result.slice(0, -suffix.length);
16
+ }
17
+
18
+ return VirtualUtils.success(result + '\n');
19
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'fs';
2
+ import { trace, VirtualUtils } from '../$.utils.mjs';
3
+
4
+ export default async function cat({ args, stdin, cwd, isCancelled, signal }) {
5
+ if (args.length === 0) {
6
+ // Read from stdin if no files specified
7
+ if (stdin !== undefined && stdin !== '') {
8
+ return VirtualUtils.success(stdin);
9
+ }
10
+ return VirtualUtils.success();
11
+ }
12
+
13
+ try {
14
+ const outputs = [];
15
+ for (const file of args) {
16
+ // Check for cancellation before processing each file
17
+ if (isCancelled?.() || signal?.aborted) {
18
+ trace('VirtualCommand', () => `cat: cancelled while processing files`);
19
+ return { code: 130, stdout: '', stderr: '' }; // SIGINT exit code
20
+ }
21
+
22
+ trace('VirtualCommand', () => `cat: reading file | ${JSON.stringify({ file }, null, 2)}`);
23
+ const resolvedPath = VirtualUtils.resolvePath(file, cwd);
24
+ try {
25
+ const content = fs.readFileSync(resolvedPath, 'utf8');
26
+ outputs.push(content);
27
+ } catch (error) {
28
+ if (error.code === 'ENOENT') {
29
+ return VirtualUtils.error(`cat: ${file}: No such file or directory`);
30
+ } else if (error.code === 'EISDIR') {
31
+ return VirtualUtils.error(`cat: ${file}: Is a directory`);
32
+ } else {
33
+ return VirtualUtils.error(`cat: ${file}: ${error.message}`);
34
+ }
35
+ }
36
+ }
37
+ const output = outputs.join('');
38
+ trace('VirtualCommand', () => `cat: success | ${JSON.stringify({ bytesRead: output.length }, null, 2)}`);
39
+ return VirtualUtils.success(output);
40
+ } catch (error) {
41
+ trace('VirtualCommand', () => `cat: unexpected error | ${JSON.stringify({ error: error.message }, null, 2)}`);
42
+ return VirtualUtils.error(`cat: ${error.message}`);
43
+ }
44
+ }
@@ -0,0 +1,16 @@
1
+ import { trace, VirtualUtils } from '../$.utils.mjs';
2
+
3
+ export default async function cd({ args }) {
4
+ const target = args[0] || process.env.HOME || process.env.USERPROFILE || '/';
5
+ trace('VirtualCommand', () => `cd: changing directory | ${JSON.stringify({ target }, null, 2)}`);
6
+
7
+ try {
8
+ process.chdir(target);
9
+ const newDir = process.cwd();
10
+ trace('VirtualCommand', () => `cd: success | ${JSON.stringify({ newDir }, null, 2)}`);
11
+ return VirtualUtils.success(newDir);
12
+ } catch (error) {
13
+ trace('VirtualCommand', () => `cd: failed | ${JSON.stringify({ error: error.message }, null, 2)}`);
14
+ return { stderr: `cd: ${error.message}`, code: 1 };
15
+ }
16
+ }
@@ -0,0 +1,112 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { trace, VirtualUtils } from '../$.utils.mjs';
4
+
5
+ export default async function cp({ args, stdin, cwd }) {
6
+ const argError = VirtualUtils.validateArgs(args, 2, 'cp');
7
+ if (argError) return VirtualUtils.invalidArgumentError('cp', 'missing destination file operand');
8
+
9
+ // Parse flags and paths
10
+ const flags = new Set();
11
+ const paths = [];
12
+
13
+ for (const arg of args) {
14
+ if (arg === '-r' || arg === '-R' || arg === '--recursive') {
15
+ flags.add('r');
16
+ } else if (arg.startsWith('-')) {
17
+ for (const flag of arg.slice(1)) {
18
+ flags.add(flag);
19
+ }
20
+ } else {
21
+ paths.push(arg);
22
+ }
23
+ }
24
+
25
+ if (paths.length < 2) {
26
+ return VirtualUtils.invalidArgumentError('cp', 'missing destination file operand');
27
+ }
28
+
29
+ const recursive = flags.has('r') || flags.has('R');
30
+ const sources = paths.slice(0, -1);
31
+ const destination = paths[paths.length - 1];
32
+
33
+ try {
34
+ const destPath = VirtualUtils.resolvePath(destination, cwd);
35
+ let destExists = false;
36
+ let destIsDir = false;
37
+
38
+ try {
39
+ const destStats = fs.statSync(destPath);
40
+ destExists = true;
41
+ destIsDir = destStats.isDirectory();
42
+ } catch (error) {
43
+ if (error.code !== 'ENOENT') throw error;
44
+ }
45
+
46
+ // Copying multiple files requires destination to be a directory
47
+ if (sources.length > 1 && destExists && !destIsDir) {
48
+ return VirtualUtils.error(`cp: target '${destination}' is not a directory`);
49
+ }
50
+
51
+ // Helper function to copy directory recursively
52
+ const copyRecursive = (src, dest) => {
53
+ const stats = fs.statSync(src);
54
+
55
+ if (stats.isDirectory()) {
56
+ fs.mkdirSync(dest, { recursive: true });
57
+ const entries = fs.readdirSync(src);
58
+
59
+ for (const entry of entries) {
60
+ copyRecursive(
61
+ path.join(src, entry),
62
+ path.join(dest, entry)
63
+ );
64
+ }
65
+ } else {
66
+ fs.copyFileSync(src, dest);
67
+ }
68
+ };
69
+
70
+ for (const source of sources) {
71
+ const sourcePath = VirtualUtils.resolvePath(source, cwd);
72
+
73
+ try {
74
+ const sourceStats = fs.statSync(sourcePath);
75
+ let finalDestPath = destPath;
76
+
77
+ if (destIsDir || (sources.length > 1 && !destExists)) {
78
+ // Copying into a directory
79
+ if (!destExists) {
80
+ fs.mkdirSync(destPath, { recursive: true });
81
+ destIsDir = true;
82
+ destExists = true;
83
+ }
84
+ finalDestPath = path.join(destPath, path.basename(sourcePath));
85
+ }
86
+
87
+ if (sourceStats.isDirectory()) {
88
+ if (!recursive) {
89
+ return VirtualUtils.error(`cp: -r not specified; omitting directory '${source}'`);
90
+ }
91
+ trace('VirtualCommand', () => `cp: copying directory | ${JSON.stringify({ from: sourcePath, to: finalDestPath }, null, 2)}`);
92
+ copyRecursive(sourcePath, finalDestPath);
93
+ } else {
94
+ trace('VirtualCommand', () => `cp: copying file | ${JSON.stringify({ from: sourcePath, to: finalDestPath }, null, 2)}`);
95
+ fs.copyFileSync(sourcePath, finalDestPath);
96
+ }
97
+
98
+ } catch (error) {
99
+ if (error.code === 'ENOENT') {
100
+ return VirtualUtils.error(`cp: cannot stat '${source}': No such file or directory`);
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ trace('VirtualCommand', () => `cp: success | ${JSON.stringify({ filesCopied: sources.length }, null, 2)}`);
107
+ return VirtualUtils.success();
108
+ } catch (error) {
109
+ trace('VirtualCommand', () => `cp: error | ${JSON.stringify({ error: error.message }, null, 2)}`);
110
+ return VirtualUtils.error(`cp: ${error.message}`);
111
+ }
112
+ }
@@ -0,0 +1,12 @@
1
+ import path from 'path';
2
+ import { VirtualUtils } from '../$.utils.mjs';
3
+
4
+ export default async function dirname({ args }) {
5
+ const argError = VirtualUtils.validateArgs(args, 1, 'dirname');
6
+ if (argError) return argError;
7
+
8
+ const filePath = args[0];
9
+ const result = path.dirname(filePath);
10
+
11
+ return VirtualUtils.success(result + '\n');
12
+ }
@@ -0,0 +1,15 @@
1
+ import { trace, VirtualUtils } from '../$.utils.mjs';
2
+
3
+ export default async function echo({ args }) {
4
+ trace('VirtualCommand', () => `echo: processing | ${JSON.stringify({ argsCount: args.length }, null, 2)}`);
5
+
6
+ let output = args.join(' ');
7
+ if (args.includes('-n')) {
8
+ // Don't add newline
9
+ trace('VirtualCommand', () => `BRANCH: echo => NO_NEWLINE | ${JSON.stringify({}, null, 2)}`);
10
+ output = args.filter(arg => arg !== '-n').join(' ');
11
+ } else {
12
+ output += '\n';
13
+ }
14
+ return VirtualUtils.success(output);
15
+ }
@@ -0,0 +1,14 @@
1
+ import { VirtualUtils } from '../$.utils.mjs';
2
+
3
+ export default async function env({ args, stdin, env }) {
4
+ if (args.length === 0) {
5
+ // Use custom env if provided, otherwise use process.env
6
+ const envVars = env || process.env;
7
+ const output = Object.entries(envVars)
8
+ .map(([key, value]) => `${key}=${value}`)
9
+ .join('\n') + '\n';
10
+ return VirtualUtils.success(output);
11
+ }
12
+ // TODO: Support setting environment variables for subsequent command
13
+ return VirtualUtils.error('env: setting variables not yet supported');
14
+ }
@@ -0,0 +1,9 @@
1
+ export default function createExitCommand(globalShellSettings) {
2
+ return async function exit({ args }) {
3
+ const code = parseInt(args[0] || 0);
4
+ if (globalShellSettings.errexit || code !== 0) {
5
+ throw { code, message: `Command failed with exit code ${code}` };
6
+ }
7
+ return { stdout: '', code };
8
+ };
9
+ }
@@ -0,0 +1,3 @@
1
+ export default async function falseCommand() {
2
+ return { stdout: '', code: 1 };
3
+ }
@@ -0,0 +1,79 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { trace, VirtualUtils } from '../$.utils.mjs';
4
+
5
+ export default async function ls({ args, stdin, cwd }) {
6
+ try {
7
+ // Parse flags
8
+ const flags = new Set();
9
+ const paths = [];
10
+
11
+ for (const arg of args) {
12
+ if (arg.startsWith('-')) {
13
+ for (const flag of arg.slice(1)) {
14
+ flags.add(flag);
15
+ }
16
+ } else {
17
+ paths.push(arg);
18
+ }
19
+ }
20
+
21
+ // Default to current directory if no paths specified
22
+ if (paths.length === 0) {
23
+ paths.push(cwd || process.cwd());
24
+ }
25
+
26
+ const showAll = flags.has('a');
27
+ const longFormat = flags.has('l');
28
+
29
+ trace('VirtualCommand', () => `ls: listing | ${JSON.stringify({ paths, flags: Array.from(flags) }, null, 2)}`);
30
+
31
+ const outputs = [];
32
+
33
+ for (const targetPath of paths) {
34
+ const resolvedPath = VirtualUtils.resolvePath(targetPath, cwd);
35
+ const stats = fs.statSync(resolvedPath);
36
+
37
+ if (stats.isDirectory()) {
38
+ let entries = fs.readdirSync(resolvedPath);
39
+
40
+ if (!showAll) {
41
+ entries = entries.filter(e => !e.startsWith('.'));
42
+ }
43
+
44
+ if (longFormat) {
45
+ for (const entry of entries) {
46
+ const entryPath = path.join(resolvedPath, entry);
47
+ const entryStats = fs.statSync(entryPath);
48
+ const mode = entryStats.isDirectory() ? 'drwxr-xr-x' : '-rw-r--r--';
49
+ const size = entryStats.size.toString().padStart(8);
50
+ const mtime = entryStats.mtime.toISOString().split('T')[0];
51
+ outputs.push(`${mode} 1 user group ${size} ${mtime} ${entry}\n`);
52
+ }
53
+ } else {
54
+ outputs.push(entries.join('\n') + '\n');
55
+ }
56
+ } else {
57
+ // Single file
58
+ if (longFormat) {
59
+ const mode = '-rw-r--r--';
60
+ const size = stats.size.toString().padStart(8);
61
+ const mtime = stats.mtime.toISOString().split('T')[0];
62
+ const basename = path.basename(resolvedPath);
63
+ outputs.push(`${mode} 1 user group ${size} ${mtime} ${basename}\n`);
64
+ } else {
65
+ outputs.push(path.basename(resolvedPath) + '\n');
66
+ }
67
+ }
68
+ }
69
+
70
+ trace('VirtualCommand', () => `ls: success | ${JSON.stringify({ entriesCount: outputs.length }, null, 2)}`);
71
+ return VirtualUtils.success(outputs.join(''));
72
+ } catch (error) {
73
+ trace('VirtualCommand', () => `ls: error | ${JSON.stringify({ error: error.message }, null, 2)}`);
74
+ if (error.code === 'ENOENT') {
75
+ return VirtualUtils.error(`ls: ${args[0] || '.'}: No such file or directory`);
76
+ }
77
+ return VirtualUtils.error(`ls: ${error.message}`);
78
+ }
79
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'fs';
2
+ import { trace, VirtualUtils } from '../$.utils.mjs';
3
+
4
+ export default async function mkdir({ args, stdin, cwd }) {
5
+ const argError = VirtualUtils.validateArgs(args, 1, 'mkdir');
6
+ if (argError) return argError;
7
+
8
+ // Parse flags and paths
9
+ const flags = new Set();
10
+ const paths = [];
11
+
12
+ for (const arg of args) {
13
+ if (arg === '-p' || arg === '--parents') {
14
+ flags.add('p');
15
+ } else if (arg.startsWith('-')) {
16
+ for (const flag of arg.slice(1)) {
17
+ flags.add(flag);
18
+ }
19
+ } else {
20
+ paths.push(arg);
21
+ }
22
+ }
23
+
24
+ if (paths.length === 0) {
25
+ return VirtualUtils.missingOperandError('mkdir');
26
+ }
27
+
28
+ const recursive = flags.has('p');
29
+
30
+ try {
31
+ for (const dir of paths) {
32
+ const resolvedPath = VirtualUtils.resolvePath(dir, cwd);
33
+ trace('VirtualCommand', () => `mkdir: creating | ${JSON.stringify({ dir: resolvedPath, recursive }, null, 2)}`);
34
+ fs.mkdirSync(resolvedPath, { recursive });
35
+ }
36
+ trace('VirtualCommand', () => `mkdir: success | ${JSON.stringify({ dirsCreated: paths.length }, null, 2)}`);
37
+ return VirtualUtils.success();
38
+ } catch (error) {
39
+ trace('VirtualCommand', () => `mkdir: error | ${JSON.stringify({ error: error.message }, null, 2)}`);
40
+ if (error.code === 'EEXIST') {
41
+ return VirtualUtils.error(`mkdir: cannot create directory '${paths[0]}': File exists`);
42
+ }
43
+ return VirtualUtils.error(`mkdir: ${error.message}`);
44
+ }
45
+ }
@@ -0,0 +1,89 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { trace, VirtualUtils } from '../$.utils.mjs';
4
+
5
+ export default async function mv({ args, stdin, cwd }) {
6
+ const argError = VirtualUtils.validateArgs(args, 2, 'mv');
7
+ if (argError) return VirtualUtils.invalidArgumentError('mv', 'missing destination file operand');
8
+
9
+ // Parse flags and paths
10
+ const flags = new Set();
11
+ const paths = [];
12
+
13
+ for (const arg of args) {
14
+ if (arg.startsWith('-')) {
15
+ for (const flag of arg.slice(1)) {
16
+ flags.add(flag);
17
+ }
18
+ } else {
19
+ paths.push(arg);
20
+ }
21
+ }
22
+
23
+ if (paths.length < 2) {
24
+ return VirtualUtils.invalidArgumentError('mv', 'missing destination file operand');
25
+ }
26
+
27
+ const force = flags.has('f');
28
+ const sources = paths.slice(0, -1);
29
+ const destination = paths[paths.length - 1];
30
+
31
+ try {
32
+ const destPath = VirtualUtils.resolvePath(destination, cwd);
33
+ let destExists = false;
34
+ let destIsDir = false;
35
+
36
+ try {
37
+ const destStats = fs.statSync(destPath);
38
+ destExists = true;
39
+ destIsDir = destStats.isDirectory();
40
+ } catch (error) {
41
+ if (error.code !== 'ENOENT') throw error;
42
+ }
43
+
44
+ // Moving multiple files requires destination to be a directory
45
+ if (sources.length > 1 && destExists && !destIsDir) {
46
+ return VirtualUtils.error(`mv: target '${destination}' is not a directory`);
47
+ }
48
+
49
+ for (const source of sources) {
50
+ const sourcePath = VirtualUtils.resolvePath(source, cwd);
51
+
52
+ try {
53
+ const sourceStats = fs.statSync(sourcePath);
54
+ let finalDestPath = destPath;
55
+
56
+ if (destIsDir) {
57
+ // Moving into a directory
58
+ finalDestPath = path.join(destPath, path.basename(sourcePath));
59
+ }
60
+
61
+ // Check if destination exists and handle force flag
62
+ if (!force) {
63
+ try {
64
+ fs.statSync(finalDestPath);
65
+ // Destination exists and no force flag
66
+ return VirtualUtils.error(`mv: cannot move '${source}': File exists`);
67
+ } catch (error) {
68
+ if (error.code !== 'ENOENT') throw error;
69
+ }
70
+ }
71
+
72
+ trace('VirtualCommand', () => `mv: moving | ${JSON.stringify({ from: sourcePath, to: finalDestPath }, null, 2)}`);
73
+ fs.renameSync(sourcePath, finalDestPath);
74
+
75
+ } catch (error) {
76
+ if (error.code === 'ENOENT') {
77
+ return VirtualUtils.error(`mv: cannot stat '${source}': No such file or directory`);
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ trace('VirtualCommand', () => `mv: success | ${JSON.stringify({ filesMoved: sources.length }, null, 2)}`);
84
+ return VirtualUtils.success();
85
+ } catch (error) {
86
+ trace('VirtualCommand', () => `mv: error | ${JSON.stringify({ error: error.message }, null, 2)}`);
87
+ return VirtualUtils.error(`mv: ${error.message}`);
88
+ }
89
+ }
@@ -0,0 +1,8 @@
1
+ import { trace, VirtualUtils } from '../$.utils.mjs';
2
+
3
+ export default async function pwd({ args, stdin, cwd }) {
4
+ // If cwd option is provided, return that instead of process.cwd()
5
+ const dir = cwd || process.cwd();
6
+ trace('VirtualCommand', () => `pwd: getting directory | ${JSON.stringify({ dir }, null, 2)}`);
7
+ return VirtualUtils.success(dir);
8
+ }
@@ -0,0 +1,60 @@
1
+ import fs from 'fs';
2
+ import { trace, VirtualUtils } from '../$.utils.mjs';
3
+
4
+ export default async function rm({ args, stdin, cwd }) {
5
+ const argError = VirtualUtils.validateArgs(args, 1, 'rm');
6
+ if (argError) return argError;
7
+
8
+ // Parse flags and paths
9
+ const flags = new Set();
10
+ const paths = [];
11
+
12
+ for (const arg of args) {
13
+ if (arg.startsWith('-')) {
14
+ for (const flag of arg.slice(1)) {
15
+ flags.add(flag);
16
+ }
17
+ } else {
18
+ paths.push(arg);
19
+ }
20
+ }
21
+
22
+ if (paths.length === 0) {
23
+ return VirtualUtils.missingOperandError('rm');
24
+ }
25
+
26
+ const recursive = flags.has('r') || flags.has('R');
27
+ const force = flags.has('f');
28
+
29
+ try {
30
+ for (const file of paths) {
31
+ const resolvedPath = VirtualUtils.resolvePath(file, cwd);
32
+ trace('VirtualCommand', () => `rm: removing | ${JSON.stringify({ file: resolvedPath, recursive, force }, null, 2)}`);
33
+
34
+ try {
35
+ const stats = fs.statSync(resolvedPath);
36
+ if (stats.isDirectory() && !recursive) {
37
+ return VirtualUtils.error(`rm: cannot remove '${file}': Is a directory`);
38
+ }
39
+
40
+ if (stats.isDirectory()) {
41
+ fs.rmSync(resolvedPath, { recursive: true, force });
42
+ } else {
43
+ fs.unlinkSync(resolvedPath);
44
+ }
45
+ } catch (error) {
46
+ if (error.code === 'ENOENT' && !force) {
47
+ return VirtualUtils.error(`rm: cannot remove '${file}': No such file or directory`);
48
+ } else if (error.code !== 'ENOENT') {
49
+ throw error;
50
+ }
51
+ }
52
+ }
53
+
54
+ trace('VirtualCommand', () => `rm: success | ${JSON.stringify({ filesRemoved: paths.length }, null, 2)}`);
55
+ return VirtualUtils.success();
56
+ } catch (error) {
57
+ trace('VirtualCommand', () => `rm: error | ${JSON.stringify({ error: error.message }, null, 2)}`);
58
+ return VirtualUtils.error(`rm: ${error.message}`);
59
+ }
60
+ }
@@ -0,0 +1,48 @@
1
+ import { VirtualUtils } from '../$.utils.mjs';
2
+
3
+ export default async function seq({ args }) {
4
+ const argError = VirtualUtils.validateArgs(args, 1, 'seq');
5
+ if (argError) return argError;
6
+
7
+ let start, step, end;
8
+
9
+ if (args.length === 1) {
10
+ // seq END
11
+ start = 1;
12
+ step = 1;
13
+ end = parseFloat(args[0]);
14
+ } else if (args.length === 2) {
15
+ // seq START END
16
+ start = parseFloat(args[0]);
17
+ step = 1;
18
+ end = parseFloat(args[1]);
19
+ } else {
20
+ // seq START STEP END
21
+ start = parseFloat(args[0]);
22
+ step = parseFloat(args[1]);
23
+ end = parseFloat(args[2]);
24
+ }
25
+
26
+ // Validate numbers
27
+ if (isNaN(start) || isNaN(step) || isNaN(end)) {
28
+ return VirtualUtils.error('seq: invalid number');
29
+ }
30
+
31
+ if (step === 0) {
32
+ return VirtualUtils.error('seq: step cannot be zero');
33
+ }
34
+
35
+ const output = [];
36
+
37
+ if (step > 0) {
38
+ for (let i = start; i <= end; i += step) {
39
+ output.push(i.toString());
40
+ }
41
+ } else {
42
+ for (let i = start; i >= end; i += step) {
43
+ output.push(i.toString());
44
+ }
45
+ }
46
+
47
+ return VirtualUtils.success(output.join('\n') + (output.length > 0 ? '\n' : ''));
48
+ }