CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-yeoman-generator

Rails-inspired generator system that provides scaffolding for your apps

Overview
Eval results
Files

command-execution.mddocs/

Command Execution

Process spawning and command execution with both synchronous and asynchronous support using execa for cross-platform compatibility.

Capabilities

Asynchronous Command Execution

Execute commands asynchronously with full control over input/output streams.

/**
 * Normalize a command across OS and spawn it (asynchronously)
 * @param command - Program to execute
 * @param options - Execa options for command execution
 * @returns ExecaChildProcess for command interaction
 */
spawnCommand(command: string, options?: ExecaOptions): ExecaChildProcess;

/**
 * Normalize a command across OS and spawn it (asynchronously) 
 * @param command - Program to execute
 * @param args - List of arguments to pass to the program
 * @param options - Execa options for command execution
 * @returns ExecaChildProcess for command interaction
 */
spawn(command: string, args?: readonly string[], options?: ExecaOptions): ExecaChildProcess;

Usage Examples:

export default class MyGenerator extends Generator {
  async install() {
    // Execute command with default options (inherits stdio)
    await this.spawnCommand('npm install');
    
    // Execute with custom options
    await this.spawnCommand('npm run build', {
      stdio: 'pipe', // Capture output
      cwd: this.destinationPath()
    });
    
    // Execute with arguments array
    await this.spawn('git', ['init'], {
      cwd: this.destinationPath()
    });
    
    // Execute with environment variables
    await this.spawn('npm', ['run', 'test'], {
      env: {
        ...process.env,
        NODE_ENV: 'test',
        CI: '1'
      }
    });
  }
  
  async writing() {
    // Conditional command execution
    if (this.answers.initGit) {
      await this.spawn('git', ['init']);
      await this.spawn('git', ['add', '.']);
      await this.spawn('git', ['commit', '-m', 'Initial commit']);
    }
    
    if (this.answers.installDeps && !this.options.skipInstall) {
      const packageManager = this.answers.packageManager || 'npm';
      await this.spawnCommand(`${packageManager} install`);
    }
  }
}

Synchronous Command Execution

Execute commands synchronously when you need to block execution.

/**
 * Normalize a command across OS and spawn it (synchronously)
 * @param command - Program to execute  
 * @param options - Execa sync options for command execution
 * @returns ExecaSyncReturnValue with command results
 */
spawnCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue;

/**
 * Normalize a command across OS and spawn it (synchronously)
 * @param command - Program to execute
 * @param args - List of arguments to pass to the program
 * @param options - Execa sync options for command execution
 * @returns ExecaSyncReturnValue with command results
 */
spawnSync(command: string, args?: readonly string[], options?: SyncOptions): ExecaSyncReturnValue;

Usage Examples:

export default class MyGenerator extends Generator {
  initializing() {
    // Check if tools are available synchronously
    try {
      const gitResult = this.spawnCommandSync('git --version', { 
        stdio: 'pipe' 
      });
      this.log(`Git available: ${gitResult.stdout}`);
    } catch (error) {
      this.log('Git not available, skipping git operations');
      this.options.skipGit = true;
    }
    
    // Get current directory info
    const lsResult = this.spawnSync('ls', ['-la'], {
      stdio: 'pipe',
      cwd: this.destinationRoot()
    });
    
    if (lsResult.stdout.includes('package.json')) {
      this.log('Existing package.json found');
    }
  }
  
  configuring() {
    // Synchronous operations for validation
    if (this.answers.framework === 'react') {
      try {
        const nodeVersion = this.spawnCommandSync('node --version', {
          stdio: 'pipe'
        });
        const version = nodeVersion.stdout.slice(1); // Remove 'v' prefix
        
        if (parseInt(version.split('.')[0]) < 16) {
          throw new Error('React requires Node.js 16 or higher');
        }
      } catch (error) {
        this.log('Warning: Could not verify Node.js version');
      }
    }
  }
}

Command Output Handling

Capture and process command output for decision making.

Usage Examples:

export default class MyGenerator extends Generator {
  async prompting() {
    // Get git user info for defaults
    let defaultAuthor = 'Anonymous';
    let defaultEmail = '';
    
    try {
      const nameResult = await this.spawn('git', ['config', 'user.name'], {
        stdio: 'pipe'
      });
      defaultAuthor = nameResult.stdout.trim();
      
      const emailResult = await this.spawn('git', ['config', 'user.email'], {
        stdio: 'pipe'
      });
      defaultEmail = emailResult.stdout.trim();
    } catch (error) {
      // Git not configured, use defaults
    }
    
    this.answers = await this.prompt([
      {
        name: 'author',
        message: 'Author name:',
        default: defaultAuthor
      },
      {
        name: 'email', 
        message: 'Author email:',
        default: defaultEmail
      }
    ]);
  }
  
  async writing() {
    // Check if dependencies are already installed
    try {
      const result = await this.spawn('npm', ['list', '--depth=0'], {
        stdio: 'pipe',
        cwd: this.destinationPath()
      });
      
      if (result.stdout.includes('express')) {
        this.log('Express already installed, skipping');
        return;
      }
    } catch (error) {
      // No package.json or no dependencies, continue
    }
    
    await this.addDependencies(['express']);
  }
}

Error Handling and Validation

Handle command failures and validate execution environments.

Usage Examples:

export default class MyGenerator extends Generator {
  async initializing() {
    // Validate required tools
    const requiredTools = [
      { cmd: 'node --version', name: 'Node.js' },
      { cmd: 'npm --version', name: 'npm' }
    ];
    
    if (this.answers.useGit) {
      requiredTools.push({ cmd: 'git --version', name: 'Git' });
    }
    
    for (const tool of requiredTools) {
      try {
        await this.spawnCommand(tool.cmd, { stdio: 'pipe' });
        this.log(`✓ ${tool.name} is available`);
      } catch (error) {
        throw new Error(`${tool.name} is required but not available`);
      }
    }
  }
  
