Skip to content
Open
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
27 changes: 27 additions & 0 deletions bin/cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node

import { repl } from '../src/repl.mjs';
import { dev } from '../src/dev.mjs';

const command = process.argv[2];

switch (command) {
case 'repl':
await repl();
break;
case 'dev':
await dev();
break;
default:
console.log(`
command-stream - Modern shell utility library

Usage:
npx command-stream repl Start interactive REPL
npx command-stream dev Start development mode

Options:
--help, -h Show this help message
`);
process.exit(1);
}
26 changes: 26 additions & 0 deletions examples/dev-mode-demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node

// Example demonstrating the new dev mode functionality

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

console.log('🚀 Dev Mode Demo');
console.log('================\n');

console.log('1. Basic dev mode (file watching):');
console.log(' $.dev() // Starts file watching\n');

console.log('2. Dev mode with REPL:');
console.log(' $.dev({ repl: true }) // Starts interactive REPL\n');

console.log('3. Custom watch patterns:');
console.log(' $.dev({ watch: ["src/**/*.js", "test/**/*.js"] })\n');

console.log('4. CLI usage:');
console.log(' npx command-stream repl // Start REPL directly');
console.log(' npx command-stream dev // Start dev mode');

console.log('\n📝 Try these commands:');
console.log(' node examples/dev-mode-demo.mjs');
console.log(' npx command-stream repl');
console.log(' npx command-stream dev');
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime",
"type": "module",
"main": "src/$.mjs",
"bin": {
"command-stream": "./bin/cli.mjs"
},
"exports": {
".": "./src/$.mjs"
},
Expand Down Expand Up @@ -44,6 +47,7 @@
},
"files": [
"src/",
"bin/",
"README.md",
"LICENSE"
]
Expand Down
4 changes: 4 additions & 0 deletions src/$.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4379,6 +4379,10 @@ function $tagged(strings, ...values) {
return runner;
}

// Import and add dev functionality
import { dev } from './dev.mjs';
$tagged.dev = dev;

function create(defaultOptions = {}) {
trace('API', () => `create ENTER | ${JSON.stringify({ defaultOptions }, null, 2)}`);

Expand Down
117 changes: 117 additions & 0 deletions src/dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import fs from 'fs';
import path from 'path';
import { repl } from './repl.mjs';
import { $ } from './$.mjs';

const DEFAULT_WATCH_PATTERNS = [
'**/*.mjs',
'**/*.js',
'**/*.json'
];

export async function dev(options = {}) {
const {
watch = DEFAULT_WATCH_PATTERNS,
repl: startRepl = false,
cwd = process.cwd(),
verbose = false
} = options;

console.log('🚀 command-stream Development Mode');
console.log(`📁 Working directory: ${cwd}`);
console.log(`👀 Watching patterns: ${watch.join(', ')}`);

if (startRepl) {
console.log('🔧 Starting interactive REPL...\n');
return await repl();
} else {
console.log('💡 Use $.dev({ repl: true }) to start interactive mode');
console.log('⏳ Development mode active - watching for changes...\n');

// Set up file watching
const watchers = setupFileWatchers(watch, cwd, verbose);

// Keep the process running
process.on('SIGINT', () => {
console.log('\n🛑 Stopping development mode...');
watchers.forEach(watcher => watcher.close());
process.exit(0);
});

return new Promise(() => {}); // Never resolves, dev mode runs until stopped
}
}

function setupFileWatchers(patterns, cwd, verbose) {
const watchers = [];

patterns.forEach(pattern => {
try {
// Convert glob pattern to directory watching
const baseDir = getBaseDirectory(pattern);
const fullPath = path.resolve(cwd, baseDir);

if (fs.existsSync(fullPath)) {
const watcher = fs.watch(fullPath, { recursive: true }, (eventType, filename) => {
if (filename && shouldWatchFile(filename, patterns)) {
const filePath = path.join(fullPath, filename);
console.log(`📝 ${eventType}: ${path.relative(cwd, filePath)}`);

if (verbose) {
console.log(` Event: ${eventType}`);
console.log(` File: ${filePath}`);
console.log(` Time: ${new Date().toISOString()}`);
}
}
});

watchers.push(watcher);

if (verbose) {
console.log(`👁️ Watching: ${fullPath}`);
}
}
} catch (error) {
console.error(`❌ Failed to watch pattern ${pattern}:`, error.message);
}
});

return watchers;
}

function getBaseDirectory(pattern) {
// Extract the base directory from a glob pattern
const parts = pattern.split('/');
const baseIndex = parts.findIndex(part => part.includes('*'));

if (baseIndex === -1) {
return path.dirname(pattern);
}

return parts.slice(0, baseIndex).join('/') || '.';
}

function shouldWatchFile(filename, patterns) {
// Simple pattern matching - in a real implementation you'd use a proper glob library
return patterns.some(pattern => {
const regex = patternToRegex(pattern);
return regex.test(filename);
});
}

function patternToRegex(pattern) {
// Convert basic glob pattern to regex
const escaped = pattern
.replace(/\./g, '\\.')
.replace(/\*\*/g, '___DOUBLESTAR___')
.replace(/\*/g, '[^/]*')
.replace(/___DOUBLESTAR___/g, '.*');

return new RegExp(`^${escaped}$`);
}

// Add $.dev() method to the main $ object
export function addDevMethodTo$($obj) {
$obj.dev = dev;
return $obj;
}
Loading
Loading