Skip to content
Merged
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"ink-table": "^3.1.0",
"ink-text-input": "^6.0.0",
"ora": "^9.0.0",
"react": "^19.2.0"
"react": "^19.2.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=18"
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions specs/20251103/018-console-log-chalk-security/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
---
status: planned
status: complete
created: '2025-11-03'
tags:
- security
- refactor
priority: high
completed: '2025-11-03'
---

# console-log-chalk-security

> **Status**: 📅 Planned · **Priority**: Medium · **Created**: 2025-11-03
> **Status**: ✅ Complete · **Priority**: High · **Created**: 2025-11-03 · **Tags**: security, refactor

**Project**: lean-spec
**Team**: Core Development
Expand Down
5 changes: 3 additions & 2 deletions src/commands/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import chalk from 'chalk';
import { loadConfig } from '../config.js';
import { resolveSpecPath } from '../utils/path-helpers.js';
import { autoCheckIfEnabled } from './check.js';
import { sanitizeUserInput } from '../utils/ui.js';

export async function archiveSpec(specPath: string): Promise<void> {
// Auto-check for conflicts before archive
Expand All @@ -17,7 +18,7 @@ export async function archiveSpec(specPath: string): Promise<void> {
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);

if (!resolvedPath) {
console.error(chalk.red(`Error: Spec not found: ${specPath}`));
console.error(chalk.red(`Error: Spec not found: ${sanitizeUserInput(specPath)}`));
process.exit(1);
}

Expand All @@ -30,5 +31,5 @@ export async function archiveSpec(specPath: string): Promise<void> {

await fs.rename(resolvedPath, archivePath);

console.log(chalk.green(`✓ Archived: ${archivePath}`));
console.log(chalk.green(`✓ Archived: ${sanitizeUserInput(archivePath)}`));
}
7 changes: 4 additions & 3 deletions src/commands/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SpecInfo } from '../spec-loader.js';
import type { SpecFilterOptions, SpecStatus, SpecPriority } from '../frontmatter.js';
import { withSpinner } from '../utils/ui.js';
import { autoCheckIfEnabled } from './check.js';
import { sanitizeUserInput } from '../utils/ui.js';

