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 {
|
interface SpecStats {
|
||||||
dimensions: {
|
dimensions: {
|
||||||
specs: SpecDimensionStats;
|
specs: SpecDimensionStats;
|
||||||
roadmap: SpecDimensionStats;
|
|
||||||
changelog: SpecDimensionStats;
|
|
||||||
personal: SpecDimensionStats;
|
personal: SpecDimensionStats;
|
||||||
};
|
};
|
||||||
injectionLength?: {
|
injectionLength?: {
|
||||||
@@ -199,8 +197,6 @@ export function GlobalSettingsTab() {
|
|||||||
// Dimension display config
|
// Dimension display config
|
||||||
const dimensionLabels: Record<string, string> = {
|
const dimensionLabels: Record<string, string> = {
|
||||||
specs: 'Specs',
|
specs: 'Specs',
|
||||||
roadmap: 'Roadmap',
|
|
||||||
changelog: 'Changelog',
|
|
||||||
personal: 'Personal',
|
personal: 'Personal',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// ========================================
|
// ========================================
|
||||||
// Tab for managing spec injection control settings
|
// 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 { useIntl } from 'react-intl';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -25,7 +25,12 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Plug,
|
||||||
|
Download,
|
||||||
|
CheckCircle2,
|
||||||
|
ExternalLink,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
||||||
|
|
||||||
// ========== Types ==========
|
// ========== Types ==========
|
||||||
|
|
||||||
@@ -39,8 +44,6 @@ export interface InjectionStats {
|
|||||||
export interface SpecStatsResponse {
|
export interface SpecStatsResponse {
|
||||||
dimensions: {
|
dimensions: {
|
||||||
specs: { count: number; requiredCount: number };
|
specs: { count: number; requiredCount: number };
|
||||||
roadmap: { count: number; requiredCount: number };
|
|
||||||
changelog: { count: number; requiredCount: number };
|
|
||||||
personal: { count: number; requiredCount: number };
|
personal: { count: number; requiredCount: number };
|
||||||
};
|
};
|
||||||
injectionLength: InjectionStats;
|
injectionLength: InjectionStats;
|
||||||
@@ -64,6 +67,27 @@ export interface InjectionControlTabProps {
|
|||||||
className?: string;
|
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 ==========
|
// ========== API Functions ==========
|
||||||
|
|
||||||
async function fetchSpecStats(): Promise<SpecStatsResponse> {
|
async function fetchSpecStats(): Promise<SpecStatsResponse> {
|
||||||
@@ -119,6 +143,7 @@ function calculatePercentage(current: number, max: number): number {
|
|||||||
|
|
||||||
export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const installHooksMutation = useInstallRecommendedHooks();
|
||||||
|
|
||||||
// State for stats
|
// State for stats
|
||||||
const [stats, setStats] = useState<SpecStatsResponse | null>(null);
|
const [stats, setStats] = useState<SpecStatsResponse | null>(null);
|
||||||
@@ -138,6 +163,9 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// State for hooks installation
|
||||||
|
const [installingHookIds, setInstallingHookIds] = useState<string[]>([]);
|
||||||
|
|
||||||
// Fetch stats
|
// Fetch stats
|
||||||
const loadStats = useCallback(async () => {
|
const loadStats = useCallback(async () => {
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
@@ -216,6 +244,61 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
setHasChanges(false);
|
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
|
// Calculate progress and status
|
||||||
const currentLength = stats?.injectionLength?.withKeywords || 0;
|
const currentLength = stats?.injectionLength?.withKeywords || 0;
|
||||||
const maxLength = settings.maxLength;
|
const maxLength = settings.maxLength;
|
||||||
@@ -227,6 +310,98 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('space-y-6', className)}>
|
<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 */}
|
{/* Current Injection Status Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
/**
|
/**
|
||||||
* Spec dimension type
|
* Spec dimension type
|
||||||
*/
|
*/
|
||||||
export type SpecDimension = 'specs' | 'roadmap' | 'changelog' | 'personal';
|
export type SpecDimension = 'specs' | 'personal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spec read mode type
|
* Spec read mode type
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"searchPlaceholder": "Search specs...",
|
"searchPlaceholder": "Search specs...",
|
||||||
"rebuildIndex": "Rebuild Index",
|
"rebuildIndex": "Rebuild Index",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"noSpecs": "No specs found. Create specs in .workflow/ directory.",
|
"noSpecs": "No specs found. Create specs in .ccw/ directory.",
|
||||||
|
|
||||||
"recommendedHooks": "Recommended Hooks",
|
"recommendedHooks": "Recommended Hooks",
|
||||||
"recommendedHooksDesc": "One-click install system-preset spec injection hooks",
|
"recommendedHooksDesc": "One-click install system-preset spec injection hooks",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"searchPlaceholder": "搜索规范...",
|
"searchPlaceholder": "搜索规范...",
|
||||||
"rebuildIndex": "重建索引",
|
"rebuildIndex": "重建索引",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"noSpecs": "未找到规范。请在 .workflow/ 目录中创建规范文件。",
|
"noSpecs": "未找到规范。请在 .ccw/ 目录中创建规范文件。",
|
||||||
|
|
||||||
"recommendedHooks": "推荐钩子",
|
"recommendedHooks": "推荐钩子",
|
||||||
"recommendedHooksDesc": "一键安装系统预设的规范注入钩子",
|
"recommendedHooksDesc": "一键安装系统预设的规范注入钩子",
|
||||||
|
|||||||
@@ -302,8 +302,8 @@ export function run(argv: string[]): void {
|
|||||||
program
|
program
|
||||||
.command('spec [subcommand] [args...]')
|
.command('spec [subcommand] [args...]')
|
||||||
.description('Project spec management for conventions and guidelines')
|
.description('Project spec management for conventions and guidelines')
|
||||||
.option('--dimension <dim>', 'Target dimension: specs, roadmap, changelog, personal')
|
.option('--dimension <dim>', 'Target dimension: specs, personal')
|
||||||
.option('--context <text>', 'Context text for keyword extraction (CLI mode)')
|
.option('--keywords <text>', 'Keywords for spec matching (CLI mode)')
|
||||||
.option('--stdin', 'Read input from stdin (Hook mode)')
|
.option('--stdin', 'Read input from stdin (Hook mode)')
|
||||||
.option('--json', 'Output as JSON')
|
.option('--json', 'Output as JSON')
|
||||||
.action((subcommand, args, options) => specCommand(subcommand, args, options));
|
.action((subcommand, args, options) => specCommand(subcommand, args, options));
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export function filterSpecs(
|
|||||||
/**
|
/**
|
||||||
* Merge loaded spec content by dimension priority.
|
* 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).
|
* Within a dimension, specs are ordered by priority weight (critical > high > medium > low).
|
||||||
*
|
*
|
||||||
* @param specs - All loaded specs
|
* @param specs - All loaded specs
|
||||||
|
|||||||
Reference in New Issue
Block a user