Files
ccs/bin/utils/helpers.js
T
Kai (Tam Nhu) TranandGitHub f40e9647ec feat(cli): comprehensive UX improvements for v3.5.0 (#7)
Implements 6-phase CLI UX improvement plan with comprehensive error handling, 
interactive features, and cross-platform consistency.

### Added
- Shell auto-completion (bash, zsh, PowerShell, Fish)
- Error codes (E101-E901) with documentation URLs
- Fuzzy matching "Did you mean?" suggestions (Levenshtein distance)
- Progress indicators (doctor [n/9] counter, GLMT proxy spinner)
- Interactive confirmation prompts with --yes/-y automation flag
- JSON output format (--json) for auth commands
- Impact display (session count, paths) before destructive operations
- Comprehensive test suite (15 tests, 100% pass rate)
- Complete error documentation in docs/errors/
- Cross-platform `--shell-completion` command

### Changed
- Error boxes: Unicode (╔═╗) → ASCII (===) for compatibility
- JSON output uses CCS version instead of schema version
- Help text includes EXAMPLES section across platforms
- Test suite properly counts test cases (not assertions)

### Fixed
- --yes flag bug (returned false instead of true)
- Help text consistency (added Uninstall section to bash)
- Test pass rate calculation (excludes skipped tests)
- Help section comparison (locale-specific sort)

### Testing
- 13/13 tests passing (2 legitimately skipped)
- Cross-platform verified (Node.js, bash, PowerShell)
- All error codes documented and tested
2025-11-15 01:26:50 -05:00

136 lines
3.6 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
// TTY-aware color detection (matches lib/ccs bash logic)
function getColors() {
const forcedColors = process.env.FORCE_COLOR;
const noColor = process.env.NO_COLOR;
const isTTY = process.stdout.isTTY === true; // Must be explicitly true
const useColors = forcedColors || (isTTY && !noColor);
if (useColors) {
return {
red: '\x1b[0;31m',
yellow: '\x1b[1;33m',
cyan: '\x1b[0;36m',
green: '\x1b[0;32m',
bold: '\x1b[1m',
reset: '\x1b[0m'
};
}
return { red: '', yellow: '', cyan: '', green: '', bold: '', reset: '' };
}
// Colors object (dynamic)
const colors = getColors();
// Helper: Apply color to text (returns plain text if colors disabled)
function colored(text, colorName = 'reset') {
const currentColors = getColors();
const color = currentColors[colorName] || '';
return color ? `${color}${text}${currentColors.reset}` : text;
}
// Simple error formatting
function error(message) {
console.error(`ERROR: ${message}`);
console.error('Try: npm install -g @kaitranntt/ccs --force');
process.exit(1);
}
// Path expansion (~ and env vars)
function expandPath(pathStr) {
// Handle tilde expansion
if (pathStr.startsWith('~/') || pathStr.startsWith('~\\')) {
pathStr = path.join(os.homedir(), pathStr.slice(2));
}
// Expand environment variables (Windows and Unix)
pathStr = pathStr.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] || '');
pathStr = pathStr.replace(/\$([A-Z_][A-Z0-9_]*)/gi, (_, name) => process.env[name] || '');
// Windows %VAR% style
if (process.platform === 'win32') {
pathStr = pathStr.replace(/%([^%]+)%/g, (_, name) => process.env[name] || '');
}
return path.normalize(pathStr);
}
/**
* Calculate Levenshtein distance between two strings
* @param {string} a - First string
* @param {string} b - Second string
* @returns {number} Edit distance
*/
function levenshteinDistance(a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix = [];
// Initialize first row and column
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill in the rest of the matrix
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1 // deletion
);
}
}
}
return matrix[b.length][a.length];
}
/**
* Find similar strings using fuzzy matching
* @param {string} target - Target string
* @param {string[]} candidates - List of candidate strings
* @param {number} maxDistance - Maximum edit distance (default: 2)
* @returns {string[]} Similar strings sorted by distance
*/
function findSimilarStrings(target, candidates, maxDistance = 2) {
const targetLower = target.toLowerCase();
const matches = candidates
.map(candidate => ({
name: candidate,
distance: levenshteinDistance(targetLower, candidate.toLowerCase())
}))
.filter(item => item.distance <= maxDistance && item.distance > 0)
.sort((a, b) => a.distance - b.distance)
.slice(0, 3) // Show at most 3 suggestions
.map(item => item.name);
return matches;
}
module.exports = {
colors,
colored,
error,
expandPath,
levenshteinDistance,
findSimilarStrings
};