Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/modular-utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'command-stream': patch
---

Reorganize codebase with modular utilities for better maintainability

- Extract trace/logging utilities to $.trace.mjs
- Extract shell detection to $.shell.mjs
- Extract stream utilities to $.stream-utils.mjs and $.stream-emitter.mjs
- Extract shell quoting to $.quote.mjs
- Extract result creation to $.result.mjs
- Extract ANSI utilities to $.ansi.mjs
- Extract global state management to $.state.mjs
- Extract shell settings to $.shell-settings.mjs
- Extract virtual command registration to $.virtual-commands.mjs
- Add commands/index.mjs for module exports
- Update $.utils.mjs to use shared trace module

All new modules follow the 1500-line limit guideline. The Rust code
structure already follows best practices with tests in separate files.
24 changes: 23 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default [
],
'max-params': ['warn', 6], // Maximum function parameters - slightly more lenient than strict 5
'max-statements': ['warn', 60], // Maximum statements per function - reasonable limit for orchestration functions
'max-lines': ['warn', 1500], // Maximum lines per file - set to warn for existing large files
'max-lines': ['error', 1500], // Maximum lines per file - enforced for all source files
},
},
{
Expand Down Expand Up @@ -193,6 +193,28 @@ export default [
},
},
},
{
// ProcessRunner modular architecture uses attachment pattern
// where methods are attached to prototypes within wrapper functions.
// These wrapper functions are larger than typical functions but contain
// method definitions themselves, not complex logic.
files: [
'js/src/$.process-runner-execution.mjs',
'js/src/$.process-runner-pipeline.mjs',
'src/$.process-runner-execution.mjs',
'src/$.process-runner-pipeline.mjs',
],
rules: {
'max-lines-per-function': [
'warn',
{
max: 450,
skipBlankLines: true,
skipComments: true,
},
],
},
},
{
ignores: [
'node_modules/**',
Expand Down
147 changes: 147 additions & 0 deletions js/src/$.ansi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// ANSI control character utilities for command-stream
// Handles stripping and processing of ANSI escape codes

import { trace } from './$.trace.mjs';

/**
* ANSI control character utilities
*/
export const AnsiUtils = {
/**
* Strip ANSI escape codes from text
* @param {string} text - Text to process
* @returns {string} Text without ANSI codes
*/
stripAnsi(text) {
if (typeof text !== 'string') {
return text;
}
return text.replace(/\x1b\[[0-9;]*[mGKHFJ]/g, '');
},

/**
* Strip control characters from text (preserving newlines, carriage returns, tabs)
* @param {string} text - Text to process
* @returns {string} Text without control characters
*/
stripControlChars(text) {
if (typeof text !== 'string') {
return text;
}
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
},

/**
* Strip both ANSI codes and control characters
* @param {string} text - Text to process
* @returns {string} Cleaned text
*/
stripAll(text) {
if (typeof text !== 'string') {
return text;
}
// Preserve newlines (\n = \x0A), carriage returns (\r = \x0D), and tabs (\t = \x09)
return text.replace(
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|\x1b\[[0-9;]*[mGKHFJ]/g,
''
);
},

/**
* Clean data for processing (handles both Buffer and string)
* @param {Buffer|string} data - Data to clean
* @returns {Buffer|string} Cleaned data
*/
cleanForProcessing(data) {
if (Buffer.isBuffer(data)) {
return Buffer.from(this.stripAll(data.toString('utf8')));
}
return this.stripAll(data);
},
};

// Global ANSI configuration
let globalAnsiConfig = {
preserveAnsi: true,
preserveControlChars: true,
};

/**
* Configure global ANSI handling
* @param {object} options - Configuration options
* @param {boolean} options.preserveAnsi - Whether to preserve ANSI codes
* @param {boolean} options.preserveControlChars - Whether to preserve control chars
* @returns {object} Current configuration
*/
export function configureAnsi(options = {}) {
trace(
'AnsiUtils',
() => `configureAnsi() called | ${JSON.stringify({ options }, null, 2)}`
);
globalAnsiConfig = { ...globalAnsiConfig, ...options };
trace(
'AnsiUtils',
() => `New ANSI config | ${JSON.stringify({ globalAnsiConfig }, null, 2)}`
);
return globalAnsiConfig;
}

/**
* Get current ANSI configuration
* @returns {object} Current configuration
*/
export function getAnsiConfig() {
trace(
'AnsiUtils',
() =>
`getAnsiConfig() returning | ${JSON.stringify({ globalAnsiConfig }, null, 2)}`
);
return { ...globalAnsiConfig };
}

/**
* Reset ANSI configuration to defaults
*/
export function resetAnsiConfig() {
globalAnsiConfig = {
preserveAnsi: true,
preserveControlChars: true,
};
trace('AnsiUtils', () => 'ANSI config reset to defaults');
}

/**
* Process output data according to current ANSI configuration
* @param {Buffer|string} data - Data to process
* @param {object} options - Override options
* @returns {Buffer|string} Processed data
*/
export function processOutput(data, options = {}) {
trace(
'AnsiUtils',
() =>
`processOutput() called | ${JSON.stringify(
{
dataType: typeof data,
dataLength: Buffer.isBuffer(data) ? data.length : data?.length,
options,
},
null,
2
)}`
);
const config = { ...globalAnsiConfig, ...options };
if (!config.preserveAnsi && !config.preserveControlChars) {
return AnsiUtils.cleanForProcessing(data);
} else if (!config.preserveAnsi) {
return Buffer.isBuffer(data)
? Buffer.from(AnsiUtils.stripAnsi(data.toString('utf8')))
: AnsiUtils.stripAnsi(data);
} else if (!config.preserveControlChars) {
return Buffer.isBuffer(data)
? Buffer.from(AnsiUtils.stripControlChars(data.toString('utf8')))
: AnsiUtils.stripControlChars(data);
}
return data;
}
Loading
Loading