@vibecodetown/mcp-server 2.1.4 → 2.2.1
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/README.md +10 -10
- package/build/auth/credential_store.js +146 -0
- package/build/auth/public_key.js +6 -4
- package/build/bootstrap/doctor.js +113 -5
- package/build/bootstrap/installer.js +85 -15
- package/build/bootstrap/registry.js +11 -6
- package/build/bootstrap/skills-installer.js +365 -0
- package/build/control_plane/gate.js +52 -70
- package/build/dx/activity.js +26 -3
- package/build/engine.js +151 -0
- package/build/errors.js +107 -0
- package/build/generated/bridge_build_seed_input.js +2 -0
- package/build/generated/bridge_build_seed_output.js +2 -0
- package/build/generated/bridge_confirm_reference_input.js +2 -0
- package/build/generated/bridge_confirm_reference_output.js +2 -0
- package/build/generated/bridge_confirmed_reference_file.js +2 -0
- package/build/generated/bridge_generate_references_input.js +2 -0
- package/build/generated/bridge_generate_references_output.js +2 -0
- package/build/generated/bridge_references_file.js +2 -0
- package/build/generated/bridge_work_order_seed_file.js +2 -0
- package/build/generated/contracts_bundle_info.js +3 -3
- package/build/generated/index.js +14 -0
- package/build/generated/ingress_input.js +2 -0
- package/build/generated/ingress_output.js +2 -0
- package/build/generated/ingress_resolution_file.js +2 -0
- package/build/generated/ingress_summary_file.js +2 -0
- package/build/generated/message_template_id_mapping_file.js +2 -0
- package/build/generated/run_app_input.js +1 -1
- package/build/index.js +4 -1
- package/build/local-mode/git.js +36 -22
- package/build/local-mode/paths.js +1 -0
- package/build/local-mode/project-state.js +176 -0
- package/build/local-mode/setup.js +21 -1
- package/build/local-mode/templates.js +3 -3
- package/build/path-utils.js +68 -0
- package/build/runtime/cli_invoker.js +416 -0
- package/build/tools/vibe_pm/advisory_review.js +5 -3
- package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
- package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
- package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
- package/build/tools/vibe_pm/briefing.js +26 -1
- package/build/tools/vibe_pm/context.js +79 -0
- package/build/tools/vibe_pm/create_work_order.js +200 -3
- package/build/tools/vibe_pm/doctor.js +95 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
- package/build/tools/vibe_pm/export_output.js +14 -13
- package/build/tools/vibe_pm/finalize_work.js +74 -0
- package/build/tools/vibe_pm/force_override.js +104 -0
- package/build/tools/vibe_pm/get_decision.js +2 -2
- package/build/tools/vibe_pm/index.js +160 -3
- package/build/tools/vibe_pm/ingress.js +645 -0
- package/build/tools/vibe_pm/ingress_gate.js +116 -0
- package/build/tools/vibe_pm/inspect_code.js +90 -20
- package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
- package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
- package/build/tools/vibe_pm/kce/preflight.js +11 -7
- package/build/tools/vibe_pm/list_rules.js +135 -0
- package/build/tools/vibe_pm/memory_status.js +11 -8
- package/build/tools/vibe_pm/memory_sync.js +11 -8
- package/build/tools/vibe_pm/pm_language.js +17 -16
- 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/python_error.js +115 -0
- package/build/tools/vibe_pm/run_app.js +215 -86
- package/build/tools/vibe_pm/run_app_podman.js +64 -2
- package/build/tools/vibe_pm/save_rule.js +120 -0
- package/build/tools/vibe_pm/search_oss.js +5 -3
- package/build/tools/vibe_pm/spec_rag.js +185 -0
- package/build/tools/vibe_pm/status.js +50 -3
- package/build/tools/vibe_pm/submit_decision.js +2 -2
- package/build/tools/vibe_pm/types.js +28 -0
- package/build/tools/vibe_pm/undo_last_task.js +23 -20
- package/build/tools/vibe_pm/waiter_mapping.js +155 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
- package/build/tools.js +13 -5
- package/build/version-check.js +5 -5
- package/build/vibe-cli.js +742 -39
- package/package.json +5 -4
- package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
- package/skills/index.json +14 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/local-mode/project-state.ts
|
|
2
|
+
// Per-folder project state persistence
|
|
3
|
+
// Stores workflow state, reminders, and pending actions
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { getVibeRepoPaths } from "./paths.js";
|
|
7
|
+
const DEFAULT_STATE = {
|
|
8
|
+
schema_version: 1,
|
|
9
|
+
phases: [],
|
|
10
|
+
pending_actions: [],
|
|
11
|
+
reminders: [],
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Load project state from .vibe/project_state.json
|
|
15
|
+
*/
|
|
16
|
+
export function loadProjectState(basePath = process.cwd()) {
|
|
17
|
+
const paths = getVibeRepoPaths(basePath);
|
|
18
|
+
const statePath = path.join(paths.vibeDir, "project_state.json");
|
|
19
|
+
if (!fs.existsSync(statePath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(statePath, "utf-8");
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Save project state to .vibe/project_state.json
|
|
32
|
+
*/
|
|
33
|
+
export function saveProjectState(state, basePath = process.cwd()) {
|
|
34
|
+
const paths = getVibeRepoPaths(basePath);
|
|
35
|
+
const statePath = path.join(paths.vibeDir, "project_state.json");
|
|
36
|
+
// Ensure .vibe directory exists
|
|
37
|
+
if (!fs.existsSync(paths.vibeDir)) {
|
|
38
|
+
fs.mkdirSync(paths.vibeDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
state.updated_at = new Date().toISOString();
|
|
41
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get or create project state
|
|
45
|
+
*/
|
|
46
|
+
export function getProjectState(basePath = process.cwd()) {
|
|
47
|
+
const existing = loadProjectState(basePath);
|
|
48
|
+
if (existing) {
|
|
49
|
+
return existing;
|
|
50
|
+
}
|
|
51
|
+
const projectId = path.basename(basePath).toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
52
|
+
return {
|
|
53
|
+
...DEFAULT_STATE,
|
|
54
|
+
project_id: projectId,
|
|
55
|
+
updated_at: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Add a pending action
|
|
60
|
+
*/
|
|
61
|
+
export function addPendingAction(action, basePath = process.cwd()) {
|
|
62
|
+
const state = getProjectState(basePath);
|
|
63
|
+
// Check for duplicate
|
|
64
|
+
const exists = state.pending_actions.some((a) => a.type === action.type && a.reason === action.reason);
|
|
65
|
+
if (exists)
|
|
66
|
+
return;
|
|
67
|
+
state.pending_actions.push({
|
|
68
|
+
...action,
|
|
69
|
+
created_at: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
saveProjectState(state, basePath);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Remove a pending action by type
|
|
75
|
+
*/
|
|
76
|
+
export function removePendingAction(type, basePath = process.cwd()) {
|
|
77
|
+
const state = getProjectState(basePath);
|
|
78
|
+
state.pending_actions = state.pending_actions.filter((a) => a.type !== type);
|
|
79
|
+
saveProjectState(state, basePath);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get all pending actions (for reminders)
|
|
83
|
+
*/
|
|
84
|
+
export function getPendingActions(basePath = process.cwd()) {
|
|
85
|
+
const state = loadProjectState(basePath);
|
|
86
|
+
return state?.pending_actions || [];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Update last commit info
|
|
90
|
+
*/
|
|
91
|
+
export function updateLastCommit(info, basePath = process.cwd()) {
|
|
92
|
+
const state = getProjectState(basePath);
|
|
93
|
+
state.last_commit = info;
|
|
94
|
+
// If pushed, remove git_push pending action
|
|
95
|
+
if (info.pushed) {
|
|
96
|
+
state.pending_actions = state.pending_actions.filter((a) => a.type !== "git_push");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Add git_push pending action if not pushed
|
|
100
|
+
const exists = state.pending_actions.some((a) => a.type === "git_push");
|
|
101
|
+
if (!exists) {
|
|
102
|
+
state.pending_actions.push({
|
|
103
|
+
type: "git_push",
|
|
104
|
+
reason: `커밋 ${info.hash.slice(0, 7)} 푸시 필요`,
|
|
105
|
+
created_at: new Date().toISOString(),
|
|
106
|
+
priority: "medium",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
saveProjectState(state, basePath);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Update last MCP build info
|
|
114
|
+
*/
|
|
115
|
+
export function updateLastMcpBuild(info, basePath = process.cwd()) {
|
|
116
|
+
const state = getProjectState(basePath);
|
|
117
|
+
state.last_mcp_build = info;
|
|
118
|
+
// If published, remove mcp_build and npm_publish pending actions
|
|
119
|
+
if (info.published) {
|
|
120
|
+
state.pending_actions = state.pending_actions.filter((a) => a.type !== "mcp_build" && a.type !== "npm_publish");
|
|
121
|
+
}
|
|
122
|
+
saveProjectState(state, basePath);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format pending actions as reminder text
|
|
126
|
+
*/
|
|
127
|
+
export function formatReminders(basePath = process.cwd()) {
|
|
128
|
+
const actions = getPendingActions(basePath);
|
|
129
|
+
if (actions.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
const lines = ["## 📋 대기 중인 작업"];
|
|
132
|
+
// Sort by priority
|
|
133
|
+
const sorted = [...actions].sort((a, b) => {
|
|
134
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
135
|
+
return order[a.priority] - order[b.priority];
|
|
136
|
+
});
|
|
137
|
+
for (const action of sorted) {
|
|
138
|
+
const emoji = action.priority === "high" ? "🔴" : action.priority === "medium" ? "🟡" : "🟢";
|
|
139
|
+
const tool = getToolForAction(action.type);
|
|
140
|
+
lines.push(`- ${emoji} ${action.reason}${tool ? ` → \`${tool}\`` : ""}`);
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
function getToolForAction(type) {
|
|
145
|
+
const mapping = {
|
|
146
|
+
mcp_build: "vibe_pm.publish_mcp",
|
|
147
|
+
engine_build: null, // Manual
|
|
148
|
+
npm_publish: "vibe_pm.publish_mcp",
|
|
149
|
+
git_push: "vibe_pm.finalize_work",
|
|
150
|
+
review: "vibe_pm.inspect_code",
|
|
151
|
+
custom: null,
|
|
152
|
+
};
|
|
153
|
+
return mapping[type];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check and add MCP build pending action if needed
|
|
157
|
+
*/
|
|
158
|
+
export async function checkAndAddMcpBuildAction(basePath = process.cwd()) {
|
|
159
|
+
// Dynamically import to avoid circular dependency
|
|
160
|
+
const { needsMcpBuild } = await import("../tools/vibe_pm/publish_mcp.js");
|
|
161
|
+
try {
|
|
162
|
+
const needs = await needsMcpBuild(basePath);
|
|
163
|
+
if (needs) {
|
|
164
|
+
addPendingAction({
|
|
165
|
+
type: "mcp_build",
|
|
166
|
+
reason: "MCP 소스 변경됨 - 빌드 필요",
|
|
167
|
+
priority: "high",
|
|
168
|
+
}, basePath);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
@@ -4,6 +4,7 @@ import { initLocalModeRepo } from "./init.js";
|
|
|
4
4
|
import { writeVersionLock } from "./version-lock.js";
|
|
5
5
|
import { ensureEngines } from "../bootstrap/installer.js";
|
|
6
6
|
import { healthCheck } from "../bootstrap/doctor.js";
|
|
7
|
+
import { activateSkills, getSkillsHealth } from "../bootstrap/skills-installer.js";
|
|
7
8
|
/**
|
|
8
9
|
* Run unified setup process
|
|
9
10
|
*/
|
|
@@ -42,5 +43,24 @@ export async function runSetup(repoRoot, cliVersion, opts) {
|
|
|
42
43
|
catch (e) {
|
|
43
44
|
warnings.push(`버전 잠금 파일 생성 실패: ${e instanceof Error ? e.message : String(e)}`);
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
// Step 4: Skills activation
|
|
47
|
+
let skills = { activated: [], errors: [] };
|
|
48
|
+
if (!opts.skipSkills) {
|
|
49
|
+
try {
|
|
50
|
+
const skillsHealth = getSkillsHealth();
|
|
51
|
+
if (skillsHealth.installed && skillsHealth.skills.length > 0) {
|
|
52
|
+
// Activate all available skills by default
|
|
53
|
+
skills = await activateSkills(repoRoot);
|
|
54
|
+
if (skills.errors.length > 0) {
|
|
55
|
+
for (const err of skills.errors) {
|
|
56
|
+
warnings.push(`스킬 활성화 오류: ${err}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
warnings.push(`스킬 활성화 실패: ${e instanceof Error ? e.message : String(e)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { local, engines, skills, versionLock, warnings };
|
|
46
66
|
}
|
|
@@ -763,9 +763,9 @@ jobs:
|
|
|
763
763
|
- name: Run Vibe guard
|
|
764
764
|
shell: bash
|
|
765
765
|
env:
|
|
766
|
-
#
|
|
767
|
-
#
|
|
768
|
-
VIBE_FAIL_ON_WARN: "
|
|
766
|
+
# P0-2: CI 환경에서는 WARN도 빌드 실패 처리 (기본값)
|
|
767
|
+
# 이전 동작이 필요하면 "false"로 변경
|
|
768
|
+
VIBE_FAIL_ON_WARN: "true"
|
|
769
769
|
run: |
|
|
770
770
|
set -euo pipefail
|
|
771
771
|
if [[ ! -f ".vibe/lib/validate.sh" ]]; then
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/path-utils.ts
|
|
2
|
+
// P1-5: Centralized path normalization utilities (DRY)
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Convert Windows backslashes to POSIX forward slashes.
|
|
6
|
+
* This is the most basic normalization used across the codebase.
|
|
7
|
+
*/
|
|
8
|
+
export function toPosixPath(value) {
|
|
9
|
+
return value.replace(/\\/g, "/");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a relative path for POSIX style:
|
|
13
|
+
* - Convert backslashes to forward slashes
|
|
14
|
+
* - Trim whitespace
|
|
15
|
+
* - Remove leading slashes (makes it relative)
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeRelativePosix(value) {
|
|
18
|
+
return (value ?? "").trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a path for comparison/matching:
|
|
22
|
+
* - Convert backslashes to forward slashes
|
|
23
|
+
* - Trim whitespace
|
|
24
|
+
* - Remove leading "./" prefix
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeForMatch(value) {
|
|
27
|
+
let v = (value ?? "").trim().replace(/\\/g, "/");
|
|
28
|
+
if (v.startsWith("./"))
|
|
29
|
+
v = v.slice(2);
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a path relative to a base path and normalize to POSIX.
|
|
34
|
+
* Returns empty string for empty input, "." for base path itself.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveRelativePosix(basePath, value) {
|
|
37
|
+
const trimmed = (value ?? "").trim();
|
|
38
|
+
if (!trimmed)
|
|
39
|
+
return "";
|
|
40
|
+
const absolute = path.isAbsolute(trimmed) ? trimmed : path.resolve(basePath, trimmed);
|
|
41
|
+
const relative = path.relative(basePath, absolute);
|
|
42
|
+
if (!relative)
|
|
43
|
+
return ".";
|
|
44
|
+
return toPosixPath(relative);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Process and deduplicate an array of paths, normalizing each to POSIX relative format.
|
|
48
|
+
* Empty and duplicate paths are filtered out.
|
|
49
|
+
*/
|
|
50
|
+
export function normalizePathArray(basePath, values) {
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const raw of values) {
|
|
54
|
+
const normalized = resolveRelativePosix(basePath, raw);
|
|
55
|
+
if (!normalized || seen.has(normalized))
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(normalized);
|
|
58
|
+
out.push(normalized);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Deduplicate and normalize an array of strings for set operations.
|
|
64
|
+
* Uses basic POSIX normalization (backslash to forward slash).
|
|
65
|
+
*/
|
|
66
|
+
export function uniquePosixPaths(items) {
|
|
67
|
+
return Array.from(new Set(items.map((p) => normalizeRelativePosix(p)).filter(Boolean))).sort();
|
|
68
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli_invoker.ts - Single CLI Invocation Point (SSOT)
|
|
3
|
+
*
|
|
4
|
+
* All MCP tools MUST use this module for CLI invocations.
|
|
5
|
+
* Direct usage of child_process outside this file is forbidden.
|
|
6
|
+
*
|
|
7
|
+
* This module provides:
|
|
8
|
+
* - Centralized CLI invocation
|
|
9
|
+
* - Invocation logging for test verification
|
|
10
|
+
* - Environment sanitization
|
|
11
|
+
* - Timeout handling
|
|
12
|
+
*
|
|
13
|
+
* @module runtime/cli_invoker
|
|
14
|
+
*/
|
|
15
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
+
import { sanitizeEnv } from "../cli.js";
|
|
17
|
+
import { getEngineCtx } from "../engine.js";
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Invocation Log (for test verification)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
const invocationLog = [];
|
|
22
|
+
/**
|
|
23
|
+
* Get a copy of the invocation log
|
|
24
|
+
* Used by tests to verify CLI calls
|
|
25
|
+
*/
|
|
26
|
+
export function getInvocationLog() {
|
|
27
|
+
return [...invocationLog];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Clear the invocation log
|
|
31
|
+
* Should be called in test beforeEach()
|
|
32
|
+
*/
|
|
33
|
+
export function clearInvocationLog() {
|
|
34
|
+
invocationLog.length = 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if a specific CLI prefix was invoked
|
|
38
|
+
* Used by enforcement tests
|
|
39
|
+
*/
|
|
40
|
+
export function wasInvoked(bin, argsPrefix) {
|
|
41
|
+
return invocationLog.some((record) => record.bin === bin &&
|
|
42
|
+
argsPrefix.every((prefix, i) => record.args[i] === prefix));
|
|
43
|
+
}
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Binary Resolution
|
|
46
|
+
// ============================================================================
|
|
47
|
+
let vibeBinPath = null;
|
|
48
|
+
/**
|
|
49
|
+
* Get the vibe CLI binary path
|
|
50
|
+
* This is the path to the vibe-cli.ts entry point
|
|
51
|
+
*/
|
|
52
|
+
async function getVibeBinPath() {
|
|
53
|
+
if (vibeBinPath)
|
|
54
|
+
return vibeBinPath;
|
|
55
|
+
// In production, vibe is invoked via npx or global install
|
|
56
|
+
// For now, we use the current process argv
|
|
57
|
+
// TODO: Resolve proper vibe binary path from package
|
|
58
|
+
vibeBinPath = process.argv[1] ?? "vibe";
|
|
59
|
+
return vibeBinPath;
|
|
60
|
+
}
|
|
61
|
+
/** Known system binaries that should be resolved via PATH */
|
|
62
|
+
const SYSTEM_BINS = new Set([
|
|
63
|
+
"git", "semgrep", "python", "python3", "opa", "node", "npm", "npx",
|
|
64
|
+
// Common utilities that might be needed
|
|
65
|
+
"tar", "curl", "wget", "which", "bash", "sh",
|
|
66
|
+
]);
|
|
67
|
+
/** Engine binary names (for type checking) */
|
|
68
|
+
const ENGINE_BINS = new Set([
|
|
69
|
+
"spec-high", "vibe-execution-engine", "clinic",
|
|
70
|
+
]);
|
|
71
|
+
/**
|
|
72
|
+
* Resolve binary path for a given CLI bin
|
|
73
|
+
*/
|
|
74
|
+
async function resolveBinPath(bin) {
|
|
75
|
+
if (bin === "vibe") {
|
|
76
|
+
return getVibeBinPath();
|
|
77
|
+
}
|
|
78
|
+
// System binary - use the name directly (resolved via PATH)
|
|
79
|
+
if (SYSTEM_BINS.has(bin)) {
|
|
80
|
+
return bin;
|
|
81
|
+
}
|
|
82
|
+
// Engine binary - use cached path from bootstrap
|
|
83
|
+
if (ENGINE_BINS.has(bin)) {
|
|
84
|
+
const ctx = await getEngineCtx();
|
|
85
|
+
const enginePath = ctx.bins[bin];
|
|
86
|
+
if (!enginePath) {
|
|
87
|
+
throw new Error(`Engine binary not installed: ${bin}`);
|
|
88
|
+
}
|
|
89
|
+
return enginePath;
|
|
90
|
+
}
|
|
91
|
+
// Unknown binary - assume it's a system command (PATH resolution)
|
|
92
|
+
// This allows flexibility for arbitrary commands while logging them
|
|
93
|
+
return bin;
|
|
94
|
+
}
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// CLI Invocation
|
|
97
|
+
// ============================================================================
|
|
98
|
+
/**
|
|
99
|
+
* Invoke a CLI command
|
|
100
|
+
*
|
|
101
|
+
* This is the ONLY function that should spawn CLI processes.
|
|
102
|
+
* All other code should use this function.
|
|
103
|
+
*
|
|
104
|
+
* @param inv - Invocation request
|
|
105
|
+
* @returns CLI result with exit code, stdout, stderr, and duration
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const result = await invokeCli({
|
|
110
|
+
* bin: "vibe",
|
|
111
|
+
* args: ["inspect", "--run-id", "abc123"],
|
|
112
|
+
* cwd: "/path/to/project",
|
|
113
|
+
* timeoutMs: 60000
|
|
114
|
+
* });
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export async function invokeCli(inv) {
|
|
118
|
+
const startTime = Date.now();
|
|
119
|
+
const timeoutMs = inv.timeoutMs ?? 120_000;
|
|
120
|
+
// Create log record
|
|
121
|
+
const record = {
|
|
122
|
+
...inv,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
invocationLog.push(record);
|
|
126
|
+
// Resolve binary path
|
|
127
|
+
const binPath = await resolveBinPath(inv.bin);
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
let resolved = false;
|
|
130
|
+
const p = spawn(binPath, inv.args, {
|
|
131
|
+
cwd: inv.cwd,
|
|
132
|
+
env: sanitizeEnv(inv.env),
|
|
133
|
+
shell: false,
|
|
134
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
135
|
+
});
|
|
136
|
+
let stdout = "";
|
|
137
|
+
let stderr = "";
|
|
138
|
+
const finish = (exitCode) => {
|
|
139
|
+
if (resolved)
|
|
140
|
+
return;
|
|
141
|
+
resolved = true;
|
|
142
|
+
const result = {
|
|
143
|
+
exitCode,
|
|
144
|
+
stdout,
|
|
145
|
+
stderr,
|
|
146
|
+
durationMs: Date.now() - startTime,
|
|
147
|
+
};
|
|
148
|
+
// Update log record with result
|
|
149
|
+
record.result = result;
|
|
150
|
+
resolve(result);
|
|
151
|
+
};
|
|
152
|
+
const killTimer = setTimeout(() => {
|
|
153
|
+
try {
|
|
154
|
+
p.kill("SIGKILL");
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Ignore kill errors
|
|
158
|
+
}
|
|
159
|
+
stderr += "\n[TIMEOUT]";
|
|
160
|
+
finish(124);
|
|
161
|
+
}, timeoutMs);
|
|
162
|
+
p.stdout.on("data", (d) => (stdout += d.toString("utf-8")));
|
|
163
|
+
p.stderr.on("data", (d) => (stderr += d.toString("utf-8")));
|
|
164
|
+
p.on("error", (e) => {
|
|
165
|
+
clearTimeout(killTimer);
|
|
166
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
167
|
+
stderr += `\n[SPAWN_ERROR] ${msg}`;
|
|
168
|
+
finish(127);
|
|
169
|
+
});
|
|
170
|
+
p.on("close", (code) => {
|
|
171
|
+
clearTimeout(killTimer);
|
|
172
|
+
finish(code ?? 1);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Convenience Functions
|
|
178
|
+
// ============================================================================
|
|
179
|
+
/**
|
|
180
|
+
* Invoke vibe CLI command (convenience wrapper)
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const result = await invokeVibe(["inspect", "--run-id", "abc123"], "/project");
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export async function invokeVibe(args, cwd, opts) {
|
|
188
|
+
return invokeCli({
|
|
189
|
+
bin: "vibe",
|
|
190
|
+
args,
|
|
191
|
+
cwd,
|
|
192
|
+
env: opts?.env,
|
|
193
|
+
timeoutMs: opts?.timeoutMs,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Invoke engine binary directly (legacy, will be deprecated)
|
|
198
|
+
*
|
|
199
|
+
* Prefer using invokeVibe() with appropriate vibe subcommand instead.
|
|
200
|
+
*
|
|
201
|
+
* @deprecated Use invokeVibe() instead
|
|
202
|
+
*/
|
|
203
|
+
export async function invokeEngine(engine, args, cwd, opts) {
|
|
204
|
+
return invokeCli({
|
|
205
|
+
bin: engine,
|
|
206
|
+
args,
|
|
207
|
+
cwd,
|
|
208
|
+
env: opts?.env,
|
|
209
|
+
timeoutMs: opts?.timeoutMs,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Invoke a system binary (git, semgrep, python, etc.)
|
|
214
|
+
*
|
|
215
|
+
* These commands are resolved via PATH.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* // Run git status
|
|
220
|
+
* const result = await invokeSystem("git", ["status"], "/project");
|
|
221
|
+
*
|
|
222
|
+
* // Run semgrep scan
|
|
223
|
+
* const result = await invokeSystem("semgrep", ["scan", "--config", "auto"], "/project");
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export async function invokeSystem(bin, args, cwd, opts) {
|
|
227
|
+
return invokeCli({
|
|
228
|
+
bin,
|
|
229
|
+
args,
|
|
230
|
+
cwd,
|
|
231
|
+
env: opts?.env,
|
|
232
|
+
timeoutMs: opts?.timeoutMs,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Invoke git command (convenience wrapper)
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const result = await invokeGit(["status"], "/project");
|
|
241
|
+
* const result = await invokeGit(["add", "."], "/project");
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export async function invokeGit(args, cwd, opts) {
|
|
245
|
+
return invokeSystem("git", args, cwd, opts);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Invoke a system binary synchronously
|
|
249
|
+
*
|
|
250
|
+
* Use sparingly - prefer async versions. Only use for:
|
|
251
|
+
* - Startup/bootstrap code
|
|
252
|
+
* - Simple checks that must block
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const result = invokeSystemSync("git", ["rev-parse", "--show-toplevel"], "/project");
|
|
257
|
+
*
|
|
258
|
+
* // With stdin input
|
|
259
|
+
* const result = invokeSystemSync("opa", ["eval", "-I", ...], "/project", {
|
|
260
|
+
* input: JSON.stringify(data)
|
|
261
|
+
* });
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
export function invokeSystemSync(bin, args, cwd, opts) {
|
|
265
|
+
const timeoutMs = opts?.timeoutMs ?? 30_000;
|
|
266
|
+
// Log the invocation (sync version also logs for consistency)
|
|
267
|
+
const record = {
|
|
268
|
+
bin,
|
|
269
|
+
args,
|
|
270
|
+
cwd,
|
|
271
|
+
env: opts?.env,
|
|
272
|
+
timeoutMs,
|
|
273
|
+
timestamp: new Date().toISOString(),
|
|
274
|
+
};
|
|
275
|
+
invocationLog.push(record);
|
|
276
|
+
const result = spawnSync(bin, args, {
|
|
277
|
+
cwd,
|
|
278
|
+
encoding: "utf-8",
|
|
279
|
+
env: sanitizeEnv(opts?.env),
|
|
280
|
+
input: opts?.input,
|
|
281
|
+
stdio: opts?.input ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
282
|
+
timeout: timeoutMs,
|
|
283
|
+
});
|
|
284
|
+
const cliResult = {
|
|
285
|
+
exitCode: result.status ?? (result.signal ? 124 : 1),
|
|
286
|
+
stdout: result.stdout ?? "",
|
|
287
|
+
stderr: result.stderr ?? "",
|
|
288
|
+
};
|
|
289
|
+
// Check for spawn errors (e.g., binary not found)
|
|
290
|
+
if (result.error) {
|
|
291
|
+
const err = result.error;
|
|
292
|
+
cliResult.exitCode = err.code === "ENOENT" ? 127 : 1;
|
|
293
|
+
cliResult.stderr = `[SPAWN_ERROR] ${err.message}`;
|
|
294
|
+
}
|
|
295
|
+
// Update log record
|
|
296
|
+
record.result = { ...cliResult, durationMs: 0 };
|
|
297
|
+
return cliResult;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Invoke git command synchronously
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```typescript
|
|
304
|
+
* const result = invokeGitSync(["rev-parse", "--show-toplevel"], "/project");
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
export function invokeGitSync(args, cwd, opts) {
|
|
308
|
+
return invokeSystemSync("git", args, cwd, opts);
|
|
309
|
+
}
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Detached Process (for background services)
|
|
312
|
+
// ============================================================================
|
|
313
|
+
/**
|
|
314
|
+
* Spawn a detached process that continues running after parent exits
|
|
315
|
+
*
|
|
316
|
+
* Use for background services like web servers.
|
|
317
|
+
* The process is spawned with `detached: true` and `unref()`.
|
|
318
|
+
*
|
|
319
|
+
* @returns PID of the spawned process, or null if spawn failed
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* const pid = spawnDetached("npm", ["run", "dev"], "/project", {
|
|
324
|
+
* PORT: "3000"
|
|
325
|
+
* });
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
export function spawnDetached(bin, args, cwd, env) {
|
|
329
|
+
// Log the invocation
|
|
330
|
+
const record = {
|
|
331
|
+
bin,
|
|
332
|
+
args,
|
|
333
|
+
cwd,
|
|
334
|
+
env,
|
|
335
|
+
timestamp: new Date().toISOString(),
|
|
336
|
+
};
|
|
337
|
+
invocationLog.push(record);
|
|
338
|
+
try {
|
|
339
|
+
const child = spawn(bin, args, {
|
|
340
|
+
cwd,
|
|
341
|
+
detached: true,
|
|
342
|
+
stdio: "ignore",
|
|
343
|
+
env: sanitizeEnv(env),
|
|
344
|
+
});
|
|
345
|
+
// Unref to allow parent to exit independently
|
|
346
|
+
child.unref();
|
|
347
|
+
const pid = child.pid ?? null;
|
|
348
|
+
// Update log record
|
|
349
|
+
record.result = {
|
|
350
|
+
exitCode: 0,
|
|
351
|
+
stdout: "",
|
|
352
|
+
stderr: "",
|
|
353
|
+
durationMs: 0,
|
|
354
|
+
};
|
|
355
|
+
return pid;
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
record.result = {
|
|
359
|
+
exitCode: 127,
|
|
360
|
+
stdout: "",
|
|
361
|
+
stderr: "[SPAWN_ERROR]",
|
|
362
|
+
durationMs: 0,
|
|
363
|
+
};
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Parse VRIP control response from CLI result
|
|
369
|
+
*
|
|
370
|
+
* @param result - CLI invocation result
|
|
371
|
+
* @returns Parsed control response or error
|
|
372
|
+
*/
|
|
373
|
+
export function parseVripResponse(result) {
|
|
374
|
+
if (result.exitCode > 1) {
|
|
375
|
+
// System crash - no JSON expected
|
|
376
|
+
return { ok: false, error: `CLI crashed (code ${result.exitCode}): ${result.stderr}` };
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const ctrl = JSON.parse(result.stdout.trim());
|
|
380
|
+
if (typeof ctrl.ok !== "boolean" || typeof ctrl.run_id !== "string") {
|
|
381
|
+
return { ok: false, error: "Invalid VRIP response: missing ok or run_id" };
|
|
382
|
+
}
|
|
383
|
+
return { ok: true, ctrl };
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
387
|
+
return { ok: false, error: `JSON parse failed: ${msg}\nRaw: ${result.stdout}` };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Invoke vibe CLI with VRIP protocol
|
|
392
|
+
*
|
|
393
|
+
* Automatically adds --json flag and parses VRIP response.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```typescript
|
|
397
|
+
* const { ctrl, result } = await invokeVibeVrip(
|
|
398
|
+
* ["inspect", "--target", "src/"],
|
|
399
|
+
* "/project"
|
|
400
|
+
* );
|
|
401
|
+
* if (ctrl.ok) {
|
|
402
|
+
* const runDir = `.vibe/runs/${ctrl.run_id}`;
|
|
403
|
+
* // Read actual results from run_dir
|
|
404
|
+
* }
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
export async function invokeVibeVrip(args, cwd, opts) {
|
|
408
|
+
// Always add --json for VRIP protocol
|
|
409
|
+
const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
|
|
410
|
+
const result = await invokeVibe(jsonArgs, cwd, opts);
|
|
411
|
+
const parsed = parseVripResponse(result);
|
|
412
|
+
if (!parsed.ok) {
|
|
413
|
+
throw new Error(parsed.error);
|
|
414
|
+
}
|
|
415
|
+
return { ctrl: parsed.ctrl, result };
|
|
416
|
+
}
|