mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: Enhance spec management with new hooks and settings features
- Updated test cycle execution steps to streamline agent execution. - Improved HookDialog component with enhanced validation messages and localization. - Introduced SpecDialog component for better spec management. - Added new hooks for fetching and updating specs list and frontmatter. - Implemented API functions for specs list retrieval and index rebuilding. - Added localization support for new specs settings and hooks. - Enhanced SpecsSettingsPage to manage project and personal specs effectively. - Updated CLI commands to support keyword-based spec loading. - Improved spec index builder to categorize specs by workflow stages.
This commit is contained in:
@@ -111,19 +111,19 @@ export function HookDialog({
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = formatMessage({ id: 'hooks.validation.nameRequired' });
|
||||
newErrors.name = formatMessage({ id: 'specs.hooks.validation.nameRequired', defaultMessage: 'Name is required' });
|
||||
}
|
||||
|
||||
if (!formData.command.trim()) {
|
||||
newErrors.command = formatMessage({ id: 'hooks.validation.commandRequired' });
|
||||
newErrors.command = formatMessage({ id: 'specs.hooks.validation.commandRequired', defaultMessage: 'Command is required' });
|
||||
}
|
||||
|
||||
if (formData.timeout && formData.timeout < 1000) {
|
||||
newErrors.timeout = formatMessage({ id: 'hooks.validation.timeoutMin' });
|
||||
newErrors.timeout = formatMessage({ id: 'specs.hooks.validation.timeoutMin', defaultMessage: 'Minimum timeout is 1000ms' });
|
||||
}
|
||||
|
||||
if (formData.timeout && formData.timeout > 300000) {
|
||||
newErrors.timeout = formatMessage({ id: 'hooks.validation.timeoutMax' });
|
||||
newErrors.timeout = formatMessage({ id: 'specs.hooks.validation.timeoutMax', defaultMessage: 'Maximum timeout is 300000ms' });
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
@@ -153,11 +153,11 @@ export function HookDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditing
|
||||
? formatMessage({ id: 'hooks.dialog.editTitle' })
|
||||
: formatMessage({ id: 'hooks.dialog.createTitle' })}
|
||||
? formatMessage({ id: 'specs.hooks.dialog.editTitle', defaultMessage: 'Edit Hook' })
|
||||
: formatMessage({ id: 'specs.hooks.dialog.createTitle', defaultMessage: 'Create Hook' })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{formatMessage({ id: 'hooks.dialog.description' })}
|
||||
{formatMessage({ id: 'specs.hooks.dialog.description', defaultMessage: 'Configure the hook trigger event, command, and other settings.' })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -165,13 +165,13 @@ export function HookDialog({
|
||||
{/* Name field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="required">
|
||||
{formatMessage({ id: 'hooks.fields.name' })}
|
||||
{formatMessage({ id: 'specs.hooks.fields.name', defaultMessage: 'Hook Name' })}
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'hooks.placeholders.name' })}
|
||||
placeholder={formatMessage({ id: 'specs.hooks.placeholders.name', defaultMessage: 'Enter hook name' })}
|
||||
className={errors.name ? 'border-destructive' : ''}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -183,7 +183,7 @@ export function HookDialog({
|
||||
{/* Event type field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="event" className="required">
|
||||
{formatMessage({ id: 'hooks.fields.event' })}
|
||||
{formatMessage({ id: 'specs.hooks.fields.event', defaultMessage: 'Trigger Event' })}
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.event}
|
||||
@@ -191,29 +191,29 @@ export function HookDialog({
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger id="event">
|
||||
<SelectValue placeholder={formatMessage({ id: 'hooks.placeholders.event' })} />
|
||||
<SelectValue placeholder={formatMessage({ id: 'specs.hooks.placeholders.event', defaultMessage: 'Select event' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="SessionStart">
|
||||
{formatMessage({ id: 'hooks.events.sessionStart' })}
|
||||
{formatMessage({ id: 'specs.hooks.events.sessionStart', defaultMessage: 'Session Start' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="UserPromptSubmit">
|
||||
{formatMessage({ id: 'hooks.events.userPromptSubmit' })}
|
||||
{formatMessage({ id: 'specs.hooks.events.userPromptSubmit', defaultMessage: 'User Prompt Submit' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="SessionEnd">
|
||||
{formatMessage({ id: 'hooks.events.sessionEnd' })}
|
||||
{formatMessage({ id: 'specs.hooks.events.sessionEnd', defaultMessage: 'Session End' })}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'hints.hookEvents' })}
|
||||
{formatMessage({ id: 'specs.hints.hookEvents', defaultMessage: 'Select when this hook should be triggered' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Scope field */}
|
||||
<div className="space-y-2">
|
||||
<Label className="required">
|
||||
{formatMessage({ id: 'hooks.fields.scope' })}
|
||||
{formatMessage({ id: 'specs.hooks.fields.scope', defaultMessage: 'Scope' })}
|
||||
</Label>
|
||||
<RadioGroup
|
||||
value={formData.scope}
|
||||
@@ -225,32 +225,32 @@ export function HookDialog({
|
||||
<RadioGroupItem value="global" id="scope-global" />
|
||||
<Label htmlFor="scope-global" className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Globe className="h-4 w-4" />
|
||||
{formatMessage({ id: 'hooks.scope.global' })}
|
||||
{formatMessage({ id: 'specs.hooks.scope.global', defaultMessage: 'Global' })}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="project" id="scope-project" />
|
||||
<Label htmlFor="scope-project" className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Folder className="h-4 w-4" />
|
||||
{formatMessage({ id: 'hooks.scope.project' })}
|
||||
{formatMessage({ id: 'specs.hooks.scope.project', defaultMessage: 'Project' })}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'hints.hookScope' })}
|
||||
{formatMessage({ id: 'specs.hints.hookScope', defaultMessage: 'Global hooks apply to all projects, project hooks only to current project' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Command field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="command" className="required">
|
||||
{formatMessage({ id: 'hooks.fields.command' })}
|
||||
{formatMessage({ id: 'specs.hooks.fields.command', defaultMessage: 'Command' })}
|
||||
</Label>
|
||||
<Input
|
||||
id="command"
|
||||
value={formData.command}
|
||||
onChange={(e) => updateField('command', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'hooks.placeholders.command' })}
|
||||
placeholder={formatMessage({ id: 'specs.hooks.placeholders.command', defaultMessage: 'Enter command to execute' })}
|
||||
className={cn('font-mono', errors.command ? 'border-destructive' : '')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -258,20 +258,20 @@ export function HookDialog({
|
||||
<p className="text-xs text-destructive">{errors.command}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'hints.hookCommand' })}
|
||||
{formatMessage({ id: 'specs.hints.hookCommand', defaultMessage: 'Command to execute, can use environment variables' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">
|
||||
{formatMessage({ id: 'hooks.fields.description' })}
|
||||
{formatMessage({ id: 'specs.hooks.fields.description', defaultMessage: 'Description' })}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description || ''}
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
placeholder={formatMessage({ id: 'hooks.placeholders.description' })}
|
||||
placeholder={formatMessage({ id: 'specs.hooks.placeholders.description', defaultMessage: 'Enter description (optional)' })}
|
||||
rows={2}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
@@ -280,9 +280,9 @@ export function HookDialog({
|
||||
{/* Timeout field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timeout" className="flex items-center gap-1">
|
||||
{formatMessage({ id: 'hooks.fields.timeout' })}
|
||||
{formatMessage({ id: 'specs.hooks.fields.timeout', defaultMessage: 'Timeout' })}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({formatMessage({ id: 'hooks.fields.timeoutUnit' })})
|
||||
({formatMessage({ id: 'specs.hooks.fields.timeoutUnit', defaultMessage: 'ms' })})
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -300,14 +300,14 @@ export function HookDialog({
|
||||
<p className="text-xs text-destructive">{errors.timeout}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'hints.hookTimeout' })}
|
||||
{formatMessage({ id: 'specs.hints.hookTimeout', defaultMessage: 'Timeout for command execution' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fail mode field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="failMode" className="flex items-center gap-1">
|
||||
{formatMessage({ id: 'hooks.fields.failMode' })}
|
||||
{formatMessage({ id: 'specs.hooks.fields.failMode', defaultMessage: 'Failure Mode' })}
|
||||
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</Label>
|
||||
<Select
|
||||
@@ -320,30 +320,30 @@ export function HookDialog({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="continue">
|
||||
{formatMessage({ id: 'hooks.failModes.continue' })}
|
||||
{formatMessage({ id: 'specs.hooks.failModes.continue', defaultMessage: 'Continue' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="warn">
|
||||
{formatMessage({ id: 'hooks.failModes.warn' })}
|
||||
{formatMessage({ id: 'specs.hooks.failModes.warn', defaultMessage: 'Warn' })}
|
||||
</SelectItem>
|
||||
<SelectItem value="block">
|
||||
{formatMessage({ id: 'hooks.failModes.block' })}
|
||||
{formatMessage({ id: 'specs.hooks.failModes.block', defaultMessage: 'Block' })}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'hints.hookFailMode' })}
|
||||
{formatMessage({ id: 'specs.hints.hookFailMode', defaultMessage: 'How to handle command execution failure' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel} disabled={isLoading}>
|
||||
{formatMessage({ id: 'common.cancel' })}
|
||||
{formatMessage({ id: 'specs.common.cancel', defaultMessage: 'Cancel' })}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isLoading}>
|
||||
{isLoading
|
||||
? formatMessage({ id: 'common.saving' })
|
||||
: formatMessage({ id: 'common.save' })}
|
||||
? formatMessage({ id: 'specs.common.saving', defaultMessage: 'Saving...' })
|
||||
: formatMessage({ id: 'specs.common.save', defaultMessage: 'Save' })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -336,10 +336,13 @@ import {
|
||||
updateSystemSettings,
|
||||
installRecommendedHooks,
|
||||
getSpecStats,
|
||||
getSpecsList,
|
||||
rebuildSpecIndex,
|
||||
updateSpecFrontmatter,
|
||||
type SystemSettings,
|
||||
type UpdateSystemSettingsInput,
|
||||
type InstallRecommendedHooksResponse,
|
||||
type SpecStats,
|
||||
type SpecsListResponse,
|
||||
} from '../lib/api';
|
||||
|
||||
// Query keys for specs settings
|
||||
@@ -463,3 +466,110 @@ export function useSpecStats(options: UseSpecStatsOptions = {}): UseSpecStatsRet
|
||||
refetch: () => { query.refetch(); },
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Specs List Hook
|
||||
// ========================================
|
||||
|
||||
export interface UseSpecsListOptions {
|
||||
projectPath?: string;
|
||||
enabled?: boolean;
|
||||
staleTime?: number;
|
||||
}
|
||||
|
||||
export interface UseSpecsListReturn {
|
||||
data: SpecsListResponse | undefined;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch specs list for all dimensions
|
||||
* @param options - Options including projectPath for workspace isolation
|
||||
*/
|
||||
export function useSpecsList(options: UseSpecsListOptions = {}): UseSpecsListReturn {
|
||||
const { projectPath, enabled = true, staleTime = STALE_TIME } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: specsSettingsKeys.specStats(projectPath), // Reuse for specs list
|
||||
queryFn: () => getSpecsList(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
refetch: () => { query.refetch(); },
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Rebuild Spec Index Mutation Hook
|
||||
// ========================================
|
||||
|
||||
export interface UseRebuildSpecIndexOptions {
|
||||
projectPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to rebuild spec index
|
||||
* @param options - Options including projectPath for workspace isolation
|
||||
*/
|
||||
export function useRebuildSpecIndex(options: UseRebuildSpecIndexOptions = {}) {
|
||||
const { projectPath } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => rebuildSpecIndex(projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate specs list and stats queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
mutate: mutation.mutate,
|
||||
mutateAsync: mutation.mutateAsync,
|
||||
isPending: mutation.isPending,
|
||||
error: mutation.error,
|
||||
data: mutation.data,
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Update Spec Frontmatter Mutation Hook
|
||||
// ========================================
|
||||
|
||||
export interface UseUpdateSpecFrontmatterOptions {
|
||||
projectPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to update spec frontmatter (e.g., toggle readMode)
|
||||
* @param options - Options including projectPath for workspace isolation
|
||||
*/
|
||||
export function useUpdateSpecFrontmatter(options: UseUpdateSpecFrontmatterOptions = {}) {
|
||||
const { projectPath } = options;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ file, readMode }: { file: string; readMode: string }) =>
|
||||
updateSpecFrontmatter(file, readMode, projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate specs list to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
mutate: mutation.mutate,
|
||||
mutateAsync: mutation.mutateAsync,
|
||||
isPending: mutation.isPending,
|
||||
error: mutation.error,
|
||||
data: mutation.data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7261,6 +7261,65 @@ export async function getSpecStats(projectPath?: string): Promise<SpecStats> {
|
||||
return fetchApi<SpecStats>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec entry from index
|
||||
*/
|
||||
export interface SpecEntry {
|
||||
file: string;
|
||||
title: string;
|
||||
dimension: string;
|
||||
readMode: 'required' | 'optional' | 'keywords';
|
||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specs list response from /api/specs/list
|
||||
*/
|
||||
export interface SpecsListResponse {
|
||||
specs: Record<string, SpecEntry[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch specs list for all dimensions
|
||||
* @param projectPath - Optional project path
|
||||
*/
|
||||
export async function getSpecsList(projectPath?: string): Promise<SpecsListResponse> {
|
||||
const url = projectPath
|
||||
? `/api/specs/list?path=${encodeURIComponent(projectPath)}`
|
||||
: '/api/specs/list';
|
||||
return fetchApi<SpecsListResponse>(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild spec index
|
||||
*/
|
||||
export async function rebuildSpecIndex(projectPath?: string): Promise<{ success: boolean; stats?: Record<string, number> }> {
|
||||
const url = projectPath
|
||||
? `/api/specs/rebuild?path=${encodeURIComponent(projectPath)}`
|
||||
: '/api/specs/rebuild';
|
||||
return fetchApi<{ success: boolean; stats?: Record<string, number> }>(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update spec frontmatter (toggle readMode)
|
||||
*/
|
||||
export async function updateSpecFrontmatter(
|
||||
file: string,
|
||||
readMode: string,
|
||||
projectPath?: string
|
||||
): Promise<{ success: boolean; readMode?: string }> {
|
||||
const url = projectPath
|
||||
? `/api/specs/update-frontmatter?path=${encodeURIComponent(projectPath)}`
|
||||
: '/api/specs/update-frontmatter';
|
||||
return fetchApi<{ success: boolean; readMode?: string }>(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ file, readMode }),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Analysis API ==========
|
||||
|
||||
import type { AnalysisSessionSummary, AnalysisSessionDetail } from '../types/analysis';
|
||||
|
||||
@@ -42,6 +42,7 @@ import team from './team.json';
|
||||
import terminalDashboard from './terminal-dashboard.json';
|
||||
import skillHub from './skill-hub.json';
|
||||
import nativeSession from './native-session.json';
|
||||
import specs from './specs.json';
|
||||
|
||||
/**
|
||||
* Flattens nested JSON object to dot-separated keys
|
||||
@@ -107,4 +108,5 @@ export default {
|
||||
...flattenMessages(terminalDashboard, 'terminalDashboard'),
|
||||
...flattenMessages(skillHub, 'skillHub'),
|
||||
...flattenMessages(nativeSession, 'nativeSession'),
|
||||
...flattenMessages(specs, 'specs'),
|
||||
} as Record<string, string>;
|
||||
|
||||
93
ccw/frontend/src/locales/en/specs.json
Normal file
93
ccw/frontend/src/locales/en/specs.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"pageTitle": "Spec Settings",
|
||||
"pageDescription": "Manage specification injection, hooks, and system settings",
|
||||
"tabProjectSpecs": "Project Specs",
|
||||
"tabPersonalSpecs": "Personal",
|
||||
"tabHooks": "Hooks",
|
||||
"tabInjection": "Injection",
|
||||
"tabSettings": "Settings",
|
||||
"searchPlaceholder": "Search specs...",
|
||||
"rebuildIndex": "Rebuild Index",
|
||||
"loading": "Loading...",
|
||||
"noSpecs": "No specs found. Create specs in .workflow/ directory.",
|
||||
|
||||
"recommendedHooks": "Recommended Hooks",
|
||||
"recommendedHooksDesc": "One-click install system-preset spec injection hooks",
|
||||
"installAll": "Install All Recommended Hooks",
|
||||
"installedHooks": "Installed Hooks",
|
||||
"installedHooksDesc": "Manage your installed hooks configuration",
|
||||
"searchHooks": "Search hooks...",
|
||||
"noHooks": "No hooks installed. Install recommended hooks above.",
|
||||
|
||||
"spec": {
|
||||
"edit": "Edit",
|
||||
"toggle": "Toggle",
|
||||
"delete": "Delete",
|
||||
"required": "Required",
|
||||
"optional": "Optional",
|
||||
"priority": {
|
||||
"critical": "Critical",
|
||||
"high": "High",
|
||||
"medium": "Medium",
|
||||
"low": "Low"
|
||||
}
|
||||
},
|
||||
|
||||
"hook": {
|
||||
"install": "Install",
|
||||
"edit": "Edit",
|
||||
"toggle": "Toggle",
|
||||
"delete": "Delete",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"scope": {
|
||||
"global": "Global",
|
||||
"project": "Project"
|
||||
},
|
||||
"event": {
|
||||
"SessionStart": "Session Start",
|
||||
"UserPromptSubmit": "Prompt Submit",
|
||||
"SessionEnd": "Session End"
|
||||
}
|
||||
},
|
||||
|
||||
"injection": {
|
||||
"title": "Injection Control",
|
||||
"description": "Monitor and manage spec injection length",
|
||||
"currentLength": "Current Length",
|
||||
"maxLength": "Max Length",
|
||||
"warnThreshold": "Warn Threshold",
|
||||
"percentage": "Usage",
|
||||
"truncateOnExceed": "Truncate on Exceed",
|
||||
"truncateDescription": "Automatically truncate when injection exceeds max length",
|
||||
"overLimit": "Over Limit",
|
||||
"warning": "Warning",
|
||||
"normal": "Normal"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Global Settings",
|
||||
"description": "Configure personal spec defaults and system settings",
|
||||
"personalSpecDefaults": "Personal Spec Defaults",
|
||||
"defaultReadMode": "Default Read Mode",
|
||||
"autoEnable": "Auto Enable",
|
||||
"autoEnableDescription": "Automatically enable newly created personal specs"
|
||||
},
|
||||
|
||||
"dialog": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"editSpec": "Edit Spec",
|
||||
"editHook": "Edit Hook",
|
||||
"specTitle": "Spec Title",
|
||||
"keywords": "Keywords",
|
||||
"readMode": "Read Mode",
|
||||
"priority": "Priority",
|
||||
"hookName": "Hook Name",
|
||||
"hookEvent": "Event",
|
||||
"hookCommand": "Command",
|
||||
"hookScope": "Scope",
|
||||
"hookTimeout": "Timeout (ms)",
|
||||
"hookFailMode": "Fail Mode"
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import team from './team.json';
|
||||
import terminalDashboard from './terminal-dashboard.json';
|
||||
import skillHub from './skill-hub.json';
|
||||
import nativeSession from './native-session.json';
|
||||
import specs from './specs.json';
|
||||
|
||||
/**
|
||||
* Flattens nested JSON object to dot-separated keys
|
||||
@@ -107,4 +108,5 @@ export default {
|
||||
...flattenMessages(terminalDashboard, 'terminalDashboard'),
|
||||
...flattenMessages(skillHub, 'skillHub'),
|
||||
...flattenMessages(nativeSession, 'nativeSession'),
|
||||
...flattenMessages(specs, 'specs'),
|
||||
} as Record<string, string>;
|
||||
|
||||
202
ccw/frontend/src/locales/zh/specs.json
Normal file
202
ccw/frontend/src/locales/zh/specs.json
Normal file
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"pageTitle": "规范设置",
|
||||
"pageDescription": "管理规范注入、钩子和系统设置",
|
||||
"tabProjectSpecs": "项目规范",
|
||||
"tabPersonalSpecs": "个人",
|
||||
"tabHooks": "钩子",
|
||||
"tabInjection": "注入控制",
|
||||
"tabSettings": "设置",
|
||||
"searchPlaceholder": "搜索规范...",
|
||||
"rebuildIndex": "重建索引",
|
||||
"loading": "加载中...",
|
||||
"noSpecs": "未找到规范。请在 .workflow/ 目录中创建规范文件。",
|
||||
|
||||
"recommendedHooks": "推荐钩子",
|
||||
"recommendedHooksDesc": "一键安装系统预设的规范注入钩子",
|
||||
"installAll": "安装所有推荐钩子",
|
||||
"installedHooks": "已安装钩子",
|
||||
"installedHooksDesc": "管理已安装的钩子配置",
|
||||
"searchHooks": "搜索钩子...",
|
||||
"noHooks": "未安装钩子。请安装上方的推荐钩子。",
|
||||
|
||||
"actions": {
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"reset": "重置",
|
||||
"save": "保存",
|
||||
"saving": "保存中..."
|
||||
},
|
||||
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用"
|
||||
},
|
||||
|
||||
"readMode": {
|
||||
"required": "必读",
|
||||
"optional": "选读"
|
||||
},
|
||||
|
||||
"priority": {
|
||||
"critical": "关键",
|
||||
"high": "高",
|
||||
"medium": "中",
|
||||
"low": "低"
|
||||
},
|
||||
|
||||
"spec": {
|
||||
"edit": "编辑规范",
|
||||
"toggle": "切换状态",
|
||||
"delete": "删除规范",
|
||||
"deleteConfirm": "确定要删除此规范吗?",
|
||||
"title": "规范标题",
|
||||
"keywords": "关键词",
|
||||
"keywordsPlaceholder": "输入关键词,用逗号分隔",
|
||||
"readMode": "读取模式",
|
||||
"priority": "优先级",
|
||||
"file": "文件路径"
|
||||
},
|
||||
|
||||
"hook": {
|
||||
"install": "安装",
|
||||
"uninstall": "卸载",
|
||||
"edit": "编辑钩子",
|
||||
"toggle": "切换状态",
|
||||
"delete": "删除钩子",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装",
|
||||
"scope": {
|
||||
"global": "全局",
|
||||
"project": "项目"
|
||||
},
|
||||
"event": {
|
||||
"SessionStart": "会话开始",
|
||||
"UserPromptSubmit": "提示词提交",
|
||||
"SessionEnd": "会话结束"
|
||||
},
|
||||
"name": "钩子名称",
|
||||
"eventLabel": "触发事件",
|
||||
"command": "执行命令",
|
||||
"scopeLabel": "作用域",
|
||||
"timeout": "超时时间(ms)",
|
||||
"failMode": "失败模式",
|
||||
"failModeContinue": "继续",
|
||||
"failModeBlock": "阻止",
|
||||
"failModeWarn": "警告"
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"dialog": {
|
||||
"createTitle": "创建钩子",
|
||||
"editTitle": "编辑钩子",
|
||||
"description": "配置钩子的触发事件、执行命令和其他参数。"
|
||||
},
|
||||
"fields": {
|
||||
"name": "钩子名称",
|
||||
"event": "触发事件",
|
||||
"scope": "作用域",
|
||||
"command": "执行命令",
|
||||
"description": "描述",
|
||||
"timeout": "超时时间",
|
||||
"timeoutUnit": "毫秒",
|
||||
"failMode": "失败处理模式"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "输入钩子名称",
|
||||
"event": "选择触发事件",
|
||||
"command": "输入要执行的命令",
|
||||
"description": "输入钩子描述(可选)"
|
||||
},
|
||||
"events": {
|
||||
"sessionStart": "会话开始",
|
||||
"userPromptSubmit": "提示词提交",
|
||||
"sessionEnd": "会话结束"
|
||||
},
|
||||
"scope": {
|
||||
"global": "全局",
|
||||
"project": "项目"
|
||||
},
|
||||
"failModes": {
|
||||
"continue": "继续执行",
|
||||
"warn": "显示警告",
|
||||
"block": "阻止操作"
|
||||
}
|
||||
},
|
||||
|
||||
"hints": {
|
||||
"hookEvents": "选择钩子触发的事件类型",
|
||||
"hookScope": "全局钩子应用于所有项目,项目钩子仅当前项目",
|
||||
"hookCommand": "执行的命令,可使用环境变量",
|
||||
"hookTimeout": "命令执行的超时时间",
|
||||
"hookFailMode": "命令执行失败时的处理方式"
|
||||
},
|
||||
|
||||
"common": {
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"reset": "重置",
|
||||
"confirm": "确认"
|
||||
},
|
||||
|
||||
"injection": {
|
||||
"title": "注入控制",
|
||||
"description": "监控和管理规范注入长度",
|
||||
"statusTitle": "当前注入状态",
|
||||
"settingsTitle": "注入控制设置",
|
||||
"settingsDescription": "配置如何将规范内容注入到 AI 上下文中。",
|
||||
"currentLength": "当前长度",
|
||||
"maxLength": "最大注入长度(字符)",
|
||||
"maxLengthHelp": "推荐值:4000-10000。过大会消耗过多上下文,过小可能截断重要规范。",
|
||||
"warnThreshold": "警告阈值",
|
||||
"warnThresholdLabel": "警告阈值(字符)",
|
||||
"warnThresholdHelp": "当注入长度超过此值时显示警告。",
|
||||
"percentage": "使用率",
|
||||
"truncateOnExceed": "超出时截断",
|
||||
"truncateHelp": "当内容超出最大长度时自动截断。",
|
||||
"overLimit": "已超出限制",
|
||||
"overLimitDescription": "当前注入内容已超出最大长度限制 {max} 字符,超出部分将被截断。",
|
||||
"warning": "接近限制",
|
||||
"normal": "正常",
|
||||
"characters": "字符",
|
||||
"statsInfo": "统计信息",
|
||||
"requiredLength": "必读规范长度:",
|
||||
"matchedLength": "关键词匹配长度:",
|
||||
"remaining": "剩余空间:",
|
||||
"loadError": "加载统计数据失败",
|
||||
"saveSuccess": "设置已保存",
|
||||
"saveError": "保存设置失败"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "全局设置",
|
||||
"description": "配置个人规范默认值和系统设置",
|
||||
"personalSpecDefaults": "个人规范默认值",
|
||||
"defaultReadMode": "默认读取模式",
|
||||
"defaultReadModeHelp": "新创建的个人规范的默认读取模式",
|
||||
"autoEnable": "自动启用",
|
||||
"autoEnableDescription": "新创建的个人规范自动启用"
|
||||
},
|
||||
|
||||
"dialog": {
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"close": "关闭",
|
||||
"editSpec": "编辑规范",
|
||||
"editHook": "编辑钩子",
|
||||
"confirmDelete": "确认删除",
|
||||
"specTitle": "规范标题",
|
||||
"keywords": "关键词",
|
||||
"readMode": "读取模式",
|
||||
"priority": "优先级",
|
||||
"hookName": "钩子名称",
|
||||
"hookEvent": "触发事件",
|
||||
"hookCommand": "执行命令",
|
||||
"hookScope": "作用域",
|
||||
"hookTimeout": "超时时间(ms)",
|
||||
"hookFailMode": "失败模式"
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,40 @@
|
||||
* Main page for managing spec settings, hooks, injection control, and global settings.
|
||||
* Uses 5 tabs: Project Specs | Personal Specs | Hooks | Injection | Settings
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { ScrollText, User, Plug, Gauge, Settings, RefreshCw, Search } from 'lucide-react';
|
||||
import { SpecCard, SpecDialog, type Spec, type SpecFormData } from '@/components/specs';
|
||||
import { HookCard, HookDialog, type HookConfig } from '@/components/specs';
|
||||
import { InjectionControlTab } from '@/components/specs/InjectionControlTab';
|
||||
import { GlobalSettingsTab } from '@/components/specs/GlobalSettingsTab';
|
||||
import { useSpecStats } from '@/hooks/useSystemSettings';
|
||||
import { useSpecStats, useSpecsList, useSystemSettings, useRebuildSpecIndex } from '@/hooks/useSystemSettings';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import type { SpecEntry } from '@/lib/api';
|
||||
|
||||
type SettingsTab = 'project-specs' | 'personal-specs' | 'hooks' | 'injection' | 'settings';
|
||||
|
||||
// Convert SpecEntry to Spec for display
|
||||
function specEntryToSpec(entry: SpecEntry, dimension: string): Spec {
|
||||
return {
|
||||
id: entry.file,
|
||||
title: entry.title,
|
||||
dimension: dimension as Spec['dimension'],
|
||||
keywords: entry.keywords,
|
||||
readMode: entry.readMode as Spec['readMode'],
|
||||
priority: entry.priority as Spec['priority'],
|
||||
file: entry.file,
|
||||
enabled: true, // Default to enabled
|
||||
};
|
||||
}
|
||||
|
||||
export function SpecsSettingsPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('project-specs');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
@@ -29,13 +45,50 @@ export function SpecsSettingsPage() {
|
||||
const [editingSpec, setEditingSpec] = useState<Spec | null>(null);
|
||||
const [editingHook, setEditingHook] = useState<HookConfig | null>(null);
|
||||
|
||||
// Mock data for demonstration - will be replaced with real API calls
|
||||
const [projectSpecs] = useState<Spec[]>([]);
|
||||
const [personalSpecs] = useState<Spec[]>([]);
|
||||
const [hooks] = useState<HookConfig[]>([]);
|
||||
const [isLoading] = useState(false);
|
||||
// Fetch real data
|
||||
const { data: specsListData, isLoading: specsLoading, refetch: refetchSpecs } = useSpecsList({ projectPath });
|
||||
const { data: statsData } = useSpecStats({ projectPath });
|
||||
const { data: systemSettings } = useSystemSettings();
|
||||
const rebuildMutation = useRebuildSpecIndex();
|
||||
|
||||
const { data: statsData, refetch: refetchStats } = useSpecStats();
|
||||
// Convert specs data to display format
|
||||
const { projectSpecs, personalSpecs } = useMemo(() => {
|
||||
if (!specsListData?.specs) {
|
||||
return { projectSpecs: [], personalSpecs: [] };
|
||||
}
|
||||
|
||||
const specs: Spec[] = [];
|
||||
const personal: Spec[] = [];
|
||||
|
||||
for (const [dimension, entries] of Object.entries(specsListData.specs)) {
|
||||
for (const entry of entries) {
|
||||
const spec = specEntryToSpec(entry, dimension);
|
||||
if (dimension === 'personal') {
|
||||
personal.push(spec);
|
||||
} else {
|
||||
specs.push(spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { projectSpecs: specs, personalSpecs: personal };
|
||||
}, [specsListData]);
|
||||
|
||||
// Get hooks from system settings
|
||||
const hooks: HookConfig[] = useMemo(() => {
|
||||
return systemSettings?.recommendedHooks?.map(h => ({
|
||||
id: h.id,
|
||||
name: h.name,
|
||||
event: h.event as HookConfig['event'],
|
||||
command: h.command,
|
||||
description: h.description,
|
||||
scope: h.scope as HookConfig['scope'],
|
||||
enabled: h.autoInstall ?? false,
|
||||
installed: h.autoInstall ?? false,
|
||||
})) ?? [];
|
||||
}, [systemSettings]);
|
||||
|
||||
const isLoading = specsLoading;
|
||||
|
||||
const handleSpecEdit = (spec: Spec) => {
|
||||
setEditingSpec(spec);
|
||||
@@ -81,7 +134,11 @@ export function SpecsSettingsPage() {
|
||||
|
||||
const handleRebuildIndex = async () => {
|
||||
console.log('Rebuilding index...');
|
||||
// TODO: Implement rebuild logic
|
||||
rebuildMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
refetchSpecs();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filterSpecs = (specs: Spec[]) => {
|
||||
@@ -117,7 +174,7 @@ export function SpecsSettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Stats Summary */}
|
||||
{statsData && (
|
||||
{statsData?.dimensions && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{Object.entries(statsData.dimensions).map(([dim, data]) => (
|
||||
<Card key={dim}>
|
||||
@@ -178,7 +235,7 @@ export function SpecsSettingsPage() {
|
||||
scope: 'global',
|
||||
enabled: true,
|
||||
timeout: 5000,
|
||||
failMode: 'silent'
|
||||
failMode: 'continue'
|
||||
},
|
||||
{
|
||||
id: 'spec-injection-prompt',
|
||||
@@ -188,7 +245,7 @@ export function SpecsSettingsPage() {
|
||||
scope: 'project',
|
||||
enabled: true,
|
||||
timeout: 5000,
|
||||
failMode: 'silent'
|
||||
failMode: 'continue'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -219,11 +276,11 @@ export function SpecsSettingsPage() {
|
||||
<HookCard
|
||||
key={hook.id}
|
||||
hook={hook}
|
||||
isRecommended={true}
|
||||
isRecommendedCard={true}
|
||||
onInstall={() => console.log('Install:', hook.id)}
|
||||
onEdit={handleHookEdit}
|
||||
onToggle={handleHookToggle}
|
||||
onDelete={handleHookDelete}
|
||||
onUninstall={handleHookDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -261,7 +318,7 @@ export function SpecsSettingsPage() {
|
||||
hook={hook}
|
||||
onEdit={handleHookEdit}
|
||||
onToggle={handleHookToggle}
|
||||
onDelete={handleHookDelete}
|
||||
onUninstall={handleHookDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -273,7 +330,7 @@ export function SpecsSettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container py-6 max-w-6xl">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
@@ -343,8 +400,10 @@ export function SpecsSettingsPage() {
|
||||
<HookDialog
|
||||
open={hookDialogOpen}
|
||||
onOpenChange={setHookDialogOpen}
|
||||
hook={editingHook}
|
||||
onSave={handleHookSave}
|
||||
hook={editingHook ?? undefined}
|
||||
onSave={(hookData) => {
|
||||
handleHookSave(editingHook?.id ?? null, hookData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import chalk from 'chalk';
|
||||
|
||||
interface SpecOptions {
|
||||
dimension?: string;
|
||||
context?: string;
|
||||
keywords?: string;
|
||||
stdin?: boolean;
|
||||
json?: boolean;
|
||||
}
|
||||
@@ -60,11 +60,11 @@ function getProjectPath(hookCwd?: string): string {
|
||||
/**
|
||||
* Load action - load specs matching dimension/keywords.
|
||||
*
|
||||
* CLI mode: --dimension and --context options, outputs formatted markdown.
|
||||
* CLI mode: --dimension and --keywords options, outputs formatted markdown.
|
||||
* Hook mode: --stdin reads JSON {session_id, cwd, user_prompt}, outputs JSON {continue, systemMessage}.
|
||||
*/
|
||||
async function loadAction(options: SpecOptions): Promise<void> {
|
||||
const { stdin, dimension, context } = options;
|
||||
const { stdin, dimension, keywords: keywordsInput } = options;
|
||||
let projectPath: string;
|
||||
let stdinData: StdinData | undefined;
|
||||
|
||||
@@ -89,8 +89,8 @@ async function loadAction(options: SpecOptions): Promise<void> {
|
||||
try {
|
||||
const { loadSpecs } = await import('../tools/spec-loader.js');
|
||||
|
||||
const keywords = context
|
||||
? context.split(/[\s,]+/).filter(Boolean)
|
||||
const keywords = keywordsInput
|
||||
? keywordsInput.split(/[\s,]+/).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
const result = await loadSpecs({
|
||||
@@ -361,19 +361,31 @@ ${chalk.bold('SUBCOMMANDS')}
|
||||
|
||||
${chalk.bold('OPTIONS')}
|
||||
--dimension <dim> Target dimension: specs, roadmap, changelog, personal
|
||||
--context <text> Context text for keyword extraction (CLI mode)
|
||||
--keywords <text> Keywords for spec matching (space or comma separated)
|
||||
--stdin Read input from stdin (Hook mode)
|
||||
--json Output as JSON
|
||||
|
||||
${chalk.bold('KEYWORD CATEGORIES')}
|
||||
Use these predefined keywords to load specs for specific workflow stages:
|
||||
${chalk.cyan('exploration')} - Code exploration, analysis, debugging context
|
||||
${chalk.cyan('planning')} - Task planning, roadmap, requirements context
|
||||
${chalk.cyan('execution')} - Implementation, testing, deployment context
|
||||
|
||||
${chalk.bold('EXAMPLES')}
|
||||
${chalk.gray('# Initialize spec system:')}
|
||||
ccw spec init
|
||||
|
||||
${chalk.gray('# Load specs for a topic (CLI mode):')}
|
||||
ccw spec load --dimension specs --context "auth jwt security"
|
||||
${chalk.gray('# Load exploration-phase specs:')}
|
||||
ccw spec load --keywords exploration
|
||||
|
||||
${chalk.gray('# Load all matching specs:')}
|
||||
ccw spec load --context "implement user authentication"
|
||||
${chalk.gray('# Load planning-phase specs with auth topic:')}
|
||||
ccw spec load --keywords "planning auth"
|
||||
|
||||
${chalk.gray('# Load execution-phase specs:')}
|
||||
ccw spec load --keywords execution
|
||||
|
||||
${chalk.gray('# Load specs for a topic (CLI mode):')}
|
||||
ccw spec load --dimension specs --keywords "auth jwt security"
|
||||
|
||||
${chalk.gray('# Use as Claude Code hook (settings.json):')}
|
||||
ccw spec load --stdin
|
||||
@@ -24,6 +24,19 @@ import { join, basename, extname, relative } from 'path';
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Spec categories for workflow stage-based loading (used as keywords).
|
||||
* - exploration: Code exploration, analysis, debugging context
|
||||
* - planning: Task planning, roadmap, requirements context
|
||||
* - execution: Implementation, testing, deployment context
|
||||
*
|
||||
* Usage: Add these as keywords in spec frontmatter, e.g.:
|
||||
* keywords: [exploration, auth, security]
|
||||
*/
|
||||
export const SPEC_CATEGORIES = ['exploration', 'planning', 'execution'] as const;
|
||||
|
||||
export type SpecCategory = typeof SPEC_CATEGORIES[number];
|
||||
|
||||
/**
|
||||
* YAML frontmatter schema for spec MD files.
|
||||
*/
|
||||
@@ -45,7 +58,7 @@ export interface SpecIndexEntry {
|
||||
file: string;
|
||||
/** Dimension this spec belongs to */
|
||||
dimension: string;
|
||||
/** Keywords for matching against user prompts */
|
||||
/** Keywords for matching against user prompts (may include category markers) */
|
||||
keywords: string[];
|
||||
/** Whether this spec is required or optional */
|
||||
readMode: 'required' | 'optional';
|
||||
@@ -87,8 +100,9 @@ const VALID_READ_MODES = ['required', 'optional'] as const;
|
||||
const VALID_PRIORITIES = ['critical', 'high', 'medium', 'low'] as const;
|
||||
|
||||
/**
|
||||
* Directory name for spec index cache files.
|
||||
* Directory name for spec index cache files (inside .workflow/).
|
||||
*/
|
||||
const WORKFLOW_DIR = '.workflow';
|
||||
const SPEC_INDEX_DIR = '.spec-index';
|
||||
|
||||
// ============================================================================
|
||||
@@ -100,10 +114,10 @@ const SPEC_INDEX_DIR = '.spec-index';
|
||||
*
|
||||
* @param projectPath - Project root directory
|
||||
* @param dimension - The dimension name
|
||||
* @returns Absolute path to .spec-index/{dimension}.index.json
|
||||
* @returns Absolute path to .workflow/.spec-index/{dimension}.index.json
|
||||
*/
|
||||
export function getIndexPath(projectPath: string, dimension: string): string {
|
||||
return join(projectPath, SPEC_INDEX_DIR, `${dimension}.index.json`);
|
||||
return join(projectPath, WORKFLOW_DIR, SPEC_INDEX_DIR, `${dimension}.index.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +202,7 @@ export async function buildDimensionIndex(
|
||||
* @param projectPath - Project root directory
|
||||
*/
|
||||
export async function buildAllIndices(projectPath: string): Promise<void> {
|
||||
const indexDir = join(projectPath, SPEC_INDEX_DIR);
|
||||
const indexDir = join(projectPath, WORKFLOW_DIR, SPEC_INDEX_DIR);
|
||||
|
||||
// Ensure .spec-index directory exists
|
||||
if (!existsSync(indexDir)) {
|
||||
@@ -269,7 +283,7 @@ export async function getDimensionIndex(
|
||||
// Build fresh and cache
|
||||
const index = await buildDimensionIndex(projectPath, dimension);
|
||||
|
||||
const indexDir = join(projectPath, SPEC_INDEX_DIR);
|
||||
const indexDir = join(projectPath, WORKFLOW_DIR, SPEC_INDEX_DIR);
|
||||
if (!existsSync(indexDir)) {
|
||||
mkdirSync(indexDir, { recursive: true });
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SpecIndexEntry,
|
||||
DimensionIndex,
|
||||
SPEC_DIMENSIONS,
|
||||
SPEC_CATEGORIES,
|
||||
type SpecDimension,
|
||||
} from './spec-index-builder.js';
|
||||
|
||||
Reference in New Issue
Block a user