diff --git a/src/commands/init.command.ts b/src/commands/init.command.ts index a81d82d..2c109aa 100644 --- a/src/commands/init.command.ts +++ b/src/commands/init.command.ts @@ -1,6 +1,7 @@ import { Command, ParsedArguments } from 'ts-commands'; import { createConfigFile } from '../config/create-config-file'; import { gitInit } from '../git/git-init'; +import { npmInit, npmInstall } from '../npm'; /** * Initialize a new SDLC project @@ -13,5 +14,10 @@ export class InitCommand extends Command { public async handle(args: ParsedArguments): Promise { await gitInit(); await createConfigFile(); + await npmInit(); + await npmInstall({ + package: 'sdlc@^1.0.0', + isDev: true, + }); } } diff --git a/src/git/git.ts b/src/git/git.ts index 0dac180..91f8283 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -1,37 +1,3 @@ -import { spawn, SpawnOptionsWithoutStdio } from 'child_process'; +import { syscall } from '../syscall'; -export async function git( - command: string, - args: string[] = [], - options: SpawnOptionsWithoutStdio = {} -): Promise { - return new Promise((resolve, reject) => { - const git = spawn('git', [command, ...args], options); - - let stdout = ''; - let stderr = ''; - - git.stdout.on('data', (data: Buffer) => { - stdout += data.toString(); - }); - - git.stderr.on('data', (data: Buffer) => { - stderr += data.toString(); - }); - - git.on('close', (code: number) => { - if (code === 0) { - resolve(stdout.trim()); - } - else { - reject( - new Error(`Git command failed with code ${code}: ${stderr}`) - ); - } - }); - - git.on('error', (error: Error) => { - reject(error); - }); - }); -} +export const git = syscall('git'); diff --git a/src/npm/index.ts b/src/npm/index.ts new file mode 100644 index 0000000..9a7cef0 --- /dev/null +++ b/src/npm/index.ts @@ -0,0 +1,3 @@ +export { npmInit } from './npm-init'; +export { npmInstall } from './npm-install'; +export { npm } from './npm'; diff --git a/src/npm/npm-init.ts b/src/npm/npm-init.ts new file mode 100644 index 0000000..db91b83 --- /dev/null +++ b/src/npm/npm-init.ts @@ -0,0 +1,5 @@ +import { npm } from './npm'; + +export async function npmInit(): Promise { + await npm('init', ['-y']); +} diff --git a/src/npm/npm-install.ts b/src/npm/npm-install.ts new file mode 100644 index 0000000..076a7be --- /dev/null +++ b/src/npm/npm-install.ts @@ -0,0 +1,14 @@ +import { npm } from './npm'; + +export async function npmInstall(options: { + package: string; + isDev?: boolean; +}): Promise { + const args = [options.package]; + + if (options.isDev) { + args.push('--save-dev'); + } + + await npm('install', args); +} diff --git a/src/npm/npm.ts b/src/npm/npm.ts new file mode 100644 index 0000000..ffa60ae --- /dev/null +++ b/src/npm/npm.ts @@ -0,0 +1,3 @@ +import { syscall } from '../syscall'; + +export const npm = syscall('npm'); diff --git a/src/syscall.ts b/src/syscall.ts new file mode 100644 index 0000000..4b85ca9 --- /dev/null +++ b/src/syscall.ts @@ -0,0 +1,39 @@ +import { execSync } from 'child_process'; + +export function syscall(command: string) { + return async function ( + subcommand: string, + args: string[] = [], + options: { cwd?: string } = {} + ): Promise { + try { + const cwd = options.cwd || process.cwd(); + args = args.map((arg) => `"${arg.replace(/"/g, '\\"')}"`); + const cmd = [command, subcommand, ...args].join(' '); + const stdout = execSync(cmd, { + cwd, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }); + return stdout.trim(); + } + catch (error) { + const exitCode = + error && typeof error === 'object' && 'status' in error + ? (error as { status?: number }).status + : undefined; + const stderr = + error && + typeof error === 'object' && + 'stderr' in error && + error.stderr + ? String((error as { stderr?: Buffer }).stderr) + : (error as Error).message; + const errMsg = + `${command} ${subcommand} failed` + + (exitCode !== undefined ? ` (exit code: ${exitCode})` : '') + + `: ${stderr}`; + throw new Error(errMsg); + } + }; +} diff --git a/test/spec/commands/init-command.spec.ts b/test/spec/commands/init-command.spec.ts index 25b6eea..1d35653 100644 --- a/test/spec/commands/init-command.spec.ts +++ b/test/spec/commands/init-command.spec.ts @@ -52,6 +52,21 @@ describe('InitCommand', () => { // Verify console output expect(mockConsole.stdout).toEqual(''); + + // Verify package.json was created and sdlc dependency added + const packageJsonPath = testDir + '/package.json'; + const packageJsonExists = + await TestUtils.assertExists(packageJsonPath); + expect(packageJsonExists).toBe(true); + + const packageJsonContent = JSON.parse( + await TestUtils.getFileContent(packageJsonPath) + ); + expect(packageJsonContent.devDependencies).toBeDefined(); + expect(packageJsonContent.devDependencies['sdlc']).toBeDefined(); + expect(packageJsonContent.devDependencies['sdlc']).toMatch( + /^\^\d+\.\d+\.\d+$/ + ); }); }); diff --git a/test/spec/git/git.spec.ts b/test/spec/git/git.spec.ts index d3840fe..a765742 100644 --- a/test/spec/git/git.spec.ts +++ b/test/spec/git/git.spec.ts @@ -10,7 +10,7 @@ describe('git', () => { await expectAsync( git('invalidcmd', [], { cwd: testDir }) ).toBeRejectedWithError( - 'Git command failed with code 1: git: \'invalidcmd\' ' + + 'git invalidcmd failed (exit code: 1): git: \'invalidcmd\' ' + 'is not a git command. See \'git --help\'.\n' ); }); diff --git a/test/spec/index.spec.ts b/test/spec/index.spec.ts index 20c1a6b..0f8529e 100644 --- a/test/spec/index.spec.ts +++ b/test/spec/index.spec.ts @@ -1,6 +1,8 @@ import 'jasmine'; import { TestUtils } from '../utilities'; +jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; + beforeAll(async () => { await TestUtils.cleanupTestDirectories(); }); diff --git a/test/spec/npm/npm-init.spec.ts b/test/spec/npm/npm-init.spec.ts new file mode 100644 index 0000000..adf515e --- /dev/null +++ b/test/spec/npm/npm-init.spec.ts @@ -0,0 +1,11 @@ +import { npmInit } from '../../../src/npm'; +import { TestUtils } from '../../utilities'; + +describe('npm init', () => { + it('can initialize an npm package', async () => { + await TestUtils.useDirectory('npm-init-basic'); + await npmInit(); + + await TestUtils.assertExists('package.json'); + }); +}); diff --git a/test/spec/npm/npm-install.spec.ts b/test/spec/npm/npm-install.spec.ts new file mode 100644 index 0000000..d4cfee6 --- /dev/null +++ b/test/spec/npm/npm-install.spec.ts @@ -0,0 +1,20 @@ +import { npmInit, npmInstall } from '../../../src/npm'; +import { TestUtils } from '../../utilities'; + +describe('npm install', () => { + it('can install a package', async () => { + await TestUtils.useDirectory('npm-install-basic'); + await npmInit(); + await npmInstall({ package: 'ts-tiny-log' }); + + await TestUtils.assertExists('node_modules/ts-tiny-log'); + }); + + it('can install dev dependencies', async () => { + await TestUtils.useDirectory('npm-install-dev'); + await npmInit(); + await npmInstall({ package: 'ts-tiny-log', isDev: true }); + + await TestUtils.assertExists('node_modules/ts-tiny-log'); + }); +}); diff --git a/test/spec/npm/npm.spec.ts b/test/spec/npm/npm.spec.ts new file mode 100644 index 0000000..2645b49 --- /dev/null +++ b/test/spec/npm/npm.spec.ts @@ -0,0 +1,14 @@ +import { TestUtils } from '../../utilities/test-utilities'; +import { npm } from '../../../src/npm'; + +describe('npm', () => { + let testDir: string; + + it('should return stdout when npm command succeeds', async () => { + testDir = await TestUtils.useDirectory('npm-status'); + const result = await npm('info', ['ts-tiny-log'], { cwd: testDir }); + + expect(typeof result).toBe('string'); + expect(result).toBeDefined(); + }); +});