const STATUS_CONFIG: Record<SpecStatus, { emoji: string; label: string; colorFn: (s: string) => string }> = {
planned: { emoji: '📅', label: 'Planned', colorFn: chalk.cyan },
Expand Down Expand Up @@ -145,20 +146,20 @@ function renderColumn(
// Build spec line with metadata
let assigneeStr = '';
if (spec.frontmatter.assignee) {
assigneeStr = ' ' + chalk.cyan(`@${spec.frontmatter.assignee}`);
assigneeStr = ' ' + chalk.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
}

let tagsStr = '';
if (spec.frontmatter.tags?.length) {
// Defensive check: ensure tags is an array
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
if (tags.length > 0) {
const tagStr = tags.map(tag => `#${tag}`).join(' ');
const tagStr = tags.map(tag => `#${sanitizeUserInput(tag)}`).join(' ');
tagsStr = ' ' + chalk.dim(chalk.magenta(tagStr));
}
}

console.log(` ${chalk.cyan(spec.path)}${assigneeStr}${tagsStr}`);
console.log(` ${chalk.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import chalk from 'chalk';
import { loadConfig } from '../config.js';
import { loadAllSpecs } from '../spec-loader.js';
import { createSpecDirPattern } from '../utils/path-helpers.js';
import { sanitizeUserInput } from '../utils/ui.js';

/**
* Check for sequence conflicts in specs
Expand Down Expand Up @@ -59,7 +60,7 @@ export async function checkSpecs(options: {
for (const [seq, paths] of conflicts) {
console.log(chalk.red(` Sequence ${String(seq).padStart(config.structure.sequenceDigits, '0')}:`));
for (const p of paths) {
console.log(chalk.gray(` - ${p}`));
console.log(chalk.gray(` - ${sanitizeUserInput(p)}`));
}
console.log('');
}
Expand Down
7 changes: 4 additions & 3 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { buildVariableContext, resolveVariables } from '../utils/variable-resolv
import type { SpecPriority } from '../frontmatter.js';
import { normalizeDateFields } from '../frontmatter.js';
import { autoCheckIfEnabled } from './check.js';
import { sanitizeUserInput } from '../utils/ui.js';

export async function createSpec(name: string, options: {
title?: string;
Expand Down Expand Up @@ -65,7 +66,7 @@ export async function createSpec(name: string, options: {
// Check if directory exists
try {
await fs.access(specDir);
console.log(chalk.yellow(`Warning: Spec already exists: ${specDir}`));
console.log(chalk.yellow(`Warning: Spec already exists: ${sanitizeUserInput(specDir)}`));
process.exit(1);
} catch {
// Directory doesn't exist, continue
Expand Down Expand Up @@ -157,8 +158,8 @@ export async function createSpec(name: string, options: {

await fs.writeFile(specFile, content, 'utf-8');

console.log(chalk.green(`✓ Created: ${specDir}/`));
console.log(chalk.gray(` Edit: ${specFile}`));
console.log(chalk.green(`✓ Created: ${sanitizeUserInput(specDir)}/`));
console.log(chalk.gray(` Edit: ${sanitizeUserInput(specFile)}`));

// Auto-check for conflicts after creation
await autoCheckIfEnabled();
Expand Down
11 changes: 6 additions & 5 deletions src/commands/deps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import chalk from 'chalk';
import { getSpec, loadAllSpecs, type SpecInfo } from '../spec-loader.js';
import { autoCheckIfEnabled } from './check.js';
import { sanitizeUserInput } from '../utils/ui.js';

export async function depsCommand(specPath: string, options: {
depth?: number;
Expand All @@ -13,7 +14,7 @@ export async function depsCommand(specPath: string, options: {
const spec = await getSpec(specPath);

if (!spec) {
console.error(chalk.red(`Error: Spec not found: ${specPath}`));
console.error(chalk.red(`Error: Spec not found: ${sanitizeUserInput(specPath)}`));
process.exit(1);
}

Expand Down Expand Up @@ -44,15 +45,15 @@ export async function depsCommand(specPath: string, options: {

// Display dependencies
console.log('');
console.log(chalk.green(`📦 Dependencies for ${chalk.cyan(spec.path)}`));
console.log(chalk.green(`📦 Dependencies for ${chalk.cyan(sanitizeUserInput(spec.path))}`));
console.log('');

// Depends On section
console.log(chalk.bold('Depends On:'));
if (dependsOn.length > 0) {
for (const dep of dependsOn) {
const status = getStatusIndicator(dep.frontmatter.status);
console.log(` → ${dep.path} ${status}`);
console.log(` → ${sanitizeUserInput(dep.path)} ${status}`);
}
} else {
console.log(chalk.gray(' (none)'));
Expand All @@ -64,7 +65,7 @@ export async function depsCommand(specPath: string, options: {
if (blocks.length > 0) {
for (const blocked of blocks) {
const status = getStatusIndicator(blocked.frontmatter.status);
console.log(` ← ${blocked.path} ${status}`);
console.log(` ← ${sanitizeUserInput(blocked.path)} ${status}`);
}
} else {
console.log(chalk.gray(' (none)'));
Expand All @@ -76,7 +77,7 @@ export async function depsCommand(specPath: string, options: {
console.log(chalk.bold('Related:'));
for (const rel of related) {
const status = getStatusIndicator(rel.frontmatter.status);
console.log(` ⟷ ${rel.path} ${status}`);
console.log(` ⟷ ${sanitizeUserInput(rel.path)} ${status}`);
}
console.log('');
}
Expand Down
11 changes: 6 additions & 5 deletions src/commands/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSpec, loadSubFiles } from '../spec-loader.js';
import { resolveSpecPath } from '../utils/path-helpers.js';
import { loadConfig } from '../config.js';
import { autoCheckIfEnabled } from './check.js';
import { sanitizeUserInput } from '../utils/ui.js';

export async function filesCommand(
specPath: string,
Expand All @@ -23,7 +24,7 @@ export async function filesCommand(
// Resolve spec path
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
if (!resolvedPath) {
console.error(chalk.red(`Spec not found: ${specPath}`));
console.error(chalk.red(`Spec not found: ${sanitizeUserInput(specPath)}`));
console.error(
chalk.gray('Try using the full path or spec name (e.g., 001-my-spec)')
);
Expand All @@ -33,15 +34,15 @@ export async function filesCommand(
// Load spec info
const spec = await getSpec(resolvedPath);
if (!spec) {
console.error(chalk.red(`Could not load spec: ${specPath}`));
console.error(chalk.red(`Could not load spec: ${sanitizeUserInput(specPath)}`));
process.exit(1);
}

// Load sub-files
const subFiles = await loadSubFiles(spec.fullPath);

console.log('');
console.log(chalk.cyan(`📄 Files in ${spec.name}`));
console.log(chalk.cyan(`📄 Files in ${sanitizeUserInput(spec.name)}`));
console.log('');

// Show README.md (required)
Expand Down Expand Up @@ -73,7 +74,7 @@ export async function filesCommand(
console.log(chalk.cyan('Documents:'));
for (const file of documents) {
const size = formatSize(file.size);
console.log(chalk.cyan(` ✓ ${file.name.padEnd(20)} (${size})`));
console.log(chalk.cyan(` ✓ ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

Calling .padEnd(20) on the sanitized filename can cause misalignment when the filename contains multi-byte unicode characters or emojis. The sanitization preserves unicode/emojis but .padEnd() counts by string length, not visual width. Consider sanitizing after padding or using a display-width-aware padding function.

Copilot uses AI. Check for mistakes.
}
console.log('');
}
Expand All @@ -82,7 +83,7 @@ export async function filesCommand(
console.log(chalk.yellow('Assets:'));
for (const file of assets) {
const size = formatSize(file.size);
console.log(chalk.yellow(` ✓ ${file.name.padEnd(20)} (${size})`));
console.log(chalk.yellow(` ✓ ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

Calling .padEnd(20) on the sanitized filename can cause misalignment when the filename contains multi-byte unicode characters or emojis. The sanitization preserves unicode/emojis but .padEnd() counts by string length, not visual width. Consider sanitizing after padding or using a display-width-aware padding function.

Copilot uses AI. Check for mistakes.
}
console.log('');
}
Expand Down
27 changes: 14 additions & 13 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { loadAllSpecs } from '../spec-loader.js';
import type { SpecStatus, SpecPriority, SpecFilterOptions } from '../frontmatter.js';
import { withSpinner } from '../utils/ui.js';
import { autoCheckIfEnabled } from './check.js';
import { sanitizeUserInput } from '../utils/ui.js';

export async function searchCommand(query: string, options: {
status?: SpecStatus;
Expand Down Expand Up @@ -78,15 +79,15 @@ export async function searchCommand(query: string, options: {
// Display results
if (results.length === 0) {
console.log('');
console.log(chalk.yellow(`🔍 No specs found matching "${query}"`));
console.log(chalk.yellow(`🔍 No specs found matching "${sanitizeUserInput(query)}"`));

// Show active filters
if (Object.keys(filter).length > 0) {
const filters: string[] = [];
if (options.status) filters.push(`status=${options.status}`);
if (options.tag) filters.push(`tag=${options.tag}`);
if (options.priority) filters.push(`priority=${options.priority}`);
if (options.assignee) filters.push(`assignee=${options.assignee}`);
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
console.log(chalk.gray(`With filters: ${filters.join(', ')}`));
}
console.log('');
Expand All @@ -95,15 +96,15 @@ export async function searchCommand(query: string, options: {

// Show summary header
console.log('');
console.log(chalk.green(`🔍 Found ${results.length} spec${results.length === 1 ? '' : 's'} matching "${query}"`));
console.log(chalk.green(`🔍 Found ${results.length} spec${results.length === 1 ? '' : 's'} matching "${sanitizeUserInput(query)}"`));

// Show active filters
if (Object.keys(filter).length > 0) {
const filters: string[] = [];
if (options.status) filters.push(`status=${options.status}`);
if (options.tag) filters.push(`tag=${options.tag}`);
if (options.priority) filters.push(`priority=${options.priority}`);
if (options.assignee) filters.push(`assignee=${options.assignee}`);
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
console.log(chalk.gray(`With filters: ${filters.join(', ')}`));
}
console.log('');
Expand All @@ -113,18 +114,18 @@ export async function searchCommand(query: string, options: {
const { spec, matches } = result;

// Spec header
console.log(chalk.cyan(`${spec.frontmatter.status === 'in-progress' ? '🔨' : spec.frontmatter.status === 'complete' ? '✅' : '📅'} ${spec.path}`));
console.log(chalk.cyan(`${spec.frontmatter.status === 'in-progress' ? '🔨' : spec.frontmatter.status === 'complete' ? '✅' : '📅'} ${sanitizeUserInput(spec.path)}`));

// Metadata
const meta: string[] = [];
if (spec.frontmatter.priority) {
const priorityEmoji = spec.frontmatter.priority === 'critical' ? '🔴' :
spec.frontmatter.priority === 'high' ? '🟡' :
spec.frontmatter.priority === 'medium' ? '🟠' : '🟢';
meta.push(`${priorityEmoji} ${spec.frontmatter.priority}`);
meta.push(`${priorityEmoji} ${sanitizeUserInput(spec.frontmatter.priority)}`);
}
if (spec.frontmatter.tags && spec.frontmatter.tags.length > 0) {
meta.push(`[${spec.frontmatter.tags.join(', ')}]`);
meta.push(`[${spec.frontmatter.tags.map(tag => sanitizeUserInput(tag)).join(', ')}]`);
}
if (meta.length > 0) {
console.log(chalk.gray(` ${meta.join(' • ')}`));
Expand Down
9 changes: 5 additions & 4 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSpecFile, updateFrontmatter } from '../frontmatter.js';
import { resolveSpecPath } from '../utils/path-helpers.js';
import type { SpecStatus, SpecPriority } from '../frontmatter.js';
import { autoCheckIfEnabled } from './check.js';
import { sanitizeUserInput } from '../utils/ui.js';

export async function updateSpec(
specPath: string,
Expand All @@ -26,15 +27,15 @@ export async function updateSpec(
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);

if (!resolvedPath) {
console.error(chalk.red(`Error: Spec not found: ${specPath}`));
console.error(chalk.gray(`Tried: ${specPath}, specs/${specPath}, and searching in date directories`));
console.error(chalk.red(`Error: Spec not found: ${sanitizeUserInput(specPath)}`));
console.error(chalk.gray(`Tried: ${sanitizeUserInput(specPath)}, specs/${sanitizeUserInput(specPath)}, and searching in date directories`));
process.exit(1);
}

// Get spec file
const specFile = await getSpecFile(resolvedPath, config.structure.defaultFile);
if (!specFile) {
console.error(chalk.red(`Error: No spec file found in: ${specPath}`));
console.error(chalk.red(`Error: No spec file found in: ${sanitizeUserInput(specPath)}`));
process.exit(1);
}

Expand All @@ -48,7 +49,7 @@ export async function updateSpec(
// Update frontmatter
await updateFrontmatter(specFile, allUpdates);

console.log(chalk.green(`✓ Updated: ${path.relative(cwd, resolvedPath)}`));
console.log(chalk.green(`✓ Updated: ${sanitizeUserInput(path.relative(cwd, resolvedPath))}`));

// Show what was updated
const updatedFields = Object.keys(updates).filter(k => k !== 'customFields');
Expand Down
Loading