github-issue-136-takt-catalog (#146)
* planner と architect-planner を統合し、knowledge で設計知識を補完する構成に変更 plan → architect → implement の3ステップを plan → implement の2ステップに統合。 planner ペルソナに構造設計・モジュール設計の知識を追加し、plan ステップに knowledge: architecture を付与することで architect ステップを不要にした。 prompt-log-viewer ツールを追加。 * takt: github-issue-136-takt-catalog
This commit is contained in:
parent
f3b8c772cb
commit
85271075a2
373
src/__tests__/catalog.test.ts
Normal file
373
src/__tests__/catalog.test.ts
Normal file
@ -0,0 +1,373 @@
|
||||
/**
|
||||
* Tests for facet catalog scanning and display.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import {
|
||||
extractDescription,
|
||||
parseFacetType,
|
||||
scanFacets,
|
||||
displayFacets,
|
||||
showCatalog,
|
||||
type FacetEntry,
|
||||
} from '../features/catalog/catalogFacets.js';
|
||||
|
||||
// Mock external dependencies to isolate unit tests
|
||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
getLanguage: () => 'en',
|
||||
getBuiltinPiecesEnabled: () => true,
|
||||
}));
|
||||
|
||||
const mockLogError = vi.fn();
|
||||
const mockInfo = vi.fn();
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
error: (...args: unknown[]) => mockLogError(...args),
|
||||
info: (...args: unknown[]) => mockInfo(...args),
|
||||
section: (title: string) => console.log(title),
|
||||
}));
|
||||
|
||||
let mockBuiltinDir: string;
|
||||
vi.mock('../infra/resources/index.js', () => ({
|
||||
getLanguageResourcesDir: () => mockBuiltinDir,
|
||||
}));
|
||||
|
||||
let mockGlobalDir: string;
|
||||
vi.mock('../infra/config/paths.js', () => ({
|
||||
getGlobalConfigDir: () => mockGlobalDir,
|
||||
getProjectConfigDir: (cwd: string) => join(cwd, '.takt'),
|
||||
}));
|
||||
|
||||
describe('parseFacetType', () => {
|
||||
it('should return FacetType for valid inputs', () => {
|
||||
expect(parseFacetType('personas')).toBe('personas');
|
||||
expect(parseFacetType('policies')).toBe('policies');
|
||||
expect(parseFacetType('knowledge')).toBe('knowledge');
|
||||
expect(parseFacetType('instructions')).toBe('instructions');
|
||||
expect(parseFacetType('output-contracts')).toBe('output-contracts');
|
||||
});
|
||||
|
||||
it('should return null for invalid inputs', () => {
|
||||
expect(parseFacetType('unknown')).toBeNull();
|
||||
expect(parseFacetType('persona')).toBeNull();
|
||||
expect(parseFacetType('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractDescription', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'takt-catalog-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should extract first heading from markdown file', () => {
|
||||
const filePath = join(tempDir, 'test.md');
|
||||
writeFileSync(filePath, '# My Persona\n\nSome content here.');
|
||||
|
||||
expect(extractDescription(filePath)).toBe('My Persona');
|
||||
});
|
||||
|
||||
it('should return first non-empty line when no heading exists', () => {
|
||||
const filePath = join(tempDir, 'test.md');
|
||||
writeFileSync(filePath, 'No heading in this file\nJust plain text.');
|
||||
|
||||
expect(extractDescription(filePath)).toBe('No heading in this file');
|
||||
});
|
||||
|
||||
it('should return empty string when file is empty', () => {
|
||||
const filePath = join(tempDir, 'test.md');
|
||||
writeFileSync(filePath, '');
|
||||
|
||||
expect(extractDescription(filePath)).toBe('');
|
||||
});
|
||||
|
||||
it('should skip blank lines and return first non-empty line', () => {
|
||||
const filePath = join(tempDir, 'test.md');
|
||||
writeFileSync(filePath, '\n\n \nActual content here\nMore text.');
|
||||
|
||||
expect(extractDescription(filePath)).toBe('Actual content here');
|
||||
});
|
||||
|
||||
it('should extract from first heading, ignoring later headings', () => {
|
||||
const filePath = join(tempDir, 'test.md');
|
||||
writeFileSync(filePath, 'Preamble\n# First Heading\n# Second Heading');
|
||||
|
||||
expect(extractDescription(filePath)).toBe('First Heading');
|
||||
});
|
||||
|
||||
it('should trim whitespace from heading text', () => {
|
||||
const filePath = join(tempDir, 'test.md');
|
||||
writeFileSync(filePath, '# Spaced Heading \n');
|
||||
|
||||
expect(extractDescription(filePath)).toBe('Spaced Heading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('scanFacets', () => {
|
||||
let tempDir: string;
|
||||
let builtinDir: string;
|
||||
let globalDir: string;
|
||||
let projectDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'takt-catalog-test-'));
|
||||
builtinDir = join(tempDir, 'builtin-lang');
|
||||
globalDir = join(tempDir, 'global');
|
||||
projectDir = join(tempDir, 'project');
|
||||
|
||||
mockBuiltinDir = builtinDir;
|
||||
mockGlobalDir = globalDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should collect facets from all three layers', () => {
|
||||
// Given: facets in builtin, user, and project layers
|
||||
const builtinPersonas = join(builtinDir, 'personas');
|
||||
const globalPersonas = join(globalDir, 'personas');
|
||||
const projectPersonas = join(projectDir, '.takt', 'personas');
|
||||
mkdirSync(builtinPersonas, { recursive: true });
|
||||
mkdirSync(globalPersonas, { recursive: true });
|
||||
mkdirSync(projectPersonas, { recursive: true });
|
||||
|
||||
writeFileSync(join(builtinPersonas, 'coder.md'), '# Coder Agent');
|
||||
writeFileSync(join(globalPersonas, 'my-reviewer.md'), '# My Reviewer');
|
||||
writeFileSync(join(projectPersonas, 'project-coder.md'), '# Project Coder');
|
||||
|
||||
// When: scanning personas
|
||||
const entries = scanFacets('personas', projectDir);
|
||||
|
||||
// Then: all three entries are collected
|
||||
expect(entries).toHaveLength(3);
|
||||
|
||||
const coder = entries.find((e) => e.name === 'coder');
|
||||
expect(coder).toBeDefined();
|
||||
expect(coder!.source).toBe('builtin');
|
||||
expect(coder!.description).toBe('Coder Agent');
|
||||
|
||||
const myReviewer = entries.find((e) => e.name === 'my-reviewer');
|
||||
expect(myReviewer).toBeDefined();
|
||||
expect(myReviewer!.source).toBe('user');
|
||||
|
||||
const projectCoder = entries.find((e) => e.name === 'project-coder');
|
||||
expect(projectCoder).toBeDefined();
|
||||
expect(projectCoder!.source).toBe('project');
|
||||
});
|
||||
|
||||
it('should detect override when higher layer has same name', () => {
|
||||
// Given: same facet name in builtin and user layers
|
||||
const builtinPersonas = join(builtinDir, 'personas');
|
||||
const globalPersonas = join(globalDir, 'personas');
|
||||
mkdirSync(builtinPersonas, { recursive: true });
|
||||
mkdirSync(globalPersonas, { recursive: true });
|
||||
|
||||
writeFileSync(join(builtinPersonas, 'coder.md'), '# Builtin Coder');
|
||||
writeFileSync(join(globalPersonas, 'coder.md'), '# Custom Coder');
|
||||
|
||||
// When: scanning personas
|
||||
const entries = scanFacets('personas', tempDir);
|
||||
|
||||
// Then: builtin entry is marked as overridden by user
|
||||
const builtinCoder = entries.find((e) => e.name === 'coder' && e.source === 'builtin');
|
||||
expect(builtinCoder).toBeDefined();
|
||||
expect(builtinCoder!.overriddenBy).toBe('user');
|
||||
|
||||
const userCoder = entries.find((e) => e.name === 'coder' && e.source === 'user');
|
||||
expect(userCoder).toBeDefined();
|
||||
expect(userCoder!.overriddenBy).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detect override through project layer', () => {
|
||||
// Given: same facet name in builtin and project layers
|
||||
const builtinPolicies = join(builtinDir, 'policies');
|
||||
const projectPolicies = join(projectDir, '.takt', 'policies');
|
||||
mkdirSync(builtinPolicies, { recursive: true });
|
||||
mkdirSync(projectPolicies, { recursive: true });
|
||||
|
||||
writeFileSync(join(builtinPolicies, 'coding.md'), '# Builtin Coding');
|
||||
writeFileSync(join(projectPolicies, 'coding.md'), '# Project Coding');
|
||||
|
||||
// When: scanning policies
|
||||
const entries = scanFacets('policies', projectDir);
|
||||
|
||||
// Then: builtin entry is marked as overridden by project
|
||||
const builtinCoding = entries.find((e) => e.name === 'coding' && e.source === 'builtin');
|
||||
expect(builtinCoding).toBeDefined();
|
||||
expect(builtinCoding!.overriddenBy).toBe('project');
|
||||
});
|
||||
|
||||
it('should handle non-existent directories gracefully', () => {
|
||||
// Given: no directories exist
|
||||
// When: scanning a facet type
|
||||
const entries = scanFacets('knowledge', projectDir);
|
||||
|
||||
// Then: returns empty array
|
||||
expect(entries).toEqual([]);
|
||||
});
|
||||
|
||||
it('should only include .md files', () => {
|
||||
// Given: directory with mixed file types
|
||||
const builtinKnowledge = join(builtinDir, 'knowledge');
|
||||
mkdirSync(builtinKnowledge, { recursive: true });
|
||||
|
||||
writeFileSync(join(builtinKnowledge, 'valid.md'), '# Valid');
|
||||
writeFileSync(join(builtinKnowledge, 'ignored.txt'), 'Not a markdown');
|
||||
writeFileSync(join(builtinKnowledge, 'also-ignored.yaml'), 'name: yaml');
|
||||
|
||||
// When: scanning knowledge
|
||||
const entries = scanFacets('knowledge', tempDir);
|
||||
|
||||
// Then: only .md file is included
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0]!.name).toBe('valid');
|
||||
});
|
||||
|
||||
it('should work with all facet types', () => {
|
||||
// Given: one facet in each type directory
|
||||
const types = ['personas', 'policies', 'knowledge', 'instructions', 'output-contracts'] as const;
|
||||
for (const type of types) {
|
||||
const dir = join(builtinDir, type);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, 'test.md'), `# Test ${type}`);
|
||||
}
|
||||
|
||||
// When/Then: each type is scannable
|
||||
for (const type of types) {
|
||||
const entries = scanFacets(type, tempDir);
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0]!.name).toBe('test');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayFacets', () => {
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should display entries with name, description, and source', () => {
|
||||
// Given: a list of facet entries
|
||||
const entries: FacetEntry[] = [
|
||||
{ name: 'coder', description: 'Coder Agent', source: 'builtin' },
|
||||
{ name: 'my-reviewer', description: 'My Reviewer', source: 'user' },
|
||||
];
|
||||
|
||||
// When: displaying facets
|
||||
displayFacets('personas', entries);
|
||||
|
||||
// Then: output contains facet names
|
||||
const output = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
|
||||
expect(output).toContain('coder');
|
||||
expect(output).toContain('my-reviewer');
|
||||
expect(output).toContain('Personas');
|
||||
});
|
||||
|
||||
it('should display (none) when entries are empty', () => {
|
||||
// Given: empty entries
|
||||
const entries: FacetEntry[] = [];
|
||||
|
||||
// When: displaying facets
|
||||
displayFacets('policies', entries);
|
||||
|
||||
// Then: output shows (none)
|
||||
const output = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
|
||||
expect(output).toContain('(none)');
|
||||
});
|
||||
|
||||
it('should display override information', () => {
|
||||
// Given: an overridden entry
|
||||
const entries: FacetEntry[] = [
|
||||
{ name: 'coder', description: 'Builtin Coder', source: 'builtin', overriddenBy: 'user' },
|
||||
];
|
||||
|
||||
// When: displaying facets
|
||||
displayFacets('personas', entries);
|
||||
|
||||
// Then: output contains override info
|
||||
const output = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
|
||||
expect(output).toContain('overridden by user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('showCatalog', () => {
|
||||
let tempDir: string;
|
||||
let builtinDir: string;
|
||||
let globalDir: string;
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'takt-catalog-test-'));
|
||||
builtinDir = join(tempDir, 'builtin-lang');
|
||||
globalDir = join(tempDir, 'global');
|
||||
|
||||
mockBuiltinDir = builtinDir;
|
||||
mockGlobalDir = globalDir;
|
||||
mockLogError.mockClear();
|
||||
mockInfo.mockClear();
|
||||
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should display only the specified facet type when valid type is given', () => {
|
||||
// Given: personas facet exists
|
||||
const builtinPersonas = join(builtinDir, 'personas');
|
||||
mkdirSync(builtinPersonas, { recursive: true });
|
||||
writeFileSync(join(builtinPersonas, 'coder.md'), '# Coder Agent');
|
||||
|
||||
// When: showing catalog for personas only
|
||||
showCatalog(tempDir, 'personas');
|
||||
|
||||
// Then: output contains the facet name and no error
|
||||
const output = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
|
||||
expect(output).toContain('coder');
|
||||
expect(mockLogError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show error when invalid facet type is given', () => {
|
||||
// When: showing catalog for an invalid type
|
||||
showCatalog(tempDir, 'invalid-type');
|
||||
|
||||
// Then: error is logged with the invalid type name
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('invalid-type'),
|
||||
);
|
||||
// Then: available types are shown via info
|
||||
expect(mockInfo).toHaveBeenCalledWith(
|
||||
expect.stringContaining('personas'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should display all five facet types when no type is specified', () => {
|
||||
// Given: no facets exist (empty directories)
|
||||
|
||||
// When: showing catalog without specifying a type
|
||||
showCatalog(tempDir);
|
||||
|
||||
// Then: all 5 facet type headings are displayed
|
||||
const output = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
|
||||
expect(output).toContain('Personas');
|
||||
expect(output).toContain('Policies');
|
||||
expect(output).toContain('Knowledge');
|
||||
expect(output).toContain('Instructions');
|
||||
expect(output).toContain('Output-contracts');
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* CLI subcommand definitions
|
||||
*
|
||||
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, config, prompt).
|
||||
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, config, prompt, catalog).
|
||||
*/
|
||||
|
||||
import { clearPersonaSessions, getCurrentPiece } from '../../infra/config/index.js';
|
||||
@ -9,6 +9,7 @@ import { success } from '../../shared/ui/index.js';
|
||||
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
|
||||
import { switchPiece, switchConfig, ejectBuiltin, resetCategoriesToDefault, deploySkill } from '../../features/config/index.js';
|
||||
import { previewPrompts } from '../../features/prompt/index.js';
|
||||
import { showCatalog } from '../../features/catalog/index.js';
|
||||
import { program, resolvedCwd } from './program.js';
|
||||
import { resolveAgentOverrides } from './helpers.js';
|
||||
|
||||
@ -115,3 +116,11 @@ program
|
||||
.action(async () => {
|
||||
await deploySkill();
|
||||
});
|
||||
|
||||
program
|
||||
.command('catalog')
|
||||
.description('List available facets (personas, policies, knowledge, instructions, output-contracts)')
|
||||
.argument('[type]', 'Facet type to list')
|
||||
.action((type?: string) => {
|
||||
showCatalog(resolvedCwd, type);
|
||||
});
|
||||
|
||||
178
src/features/catalog/catalogFacets.ts
Normal file
178
src/features/catalog/catalogFacets.ts
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Facet catalog — scan and display available facets across 3 layers.
|
||||
*
|
||||
* Scans builtin, user (~/.takt/), and project (.takt/) directories
|
||||
* for facet files (.md) and displays them with layer provenance.
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, basename } from 'node:path';
|
||||
import chalk from 'chalk';
|
||||
import type { PieceSource } from '../../infra/config/loaders/pieceResolver.js';
|
||||
import { getLanguageResourcesDir } from '../../infra/resources/index.js';
|
||||
import { getGlobalConfigDir, getProjectConfigDir } from '../../infra/config/paths.js';
|
||||
import { getLanguage, getBuiltinPiecesEnabled } from '../../infra/config/global/globalConfig.js';
|
||||
import { section, error as logError, info } from '../../shared/ui/index.js';
|
||||
|
||||
const FACET_TYPES = [
|
||||
'personas',
|
||||
'policies',
|
||||
'knowledge',
|
||||
'instructions',
|
||||
'output-contracts',
|
||||
] as const;
|
||||
|
||||
export type FacetType = (typeof FACET_TYPES)[number];
|
||||
|
||||
export interface FacetEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
source: PieceSource;
|
||||
overriddenBy?: PieceSource;
|
||||
}
|
||||
|
||||
/** Validate a string as a FacetType. Returns the type or null. */
|
||||
export function parseFacetType(input: string): FacetType | null {
|
||||
if ((FACET_TYPES as readonly string[]).includes(input)) {
|
||||
return input as FacetType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract description from a markdown file.
|
||||
* Returns the first `# ` heading text, or falls back to the first non-empty line.
|
||||
*/
|
||||
export function extractDescription(filePath: string): string {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
let firstNonEmpty = '';
|
||||
for (const line of content.split('\n')) {
|
||||
if (line.startsWith('# ')) {
|
||||
return line.slice(2).trim();
|
||||
}
|
||||
if (!firstNonEmpty && line.trim()) {
|
||||
firstNonEmpty = line.trim();
|
||||
}
|
||||
}
|
||||
return firstNonEmpty;
|
||||
}
|
||||
|
||||
/** Build the 3-layer directory list for a given facet type. */
|
||||
function getFacetDirs(
|
||||
facetType: FacetType,
|
||||
cwd: string,
|
||||
): { dir: string; source: PieceSource }[] {
|
||||
const dirs: { dir: string; source: PieceSource }[] = [];
|
||||
|
||||
if (getBuiltinPiecesEnabled()) {
|
||||
const lang = getLanguage();
|
||||
dirs.push({ dir: join(getLanguageResourcesDir(lang), facetType), source: 'builtin' });
|
||||
}
|
||||
|
||||
dirs.push({ dir: join(getGlobalConfigDir(), facetType), source: 'user' });
|
||||
dirs.push({ dir: join(getProjectConfigDir(cwd), facetType), source: 'project' });
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
/** Scan a single directory for .md facet files. */
|
||||
function scanDirectory(dir: string): string[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
return readdirSync(dir).filter((f) => f.endsWith('.md'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all layers for facets of a given type.
|
||||
*
|
||||
* Scans builtin → user → project in order.
|
||||
* When a facet name appears in a higher-priority layer, the lower-priority
|
||||
* entry gets `overriddenBy` set to the overriding layer.
|
||||
*/
|
||||
export function scanFacets(facetType: FacetType, cwd: string): FacetEntry[] {
|
||||
const dirs = getFacetDirs(facetType, cwd);
|
||||
const entriesByName = new Map<string, FacetEntry>();
|
||||
const allEntries: FacetEntry[] = [];
|
||||
|
||||
for (const { dir, source } of dirs) {
|
||||
const files = scanDirectory(dir);
|
||||
for (const file of files) {
|
||||
const name = basename(file, '.md');
|
||||
const description = extractDescription(join(dir, file));
|
||||
const entry: FacetEntry = { name, description, source };
|
||||
|
||||
const existing = entriesByName.get(name);
|
||||
if (existing) {
|
||||
existing.overriddenBy = source;
|
||||
}
|
||||
|
||||
entriesByName.set(name, entry);
|
||||
allEntries.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return allEntries;
|
||||
}
|
||||
|
||||
/** Color a source tag for terminal display. */
|
||||
function colorSourceTag(source: PieceSource): string {
|
||||
switch (source) {
|
||||
case 'builtin':
|
||||
return chalk.gray(`[${source}]`);
|
||||
case 'user':
|
||||
return chalk.yellow(`[${source}]`);
|
||||
case 'project':
|
||||
return chalk.green(`[${source}]`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Format and print a list of facet entries for one facet type. */
|
||||
export function displayFacets(facetType: FacetType, entries: FacetEntry[]): void {
|
||||
section(`${capitalize(facetType)}:`);
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log(chalk.gray(' (none)'));
|
||||
return;
|
||||
}
|
||||
|
||||
const maxNameLen = Math.max(...entries.map((e) => e.name.length));
|
||||
const maxDescLen = Math.max(...entries.map((e) => e.description.length));
|
||||
|
||||
for (const entry of entries) {
|
||||
const name = entry.name.padEnd(maxNameLen + 2);
|
||||
const desc = entry.description.padEnd(maxDescLen + 2);
|
||||
const tag = colorSourceTag(entry.source);
|
||||
const override = entry.overriddenBy
|
||||
? chalk.gray(` (overridden by ${entry.overriddenBy})`)
|
||||
: '';
|
||||
console.log(` ${name}${chalk.dim(desc)}${tag}${override}`);
|
||||
}
|
||||
}
|
||||
|
||||
function capitalize(s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point: show facet catalog.
|
||||
*
|
||||
* If facetType is provided, shows only that type.
|
||||
* Otherwise shows all facet types.
|
||||
*/
|
||||
export function showCatalog(cwd: string, facetType?: string): void {
|
||||
if (facetType !== undefined) {
|
||||
const parsed = parseFacetType(facetType);
|
||||
if (!parsed) {
|
||||
logError(`Unknown facet type: "${facetType}"`);
|
||||
info(`Available types: ${FACET_TYPES.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
const entries = scanFacets(parsed, cwd);
|
||||
displayFacets(parsed, entries);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const type of FACET_TYPES) {
|
||||
const entries = scanFacets(type, cwd);
|
||||
displayFacets(type, entries);
|
||||
}
|
||||
}
|
||||
5
src/features/catalog/index.ts
Normal file
5
src/features/catalog/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Catalog feature — list available facets across layers.
|
||||
*/
|
||||
|
||||
export { showCatalog } from './catalogFacets.js';
|
||||
Loading…
x
Reference in New Issue
Block a user