@vibecodetown/mcp-server 2.1.3 → 2.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/build/auth/credential_store.js +146 -0
- package/build/auth/index.js +2 -0
- package/build/control_plane/gate.js +52 -70
- package/build/index.js +2 -0
- package/build/local-mode/git.js +36 -22
- package/build/local-mode/install-state.js +37 -0
- package/build/local-mode/paths.js +1 -0
- package/build/local-mode/project-state.js +176 -0
- package/build/local-mode/setup.js +46 -0
- package/build/local-mode/templates.js +3 -3
- package/build/local-mode/version-lock.js +53 -0
- package/build/runtime/cli_invoker.js +416 -0
- package/build/tools/vibe_pm/briefing.js +2 -1
- package/build/tools/vibe_pm/finalize_work.js +40 -4
- package/build/tools/vibe_pm/force_override.js +104 -0
- package/build/tools/vibe_pm/index.js +73 -2
- package/build/tools/vibe_pm/list_rules.js +135 -0
- package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
- package/build/tools/vibe_pm/publish_mcp.js +271 -0
- package/build/tools/vibe_pm/run_app.js +48 -45
- package/build/tools/vibe_pm/save_rule.js +120 -0
- package/build/tools/vibe_pm/undo_last_task.js +16 -20
- package/build/version-check.js +5 -5
- package/build/vibe-cli.js +610 -37
- package/package.json +4 -4
|
@@ -23,6 +23,9 @@ import { exportOutput } from "./export_output.js";
|
|
|
23
23
|
import { searchOss } from "./search_oss.js";
|
|
24
24
|
import { zoektEvidence } from "./zoekt_evidence.js";
|
|
25
25
|
import { gate, gateInputSchema, gateOutputSchema } from "./gate.js";
|
|
26
|
+
import { forceOverride, forceOverrideInputSchema, forceOverrideOutputSchema } from "./force_override.js";
|
|
27
|
+
import { saveRule, saveRuleInputSchema, saveRuleOutputSchema } from "./save_rule.js";
|
|
28
|
+
import { listRules, listRulesInputSchema, listRulesOutputSchema } from "./list_rules.js";
|
|
26
29
|
import { getAuthGate } from "../../auth/index.js";
|
|
27
30
|
import { ToolErrorOutputSchema } from "../../generated/tool_error_output.js";
|
|
28
31
|
import { decorateToolOutputText, notifyDesktopBestEffort } from "../../dx/activity.js";
|
|
@@ -491,6 +494,60 @@ async function vibePmGate(input) {
|
|
|
491
494
|
});
|
|
492
495
|
}
|
|
493
496
|
}
|
|
497
|
+
async function vibePmForceOverride(input) {
|
|
498
|
+
const startedAt = Date.now();
|
|
499
|
+
try {
|
|
500
|
+
const result = await forceOverride(input);
|
|
501
|
+
const validated = forceOverrideOutputSchema.parse(result);
|
|
502
|
+
return toolResult("vibe_pm.force_override", validated, Date.now() - startedAt);
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
if (e instanceof McpError) {
|
|
506
|
+
return errorResult("vibe_pm.force_override", e);
|
|
507
|
+
}
|
|
508
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
509
|
+
return err("vibe_pm.force_override", "force_override_failed", {
|
|
510
|
+
message: msg,
|
|
511
|
+
recovery: "acknowledge_risks: true 를 반드시 포함해야 합니다."
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function vibePmSaveRule(input) {
|
|
516
|
+
const startedAt = Date.now();
|
|
517
|
+
try {
|
|
518
|
+
const result = await saveRule(input);
|
|
519
|
+
const validated = saveRuleOutputSchema.parse(result);
|
|
520
|
+
return toolResult("vibe_pm.save_rule", validated, Date.now() - startedAt);
|
|
521
|
+
}
|
|
522
|
+
catch (e) {
|
|
523
|
+
if (e instanceof McpError) {
|
|
524
|
+
return errorResult("vibe_pm.save_rule", e);
|
|
525
|
+
}
|
|
526
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
527
|
+
return err("vibe_pm.save_rule", "save_rule_failed", {
|
|
528
|
+
message: msg,
|
|
529
|
+
recovery: "규칙 제목, 트리거, 액션을 확인하세요."
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function vibePmListRules(input) {
|
|
534
|
+
const startedAt = Date.now();
|
|
535
|
+
try {
|
|
536
|
+
const result = await listRules(input);
|
|
537
|
+
const validated = listRulesOutputSchema.parse(result);
|
|
538
|
+
return toolResult("vibe_pm.list_rules", validated, Date.now() - startedAt);
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
if (e instanceof McpError) {
|
|
542
|
+
return errorResult("vibe_pm.list_rules", e);
|
|
543
|
+
}
|
|
544
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
545
|
+
return err("vibe_pm.list_rules", "list_rules_failed", {
|
|
546
|
+
message: msg,
|
|
547
|
+
recovery: ".vibe 폴더가 있는지 확인하세요."
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
494
551
|
// ============================================================
|
|
495
552
|
// Export
|
|
496
553
|
// ============================================================
|
|
@@ -518,6 +575,9 @@ export function defineVibePmTools() {
|
|
|
518
575
|
searchOssInputSchema,
|
|
519
576
|
zoektEvidenceInputSchema,
|
|
520
577
|
gateInputSchema,
|
|
578
|
+
forceOverrideInputSchema,
|
|
579
|
+
saveRuleInputSchema,
|
|
580
|
+
listRulesInputSchema,
|
|
521
581
|
// Tool handlers
|
|
522
582
|
vibePmBriefing,
|
|
523
583
|
vibePmGetDecision,
|
|
@@ -539,7 +599,10 @@ export function defineVibePmTools() {
|
|
|
539
599
|
vibePmExportOutput,
|
|
540
600
|
vibePmSearchOss,
|
|
541
601
|
vibePmZoektEvidence,
|
|
542
|
-
vibePmGate
|
|
602
|
+
vibePmGate,
|
|
603
|
+
vibePmForceOverride,
|
|
604
|
+
vibePmSaveRule,
|
|
605
|
+
vibePmListRules
|
|
543
606
|
};
|
|
544
607
|
}
|
|
545
608
|
// ============================================================
|
|
@@ -589,5 +652,13 @@ export const VIBE_PM_TOOL_DESCRIPTIONS = {
|
|
|
589
652
|
사용 시점: 기능 구현 및 검수 후, 결과물을 다른 형태로 배포/공유하고 싶을 때`,
|
|
590
653
|
"vibe_pm.gate": `작업 지시서가 시스템 설계 규칙을 준수하는지 검증합니다. (Schema/Path/Runtime/Semgrep Gate)
|
|
591
654
|
사용 시점: 실행 전 work order가 규칙을 준수하는지 직접 확인하고 싶을 때
|
|
592
|
-
BLOCK이 없으면 실행 가능, BLOCK이 있으면 수정이
|
|
655
|
+
BLOCK이 없으면 실행 가능, BLOCK이 있으면 수정이 필요합니다.`,
|
|
656
|
+
"vibe_pm.force_override": `차단된 작업을 강제로 실행할 수 있는 탈출 경로입니다. (P2-1)
|
|
657
|
+
사용 시점: 정책에 의해 차단되었지만 긴급히 실행이 필요할 때
|
|
658
|
+
⚠ 주의: acknowledge_risks=true 필수, 모든 사용은 감사 로그에 기록됩니다.`,
|
|
659
|
+
"vibe_pm.save_rule": `프로젝트별 워크플로우 규칙을 저장합니다.
|
|
660
|
+
사용 시점: 사용자가 "~할 때마다 ~해", "항상 ~해줘" 같은 반복 패턴을 지시할 때
|
|
661
|
+
저장 위치: .vibe/project_rules.md`,
|
|
662
|
+
"vibe_pm.list_rules": `현재 프로젝트에 저장된 워크플로우 규칙 목록을 조회합니다.
|
|
663
|
+
사용 시점: 작업 시작 전 적용할 규칙을 확인하고 싶을 때`
|
|
593
664
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/list_rules.ts
|
|
2
|
+
// vibe_pm.list_rules - 프로젝트 워크플로우 규칙 조회
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import * as fs from "fs/promises";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { getVibeRepoPaths } from "../../local-mode/paths.js";
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Schema
|
|
9
|
+
// ============================================================
|
|
10
|
+
export const listRulesInputSchema = z.object({
|
|
11
|
+
base_path: z.string().describe("프로젝트 루트 경로")
|
|
12
|
+
}).strict();
|
|
13
|
+
const ruleSchema = z.object({
|
|
14
|
+
title: z.string(),
|
|
15
|
+
trigger: z.string(),
|
|
16
|
+
action: z.string(),
|
|
17
|
+
example: z.string().optional(),
|
|
18
|
+
registered_at: z.string().optional()
|
|
19
|
+
});
|
|
20
|
+
export const listRulesOutputSchema = z.object({
|
|
21
|
+
status: z.enum(["ok", "empty", "error"]),
|
|
22
|
+
message: z.string(),
|
|
23
|
+
rules_file: z.string().describe(".vibe/project_rules.md 경로"),
|
|
24
|
+
rules: z.array(ruleSchema),
|
|
25
|
+
rule_count: z.number()
|
|
26
|
+
}).strict();
|
|
27
|
+
// ============================================================
|
|
28
|
+
// Implementation
|
|
29
|
+
// ============================================================
|
|
30
|
+
const RULES_FILENAME = "project_rules.md";
|
|
31
|
+
/**
|
|
32
|
+
* 규칙 파일 경로 반환
|
|
33
|
+
*/
|
|
34
|
+
function getRulesFilePath(basePath) {
|
|
35
|
+
const { vibeDir } = getVibeRepoPaths(basePath);
|
|
36
|
+
return path.join(vibeDir, RULES_FILENAME);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 마크다운에서 규칙 파싱
|
|
40
|
+
*/
|
|
41
|
+
function parseRules(content) {
|
|
42
|
+
const rules = [];
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
let currentRule = null;
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
// 새 규칙 시작 (## 제목)
|
|
47
|
+
if (line.startsWith("## ") && !line.includes("Project Workflow Rules")) {
|
|
48
|
+
// 이전 규칙 저장
|
|
49
|
+
if (currentRule && currentRule.title) {
|
|
50
|
+
rules.push({
|
|
51
|
+
title: currentRule.title,
|
|
52
|
+
trigger: currentRule.trigger ?? "",
|
|
53
|
+
action: currentRule.action ?? "",
|
|
54
|
+
example: currentRule.example,
|
|
55
|
+
registered_at: currentRule.registered_at
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
currentRule = { title: line.substring(3).trim() };
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!currentRule)
|
|
62
|
+
continue;
|
|
63
|
+
// 규칙 필드 파싱
|
|
64
|
+
if (line.startsWith("- **언제**:")) {
|
|
65
|
+
currentRule.trigger = line.replace("- **언제**:", "").trim();
|
|
66
|
+
}
|
|
67
|
+
else if (line.startsWith("- **무엇을**:")) {
|
|
68
|
+
currentRule.action = line.replace("- **무엇을**:", "").trim();
|
|
69
|
+
}
|
|
70
|
+
else if (line.startsWith("- **예시**:")) {
|
|
71
|
+
currentRule.example = line.replace("- **예시**:", "").trim();
|
|
72
|
+
}
|
|
73
|
+
else if (line.startsWith("- **등록일**:")) {
|
|
74
|
+
currentRule.registered_at = line.replace("- **등록일**:", "").trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// 마지막 규칙 저장
|
|
78
|
+
if (currentRule && currentRule.title) {
|
|
79
|
+
rules.push({
|
|
80
|
+
title: currentRule.title,
|
|
81
|
+
trigger: currentRule.trigger ?? "",
|
|
82
|
+
action: currentRule.action ?? "",
|
|
83
|
+
example: currentRule.example,
|
|
84
|
+
registered_at: currentRule.registered_at
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return rules;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 규칙 목록 조회
|
|
91
|
+
*/
|
|
92
|
+
export async function listRules(input) {
|
|
93
|
+
const rulesFile = getRulesFilePath(input.base_path);
|
|
94
|
+
try {
|
|
95
|
+
// 파일 읽기
|
|
96
|
+
const content = await fs.readFile(rulesFile, "utf-8");
|
|
97
|
+
const rules = parseRules(content);
|
|
98
|
+
if (rules.length === 0) {
|
|
99
|
+
return {
|
|
100
|
+
status: "empty",
|
|
101
|
+
message: "저장된 규칙이 없습니다.",
|
|
102
|
+
rules_file: rulesFile,
|
|
103
|
+
rules: [],
|
|
104
|
+
rule_count: 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
status: "ok",
|
|
109
|
+
message: `${rules.length}개의 규칙이 등록되어 있습니다.`,
|
|
110
|
+
rules_file: rulesFile,
|
|
111
|
+
rules,
|
|
112
|
+
rule_count: rules.length
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
// 파일이 없는 경우
|
|
117
|
+
if (e.code === "ENOENT") {
|
|
118
|
+
return {
|
|
119
|
+
status: "empty",
|
|
120
|
+
message: "규칙 파일이 없습니다. vibe_pm.save_rule로 규칙을 추가하세요.",
|
|
121
|
+
rules_file: rulesFile,
|
|
122
|
+
rules: [],
|
|
123
|
+
rule_count: 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
127
|
+
return {
|
|
128
|
+
status: "error",
|
|
129
|
+
message: `규칙 조회 실패: ${msg}`,
|
|
130
|
+
rules_file: rulesFile,
|
|
131
|
+
rules: [],
|
|
132
|
+
rule_count: 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/pre_commit_analysis.ts
|
|
2
|
+
// Pre-commit analysis: categorize changes and suggest staging strategy
|
|
3
|
+
import { simpleGit } from "simple-git";
|
|
4
|
+
/**
|
|
5
|
+
* Categorize a file based on its path
|
|
6
|
+
*/
|
|
7
|
+
function categorizeFile(filePath) {
|
|
8
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
9
|
+
// MCP TypeScript
|
|
10
|
+
if (normalized.includes("adapters/mcp-ts/src/"))
|
|
11
|
+
return "mcp_source";
|
|
12
|
+
if (normalized.includes("adapters/mcp-ts/build/"))
|
|
13
|
+
return "mcp_build";
|
|
14
|
+
if (normalized.includes("adapters/mcp-ts/test"))
|
|
15
|
+
return "mcp_test";
|
|
16
|
+
if (normalized.includes("adapters/mcp-ts/"))
|
|
17
|
+
return "mcp_other";
|
|
18
|
+
// Engines (Rust)
|
|
19
|
+
if (normalized.includes("engines/") && normalized.endsWith(".rs"))
|
|
20
|
+
return "engine_rust";
|
|
21
|
+
if (normalized.includes("engines/"))
|
|
22
|
+
return "engine_other";
|
|
23
|
+
// Documentation
|
|
24
|
+
if (normalized.includes("docs/"))
|
|
25
|
+
return "docs";
|
|
26
|
+
if (normalized.endsWith(".md"))
|
|
27
|
+
return "docs";
|
|
28
|
+
// Python
|
|
29
|
+
if (normalized.includes("vibecoding_helper/"))
|
|
30
|
+
return "python_core";
|
|
31
|
+
if (normalized.endsWith(".py"))
|
|
32
|
+
return "python";
|
|
33
|
+
// Config/CI
|
|
34
|
+
if (normalized.includes(".github/"))
|
|
35
|
+
return "ci";
|
|
36
|
+
if (normalized.includes(".vibe/"))
|
|
37
|
+
return "vibe_config";
|
|
38
|
+
if (normalized.includes("schemas/"))
|
|
39
|
+
return "schemas";
|
|
40
|
+
if (normalized.endsWith(".json") ||
|
|
41
|
+
normalized.endsWith(".yaml") ||
|
|
42
|
+
normalized.endsWith(".yml") ||
|
|
43
|
+
normalized.endsWith(".toml"))
|
|
44
|
+
return "config";
|
|
45
|
+
// Generated
|
|
46
|
+
if (normalized.includes("/generated/"))
|
|
47
|
+
return "generated";
|
|
48
|
+
// Tests
|
|
49
|
+
if (normalized.includes("/test") || normalized.includes("/tests/"))
|
|
50
|
+
return "tests";
|
|
51
|
+
// Build artifacts
|
|
52
|
+
if (normalized.includes("/dist/") ||
|
|
53
|
+
normalized.includes("/build/") ||
|
|
54
|
+
normalized.includes("/target/"))
|
|
55
|
+
return "build_artifacts";
|
|
56
|
+
// Lock files
|
|
57
|
+
if (normalized.endsWith("-lock.json") ||
|
|
58
|
+
normalized.endsWith(".lock") ||
|
|
59
|
+
normalized.endsWith("-lock.yaml"))
|
|
60
|
+
return "lock_files";
|
|
61
|
+
return "other";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get human-readable category info
|
|
65
|
+
*/
|
|
66
|
+
function getCategoryInfo(category) {
|
|
67
|
+
const info = {
|
|
68
|
+
mcp_source: {
|
|
69
|
+
name: "MCP 소스 코드",
|
|
70
|
+
description: "TypeScript MCP 서버 소스 (빌드 후 npm 배포 필요할 수 있음)",
|
|
71
|
+
suggested: true,
|
|
72
|
+
},
|
|
73
|
+
mcp_build: {
|
|
74
|
+
name: "MCP 빌드 결과물",
|
|
75
|
+
description: "컴파일된 JavaScript (보통 gitignore됨)",
|
|
76
|
+
suggested: false,
|
|
77
|
+
},
|
|
78
|
+
mcp_test: {
|
|
79
|
+
name: "MCP 테스트",
|
|
80
|
+
description: "TypeScript 테스트 파일",
|
|
81
|
+
suggested: true,
|
|
82
|
+
},
|
|
83
|
+
mcp_other: {
|
|
84
|
+
name: "MCP 기타",
|
|
85
|
+
description: "MCP 패키지 설정, 스크립트 등",
|
|
86
|
+
suggested: true,
|
|
87
|
+
},
|
|
88
|
+
engine_rust: {
|
|
89
|
+
name: "Rust 엔진 코드",
|
|
90
|
+
description: "엔진 소스 (빌드 후 GitHub Release 필요할 수 있음)",
|
|
91
|
+
suggested: true,
|
|
92
|
+
},
|
|
93
|
+
engine_other: {
|
|
94
|
+
name: "엔진 기타",
|
|
95
|
+
description: "엔진 설정, 문서 등",
|
|
96
|
+
suggested: true,
|
|
97
|
+
},
|
|
98
|
+
docs: {
|
|
99
|
+
name: "문서",
|
|
100
|
+
description: "마크다운 문서, 스펙",
|
|
101
|
+
suggested: true,
|
|
102
|
+
},
|
|
103
|
+
python_core: {
|
|
104
|
+
name: "Python 코어",
|
|
105
|
+
description: "vibecoding_helper 모듈",
|
|
106
|
+
suggested: true,
|
|
107
|
+
},
|
|
108
|
+
python: {
|
|
109
|
+
name: "Python 기타",
|
|
110
|
+
description: "Python 스크립트, 테스트",
|
|
111
|
+
suggested: true,
|
|
112
|
+
},
|
|
113
|
+
ci: {
|
|
114
|
+
name: "CI/CD",
|
|
115
|
+
description: "GitHub Actions 워크플로우",
|
|
116
|
+
suggested: true,
|
|
117
|
+
},
|
|
118
|
+
vibe_config: {
|
|
119
|
+
name: "Vibe 설정",
|
|
120
|
+
description: ".vibe/ 로컬 설정 (보통 gitignore됨)",
|
|
121
|
+
suggested: false,
|
|
122
|
+
},
|
|
123
|
+
schemas: {
|
|
124
|
+
name: "스키마",
|
|
125
|
+
description: "JSON Schema 정의",
|
|
126
|
+
suggested: true,
|
|
127
|
+
},
|
|
128
|
+
config: {
|
|
129
|
+
name: "설정 파일",
|
|
130
|
+
description: "JSON, YAML, TOML 설정",
|
|
131
|
+
suggested: true,
|
|
132
|
+
},
|
|
133
|
+
generated: {
|
|
134
|
+
name: "생성된 코드",
|
|
135
|
+
description: "스키마에서 자동 생성된 코드 (재생성 가능)",
|
|
136
|
+
suggested: false,
|
|
137
|
+
},
|
|
138
|
+
tests: {
|
|
139
|
+
name: "테스트",
|
|
140
|
+
description: "테스트 파일",
|
|
141
|
+
suggested: true,
|
|
142
|
+
},
|
|
143
|
+
build_artifacts: {
|
|
144
|
+
name: "빌드 결과물",
|
|
145
|
+
description: "컴파일된 파일 (보통 gitignore됨)",
|
|
146
|
+
suggested: false,
|
|
147
|
+
},
|
|
148
|
+
lock_files: {
|
|
149
|
+
name: "Lock 파일",
|
|
150
|
+
description: "의존성 잠금 파일",
|
|
151
|
+
suggested: true,
|
|
152
|
+
},
|
|
153
|
+
other: {
|
|
154
|
+
name: "기타",
|
|
155
|
+
description: "분류되지 않은 파일",
|
|
156
|
+
suggested: false,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
return info[category] || info.other;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Analyze git status and categorize changes
|
|
163
|
+
*/
|
|
164
|
+
export async function analyzePreCommit(basePath = process.cwd()) {
|
|
165
|
+
const git = simpleGit({ baseDir: basePath });
|
|
166
|
+
const status = await git.status();
|
|
167
|
+
// Collect all changed files
|
|
168
|
+
const changes = [];
|
|
169
|
+
for (const file of status.not_added) {
|
|
170
|
+
changes.push({ path: file, status: "new", staged: false });
|
|
171
|
+
}
|
|
172
|
+
for (const file of status.created) {
|
|
173
|
+
changes.push({ path: file, status: "new", staged: true });
|
|
174
|
+
}
|
|
175
|
+
for (const file of status.modified) {
|
|
176
|
+
changes.push({ path: file, status: "modified", staged: false });
|
|
177
|
+
}
|
|
178
|
+
for (const file of status.staged) {
|
|
179
|
+
if (!changes.find((c) => c.path === file)) {
|
|
180
|
+
changes.push({ path: file, status: "modified", staged: true });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const file of status.deleted) {
|
|
184
|
+
changes.push({ path: file, status: "deleted", staged: false });
|
|
185
|
+
}
|
|
186
|
+
for (const file of status.renamed) {
|
|
187
|
+
changes.push({ path: file.to, status: "renamed", staged: true });
|
|
188
|
+
}
|
|
189
|
+
// Group by category
|
|
190
|
+
const categoryMap = new Map();
|
|
191
|
+
for (const change of changes) {
|
|
192
|
+
const cat = categorizeFile(change.path);
|
|
193
|
+
if (!categoryMap.has(cat)) {
|
|
194
|
+
categoryMap.set(cat, []);
|
|
195
|
+
}
|
|
196
|
+
categoryMap.get(cat).push(change);
|
|
197
|
+
}
|
|
198
|
+
// Build categories
|
|
199
|
+
const categories = [];
|
|
200
|
+
for (const [cat, files] of categoryMap) {
|
|
201
|
+
const info = getCategoryInfo(cat);
|
|
202
|
+
categories.push({
|
|
203
|
+
name: info.name,
|
|
204
|
+
description: info.description,
|
|
205
|
+
files,
|
|
206
|
+
suggested: info.suggested,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// Sort: suggested first, then by file count
|
|
210
|
+
categories.sort((a, b) => {
|
|
211
|
+
if (a.suggested !== b.suggested)
|
|
212
|
+
return a.suggested ? -1 : 1;
|
|
213
|
+
return b.files.length - a.files.length;
|
|
214
|
+
});
|
|
215
|
+
// Generate suggestions
|
|
216
|
+
const suggestions = [];
|
|
217
|
+
const warnings = [];
|
|
218
|
+
// Check for MCP source changes
|
|
219
|
+
const mcpSource = categoryMap.get("mcp_source");
|
|
220
|
+
if (mcpSource && mcpSource.length > 0) {
|
|
221
|
+
suggestions.push("MCP 소스 변경됨 → 커밋 후 `vibe_pm.publish_mcp` 실행 권장");
|
|
222
|
+
}
|
|
223
|
+
// Check for engine changes
|
|
224
|
+
const engineRust = categoryMap.get("engine_rust");
|
|
225
|
+
if (engineRust && engineRust.length > 0) {
|
|
226
|
+
suggestions.push("Rust 엔진 변경됨 → 커밋 후 엔진 빌드 + GitHub Release 필요할 수 있음");
|
|
227
|
+
}
|
|
228
|
+
// Check for vibe config
|
|
229
|
+
const vibeConfig = categoryMap.get("vibe_config");
|
|
230
|
+
if (vibeConfig && vibeConfig.length > 0) {
|
|
231
|
+
warnings.push(".vibe/ 파일은 보통 gitignore됩니다. 의도적 커밋인지 확인하세요.");
|
|
232
|
+
}
|
|
233
|
+
// Check for build artifacts
|
|
234
|
+
const buildArtifacts = categoryMap.get("build_artifacts");
|
|
235
|
+
if (buildArtifacts && buildArtifacts.length > 0) {
|
|
236
|
+
warnings.push("빌드 결과물은 보통 gitignore됩니다. 의도적 커밋인지 확인하세요.");
|
|
237
|
+
}
|
|
238
|
+
// Check for sensitive patterns
|
|
239
|
+
for (const change of changes) {
|
|
240
|
+
const lower = change.path.toLowerCase();
|
|
241
|
+
if (lower.includes("secret") ||
|
|
242
|
+
lower.includes("credential") ||
|
|
243
|
+
lower.includes(".env") ||
|
|
244
|
+
lower.includes("token")) {
|
|
245
|
+
warnings.push(`민감한 파일 감지: ${change.path} - 커밋 전 확인 필요`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
categories,
|
|
250
|
+
total_files: changes.length,
|
|
251
|
+
suggestions,
|
|
252
|
+
warnings,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Generate a summary for user decision
|
|
257
|
+
*/
|
|
258
|
+
export function formatAnalysisSummary(analysis) {
|
|
259
|
+
const lines = [];
|
|
260
|
+
lines.push(`## 변경 파일 분석 (총 ${analysis.total_files}개)\n`);
|
|
261
|
+
for (const cat of analysis.categories) {
|
|
262
|
+
const emoji = cat.suggested ? "✅" : "⚠️";
|
|
263
|
+
lines.push(`### ${emoji} ${cat.name} (${cat.files.length}개)`);
|
|
264
|
+
lines.push(`> ${cat.description}`);
|
|
265
|
+
lines.push("");
|
|
266
|
+
// Show up to 5 files
|
|
267
|
+
const shown = cat.files.slice(0, 5);
|
|
268
|
+
for (const f of shown) {
|
|
269
|
+
const statusIcon = f.staged ? "📦" : "📝";
|
|
270
|
+
lines.push(`- ${statusIcon} ${f.path}`);
|
|
271
|
+
}
|
|
272
|
+
if (cat.files.length > 5) {
|
|
273
|
+
lines.push(`- ... 외 ${cat.files.length - 5}개`);
|
|
274
|
+
}
|
|
275
|
+
lines.push("");
|
|
276
|
+
}
|
|
277
|
+
if (analysis.suggestions.length > 0) {
|
|
278
|
+
lines.push("### 💡 제안");
|
|
279
|
+
for (const s of analysis.suggestions) {
|
|
280
|
+
lines.push(`- ${s}`);
|
|
281
|
+
}
|
|
282
|
+
lines.push("");
|
|
283
|
+
}
|
|
284
|
+
if (analysis.warnings.length > 0) {
|
|
285
|
+
lines.push("### ⚠️ 경고");
|
|
286
|
+
for (const w of analysis.warnings) {
|
|
287
|
+
lines.push(`- ${w}`);
|
|
288
|
+
}
|
|
289
|
+
lines.push("");
|
|
290
|
+
}
|
|
291
|
+
return lines.join("\n");
|
|
292
|
+
}
|