diff --git a/scripts/skill-create-output.js b/scripts/skill-create-output.js index 718fae6..6e0bea6 100644 --- a/scripts/skill-create-output.js +++ b/scripts/skill-create-output.js @@ -91,7 +91,7 @@ class SkillCreateOutput { console.log('\n'); console.log(chalk.bold(chalk.magenta('╔════════════════════════════════════════════════════════════════╗'))); console.log(chalk.bold(chalk.magenta('║')) + chalk.bold(' 🔮 ECC Skill Creator ') + chalk.bold(chalk.magenta('║'))); - console.log(chalk.bold(chalk.magenta('║')) + ` ${subtitle}${' '.repeat(Math.max(0, 55 - stripAnsi(subtitle).length))}` + chalk.bold(chalk.magenta('║'))); + console.log(chalk.bold(chalk.magenta('║')) + ` ${subtitle}${' '.repeat(Math.max(0, 59 - stripAnsi(subtitle).length))}` + chalk.bold(chalk.magenta('║'))); console.log(chalk.bold(chalk.magenta('╚════════════════════════════════════════════════════════════════╝'))); console.log(''); } diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index 765d2e9..39cdfb8 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -1093,6 +1093,77 @@ function runTests() { } })) passed++; else failed++; + // ── Round 34: getExecCommand non-string args & packageManager type ── + console.log('\nRound 34: getExecCommand non-string args:'); + + if (test('getExecCommand with args=0 produces command without extra args', () => { + const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; + try { + process.env.CLAUDE_PACKAGE_MANAGER = 'npm'; + const cmd = pm.getExecCommand('prettier', 0); + // 0 is falsy, so ternary `args ? ' ' + args : ''` yields '' + assert.ok(!cmd.includes(' 0'), 'Should not append 0 as args'); + assert.ok(cmd.includes('prettier'), 'Should include binary name'); + } finally { + if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv; + else delete process.env.CLAUDE_PACKAGE_MANAGER; + } + })) passed++; else failed++; + + if (test('getExecCommand with args=false produces command without extra args', () => { + const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; + try { + process.env.CLAUDE_PACKAGE_MANAGER = 'npm'; + const cmd = pm.getExecCommand('eslint', false); + assert.ok(!cmd.includes('false'), 'Should not append false as args'); + assert.ok(cmd.includes('eslint'), 'Should include binary name'); + } finally { + if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv; + else delete process.env.CLAUDE_PACKAGE_MANAGER; + } + })) passed++; else failed++; + + if (test('getExecCommand with args=null produces command without extra args', () => { + const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; + try { + process.env.CLAUDE_PACKAGE_MANAGER = 'npm'; + const cmd = pm.getExecCommand('tsc', null); + assert.ok(!cmd.includes('null'), 'Should not append null as args'); + assert.ok(cmd.includes('tsc'), 'Should include binary name'); + } finally { + if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv; + else delete process.env.CLAUDE_PACKAGE_MANAGER; + } + })) passed++; else failed++; + + console.log('\nRound 34: detectFromPackageJson with non-string packageManager:'); + + if (test('detectFromPackageJson handles array packageManager field gracefully', () => { + const tmpDir = createTestDir(); + try { + // Write a malformed package.json with array instead of string + fs.writeFileSync(path.join(tmpDir, 'package.json'), + JSON.stringify({ packageManager: ['pnpm@8', 'yarn@3'] })); + // Should not crash — try/catch in detectFromPackageJson catches TypeError + const result = pm.getPackageManager({ projectDir: tmpDir }); + assert.ok(result.name, 'Should fallback to a valid package manager'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + + if (test('detectFromPackageJson handles numeric packageManager field gracefully', () => { + const tmpDir = createTestDir(); + try { + fs.writeFileSync(path.join(tmpDir, 'package.json'), + JSON.stringify({ packageManager: 42 })); + const result = pm.getPackageManager({ projectDir: tmpDir }); + assert.ok(result.name, 'Should fallback to a valid package manager'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`); diff --git a/tests/scripts/skill-create-output.test.js b/tests/scripts/skill-create-output.test.js index 597900f..77f92c5 100644 --- a/tests/scripts/skill-create-output.test.js +++ b/tests/scripts/skill-create-output.test.js @@ -319,6 +319,61 @@ function runTests() { assert.ok(combined.includes('Powered by'), 'Should include attribution text'); })) passed++; else failed++; + // ── Round 34: header width alignment ── + console.log('\nheader() width alignment (Round 34):'); + + if (test('header subtitle line matches border width', () => { + const output = new SkillCreateOutput('test-repo'); + const logs = captureLog(() => output.header()); + // Find the border and subtitle lines + const lines = logs.map(l => stripAnsi(l)); + const borderLine = lines.find(l => l.includes('═══')); + const subtitleLine = lines.find(l => l.includes('Extracting patterns')); + assert.ok(borderLine, 'Should find border line'); + assert.ok(subtitleLine, 'Should find subtitle line'); + // Both lines should have the same visible width + assert.strictEqual(subtitleLine.length, borderLine.length, + `Subtitle width (${subtitleLine.length}) should match border width (${borderLine.length})`); + })) passed++; else failed++; + + if (test('header all lines have consistent width for short repo name', () => { + const output = new SkillCreateOutput('abc'); + const logs = captureLog(() => output.header()); + const lines = logs.map(l => stripAnsi(l)).filter(l => l.includes('║') || l.includes('╔') || l.includes('╚')); + assert.ok(lines.length >= 4, 'Should have at least 4 box lines'); + const widths = lines.map(l => l.length); + const first = widths[0]; + widths.forEach((w, i) => { + assert.strictEqual(w, first, + `Line ${i} width (${w}) should match first line (${first})`); + }); + })) passed++; else failed++; + + if (test('header subtitle has correct content area width of 64 chars', () => { + const output = new SkillCreateOutput('myrepo'); + const logs = captureLog(() => output.header()); + const lines = logs.map(l => stripAnsi(l)); + const subtitleLine = lines.find(l => l.includes('Extracting patterns')); + assert.ok(subtitleLine, 'Should find subtitle line'); + // Content between ║ and ║ should be 64 chars (border is 66 total) + // Format: ║ + content(64) + ║ = 66 + assert.strictEqual(subtitleLine.length, 66, + `Total subtitle line width should be 66, got ${subtitleLine.length}`); + })) passed++; else failed++; + + if (test('header subtitle line does not truncate with medium-length repo name', () => { + const output = new SkillCreateOutput('my-medium-repo-name'); + const logs = captureLog(() => output.header()); + const combined = logs.join('\n'); + assert.ok(combined.includes('my-medium-repo-name'), 'Should include full repo name'); + const lines = logs.map(l => stripAnsi(l)); + const subtitleLine = lines.find(l => l.includes('Extracting patterns')); + assert.ok(subtitleLine, 'Should have subtitle line'); + // Should still be 66 chars even with a longer name + assert.strictEqual(subtitleLine.length, 66, + `Subtitle line should be 66 chars, got ${subtitleLine.length}`); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);