mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-10 17:11:04 +08:00
feat: implement ignore patterns and extension filters in CodexLens
- Added tests to ensure loading of ignore patterns and extension filters from settings. - Implemented functionality to respect ignore patterns and extension filters during file indexing. - Created integration tests for CodexLens ignore-pattern configuration routes. - Added a new AdvancedTab component with tests for managing ignore patterns and extension filters. - Established a comprehensive branding naming system for the Maestro project, including guidelines for package names, CLI commands, and directory structure.
This commit is contained in:
@@ -502,40 +502,3 @@ if (plannerAgent) {
|
||||
| `check` / `status` | Show progress: planned / executing / completed / failed counts |
|
||||
| `resume` / `continue` | Re-enter loop from Phase 2 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Coordinator Role Constraints (Main Agent)
|
||||
|
||||
**CRITICAL**: The coordinator (main agent executing this skill) is responsible for **orchestration only**, NOT implementation.
|
||||
|
||||
15. **Coordinator Does NOT Execute Code**: The main agent MUST NOT write, modify, or implement any code directly. All implementation work is delegated to spawned team agents. The coordinator only:
|
||||
- Spawns agents with task assignments
|
||||
- Waits for agent callbacks
|
||||
- Merges results and coordinates workflow
|
||||
- Manages workflow transitions between phases
|
||||
|
||||
16. **Patient Waiting is Mandatory**: Agent execution takes significant time (typically 10-30 minutes per phase, sometimes longer). The coordinator MUST:
|
||||
- Wait patiently for `wait()` calls to complete
|
||||
- NOT skip workflow steps due to perceived delays
|
||||
- NOT assume agents have failed just because they're taking time
|
||||
- Trust the timeout mechanisms defined in the skill
|
||||
|
||||
17. **Use send_input for Clarification**: When agents need guidance or appear stuck, the coordinator MUST:
|
||||
- Use `send_input()` to ask questions or provide clarification
|
||||
- NOT skip the agent or move to next phase prematurely
|
||||
- Give agents opportunity to respond before escalating
|
||||
- Example: `send_input({ id: agent_id, message: "Please provide status update or clarify blockers" })`
|
||||
|
||||
18. **No Workflow Shortcuts**: The coordinator MUST NOT:
|
||||
- Skip phases or stages defined in the workflow
|
||||
- Bypass required approval or review steps
|
||||
- Execute dependent tasks before prerequisites complete
|
||||
- Assume task completion without explicit agent callback
|
||||
- Make up or fabricate agent results
|
||||
|
||||
19. **Respect Long-Running Processes**: This is a complex multi-agent workflow that requires patience:
|
||||
- Total execution time may range from 30-90 minutes or longer
|
||||
- Each phase may take 10-30 minutes depending on complexity
|
||||
- The coordinator must remain active and attentive throughout the entire process
|
||||
- Do not terminate or skip steps due to time concerns
|
||||
|
||||
154
ccw/frontend/src/components/codexlens/AdvancedTab.test.tsx
Normal file
154
ccw/frontend/src/components/codexlens/AdvancedTab.test.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@/test/i18n';
|
||||
import { AdvancedTab } from './AdvancedTab';
|
||||
|
||||
vi.mock('@/hooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/hooks')>();
|
||||
return {
|
||||
...actual,
|
||||
useCodexLensEnv: vi.fn(),
|
||||
useUpdateCodexLensEnv: vi.fn(),
|
||||
useCodexLensIgnorePatterns: vi.fn(),
|
||||
useUpdateIgnorePatterns: vi.fn(),
|
||||
useNotifications: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
useCodexLensEnv,
|
||||
useUpdateCodexLensEnv,
|
||||
useCodexLensIgnorePatterns,
|
||||
useUpdateIgnorePatterns,
|
||||
useNotifications,
|
||||
} from '@/hooks';
|
||||
|
||||
const mockRefetchEnv = vi.fn().mockResolvedValue(undefined);
|
||||
const mockRefetchPatterns = vi.fn().mockResolvedValue(undefined);
|
||||
const mockUpdateEnv = vi.fn().mockResolvedValue({ success: true, message: 'Saved' });
|
||||
const mockUpdatePatterns = vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
patterns: ['dist', 'frontend/dist'],
|
||||
extensionFilters: ['*.min.js', 'frontend/skip.ts'],
|
||||
defaults: {
|
||||
patterns: ['dist', 'build'],
|
||||
extensionFilters: ['*.min.js'],
|
||||
},
|
||||
});
|
||||
const mockToastSuccess = vi.fn();
|
||||
const mockToastError = vi.fn();
|
||||
|
||||
function setupDefaultMocks() {
|
||||
vi.mocked(useCodexLensEnv).mockReturnValue({
|
||||
data: { success: true, env: {}, settings: {}, raw: '', path: '~/.codexlens/.env' },
|
||||
raw: '',
|
||||
env: {},
|
||||
settings: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: mockRefetchEnv,
|
||||
});
|
||||
|
||||
vi.mocked(useUpdateCodexLensEnv).mockReturnValue({
|
||||
updateEnv: mockUpdateEnv,
|
||||
isUpdating: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
vi.mocked(useCodexLensIgnorePatterns).mockReturnValue({
|
||||
data: {
|
||||
success: true,
|
||||
patterns: ['dist', 'coverage'],
|
||||
extensionFilters: ['*.min.js', '*.map'],
|
||||
defaults: {
|
||||
patterns: ['dist', 'build', 'coverage'],
|
||||
extensionFilters: ['*.min.js', '*.map'],
|
||||
},
|
||||
},
|
||||
patterns: ['dist', 'coverage'],
|
||||
extensionFilters: ['*.min.js', '*.map'],
|
||||
defaults: {
|
||||
patterns: ['dist', 'build', 'coverage'],
|
||||
extensionFilters: ['*.min.js', '*.map'],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: mockRefetchPatterns,
|
||||
});
|
||||
|
||||
vi.mocked(useUpdateIgnorePatterns).mockReturnValue({
|
||||
updatePatterns: mockUpdatePatterns,
|
||||
isUpdating: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
vi.mocked(useNotifications).mockReturnValue({
|
||||
success: mockToastSuccess,
|
||||
error: mockToastError,
|
||||
} as ReturnType<typeof useNotifications>);
|
||||
}
|
||||
|
||||
describe('AdvancedTab', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setupDefaultMocks();
|
||||
});
|
||||
|
||||
it('renders existing filter configuration', () => {
|
||||
render(<AdvancedTab enabled={true} />);
|
||||
|
||||
expect(screen.getByLabelText(/Ignored directories \/ paths/i)).toHaveValue('dist\ncoverage');
|
||||
expect(screen.getByLabelText(/Skipped files \/ globs/i)).toHaveValue('*.min.js\n*.map');
|
||||
expect(screen.getByText(/Directory filters: 2/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/File filters: 2/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saves parsed filter configuration', async () => {
|
||||
render(<AdvancedTab enabled={true} />);
|
||||
|
||||
const ignorePatternsInput = screen.getByLabelText(/Ignored directories \/ paths/i);
|
||||
const extensionFiltersInput = screen.getByLabelText(/Skipped files \/ globs/i);
|
||||
|
||||
fireEvent.change(ignorePatternsInput, { target: { value: 'dist,\nfrontend/dist' } });
|
||||
fireEvent.change(extensionFiltersInput, { target: { value: '*.min.js,\nfrontend/skip.ts' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /Save filters/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePatterns).toHaveBeenCalledWith({
|
||||
patterns: ['dist', 'frontend/dist'],
|
||||
extensionFilters: ['*.min.js', 'frontend/skip.ts'],
|
||||
});
|
||||
});
|
||||
expect(mockRefetchPatterns).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('restores default filter values before saving', async () => {
|
||||
render(<AdvancedTab enabled={true} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Restore defaults/i }));
|
||||
|
||||
expect(screen.getByLabelText(/Ignored directories \/ paths/i)).toHaveValue('dist\nbuild\ncoverage');
|
||||
expect(screen.getByLabelText(/Skipped files \/ globs/i)).toHaveValue('*.min.js\n*.map');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Save filters/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdatePatterns).toHaveBeenCalledWith({
|
||||
patterns: ['dist', 'build', 'coverage'],
|
||||
extensionFilters: ['*.min.js', '*.map'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('blocks invalid filter entries before saving', async () => {
|
||||
render(<AdvancedTab enabled={true} />);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/Ignored directories \/ paths/i), {
|
||||
target: { value: 'bad pattern!' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /Save filters/i }));
|
||||
|
||||
expect(mockUpdatePatterns).not.toHaveBeenCalled();
|
||||
expect(screen.getByText(/Invalid ignore patterns/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,13 +5,18 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle } from 'lucide-react';
|
||||
import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle, Filter } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Label } from '@/components/ui/Label';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useCodexLensEnv, useUpdateCodexLensEnv } from '@/hooks';
|
||||
import {
|
||||
useCodexLensEnv,
|
||||
useCodexLensIgnorePatterns,
|
||||
useUpdateCodexLensEnv,
|
||||
useUpdateIgnorePatterns,
|
||||
} from '@/hooks';
|
||||
import { useNotifications } from '@/hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CcwToolsCard } from './CcwToolsCard';
|
||||
@@ -22,6 +27,28 @@ interface AdvancedTabProps {
|
||||
|
||||
interface FormErrors {
|
||||
env?: string;
|
||||
ignorePatterns?: string;
|
||||
extensionFilters?: string;
|
||||
}
|
||||
|
||||
const FILTER_ENTRY_PATTERN = /^[-\w.*\\/]+$/;
|
||||
|
||||
function parseListEntries(text: string): string[] {
|
||||
return text
|
||||
.split(/[\n,]/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeListEntries(entries: string[]): string {
|
||||
return entries
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function normalizeListText(text: string): string {
|
||||
return normalizeListEntries(parseListEntries(text));
|
||||
}
|
||||
|
||||
export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
@@ -37,14 +64,32 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
refetch,
|
||||
} = useCodexLensEnv({ enabled });
|
||||
|
||||
const {
|
||||
patterns,
|
||||
extensionFilters,
|
||||
defaults,
|
||||
isLoading: isLoadingPatterns,
|
||||
error: patternsError,
|
||||
refetch: refetchPatterns,
|
||||
} = useCodexLensIgnorePatterns({ enabled });
|
||||
|
||||
const { updateEnv, isUpdating } = useUpdateCodexLensEnv();
|
||||
const { updatePatterns, isUpdating: isUpdatingPatterns } = useUpdateIgnorePatterns();
|
||||
|
||||
// Form state
|
||||
const [envInput, setEnvInput] = useState('');
|
||||
const [ignorePatternsInput, setIgnorePatternsInput] = useState('');
|
||||
const [extensionFiltersInput, setExtensionFiltersInput] = useState('');
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [hasFilterChanges, setHasFilterChanges] = useState(false);
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
|
||||
const currentIgnorePatterns = patterns ?? [];
|
||||
const currentExtensionFilters = extensionFilters ?? [];
|
||||
const defaultIgnorePatterns = defaults?.patterns ?? [];
|
||||
const defaultExtensionFilters = defaults?.extensionFilters ?? [];
|
||||
|
||||
// Initialize form from env - handles both undefined (loading) and empty string (empty file)
|
||||
// The hook returns raw directly, so we check if it's been set (not undefined means data loaded)
|
||||
useEffect(() => {
|
||||
@@ -58,6 +103,31 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
}
|
||||
}, [raw, isLoadingEnv]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingPatterns) {
|
||||
const nextIgnorePatterns = patterns ?? [];
|
||||
const nextExtensionFilters = extensionFilters ?? [];
|
||||
setIgnorePatternsInput(nextIgnorePatterns.join('\n'));
|
||||
setExtensionFiltersInput(nextExtensionFilters.join('\n'));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
ignorePatterns: undefined,
|
||||
extensionFilters: undefined,
|
||||
}));
|
||||
setHasFilterChanges(false);
|
||||
}
|
||||
}, [extensionFilters, isLoadingPatterns, patterns]);
|
||||
|
||||
const updateFilterChangeState = (nextIgnorePatternsInput: string, nextExtensionFiltersInput: string) => {
|
||||
const normalizedCurrentIgnorePatterns = normalizeListEntries(currentIgnorePatterns);
|
||||
const normalizedCurrentExtensionFilters = normalizeListEntries(currentExtensionFilters);
|
||||
|
||||
setHasFilterChanges(
|
||||
normalizeListText(nextIgnorePatternsInput) !== normalizedCurrentIgnorePatterns
|
||||
|| normalizeListText(nextExtensionFiltersInput) !== normalizedCurrentExtensionFilters
|
||||
);
|
||||
};
|
||||
|
||||
const handleEnvChange = (value: string) => {
|
||||
setEnvInput(value);
|
||||
// Check if there are changes - compare with raw value (handle undefined as empty)
|
||||
@@ -69,6 +139,22 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleIgnorePatternsChange = (value: string) => {
|
||||
setIgnorePatternsInput(value);
|
||||
updateFilterChangeState(value, extensionFiltersInput);
|
||||
if (errors.ignorePatterns) {
|
||||
setErrors((prev) => ({ ...prev, ignorePatterns: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtensionFiltersChange = (value: string) => {
|
||||
setExtensionFiltersInput(value);
|
||||
updateFilterChangeState(ignorePatternsInput, value);
|
||||
if (errors.extensionFilters) {
|
||||
setErrors((prev) => ({ ...prev, extensionFilters: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const parseEnvVariables = (text: string): Record<string, string> => {
|
||||
const envObj: Record<string, string> = {};
|
||||
const lines = text.split('\n');
|
||||
@@ -101,10 +187,50 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
);
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
setErrors((prev) => ({ ...prev, env: newErrors.env }));
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const validateFilterForm = (): boolean => {
|
||||
const nextErrors: Pick<FormErrors, 'ignorePatterns' | 'extensionFilters'> = {};
|
||||
const parsedIgnorePatterns = parseListEntries(ignorePatternsInput);
|
||||
const parsedExtensionFilters = parseListEntries(extensionFiltersInput);
|
||||
|
||||
const invalidIgnorePatterns = parsedIgnorePatterns.filter(
|
||||
(entry) => !FILTER_ENTRY_PATTERN.test(entry)
|
||||
);
|
||||
if (invalidIgnorePatterns.length > 0) {
|
||||
nextErrors.ignorePatterns = formatMessage(
|
||||
{
|
||||
id: 'codexlens.advanced.validation.invalidIgnorePatterns',
|
||||
defaultMessage: 'Invalid ignore patterns: {values}',
|
||||
},
|
||||
{ values: invalidIgnorePatterns.join(', ') }
|
||||
);
|
||||
}
|
||||
|
||||
const invalidExtensionFilters = parsedExtensionFilters.filter(
|
||||
(entry) => !FILTER_ENTRY_PATTERN.test(entry)
|
||||
);
|
||||
if (invalidExtensionFilters.length > 0) {
|
||||
nextErrors.extensionFilters = formatMessage(
|
||||
{
|
||||
id: 'codexlens.advanced.validation.invalidExtensionFilters',
|
||||
defaultMessage: 'Invalid file filters: {values}',
|
||||
},
|
||||
{ values: invalidExtensionFilters.join(', ') }
|
||||
);
|
||||
}
|
||||
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
ignorePatterns: nextErrors.ignorePatterns,
|
||||
extensionFilters: nextErrors.extensionFilters,
|
||||
}));
|
||||
|
||||
return Object.values(nextErrors).every((value) => !value);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
@@ -138,12 +264,68 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
const handleReset = () => {
|
||||
// Reset to current raw value (handle undefined as empty)
|
||||
setEnvInput(raw ?? '');
|
||||
setErrors({});
|
||||
setErrors((prev) => ({ ...prev, env: undefined }));
|
||||
setHasChanges(false);
|
||||
setShowWarning(false);
|
||||
};
|
||||
|
||||
const handleSaveFilters = async () => {
|
||||
if (!validateFilterForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedIgnorePatterns = parseListEntries(ignorePatternsInput);
|
||||
const parsedExtensionFilters = parseListEntries(extensionFiltersInput);
|
||||
|
||||
try {
|
||||
const result = await updatePatterns({
|
||||
patterns: parsedIgnorePatterns,
|
||||
extensionFilters: parsedExtensionFilters,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setIgnorePatternsInput((result.patterns ?? parsedIgnorePatterns).join('\n'));
|
||||
setExtensionFiltersInput((result.extensionFilters ?? parsedExtensionFilters).join('\n'));
|
||||
setHasFilterChanges(false);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
ignorePatterns: undefined,
|
||||
extensionFilters: undefined,
|
||||
}));
|
||||
await refetchPatterns();
|
||||
}
|
||||
} catch {
|
||||
// Hook-level mutation already reports the error.
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setIgnorePatternsInput(currentIgnorePatterns.join('\n'));
|
||||
setExtensionFiltersInput(currentExtensionFilters.join('\n'));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
ignorePatterns: undefined,
|
||||
extensionFilters: undefined,
|
||||
}));
|
||||
setHasFilterChanges(false);
|
||||
};
|
||||
|
||||
const handleRestoreDefaultFilters = () => {
|
||||
const defaultIgnoreText = defaultIgnorePatterns.join('\n');
|
||||
const defaultExtensionText = defaultExtensionFilters.join('\n');
|
||||
|
||||
setIgnorePatternsInput(defaultIgnoreText);
|
||||
setExtensionFiltersInput(defaultExtensionText);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
ignorePatterns: undefined,
|
||||
extensionFilters: undefined,
|
||||
}));
|
||||
updateFilterChangeState(defaultIgnoreText, defaultExtensionText);
|
||||
};
|
||||
|
||||
const isLoading = isLoadingEnv;
|
||||
const isLoadingFilters = isLoadingPatterns;
|
||||
|
||||
// Get current env variables as array for display
|
||||
const currentEnvVars = env
|
||||
@@ -242,6 +424,204 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
|
||||
{/* CCW Tools Card */}
|
||||
<CcwToolsCard />
|
||||
|
||||
{/* Index Filters */}
|
||||
<Card className="p-6">
|
||||
<div className="flex flex-col gap-3 mb-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-muted-foreground" />
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.indexFilters',
|
||||
defaultMessage: 'Index Filters',
|
||||
})}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'codexlens.advanced.ignorePatternCount',
|
||||
defaultMessage: 'Directory filters: {count}',
|
||||
},
|
||||
{ count: currentIgnorePatterns.length }
|
||||
)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'codexlens.advanced.extensionFilterCount',
|
||||
defaultMessage: 'File filters: {count}',
|
||||
},
|
||||
{ count: currentExtensionFilters.length }
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{patternsError && (
|
||||
<div className="rounded-md border border-destructive/20 bg-destructive/5 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 mt-0.5 text-destructive" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.filtersLoadError',
|
||||
defaultMessage: 'Unable to load current filter settings',
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-destructive/80 mt-1">{patternsError.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ignore-patterns-input">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.ignorePatterns',
|
||||
defaultMessage: 'Ignored directories / paths',
|
||||
})}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="ignore-patterns-input"
|
||||
value={ignorePatternsInput}
|
||||
onChange={(event) => handleIgnorePatternsChange(event.target.value)}
|
||||
placeholder={formatMessage({
|
||||
id: 'codexlens.advanced.ignorePatternsPlaceholder',
|
||||
defaultMessage: 'dist\nfrontend/dist\ncoverage',
|
||||
})}
|
||||
className={cn(
|
||||
'min-h-[220px] font-mono text-sm',
|
||||
errors.ignorePatterns && 'border-destructive focus-visible:ring-destructive'
|
||||
)}
|
||||
disabled={isLoadingFilters || isUpdatingPatterns}
|
||||
/>
|
||||
{errors.ignorePatterns && (
|
||||
<p className="text-sm text-destructive">{errors.ignorePatterns}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.ignorePatternsHint',
|
||||
defaultMessage: 'One entry per line. Supports exact names, relative paths, and glob patterns.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="extension-filters-input">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.extensionFilters',
|
||||
defaultMessage: 'Skipped files / globs',
|
||||
})}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="extension-filters-input"
|
||||
value={extensionFiltersInput}
|
||||
onChange={(event) => handleExtensionFiltersChange(event.target.value)}
|
||||
placeholder={formatMessage({
|
||||
id: 'codexlens.advanced.extensionFiltersPlaceholder',
|
||||
defaultMessage: '*.min.js\n*.map\npackage-lock.json',
|
||||
})}
|
||||
className={cn(
|
||||
'min-h-[220px] font-mono text-sm',
|
||||
errors.extensionFilters && 'border-destructive focus-visible:ring-destructive'
|
||||
)}
|
||||
disabled={isLoadingFilters || isUpdatingPatterns}
|
||||
/>
|
||||
{errors.extensionFilters && (
|
||||
<p className="text-sm text-destructive">{errors.extensionFilters}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.extensionFiltersHint',
|
||||
defaultMessage: 'Use this for generated or low-value files that should stay out of the index.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 rounded-md border border-border/60 bg-muted/30 p-3 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.defaultIgnorePatterns',
|
||||
defaultMessage: 'Default directory filters',
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{defaultIgnorePatterns.slice(0, 6).map((pattern) => (
|
||||
<Badge key={pattern} variant="secondary" className="font-mono text-xs">
|
||||
{pattern}
|
||||
</Badge>
|
||||
))}
|
||||
{defaultIgnorePatterns.length > 6 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{defaultIgnorePatterns.length - 6}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.defaultExtensionFilters',
|
||||
defaultMessage: 'Default file filters',
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{defaultExtensionFilters.slice(0, 6).map((pattern) => (
|
||||
<Badge key={pattern} variant="secondary" className="font-mono text-xs">
|
||||
{pattern}
|
||||
</Badge>
|
||||
))}
|
||||
{defaultExtensionFilters.length > 6 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{defaultExtensionFilters.length - 6}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
onClick={handleSaveFilters}
|
||||
disabled={isLoadingFilters || isUpdatingPatterns || !hasFilterChanges}
|
||||
>
|
||||
<Save className={cn('w-4 h-4 mr-2', isUpdatingPatterns && 'animate-spin')} />
|
||||
{isUpdatingPatterns
|
||||
? formatMessage({ id: 'codexlens.advanced.saving', defaultMessage: 'Saving...' })
|
||||
: formatMessage({
|
||||
id: 'codexlens.advanced.saveFilters',
|
||||
defaultMessage: 'Save filters',
|
||||
})
|
||||
}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleResetFilters}
|
||||
disabled={isLoadingFilters || !hasFilterChanges}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'codexlens.advanced.reset', defaultMessage: 'Reset' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRestoreDefaultFilters}
|
||||
disabled={isLoadingFilters || isUpdatingPatterns}
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'codexlens.advanced.restoreDefaults',
|
||||
defaultMessage: 'Restore defaults',
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Environment Variables Editor */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
||||
@@ -212,7 +212,7 @@ export function McpServerDialog({
|
||||
const { formatMessage } = useIntl();
|
||||
const queryClient = useQueryClient();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const { error: showError } = useNotifications();
|
||||
const { error: showError, success: showSuccess } = useNotifications();
|
||||
|
||||
// Fetch templates from backend
|
||||
const { templates, isLoading: templatesLoading } = useMcpTemplates();
|
||||
@@ -241,6 +241,10 @@ export function McpServerDialog({
|
||||
const [saveAsTemplate, setSaveAsTemplate] = useState(false);
|
||||
const projectConfigType: McpProjectConfigType = configType === 'claude-json' ? 'claude' : 'mcp';
|
||||
|
||||
// JSON import mode state
|
||||
const [inputMode, setInputMode] = useState<'form' | 'json'>('form');
|
||||
const [jsonInput, setJsonInput] = useState('');
|
||||
|
||||
// Helper to detect transport type from server data
|
||||
const detectTransportType = useCallback((serverData: McpServer | undefined): McpTransportType => {
|
||||
if (!serverData) return 'stdio';
|
||||
@@ -249,6 +253,73 @@ export function McpServerDialog({
|
||||
return 'stdio';
|
||||
}, []);
|
||||
|
||||
// Parse JSON config and populate form
|
||||
const parseJsonConfig = useCallback(() => {
|
||||
try {
|
||||
const config = JSON.parse(jsonInput);
|
||||
|
||||
// Detect transport type based on config structure
|
||||
if (config.url) {
|
||||
// HTTP transport
|
||||
setTransportType('http');
|
||||
|
||||
// Parse headers
|
||||
const headers: HttpHeader[] = [];
|
||||
if (config.headers && typeof config.headers === 'object') {
|
||||
Object.entries(config.headers).forEach(([name, value], idx) => {
|
||||
headers.push({
|
||||
id: `header-${Date.now()}-${idx}`,
|
||||
name,
|
||||
value: String(value),
|
||||
isEnvVar: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
url: config.url || '',
|
||||
headers,
|
||||
bearerTokenEnvVar: config.bearer_token_env_var || config.bearerTokenEnvVar || '',
|
||||
}));
|
||||
} else {
|
||||
// STDIO transport
|
||||
setTransportType('stdio');
|
||||
|
||||
const args = Array.isArray(config.args) ? config.args : [];
|
||||
const env = config.env && typeof config.env === 'object' ? config.env : {};
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
command: config.command || '',
|
||||
args,
|
||||
env,
|
||||
}));
|
||||
|
||||
setArgsInput(args.join(', '));
|
||||
setEnvInput(
|
||||
Object.entries(env)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
// Switch to form mode to show parsed data
|
||||
setInputMode('form');
|
||||
setErrors({});
|
||||
showSuccess(
|
||||
formatMessage({ id: 'mcp.dialog.json.parseSuccess' }),
|
||||
formatMessage({ id: 'mcp.dialog.json.parseSuccessDesc' })
|
||||
);
|
||||
} catch (error) {
|
||||
setErrors({
|
||||
name: formatMessage({ id: 'mcp.dialog.json.parseError' }, {
|
||||
error: error instanceof Error ? error.message : 'Invalid JSON'
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [jsonInput, formatMessage, showSuccess]);
|
||||
|
||||
// Initialize form from server prop (edit mode)
|
||||
useEffect(() => {
|
||||
if (server && mode === 'edit') {
|
||||
@@ -578,9 +649,96 @@ export function McpServerDialog({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Input Mode Switcher - Only in add mode */}
|
||||
{mode === 'add' && (
|
||||
<div className="flex gap-2 border-b pb-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant={inputMode === 'form' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setInputMode('form')}
|
||||
className="flex-1"
|
||||
>
|
||||
{formatMessage({ id: 'mcp.dialog.mode.form' })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={inputMode === 'json' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setInputMode('json')}
|
||||
className="flex-1"
|
||||
>
|
||||
{formatMessage({ id: 'mcp.dialog.mode.json' })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Template Selector - Only for STDIO */}
|
||||
{transportType === 'stdio' && (
|
||||
{/* JSON Input Mode */}
|
||||
{inputMode === 'json' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.json.label' })}
|
||||
</label>
|
||||
<textarea
|
||||
value={jsonInput}
|
||||
onChange={(e) => setJsonInput(e.target.value)}
|
||||
placeholder={formatMessage({ id: 'mcp.dialog.json.placeholder' })}
|
||||
className={cn(
|
||||
'flex min-h-[300px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono',
|
||||
errors.name && 'border-destructive focus-visible:ring-destructive'
|
||||
)}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-destructive">{errors.name}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.json.hint' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Example JSON */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.json.example' })}
|
||||
</label>
|
||||
<div className="bg-muted p-3 rounded-md">
|
||||
<p className="text-xs font-medium mb-2">STDIO:</p>
|
||||
<pre className="text-xs overflow-x-auto">
|
||||
{`{
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"],
|
||||
"env": {
|
||||
"API_KEY": "your-key"
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
<p className="text-xs font-medium mt-3 mb-2">HTTP:</p>
|
||||
<pre className="text-xs overflow-x-auto">
|
||||
{`{
|
||||
"url": "http://localhost:3000",
|
||||
"headers": {
|
||||
"Authorization": "Bearer token"
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={parseJsonConfig}
|
||||
disabled={!jsonInput.trim()}
|
||||
className="w-full"
|
||||
>
|
||||
{formatMessage({ id: 'mcp.dialog.json.parse' })}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Template Selector - Only for STDIO */}
|
||||
{transportType === 'stdio' && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.dialog.form.template' })}
|
||||
@@ -901,6 +1059,8 @@ export function McpServerDialog({
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
fetchMcpServers,
|
||||
toggleMcpServer,
|
||||
@@ -26,7 +26,29 @@ function getLastFetchCall(fetchMock: any) {
|
||||
return calls[calls.length - 1] as [RequestInfo | URL, RequestInit | undefined];
|
||||
}
|
||||
|
||||
const TEST_CSRF_TOKEN = 'test-csrf-token';
|
||||
|
||||
function mockFetchWithCsrf(
|
||||
handler: (input: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>
|
||||
) {
|
||||
return vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||
if (input === '/api/csrf-token') {
|
||||
return jsonResponse({ csrfToken: TEST_CSRF_TOKEN });
|
||||
}
|
||||
|
||||
return handler(input, init);
|
||||
});
|
||||
}
|
||||
|
||||
describe('MCP API (frontend ↔ backend contract)', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
it('fetchMcpServers derives lists from /api/mcp-config and computes enabled from disabledMcpServers', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
jsonResponse({
|
||||
@@ -58,7 +80,8 @@ describe('MCP API (frontend ↔ backend contract)', () => {
|
||||
expect(fetchMock.mock.calls[0]?.[0]).toBe('/api/mcp-config');
|
||||
|
||||
expect(result.global.map((s) => s.name).sort()).toEqual(['global1', 'globalDup']);
|
||||
expect(result.project.map((s) => s.name)).toEqual(['projOnly']);
|
||||
expect(result.project.map((s) => s.name)).toEqual(['projOnly', 'globalDup', 'entDup']);
|
||||
expect(result.conflicts.map((c) => c.name)).toEqual(['globalDup']);
|
||||
|
||||
const global1 = result.global.find((s) => s.name === 'global1');
|
||||
expect(global1?.enabled).toBe(false);
|
||||
@@ -76,9 +99,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
|
||||
});
|
||||
|
||||
it('toggleMcpServer uses /api/mcp-toggle with { projectPath, serverName, enable }', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockImplementation(async (input, _init) => {
|
||||
const fetchMock = mockFetchWithCsrf(async (input, _init) => {
|
||||
if (input === '/api/mcp-toggle') {
|
||||
return jsonResponse({ success: true, serverName: 'global1', enabled: false });
|
||||
}
|
||||
@@ -111,7 +132,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
|
||||
});
|
||||
|
||||
it('deleteMcpServer calls the correct backend endpoint for project/global scopes', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const fetchMock = mockFetchWithCsrf(async (input) => {
|
||||
if (input === '/api/mcp-remove-global-server') {
|
||||
return jsonResponse({ success: true });
|
||||
}
|
||||
@@ -129,9 +150,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
|
||||
});
|
||||
|
||||
it('createMcpServer (project) uses /api/mcp-copy-server and includes serverName + serverConfig', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockImplementation(async (input) => {
|
||||
const fetchMock = mockFetchWithCsrf(async (input) => {
|
||||
if (input === '/api/mcp-copy-server') {
|
||||
return jsonResponse({ success: true });
|
||||
}
|
||||
@@ -181,7 +200,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
|
||||
});
|
||||
|
||||
it('updateMcpServer (global) upserts via /api/mcp-add-global-server', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const fetchMock = mockFetchWithCsrf(async (input) => {
|
||||
if (input === '/api/mcp-add-global-server') {
|
||||
return jsonResponse({ success: true });
|
||||
}
|
||||
@@ -232,7 +251,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
|
||||
});
|
||||
|
||||
it('crossCliCopy codex->claude copies via /api/mcp-copy-server per server', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const fetchMock = mockFetchWithCsrf(async (input) => {
|
||||
if (input === '/api/codex-mcp-config') {
|
||||
return jsonResponse({ servers: { s1: { command: 'node' } }, configPath: 'x', exists: true });
|
||||
}
|
||||
|
||||
@@ -115,6 +115,20 @@
|
||||
"addHeader": "Add Header"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"form": "Form Mode",
|
||||
"json": "JSON Mode"
|
||||
},
|
||||
"json": {
|
||||
"label": "JSON Configuration",
|
||||
"placeholder": "Paste MCP server JSON configuration...",
|
||||
"hint": "Paste complete MCP server configuration JSON, then click Parse button",
|
||||
"example": "Example Format",
|
||||
"parse": "Parse JSON",
|
||||
"parseSuccess": "JSON Parsed Successfully",
|
||||
"parseSuccessDesc": "Configuration has been populated to the form, please review and save",
|
||||
"parseError": "JSON Parse Error: {error}"
|
||||
},
|
||||
"templates": {
|
||||
"npx-stdio": "NPX STDIO",
|
||||
"python-stdio": "Python STDIO",
|
||||
|
||||
@@ -104,6 +104,20 @@
|
||||
"addHeader": "添加请求头"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"form": "表单模式",
|
||||
"json": "JSON 模式"
|
||||
},
|
||||
"json": {
|
||||
"label": "JSON 配置",
|
||||
"placeholder": "粘贴 MCP 服务器 JSON 配置...",
|
||||
"hint": "粘贴完整的 MCP 服务器配置 JSON,然后点击解析按钮",
|
||||
"example": "示例格式",
|
||||
"parse": "解析 JSON",
|
||||
"parseSuccess": "JSON 解析成功",
|
||||
"parseSuccessDesc": "配置已填充到表单中,请检查并保存",
|
||||
"parseError": "JSON 解析失败:{error}"
|
||||
},
|
||||
"templates": {
|
||||
"npx-stdio": "NPX STDIO",
|
||||
"python-stdio": "Python STDIO",
|
||||
|
||||
144
ccw/tests/integration/codexlens-ignore-pattern-routes.test.ts
Normal file
144
ccw/tests/integration/codexlens-ignore-pattern-routes.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Integration tests for CodexLens ignore-pattern configuration routes.
|
||||
*
|
||||
* Notes:
|
||||
* - Targets runtime implementation shipped in `ccw/dist`.
|
||||
* - Calls route handler directly (no HTTP server required).
|
||||
* - Uses temporary CODEXLENS_DATA_DIR to isolate ~/.codexlens writes.
|
||||
*/
|
||||
|
||||
import { after, before, describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const CODEXLENS_HOME = mkdtempSync(join(tmpdir(), 'codexlens-ignore-home-'));
|
||||
const PROJECT_ROOT = mkdtempSync(join(tmpdir(), 'codexlens-ignore-project-'));
|
||||
|
||||
const configRoutesUrl = new URL('../../dist/core/routes/codexlens/config-handlers.js', import.meta.url);
|
||||
configRoutesUrl.searchParams.set('t', String(Date.now()));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mod: any;
|
||||
|
||||
const originalEnv = {
|
||||
CODEXLENS_DATA_DIR: process.env.CODEXLENS_DATA_DIR,
|
||||
};
|
||||
|
||||
async function callConfigRoute(
|
||||
initialPath: string,
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<{ handled: boolean; status: number; json: any }> {
|
||||
const url = new URL(path, 'http://localhost');
|
||||
let status = 0;
|
||||
let text = '';
|
||||
let postPromise: Promise<void> | null = null;
|
||||
|
||||
const res = {
|
||||
writeHead(code: number) {
|
||||
status = code;
|
||||
},
|
||||
end(chunk?: unknown) {
|
||||
text = chunk === undefined ? '' : String(chunk);
|
||||
},
|
||||
};
|
||||
|
||||
const handlePostRequest = (_req: unknown, _res: unknown, handler: (parsed: any) => Promise<any>) => {
|
||||
postPromise = (async () => {
|
||||
const result = await handler(body ?? {});
|
||||
const errorValue = result && typeof result === 'object' ? result.error : undefined;
|
||||
const statusValue = result && typeof result === 'object' ? result.status : undefined;
|
||||
|
||||
if (typeof errorValue === 'string' && errorValue.length > 0) {
|
||||
res.writeHead(typeof statusValue === 'number' ? statusValue : 500);
|
||||
res.end(JSON.stringify({ error: errorValue }));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify(result));
|
||||
})();
|
||||
};
|
||||
|
||||
const handled = await mod.handleCodexLensConfigRoutes({
|
||||
pathname: url.pathname,
|
||||
url,
|
||||
req: { method },
|
||||
res,
|
||||
initialPath,
|
||||
handlePostRequest,
|
||||
broadcastToClients() {},
|
||||
});
|
||||
|
||||
if (postPromise) {
|
||||
await postPromise;
|
||||
}
|
||||
|
||||
return { handled, status, json: text ? JSON.parse(text) : null };
|
||||
}
|
||||
|
||||
describe('codexlens ignore-pattern routes integration', async () => {
|
||||
before(async () => {
|
||||
process.env.CODEXLENS_DATA_DIR = CODEXLENS_HOME;
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'warn', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mod = await import(configRoutesUrl.href);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
mock.restoreAll();
|
||||
process.env.CODEXLENS_DATA_DIR = originalEnv.CODEXLENS_DATA_DIR;
|
||||
rmSync(CODEXLENS_HOME, { recursive: true, force: true });
|
||||
rmSync(PROJECT_ROOT, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('GET /api/codexlens/ignore-patterns returns defaults before config exists', async () => {
|
||||
const res = await callConfigRoute(PROJECT_ROOT, 'GET', '/api/codexlens/ignore-patterns');
|
||||
|
||||
assert.equal(res.handled, true);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.json.success, true);
|
||||
assert.equal(Array.isArray(res.json.patterns), true);
|
||||
assert.equal(Array.isArray(res.json.extensionFilters), true);
|
||||
assert.ok(res.json.patterns.includes('dist'));
|
||||
assert.ok(res.json.extensionFilters.includes('*.min.js'));
|
||||
});
|
||||
|
||||
it('POST /api/codexlens/ignore-patterns persists custom patterns and filters', async () => {
|
||||
const saveRes = await callConfigRoute(PROJECT_ROOT, 'POST', '/api/codexlens/ignore-patterns', {
|
||||
patterns: ['dist', 'frontend/dist'],
|
||||
extensionFilters: ['*.min.js', 'frontend/skip.ts'],
|
||||
});
|
||||
|
||||
assert.equal(saveRes.handled, true);
|
||||
assert.equal(saveRes.status, 200);
|
||||
assert.equal(saveRes.json.success, true);
|
||||
assert.deepEqual(saveRes.json.patterns, ['dist', 'frontend/dist']);
|
||||
assert.deepEqual(saveRes.json.extensionFilters, ['*.min.js', 'frontend/skip.ts']);
|
||||
|
||||
const settingsPath = join(CODEXLENS_HOME, 'settings.json');
|
||||
assert.equal(existsSync(settingsPath), true);
|
||||
const savedSettings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
||||
assert.deepEqual(savedSettings.ignore_patterns, ['dist', 'frontend/dist']);
|
||||
assert.deepEqual(savedSettings.extension_filters, ['*.min.js', 'frontend/skip.ts']);
|
||||
|
||||
const getRes = await callConfigRoute(PROJECT_ROOT, 'GET', '/api/codexlens/ignore-patterns');
|
||||
assert.equal(getRes.status, 200);
|
||||
assert.deepEqual(getRes.json.patterns, ['dist', 'frontend/dist']);
|
||||
assert.deepEqual(getRes.json.extensionFilters, ['*.min.js', 'frontend/skip.ts']);
|
||||
});
|
||||
|
||||
it('POST /api/codexlens/ignore-patterns rejects invalid entries', async () => {
|
||||
const res = await callConfigRoute(PROJECT_ROOT, 'POST', '/api/codexlens/ignore-patterns', {
|
||||
patterns: ['bad pattern!'],
|
||||
});
|
||||
|
||||
assert.equal(res.handled, true);
|
||||
assert.equal(res.status, 400);
|
||||
assert.match(String(res.json.error), /Invalid patterns:/);
|
||||
});
|
||||
});
|
||||
10
ccw/undefined/settings.json
Normal file
10
ccw/undefined/settings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"ignore_patterns": [
|
||||
"dist",
|
||||
"frontend/dist"
|
||||
],
|
||||
"extension_filters": [
|
||||
"*.min.js",
|
||||
"frontend/skip.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"ignore_patterns": ["frontend/dist"], "extension_filters": ["*.min.js"]}
|
||||
@@ -0,0 +1 @@
|
||||
export const app = 1
|
||||
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/bundle.min.js
vendored
Normal file
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/bundle.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const bundle = 1
|
||||
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/dist/compiled.ts
vendored
Normal file
1
codex-lens/.pytest-temp/test_builder_loads_saved_ignor0/frontend/dist/compiled.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const compiled = 1
|
||||
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_res0/frontend/dist/bundle.ts
vendored
Normal file
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_res0/frontend/dist/bundle.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const bundle = 1
|
||||
@@ -0,0 +1 @@
|
||||
export const app = 1
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/dist/generated.py
vendored
Normal file
1
codex-lens/.pytest-temp/test_collect_dirs_by_depth_ski0/dist/generated.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
print('ok')
|
||||
@@ -0,0 +1 @@
|
||||
print('artifact')
|
||||
@@ -0,0 +1 @@
|
||||
export const app = 1
|
||||
1
codex-lens/.pytest-temp/test_iter_source_files_respect0/frontend/bundle.min.js
vendored
Normal file
1
codex-lens/.pytest-temp/test_iter_source_files_respect0/frontend/bundle.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const bundle = 1
|
||||
@@ -0,0 +1 @@
|
||||
export const skip = 1
|
||||
@@ -0,0 +1 @@
|
||||
{"ignore_patterns": ["frontend/dist", "coverage"], "extension_filters": ["*.min.js", "*.map"]}
|
||||
1
codex-lens/.pytest-temp/test_should_index_dir_ignores_0/package/dist/bundle.py
vendored
Normal file
1
codex-lens/.pytest-temp/test_should_index_dir_ignores_0/package/dist/bundle.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
print('compiled')
|
||||
@@ -123,11 +123,12 @@ class IndexTreeBuilder:
|
||||
"""
|
||||
self.registry = registry
|
||||
self.mapper = mapper
|
||||
self.config = config or Config()
|
||||
self.config = config or Config.load()
|
||||
self.parser_factory = ParserFactory(self.config)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.incremental = incremental
|
||||
self.ignore_patterns = self._resolve_ignore_patterns()
|
||||
self.extension_filters = self._resolve_extension_filters()
|
||||
|
||||
def _resolve_ignore_patterns(self) -> Tuple[str, ...]:
|
||||
configured_patterns = getattr(self.config, "ignore_patterns", None)
|
||||
@@ -139,6 +140,18 @@ class IndexTreeBuilder:
|
||||
cleaned.append(pattern)
|
||||
return tuple(dict.fromkeys(cleaned))
|
||||
|
||||
def _resolve_extension_filters(self) -> Tuple[str, ...]:
|
||||
configured_filters = getattr(self.config, "extension_filters", None)
|
||||
if not configured_filters:
|
||||
return tuple()
|
||||
|
||||
cleaned: List[str] = []
|
||||
for item in configured_filters:
|
||||
pattern = str(item).strip().replace('\\', '/').rstrip('/')
|
||||
if pattern:
|
||||
cleaned.append(pattern)
|
||||
return tuple(dict.fromkeys(cleaned))
|
||||
|
||||
def _is_ignored_dir(self, dir_path: Path, source_root: Optional[Path] = None) -> bool:
|
||||
name = dir_path.name
|
||||
if name.startswith('.'):
|
||||
@@ -159,6 +172,25 @@ class IndexTreeBuilder:
|
||||
|
||||
return False
|
||||
|
||||
def _is_filtered_file(self, file_path: Path, source_root: Optional[Path] = None) -> bool:
|
||||
if not self.extension_filters:
|
||||
return False
|
||||
|
||||
rel_path: Optional[str] = None
|
||||
if source_root is not None:
|
||||
try:
|
||||
rel_path = file_path.relative_to(source_root).as_posix()
|
||||
except ValueError:
|
||||
rel_path = None
|
||||
|
||||
for pattern in self.extension_filters:
|
||||
if pattern == file_path.name or fnmatch.fnmatch(file_path.name, pattern):
|
||||
return True
|
||||
if rel_path and (pattern == rel_path or fnmatch.fnmatch(rel_path, pattern)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def build(
|
||||
self,
|
||||
source_root: Path,
|
||||
@@ -259,6 +291,7 @@ class IndexTreeBuilder:
|
||||
dirs,
|
||||
languages,
|
||||
workers,
|
||||
source_root=source_root,
|
||||
project_id=project_info.id,
|
||||
global_index_db_path=global_index_db_path,
|
||||
)
|
||||
@@ -410,6 +443,7 @@ class IndexTreeBuilder:
|
||||
return self._build_single_dir(
|
||||
source_path,
|
||||
languages=None,
|
||||
source_root=project_root,
|
||||
project_id=project_info.id,
|
||||
global_index_db_path=global_index_db_path,
|
||||
)
|
||||
@@ -491,7 +525,7 @@ class IndexTreeBuilder:
|
||||
return False
|
||||
|
||||
# Check for supported files in this directory
|
||||
source_files = self._iter_source_files(dir_path, languages)
|
||||
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
|
||||
if len(source_files) > 0:
|
||||
return True
|
||||
|
||||
@@ -519,7 +553,7 @@ class IndexTreeBuilder:
|
||||
True if directory tree contains indexable files
|
||||
"""
|
||||
# Check for supported files in this directory
|
||||
source_files = self._iter_source_files(dir_path, languages)
|
||||
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
|
||||
if len(source_files) > 0:
|
||||
return True
|
||||
|
||||
@@ -543,6 +577,7 @@ class IndexTreeBuilder:
|
||||
languages: List[str],
|
||||
workers: int,
|
||||
*,
|
||||
source_root: Path,
|
||||
project_id: int,
|
||||
global_index_db_path: Path,
|
||||
) -> List[DirBuildResult]:
|
||||
@@ -570,6 +605,7 @@ class IndexTreeBuilder:
|
||||
result = self._build_single_dir(
|
||||
dirs[0],
|
||||
languages,
|
||||
source_root=source_root,
|
||||
project_id=project_id,
|
||||
global_index_db_path=global_index_db_path,
|
||||
)
|
||||
@@ -585,6 +621,7 @@ class IndexTreeBuilder:
|
||||
"static_graph_relationship_types": self.config.static_graph_relationship_types,
|
||||
"use_astgrep": getattr(self.config, "use_astgrep", False),
|
||||
"ignore_patterns": list(getattr(self.config, "ignore_patterns", [])),
|
||||
"extension_filters": list(getattr(self.config, "extension_filters", [])),
|
||||
}
|
||||
|
||||
worker_args = [
|
||||
@@ -595,6 +632,7 @@ class IndexTreeBuilder:
|
||||
config_dict,
|
||||
int(project_id),
|
||||
str(global_index_db_path),
|
||||
str(source_root),
|
||||
)
|
||||
for dir_path in dirs
|
||||
]
|
||||
@@ -631,6 +669,7 @@ class IndexTreeBuilder:
|
||||
dir_path: Path,
|
||||
languages: List[str] = None,
|
||||
*,
|
||||
source_root: Path,
|
||||
project_id: int,
|
||||
global_index_db_path: Path,
|
||||
) -> DirBuildResult:
|
||||
@@ -663,7 +702,7 @@ class IndexTreeBuilder:
|
||||
store.initialize()
|
||||
|
||||
# Get source files in this directory only
|
||||
source_files = self._iter_source_files(dir_path, languages)
|
||||
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
|
||||
|
||||
files_count = 0
|
||||
symbols_count = 0
|
||||
@@ -731,7 +770,7 @@ class IndexTreeBuilder:
|
||||
d.name
|
||||
for d in dir_path.iterdir()
|
||||
if d.is_dir()
|
||||
and not self._is_ignored_dir(d)
|
||||
and not self._is_ignored_dir(d, source_root=source_root)
|
||||
]
|
||||
|
||||
store.update_merkle_root()
|
||||
@@ -826,7 +865,7 @@ class IndexTreeBuilder:
|
||||
)
|
||||
|
||||
def _iter_source_files(
|
||||
self, dir_path: Path, languages: List[str] = None
|
||||
self, dir_path: Path, languages: List[str] = None, source_root: Optional[Path] = None
|
||||
) -> List[Path]:
|
||||
"""Iterate source files in directory (non-recursive).
|
||||
|
||||
@@ -852,6 +891,9 @@ class IndexTreeBuilder:
|
||||
if item.name.startswith("."):
|
||||
continue
|
||||
|
||||
if self._is_filtered_file(item, source_root=source_root):
|
||||
continue
|
||||
|
||||
# Check language support
|
||||
language_id = self.config.language_for_path(item)
|
||||
if not language_id:
|
||||
@@ -1027,19 +1069,37 @@ def _compute_graph_neighbors(
|
||||
# === Worker Function for ProcessPoolExecutor ===
|
||||
|
||||
|
||||
def _matches_ignore_patterns(path: Path, patterns: List[str]) -> bool:
|
||||
name = path.name
|
||||
if name.startswith('.'):
|
||||
return True
|
||||
def _matches_path_patterns(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
|
||||
rel_path: Optional[str] = None
|
||||
if source_root is not None:
|
||||
try:
|
||||
rel_path = path.relative_to(source_root).as_posix()
|
||||
except ValueError:
|
||||
rel_path = None
|
||||
|
||||
for pattern in patterns:
|
||||
normalized = str(pattern).strip().replace('\\', '/').rstrip('/')
|
||||
if not normalized:
|
||||
continue
|
||||
if normalized == name or fnmatch.fnmatch(name, normalized):
|
||||
if normalized == path.name or fnmatch.fnmatch(path.name, normalized):
|
||||
return True
|
||||
if rel_path and (normalized == rel_path or fnmatch.fnmatch(rel_path, normalized)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _matches_ignore_patterns(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
|
||||
if path.name.startswith('.'):
|
||||
return True
|
||||
return _matches_path_patterns(path, patterns, source_root)
|
||||
|
||||
|
||||
def _matches_extension_filters(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
|
||||
if not patterns:
|
||||
return False
|
||||
return _matches_path_patterns(path, patterns, source_root)
|
||||
|
||||
|
||||
def _build_dir_worker(args: tuple) -> DirBuildResult:
|
||||
"""Worker function for parallel directory building.
|
||||
|
||||
@@ -1047,12 +1107,12 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
|
||||
Reconstructs necessary objects from serializable arguments.
|
||||
|
||||
Args:
|
||||
args: Tuple of (dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path)
|
||||
args: Tuple of (dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path, source_root)
|
||||
|
||||
Returns:
|
||||
DirBuildResult for the directory
|
||||
"""
|
||||
dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path = args
|
||||
dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path, source_root = args
|
||||
|
||||
# Reconstruct config
|
||||
config = Config(
|
||||
@@ -1064,9 +1124,11 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
|
||||
static_graph_relationship_types=list(config_dict.get("static_graph_relationship_types", ["imports", "inherits"])),
|
||||
use_astgrep=bool(config_dict.get("use_astgrep", False)),
|
||||
ignore_patterns=list(config_dict.get("ignore_patterns", [])),
|
||||
extension_filters=list(config_dict.get("extension_filters", [])),
|
||||
)
|
||||
|
||||
parser_factory = ParserFactory(config)
|
||||
source_root_path = Path(source_root) if source_root else None
|
||||
|
||||
global_index: GlobalSymbolIndex | None = None
|
||||
try:
|
||||
@@ -1092,6 +1154,9 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
|
||||
if item.name.startswith("."):
|
||||
continue
|
||||
|
||||
if _matches_extension_filters(item, config.extension_filters, source_root_path):
|
||||
continue
|
||||
|
||||
language_id = config.language_for_path(item)
|
||||
if not language_id:
|
||||
continue
|
||||
@@ -1146,7 +1211,7 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
|
||||
subdirs = [
|
||||
d.name
|
||||
for d in dir_path.iterdir()
|
||||
if d.is_dir() and not _matches_ignore_patterns(d, ignore_patterns)
|
||||
if d.is_dir() and not _matches_ignore_patterns(d, ignore_patterns, source_root_path)
|
||||
]
|
||||
|
||||
store.update_merkle_root()
|
||||
|
||||
@@ -19,6 +19,7 @@ def test_load_settings_reads_ignore_patterns_and_extension_filters(tmp_path: Pat
|
||||
)
|
||||
|
||||
config = Config(data_dir=tmp_path)
|
||||
config.load_settings()
|
||||
|
||||
assert config.ignore_patterns == ["frontend/dist", "coverage"]
|
||||
assert config.extension_filters == ["*.min.js", "*.map"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -84,3 +85,63 @@ def test_collect_dirs_by_depth_respects_relative_ignore_patterns_from_config(tmp
|
||||
|
||||
assert "frontend/src" in discovered_dirs
|
||||
assert "frontend/dist" not in discovered_dirs
|
||||
|
||||
|
||||
def test_iter_source_files_respects_extension_filters_and_relative_patterns(tmp_path: Path) -> None:
|
||||
frontend_dir = tmp_path / "frontend"
|
||||
frontend_dir.mkdir()
|
||||
(frontend_dir / "app.ts").write_text("export const app = 1\n", encoding="utf-8")
|
||||
(frontend_dir / "bundle.min.js").write_text("export const bundle = 1\n", encoding="utf-8")
|
||||
(frontend_dir / "skip.ts").write_text("export const skip = 1\n", encoding="utf-8")
|
||||
|
||||
builder = IndexTreeBuilder(
|
||||
registry=MagicMock(),
|
||||
mapper=MagicMock(),
|
||||
config=Config(
|
||||
data_dir=tmp_path / "data",
|
||||
extension_filters=["*.min.js", "frontend/skip.ts"],
|
||||
),
|
||||
incremental=False,
|
||||
)
|
||||
|
||||
source_files = builder._iter_source_files(frontend_dir, source_root=tmp_path)
|
||||
|
||||
assert [path.name for path in source_files] == ["app.ts"]
|
||||
assert builder._should_index_dir(frontend_dir, source_root=tmp_path) is True
|
||||
|
||||
|
||||
def test_builder_loads_saved_ignore_and_extension_filters_by_default(tmp_path: Path, monkeypatch) -> None:
|
||||
codexlens_home = tmp_path / "codexlens-home"
|
||||
codexlens_home.mkdir()
|
||||
(codexlens_home / "settings.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"ignore_patterns": ["frontend/dist"],
|
||||
"extension_filters": ["*.min.js"],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("CODEXLENS_DATA_DIR", str(codexlens_home))
|
||||
|
||||
frontend_dir = tmp_path / "frontend"
|
||||
frontend_dir.mkdir()
|
||||
dist_dir = frontend_dir / "dist"
|
||||
dist_dir.mkdir()
|
||||
(frontend_dir / "app.ts").write_text("export const app = 1\n", encoding="utf-8")
|
||||
(frontend_dir / "bundle.min.js").write_text("export const bundle = 1\n", encoding="utf-8")
|
||||
(dist_dir / "compiled.ts").write_text("export const compiled = 1\n", encoding="utf-8")
|
||||
|
||||
builder = IndexTreeBuilder(
|
||||
registry=MagicMock(),
|
||||
mapper=MagicMock(),
|
||||
config=None,
|
||||
incremental=False,
|
||||
)
|
||||
|
||||
source_files = builder._iter_source_files(frontend_dir, source_root=tmp_path)
|
||||
dirs_by_depth = builder._collect_dirs_by_depth(tmp_path)
|
||||
discovered_dirs = _relative_dirs(tmp_path, dirs_by_depth)
|
||||
|
||||
assert [path.name for path in source_files] == ["app.ts"]
|
||||
assert "frontend/dist" not in discovered_dirs
|
||||
|
||||
415
docs/branding/naming-system.md
Normal file
415
docs/branding/naming-system.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# Maestro 品牌命名系统
|
||||
|
||||
> **文档版本**: 1.0.0
|
||||
> **最后更新**: 2026-03-09
|
||||
> **状态**: 已确定
|
||||
|
||||
## 概述
|
||||
|
||||
本文档定义了 Maestro 项目的完整品牌命名系统,包括总品牌、子品牌(工作流)、包名、CLI 命令、域名等命名规范。
|
||||
|
||||
### 品牌理念
|
||||
|
||||
**Maestro**(指挥家/编排大师)是一个智能编排平台,协调多个 AI CLI 工具,为开发者提供统一的工作流体验。
|
||||
|
||||
---
|
||||
|
||||
## 品牌架构
|
||||
|
||||
```
|
||||
Maestro (总平台/编排系统)
|
||||
├── Maestro Claude (基于 Claude Code 的工作流)
|
||||
├── Maestro Codex (基于 Codex 的工作流)
|
||||
├── Maestro Gemini (基于 Gemini 的工作流)
|
||||
└── Maestro Qwen (基于 Qwen 的工作流)
|
||||
```
|
||||
|
||||
### 设计原则
|
||||
|
||||
1. **清晰直观** - 工作流名称直接表明使用的 CLI 工具
|
||||
2. **品牌统一** - 所有工作流都在 Maestro 品牌下
|
||||
3. **易于扩展** - 未来添加新 CLI 时,命名规则保持一致
|
||||
4. **技术透明** - 开发者清楚底层技术栈
|
||||
|
||||
---
|
||||
|
||||
## 完整命名规范
|
||||
|
||||
| 工作流 | 品牌名 | NPM 包 | CLI 命令 | GitHub Repo | 域名 |
|
||||
|--------|--------|---------|----------|-------------|------|
|
||||
| Claude Code 工作流 | **Maestro Claude** | `@maestro/claude` | `maestro claude` | `maestro-claude` | `claude.maestro.dev` |
|
||||
| Codex 工作流 | **Maestro Codex** | `@maestro/codex` | `maestro codex` | `maestro-codex` | `codex.maestro.dev` |
|
||||
| Gemini 工作流 | **Maestro Gemini** | `@maestro/gemini` | `maestro gemini` | `maestro-gemini` | `gemini.maestro.dev` |
|
||||
| Qwen 工作流 | **Maestro Qwen** | `@maestro/qwen` | `maestro qwen` | `maestro-qwen` | `qwen.maestro.dev` |
|
||||
|
||||
### 命名规则
|
||||
|
||||
- **品牌名**: `Maestro <CLI名称>`
|
||||
- **NPM 包**: `@maestro/<cli-name>`(小写,使用 scope)
|
||||
- **CLI 命令**: `maestro <cli-name>`(小写)
|
||||
- **GitHub 仓库**: `maestro-<cli-name>`(小写,连字符)
|
||||
- **域名**: `<cli-name>.maestro.dev`(小写,子域名)
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
### 推荐的项目结构
|
||||
|
||||
```
|
||||
maestro/
|
||||
├── packages/
|
||||
│ ├── core/ # Maestro 核心引擎
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ ├── claude/ # Maestro Claude 工作流
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ ├── codex/ # Maestro Codex 工作流
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ ├── gemini/ # Maestro Gemini 工作流
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ ├── qwen/ # Maestro Qwen 工作流
|
||||
│ │ ├── src/
|
||||
│ │ └── package.json
|
||||
│ └── podium/ # Maestro UI (原 CCW)
|
||||
│ ├── frontend/
|
||||
│ ├── backend/
|
||||
│ └── package.json
|
||||
├── docs/
|
||||
│ ├── branding/ # 品牌文档
|
||||
│ ├── guides/ # 使用指南
|
||||
│ └── api/ # API 文档
|
||||
├── .codex/ # Codex 配置和技能
|
||||
├── .workflow/ # 工作流配置
|
||||
├── package.json # Monorepo 根配置
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI 使用示例
|
||||
|
||||
### 基本调用
|
||||
|
||||
```bash
|
||||
# 使用 Claude Code 工作流
|
||||
maestro claude --prompt "implement user authentication"
|
||||
|
||||
# 使用 Codex 工作流
|
||||
maestro codex --analyze "src/**/*.ts"
|
||||
|
||||
# 使用 Gemini 工作流
|
||||
maestro gemini --task "summarize this document"
|
||||
|
||||
# 使用 Qwen 工作流
|
||||
maestro qwen --prompt "explain this code"
|
||||
```
|
||||
|
||||
### 带参数调用
|
||||
|
||||
```bash
|
||||
# Claude 工作流 - 代码生成
|
||||
maestro claude generate --file "components/Button.tsx" --prompt "add loading state"
|
||||
|
||||
# Codex 工作流 - 代码分析
|
||||
maestro codex search --pattern "useEffect" --path "src/"
|
||||
|
||||
# Gemini 工作流 - 多模态任务
|
||||
maestro gemini analyze --image "screenshot.png" --prompt "describe this UI"
|
||||
|
||||
# Qwen 工作流 - 快速任务
|
||||
maestro qwen translate --from "en" --to "zh" --text "Hello World"
|
||||
```
|
||||
|
||||
### 工作流选择
|
||||
|
||||
```bash
|
||||
# 查看可用工作流
|
||||
maestro list
|
||||
|
||||
# 查看特定工作流信息
|
||||
maestro info claude
|
||||
|
||||
# 设置默认工作流
|
||||
maestro config set default-workflow claude
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
### maestro.config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"workflows": {
|
||||
"claude": {
|
||||
"name": "Maestro Claude",
|
||||
"description": "Claude Code workflow for code generation and refactoring",
|
||||
"cli": "claude-code",
|
||||
"enabled": true,
|
||||
"defaultModel": "claude-sonnet-4",
|
||||
"capabilities": ["generate", "refactor", "explain", "chat"]
|
||||
},
|
||||
"codex": {
|
||||
"name": "Maestro Codex",
|
||||
"description": "Codex workflow for code analysis and understanding",
|
||||
"cli": "codex",
|
||||
"enabled": true,
|
||||
"defaultModel": "gpt-5.2",
|
||||
"capabilities": ["analyze", "search", "visualize", "index"]
|
||||
},
|
||||
"gemini": {
|
||||
"name": "Maestro Gemini",
|
||||
"description": "Gemini workflow for general-purpose AI tasks",
|
||||
"cli": "gemini",
|
||||
"enabled": true,
|
||||
"defaultModel": "gemini-2.5-pro",
|
||||
"capabilities": ["multimodal", "general", "experimental"]
|
||||
},
|
||||
"qwen": {
|
||||
"name": "Maestro Qwen",
|
||||
"description": "Qwen workflow for fast response and experimental tasks",
|
||||
"cli": "qwen",
|
||||
"enabled": true,
|
||||
"defaultModel": "coder-model",
|
||||
"capabilities": ["fast", "experimental", "assistant"]
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"name": "Maestro",
|
||||
"tagline": "Orchestrate Your Development Workflow",
|
||||
"website": "https://maestro.dev",
|
||||
"repository": "https://github.com/maestro-suite/maestro"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 包名规范
|
||||
|
||||
### NPM 包
|
||||
|
||||
#### @maestro/claude
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@maestro/claude",
|
||||
"version": "1.0.0",
|
||||
"description": "Maestro Claude - Claude Code workflow orchestration",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"maestro-claude": "./bin/cli.js"
|
||||
},
|
||||
"keywords": [
|
||||
"maestro",
|
||||
"claude",
|
||||
"claude-code",
|
||||
"workflow",
|
||||
"ai",
|
||||
"code-generation"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maestro-suite/maestro-claude"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### @maestro/codex
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@maestro/codex",
|
||||
"version": "1.0.0",
|
||||
"description": "Maestro Codex - Codex workflow orchestration",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"maestro-codex": "./bin/cli.js"
|
||||
},
|
||||
"keywords": [
|
||||
"maestro",
|
||||
"codex",
|
||||
"workflow",
|
||||
"ai",
|
||||
"code-analysis"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maestro-suite/maestro-codex"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python 包(如果需要)
|
||||
|
||||
#### maestro-claude
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "maestro-claude"
|
||||
version = "1.0.0"
|
||||
description = "Maestro Claude - Claude Code workflow orchestration"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
keywords = ["maestro", "claude", "workflow", "ai"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://claude.maestro.dev"
|
||||
Repository = "https://github.com/maestro-suite/maestro-claude"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 品牌视觉
|
||||
|
||||
### Logo 设计
|
||||
|
||||
每个工作流使用不同的颜色来区分,但保持统一的设计语言:
|
||||
|
||||
| 工作流 | 主色 | 辅助色 | 图标元素 |
|
||||
|--------|------|--------|----------|
|
||||
| **Maestro Claude** | 橙色 `#FF6B35` | 深橙 `#D94F1C` | Claude 的 C 字母 + 指挥棒 |
|
||||
| **Maestro Codex** | 绿色 `#00D084` | 深绿 `#00A86B` | Codex 的代码符号 + 指挥棒 |
|
||||
| **Maestro Gemini** | 蓝色 `#4285F4` | 深蓝 `#1967D2` | Gemini 的双子星 + 指挥棒 |
|
||||
| **Maestro Qwen** | 紫色 `#9C27B0` | 深紫 `#7B1FA2` | Qwen 的 Q 字母 + 指挥棒 |
|
||||
|
||||
### 总品牌色彩
|
||||
|
||||
- **主色**: 深蓝/午夜蓝 `#192A56` - 专业、稳定
|
||||
- **强调色**: 活力青/薄荷绿 `#48D1CC` - 智能、创新
|
||||
- **中性色**: 浅灰 `#F5F5F5`, 深灰 `#333333`
|
||||
|
||||
### 设计元素
|
||||
|
||||
- **指挥棒**: 所有 Logo 的核心元素,象征编排和指挥
|
||||
- **声波/轨迹**: 动态的线条,表示工作流的流动
|
||||
- **几何化**: 现代、简洁的几何图形
|
||||
|
||||
---
|
||||
|
||||
## 方案优点
|
||||
|
||||
### 1. 清晰直观
|
||||
- 用户一眼就知道使用的是哪个 CLI 工具
|
||||
- 不需要学习额外的术语映射
|
||||
|
||||
### 2. 易于理解
|
||||
- 命名规则简单一致
|
||||
- 新用户快速上手
|
||||
|
||||
### 3. 灵活扩展
|
||||
- 未来添加新 CLI 时,命名规则保持一致
|
||||
- 例如:添加 `Maestro GPT`、`Maestro Llama` 等
|
||||
|
||||
### 4. 品牌统一
|
||||
- 所有工作流都在 Maestro 品牌下
|
||||
- 强化 Maestro 作为编排平台的定位
|
||||
|
||||
### 5. 技术透明
|
||||
- 开发者清楚底层使用的技术栈
|
||||
- 便于调试和问题排查
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 商标问题
|
||||
|
||||
使用 "Maestro Claude"、"Maestro Codex" 等名称时,需要注意:
|
||||
|
||||
- ⚠️ 确保不侵犯原 CLI 的商标权
|
||||
- ✅ 在文档中明确说明这些是"基于 XXX 的工作流",而不是官方产品
|
||||
- ✅ 添加免责声明:
|
||||
```
|
||||
Maestro Claude 是基于 Claude Code 的工作流编排系统。
|
||||
Claude 和 Claude Code 是 Anthropic 的商标。
|
||||
本项目与 Anthropic 无关联。
|
||||
```
|
||||
|
||||
### 2. 命名冲突
|
||||
|
||||
在发布前需要检查:
|
||||
|
||||
- [ ] npm 包名 `@maestro/claude`、`@maestro/codex` 等是否可用
|
||||
- [ ] PyPI 包名 `maestro-claude`、`maestro-codex` 等是否可用
|
||||
- [ ] GitHub 组织名 `maestro-suite` 是否可用
|
||||
- [ ] 域名 `maestro.dev`、`claude.maestro.dev` 等是否可用
|
||||
|
||||
### 3. 用户认知
|
||||
|
||||
需要在文档中清楚说明:
|
||||
|
||||
- **Maestro** 是编排平台(总品牌)
|
||||
- **Maestro Claude/Codex/Gemini/Qwen** 是工作流系统(子品牌)
|
||||
- 底层使用的是对应的 CLI 工具(技术实现)
|
||||
|
||||
示例说明:
|
||||
```
|
||||
Maestro 是一个 AI 工作流编排平台。
|
||||
Maestro Claude 是基于 Claude Code 的工作流系统,
|
||||
它调用 Claude Code CLI 来执行代码生成和重构任务。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步行动
|
||||
|
||||
### 阶段 1: 资源可用性检查
|
||||
|
||||
- [ ] 检查域名可用性
|
||||
- [ ] `maestro.dev`
|
||||
- [ ] `claude.maestro.dev`
|
||||
- [ ] `codex.maestro.dev`
|
||||
- [ ] `gemini.maestro.dev`
|
||||
- [ ] `qwen.maestro.dev`
|
||||
|
||||
- [ ] 检查 npm 包名可用性
|
||||
- [ ] `@maestro/core`
|
||||
- [ ] `@maestro/claude`
|
||||
- [ ] `@maestro/codex`
|
||||
- [ ] `@maestro/gemini`
|
||||
- [ ] `@maestro/qwen`
|
||||
|
||||
- [ ] 检查 GitHub 可用性
|
||||
- [ ] 组织名 `maestro-suite`
|
||||
- [ ] 仓库名 `maestro`, `maestro-claude`, `maestro-codex` 等
|
||||
|
||||
### 阶段 2: 迁移计划
|
||||
|
||||
- [ ] 创建迁移文档(详见 `docs/migration/renaming-plan.md`)
|
||||
- [ ] 重命名根目录:`Claude_dms3` → `maestro`
|
||||
- [ ] 重组包结构:创建 `packages/` 目录
|
||||
- [ ] 更新所有配置文件
|
||||
- [ ] 更新代码中的引用
|
||||
|
||||
### 阶段 3: 实施和发布
|
||||
|
||||
- [ ] 执行迁移
|
||||
- [ ] 更新文档和 README
|
||||
- [ ] 创建 Logo 和品牌资产
|
||||
- [ ] 发布到 npm/PyPI
|
||||
- [ ] 配置域名和网站
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [品牌架构设计](./brand-architecture.md)
|
||||
- [迁移计划](../migration/renaming-plan.md)
|
||||
- [视觉设计指南](./visual-identity.md)
|
||||
|
||||
---
|
||||
|
||||
## 变更历史
|
||||
|
||||
| 版本 | 日期 | 变更内容 | 作者 |
|
||||
|------|------|----------|------|
|
||||
| 1.0.0 | 2026-03-09 | 初始版本,确定品牌命名系统 | - |
|
||||
Reference in New Issue
Block a user