All files / apps/cli Cli.ts

88.88% Statements 24/27
57.14% Branches 8/14
100% Functions 6/6
88.88% Lines 24/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153                                    3x       3x 3x       4x 4x   4x                                                   4x                                                                             3x 3x                                 3x       3x             3x 3x 3x 1x   2x 1x     3x         3x       4x       4x 4x 4x   4x    
import 'reflect-metadata';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Command } from 'commander';
import type { Container } from 'inversify';
import pc from 'picocolors';
import { DispatchActionCommand } from '../../envilder/application/dispatch/DispatchActionCommand.js';
import type { DispatchActionCommandHandler } from '../../envilder/application/dispatch/DispatchActionCommandHandler.js';
import type { CliOptions } from '../../envilder/domain/CliOptions.js';
import type { MapFileConfig } from '../../envilder/domain/MapFileConfig.js';
import { PackageVersionReader } from '../../envilder/infrastructure/package/PackageVersionReader.js';
import { readMapFileConfig } from '../../envilder/infrastructure/variableStore/FileVariableStore.js';
import { TYPES } from '../../envilder/types.js';
import { Startup } from './Startup.js';
 
let serviceProvider: Container;
 
async function executeCommand(options: CliOptions): Promise<void> {
  const commandHandler = serviceProvider.get<DispatchActionCommandHandler>(
    TYPES.DispatchActionCommandHandler,
  );
 
  const command = DispatchActionCommand.fromCliOptions(options);
  await commandHandler.handleCommand(command);
}
 
export async function main() {
  const program = new Command();
  const version = await readPackageVersion();
 
  const banner = `
  ${pc.green('███████╗')}${pc.cyan('███╗   ██╗')}${pc.magenta('██╗   ██╗')}${pc.yellow('██╗')}${pc.red('██╗     ')}${pc.blue('██████╗ ')}${pc.green('███████╗')}${pc.cyan('██████╗ ')}
  ${pc.green('██╔════╝')}${pc.cyan('████╗  ██║')}${pc.magenta('██║   ██║')}${pc.yellow('██║')}${pc.red('██║     ')}${pc.blue('██╔══██╗')}${pc.green('██╔════╝')}${pc.cyan('██╔══██╗')}
  ${pc.green('█████╗  ')}${pc.cyan('██╔██╗ ██║')}${pc.magenta('██║   ██║')}${pc.yellow('██║')}${pc.red('██║     ')}${pc.blue('██║  ██║')}${pc.green('█████╗  ')}${pc.cyan('██████╔╝')}
  ${pc.green('██╔══╝  ')}${pc.cyan('██║╚██╗██║')}${pc.magenta('╚██╗ ██╔╝')}${pc.yellow('██║')}${pc.red('██║     ')}${pc.blue('██║  ██║')}${pc.green('██╔══╝  ')}${pc.cyan('██╔══██╗')}
  ${pc.green('███████╗')}${pc.cyan('██║ ╚████║')}${pc.magenta(' ╚████╔╝ ')}${pc.yellow('██║')}${pc.red('███████╗')}${pc.blue('██████╔╝')}${pc.green('███████╗')}${pc.cyan('██║  ██║')}
  ${pc.green('╚══════╝')}${pc.cyan('╚═╝  ╚═══╝')}${pc.magenta('  ╚═══╝  ')}${pc.yellow('╚═╝')}${pc.red('╚══════╝')}${pc.blue('╚═════╝ ')}${pc.green('╚══════╝')}${pc.cyan('╚═╝  ╚═╝')}
  ${pc.dim('Your secrets, one command away')}          ${pc.dim('aws & azure')}
 
  ${pc.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}
  ${pc.green('WORLD 1-1')} ${pc.dim('— SELECT YOUR MISSION')}
  ${pc.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}
 
  ${pc.green('>')} ${pc.bold('Generate a .env file')}  ${pc.dim('(pull secrets from the cloud)')}
    ${pc.cyan('envilder --map=param-map.json --envfile=.env')}
 
  ${pc.magenta('>')} ${pc.bold('Sync .env back to cloud')}  ${pc.dim('(push secrets up)')}
    ${pc.cyan('envilder --push --map=param-map.json --envfile=.env')}
 
  ${pc.red('>')} ${pc.bold('Push a single secret')}
    ${pc.cyan('envilder --push --key=API_KEY --value=s3cret --secret-path=/my/path')}
 
  ${pc.blue('>')} ${pc.bold('Use Azure Key Vault')}
    ${pc.cyan('envilder --provider=azure --map=param-map.json --envfile=.env')}
`;
 
  program
    .name('envilder')
    .description(banner)
    .version(version)
    .option(
      '--map <path>',
      'Path to the JSON file with environment variable mapping (required for most commands)',
    )
    .option(
      '--envfile <path>',
      'Path to the .env file to be generated or imported (required for most commands)',
    )
    .option('--profile <name>', 'AWS CLI profile to use (optional)')
    .option(
      '--provider <name>',
      'Cloud provider to use: aws or azure (default: aws)',
    )
    .option(
      '--vault-url <url>',
      'Azure Key Vault URL (overrides $config.vaultUrl in map file)',
    )
    .option('--push', 'Push local .env file back to cloud provider')
    .option(
      '--key <name>',
      'Single environment variable name to push (only with --push)',
    )
    .option(
      '--value <value>',
      'Value of the single environment variable to push (only with --push)',
    )
    .option(
      '--secret-path <path>',
      'Secret path in your cloud provider for the single variable (only with --push)',
    )
    .option(
      '--ssm-path <path>',
      '[DEPRECATED: use --secret-path] Alias for --secret-path',
    )
    .hook('preAction', (thisCommand) => {
      const opts = thisCommand.opts();
      Iif (opts.ssmPath) {
        console.warn(
          pc.yellow(
            '⚠️  --ssm-path is deprecated and will be removed in a future release. Use --secret-path instead.',
          ),
        );
        if (!opts.secretPath) {
          thisCommand.setOptionValue('secretPath', opts.ssmPath);
        }
      }
    })
    .action(
      async ({
        provider,
        vaultUrl,
        ...options
      }: CliOptions & { provider?: string; vaultUrl?: string }) => {
        const fileConfig = options.map
          ? await readMapFileConfig(options.map)
          : {};
 
        const config: MapFileConfig = {
          ...fileConfig,
          ...(provider && { provider }),
          ...(vaultUrl && { vaultUrl }),
          ...(options.profile && { profile: options.profile }),
        };
 
        const infraOptions: Record<string, unknown> = {};
        const extraHosts = process.env.ENVILDER_ALLOWED_VAULT_HOSTS;
        if (extraHosts) {
          infraOptions.allowedVaultHosts = extraHosts
            .split(',')
            .map((h) => h.trim());
          infraOptions.disableChallengeResourceVerification = true;
        }
 
        serviceProvider = Startup.build()
          .configureServices()
          .configureInfrastructure(config, infraOptions)
          .create();
 
        await executeCommand(options);
      },
    );
 
  await program.parseAsync(process.argv);
}
 
function readPackageVersion(): Promise<string> {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = dirname(__filename);
  const packageJsonPath = join(__dirname, '../../../package.json');
 
  return new PackageVersionReader().getVersion(packageJsonPath);
}