@vibecodetown/mcp-server 2.1.4 → 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/project-state.js +176 -0
- package/build/local-mode/templates.js +3 -3
- 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 +497 -32
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/publish_mcp.ts
|
|
2
|
+
// MCP build and publish workflow
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { getCredentialStore } from "../../auth/credential_store.js";
|
|
7
|
+
/**
|
|
8
|
+
* Check if MCP source files have changed since last build
|
|
9
|
+
*/
|
|
10
|
+
async function hasSourceChanges(mcpRoot) {
|
|
11
|
+
const srcDir = path.join(mcpRoot, "src");
|
|
12
|
+
const buildDir = path.join(mcpRoot, "build");
|
|
13
|
+
// If no build folder, definitely changed
|
|
14
|
+
if (!fs.existsSync(buildDir)) {
|
|
15
|
+
return { changed: true, files: ["build/ not found"] };
|
|
16
|
+
}
|
|
17
|
+
// Get build timestamp
|
|
18
|
+
const buildIndex = path.join(buildDir, "index.js");
|
|
19
|
+
if (!fs.existsSync(buildIndex)) {
|
|
20
|
+
return { changed: true, files: ["build/index.js not found"] };
|
|
21
|
+
}
|
|
22
|
+
const buildStat = fs.statSync(buildIndex);
|
|
23
|
+
const buildTime = buildStat.mtime.getTime();
|
|
24
|
+
// Find source files newer than build
|
|
25
|
+
const changedFiles = [];
|
|
26
|
+
function checkDir(dir) {
|
|
27
|
+
if (!fs.existsSync(dir))
|
|
28
|
+
return;
|
|
29
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const fullPath = path.join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
checkDir(fullPath);
|
|
34
|
+
}
|
|
35
|
+
else if (entry.name.endsWith(".ts")) {
|
|
36
|
+
const stat = fs.statSync(fullPath);
|
|
37
|
+
if (stat.mtime.getTime() > buildTime) {
|
|
38
|
+
changedFiles.push(path.relative(mcpRoot, fullPath));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
checkDir(srcDir);
|
|
44
|
+
return { changed: changedFiles.length > 0, files: changedFiles };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Run TypeScript build
|
|
48
|
+
*/
|
|
49
|
+
function runBuild(mcpRoot) {
|
|
50
|
+
try {
|
|
51
|
+
const result = spawnSync("npm", ["run", "build"], {
|
|
52
|
+
cwd: mcpRoot,
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
shell: true,
|
|
55
|
+
timeout: 120_000, // 2 minutes
|
|
56
|
+
});
|
|
57
|
+
if (result.status === 0) {
|
|
58
|
+
return { success: true, output: result.stdout || "" };
|
|
59
|
+
}
|
|
60
|
+
return { success: false, output: result.stderr || result.stdout || "Build failed" };
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return { success: false, output: e instanceof Error ? e.message : String(e) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Bump version in package.json
|
|
68
|
+
*/
|
|
69
|
+
function bumpVersion(mcpRoot, type) {
|
|
70
|
+
try {
|
|
71
|
+
const result = spawnSync("npm", ["version", type, "--no-git-tag-version"], {
|
|
72
|
+
cwd: mcpRoot,
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
shell: true,
|
|
75
|
+
});
|
|
76
|
+
if (result.status === 0) {
|
|
77
|
+
// Read new version from package.json
|
|
78
|
+
const pkgPath = path.join(mcpRoot, "package.json");
|
|
79
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
80
|
+
return pkg.version;
|
|
81
|
+
}
|
|
82
|
+
throw new Error(result.stderr || "Version bump failed");
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
throw new Error(`Version bump failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Run npm publish
|
|
90
|
+
*/
|
|
91
|
+
async function runPublish(mcpRoot, dryRun) {
|
|
92
|
+
const store = getCredentialStore();
|
|
93
|
+
const npmToken = await store.getNpmToken();
|
|
94
|
+
// Set NPM_TOKEN if we have one stored
|
|
95
|
+
const env = { ...process.env };
|
|
96
|
+
if (npmToken) {
|
|
97
|
+
env.NPM_TOKEN = npmToken;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const args = ["publish", "--access", "public"];
|
|
101
|
+
if (dryRun) {
|
|
102
|
+
args.push("--dry-run");
|
|
103
|
+
}
|
|
104
|
+
const result = spawnSync("npm", args, {
|
|
105
|
+
cwd: mcpRoot,
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
shell: true,
|
|
108
|
+
env,
|
|
109
|
+
timeout: 120_000, // 2 minutes
|
|
110
|
+
});
|
|
111
|
+
if (result.status === 0) {
|
|
112
|
+
return { success: true, output: result.stdout || "" };
|
|
113
|
+
}
|
|
114
|
+
return { success: false, output: result.stderr || result.stdout || "Publish failed" };
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
return { success: false, output: e instanceof Error ? e.message : String(e) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* vibe_pm.publish_mcp - Build and publish MCP package
|
|
122
|
+
*
|
|
123
|
+
* Workflow:
|
|
124
|
+
* 1. Check for source changes (skip if no changes unless --force)
|
|
125
|
+
* 2. Run npm build (tsc)
|
|
126
|
+
* 3. Optionally bump version
|
|
127
|
+
* 4. Run npm publish
|
|
128
|
+
*
|
|
129
|
+
* PM-friendly description:
|
|
130
|
+
* MCP 패키지를 빌드하고 npm에 배포합니다.
|
|
131
|
+
*/
|
|
132
|
+
export async function publishMcp(input, basePath = process.cwd()) {
|
|
133
|
+
const warnings = [];
|
|
134
|
+
// Find MCP root (adapters/mcp-ts)
|
|
135
|
+
const mcpRoot = path.join(basePath, "adapters", "mcp-ts");
|
|
136
|
+
if (!fs.existsSync(path.join(mcpRoot, "package.json"))) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
phase: "check",
|
|
140
|
+
version: "unknown",
|
|
141
|
+
built: false,
|
|
142
|
+
published: false,
|
|
143
|
+
message: "MCP 패키지를 찾을 수 없습니다: adapters/mcp-ts/package.json",
|
|
144
|
+
warnings,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Read current version
|
|
148
|
+
const pkgPath = path.join(mcpRoot, "package.json");
|
|
149
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
150
|
+
let version = pkg.version;
|
|
151
|
+
// Phase 1: Check for changes
|
|
152
|
+
const { changed, files } = await hasSourceChanges(mcpRoot);
|
|
153
|
+
if (!changed && !input.force && !input.skip_build) {
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
phase: "check",
|
|
157
|
+
version,
|
|
158
|
+
built: false,
|
|
159
|
+
published: false,
|
|
160
|
+
message: "소스 변경 없음. 빌드 스킵.",
|
|
161
|
+
warnings: ["--force로 강제 빌드 가능"],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// Phase 2: Build
|
|
165
|
+
let built = false;
|
|
166
|
+
if (!input.skip_build) {
|
|
167
|
+
const buildResult = runBuild(mcpRoot);
|
|
168
|
+
if (!buildResult.success) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
phase: "build",
|
|
172
|
+
version,
|
|
173
|
+
built: false,
|
|
174
|
+
published: false,
|
|
175
|
+
message: `빌드 실패: ${buildResult.output}`,
|
|
176
|
+
warnings,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
built = true;
|
|
180
|
+
if (files.length > 0) {
|
|
181
|
+
warnings.push(`변경된 파일: ${files.slice(0, 5).join(", ")}${files.length > 5 ? ` 외 ${files.length - 5}개` : ""}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Build only mode
|
|
185
|
+
if (input.build_only) {
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
phase: "build",
|
|
189
|
+
version,
|
|
190
|
+
built,
|
|
191
|
+
published: false,
|
|
192
|
+
message: `빌드 완료 (v${version})`,
|
|
193
|
+
warnings,
|
|
194
|
+
next_action: {
|
|
195
|
+
tool: "vibe_pm.publish_mcp",
|
|
196
|
+
reason: "배포하려면 build_only: false로 다시 실행",
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Phase 3: Version bump (optional)
|
|
201
|
+
if (input.bump) {
|
|
202
|
+
try {
|
|
203
|
+
version = bumpVersion(mcpRoot, input.bump);
|
|
204
|
+
warnings.push(`버전 범프: ${pkg.version} → ${version}`);
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
phase: "publish",
|
|
210
|
+
version,
|
|
211
|
+
built,
|
|
212
|
+
published: false,
|
|
213
|
+
message: e instanceof Error ? e.message : String(e),
|
|
214
|
+
warnings,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Phase 4: Publish
|
|
219
|
+
const publishResult = await runPublish(mcpRoot, input.dry_run ?? false);
|
|
220
|
+
if (!publishResult.success) {
|
|
221
|
+
// Check if it's an auth error
|
|
222
|
+
if (publishResult.output.includes("ENEEDAUTH") || publishResult.output.includes("401")) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
phase: "publish",
|
|
226
|
+
version,
|
|
227
|
+
built,
|
|
228
|
+
published: false,
|
|
229
|
+
message: "npm 인증 실패. NPM_TOKEN을 설정하거나 npm login을 실행하세요.",
|
|
230
|
+
warnings: [
|
|
231
|
+
"vibe config set npm_token <TOKEN> 으로 토큰 저장 가능",
|
|
232
|
+
"또는 NPM_TOKEN 환경변수 설정",
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
phase: "publish",
|
|
239
|
+
version,
|
|
240
|
+
built,
|
|
241
|
+
published: false,
|
|
242
|
+
message: `배포 실패: ${publishResult.output}`,
|
|
243
|
+
warnings,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const dryRunMsg = input.dry_run ? " (dry-run)" : "";
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
phase: "complete",
|
|
250
|
+
version,
|
|
251
|
+
built,
|
|
252
|
+
published: !input.dry_run,
|
|
253
|
+
message: `@vibecodetown/mcp-server@${version} 배포 완료${dryRunMsg}`,
|
|
254
|
+
warnings,
|
|
255
|
+
next_action: {
|
|
256
|
+
tool: "vibe_pm.status",
|
|
257
|
+
reason: "배포 상태 확인",
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Quick check if MCP needs rebuild
|
|
263
|
+
*/
|
|
264
|
+
export async function needsMcpBuild(basePath = process.cwd()) {
|
|
265
|
+
const mcpRoot = path.join(basePath, "adapters", "mcp-ts");
|
|
266
|
+
if (!fs.existsSync(path.join(mcpRoot, "package.json"))) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const { changed } = await hasSourceChanges(mcpRoot);
|
|
270
|
+
return changed;
|
|
271
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// adapters/mcp-ts/src/tools/vibe_pm/run_app.ts
|
|
2
2
|
// vibe_pm.run_app - One-click application launch
|
|
3
|
+
//
|
|
4
|
+
// All subprocess invocations go through cli_invoker for centralized management.
|
|
3
5
|
import * as fs from "node:fs";
|
|
4
6
|
import * as path from "node:path";
|
|
5
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
8
|
+
import { invokeSystem, invokeSystemSync, spawnDetached } from "../../runtime/cli_invoker.js";
|
|
7
9
|
import { resolveRunDir, resolveRunId } from "./context.js";
|
|
8
10
|
import { buildPodmanRunCommand, filterContainerEnvAllowlist, normalizeMounts, normalizePorts } from "./run_app_podman.js";
|
|
9
11
|
/**
|
|
@@ -49,19 +51,10 @@ export async function runApp(input) {
|
|
|
49
51
|
// Step 2: Start process
|
|
50
52
|
const fullCommand = `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim();
|
|
51
53
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
stdio: "ignore",
|
|
56
|
-
env: {
|
|
57
|
-
...process.env,
|
|
58
|
-
PORT: String(projectInfo.port),
|
|
59
|
-
NODE_ENV: mode === "prod" ? "production" : "development"
|
|
60
|
-
}
|
|
54
|
+
const pid = spawnDetached(projectInfo.command, projectInfo.args, basePath, {
|
|
55
|
+
PORT: String(projectInfo.port),
|
|
56
|
+
NODE_ENV: mode === "prod" ? "production" : "development"
|
|
61
57
|
});
|
|
62
|
-
// Unref to allow parent to exit independently
|
|
63
|
-
child.unref();
|
|
64
|
-
const pid = child.pid;
|
|
65
58
|
// Give the process a moment to start
|
|
66
59
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
67
60
|
writeRunAppEvidenceBestEffort(basePath, {
|
|
@@ -70,11 +63,11 @@ export async function runApp(input) {
|
|
|
70
63
|
port: projectInfo.port,
|
|
71
64
|
url: projectInfo.url,
|
|
72
65
|
command_executed: fullCommand,
|
|
73
|
-
process_id: pid
|
|
66
|
+
process_id: pid ?? undefined
|
|
74
67
|
});
|
|
75
68
|
return {
|
|
76
69
|
success: true,
|
|
77
|
-
process_id: pid,
|
|
70
|
+
process_id: pid ?? undefined,
|
|
78
71
|
url: projectInfo.url,
|
|
79
72
|
command_executed: fullCommand,
|
|
80
73
|
message: `애플리케이션이 시작되었습니다. ${projectInfo.url} 에서 확인하세요.`,
|
|
@@ -160,10 +153,31 @@ function normalizeOpaPaths(basePath, values) {
|
|
|
160
153
|
}
|
|
161
154
|
return out;
|
|
162
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* P1-3: Extract script name from package manager commands
|
|
158
|
+
* npm run dev -> "dev", npm start -> "start", etc.
|
|
159
|
+
*/
|
|
160
|
+
function extractScriptName(command, args) {
|
|
161
|
+
const packageManagers = ["npm", "yarn", "pnpm", "bun"];
|
|
162
|
+
if (!packageManagers.includes(command))
|
|
163
|
+
return undefined;
|
|
164
|
+
// npm run <script>, yarn run <script>, pnpm run <script>, bun run <script>
|
|
165
|
+
const runIdx = args.indexOf("run");
|
|
166
|
+
if (runIdx >= 0 && args[runIdx + 1]) {
|
|
167
|
+
return args[runIdx + 1];
|
|
168
|
+
}
|
|
169
|
+
// npm start, npm test, etc. (shortcuts)
|
|
170
|
+
const shortcuts = ["start", "test", "build"];
|
|
171
|
+
if (args.length > 0 && shortcuts.includes(args[0])) {
|
|
172
|
+
return args[0];
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
163
176
|
function buildOpaPolicyInput(basePath, command, args, opts) {
|
|
164
177
|
const cwd = normalizeOpaPath(basePath);
|
|
165
178
|
const paths = normalizeOpaPaths(basePath, opts?.paths ?? [basePath]);
|
|
166
179
|
const mounts = opts?.mounts ? normalizeOpaPaths(basePath, opts.mounts) : undefined;
|
|
180
|
+
const scriptName = extractScriptName(command, args);
|
|
167
181
|
return {
|
|
168
182
|
action: "run_app",
|
|
169
183
|
project_root: cwd,
|
|
@@ -172,7 +186,8 @@ function buildOpaPolicyInput(basePath, command, args, opts) {
|
|
|
172
186
|
args,
|
|
173
187
|
cwd,
|
|
174
188
|
paths,
|
|
175
|
-
mounts
|
|
189
|
+
mounts,
|
|
190
|
+
script_name: scriptName
|
|
176
191
|
},
|
|
177
192
|
meta: {
|
|
178
193
|
tool: "vibe_pm.run_app",
|
|
@@ -198,14 +213,14 @@ function evaluateOpaGate(basePath, input) {
|
|
|
198
213
|
writeJsonBestEffort(resultPath, deny);
|
|
199
214
|
return deny;
|
|
200
215
|
}
|
|
201
|
-
const opa =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const missing =
|
|
216
|
+
const opa = invokeSystemSync("opa", ["eval", "-f", "json", "-I", "-d", policyPath, "data.vibepm.runapp"], basePath, { input: JSON.stringify(input), timeoutMs: 1500 });
|
|
217
|
+
// Check for spawn errors (binary not found, etc.)
|
|
218
|
+
if (opa.exitCode === 127 || opa.stderr.includes("[SPAWN_ERROR]")) {
|
|
219
|
+
const missing = opa.stderr.includes("ENOENT") || opa.stderr.includes("not found");
|
|
205
220
|
const deny = {
|
|
206
221
|
allow: false,
|
|
207
222
|
reasons: [
|
|
208
|
-
missing ? "OPA_POLICY_MISSING: opa binary not found" : `OPA_POLICY_EVAL_FAILED: ${
|
|
223
|
+
missing ? "OPA_POLICY_MISSING: opa binary not found" : `OPA_POLICY_EVAL_FAILED: ${opa.stderr}`
|
|
209
224
|
],
|
|
210
225
|
matched_rules: [missing ? "OPA_POLICY_MISSING" : "OPA_POLICY_EVAL_FAILED"]
|
|
211
226
|
};
|
|
@@ -213,10 +228,10 @@ function evaluateOpaGate(basePath, input) {
|
|
|
213
228
|
writeJsonBestEffort(resultPath, deny);
|
|
214
229
|
return deny;
|
|
215
230
|
}
|
|
216
|
-
if (opa.
|
|
231
|
+
if (opa.exitCode !== 0) {
|
|
217
232
|
const deny = {
|
|
218
233
|
allow: false,
|
|
219
|
-
reasons: [`OPA_POLICY_EVAL_FAILED: exit_code=${opa.
|
|
234
|
+
reasons: [`OPA_POLICY_EVAL_FAILED: exit_code=${opa.exitCode}`],
|
|
220
235
|
matched_rules: ["OPA_POLICY_EVAL_FAILED"]
|
|
221
236
|
};
|
|
222
237
|
if (resultPath)
|
|
@@ -282,19 +297,10 @@ function writeRunAppEvidenceBestEffort(repoRootAbs, payload) {
|
|
|
282
297
|
}
|
|
283
298
|
}
|
|
284
299
|
async function assertPodmanAvailable() {
|
|
285
|
-
await
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
p.stdout.on("data", (d) => (out += String(d)));
|
|
290
|
-
p.stderr.on("data", (d) => (err += String(d)));
|
|
291
|
-
p.on("error", reject);
|
|
292
|
-
p.on("close", (code) => {
|
|
293
|
-
if (code === 0)
|
|
294
|
-
return resolve();
|
|
295
|
-
reject(new Error(`podman_unavailable: code=${code} out=${out} err=${err}`));
|
|
296
|
-
});
|
|
297
|
-
});
|
|
300
|
+
const result = await invokeSystem("podman", ["--version"], process.cwd());
|
|
301
|
+
if (result.exitCode !== 0) {
|
|
302
|
+
throw new Error(`podman_unavailable: code=${result.exitCode} out=${result.stdout} err=${result.stderr}`);
|
|
303
|
+
}
|
|
298
304
|
}
|
|
299
305
|
function defaultImageForProject(projectType) {
|
|
300
306
|
if (projectType === "node")
|
|
@@ -328,15 +334,12 @@ function buildInsideCommand(projectInfo) {
|
|
|
328
334
|
return run;
|
|
329
335
|
}
|
|
330
336
|
async function runPodmanDetached(cmd) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
p.on("error", reject);
|
|
338
|
-
p.on("close", (code) => resolve({ exitCode: code ?? 1, stdout: out, stderr: err }));
|
|
339
|
-
});
|
|
337
|
+
const result = await invokeSystem(cmd.cmd, cmd.args, process.cwd());
|
|
338
|
+
return {
|
|
339
|
+
exitCode: result.exitCode,
|
|
340
|
+
stdout: result.stdout,
|
|
341
|
+
stderr: result.stderr
|
|
342
|
+
};
|
|
340
343
|
}
|
|
341
344
|
async function runAppWithPodman(input, repoRootAbs, mode, projectInfo) {
|
|
342
345
|
const image = input.container_image ?? defaultImageForProject(projectInfo.type);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/save_rule.ts
|
|
2
|
+
// vibe_pm.save_rule - 프로젝트 워크플로우 규칙 저장
|
|
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 saveRuleInputSchema = z.object({
|
|
11
|
+
base_path: z.string().describe("프로젝트 루트 경로"),
|
|
12
|
+
rule_title: z.string().min(1).describe("규칙 제목 (예: 'API 변경 시 문서 갱신')"),
|
|
13
|
+
trigger: z.string().min(1).describe("언제 적용되는지 (예: 'API 엔드포인트를 수정할 때마다')"),
|
|
14
|
+
action: z.string().min(1).describe("무엇을 해야 하는지 (예: 'docs/api.md를 함께 갱신한다')"),
|
|
15
|
+
example: z.string().optional().describe("구체적인 예시 (선택)")
|
|
16
|
+
}).strict();
|
|
17
|
+
export const saveRuleOutputSchema = z.object({
|
|
18
|
+
status: z.enum(["saved", "error"]),
|
|
19
|
+
message: z.string(),
|
|
20
|
+
rules_file: z.string().describe(".vibe/project_rules.md 경로"),
|
|
21
|
+
rule_count: z.number().describe("현재 저장된 규칙 수")
|
|
22
|
+
}).strict();
|
|
23
|
+
// ============================================================
|
|
24
|
+
// Implementation
|
|
25
|
+
// ============================================================
|
|
26
|
+
const RULES_FILENAME = "project_rules.md";
|
|
27
|
+
/**
|
|
28
|
+
* 규칙 파일 경로 반환
|
|
29
|
+
*/
|
|
30
|
+
function getRulesFilePath(basePath) {
|
|
31
|
+
const { vibeDir } = getVibeRepoPaths(basePath);
|
|
32
|
+
return path.join(vibeDir, RULES_FILENAME);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 규칙 파일 읽기 (없으면 기본 템플릿 반환)
|
|
36
|
+
*/
|
|
37
|
+
async function readRulesFile(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
return await fs.readFile(filePath, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// 파일이 없으면 기본 템플릿 반환
|
|
43
|
+
return `# Project Workflow Rules
|
|
44
|
+
|
|
45
|
+
> 이 파일은 Vibe PM이 자동으로 관리합니다.
|
|
46
|
+
> 프로젝트별 워크플로우 규칙이 여기에 저장됩니다.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 규칙 수 계산
|
|
55
|
+
*/
|
|
56
|
+
function countRules(content) {
|
|
57
|
+
const lines = content.split("\n");
|
|
58
|
+
let count = 0;
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
if (line.startsWith("## ") && !line.includes("Project Workflow Rules")) {
|
|
61
|
+
count++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 새 규칙을 마크다운 형식으로 포맷
|
|
68
|
+
*/
|
|
69
|
+
function formatRule(input) {
|
|
70
|
+
const timestamp = new Date().toISOString().split("T")[0];
|
|
71
|
+
let rule = `## ${input.rule_title}
|
|
72
|
+
|
|
73
|
+
- **언제**: ${input.trigger}
|
|
74
|
+
- **무엇을**: ${input.action}
|
|
75
|
+
`;
|
|
76
|
+
if (input.example) {
|
|
77
|
+
rule += `- **예시**: ${input.example}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
rule += `- **등록일**: ${timestamp}
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
`;
|
|
85
|
+
return rule;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 규칙 저장
|
|
89
|
+
*/
|
|
90
|
+
export async function saveRule(input) {
|
|
91
|
+
const rulesFile = getRulesFilePath(input.base_path);
|
|
92
|
+
try {
|
|
93
|
+
// .vibe 디렉토리 확인/생성
|
|
94
|
+
const vibeDir = path.dirname(rulesFile);
|
|
95
|
+
await fs.mkdir(vibeDir, { recursive: true });
|
|
96
|
+
// 기존 규칙 읽기
|
|
97
|
+
let content = await readRulesFile(rulesFile);
|
|
98
|
+
// 새 규칙 추가
|
|
99
|
+
const newRule = formatRule(input);
|
|
100
|
+
content += newRule;
|
|
101
|
+
// 파일 저장
|
|
102
|
+
await fs.writeFile(rulesFile, content, "utf-8");
|
|
103
|
+
const ruleCount = countRules(content);
|
|
104
|
+
return {
|
|
105
|
+
status: "saved",
|
|
106
|
+
message: `규칙 '${input.rule_title}'이(가) 저장되었습니다.`,
|
|
107
|
+
rules_file: rulesFile,
|
|
108
|
+
rule_count: ruleCount
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
113
|
+
return {
|
|
114
|
+
status: "error",
|
|
115
|
+
message: `규칙 저장 실패: ${msg}`,
|
|
116
|
+
rules_file: rulesFile,
|
|
117
|
+
rule_count: 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// adapters/mcp-ts/src/tools/vibe_pm/undo_last_task.ts
|
|
2
2
|
// vibe_pm.undo_last_task - Time machine rollback via git revert
|
|
3
|
+
//
|
|
4
|
+
// All subprocess invocations go through cli_invoker for centralized management.
|
|
3
5
|
import * as fs from "node:fs";
|
|
4
6
|
import * as path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
const execAsync = promisify(exec);
|
|
7
|
+
import { invokeGit } from "../../runtime/cli_invoker.js";
|
|
8
8
|
/**
|
|
9
9
|
* vibe_pm.undo_last_task - Rollback recent commits via git revert
|
|
10
10
|
*
|
|
@@ -43,25 +43,20 @@ export async function undoLastTask(input) {
|
|
|
43
43
|
const revertedTasks = [];
|
|
44
44
|
const revertErrors = [];
|
|
45
45
|
for (const commit of commits) {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const result = await invokeGit(["revert", "--no-edit", commit.hash], basePath);
|
|
47
|
+
if (result.exitCode === 0) {
|
|
48
48
|
revertedTasks.push({
|
|
49
49
|
commit_hash: commit.shortHash,
|
|
50
50
|
task_summary: commit.subject,
|
|
51
51
|
timestamp: commit.timestamp
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
const errMsg =
|
|
54
|
+
else {
|
|
55
|
+
const errMsg = result.stderr || result.stdout;
|
|
56
56
|
revertErrors.push(`${commit.shortHash}: ${errMsg}`);
|
|
57
57
|
// If revert fails due to conflicts, abort and stop
|
|
58
58
|
if (errMsg.includes("conflict") || errMsg.includes("CONFLICT")) {
|
|
59
|
-
|
|
60
|
-
await execAsync("git revert --abort", { cwd: basePath });
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// Ignore abort errors
|
|
64
|
-
}
|
|
59
|
+
await invokeGit(["revert", "--abort"], basePath);
|
|
65
60
|
break;
|
|
66
61
|
}
|
|
67
62
|
}
|
|
@@ -99,9 +94,12 @@ export async function undoLastTask(input) {
|
|
|
99
94
|
* Get recent commits from git log
|
|
100
95
|
*/
|
|
101
96
|
async function getRecentCommits(basePath, count) {
|
|
102
|
-
const
|
|
97
|
+
const result = await invokeGit(["log", "--oneline", "-n", String(count), "--format=%H|%h|%s|%ci"], basePath);
|
|
98
|
+
if (result.exitCode !== 0) {
|
|
99
|
+
throw new Error(result.stderr || "git log failed");
|
|
100
|
+
}
|
|
103
101
|
const commits = [];
|
|
104
|
-
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
102
|
+
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
|
105
103
|
for (const line of lines) {
|
|
106
104
|
const [hash, shortHash, subject, timestamp] = line.split("|");
|
|
107
105
|
if (hash && shortHash && subject) {
|
|
@@ -119,13 +117,11 @@ async function getRecentCommits(basePath, count) {
|
|
|
119
117
|
* Get current HEAD commit hash
|
|
120
118
|
*/
|
|
121
119
|
async function getCurrentCommit(basePath) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return stdout.trim();
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
120
|
+
const result = await invokeGit(["rev-parse", "--short", "HEAD"], basePath);
|
|
121
|
+
if (result.exitCode !== 0) {
|
|
127
122
|
return "unknown";
|
|
128
123
|
}
|
|
124
|
+
return result.stdout.trim();
|
|
129
125
|
}
|
|
130
126
|
/**
|
|
131
127
|
* Add rollback entry to DEV_LOG
|
package/build/version-check.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
// 정책: 절대 실행을 막지 않는다 (모든 실패는 GO)
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
6
|
import readline from "node:readline";
|
|
7
|
+
import { invokeGitSync } from "./runtime/cli_invoker.js";
|
|
8
8
|
function execGit(args, cwd) {
|
|
9
|
-
const
|
|
9
|
+
const result = invokeGitSync(args, cwd, { timeoutMs: 10000 });
|
|
10
10
|
return {
|
|
11
|
-
status:
|
|
12
|
-
stdout:
|
|
13
|
-
stderr:
|
|
11
|
+
status: result.exitCode,
|
|
12
|
+
stdout: result.stdout,
|
|
13
|
+
stderr: result.stderr,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
function repoRootOrNull(cwd) {
|