@vibecodetown/mcp-server 2.1.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.
Files changed (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +269 -0
  3. package/build/auth/gate.js +225 -0
  4. package/build/auth/index.js +55 -0
  5. package/build/auth/public_key.js +27 -0
  6. package/build/auth/token_cache.js +122 -0
  7. package/build/auth/token_verifier.js +103 -0
  8. package/build/bootstrap/doctor.js +115 -0
  9. package/build/bootstrap/installer.js +673 -0
  10. package/build/bootstrap/lock.js +37 -0
  11. package/build/bootstrap/platform.js +26 -0
  12. package/build/bootstrap/registry.js +37 -0
  13. package/build/cache/index.js +147 -0
  14. package/build/cli.js +101 -0
  15. package/build/contracts.js +22 -0
  16. package/build/control_plane/gate.js +161 -0
  17. package/build/control_plane/index.js +6 -0
  18. package/build/dx/activity.js +139 -0
  19. package/build/engine.js +106 -0
  20. package/build/errors.js +171 -0
  21. package/build/generated/activate_input.js +2 -0
  22. package/build/generated/activate_output.js +57 -0
  23. package/build/generated/advisory_review_input.js +2 -0
  24. package/build/generated/advisory_review_output.js +35 -0
  25. package/build/generated/auth_token_file.js +2 -0
  26. package/build/generated/briefing_input.js +2 -0
  27. package/build/generated/briefing_output.js +2 -0
  28. package/build/generated/clinic_bridge_file.js +13 -0
  29. package/build/generated/contracts_bundle_info.js +5 -0
  30. package/build/generated/create_work_order_input.js +2 -0
  31. package/build/generated/create_work_order_output.js +2 -0
  32. package/build/generated/current_work_order_file.js +2 -0
  33. package/build/generated/doctor_input.js +2 -0
  34. package/build/generated/doctor_output.js +24 -0
  35. package/build/generated/execution_result.js +2 -0
  36. package/build/generated/execution_task.js +2 -0
  37. package/build/generated/export_output_input.js +2 -0
  38. package/build/generated/export_output_output.js +2 -0
  39. package/build/generated/finalize_work_input.js +2 -0
  40. package/build/generated/finalize_work_output.js +2 -0
  41. package/build/generated/gate_input.js +2 -0
  42. package/build/generated/gate_output.js +2 -0
  43. package/build/generated/gate_result_v1.js +2 -0
  44. package/build/generated/get_decision_input.js +2 -0
  45. package/build/generated/get_decision_output.js +13 -0
  46. package/build/generated/handoff_to_clinic.js +2 -0
  47. package/build/generated/index.js +75 -0
  48. package/build/generated/inspect_code_input.js +2 -0
  49. package/build/generated/inspect_code_output.js +13 -0
  50. package/build/generated/memory_retrieve_output.js +2 -0
  51. package/build/generated/memory_state_file.js +2 -0
  52. package/build/generated/memory_status_input.js +2 -0
  53. package/build/generated/memory_status_output.js +13 -0
  54. package/build/generated/memory_sync_input.js +2 -0
  55. package/build/generated/memory_sync_output.js +13 -0
  56. package/build/generated/plugin_result.js +2 -0
  57. package/build/generated/react_perf_check_patterns_input.js +2 -0
  58. package/build/generated/react_perf_check_patterns_output.js +2 -0
  59. package/build/generated/react_perf_generate_report_input.js +2 -0
  60. package/build/generated/react_perf_generate_report_output.js +2 -0
  61. package/build/generated/repair_plan_input.js +2 -0
  62. package/build/generated/repair_plan_output.js +2 -0
  63. package/build/generated/run_app_input.js +2 -0
  64. package/build/generated/run_app_output.js +2 -0
  65. package/build/generated/run_state_file.js +13 -0
  66. package/build/generated/scaffold_input.js +2 -0
  67. package/build/generated/scaffold_output.js +2 -0
  68. package/build/generated/search_oss_input.js +2 -0
  69. package/build/generated/search_oss_output.js +2 -0
  70. package/build/generated/selection_validation_result.js +2 -0
  71. package/build/generated/signal_agent_input.js +2 -0
  72. package/build/generated/spec_high_ask_queue_items_file.js +2 -0
  73. package/build/generated/spec_high_clinic_bridge_output.js +2 -0
  74. package/build/generated/spec_high_decision_draft_output.js +2 -0
  75. package/build/generated/spec_high_validate_output.js +2 -0
  76. package/build/generated/status_input.js +2 -0
  77. package/build/generated/status_output.js +2 -0
  78. package/build/generated/submit_decision_input.js +2 -0
  79. package/build/generated/submit_decision_output.js +2 -0
  80. package/build/generated/tool_error_output.js +2 -0
  81. package/build/generated/undo_last_task_input.js +2 -0
  82. package/build/generated/undo_last_task_output.js +2 -0
  83. package/build/generated/update_input.js +2 -0
  84. package/build/generated/update_output.js +2 -0
  85. package/build/generated/vibe_pm_inspection_result.js +2 -0
  86. package/build/generated/vibe_pm_report_markdown.js +2 -0
  87. package/build/generated/vibe_pm_verdict.js +2 -0
  88. package/build/generated/vibe_repo_config.js +2 -0
  89. package/build/generated/vibecoding_helper_answer_output.js +2 -0
  90. package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
  91. package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
  92. package/build/generated/work_order_v1.js +2 -0
  93. package/build/generated/zoekt_evidence_input.js +2 -0
  94. package/build/generated/zoekt_evidence_output.js +2 -0
  95. package/build/index.js +111 -0
  96. package/build/legacy_alias.js +65 -0
  97. package/build/local-mode/bash.js +61 -0
  98. package/build/local-mode/config.js +171 -0
  99. package/build/local-mode/git.js +33 -0
  100. package/build/local-mode/init.js +110 -0
  101. package/build/local-mode/paths.js +24 -0
  102. package/build/local-mode/templates.js +856 -0
  103. package/build/local-mode/work-order.js +41 -0
  104. package/build/resources/index.js +246 -0
  105. package/build/security/input-validator.js +119 -0
  106. package/build/security/path-policy.js +289 -0
  107. package/build/security/sandbox.js +228 -0
  108. package/build/tools/react_perf/check_patterns.js +172 -0
  109. package/build/tools/react_perf/generate_report.js +337 -0
  110. package/build/tools/react_perf/index.js +119 -0
  111. package/build/tools/react_perf/rules/advanced.js +325 -0
  112. package/build/tools/react_perf/rules/async.js +104 -0
  113. package/build/tools/react_perf/rules/bundle.js +101 -0
  114. package/build/tools/react_perf/rules/client.js +186 -0
  115. package/build/tools/react_perf/rules/index.js +74 -0
  116. package/build/tools/react_perf/rules/js.js +148 -0
  117. package/build/tools/react_perf/rules/rendering.js +166 -0
  118. package/build/tools/react_perf/rules/rerender.js +161 -0
  119. package/build/tools/react_perf/rules/server.js +141 -0
  120. package/build/tools/react_perf/types.js +127 -0
  121. package/build/tools/vibe_pm/activate.js +102 -0
  122. package/build/tools/vibe_pm/advisory_review.js +77 -0
  123. package/build/tools/vibe_pm/briefing.js +178 -0
  124. package/build/tools/vibe_pm/context.js +439 -0
  125. package/build/tools/vibe_pm/create_work_order.js +271 -0
  126. package/build/tools/vibe_pm/doc_status_gate.js +370 -0
  127. package/build/tools/vibe_pm/doctor.js +262 -0
  128. package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
  129. package/build/tools/vibe_pm/export_output.js +135 -0
  130. package/build/tools/vibe_pm/finalize_work.js +393 -0
  131. package/build/tools/vibe_pm/gate.js +33 -0
  132. package/build/tools/vibe_pm/get_decision.js +281 -0
  133. package/build/tools/vibe_pm/index.js +593 -0
  134. package/build/tools/vibe_pm/inspect_code.js +828 -0
  135. package/build/tools/vibe_pm/intent/generator.js +294 -0
  136. package/build/tools/vibe_pm/intent/index.js +5 -0
  137. package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
  138. package/build/tools/vibe_pm/intent/types.js +70 -0
  139. package/build/tools/vibe_pm/intent/verifier.js +237 -0
  140. package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
  141. package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
  142. package/build/tools/vibe_pm/kce/preflight.js +232 -0
  143. package/build/tools/vibe_pm/local_memory.js +26 -0
  144. package/build/tools/vibe_pm/memory_status.js +82 -0
  145. package/build/tools/vibe_pm/memory_sync.js +134 -0
  146. package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
  147. package/build/tools/vibe_pm/modules/ensure.js +100 -0
  148. package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
  149. package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
  150. package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
  151. package/build/tools/vibe_pm/modules/repo_context.js +56 -0
  152. package/build/tools/vibe_pm/modules/research_v1.js +114 -0
  153. package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
  154. package/build/tools/vibe_pm/pm_language.js +222 -0
  155. package/build/tools/vibe_pm/repair_plan.js +199 -0
  156. package/build/tools/vibe_pm/run_app.js +597 -0
  157. package/build/tools/vibe_pm/run_app_podman.js +64 -0
  158. package/build/tools/vibe_pm/scaffold.js +550 -0
  159. package/build/tools/vibe_pm/search_oss.js +124 -0
  160. package/build/tools/vibe_pm/status.js +153 -0
  161. package/build/tools/vibe_pm/submit_decision.js +87 -0
  162. package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
  163. package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
  164. package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
  165. package/build/tools/vibe_pm/types.js +229 -0
  166. package/build/tools/vibe_pm/undo_last_task.js +163 -0
  167. package/build/tools/vibe_pm/update.js +146 -0
  168. package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
  169. package/build/tools.js +269 -0
  170. package/build/version-check.js +239 -0
  171. package/build/vibe-cli.js +631 -0
  172. package/package.json +76 -0
@@ -0,0 +1,597 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/run_app.ts
2
+ // vibe_pm.run_app - One-click application launch
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { spawn, spawnSync } from "node:child_process";
6
+ import { createHash } from "node:crypto";
7
+ import { resolveRunDir, resolveRunId } from "./context.js";
8
+ import { buildPodmanRunCommand, filterContainerEnvAllowlist, normalizeMounts, normalizePorts } from "./run_app_podman.js";
9
+ /**
10
+ * vibe_pm.run_app - Auto-detect project type and run
11
+ *
12
+ * Steps:
13
+ * 1. Detect project type (Node.js vs Python)
14
+ * 2. Determine run command based on mode
15
+ * 3. Start process in background
16
+ * 4. Return URL and PID
17
+ */
18
+ export async function runApp(input) {
19
+ const basePath = process.cwd();
20
+ const mode = input.mode ?? "dev";
21
+ const customPort = input.port;
22
+ const container = input.container ?? "none";
23
+ // Step 1: Detect project type and get run info
24
+ const projectInfo = detectProject(basePath, mode, customPort);
25
+ if (projectInfo.type === "unknown") {
26
+ return {
27
+ success: false,
28
+ command_executed: "detect_project",
29
+ message: "프로젝트 유형을 감지할 수 없습니다. package.json 또는 requirements.txt가 필요합니다.",
30
+ runtime: "host"
31
+ };
32
+ }
33
+ if (container === "podman") {
34
+ return await runAppWithPodman(input, basePath, mode, projectInfo);
35
+ }
36
+ const opaInput = buildOpaPolicyInput(basePath, projectInfo.command, projectInfo.args, {
37
+ paths: [basePath],
38
+ runtime: "host"
39
+ });
40
+ const opaDecision = evaluateOpaGate(basePath, opaInput);
41
+ if (!opaDecision.allow) {
42
+ return {
43
+ success: false,
44
+ command_executed: `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim(),
45
+ message: formatOpaBlockMessage(opaDecision),
46
+ runtime: "host"
47
+ };
48
+ }
49
+ // Step 2: Start process
50
+ const fullCommand = `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim();
51
+ try {
52
+ const child = spawn(projectInfo.command, projectInfo.args, {
53
+ cwd: basePath,
54
+ detached: true,
55
+ stdio: "ignore",
56
+ env: {
57
+ ...process.env,
58
+ PORT: String(projectInfo.port),
59
+ NODE_ENV: mode === "prod" ? "production" : "development"
60
+ }
61
+ });
62
+ // Unref to allow parent to exit independently
63
+ child.unref();
64
+ const pid = child.pid;
65
+ // Give the process a moment to start
66
+ await new Promise((resolve) => setTimeout(resolve, 500));
67
+ writeRunAppEvidenceBestEffort(basePath, {
68
+ runtime: "host",
69
+ mode,
70
+ port: projectInfo.port,
71
+ url: projectInfo.url,
72
+ command_executed: fullCommand,
73
+ process_id: pid
74
+ });
75
+ return {
76
+ success: true,
77
+ process_id: pid,
78
+ url: projectInfo.url,
79
+ command_executed: fullCommand,
80
+ message: `애플리케이션이 시작되었습니다. ${projectInfo.url} 에서 확인하세요.`,
81
+ runtime: "host"
82
+ };
83
+ }
84
+ catch (err) {
85
+ return {
86
+ success: false,
87
+ command_executed: fullCommand,
88
+ message: `실행 실패: ${err instanceof Error ? err.message : String(err)}`,
89
+ runtime: "host"
90
+ };
91
+ }
92
+ }
93
+ function stableShortHash(input) {
94
+ return createHash("sha256").update(input).digest("hex").slice(0, 12);
95
+ }
96
+ function readCurrentWorkOrderRunId(basePath) {
97
+ const workOrderPath = path.join(basePath, ".vibe", "state", "current_work_order.json");
98
+ if (!fs.existsSync(workOrderPath))
99
+ return null;
100
+ try {
101
+ const raw = JSON.parse(fs.readFileSync(workOrderPath, "utf-8"));
102
+ const runId = typeof raw?.run_id === "string" ? raw.run_id.trim() : "";
103
+ if (!runId)
104
+ return null;
105
+ if (runId.includes("..") || runId.includes("/") || runId.includes("\\") || runId.includes("\0")) {
106
+ return null;
107
+ }
108
+ return runId;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ function resolveRunIdForSecurity(basePath) {
115
+ return readCurrentWorkOrderRunId(basePath) ?? resolveRunId(undefined, basePath).run_id;
116
+ }
117
+ function resolveSecurityDir(basePath, runId) {
118
+ try {
119
+ const resolved = resolveRunDir(runId, basePath);
120
+ const runDir = resolved?.run_dir ?? path.join(basePath, "runs", runId);
121
+ const securityDir = path.join(runDir, "security");
122
+ fs.mkdirSync(securityDir, { recursive: true });
123
+ return securityDir;
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ function writeJsonBestEffort(filePath, payload) {
130
+ try {
131
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
132
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
133
+ }
134
+ catch {
135
+ // ignore
136
+ }
137
+ }
138
+ function normalizeOpaPath(value) {
139
+ return value.replace(/\\/g, "/");
140
+ }
141
+ function normalizeOpaPathRelative(basePath, value) {
142
+ const trimmed = value.trim();
143
+ if (!trimmed)
144
+ return "";
145
+ const absolute = path.isAbsolute(trimmed) ? trimmed : path.resolve(basePath, trimmed);
146
+ const relative = path.relative(basePath, absolute);
147
+ if (!relative)
148
+ return ".";
149
+ return normalizeOpaPath(relative);
150
+ }
151
+ function normalizeOpaPaths(basePath, values) {
152
+ const seen = new Set();
153
+ const out = [];
154
+ for (const raw of values) {
155
+ const normalized = normalizeOpaPathRelative(basePath, raw);
156
+ if (!normalized || seen.has(normalized))
157
+ continue;
158
+ seen.add(normalized);
159
+ out.push(normalized);
160
+ }
161
+ return out;
162
+ }
163
+ function buildOpaPolicyInput(basePath, command, args, opts) {
164
+ const cwd = normalizeOpaPath(basePath);
165
+ const paths = normalizeOpaPaths(basePath, opts?.paths ?? [basePath]);
166
+ const mounts = opts?.mounts ? normalizeOpaPaths(basePath, opts.mounts) : undefined;
167
+ return {
168
+ action: "run_app",
169
+ project_root: cwd,
170
+ request: {
171
+ command,
172
+ args,
173
+ cwd,
174
+ paths,
175
+ mounts
176
+ },
177
+ meta: {
178
+ tool: "vibe_pm.run_app",
179
+ runtime: opts?.runtime ?? "host"
180
+ }
181
+ };
182
+ }
183
+ function evaluateOpaGate(basePath, input) {
184
+ const runId = resolveRunIdForSecurity(basePath);
185
+ const securityDir = resolveSecurityDir(basePath, runId);
186
+ const inputPath = securityDir ? path.join(securityDir, "policy_input.json") : null;
187
+ const resultPath = securityDir ? path.join(securityDir, "policy_result.json") : null;
188
+ if (inputPath)
189
+ writeJsonBestEffort(inputPath, input);
190
+ const policyPath = path.join(basePath, "policy", "vibepm", "run_app.rego");
191
+ if (!fs.existsSync(policyPath)) {
192
+ const deny = {
193
+ allow: false,
194
+ reasons: ["OPA_POLICY_MISSING: policy file missing"],
195
+ matched_rules: ["OPA_POLICY_MISSING"]
196
+ };
197
+ if (resultPath)
198
+ writeJsonBestEffort(resultPath, deny);
199
+ return deny;
200
+ }
201
+ const opa = spawnSync("opa", ["eval", "-f", "json", "-I", "-d", policyPath, "data.vibepm.runapp"], { input: JSON.stringify(input), encoding: "utf-8", timeout: 1500 });
202
+ if (opa.error) {
203
+ const err = opa.error;
204
+ const missing = err?.code === "ENOENT";
205
+ const deny = {
206
+ allow: false,
207
+ reasons: [
208
+ missing ? "OPA_POLICY_MISSING: opa binary not found" : `OPA_POLICY_EVAL_FAILED: ${err.message ?? "unknown error"}`
209
+ ],
210
+ matched_rules: [missing ? "OPA_POLICY_MISSING" : "OPA_POLICY_EVAL_FAILED"]
211
+ };
212
+ if (resultPath)
213
+ writeJsonBestEffort(resultPath, deny);
214
+ return deny;
215
+ }
216
+ if (opa.status !== 0) {
217
+ const deny = {
218
+ allow: false,
219
+ reasons: [`OPA_POLICY_EVAL_FAILED: exit_code=${opa.status}`],
220
+ matched_rules: ["OPA_POLICY_EVAL_FAILED"]
221
+ };
222
+ if (resultPath)
223
+ writeJsonBestEffort(resultPath, deny);
224
+ return deny;
225
+ }
226
+ try {
227
+ const parsed = JSON.parse(opa.stdout || "{}");
228
+ const value = parsed?.result?.[0]?.expressions?.[0]?.value ?? {};
229
+ const allow = Boolean(value?.allow);
230
+ const reasons = Array.isArray(value?.reasons) ? value.reasons : [];
231
+ const matched_rules = Array.isArray(value?.matched_rules) ? value.matched_rules : [];
232
+ const result = { allow, reasons, matched_rules };
233
+ if (typeof value?.allow !== "boolean") {
234
+ const deny = {
235
+ allow: false,
236
+ reasons: ["OPA_POLICY_EVAL_FAILED: missing allow result"],
237
+ matched_rules: ["OPA_POLICY_EVAL_FAILED"]
238
+ };
239
+ if (resultPath)
240
+ writeJsonBestEffort(resultPath, deny);
241
+ return deny;
242
+ }
243
+ if (resultPath)
244
+ writeJsonBestEffort(resultPath, result);
245
+ return result;
246
+ }
247
+ catch (e) {
248
+ const deny = {
249
+ allow: false,
250
+ reasons: ["OPA_POLICY_EVAL_FAILED: invalid JSON output"],
251
+ matched_rules: ["OPA_POLICY_EVAL_FAILED"]
252
+ };
253
+ if (resultPath)
254
+ writeJsonBestEffort(resultPath, deny);
255
+ return deny;
256
+ }
257
+ }
258
+ function formatOpaBlockMessage(result) {
259
+ const text = `${result.reasons?.join(" ") ?? ""} ${(result.matched_rules ?? []).join(" ")}`.toLowerCase();
260
+ if (text.includes("missing")) {
261
+ return "OPA가 설치되어 있지 않거나 정책 파일이 없어 실행을 차단했습니다. OPA 설치/정책 파일을 확인하세요.";
262
+ }
263
+ if (text.includes("eval_failed")) {
264
+ return "OPA 평가에 실패하여 안전을 위해 실행을 차단했습니다. OPA 상태를 확인하세요.";
265
+ }
266
+ return "OPA 정책에서 허용되지 않은 커맨드/경로입니다. 정책을 만족하도록 수정하세요.";
267
+ }
268
+ function ensureDir(dirPath) {
269
+ fs.mkdirSync(dirPath, { recursive: true });
270
+ }
271
+ function writeRunAppEvidenceBestEffort(repoRootAbs, payload) {
272
+ try {
273
+ const dir = path.join(repoRootAbs, ".vibe", "evidence");
274
+ ensureDir(dir);
275
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
276
+ const file = path.join(dir, `run_app_runtime_${ts}.json`);
277
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2), "utf-8");
278
+ return file;
279
+ }
280
+ catch {
281
+ return null;
282
+ }
283
+ }
284
+ async function assertPodmanAvailable() {
285
+ await new Promise((resolve, reject) => {
286
+ const p = spawn("podman", ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
287
+ let out = "";
288
+ let err = "";
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
+ });
298
+ }
299
+ function defaultImageForProject(projectType) {
300
+ if (projectType === "node")
301
+ return "docker.io/library/node:20-bookworm";
302
+ if (projectType === "python")
303
+ return "docker.io/library/python:3.11-slim";
304
+ return "docker.io/library/ubuntu:24.04";
305
+ }
306
+ function shellQuoteArg(arg) {
307
+ // Best-effort: single-quote wrapping for /bin/sh -lc
308
+ return `'${arg.replace(/'/g, `'\"'\"'`)}'`;
309
+ }
310
+ function buildInsideCommand(projectInfo) {
311
+ const cmd = projectInfo.command;
312
+ const args = projectInfo.args.map(shellQuoteArg).join(" ");
313
+ const run = `${cmd} ${args}`.trim();
314
+ if (projectInfo.type === "node") {
315
+ // Avoid polluting host node_modules by mounting a volume at /work/node_modules.
316
+ // Install only if node_modules is missing.
317
+ return `if [ ! -d node_modules ]; then if [ -f package-lock.json ]; then npm ci; else npm install; fi; fi; ${run}`;
318
+ }
319
+ if (projectInfo.type === "python") {
320
+ // Install dependencies best-effort every time (pip cache volume reduces cost).
321
+ // If command is "uvicorn", prefer python -m uvicorn so requirements installation covers it.
322
+ if (cmd === "uvicorn") {
323
+ const uvArgs = projectInfo.args.map(shellQuoteArg).join(" ");
324
+ return `python -m pip install -r requirements.txt && python -m uvicorn ${uvArgs}`.trim();
325
+ }
326
+ return `python -m pip install -r requirements.txt && ${run}`;
327
+ }
328
+ return run;
329
+ }
330
+ async function runPodmanDetached(cmd) {
331
+ return await new Promise((resolve, reject) => {
332
+ const p = spawn(cmd.cmd, cmd.args, { stdio: ["ignore", "pipe", "pipe"] });
333
+ let out = "";
334
+ let err = "";
335
+ p.stdout.on("data", (d) => (out += String(d)));
336
+ p.stderr.on("data", (d) => (err += String(d)));
337
+ p.on("error", reject);
338
+ p.on("close", (code) => resolve({ exitCode: code ?? 1, stdout: out, stderr: err }));
339
+ });
340
+ }
341
+ async function runAppWithPodman(input, repoRootAbs, mode, projectInfo) {
342
+ const image = input.container_image ?? defaultImageForProject(projectInfo.type);
343
+ const runId = stableShortHash(`${repoRootAbs}:${Date.now()}`);
344
+ // mounts & ports
345
+ let mounts;
346
+ try {
347
+ mounts = normalizeMounts(repoRootAbs, input.container_mounts);
348
+ }
349
+ catch (e) {
350
+ return {
351
+ success: false,
352
+ command_executed: "podman run",
353
+ message: `컨테이너 마운트 설정이 허용되지 않습니다: ${e instanceof Error ? e.message : String(e)}`,
354
+ runtime: "podman"
355
+ };
356
+ }
357
+ const ports = normalizePorts(projectInfo.port, input.container_ports);
358
+ // env (allowlist + defaults)
359
+ const userEnv = filterContainerEnvAllowlist(input.container_env);
360
+ const env = {
361
+ ...userEnv,
362
+ PORT: String(projectInfo.port)
363
+ };
364
+ if (projectInfo.type === "node") {
365
+ env.NODE_ENV = mode === "prod" ? "production" : "development";
366
+ }
367
+ if (projectInfo.type === "python") {
368
+ env.PYTHONUNBUFFERED = env.PYTHONUNBUFFERED ?? "1";
369
+ }
370
+ // extra volumes (cache + node_modules safety)
371
+ const repoHash = stableShortHash(repoRootAbs);
372
+ const extraVolumes = [];
373
+ if (projectInfo.type === "node") {
374
+ extraVolumes.push({ name: `vibe-${repoHash}-node-modules`, containerPath: "/work/node_modules" }, { name: `vibe-${repoHash}-npm-cache`, containerPath: "/root/.npm" });
375
+ }
376
+ if (projectInfo.type === "python") {
377
+ extraVolumes.push({ name: `vibe-${repoHash}-pip-cache`, containerPath: "/root/.cache/pip" });
378
+ }
379
+ const insideCommand = buildInsideCommand(projectInfo);
380
+ const pod = buildPodmanRunCommand({
381
+ runNameSuffix: runId,
382
+ image,
383
+ mounts,
384
+ ports,
385
+ env,
386
+ insideCommand,
387
+ extraVolumes
388
+ });
389
+ const opaInput = buildOpaPolicyInput(repoRootAbs, pod.cmd, pod.args, {
390
+ paths: [repoRootAbs, ...mounts.map((m) => m.hostAbs)],
391
+ mounts: mounts.map((m) => m.hostAbs),
392
+ runtime: "podman"
393
+ });
394
+ const opaDecision = evaluateOpaGate(repoRootAbs, opaInput);
395
+ if (!opaDecision.allow) {
396
+ return {
397
+ success: false,
398
+ command_executed: pod.command_executed,
399
+ message: formatOpaBlockMessage(opaDecision),
400
+ runtime: "podman"
401
+ };
402
+ }
403
+ try {
404
+ await assertPodmanAvailable();
405
+ }
406
+ catch (e) {
407
+ return {
408
+ success: false,
409
+ command_executed: "podman --version",
410
+ message: `Podman 실행 불가: ${e instanceof Error ? e.message : String(e)}. container 옵션을 끄고(host) 다시 실행하세요.`,
411
+ runtime: "podman"
412
+ };
413
+ }
414
+ const evidencePath = writeRunAppEvidenceBestEffort(repoRootAbs, {
415
+ runtime: "podman",
416
+ mode,
417
+ image,
418
+ ports,
419
+ env_keys: Object.keys(env),
420
+ mounts,
421
+ extra_volumes: extraVolumes,
422
+ command_executed: pod.command_executed
423
+ });
424
+ const started = await runPodmanDetached(pod);
425
+ if (started.exitCode !== 0) {
426
+ return {
427
+ success: false,
428
+ command_executed: pod.command_executed,
429
+ message: `Podman 실행 실패(exit=${started.exitCode}). ${started.stderr.trim()}`,
430
+ runtime: "podman"
431
+ };
432
+ }
433
+ const containerId = started.stdout.trim().split(/\s+/)[0] || undefined;
434
+ // Give the container a moment to start
435
+ await new Promise((resolve) => setTimeout(resolve, 500));
436
+ writeRunAppEvidenceBestEffort(repoRootAbs, {
437
+ runtime: "podman",
438
+ container_id: containerId,
439
+ url: projectInfo.url,
440
+ evidence: evidencePath
441
+ });
442
+ return {
443
+ success: true,
444
+ url: projectInfo.url,
445
+ command_executed: pod.command_executed,
446
+ message: `애플리케이션이 시작되었습니다. ${projectInfo.url} 에서 확인하세요.`,
447
+ runtime: "podman",
448
+ container_id: containerId
449
+ };
450
+ }
451
+ /**
452
+ * Detect project type and determine run command
453
+ */
454
+ function detectProject(basePath, mode, customPort) {
455
+ const packageJsonPath = path.join(basePath, "package.json");
456
+ const requirementsPath = path.join(basePath, "requirements.txt");
457
+ // Check for Node.js project
458
+ if (fs.existsSync(packageJsonPath)) {
459
+ return detectNodeProject(basePath, packageJsonPath, mode, customPort);
460
+ }
461
+ // Check for Python project
462
+ if (fs.existsSync(requirementsPath)) {
463
+ return detectPythonProject(basePath, mode, customPort);
464
+ }
465
+ return {
466
+ type: "unknown",
467
+ command: "",
468
+ args: [],
469
+ port: 3000,
470
+ url: ""
471
+ };
472
+ }
473
+ /**
474
+ * Detect Node.js project run command
475
+ */
476
+ function detectNodeProject(basePath, packageJsonPath, mode, customPort) {
477
+ const port = customPort ?? 3000;
478
+ const url = `http://localhost:${port}`;
479
+ try {
480
+ const content = fs.readFileSync(packageJsonPath, "utf-8");
481
+ const pkg = JSON.parse(content);
482
+ const scripts = pkg.scripts ?? {};
483
+ // Determine command based on mode and available scripts
484
+ if (mode === "test" && scripts.test) {
485
+ return { type: "node", command: "npm", args: ["run", "test"], port, url };
486
+ }
487
+ if (mode === "prod") {
488
+ if (scripts.start) {
489
+ return { type: "node", command: "npm", args: ["start"], port, url };
490
+ }
491
+ if (scripts.build && scripts.preview) {
492
+ return { type: "node", command: "npm", args: ["run", "preview"], port, url };
493
+ }
494
+ }
495
+ // Dev mode (default)
496
+ if (scripts.dev) {
497
+ return { type: "node", command: "npm", args: ["run", "dev"], port, url };
498
+ }
499
+ if (scripts.start) {
500
+ return { type: "node", command: "npm", args: ["start"], port, url };
501
+ }
502
+ // Fallback: run main file directly
503
+ const mainFile = pkg.main ?? "index.js";
504
+ if (fs.existsSync(path.join(basePath, mainFile))) {
505
+ return { type: "node", command: "node", args: [mainFile], port, url };
506
+ }
507
+ // Try common entry points
508
+ const commonEntries = ["src/index.js", "src/index.ts", "index.ts", "main.js", "server.js", "app.js"];
509
+ for (const entry of commonEntries) {
510
+ if (fs.existsSync(path.join(basePath, entry))) {
511
+ const runner = entry.endsWith(".ts") ? "npx" : "node";
512
+ const runArgs = entry.endsWith(".ts") ? ["tsx", entry] : [entry];
513
+ return { type: "node", command: runner, args: runArgs, port, url };
514
+ }
515
+ }
516
+ }
517
+ catch {
518
+ // Fall through to default
519
+ }
520
+ // Default Node.js command
521
+ return { type: "node", command: "npm", args: ["start"], port, url };
522
+ }
523
+ /**
524
+ * Detect Python project run command
525
+ */
526
+ function detectPythonProject(basePath, mode, customPort) {
527
+ const port = customPort ?? 8000;
528
+ const url = `http://localhost:${port}`;
529
+ // Check for test mode
530
+ if (mode === "test") {
531
+ if (fs.existsSync(path.join(basePath, "pytest.ini")) ||
532
+ fs.existsSync(path.join(basePath, "tests"))) {
533
+ return { type: "python", command: "python", args: ["-m", "pytest"], port, url };
534
+ }
535
+ return { type: "python", command: "python", args: ["-m", "unittest"], port, url };
536
+ }
537
+ // Check for Django
538
+ const managePyPath = path.join(basePath, "manage.py");
539
+ if (fs.existsSync(managePyPath)) {
540
+ return {
541
+ type: "python",
542
+ command: "python",
543
+ args: ["manage.py", "runserver", `0.0.0.0:${port}`],
544
+ port,
545
+ url
546
+ };
547
+ }
548
+ // Check for Flask
549
+ const appPyPath = path.join(basePath, "app.py");
550
+ if (fs.existsSync(appPyPath)) {
551
+ // Check if it's a Flask app
552
+ try {
553
+ const content = fs.readFileSync(appPyPath, "utf-8");
554
+ if (content.includes("flask") || content.includes("Flask")) {
555
+ return {
556
+ type: "python",
557
+ command: "python",
558
+ args: ["-m", "flask", "run", "--host=0.0.0.0", `--port=${port}`],
559
+ port,
560
+ url
561
+ };
562
+ }
563
+ }
564
+ catch {
565
+ // Fall through
566
+ }
567
+ }
568
+ // Check for FastAPI
569
+ const mainPyPath = path.join(basePath, "main.py");
570
+ if (fs.existsSync(mainPyPath)) {
571
+ try {
572
+ const content = fs.readFileSync(mainPyPath, "utf-8");
573
+ if (content.includes("fastapi") || content.includes("FastAPI")) {
574
+ return {
575
+ type: "python",
576
+ command: "uvicorn",
577
+ args: ["main:app", "--host", "0.0.0.0", "--port", String(port), "--reload"],
578
+ port,
579
+ url
580
+ };
581
+ }
582
+ }
583
+ catch {
584
+ // Fall through
585
+ }
586
+ }
587
+ // Default: run main.py
588
+ if (fs.existsSync(mainPyPath)) {
589
+ return { type: "python", command: "python", args: ["main.py"], port, url };
590
+ }
591
+ // Try app.py
592
+ if (fs.existsSync(appPyPath)) {
593
+ return { type: "python", command: "python", args: ["app.py"], port, url };
594
+ }
595
+ // Fallback
596
+ return { type: "python", command: "python", args: ["main.py"], port, url };
597
+ }
@@ -0,0 +1,64 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/run_app_podman.ts
2
+ // Pure helpers for Podman run_app path (testable without spawning Podman)
3
+ import * as path from "node:path";
4
+ export const CONTAINER_ENV_ALLOWLIST = new Set(["NODE_ENV", "PORT", "PYTHONUNBUFFERED", "CI"]);
5
+ export function filterContainerEnvAllowlist(env) {
6
+ const out = {};
7
+ if (!env)
8
+ return out;
9
+ for (const [k, v] of Object.entries(env)) {
10
+ if (CONTAINER_ENV_ALLOWLIST.has(k))
11
+ out[k] = v;
12
+ }
13
+ return out;
14
+ }
15
+ function isSubPath(childAbs, parentAbs) {
16
+ const rel = path.relative(parentAbs, childAbs);
17
+ return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
18
+ }
19
+ export function normalizeMounts(repoRootAbs, mounts) {
20
+ const out = [];
21
+ for (const m of mounts ?? []) {
22
+ const hostAbs = path.isAbsolute(m.host) ? m.host : path.resolve(repoRootAbs, m.host);
23
+ if (hostAbs !== repoRootAbs && !isSubPath(hostAbs, repoRootAbs)) {
24
+ throw new Error(`mount_escape_blocked: host=${hostAbs}`);
25
+ }
26
+ out.push({ hostAbs, container: m.container, mode: m.mode });
27
+ }
28
+ if (!out.some((m) => m.container === "/work")) {
29
+ out.unshift({ hostAbs: repoRootAbs, container: "/work", mode: "rw" });
30
+ }
31
+ return out;
32
+ }
33
+ export function normalizePorts(primary, ports) {
34
+ const out = new Set([primary]);
35
+ for (const p of ports ?? [])
36
+ out.add(p);
37
+ return [...out];
38
+ }
39
+ export function buildPodmanRunCommand(args) {
40
+ const podArgs = [
41
+ "run",
42
+ "-d",
43
+ "--rm",
44
+ "--name",
45
+ `vibe-run-${args.runNameSuffix}`,
46
+ "-w",
47
+ "/work"
48
+ ];
49
+ for (const m of args.mounts) {
50
+ podArgs.push("-v", `${m.hostAbs}:${m.container}:${m.mode}`);
51
+ }
52
+ for (const v of args.extraVolumes) {
53
+ podArgs.push("-v", `${v.name}:${v.containerPath}:rw`);
54
+ }
55
+ for (const [k, v] of Object.entries(args.env)) {
56
+ podArgs.push("-e", `${k}=${v}`);
57
+ }
58
+ for (const p of args.ports) {
59
+ podArgs.push("-p", `${p}:${p}`);
60
+ }
61
+ podArgs.push("--pull=missing");
62
+ podArgs.push(args.image, "sh", "-lc", args.insideCommand);
63
+ return { cmd: "podman", args: podArgs, command_executed: ["podman", ...podArgs].join(" ") };
64
+ }