@winspan/claude-forge 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-provider/base-provider.d.ts +40 -0
- package/dist/ai-provider/base-provider.d.ts.map +1 -0
- package/dist/ai-provider/base-provider.js +156 -0
- package/dist/ai-provider/base-provider.js.map +1 -0
- package/dist/ai-provider/factory.d.ts +14 -0
- package/dist/ai-provider/factory.d.ts.map +1 -0
- package/dist/ai-provider/factory.js +20 -0
- package/dist/ai-provider/factory.js.map +1 -0
- package/dist/ai-provider/index.d.ts +7 -0
- package/dist/ai-provider/index.d.ts.map +1 -0
- package/dist/ai-provider/index.js +6 -0
- package/dist/ai-provider/index.js.map +1 -0
- package/dist/ai-provider/ollama-provider.d.ts +12 -0
- package/dist/ai-provider/ollama-provider.d.ts.map +1 -0
- package/dist/ai-provider/ollama-provider.js +33 -0
- package/dist/ai-provider/ollama-provider.js.map +1 -0
- package/dist/ai-provider/openai-provider.d.ts +13 -0
- package/dist/ai-provider/openai-provider.d.ts.map +1 -0
- package/dist/ai-provider/openai-provider.js +42 -0
- package/dist/ai-provider/openai-provider.js.map +1 -0
- package/dist/ai-provider/types.d.ts +48 -0
- package/dist/ai-provider/types.d.ts.map +1 -0
- package/dist/ai-provider/types.js +17 -0
- package/dist/ai-provider/types.js.map +1 -0
- package/dist/autopilot/intent-engine.d.ts +2 -2
- package/dist/autopilot/intent-engine.d.ts.map +1 -1
- package/dist/autopilot/intent-engine.js.map +1 -1
- package/dist/autopilot/knowledge-engine.d.ts +2 -2
- package/dist/autopilot/knowledge-engine.d.ts.map +1 -1
- package/dist/autopilot/knowledge-engine.js.map +1 -1
- package/dist/autopilot/quality-gate.d.ts +2 -2
- package/dist/autopilot/quality-gate.d.ts.map +1 -1
- package/dist/autopilot/quality-gate.js.map +1 -1
- package/dist/claudemd/index.d.ts +2 -2
- package/dist/claudemd/index.d.ts.map +1 -1
- package/dist/claudemd/index.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +19 -5
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +22 -14
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/{init.d.ts → init/doctor.d.ts} +1 -14
- package/dist/cli/commands/init/doctor.d.ts.map +1 -0
- package/dist/cli/commands/init/doctor.js +199 -0
- package/dist/cli/commands/init/doctor.js.map +1 -0
- package/dist/cli/commands/init/hook-manager.d.ts +30 -0
- package/dist/cli/commands/init/hook-manager.d.ts.map +1 -0
- package/dist/cli/commands/init/hook-manager.js +188 -0
- package/dist/cli/commands/init/hook-manager.js.map +1 -0
- package/dist/cli/commands/init/index.d.ts +5 -0
- package/dist/cli/commands/init/index.d.ts.map +1 -0
- package/dist/cli/commands/init/index.js +145 -0
- package/dist/cli/commands/init/index.js.map +1 -0
- package/dist/cli/commands/init/project-doctor.d.ts +15 -0
- package/dist/cli/commands/init/project-doctor.d.ts.map +1 -0
- package/dist/cli/commands/init/project-doctor.js +294 -0
- package/dist/cli/commands/init/project-doctor.js.map +1 -0
- package/dist/cli/commands/logs.d.ts.map +1 -1
- package/dist/cli/commands/logs.js +30 -13
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +11 -2
- package/dist/cli/tui.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/schema.d.ts +5 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -3
- package/dist/config/schema.js.map +1 -1
- package/dist/convention/convention-distiller.d.ts +2 -2
- package/dist/convention/convention-distiller.d.ts.map +1 -1
- package/dist/convention/convention-distiller.js.map +1 -1
- package/dist/convention/convention-evolver.d.ts +2 -2
- package/dist/convention/convention-evolver.d.ts.map +1 -1
- package/dist/convention/convention-evolver.js +1 -1
- package/dist/convention/convention-evolver.js.map +1 -1
- package/dist/daemon/engine-registry/init-autopilot.d.ts +2 -2
- package/dist/daemon/engine-registry/init-autopilot.d.ts.map +1 -1
- package/dist/daemon/engine-registry/init-autopilot.js.map +1 -1
- package/dist/daemon/engine-registry/init-distill.d.ts +2 -2
- package/dist/daemon/engine-registry/init-distill.d.ts.map +1 -1
- package/dist/daemon/engine-registry/init-distill.js.map +1 -1
- package/dist/daemon/engine-registry/init-pattern-engine.d.ts +2 -2
- package/dist/daemon/engine-registry/init-pattern-engine.d.ts.map +1 -1
- package/dist/daemon/engine-registry/init-pattern-engine.js.map +1 -1
- package/dist/daemon/engine-registry/init-pipeline.d.ts +2 -2
- package/dist/daemon/engine-registry/init-pipeline.d.ts.map +1 -1
- package/dist/daemon/engine-registry/init-pipeline.js.map +1 -1
- package/dist/daemon/engine-registry/init-resume.d.ts +2 -2
- package/dist/daemon/engine-registry/init-resume.d.ts.map +1 -1
- package/dist/daemon/engine-registry/init-resume.js.map +1 -1
- package/dist/daemon/engine-registry/init-skill-registry.d.ts +2 -2
- package/dist/daemon/engine-registry/init-skill-registry.d.ts.map +1 -1
- package/dist/daemon/engine-registry/init-skill-registry.js.map +1 -1
- package/dist/daemon/engine-registry.d.ts +2 -2
- package/dist/daemon/engine-registry.d.ts.map +1 -1
- package/dist/daemon/engine-registry.js.map +1 -1
- package/dist/daemon/handler-context.d.ts +2 -2
- package/dist/daemon/handler-context.d.ts.map +1 -1
- package/dist/daemon/handlers/post-tool-use-handler.d.ts.map +1 -1
- package/dist/daemon/handlers/post-tool-use-handler.js +7 -0
- package/dist/daemon/handlers/post-tool-use-handler.js.map +1 -1
- package/dist/daemon/handlers/user-prompt-handler.d.ts.map +1 -1
- package/dist/daemon/handlers/user-prompt-handler.js +1 -12
- package/dist/daemon/handlers/user-prompt-handler.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +34 -12
- package/dist/daemon/index.js.map +1 -1
- package/dist/distill/index.d.ts +2 -2
- package/dist/distill/index.d.ts.map +1 -1
- package/dist/distill/index.js.map +1 -1
- package/dist/doc-sync/index.d.ts +2 -2
- package/dist/doc-sync/index.d.ts.map +1 -1
- package/dist/doc-sync/index.js +2 -2
- package/dist/doc-sync/index.js.map +1 -1
- package/dist/pattern-engine/index.d.ts +2 -2
- package/dist/pattern-engine/index.d.ts.map +1 -1
- package/dist/pattern-engine/index.js.map +1 -1
- package/dist/pattern-engine/pattern-evolver.d.ts +2 -2
- package/dist/pattern-engine/pattern-evolver.d.ts.map +1 -1
- package/dist/pattern-engine/pattern-evolver.js +1 -1
- package/dist/pattern-engine/pattern-evolver.js.map +1 -1
- package/dist/pattern-engine/pattern-router.d.ts +2 -2
- package/dist/pattern-engine/pattern-router.d.ts.map +1 -1
- package/dist/pattern-engine/pattern-router.js +1 -1
- package/dist/pattern-engine/pattern-router.js.map +1 -1
- package/dist/pipeline/analyzer.d.ts +2 -2
- package/dist/pipeline/analyzer.d.ts.map +1 -1
- package/dist/pipeline/analyzer.js.map +1 -1
- package/dist/pipeline/index.d.ts +6 -2
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +13 -0
- package/dist/pipeline/index.js.map +1 -1
- package/dist/profile/index.d.ts +2 -2
- package/dist/profile/index.d.ts.map +1 -1
- package/dist/profile/index.js.map +1 -1
- package/dist/resume/context-gen.d.ts +2 -2
- package/dist/resume/context-gen.d.ts.map +1 -1
- package/dist/resume/context-gen.js.map +1 -1
- package/dist/resume/index.d.ts +2 -2
- package/dist/resume/index.d.ts.map +1 -1
- package/dist/resume/index.js.map +1 -1
- package/dist/skill-registry/collector.d.ts +2 -2
- package/dist/skill-registry/collector.d.ts.map +1 -1
- package/dist/skill-registry/collector.js.map +1 -1
- package/dist/skill-registry/evolver/community-distiller.d.ts +10 -0
- package/dist/skill-registry/evolver/community-distiller.d.ts.map +1 -0
- package/dist/skill-registry/evolver/community-distiller.js +117 -0
- package/dist/skill-registry/evolver/community-distiller.js.map +1 -0
- package/dist/skill-registry/evolver/generation-engine.d.ts +7 -0
- package/dist/skill-registry/evolver/generation-engine.d.ts.map +1 -0
- package/dist/skill-registry/evolver/generation-engine.js +65 -0
- package/dist/skill-registry/evolver/generation-engine.js.map +1 -0
- package/dist/skill-registry/evolver/index.d.ts +46 -0
- package/dist/skill-registry/evolver/index.d.ts.map +1 -0
- package/dist/skill-registry/evolver/index.js +138 -0
- package/dist/skill-registry/evolver/index.js.map +1 -0
- package/dist/skill-registry/evolver/keyword-evolver.d.ts +7 -0
- package/dist/skill-registry/evolver/keyword-evolver.d.ts.map +1 -0
- package/dist/skill-registry/evolver/keyword-evolver.js +111 -0
- package/dist/skill-registry/evolver/keyword-evolver.js.map +1 -0
- package/dist/skill-registry/evolver/official-evolver.d.ts +8 -0
- package/dist/skill-registry/evolver/official-evolver.d.ts.map +1 -0
- package/dist/skill-registry/evolver/official-evolver.js +79 -0
- package/dist/skill-registry/evolver/official-evolver.js.map +1 -0
- package/dist/skill-registry/evolver/optimization-engine.d.ts +7 -0
- package/dist/skill-registry/evolver/optimization-engine.d.ts.map +1 -0
- package/dist/skill-registry/evolver/optimization-engine.js +67 -0
- package/dist/skill-registry/evolver/optimization-engine.js.map +1 -0
- package/dist/skill-registry/evolver/retirement-engine.d.ts +7 -0
- package/dist/skill-registry/evolver/retirement-engine.d.ts.map +1 -0
- package/dist/skill-registry/evolver/retirement-engine.js +46 -0
- package/dist/skill-registry/evolver/retirement-engine.js.map +1 -0
- package/dist/skill-registry/evolver/shared.d.ts +13 -0
- package/dist/skill-registry/evolver/shared.d.ts.map +1 -0
- package/dist/skill-registry/evolver/shared.js +40 -0
- package/dist/skill-registry/evolver/shared.js.map +1 -0
- package/dist/skill-registry/index.d.ts +5 -5
- package/dist/skill-registry/index.d.ts.map +1 -1
- package/dist/skill-registry/index.js +4 -4
- package/dist/skill-registry/index.js.map +1 -1
- package/dist/skill-registry/{official-sync.d.ts → official-sync/index.d.ts} +4 -7
- package/dist/skill-registry/official-sync/index.d.ts.map +1 -0
- package/dist/skill-registry/official-sync/index.js +120 -0
- package/dist/skill-registry/official-sync/index.js.map +1 -0
- package/dist/skill-registry/official-sync/skill-definitions.d.ts +24 -0
- package/dist/skill-registry/official-sync/skill-definitions.d.ts.map +1 -0
- package/dist/skill-registry/{official-sync.js → official-sync/skill-definitions.js} +3 -121
- package/dist/skill-registry/official-sync/skill-definitions.js.map +1 -0
- package/dist/skill-registry/orchestrator.d.ts +2 -2
- package/dist/skill-registry/orchestrator.d.ts.map +1 -1
- package/dist/skill-registry/orchestrator.js.map +1 -1
- package/dist/skill-registry/project-learner.d.ts +2 -2
- package/dist/skill-registry/project-learner.d.ts.map +1 -1
- package/dist/skill-registry/project-learner.js.map +1 -1
- package/dist/utils/claude-api.d.ts +7 -5
- package/dist/utils/claude-api.d.ts.map +1 -1
- package/dist/utils/claude-api.js +23 -8
- package/dist/utils/claude-api.js.map +1 -1
- package/package.json +1 -1
- package/dist/autopilot/team-orchestrator.d.ts +0 -16
- package/dist/autopilot/team-orchestrator.d.ts.map +0 -1
- package/dist/autopilot/team-orchestrator.js +0 -56
- package/dist/autopilot/team-orchestrator.js.map +0 -1
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/init.js +0 -811
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/skill-registry/evolver.d.ts +0 -84
- package/dist/skill-registry/evolver.d.ts.map +0 -1
- package/dist/skill-registry/evolver.js +0 -603
- package/dist/skill-registry/evolver.js.map +0 -1
- package/dist/skill-registry/official-sync.d.ts.map +0 -1
- package/dist/skill-registry/official-sync.js.map +0 -1
|
@@ -1,811 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import readline from 'readline';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
|
-
import { success, info, warning, heading, error } from '../utils/output.js';
|
|
7
|
-
import { ConfigManager } from '../../config/index.js';
|
|
8
|
-
import { detectBestModel } from '../../utils/claude-api.js';
|
|
9
|
-
import { isRunning } from '../../daemon/lifecycle.js';
|
|
10
|
-
const FORGE_HOME = path.join(os.homedir(), '.claude-forge');
|
|
11
|
-
const CLAUDE_HOME = path.join(os.homedir(), '.claude');
|
|
12
|
-
const SETTINGS_PATH = path.join(CLAUDE_HOME, 'settings.json');
|
|
13
|
-
const HOOKS_DIR = path.join(FORGE_HOME, 'hooks');
|
|
14
|
-
const HOOK_SCRIPT_NAMES = [
|
|
15
|
-
'pre-tool-use.sh',
|
|
16
|
-
'post-tool-use.sh',
|
|
17
|
-
'check-context-limit.sh',
|
|
18
|
-
'user-prompt-submit.sh',
|
|
19
|
-
'notification.sh',
|
|
20
|
-
'stop.sh',
|
|
21
|
-
];
|
|
22
|
-
function isForgeCommand(cmd) {
|
|
23
|
-
return cmd.includes('claude-forge') || cmd.includes('.claude-forge');
|
|
24
|
-
}
|
|
25
|
-
/** 顶层或嵌套 hooks 内是否指向 claude-forge 脚本 */
|
|
26
|
-
function entryRefersToForge(entry) {
|
|
27
|
-
const top = entry.command;
|
|
28
|
-
if (typeof top === 'string' && isForgeCommand(top)) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
if (Array.isArray(entry.hooks)) {
|
|
32
|
-
return entry.hooks.some((sub) => {
|
|
33
|
-
if (typeof sub !== 'object' || sub === null)
|
|
34
|
-
return false;
|
|
35
|
-
const c = sub.command;
|
|
36
|
-
return typeof c === 'string' && isForgeCommand(c);
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
/** 将旧版扁平 { type, command } forge 项转为 matcher + hooks(Claude Code 当前要求) */
|
|
42
|
-
function normalizeLegacyForgeEntry(entry) {
|
|
43
|
-
if (Array.isArray(entry.hooks)) {
|
|
44
|
-
return entry;
|
|
45
|
-
}
|
|
46
|
-
const cmd = entry.command;
|
|
47
|
-
const type = typeof entry.type === 'string' ? entry.type : 'command';
|
|
48
|
-
if (typeof cmd === 'string' && isForgeCommand(cmd)) {
|
|
49
|
-
return {
|
|
50
|
-
matcher: typeof entry.matcher === 'string' ? entry.matcher : '',
|
|
51
|
-
hooks: [{ type, command: cmd }],
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return entry;
|
|
55
|
-
}
|
|
56
|
-
function parseHookEntry(raw) {
|
|
57
|
-
if (typeof raw === 'object' && raw !== null) {
|
|
58
|
-
return raw;
|
|
59
|
-
}
|
|
60
|
-
return {};
|
|
61
|
-
}
|
|
62
|
-
function getForgeHookEntries() {
|
|
63
|
-
return {
|
|
64
|
-
PreToolUse: {
|
|
65
|
-
matcher: '',
|
|
66
|
-
hooks: [
|
|
67
|
-
{
|
|
68
|
-
type: 'command',
|
|
69
|
-
command: `bash ${path.join(HOOKS_DIR, 'pre-tool-use.sh')}`,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
PostToolUse: {
|
|
74
|
-
matcher: '',
|
|
75
|
-
hooks: [
|
|
76
|
-
{
|
|
77
|
-
type: 'command',
|
|
78
|
-
command: `bash ${path.join(HOOKS_DIR, 'post-tool-use.sh')}`,
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
type: 'command',
|
|
82
|
-
command: `bash ${path.join(HOOKS_DIR, 'check-context-limit.sh')}`,
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
UserPromptSubmit: {
|
|
87
|
-
matcher: '',
|
|
88
|
-
hooks: [
|
|
89
|
-
{
|
|
90
|
-
type: 'command',
|
|
91
|
-
command: `bash ${path.join(HOOKS_DIR, 'user-prompt-submit.sh')}`,
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
Notification: {
|
|
96
|
-
matcher: '',
|
|
97
|
-
hooks: [
|
|
98
|
-
{
|
|
99
|
-
type: 'command',
|
|
100
|
-
command: `bash ${path.join(HOOKS_DIR, 'notification.sh')}`,
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
Stop: {
|
|
105
|
-
matcher: '',
|
|
106
|
-
hooks: [
|
|
107
|
-
{
|
|
108
|
-
type: 'command',
|
|
109
|
-
command: `bash ${path.join(HOOKS_DIR, 'stop.sh')}`,
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
function copyHookScripts() {
|
|
116
|
-
if (!fs.existsSync(HOOKS_DIR)) {
|
|
117
|
-
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
118
|
-
}
|
|
119
|
-
const srcHooksDir = path.join(path.dirname(new URL(import.meta.url).pathname), '../../hooks');
|
|
120
|
-
for (const file of HOOK_SCRIPT_NAMES) {
|
|
121
|
-
const src = path.join(srcHooksDir, file);
|
|
122
|
-
const dest = path.join(HOOKS_DIR, file);
|
|
123
|
-
if (fs.existsSync(src)) {
|
|
124
|
-
fs.copyFileSync(src, dest);
|
|
125
|
-
fs.chmodSync(dest, 0o755);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function backupSettings() {
|
|
130
|
-
if (!fs.existsSync(SETTINGS_PATH)) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
134
|
-
const backupPath = `${SETTINGS_PATH}.backup-${timestamp}`;
|
|
135
|
-
fs.copyFileSync(SETTINGS_PATH, backupPath);
|
|
136
|
-
return backupPath;
|
|
137
|
-
}
|
|
138
|
-
function injectHooks() {
|
|
139
|
-
let settings = {};
|
|
140
|
-
if (fs.existsSync(SETTINGS_PATH)) {
|
|
141
|
-
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
|
142
|
-
settings = JSON.parse(content);
|
|
143
|
-
}
|
|
144
|
-
if (!settings.hooks) {
|
|
145
|
-
settings.hooks = {};
|
|
146
|
-
}
|
|
147
|
-
const forgeHooks = getForgeHookEntries();
|
|
148
|
-
for (const [hookType, block] of Object.entries(forgeHooks)) {
|
|
149
|
-
const existingRaw = settings.hooks[hookType] ?? [];
|
|
150
|
-
const normalized = existingRaw.map((item) => {
|
|
151
|
-
const e = parseHookEntry(item);
|
|
152
|
-
return normalizeLegacyForgeEntry(e);
|
|
153
|
-
});
|
|
154
|
-
const alreadyInjected = normalized.some((item) => entryRefersToForge(item));
|
|
155
|
-
if (!alreadyInjected) {
|
|
156
|
-
settings.hooks[hookType] = [...normalized, block];
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
settings.hooks[hookType] = normalized;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// 确保 .claude 目录存在
|
|
163
|
-
if (!fs.existsSync(CLAUDE_HOME)) {
|
|
164
|
-
fs.mkdirSync(CLAUDE_HOME, { recursive: true });
|
|
165
|
-
}
|
|
166
|
-
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* 检测 ~/.claude-forge/hooks 脚本与 ~/.claude/settings.json 中 Forge 注入是否完整。
|
|
170
|
-
* Daemon 未运行仅作提示,不纳入 hooksHealthy。
|
|
171
|
-
*/
|
|
172
|
-
export function diagnoseForgeHookEnvironment() {
|
|
173
|
-
const items = [];
|
|
174
|
-
items.push({
|
|
175
|
-
id: 'forge_home',
|
|
176
|
-
label: 'Forge 数据目录',
|
|
177
|
-
ok: fs.existsSync(FORGE_HOME),
|
|
178
|
-
detail: FORGE_HOME,
|
|
179
|
-
});
|
|
180
|
-
for (const name of HOOK_SCRIPT_NAMES) {
|
|
181
|
-
const p = path.join(HOOKS_DIR, name);
|
|
182
|
-
items.push({
|
|
183
|
-
id: `hook_script_${name.replace(/\W/g, '_')}`,
|
|
184
|
-
label: `Hook 脚本: ${name}`,
|
|
185
|
-
ok: fs.existsSync(p),
|
|
186
|
-
detail: p,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
const forgeTypes = Object.keys(getForgeHookEntries());
|
|
190
|
-
if (!fs.existsSync(SETTINGS_PATH)) {
|
|
191
|
-
items.push({
|
|
192
|
-
id: 'claude_settings',
|
|
193
|
-
label: 'Claude settings.json',
|
|
194
|
-
ok: false,
|
|
195
|
-
detail: `缺失: ${SETTINGS_PATH}`,
|
|
196
|
-
});
|
|
197
|
-
for (const hookType of forgeTypes) {
|
|
198
|
-
items.push({
|
|
199
|
-
id: `settings_hook_${hookType}`,
|
|
200
|
-
label: `settings 中 ${hookType} 注入`,
|
|
201
|
-
ok: false,
|
|
202
|
-
detail: 'settings 文件不存在,无法检测',
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
items.push({
|
|
208
|
-
id: 'claude_settings',
|
|
209
|
-
label: 'Claude settings.json',
|
|
210
|
-
ok: true,
|
|
211
|
-
detail: SETTINGS_PATH,
|
|
212
|
-
});
|
|
213
|
-
try {
|
|
214
|
-
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
|
215
|
-
const settings = JSON.parse(content);
|
|
216
|
-
for (const hookType of forgeTypes) {
|
|
217
|
-
const raw = settings.hooks?.[hookType];
|
|
218
|
-
const normalized = (raw ?? []).map((item) => {
|
|
219
|
-
const e = parseHookEntry(item);
|
|
220
|
-
return normalizeLegacyForgeEntry(e);
|
|
221
|
-
});
|
|
222
|
-
const injected = normalized.some((item) => entryRefersToForge(item));
|
|
223
|
-
items.push({
|
|
224
|
-
id: `settings_hook_${hookType}`,
|
|
225
|
-
label: `settings 中 ${hookType} Forge 注入`,
|
|
226
|
-
ok: injected,
|
|
227
|
-
detail: injected ? '已检测到 Forge 命令' : '未检测到 Forge 命令,可能被移除或未初始化',
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
catch (e) {
|
|
232
|
-
items.push({
|
|
233
|
-
id: 'claude_settings_parse',
|
|
234
|
-
label: 'settings.json 解析',
|
|
235
|
-
ok: false,
|
|
236
|
-
detail: e instanceof Error ? e.message : String(e),
|
|
237
|
-
});
|
|
238
|
-
for (const hookType of forgeTypes) {
|
|
239
|
-
items.push({
|
|
240
|
-
id: `settings_hook_${hookType}`,
|
|
241
|
-
label: `settings 中 ${hookType} 注入`,
|
|
242
|
-
ok: false,
|
|
243
|
-
detail: 'JSON 解析失败,无法检测',
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const daemonUp = isRunning();
|
|
249
|
-
items.push({
|
|
250
|
-
id: 'daemon_process',
|
|
251
|
-
label: 'Daemon 进程',
|
|
252
|
-
ok: daemonUp,
|
|
253
|
-
detail: daemonUp ? '运行中' : '未运行;若修复后仍无事件,请执行 cf daemon start 或重启',
|
|
254
|
-
});
|
|
255
|
-
const hooksRelevant = items.filter((i) => i.id === 'forge_home' ||
|
|
256
|
-
i.id.startsWith('hook_script_') ||
|
|
257
|
-
i.id.startsWith('settings_hook_') ||
|
|
258
|
-
i.id === 'claude_settings' ||
|
|
259
|
-
i.id === 'claude_settings_parse');
|
|
260
|
-
const hooksHealthy = hooksRelevant.every((i) => i.ok);
|
|
261
|
-
return { items, hooksHealthy };
|
|
262
|
-
}
|
|
263
|
-
/** 重装 Hook 脚本并重新注入 settings.json(不改动项目配置与 API Key) */
|
|
264
|
-
export function repairForgeHookEnvironment() {
|
|
265
|
-
if (!fs.existsSync(FORGE_HOME)) {
|
|
266
|
-
fs.mkdirSync(FORGE_HOME, { recursive: true });
|
|
267
|
-
}
|
|
268
|
-
copyHookScripts();
|
|
269
|
-
const backupPath = backupSettings();
|
|
270
|
-
injectHooks();
|
|
271
|
-
return { backupPath };
|
|
272
|
-
}
|
|
273
|
-
function removeHooks() {
|
|
274
|
-
if (!fs.existsSync(SETTINGS_PATH)) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const content = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
|
278
|
-
const settings = JSON.parse(content);
|
|
279
|
-
if (!settings.hooks)
|
|
280
|
-
return;
|
|
281
|
-
for (const hookType of Object.keys(settings.hooks)) {
|
|
282
|
-
const entries = settings.hooks[hookType];
|
|
283
|
-
if (entries) {
|
|
284
|
-
const kept = entries.filter((item) => !entryRefersToForge(parseHookEntry(item)));
|
|
285
|
-
if (kept.length === 0) {
|
|
286
|
-
delete settings.hooks[hookType];
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
settings.hooks[hookType] = kept;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// 如果 hooks 为空,删除整个键
|
|
294
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
295
|
-
delete settings.hooks;
|
|
296
|
-
}
|
|
297
|
-
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
|
|
298
|
-
}
|
|
299
|
-
export function registerInitCommand(program) {
|
|
300
|
-
program
|
|
301
|
-
.command('init')
|
|
302
|
-
.description('Initialize Claude Forge in the current environment')
|
|
303
|
-
.action(async () => {
|
|
304
|
-
info('Initializing Claude Forge...');
|
|
305
|
-
// 1. 创建 .claude-forge 目录(全局数据目录)
|
|
306
|
-
if (!fs.existsSync(FORGE_HOME)) {
|
|
307
|
-
fs.mkdirSync(FORGE_HOME, { recursive: true });
|
|
308
|
-
}
|
|
309
|
-
// 1b. 在当前项目目录创建 .claude-forge/ 标记(让 daemon 识别为受管理项目)
|
|
310
|
-
const projectForgeDir = path.join(process.cwd(), '.claude-forge');
|
|
311
|
-
if (!fs.existsSync(projectForgeDir)) {
|
|
312
|
-
fs.mkdirSync(projectForgeDir, { recursive: true });
|
|
313
|
-
info(`Created project marker: .claude-forge/`);
|
|
314
|
-
}
|
|
315
|
-
// 2. 创建默认配置 + 自动检测最佳模型
|
|
316
|
-
const configPath = path.join(FORGE_HOME, 'config.yaml');
|
|
317
|
-
const manager = new ConfigManager(configPath);
|
|
318
|
-
if (!fs.existsSync(configPath)) {
|
|
319
|
-
manager.save({});
|
|
320
|
-
info('Created default configuration');
|
|
321
|
-
}
|
|
322
|
-
// 自动检测代理支持的最佳模型
|
|
323
|
-
try {
|
|
324
|
-
const bestModel = await detectBestModel();
|
|
325
|
-
if (bestModel) {
|
|
326
|
-
const current = manager.get();
|
|
327
|
-
manager.save({ distill: { ...current.distill, model: bestModel } });
|
|
328
|
-
info(`Auto-detected model: ${bestModel}`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
warning('Could not auto-detect model, using default');
|
|
333
|
-
}
|
|
334
|
-
// 3. 复制 Hook 脚本
|
|
335
|
-
copyHookScripts();
|
|
336
|
-
info('Hook scripts installed');
|
|
337
|
-
// 4. 备份 settings.json
|
|
338
|
-
const backupPath = backupSettings();
|
|
339
|
-
if (backupPath) {
|
|
340
|
-
info(`Settings backed up to ${path.basename(backupPath)}`);
|
|
341
|
-
}
|
|
342
|
-
// 5. 注入 Hooks
|
|
343
|
-
injectHooks();
|
|
344
|
-
info('Hooks injected into Claude Code settings');
|
|
345
|
-
// 5b. 同步官方 Convention(BMad、Harness Engineering)
|
|
346
|
-
try {
|
|
347
|
-
const { OfficialConventionSync } = await import('../../convention/official-sync.js');
|
|
348
|
-
const syncReport = new OfficialConventionSync().sync();
|
|
349
|
-
if (syncReport.added.length > 0) {
|
|
350
|
-
info(`官方规范已安装:${syncReport.added.join(', ')}`);
|
|
351
|
-
}
|
|
352
|
-
if (syncReport.updated.length > 0) {
|
|
353
|
-
info(`官方规范已更新:${syncReport.updated.join(', ')}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
catch (err) {
|
|
357
|
-
warning(`官方规范同步失败:${err instanceof Error ? err.message : String(err)}`);
|
|
358
|
-
}
|
|
359
|
-
// 6. API Key 检测与引导
|
|
360
|
-
let apiKeyConfigured = !!process.env.ANTHROPIC_API_KEY || !!manager.get().distill.api_key;
|
|
361
|
-
if (!apiKeyConfigured) {
|
|
362
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
363
|
-
const apiKey = await new Promise((resolve) => {
|
|
364
|
-
rl.question(' ANTHROPIC_API_KEY 未设置,请输入(直接回车跳过): ', (val) => {
|
|
365
|
-
rl.close();
|
|
366
|
-
resolve(val.trim());
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
if (apiKey) {
|
|
370
|
-
const current = manager.get();
|
|
371
|
-
manager.save({ distill: { ...current.distill, api_key: apiKey } });
|
|
372
|
-
info('API Key 已保存至 ~/.claude-forge/config.yaml');
|
|
373
|
-
apiKeyConfigured = true;
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
warning('跳过 API Key 配置,AI 功能将不可用,稍后可运行 `cf config set distill.api_key <key>` 补充');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
// 7. 自动启动 daemon(后台)
|
|
380
|
-
let daemonStarted = false;
|
|
381
|
-
if (!isRunning()) {
|
|
382
|
-
try {
|
|
383
|
-
const daemonScript = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../daemon/index.js');
|
|
384
|
-
const child = spawn('node', ['-e', `import('${daemonScript}').then(m => m.startDaemon(false))`], { detached: true, stdio: 'ignore', env: process.env });
|
|
385
|
-
child.unref();
|
|
386
|
-
daemonStarted = true;
|
|
387
|
-
info('Daemon 已在后台启动');
|
|
388
|
-
}
|
|
389
|
-
catch {
|
|
390
|
-
warning('Daemon 启动失败,请手动运行 `cf daemon start`');
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
daemonStarted = true;
|
|
395
|
-
info('Daemon 已在运行中');
|
|
396
|
-
}
|
|
397
|
-
// 8. 完成提示
|
|
398
|
-
process.stdout.write('\n');
|
|
399
|
-
success('Claude Forge 初始化完成!');
|
|
400
|
-
process.stdout.write('\n');
|
|
401
|
-
process.stdout.write(' 已完成:\n');
|
|
402
|
-
process.stdout.write(' ✓ Hook 脚本安装至 ~/.claude-forge/hooks/\n');
|
|
403
|
-
process.stdout.write(' ✓ Claude Code hooks 已注入 settings.json\n');
|
|
404
|
-
if (apiKeyConfigured)
|
|
405
|
-
process.stdout.write(' ✓ ANTHROPIC_API_KEY 已配置\n');
|
|
406
|
-
if (daemonStarted)
|
|
407
|
-
process.stdout.write(' ✓ Daemon 已在后台运行\n');
|
|
408
|
-
process.stdout.write('\n');
|
|
409
|
-
process.stdout.write(' 开始使用:\n');
|
|
410
|
-
process.stdout.write(' 在 Claude Code 对话中直接说需求,forge 会自动工作\n');
|
|
411
|
-
process.stdout.write(' cf — 打开交互式控制台\n');
|
|
412
|
-
process.stdout.write(' cf pattern list — 查看可用工作流\n');
|
|
413
|
-
process.stdout.write(' cf logs — 查看运行日志\n');
|
|
414
|
-
process.stdout.write('\n');
|
|
415
|
-
});
|
|
416
|
-
program
|
|
417
|
-
.command('uninit')
|
|
418
|
-
.description('Remove Claude Forge hooks and configuration')
|
|
419
|
-
.action(async () => {
|
|
420
|
-
info('Removing Claude Forge hooks...');
|
|
421
|
-
// 1. 备份 settings.json
|
|
422
|
-
const backupPath = backupSettings();
|
|
423
|
-
if (backupPath) {
|
|
424
|
-
info(`Settings backed up to ${path.basename(backupPath)}`);
|
|
425
|
-
}
|
|
426
|
-
// 2. 移除 Hooks
|
|
427
|
-
removeHooks();
|
|
428
|
-
info('Hooks removed from Claude Code settings');
|
|
429
|
-
success('Claude Forge hooks removed (data preserved in ~/.claude-forge/)');
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
export function registerDoctorCommand(program) {
|
|
433
|
-
program
|
|
434
|
-
.command('doctor')
|
|
435
|
-
.description('检测 Claude Code Hooks 与 Forge 脚本是否完整,可选一键修复')
|
|
436
|
-
.option('-f, --fix', '重装 ~/.claude-forge/hooks 脚本并重新注入 ~/.claude/settings.json')
|
|
437
|
-
.action(async (opts) => {
|
|
438
|
-
const globalOpts = program.opts();
|
|
439
|
-
const { hooksHealthy, items } = diagnoseForgeHookEnvironment();
|
|
440
|
-
if (opts.fix) {
|
|
441
|
-
const { backupPath } = repairForgeHookEnvironment();
|
|
442
|
-
const after = diagnoseForgeHookEnvironment();
|
|
443
|
-
if (globalOpts.json) {
|
|
444
|
-
console.log(JSON.stringify({
|
|
445
|
-
repaired: true,
|
|
446
|
-
backupPath,
|
|
447
|
-
beforeHooksHealthy: hooksHealthy,
|
|
448
|
-
hooksHealthy: after.hooksHealthy,
|
|
449
|
-
items: after.items,
|
|
450
|
-
}, null, 2));
|
|
451
|
-
}
|
|
452
|
-
else if (!globalOpts.quiet) {
|
|
453
|
-
heading('【Forge:环境修复】');
|
|
454
|
-
if (backupPath) {
|
|
455
|
-
info(`已备份 settings: ${path.basename(backupPath)}`);
|
|
456
|
-
}
|
|
457
|
-
info('已重装 Hook 脚本并完成 settings 注入。');
|
|
458
|
-
heading('【Forge:环境诊断】(修复后)');
|
|
459
|
-
for (const it of after.items) {
|
|
460
|
-
if (it.ok) {
|
|
461
|
-
success(`${it.label} — ${it.detail}`);
|
|
462
|
-
}
|
|
463
|
-
else if (it.id === 'daemon_process') {
|
|
464
|
-
warning(`${it.label} — ${it.detail}`);
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
error(`${it.label} — ${it.detail}`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
if (after.hooksHealthy) {
|
|
471
|
-
success('Hooks 相关环境已恢复正常。');
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
warning('修复后仍有未通过项,请根据上述列表排查。');
|
|
475
|
-
process.exitCode = 1;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
else if (!after.hooksHealthy) {
|
|
479
|
-
process.exitCode = 1;
|
|
480
|
-
}
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
if (globalOpts.json) {
|
|
484
|
-
console.log(JSON.stringify({ hooksHealthy, items }, null, 2));
|
|
485
|
-
if (!hooksHealthy) {
|
|
486
|
-
process.exitCode = 1;
|
|
487
|
-
}
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
if (!globalOpts.quiet) {
|
|
491
|
-
heading('【Forge:环境诊断】');
|
|
492
|
-
for (const it of items) {
|
|
493
|
-
if (it.ok) {
|
|
494
|
-
success(`${it.label} — ${it.detail}`);
|
|
495
|
-
}
|
|
496
|
-
else if (it.id === 'daemon_process') {
|
|
497
|
-
warning(`${it.label} — ${it.detail}`);
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
error(`${it.label} — ${it.detail}`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
if (hooksHealthy) {
|
|
505
|
-
if (!globalOpts.quiet) {
|
|
506
|
-
success('Hooks 相关环境正常。');
|
|
507
|
-
const daemonItem = items.find((i) => i.id === 'daemon_process');
|
|
508
|
-
if (daemonItem && !daemonItem.ok) {
|
|
509
|
-
info('提示:Daemon 未运行;若 Claude Code 中无编排事件,可执行 cf daemon start。');
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
if (!globalOpts.quiet) {
|
|
515
|
-
warning('检测到 Hooks 环境不完整,请执行: cf doctor --fix(或在 TUI「初始化/卸载」中选择一键修复)。');
|
|
516
|
-
}
|
|
517
|
-
process.exitCode = 1;
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
// ── 项目级环境诊断与修复 ────────────────────────────────────────────────────
|
|
521
|
-
/**
|
|
522
|
-
* 检测项目级 .claude-forge/ 目录下关键文件的完整性。
|
|
523
|
-
*/
|
|
524
|
-
export function diagnoseProjectEnvironment(projectPath) {
|
|
525
|
-
const forgeDir = path.join(projectPath, '.claude-forge');
|
|
526
|
-
const items = [];
|
|
527
|
-
// 1. 项目标记目录
|
|
528
|
-
const forgeDirExists = fs.existsSync(forgeDir);
|
|
529
|
-
items.push({
|
|
530
|
-
id: 'project_forge_dir',
|
|
531
|
-
label: '项目 .claude-forge/ 目录',
|
|
532
|
-
ok: forgeDirExists,
|
|
533
|
-
detail: forgeDirExists ? forgeDir : '缺失 — 项目未被 Forge 管理,请执行 cf init',
|
|
534
|
-
});
|
|
535
|
-
if (!forgeDirExists) {
|
|
536
|
-
return { items, healthy: false };
|
|
537
|
-
}
|
|
538
|
-
// 2. 规范绑定文件
|
|
539
|
-
const bindingsPath = path.join(forgeDir, 'project-conventions.json');
|
|
540
|
-
const bindingsExists = fs.existsSync(bindingsPath);
|
|
541
|
-
let bindings = [];
|
|
542
|
-
let bindingsValid = false;
|
|
543
|
-
if (bindingsExists) {
|
|
544
|
-
try {
|
|
545
|
-
bindings = JSON.parse(fs.readFileSync(bindingsPath, 'utf-8'));
|
|
546
|
-
bindingsValid = Array.isArray(bindings);
|
|
547
|
-
}
|
|
548
|
-
catch {
|
|
549
|
-
bindingsValid = false;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
items.push({
|
|
553
|
-
id: 'project_conventions_json',
|
|
554
|
-
label: '规范绑定 (project-conventions.json)',
|
|
555
|
-
ok: bindingsExists && bindingsValid,
|
|
556
|
-
detail: !bindingsExists
|
|
557
|
-
? '缺失 — 规范绑定丢失,需重新绑定'
|
|
558
|
-
: bindingsValid
|
|
559
|
-
? `${bindings.length} 个规范已绑定`
|
|
560
|
-
: '文件损坏,JSON 解析失败',
|
|
561
|
-
});
|
|
562
|
-
// 3. 自定义蒸馏规范
|
|
563
|
-
const customConvPath = path.join(forgeDir, 'custom-convention.yaml');
|
|
564
|
-
const hasCustomConv = fs.existsSync(customConvPath);
|
|
565
|
-
const hasCustomBinding = bindings.some(b => b.convention_id?.startsWith('custom-'));
|
|
566
|
-
items.push({
|
|
567
|
-
id: 'project_custom_convention',
|
|
568
|
-
label: '蒸馏规范 (custom-convention.yaml)',
|
|
569
|
-
ok: hasCustomConv || !hasCustomBinding,
|
|
570
|
-
detail: hasCustomConv
|
|
571
|
-
? '存在'
|
|
572
|
-
: hasCustomBinding
|
|
573
|
-
? '缺失 — 绑定记录指向自定义规范但文件不存在,需重新 distill'
|
|
574
|
-
: '未创建(无自定义规范绑定,正常)',
|
|
575
|
-
});
|
|
576
|
-
// 4. CLAUDE.md 中 convention 标记一致性
|
|
577
|
-
const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
|
|
578
|
-
if (fs.existsSync(claudeMdPath) && bindings.length > 0) {
|
|
579
|
-
const claudeMd = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
580
|
-
for (const b of bindings) {
|
|
581
|
-
const id = b.convention_id;
|
|
582
|
-
const startTag = `<!-- forge:convention-start:${id} -->`;
|
|
583
|
-
const hasMarker = claudeMd.includes(startTag);
|
|
584
|
-
items.push({
|
|
585
|
-
id: `convention_marker_${id}`,
|
|
586
|
-
label: `CLAUDE.md 规范标记: ${id}`,
|
|
587
|
-
ok: hasMarker,
|
|
588
|
-
detail: hasMarker ? '已注入' : '缺失 — 绑定存在但 CLAUDE.md 中无对应标记',
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
// 5. 蒸馏产物
|
|
593
|
-
const decisionsPath = path.join(forgeDir, 'decisions.md');
|
|
594
|
-
items.push({
|
|
595
|
-
id: 'project_decisions',
|
|
596
|
-
label: '蒸馏决策日志 (decisions.md)',
|
|
597
|
-
ok: fs.existsSync(decisionsPath),
|
|
598
|
-
detail: fs.existsSync(decisionsPath) ? '存在' : '缺失(运行时生成,首次蒸馏后自动创建)',
|
|
599
|
-
});
|
|
600
|
-
const timelinePath = path.join(forgeDir, 'timeline.md');
|
|
601
|
-
items.push({
|
|
602
|
-
id: 'project_timeline',
|
|
603
|
-
label: '蒸馏时间线 (timeline.md)',
|
|
604
|
-
ok: fs.existsSync(timelinePath),
|
|
605
|
-
detail: fs.existsSync(timelinePath) ? '存在' : '缺失(运行时生成,首次蒸馏后自动创建)',
|
|
606
|
-
});
|
|
607
|
-
// 6. Persona
|
|
608
|
-
const personaPath = path.join(forgeDir, 'persona.md');
|
|
609
|
-
items.push({
|
|
610
|
-
id: 'project_persona',
|
|
611
|
-
label: '项目人设 (persona.md)',
|
|
612
|
-
ok: fs.existsSync(personaPath),
|
|
613
|
-
detail: fs.existsSync(personaPath) ? '存在' : '缺失(运行时生成,首次 refine 后自动创建)',
|
|
614
|
-
});
|
|
615
|
-
// 7. Refine 状态
|
|
616
|
-
const refineStatePath = path.join(forgeDir, 'refine-state.json');
|
|
617
|
-
items.push({
|
|
618
|
-
id: 'project_refine_state',
|
|
619
|
-
label: 'Refine 状态 (refine-state.json)',
|
|
620
|
-
ok: fs.existsSync(refineStatePath),
|
|
621
|
-
detail: fs.existsSync(refineStatePath) ? '存在' : '缺失(运行时生成,首次 refine 后自动创建)',
|
|
622
|
-
});
|
|
623
|
-
// 健康标准:核心文件必须存在(蒸馏产物和 persona 是运行时生成的,不作为必须项)
|
|
624
|
-
const criticalIds = ['project_forge_dir', 'project_conventions_json', 'project_custom_convention'];
|
|
625
|
-
const markerIds = items.filter(i => i.id.startsWith('convention_marker_')).map(i => i.id);
|
|
626
|
-
const healthCheckIds = [...criticalIds, ...markerIds];
|
|
627
|
-
const healthy = healthCheckIds.every(id => items.find(i => i.id === id)?.ok ?? true);
|
|
628
|
-
return { items, healthy };
|
|
629
|
-
}
|
|
630
|
-
/**
|
|
631
|
-
* 修复项目级环境:重建丢失的绑定文件和 CLAUDE.md 标记。
|
|
632
|
-
*/
|
|
633
|
-
export async function repairProjectEnvironment(projectPath) {
|
|
634
|
-
const forgeDir = path.join(projectPath, '.claude-forge');
|
|
635
|
-
const repaired = [];
|
|
636
|
-
// 1. 确保 .claude-forge/ 存在
|
|
637
|
-
if (!fs.existsSync(forgeDir)) {
|
|
638
|
-
fs.mkdirSync(forgeDir, { recursive: true });
|
|
639
|
-
repaired.push('创建 .claude-forge/ 目录');
|
|
640
|
-
}
|
|
641
|
-
// 2. 规范绑定修复
|
|
642
|
-
const bindingsPath = path.join(forgeDir, 'project-conventions.json');
|
|
643
|
-
const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
|
|
644
|
-
let bindings = [];
|
|
645
|
-
if (fs.existsSync(bindingsPath)) {
|
|
646
|
-
try {
|
|
647
|
-
bindings = JSON.parse(fs.readFileSync(bindingsPath, 'utf-8'));
|
|
648
|
-
}
|
|
649
|
-
catch {
|
|
650
|
-
bindings = [];
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
// 2a. CLAUDE.md 有 convention 标记但绑定文件缺失 → 从标记反向重建
|
|
654
|
-
if (fs.existsSync(claudeMdPath)) {
|
|
655
|
-
const claudeMd = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
656
|
-
const markerRegex = /<!-- forge:convention-start:([^ ]+) -->/g;
|
|
657
|
-
let match;
|
|
658
|
-
const markeredIds = new Set();
|
|
659
|
-
while ((match = markerRegex.exec(claudeMd)) !== null) {
|
|
660
|
-
markeredIds.add(match[1]);
|
|
661
|
-
}
|
|
662
|
-
// 找出标记存在但绑定缺失的 convention
|
|
663
|
-
for (const id of markeredIds) {
|
|
664
|
-
if (!bindings.some(b => b.convention_id === id)) {
|
|
665
|
-
bindings.push({
|
|
666
|
-
convention_id: id,
|
|
667
|
-
bound_at: new Date().toISOString(),
|
|
668
|
-
project_path: projectPath,
|
|
669
|
-
});
|
|
670
|
-
repaired.push(`从 CLAUDE.md 标记恢复绑定: ${id}`);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
if (repaired.some(r => r.includes('恢复绑定'))) {
|
|
674
|
-
fs.writeFileSync(bindingsPath, JSON.stringify(bindings, null, 2), 'utf-8');
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
// 2b. 绑定存在但 CLAUDE.md 标记缺失 → 重新注入
|
|
678
|
-
if (bindings.length > 0 && fs.existsSync(claudeMdPath)) {
|
|
679
|
-
const { ConventionManager } = await import('../../convention/convention-manager.js');
|
|
680
|
-
const manager = new ConventionManager();
|
|
681
|
-
const claudeMd = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
682
|
-
for (const b of bindings) {
|
|
683
|
-
const startTag = `<!-- forge:convention-start:${b.convention_id} -->`;
|
|
684
|
-
if (!claudeMd.includes(startTag)) {
|
|
685
|
-
// 尝试加载 convention 并注入
|
|
686
|
-
try {
|
|
687
|
-
const conventions = manager.getActiveConventions(projectPath);
|
|
688
|
-
const conv = conventions.find(c => c.id === b.convention_id);
|
|
689
|
-
if (conv) {
|
|
690
|
-
manager.injectIntoClaudeMd(projectPath, conv);
|
|
691
|
-
repaired.push(`重新注入 CLAUDE.md 规范标记: ${b.convention_id}`);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
catch {
|
|
695
|
-
// convention 文件可能也不存在,跳过
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
// 2c. custom-convention.yaml 缺失但有 custom- 绑定 → 尝试重新 distill
|
|
701
|
-
const hasCustomBinding = bindings.some(b => b.convention_id?.startsWith('custom-'));
|
|
702
|
-
const customConvPath = path.join(forgeDir, 'custom-convention.yaml');
|
|
703
|
-
if (hasCustomBinding && !fs.existsSync(customConvPath)) {
|
|
704
|
-
try {
|
|
705
|
-
const { ConventionDistiller } = await import('../../convention/convention-distiller.js');
|
|
706
|
-
const { ConventionLoader } = await import('../../convention/convention-loader.js');
|
|
707
|
-
const { ConfigManager } = await import('../../config/index.js');
|
|
708
|
-
const { ClaudeAPI } = await import('../../utils/claude-api.js');
|
|
709
|
-
const config = new ConfigManager().get();
|
|
710
|
-
const apiKey = config.distill.api_key || undefined;
|
|
711
|
-
const api = new ClaudeAPI(apiKey, config.distill.model, config.distill.base_url || undefined);
|
|
712
|
-
const loader = new ConventionLoader();
|
|
713
|
-
const distiller = new ConventionDistiller(api, loader);
|
|
714
|
-
const sourceConventions = loader.loadAll();
|
|
715
|
-
if (sourceConventions.length > 0) {
|
|
716
|
-
await distiller.distill(projectPath, sourceConventions);
|
|
717
|
-
repaired.push('重新蒸馏生成 custom-convention.yaml');
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
repaired.push('custom-convention.yaml 重建跳过:无已安装的源规范');
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
catch (err) {
|
|
724
|
-
repaired.push(`custom-convention.yaml 重建失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
return repaired;
|
|
728
|
-
}
|
|
729
|
-
export function registerProjectDoctorCommand(program) {
|
|
730
|
-
program
|
|
731
|
-
.command('project-doctor')
|
|
732
|
-
.description('检测当前项目的 Forge 环境是否完整(规范绑定、蒸馏产物等),可选修复')
|
|
733
|
-
.option('-p, --project <path>', '项目路径', process.cwd())
|
|
734
|
-
.option('-f, --fix', '自动修复可恢复的问题')
|
|
735
|
-
.action(async (opts) => {
|
|
736
|
-
const globalOpts = program.opts();
|
|
737
|
-
const projectPath = path.resolve(opts.project);
|
|
738
|
-
const { healthy, items } = diagnoseProjectEnvironment(projectPath);
|
|
739
|
-
if (opts.fix) {
|
|
740
|
-
const repaired = await repairProjectEnvironment(projectPath);
|
|
741
|
-
const after = diagnoseProjectEnvironment(projectPath);
|
|
742
|
-
if (globalOpts.json) {
|
|
743
|
-
console.log(JSON.stringify({ repaired, beforeHealthy: healthy, healthy: after.healthy, items: after.items }, null, 2));
|
|
744
|
-
}
|
|
745
|
-
else if (!globalOpts.quiet) {
|
|
746
|
-
heading('【Forge:项目环境修复】');
|
|
747
|
-
if (repaired.length > 0) {
|
|
748
|
-
for (const r of repaired)
|
|
749
|
-
success(`✓ ${r}`);
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
info('无需修复。');
|
|
753
|
-
}
|
|
754
|
-
heading('【Forge:项目环境诊断】(修复后)');
|
|
755
|
-
for (const it of after.items) {
|
|
756
|
-
if (it.ok) {
|
|
757
|
-
success(`${it.label} — ${it.detail}`);
|
|
758
|
-
}
|
|
759
|
-
else if (it.id.startsWith('project_decisions') || it.id.startsWith('project_timeline') || it.id === 'project_persona' || it.id === 'project_refine_state') {
|
|
760
|
-
info(`${it.label} — ${it.detail}`);
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
error(`${it.label} — ${it.detail}`);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
if (after.healthy) {
|
|
767
|
-
success('项目环境正常。');
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
warning('修复后仍有未通过项,请根据上述列表排查。');
|
|
771
|
-
process.exitCode = 1;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
else if (!after.healthy) {
|
|
775
|
-
process.exitCode = 1;
|
|
776
|
-
}
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
if (globalOpts.json) {
|
|
780
|
-
console.log(JSON.stringify({ healthy, items }, null, 2));
|
|
781
|
-
if (!healthy)
|
|
782
|
-
process.exitCode = 1;
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
if (!globalOpts.quiet) {
|
|
786
|
-
heading(`【Forge:项目环境诊断】${projectPath}`);
|
|
787
|
-
for (const it of items) {
|
|
788
|
-
if (it.ok) {
|
|
789
|
-
success(`${it.label} — ${it.detail}`);
|
|
790
|
-
}
|
|
791
|
-
else if (it.id.startsWith('project_decisions') || it.id.startsWith('project_timeline') || it.id === 'project_persona' || it.id === 'project_refine_state') {
|
|
792
|
-
info(`${it.label} — ${it.detail}`);
|
|
793
|
-
}
|
|
794
|
-
else {
|
|
795
|
-
error(`${it.label} — ${it.detail}`);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (healthy) {
|
|
799
|
-
success('项目环境正常。');
|
|
800
|
-
}
|
|
801
|
-
else {
|
|
802
|
-
warning('检测到项目环境不完整,请执行: cf project-doctor --fix');
|
|
803
|
-
process.exitCode = 1;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
else if (!healthy) {
|
|
807
|
-
process.exitCode = 1;
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
//# sourceMappingURL=init.js.map
|