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
2 changes: 1 addition & 1 deletion src/cli/commands/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export function formatTemplateResult(result: TemplateCommandResult, json: boolea
lines.push(`✗ ${result.error}`);

if (result.error?.includes('already exists')) {
lines.push(' Use --force to overwrite, or delete the file first.');
lines.push(' Use -force to overwrite, or delete the file first.');
}

return lines.join('\n');
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/env/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export async function executeEnvInit(options: EnvInitOptions): Promise<EnvInitRe
if (existing && !force) {
return {
success: false,
error: `Contract already exists for ${envType}. Use --force to overwrite.`,
error: `Contract already exists for ${envType}. Use -force to overwrite.`,
overwritten: false,
};
}
Expand Down
1 change: 0 additions & 1 deletion src/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ export function formatInitResult(result: InitCommandResult, json: boolean): stri

lines.push('', 'Next steps:');
lines.push(' switchyard new <feature-name> # Create your first lane');
lines.push(' /switchyard-new <feature-name> # Or use the Claude command');

return lines.join('\n');
}
2 changes: 1 addition & 1 deletion src/cli/commands/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export async function executeResume(options: ResumeCommandOptions): Promise<Resu
if (state.status === 'blocked') {
if (!force) {
throw new SwitchyardError(
`Lane is blocked: ${state.blockedReason}. Use --force to clear and resume.`,
`Lane is blocked: ${state.blockedReason}. Use -force to clear and resume.`,
ExitCode.LANE_BLOCKED
);
}
Expand Down
Empty file modified src/cli/index.ts
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion src/core/checkpoint/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ export function getCriteriaSatisfiedFormatHint(): string {
Example:
## Criteria Satisfied
- SC-001: Added unit tests for template generator
- SC-002: Implemented CLI --template flag
- SC-002: Implemented CLI -template flag

Use criterion IDs matching your specification (e.g., SC-001, REQ-001).`;
}
Expand Down
30 changes: 20 additions & 10 deletions src/core/lane/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,32 @@ async function getTemplateContent(
return content;
}

/**
* Get the switchyard package root directory
*
* Uses import.meta.dir to find the package location, regardless of
* where switchyard is installed (globally, locally, or in development).
*/
function getSwitchyardPackageRoot(): string {
// import.meta.dir gives us the directory of this file (src/core/lane/)
// Navigate up to the package root (src/core/lane -> src/core -> src -> root)
return resolve(import.meta.dir, '..', '..', '..');
}

/**
* Copy command templates to lane worktree
*
* Copies switchyard.*.md files from src/templates/claude-commands/ to
* the lane's .claude/commands/ directory. Preserves existing files
* that are not switchyard templates.
* Copies switchyard.*.md files from the switchyard package's
* src/templates/claude-commands/ to the lane's .claude/commands/ directory.
* Preserves existing files that are not switchyard templates.
*
* @param worktreePath Lane worktree path
* @param gitRoot Git repository root (where templates are located)
* @returns Copy result with list of copied and skipped files
*/
export async function copyCommandTemplates(
worktreePath: string,
gitRoot: string
): Promise<CopyTemplatesResult> {
const sourceDir = join(gitRoot, 'src', 'templates', 'claude-commands');
export async function copyCommandTemplates(worktreePath: string): Promise<CopyTemplatesResult> {
// Get templates from switchyard package location, not user's project
const packageRoot = getSwitchyardPackageRoot();
const sourceDir = join(packageRoot, 'src', 'templates', 'claude-commands');
const destDir = join(worktreePath, '.claude', 'commands');

const copied: string[] = [];
Expand Down Expand Up @@ -279,7 +289,7 @@ export async function createLane(
});

// Copy Claude command templates to .claude/commands/
const templateResult = await copyCommandTemplates(worktreePath, gitRoot);
const templateResult = await copyCommandTemplates(worktreePath);

// Install dependencies
const dependencyInstallResult = await installDependencies({ worktreePath });
Expand Down
6 changes: 3 additions & 3 deletions src/core/lane/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export async function deleteLane(
if (orphanInfo.hasWorktree && (await isLaneRunningOrLocked(lanePath))) {
if (!options.force) {
throw new SwitchyardError(
`Lane ${laneId} is running or locked. Use --force to delete anyway.`,
`Lane ${laneId} is running or locked. Use -force to delete anyway.`,
13 // EXIT_LANE_RUNNING_LOCKED
);
}
Expand All @@ -271,7 +271,7 @@ export async function deleteLane(
if (orphanInfo.hasWorktree && (await hasUncommittedChanges(lanePath))) {
if (!options.force) {
throw new SwitchyardError(
`Lane ${laneId} has uncommitted changes. Use --force to delete anyway.`,
`Lane ${laneId} has uncommitted changes. Use -force to delete anyway.`,
12 // EXIT_UNCOMMITTED_CHANGES
);
}
Expand All @@ -283,7 +283,7 @@ export async function deleteLane(
try {
archivePath = await archiveLaneState(lanePath, laneId, gitRoot);
} catch (error) {
// Archive failure should not block deletion with --force
// Archive failure should not block deletion with -force
if (options.force) {
warnings.push(`Failed to archive lane state: ${error}`);
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/core/lane/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,11 @@ export async function mergeLane(
): Promise<MergeLaneResult> {
const laneBranch = `lane/${laneId}`;

// Check if lane is in 'done' status (unless --force)
// Check if lane is in 'done' status (unless -force)
const status = await checkLaneStatus(lanePath);
if (!options.force && status !== 'done') {
throw new SwitchyardError(
`Lane ${laneId} is not in 'done' status (current: ${status ?? 'unknown'}). Use --force to merge anyway.`,
`Lane ${laneId} is not in 'done' status (current: ${status ?? 'unknown'}). Use -force to merge anyway.`,
14 // EXIT_LANE_NOT_DONE
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/lane/reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export async function resetLane(
laneId,
previousStatus,
newStatus: previousStatus,
error: 'Cannot reset a done lane. Use --force to override.',
error: 'Cannot reset a done lane. Use -force to override.',
};
}

Expand Down
126 changes: 50 additions & 76 deletions tests/unit/lane/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,30 @@

import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
import { mkdtemp, rm, mkdir, writeFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { copyCommandTemplates } from '../../../src/core/lane/create.ts';

// Get expected templates from the actual package location
const packageRoot = resolve(
import.meta.dir,
'..',
'..',
'..',
'src',
'templates',
'claude-commands'
);

describe('copyCommandTemplates', () => {
let tempDir: string;
let gitRoot: string;
let worktreePath: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'lane-create-test-'));
gitRoot = join(tempDir, 'repo');
worktreePath = join(tempDir, 'worktree');

// Create directory structure
await mkdir(gitRoot, { recursive: true });
await mkdir(worktreePath, { recursive: true });
});

Expand All @@ -30,110 +38,76 @@ describe('copyCommandTemplates', () => {
});

test('creates .claude/commands/ directory if it does not exist', async () => {
// Create source templates
const srcDir = join(gitRoot, 'src', 'templates', 'claude-commands');
await mkdir(srcDir, { recursive: true });
await writeFile(join(srcDir, 'switchyard.test.md'), '# Test Command');

const result = await copyCommandTemplates(worktreePath, gitRoot);
const result = await copyCommandTemplates(worktreePath);

expect(result.destinationDir).toBe(join(worktreePath, '.claude', 'commands'));
const files = await readdir(join(worktreePath, '.claude', 'commands'));
expect(files).toContain('switchyard.test.md');
// Should have copied at least some switchyard templates
expect(files.length).toBeGreaterThan(0);
expect(files.some((f) => f.startsWith('switchyard.'))).toBe(true);
});

test('copies all switchyard.*.md templates', async () => {
// Create source templates
const srcDir = join(gitRoot, 'src', 'templates', 'claude-commands');
await mkdir(srcDir, { recursive: true });
await writeFile(join(srcDir, 'switchyard.run.md'), '# Run Command');
await writeFile(join(srcDir, 'switchyard.status.md'), '# Status Command');
await writeFile(join(srcDir, 'switchyard.new.md'), '# New Command');
test('copies switchyard.*.md templates from package', async () => {
const result = await copyCommandTemplates(worktreePath);

const result = await copyCommandTemplates(worktreePath, gitRoot);

expect(result.copied).toHaveLength(3);
expect(result.copied).toContain('switchyard.run.md');
expect(result.copied).toContain('switchyard.status.md');
expect(result.copied).toContain('switchyard.new.md');
// Should copy the real templates from the package
expect(result.copied.length).toBeGreaterThan(0);
expect(result.copied.every((f) => f.startsWith('switchyard.') && f.endsWith('.md'))).toBe(true);
expect(result.skipped).toHaveLength(0);
});

test('ignores non-switchyard files in templates directory', async () => {
// Create source templates with non-switchyard files
const srcDir = join(gitRoot, 'src', 'templates', 'claude-commands');
await mkdir(srcDir, { recursive: true });
await writeFile(join(srcDir, 'switchyard.run.md'), '# Run Command');
await writeFile(join(srcDir, 'other-command.md'), '# Other Command');
await writeFile(join(srcDir, 'README.md'), '# Readme');

const result = await copyCommandTemplates(worktreePath, gitRoot);

expect(result.copied).toHaveLength(1);
// Verify expected templates were copied
expect(result.copied).toContain('switchyard.run.md');

// Verify non-switchyard files were not copied
const files = await readdir(join(worktreePath, '.claude', 'commands'));
expect(files).not.toContain('other-command.md');
expect(files).not.toContain('README.md');
expect(result.copied).toContain('switchyard.status.md');
});

test('preserves existing non-switchyard files in destination', async () => {
// Create source templates
const srcDir = join(gitRoot, 'src', 'templates', 'claude-commands');
await mkdir(srcDir, { recursive: true });
await writeFile(join(srcDir, 'switchyard.run.md'), '# Run Command');

// Create existing file in destination
const destDir = join(worktreePath, '.claude', 'commands');
await mkdir(destDir, { recursive: true });
await writeFile(join(destDir, 'my-custom-command.md'), '# My Custom Command');

await copyCommandTemplates(worktreePath, gitRoot);
await copyCommandTemplates(worktreePath);

// Verify existing file is preserved
const files = await readdir(destDir);
expect(files).toContain('my-custom-command.md');
expect(files).toContain('switchyard.run.md');
});

test('returns empty arrays when source directory does not exist', async () => {
// Don't create source templates directory

const result = await copyCommandTemplates(worktreePath, gitRoot);

expect(result.copied).toHaveLength(0);
expect(result.skipped).toHaveLength(0);
});

test('returns empty arrays when source directory has no switchyard templates', async () => {
// Create source directory with only non-switchyard files
const srcDir = join(gitRoot, 'src', 'templates', 'claude-commands');
await mkdir(srcDir, { recursive: true });
await writeFile(join(srcDir, 'other.md'), '# Other');

const result = await copyCommandTemplates(worktreePath, gitRoot);

expect(result.copied).toHaveLength(0);
// Also verify switchyard templates were copied
expect(files.some((f) => f.startsWith('switchyard.'))).toBe(true);
});

test('overwrites existing switchyard templates in destination', async () => {
// Create source templates
const srcDir = join(gitRoot, 'src', 'templates', 'claude-commands');
await mkdir(srcDir, { recursive: true });
await writeFile(join(srcDir, 'switchyard.run.md'), '# New Run Command');

// Create existing switchyard template in destination
const destDir = join(worktreePath, '.claude', 'commands');
await mkdir(destDir, { recursive: true });
await writeFile(join(destDir, 'switchyard.run.md'), '# Old Run Command');

const result = await copyCommandTemplates(worktreePath, gitRoot);
const result = await copyCommandTemplates(worktreePath);

expect(result.copied).toContain('switchyard.run.md');

// Verify content was updated
// Verify content was updated (should not be our old content)
const content = await Bun.file(join(destDir, 'switchyard.run.md')).text();
expect(content).toBe('# New Run Command');
expect(content).not.toBe('# Old Run Command');
expect(content).toContain('switchyard'); // Should contain real template content
});

test('returns correct destination directory', async () => {
const result = await copyCommandTemplates(worktreePath);

expect(result.destinationDir).toBe(join(worktreePath, '.claude', 'commands'));
});
});

describe('copyCommandTemplates integration', () => {
test('package templates directory exists and contains expected files', async () => {
// Verify the templates exist in the package
const files = await readdir(packageRoot);
const switchyardTemplates = files.filter(
(f) => f.startsWith('switchyard.') && f.endsWith('.md')
);

expect(switchyardTemplates.length).toBeGreaterThan(0);
expect(switchyardTemplates).toContain('switchyard.run.md');
expect(switchyardTemplates).toContain('switchyard.status.md');
});
});
Loading