  async install() {
    // Install with retry logic
    const maxRetries = 3;
    let retries = 0;
    
    while (retries < maxRetries) {
      try {
        await this.spawnCommand('npm install', {
          cwd: this.destinationPath()
        });
        this.log('Dependencies installed successfully');
        break;
      } catch (error) {
        retries++;
        this.log(`Install failed (attempt ${retries}/${maxRetries})`);
        
        if (retries >= maxRetries) {
          if (this.options.forceInstall) {
            throw error; // Fail hard if force-install is set
          } else {
            this.log('Skipping install due to errors. Run npm install manually.');
            break;
          }
        }
        
        // Wait before retry
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }
  }
}

Advanced Process Management

Complex command execution scenarios with streaming and process control.

Usage Examples:

export default class MyGenerator extends Generator {
  async install() {
    // Stream command output in real-time
    const child = this.spawn('npm', ['install', '--verbose'], {
      stdio: ['ignore', 'pipe', 'pipe']
    });
    
    child.stdout.on('data', (data) => {
      this.log(`npm: ${data.toString().trim()}`);
    });
    
    child.stderr.on('data', (data) => {
      this.log(`npm error: ${data.toString().trim()}`);
    });
    
    await child;
  }
  
  async writing() {
    // Parallel command execution
    const commands = [
      this.spawn('npm', ['run', 'lint']),
      this.spawn('npm', ['run', 'test']),
      this.spawn('npm', ['run', 'build'])
    ];
    
    try {
      await Promise.all(commands);
      this.log('All commands completed successfully');
    } catch (error) {
      this.log('Some commands failed, check output above');
    }
  }
  
  async end() {
    // Interactive command execution
    if (this.answers.openEditor) {
      const editor = process.env.EDITOR || 'code';
      
      await this.spawn(editor, ['.'], {
        stdio: 'inherit', // Allow user interaction
        cwd: this.destinationPath()
      });
    }
    
    // Conditional post-install commands
    if (this.answers.framework === 'react') {
      await this.spawnCommand('npm run start', {
        detached: true, // Run in background
        stdio: 'ignore'
      });
      
      this.log('Development server started at http://localhost:3000');
    }
  }
}

Platform-Specific Commands

Handle cross-platform compatibility for different operating systems.

Usage Examples:

export default class MyGenerator extends Generator {
  async writing() {
    // Platform-specific commands
    const isWindows = process.platform === 'win32';
    
    if (this.answers.createDesktopShortcut) {
      if (isWindows) {
        // Windows-specific shortcut creation
        await this.spawnCommand('powershell', [
          '-Command',
          `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("$Home\\Desktop\\${this.answers.name}.lnk"); $Shortcut.TargetPath = "${this.destinationPath('run.bat')}"; $Shortcut.Save()`
        ]);
      } else {
        // Unix-like systems
        await this.spawn('ln', ['-sf', 
          this.destinationPath('run.sh'),
          `${process.env.HOME}/Desktop/${this.answers.name}`
        ]);
      }
    }
    
    // Cross-platform file permissions
    if (!isWindows && this.answers.createExecutable) {
      await this.spawn('chmod', ['+x', this.destinationPath('bin/cli.js')]);
    }
  }
  
  async install() {
    // Platform-specific package managers
    const isWindows = process.platform === 'win32';
    const packageManager = this.answers.packageManager;
    
    const command = isWindows && packageManager === 'npm' 
      ? 'npm.cmd' 
      : packageManager;
    
    await this.spawn(command, ['install'], {
      cwd: this.destinationPath()
    });
  }
}

Types

// Execa child process (async)
interface ExecaChildProcess<StdoutStderrType = string> extends Promise<ExecaReturnValue<StdoutStderrType>> {
  pid?: number;
  stdout: NodeJS.ReadableStream;
  stderr: NodeJS.ReadableStream;
  stdin: NodeJS.WritableStream;
  kill(signal?: string): void;
}

// Execa return value (async)
interface ExecaReturnValue<StdoutStderrType = string> {
  stdout: StdoutStderrType;
  stderr: StdoutStderrType;
  exitCode: number;
  command: string;
  killed: boolean;
  signal?: string;
}

// Execa sync return value
interface ExecaSyncReturnValue<StdoutStderrType = string> {
  stdout: StdoutStderrType;
  stderr: StdoutStderrType;
  exitCode: number;
  command: string;
  killed: boolean;
  signal?: string;
}

// Execa options (async)
interface ExecaOptions<EncodingType = string> {
  cwd?: string;
  env?: Record<string, string>;
  stdio?: 'pipe' | 'inherit' | 'ignore' | readonly StdioOption[];
  timeout?: number;
  killSignal?: string;
  encoding?: EncodingType;
  shell?: boolean | string;
  windowsHide?: boolean;
}

// Execa sync options
interface SyncOptions<EncodingType = string> {
  cwd?: string;
  env?: Record<string, string>;
  stdio?: 'pipe' | 'inherit' | 'ignore' | readonly StdioOption[];
  timeout?: number;
  killSignal?: string;
  encoding?: EncodingType;
  shell?: boolean | string;
  windowsHide?: boolean;
}

Install with Tessl CLI

npx tessl i tessl/npm-yeoman-generator

docs

command-execution.md

command-line.md

configuration.md

core-generator.md

file-system.md

git-integration.md

index.md

package-management.md

task-lifecycle.md

user-interaction.md

tile.json