simple-task-master
Version:
A simple command-line task management tool
353 lines • 11.2 kB
JavaScript
;
/**
* Utility functions for Simple Task Master
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.validatePath = validatePath;
exports.ensureDirectory = ensureDirectory;
exports.safeReadFile = safeReadFile;
exports.fileExists = fileExists;
exports.getFileSize = getFileSize;
exports.formatTimestamp = formatTimestamp;
exports.parseTimestamp = parseTimestamp;
exports.sanitizeFilename = sanitizeFilename;
exports.truncateString = truncateString;
exports.debounce = debounce;
exports.sleep = sleep;
exports.retry = retry;
exports.readStdin = readStdin;
exports.launchEditor = launchEditor;
exports.readInput = readInput;
const path = __importStar(require("path"));
const fs = __importStar(require("fs/promises"));
const errors_1 = require("./errors");
/**
* Validates that a path is safe and within allowed directories
*/
function validatePath(filePath) {
const normalized = path.normalize(filePath);
// Prevent path traversal attacks
if (normalized.includes('..')) {
throw new errors_1.ValidationError('Path contains directory traversal sequences');
}
// Ensure path is absolute or relative to current directory
if (path.isAbsolute(normalized)) {
throw new errors_1.ValidationError('Absolute paths are not allowed');
}
return normalized;
}
/**
* Ensures a directory exists, creating it if necessary
*/
async function ensureDirectory(dirPath) {
try {
await fs.access(dirPath);
}
catch (error) {
const nodeError = error;
if (nodeError.code === 'ENOENT') {
try {
await fs.mkdir(dirPath, { recursive: true });
}
catch (mkdirError) {
throw new errors_1.FileSystemError(`Failed to create directory: ${dirPath}`, mkdirError);
}
}
else {
throw new errors_1.FileSystemError(`Failed to access directory: ${dirPath}`, error);
}
}
}
/**
* Safely reads a file with error handling
*/
async function safeReadFile(filePath) {
try {
return await fs.readFile(filePath, 'utf8');
}
catch (error) {
const nodeError = error;
if (nodeError.code === 'ENOENT') {
throw new errors_1.FileSystemError(`File not found: ${filePath}`);
}
throw new errors_1.FileSystemError(`Failed to read file: ${filePath}`, error);
}
}
/**
* Checks if a file exists
*/
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
}
catch {
return false;
}
}
/**
* Gets the size of a file in bytes
*/
async function getFileSize(filePath) {
try {
const stats = await fs.stat(filePath);
return stats.size;
}
catch (error) {
throw new errors_1.FileSystemError(`Failed to get file size: ${filePath}`, error);
}
}
/**
* Formats a timestamp to ISO string
*/
function formatTimestamp(date = new Date()) {
return date.toISOString();
}
/**
* Parses an ISO timestamp string
*/
function parseTimestamp(timestamp) {
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
throw new errors_1.ValidationError(`Invalid timestamp format: ${timestamp}`);
}
return date;
}
/**
* Sanitizes a string for use as a filename
*/
function sanitizeFilename(input) {
return input
.replace(/[<>:"|?*]/g, '-')
.replace(/[\x00-\x1f]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.toLowerCase();
}
/**
* Truncates a string to a maximum length
*/
function truncateString(str, maxLength) {
if (str.length <= maxLength) {
return str;
}
return str.substring(0, maxLength - 3) + '...';
}
/**
* Debounce function to limit how often a function can be called
*/
function debounce(func, wait) {
let timeout;
return (...args) => {
const later = () => {
timeout = undefined;
func(...args);
};
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}
/**
* Sleep for a specified number of milliseconds
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Retry a function with exponential backoff
*/
async function retry(fn, maxAttempts = 3, baseDelay = 1000) {
let lastError = new Error('Retry failed');
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
}
catch (error) {
lastError = error;
if (attempt === maxAttempts) {
break;
}
const delay = baseDelay * Math.pow(2, attempt - 1);
await sleep(delay);
}
}
throw lastError;
}
/**
* Reads input from stdin with optional timeout
*/
async function readStdin(timeoutMs = 30000) {
return new Promise((resolve, reject) => {
let input = '';
let timeoutId = null;
let finished = false;
const finish = (result) => {
if (finished)
return;
finished = true;
if (timeoutId) {
clearTimeout(timeoutId);
}
// Remove all listeners to clean up
try {
process.stdin.removeAllListeners('data');
process.stdin.removeAllListeners('end');
process.stdin.removeAllListeners('error');
// Try to pause stdin to stop any pending reads
if (process.stdin.readable) {
process.stdin.pause();
}
}
catch {
// Ignore cleanup errors - they might occur if stdin is already closed
// This prevents EPIPE errors during cleanup
}
if (result instanceof Error) {
reject(result);
}
else {
resolve(result || input.trim());
}
};
// Set up timeout
timeoutId = setTimeout(() => {
finish(new Error(`Stdin input timeout after ${timeoutMs}ms`));
}, timeoutMs);
// Handle stdin data
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
input += chunk;
});
process.stdin.on('end', () => {
finish();
});
process.stdin.on('error', (error) => {
// Handle EPIPE errors gracefully - this occurs when the writing process
// has already closed the pipe but we're trying to read from it
if (error.code === 'EPIPE') {
// EPIPE on stdin read side is less common, but handle it gracefully
finish(new Error('Input stream was closed prematurely'));
}
else {
finish(new Error(`Failed to read from stdin: ${error.message}`));
}
});
// Start reading
process.stdin.resume();
});
}
/**
* Launches an external editor to get user input
*/
async function launchEditor(initialContent = '') {
const { spawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
const { writeFile, readFile, unlink } = await Promise.resolve().then(() => __importStar(require('fs/promises')));
const { join } = await Promise.resolve().then(() => __importStar(require('path')));
const { tmpdir } = await Promise.resolve().then(() => __importStar(require('os')));
// Create a temporary file
const tempFile = join(tmpdir(), `stm-edit-${Date.now()}.md`);
try {
// Write initial content to temp file
await writeFile(tempFile, initialContent, 'utf8');
// Determine editor to use
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
// Launch editor
await new Promise((resolve, reject) => {
const editorProcess = spawn(editor, [tempFile], {
stdio: 'inherit',
shell: true
});
editorProcess.on('exit', (code) => {
if (code === 0) {
resolve();
}
else {
reject(new Error(`Editor exited with code ${code}`));
}
});
editorProcess.on('error', (error) => {
reject(new Error(`Failed to launch editor: ${error.message}`));
});
});
// Read the edited content
const content = await readFile(tempFile, 'utf8');
return content.trim();
}
finally {
// Clean up temp file
try {
await unlink(tempFile);
}
catch {
// Ignore cleanup errors
}
}
}
/**
* Reads input from either a direct value, stdin (if value is "-"), or editor fallback
*/
async function readInput(value, fallbackToEditor = false, editorPrompt = '', stdinTimeoutMs = 30000) {
// If no value provided and fallback to editor is disabled, return undefined
if (value === undefined && !fallbackToEditor) {
return undefined;
}
// If value is "-", read from stdin
if (value === '-') {
try {
return await readStdin(stdinTimeoutMs);
}
catch (error) {
if (fallbackToEditor) {
// Fallback to editor if stdin fails
return await launchEditor(editorPrompt);
}
throw error;
}
}
// If value is provided and not "-", return it directly
if (value !== undefined) {
return value;
}
// If no value provided but fallback is enabled, launch editor
if (fallbackToEditor) {
return await launchEditor(editorPrompt);
}
return undefined;
}
//# sourceMappingURL=utils.js.map