mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
refactor(spec): remove obsolete dimensions and update CLI options
This commit is contained in:
@@ -50,8 +50,6 @@ interface SpecDimensionStats {
|
||||
interface SpecStats {
|
||||
dimensions: {
|
||||
specs: SpecDimensionStats;
|
||||
roadmap: SpecDimensionStats;
|
||||
changelog: SpecDimensionStats;
|
||||
personal: SpecDimensionStats;
|
||||
};
|
||||
injectionLength?: {
|
||||
@@ -199,8 +197,6 @@ export function GlobalSettingsTab() {
|
||||
// Dimension display config
|
||||
const dimensionLabels: Record<string, string> = {
|
||||
specs: 'Specs',
|
||||
roadmap: 'Roadmap',
|
||||
changelog: 'Changelog',
|
||||
personal: 'Personal',
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// ========================================
|
||||
// Tab for managing spec injection control settings
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -25,7 +25,12 @@ import {
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
AlertTriangle,
|
||||
Plug,
|
||||
Download,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
@@ -39,8 +44,6 @@ export interface InjectionStats {
|
||||
export interface SpecStatsResponse {
|
||||
dimensions: {
|
||||
specs: { count: number; requiredCount: number };
|
||||
roadmap: { count: number; requiredCount: number };
|
||||
changelog: { count: number; requiredCount: number };
|
||||
personal: { count: number; requiredCount: number };
|
||||
};
|
||||
injectionLength: InjectionStats;
|
||||
@@ -64,6 +67,27 @@ export interface InjectionControlTabProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ========== Recommended Hooks Configuration ==========
|
||||
|
||||
const RECOMMENDED_HOOKS = [
|
||||
{
|
||||
id: 'spec-injection-session',
|
||||
name: 'Spec Context Injection (Session)',
|
||||
event: 'SessionStart',
|
||||
command: 'ccw spec load --stdin',
|
||||
scope: 'global' as const,
|
||||
description: 'Automatically inject spec context when Claude session starts',
|
||||
},
|
||||
{
|
||||
id: 'spec-injection-prompt',
|
||||
name: 'Spec Context Injection (Prompt)',
|
||||
event: 'UserPromptSubmit',
|
||||
command: 'ccw spec load --stdin',
|
||||
scope: 'project' as const,
|
||||
description: 'Inject spec context when user submits a prompt, matching keywords',
|
||||
},
|
||||
];
|
||||
|
||||
// ========== API Functions ==========
|
||||
|
||||
async function fetchSpecStats(): Promise<SpecStatsResponse> {
|
||||
@@ -119,6 +143,7 @@ function calculatePercentage(current: number, max: number): number {
|
||||
|
||||
export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const installHooksMutation = useInstallRecommendedHooks();
|
||||
|
||||
// State for stats
|
||||
const [stats, setStats] = useState<SpecStatsResponse | null>(null);
|
||||
@@ -138,6 +163,9 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// State for hooks installation
|
||||
const [installingHookIds, setInstallingHookIds] = useState<string[]>([]);
|
||||
|
||||
// Fetch stats
|
||||
const loadStats = useCallback(async () => {
|
||||
setStatsLoading(true);
|
||||
@@ -216,6 +244,61 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
// ========== Hooks Installation ==========
|
||||
|
||||
// Get installed hooks from system settings
|
||||
const installedHookIds = useMemo(() => {
|
||||
const installed = new Set<string>();
|
||||
// Check if hooks are already installed by checking system settings
|
||||
// For now, we'll track this via the mutation result
|
||||
return installed;
|
||||
}, []);
|
||||
|
||||
const installedCount = 0; // Will be updated when we have real data
|
||||
const allHooksInstalled = installedCount === RECOMMENDED_HOOKS.length;
|
||||
|
||||
// Install single hook
|
||||
const handleInstallHook = useCallback(async (hookId: string) => {
|
||||
setInstallingHookIds(prev => [...prev, hookId]);
|
||||
try {
|
||||
await installHooksMutation.installHooks([hookId], 'global');
|
||||
toast.success(
|
||||
formatMessage({ id: 'specs.hooks.installSuccess', defaultMessage: 'Hook installed successfully' })
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
formatMessage({ id: 'specs.hooks.installError', defaultMessage: 'Failed to install hook' })
|
||||
);
|
||||
console.error('Failed to install hook:', err);
|
||||
} finally {
|
||||
setInstallingHookIds(prev => prev.filter(id => id !== hookId));
|
||||
}
|
||||
}, [installHooksMutation, formatMessage]);
|
||||
|
||||
// Install all hooks
|
||||
const handleInstallAllHooks = useCallback(async () => {
|
||||
const uninstalledHooks = RECOMMENDED_HOOKS.filter(h => !installedHookIds.has(h.id));
|
||||
if (uninstalledHooks.length === 0) return;
|
||||
|
||||
setInstallingHookIds(uninstalledHooks.map(h => h.id));
|
||||
try {
|
||||
await installHooksMutation.installHooks(
|
||||
uninstalledHooks.map(h => h.id),
|
||||
'global'
|
||||
);
|
||||
toast.success(
|
||||
formatMessage({ id: 'specs.hooks.installAllSuccess', defaultMessage: 'All hooks installed successfully' })
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
formatMessage({ id: 'specs.hooks.installError', defaultMessage: 'Failed to install hooks' })
|
||||
);
|
||||
console.error('Failed to install hooks:', err);
|
||||
} finally {
|
||||
setInstallingHookIds([]);
|
||||
}
|
||||
}, [installedHookIds, installHooksMutation, formatMessage]);
|
||||
|
||||
// Calculate progress and status
|
||||
const currentLength = stats?.injectionLength?.withKeywords || 0;
|
||||
const maxLength = settings.maxLength;
|
||||
@@ -227,6 +310,98 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-6', className)}>
|
||||
{/* Recommended Hooks Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Plug className="h-5 w-5" />
|
||||
{formatMessage({ id: 'specs.recommendedHooks', defaultMessage: 'Recommended Hooks' })}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{formatMessage({
|
||||
id: 'specs.recommendedHooksDesc',
|
||||
defaultMessage: 'One-click install spec injection hooks for automatic context loading',
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
onClick={handleInstallAllHooks}
|
||||
disabled={allHooksInstalled || installingHookIds.length > 0}
|
||||
>
|
||||
{allHooksInstalled ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'specs.allHooksInstalled', defaultMessage: 'All Hooks Installed' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{formatMessage({ id: 'specs.installAllHooks', defaultMessage: 'Install All Hooks' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{installedCount} / {RECOMMENDED_HOOKS.length}{' '}
|
||||
{formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<a href="/hooks" target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="h-4 w-4 mr-1" />
|
||||
{formatMessage({ id: 'specs.manageHooks', defaultMessage: 'Manage Hooks' })}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{RECOMMENDED_HOOKS.map(hook => {
|
||||
const isInstalled = installedHookIds.has(hook.id);
|
||||
const isInstalling = installingHookIds.includes(hook.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={hook.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{hook.name}</span>
|
||||
{isInstalled && <CheckCircle2 className="h-4 w-4 text-green-500" />}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
{hook.description}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
||||
<span>
|
||||
{formatMessage({ id: 'specs.hookEvent', defaultMessage: 'Event' })}:{' '}
|
||||
<code className="bg-muted px-1 rounded">{hook.event}</code>
|
||||
</span>
|
||||
<span>
|
||||
{formatMessage({ id: 'specs.hookScope', defaultMessage: 'Scope' })}:{' '}
|
||||
<code className="bg-muted px-1 rounded">{hook.scope}</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={isInstalled ? 'outline' : 'default'}
|
||||
size="sm"
|
||||
disabled={isInstalled || isInstalling}
|
||||
onClick={() => handleInstallHook(hook.id)}
|
||||
>
|
||||
{isInstalling
|
||||
? formatMessage({ id: 'specs.installing', defaultMessage: 'Installing...' })
|
||||
: isInstalled
|
||||
? formatMessage({ id: 'specs.installed', defaultMessage: 'Installed' })
|
||||
: formatMessage({ id: 'specs.install', defaultMessage: 'Install' })}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Current Injection Status Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
/**
|
||||
* Spec dimension type
|
||||
*/
|
||||
export type SpecDimension = 'specs' | 'roadmap' | 'changelog' | 'personal';
|
||||
export type SpecDimension = 'specs' | 'personal';
|
||||
|
||||
/**
|
||||
* Spec read mode type
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"searchPlaceholder": "Search specs...",
|
||||
"rebuildIndex": "Rebuild Index",
|
||||
"loading": "Loading...",
|
||||
"noSpecs": "No specs found. Create specs in .workflow/ directory.",
|
||||
"noSpecs": "No specs found. Create specs in .ccw/ directory.",
|
||||
|
||||
"recommendedHooks": "Recommended Hooks",
|
||||
"recommendedHooksDesc": "One-click install system-preset spec injection hooks",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"searchPlaceholder": "搜索规范...",
|
||||
"rebuildIndex": "重建索引",
|
||||
"loading": "加载中...",
|
||||
"noSpecs": "未找到规范。请在 .workflow/ 目录中创建规范文件。",
|
||||
"noSpecs": "未找到规范。请在 .ccw/ 目录中创建规范文件。",
|
||||
|
||||
"recommendedHooks": "推荐钩子",
|
||||
"recommendedHooksDesc": "一键安装系统预设的规范注入钩子",
|
||||
|
||||
@@ -302,8 +302,8 @@ export function run(argv: string[]): void {
|
||||
program
|
||||
.command('spec [subcommand] [args...]')
|
||||
.description('Project spec management for conventions and guidelines')
|
||||
.option('--dimension <dim>', 'Target dimension: specs, roadmap, changelog, personal')
|
||||
.option('--context <text>', 'Context text for keyword extraction (CLI mode)')
|
||||
.option('--dimension <dim>', 'Target dimension: specs, personal')
|
||||
.option('--keywords <text>', 'Keywords for spec matching (CLI mode)')
|
||||
.option('--stdin', 'Read input from stdin (Hook mode)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((subcommand, args, options) => specCommand(subcommand, args, options));
|
||||
|
||||
@@ -266,7 +266,7 @@ export function filterSpecs(
|
||||
/**
|
||||
* Merge loaded spec content by dimension priority.
|
||||
*
|
||||
* Dimension priority order: personal(1) < changelog(2) < roadmap(3) < specs(4).
|
||||
* Dimension priority order: personal(1) < specs(2).
|
||||
* Within a dimension, specs are ordered by priority weight (critical > high > medium > low).
|
||||
*
|
||||
* @param specs - All loaded specs
|
||||
|
||||
Reference in New Issue
Block a user