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
6 changes: 6 additions & 0 deletions src/commands/init.command.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,5 +14,10 @@ export class InitCommand extends Command {
public async handle(args: ParsedArguments): Promise<void> {
await gitInit();
await createConfigFile();
await npmInit();
await npmInstall({
package: 'sdlc@^1.0.0',
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version ^1.0.0 is hardcoded in the init command. This creates a maintenance burden as the version needs to be manually updated when new versions are released. Consider dynamically reading the version from the current package's package.json or using a constant that can be updated in a single location.

Copilot uses AI. Check for mistakes.
isDev: true,
});
}
}
38 changes: 2 additions & 36 deletions src/git/git.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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');
3 changes: 3 additions & 0 deletions src/npm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { npmInit } from './npm-init';
export { npmInstall } from './npm-install';
export { npm } from './npm';
5 changes: 5 additions & 0 deletions src/npm/npm-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { npm } from './npm';

export async function npmInit(): Promise<void> {
await npm('init', ['-y']);
}
14 changes: 14 additions & 0 deletions src/npm/npm-install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { npm } from './npm';

export async function npmInstall(options: {
package: string;
isDev?: boolean;
}): Promise<void> {
const args = [options.package];

if (options.isDev) {
args.push('--save-dev');
}

await npm('install', args);
}
3 changes: 3 additions & 0 deletions src/npm/npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { syscall } from '../syscall';

export const npm = syscall('npm');
39 changes: 39 additions & 0 deletions src/syscall.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
try {
const cwd = options.cwd || process.cwd();
args = args.map((arg) => `"${arg.replace(/"/g, '\\"')}"`);
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguments are wrapped in double quotes and escaped, but this may not handle all edge cases for shell command injection. For example, backticks, dollar signs, and other shell metacharacters could still pose security risks. Consider using an array-based execution approach or a more robust escaping library to prevent command injection vulnerabilities.

Copilot uses AI. Check for mistakes.
const cmd = [command, subcommand, ...args].join(' ');
Comment on lines +11 to +12
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguments are being joined into a single string for shell execution, but execSync with a string command uses shell parsing which can be problematic. The original git implementation used spawn with an array of arguments, which is safer. Consider using execFileSync(command, [subcommand, ...args], options) instead to avoid shell interpretation and maintain the safety of the original implementation.

Copilot uses AI. Check for mistakes.
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);
}
};
}
15 changes: 15 additions & 0 deletions test/spec/commands/init-command.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+$/
);
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/spec/git/git.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
});
Expand Down
2 changes: 2 additions & 0 deletions test/spec/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'jasmine';
import { TestUtils } from '../utilities';

jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

beforeAll(async () => {
await TestUtils.cleanupTestDirectories();
});
11 changes: 11 additions & 0 deletions test/spec/npm/npm-init.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
20 changes: 20 additions & 0 deletions test/spec/npm/npm-install.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
14 changes: 14 additions & 0 deletions test/spec/npm/npm.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});