@yr-kits/dev-copilot 0.1.0 → 0.1.2

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 (33) hide show
  1. package/dist/app/index.d.mts +2 -0
  2. package/dist/app/index.mjs +4 -0
  3. package/dist/bin/bridge.mjs +4 -881
  4. package/dist/bin/bridge.mjs.map +1 -1
  5. package/dist/bin/dev-copilot.mjs +13 -3
  6. package/dist/bin/dev-copilot.mjs.map +1 -1
  7. package/dist/bridge/app/index.d.mts +24 -0
  8. package/dist/bridge/app/index.d.mts.map +1 -0
  9. package/dist/bridge/app/index.mjs +3 -0
  10. package/dist/{types/index.d.mts → copilot-D8--qKgC.d.mts} +3 -3
  11. package/dist/copilot-D8--qKgC.d.mts.map +1 -0
  12. package/dist/copilot-overlay-ClRoIHew.mjs +1129 -0
  13. package/dist/copilot-overlay-ClRoIHew.mjs.map +1 -0
  14. package/dist/copilot-overlay-NlUgrMjy.d.mts +7 -0
  15. package/dist/copilot-overlay-NlUgrMjy.d.mts.map +1 -0
  16. package/dist/dev-copilot-context-CA9bqV_U.mjs +35 -0
  17. package/dist/dev-copilot-context-CA9bqV_U.mjs.map +1 -0
  18. package/dist/dev-copilot-provider-CP_gJvWG.d.mts +22 -0
  19. package/dist/dev-copilot-provider-CP_gJvWG.d.mts.map +1 -0
  20. package/dist/dev-copilot-provider-D0_wEEem.mjs +14 -0
  21. package/dist/dev-copilot-provider-D0_wEEem.mjs.map +1 -0
  22. package/dist/index.d.mts +5 -33
  23. package/dist/index.mjs +6 -849
  24. package/dist/run-bridge-cli-DVLGcVgq.mjs +1510 -0
  25. package/dist/run-bridge-cli-DVLGcVgq.mjs.map +1 -0
  26. package/dist/types.d.mts +2 -0
  27. package/dist/widgets/copilot-overlay/index.d.mts +2 -0
  28. package/dist/widgets/copilot-overlay/index.mjs +4 -0
  29. package/package.json +29 -9
  30. package/dist/index.d.mts.map +0 -1
  31. package/dist/index.mjs.map +0 -1
  32. package/dist/types/index.d.mts.map +0 -1
  33. /package/dist/{types/index.mjs → types.mjs} +0 -0
@@ -1,889 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { promises } from "node:fs";
3
- import path from "node:path";
4
- import { execFile } from "node:child_process";
5
- import { createServer } from "node:http";
6
- import { promisify } from "node:util";
7
- import os from "node:os";
8
- import { randomUUID } from "node:crypto";
2
+ import { t as runDevCopilotBridgeCli } from "../run-bridge-cli-DVLGcVgq.mjs";
9
3
 
10
- //#region src/bridge/lib/config.ts
11
- const DEFAULT_ALLOWED_PATHS = [
12
- "app",
13
- "src",
14
- "widgets",
15
- "features",
16
- "entities",
17
- "shared",
18
- "components"
19
- ];
20
- const createDevCopilotBridgeConfig = (config) => {
21
- return {
22
- rootDir: config.rootDir,
23
- host: config.host ?? "127.0.0.1",
24
- port: config.port ?? 3339,
25
- corsOrigin: config.corsOrigin ?? "*",
26
- agent: config.agent ?? "codex",
27
- allowedPaths: config.allowedPaths ?? DEFAULT_ALLOWED_PATHS
28
- };
29
- };
30
-
31
- //#endregion
32
- //#region src/bridge/lib/guards.ts
33
- const resolveAndValidatePath = (filePath, allowedPaths, rootDir) => {
34
- if (!filePath) throw new Error("허용되지 않은 파일 경로입니다.");
35
- let normalizedInput = filePath.replaceAll("\\", "/");
36
- if (rootDir) {
37
- const normalizedRootDir = rootDir.replaceAll("\\", "/").replace(/\/+$/, "");
38
- if (normalizedInput.startsWith(`${normalizedRootDir}/`)) normalizedInput = normalizedInput.slice(normalizedRootDir.length + 1);
39
- }
40
- if (path.isAbsolute(normalizedInput) || normalizedInput.includes("..")) throw new Error("허용되지 않은 파일 경로입니다.");
41
- const normalized = normalizedInput;
42
- const firstSegment = normalized.split("/")[0];
43
- if (!(allowedPaths.includes(".") || allowedPaths.includes("*")) && !allowedPaths.includes(firstSegment)) throw new Error(`허용 경로 외 파일은 수정할 수 없습니다: ${firstSegment}`);
44
- return normalized;
45
- };
46
-
47
- //#endregion
48
- //#region src/bridge/internal/prompts.ts
49
- const buildAgentPrompt = (request) => {
50
- if (request.mode === "answer") return [
51
- "Answer the user's web overlay request.",
52
- "Do not modify files.",
53
- "Use the provided project context when it is relevant.",
54
- "Respond in Korean.",
55
- "Keep the answer concise and actionable.",
56
- "",
57
- `Current route: ${request.route ?? "unknown"}`,
58
- `Selected text:\n${request.selectedText || "(none)"}`,
59
- `User prompt:\n${request.prompt}`,
60
- `Previous AI response:\n${request.previousResponse ?? "(none)"}`,
61
- `Project context:\n${request.projectContext ?? "(none)"}`
62
- ].join("\n");
63
- return [
64
- "You are a local code-edit proposal generator called from a web overlay.",
65
- "Do not modify files directly.",
66
- "Return JSON only.",
67
- "Do not generate unified diff directly.",
68
- "Put repository-relative file path(path), exact old text(oldText), and replacement text(newText) into the changes array.",
69
- "Never use absolute paths. The path must be relative to the repository root, for example src/features/article/model/data.ts.",
70
- "oldText must exactly match the file content and must not use ellipsis.",
71
- "Ground the proposal in the selected text, user prompt, and provided project context.",
72
- "Write the message field in Korean.",
73
- "",
74
- `Current route: ${request.route ?? "unknown"}`,
75
- `Allowed path hints: ${(request.fileHints ?? []).join(", ") || "(none)"}`,
76
- `Selected text:\n${request.selectedText || "(none)"}`,
77
- `User prompt:\n${request.prompt}`,
78
- `Previous AI response:\n${request.previousResponse ?? "(none)"}`,
79
- `Project context:\n${request.projectContext ?? "(none)"}`
80
- ].join("\n");
81
- };
82
-
83
- //#endregion
84
- //#region src/bridge/internal/agents/claude-adapter.ts
85
- const execFileAsync$3 = promisify(execFile);
86
- const toClaudeErrorMessage = (error) => {
87
- const message = error instanceof Error ? error.message : String(error);
88
- if (/401|authentication|invalid authentication credentials|please run \/login/i.test(message)) return "Claude Code 로그인이 필요합니다. 터미널에서 `claude /login`을 실행해 주세요.";
89
- if (/ENOENT/.test(message)) return "Claude CLI를 찾을 수 없습니다. Claude Code CLI 설치 상태를 확인해 주세요.";
90
- return message;
91
- };
92
- const findFirstString = (value) => {
93
- if (typeof value === "string") {
94
- const text = value.trim();
95
- return text ? text : null;
96
- }
97
- if (Array.isArray(value)) {
98
- for (const item of value) {
99
- const found = findFirstString(item);
100
- if (found) return found;
101
- }
102
- return null;
103
- }
104
- if (value && typeof value === "object") {
105
- const record = value;
106
- for (const key of [
107
- "result",
108
- "message",
109
- "output",
110
- "text",
111
- "content"
112
- ]) if (key in record) {
113
- const found = findFirstString(record[key]);
114
- if (found) return found;
115
- }
116
- for (const nestedValue of Object.values(record)) {
117
- const found = findFirstString(nestedValue);
118
- if (found) return found;
119
- }
120
- }
121
- return null;
122
- };
123
- const parseClaudeJsonOutput = (raw) => {
124
- const parsed = JSON.parse(raw);
125
- return {
126
- parsed,
127
- text: findFirstString(parsed) ?? ""
128
- };
129
- };
130
- const parseClaudeEditResponse = (raw) => {
131
- const { text } = parseClaudeJsonOutput(raw);
132
- const parsed = JSON.parse(text);
133
- return {
134
- message: parsed.message,
135
- patchPreview: parsed.patchPreview,
136
- changes: parsed.changes,
137
- warnings: parsed.warnings ?? []
138
- };
139
- };
140
- const claudeAdapter = {
141
- agent: "claude",
142
- async run(request) {
143
- const prompt = buildAgentPrompt(request);
144
- const args = [
145
- "-p",
146
- "--output-format",
147
- "json",
148
- ...request.mode === "edit" ? ["--json-schema", "{\"type\":\"object\",\"properties\":{\"message\":{\"type\":\"string\"},\"patchPreview\":{\"type\":\"string\"},\"warnings\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"changes\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"},\"oldText\":{\"type\":\"string\"},\"newText\":{\"type\":\"string\"}},\"required\":[\"path\",\"oldText\",\"newText\"],\"additionalProperties\":false}}},\"required\":[\"message\"],\"additionalProperties\":false}"] : [],
149
- prompt
150
- ];
151
- try {
152
- const { stdout } = await execFileAsync$3("claude", args, {
153
- cwd: request.cwd,
154
- maxBuffer: 1024 * 1024 * 5,
155
- timeout: Number(process.env.DEV_COPILOT_AGENT_TIMEOUT_MS ?? 12e4),
156
- env: {
157
- ...process.env,
158
- NO_COLOR: "1"
159
- }
160
- });
161
- if (request.mode === "answer") {
162
- const { text } = parseClaudeJsonOutput(stdout);
163
- return {
164
- message: text,
165
- warnings: []
166
- };
167
- }
168
- return parseClaudeEditResponse(stdout);
169
- } catch (error) {
170
- throw new Error(toClaudeErrorMessage(error));
171
- }
172
- },
173
- async getStatus(cwd) {
174
- try {
175
- await execFileAsync$3("claude", [
176
- "-p",
177
- "--output-format",
178
- "json",
179
- "OK만 출력해줘."
180
- ], {
181
- cwd,
182
- timeout: 15e3,
183
- maxBuffer: 1024 * 512
184
- });
185
- return {
186
- available: true,
187
- authenticated: true,
188
- agent: "claude",
189
- message: "Claude Code CLI에 로그인되어 있습니다."
190
- };
191
- } catch (error) {
192
- const message = error instanceof Error ? error.message : String(error);
193
- const unavailable = /ENOENT/.test(message);
194
- const authError = /401|authentication|please run \/login|invalid authentication credentials/i.test(message);
195
- return {
196
- available: !unavailable,
197
- authenticated: false,
198
- agent: "claude",
199
- message: unavailable ? "Claude CLI를 찾을 수 없습니다." : authError ? "Claude Code 로그인이 필요합니다." : message,
200
- loginCommand: unavailable ? void 0 : "claude /login"
201
- };
202
- }
203
- }
204
- };
205
-
206
- //#endregion
207
- //#region src/bridge/internal/agents/codex-adapter.ts
208
- const execFileAsync$2 = promisify(execFile);
209
- const codexDiffResponseSchema = {
210
- type: "object",
211
- properties: {
212
- message: { type: "string" },
213
- patchPreview: { type: "string" },
214
- warnings: {
215
- type: "array",
216
- items: { type: "string" }
217
- },
218
- changes: {
219
- type: "array",
220
- items: {
221
- type: "object",
222
- properties: {
223
- path: { type: "string" },
224
- oldText: { type: "string" },
225
- newText: { type: "string" }
226
- },
227
- required: [
228
- "path",
229
- "oldText",
230
- "newText"
231
- ],
232
- additionalProperties: false
233
- }
234
- }
235
- },
236
- required: ["message"],
237
- additionalProperties: false
238
- };
239
- const parseAnswerResponse = (rawOutput) => {
240
- return {
241
- message: rawOutput.trim(),
242
- warnings: []
243
- };
244
- };
245
- const parseEditResponse = async (outputPath) => {
246
- const content = await promises.readFile(outputPath, "utf-8");
247
- const parsed = JSON.parse(content);
248
- return {
249
- message: parsed.message,
250
- patchPreview: parsed.patchPreview,
251
- changes: parsed.changes,
252
- warnings: parsed.warnings ?? []
253
- };
254
- };
255
- const toCodexErrorMessage = (error) => {
256
- const message = error instanceof Error ? error.message : String(error);
257
- if (/not logged in|login required|authentication|unauthorized/i.test(message)) return "Codex CLI 로그인이 필요합니다. 터미널에서 `codex login`을 실행해 주세요.";
258
- if (/ENOENT/.test(message)) return "Codex CLI를 찾을 수 없습니다. Codex CLI 설치 상태를 확인해 주세요.";
259
- return message;
260
- };
261
- const getCodexModelName = async (cwd) => {
262
- const outputPath = path.join(os.tmpdir(), `dev-copilot-codex-status-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
263
- try {
264
- const { stdout, stderr } = await execFileAsync$2("codex", [
265
- "exec",
266
- "--cd",
267
- cwd,
268
- "--sandbox",
269
- "read-only",
270
- "--skip-git-repo-check",
271
- "--output-last-message",
272
- outputPath,
273
- "OK만 출력해줘."
274
- ], {
275
- cwd,
276
- timeout: 3e4,
277
- maxBuffer: 1024 * 512,
278
- env: {
279
- ...process.env,
280
- NO_COLOR: "1"
281
- }
282
- });
283
- return `${stdout}\n${stderr}`.match(/^model:\s*(.+)$/m)?.[1]?.trim();
284
- } catch {
285
- return;
286
- } finally {
287
- await promises.rm(outputPath, { force: true });
288
- }
289
- };
290
- const codexAdapter = {
291
- agent: "codex",
292
- async run(request) {
293
- const outputPath = path.join(os.tmpdir(), `dev-copilot-codex-${Date.now()}-${Math.random().toString(16).slice(2)}.json`);
294
- const schemaPath = path.join(os.tmpdir(), `dev-copilot-codex-schema-${Date.now()}-${Math.random().toString(16).slice(2)}.json`);
295
- await promises.writeFile(schemaPath, JSON.stringify(codexDiffResponseSchema), "utf-8");
296
- const prompt = buildAgentPrompt(request);
297
- const args = [
298
- "exec",
299
- "--cd",
300
- request.cwd,
301
- "--sandbox",
302
- "read-only",
303
- "--skip-git-repo-check",
304
- "--output-last-message",
305
- outputPath,
306
- ...request.mode === "edit" ? ["--output-schema", schemaPath] : [],
307
- prompt
308
- ];
309
- try {
310
- const { stdout } = await execFileAsync$2("codex", args, {
311
- cwd: request.cwd,
312
- maxBuffer: 1024 * 1024 * 5,
313
- timeout: Number(process.env.DEV_COPILOT_AGENT_TIMEOUT_MS ?? 12e4),
314
- env: {
315
- ...process.env,
316
- NO_COLOR: "1"
317
- }
318
- });
319
- if (request.mode === "answer") try {
320
- return parseAnswerResponse(await promises.readFile(outputPath, "utf-8"));
321
- } catch {
322
- return parseAnswerResponse(stdout);
323
- }
324
- return await parseEditResponse(outputPath);
325
- } catch (error) {
326
- throw new Error(toCodexErrorMessage(error));
327
- } finally {
328
- await promises.rm(outputPath, { force: true });
329
- await promises.rm(schemaPath, { force: true });
330
- }
331
- },
332
- async getStatus(cwd) {
333
- try {
334
- const { stdout, stderr } = await execFileAsync$2("codex", ["login", "status"], {
335
- cwd,
336
- timeout: 1e4,
337
- maxBuffer: 1024 * 128
338
- });
339
- const output = `${stdout}\n${stderr}`.trim();
340
- const authenticated = /logged in/i.test(output);
341
- const model = authenticated ? await getCodexModelName(cwd) : void 0;
342
- return {
343
- available: true,
344
- authenticated,
345
- agent: "codex",
346
- message: authenticated ? output || "Codex CLI에 로그인되어 있습니다." : output || "Codex CLI 로그인이 필요합니다.",
347
- model,
348
- loginCommand: authenticated ? void 0 : "codex login"
349
- };
350
- } catch (error) {
351
- const message = error instanceof Error ? error.message : "Codex CLI 상태 확인에 실패했습니다.";
352
- return {
353
- available: !message.includes("ENOENT"),
354
- authenticated: false,
355
- agent: "codex",
356
- message: message.includes("ENOENT") ? "Codex CLI를 찾을 수 없습니다." : message,
357
- loginCommand: message.includes("ENOENT") ? void 0 : "codex login"
358
- };
359
- }
360
- }
361
- };
362
-
363
- //#endregion
364
- //#region src/bridge/internal/agents/index.ts
365
- const adapters = {
366
- codex: codexAdapter,
367
- claude: claudeAdapter
368
- };
369
- const resolveAgentAdapter = (agent) => {
370
- return adapters[agent];
371
- };
372
-
373
- //#endregion
374
- //#region src/bridge/internal/project-context.ts
375
- const execFileAsync$1 = promisify(execFile);
376
- const normalizeRelativePath = (filePath) => {
377
- return filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
378
- };
379
- const isIgnoredPath = (relativePath, config) => {
380
- return normalizeRelativePath(relativePath).split("/").some((segment) => config.ignoredDirs.includes(segment));
381
- };
382
- const walk = async (relativeDir, config, results, limit) => {
383
- if (results.length >= limit || isIgnoredPath(relativeDir, config)) return;
384
- const absoluteDir = path.join(config.rootDir, relativeDir);
385
- const entries = await promises.readdir(absoluteDir, { withFileTypes: true });
386
- for (const entry of entries) {
387
- if (results.length >= limit) return;
388
- const relativePath = normalizeRelativePath(path.join(relativeDir, entry.name));
389
- if (isIgnoredPath(relativePath, config)) continue;
390
- if (entry.isDirectory()) {
391
- await walk(relativePath, config, results, limit);
392
- continue;
393
- }
394
- if (entry.isFile()) results.push(relativePath);
395
- }
396
- };
397
- const listProjectFiles = async (config, query, limit = 80) => {
398
- const results = [];
399
- for (const allowedDir of config.allowedDirs) {
400
- const absoluteDir = path.join(config.rootDir, allowedDir);
401
- try {
402
- if ((await promises.stat(absoluteDir)).isDirectory()) await walk(allowedDir, config, results, limit);
403
- } catch {
404
- continue;
405
- }
406
- }
407
- if (!query) return results;
408
- return results.filter((filePath) => filePath.toLowerCase().includes(query.toLowerCase()));
409
- };
410
- const readProjectFile = async (config, filePath, maxBytes = config.maxReadBytes) => {
411
- const absolutePath = path.join(config.rootDir, normalizeRelativePath(filePath));
412
- return {
413
- path: filePath,
414
- content: (await promises.readFile(absolutePath, "utf-8")).slice(0, maxBytes)
415
- };
416
- };
417
- const searchWithRg = async (config, query, limit) => {
418
- const { stdout } = await execFileAsync$1("rg", [
419
- "--line-number",
420
- "--fixed-strings",
421
- "--color",
422
- "never",
423
- "--glob",
424
- "!node_modules/**",
425
- "--glob",
426
- "!.next/**",
427
- "--glob",
428
- "!.git/**",
429
- query,
430
- ...config.allowedDirs
431
- ], {
432
- cwd: config.rootDir,
433
- maxBuffer: 1024 * 1024
434
- });
435
- return stdout.split("\n").filter(Boolean).slice(0, limit).map((line) => {
436
- const [filePath, lineNumber, ...rest] = line.split(":");
437
- return {
438
- path: filePath,
439
- line: Number(lineNumber),
440
- text: rest.join(":").trim()
441
- };
442
- });
443
- };
444
- const searchWithNode = async (config, query, limit) => {
445
- const files = await listProjectFiles(config, void 0, 500);
446
- const results = [];
447
- for (const filePath of files) {
448
- if (results.length >= limit) break;
449
- try {
450
- (await readProjectFile(config, filePath, 1e5)).content.split("\n").forEach((line, index) => {
451
- if (results.length < limit && line.includes(query)) results.push({
452
- path: filePath,
453
- line: index + 1,
454
- text: line.trim()
455
- });
456
- });
457
- } catch {
458
- continue;
459
- }
460
- }
461
- return results;
462
- };
463
- const searchProjectText = async (config, query, limit = config.maxSearchResults) => {
464
- if (!query.trim()) return [];
465
- try {
466
- return await searchWithRg(config, query, limit);
467
- } catch {
468
- return searchWithNode(config, query, limit);
469
- }
470
- };
471
- const findComponentByText = async (config, text, limit = 8) => {
472
- const normalized = text.replace(/\s+/g, " ").trim();
473
- const candidates = [
474
- normalized,
475
- normalized.slice(0, 120),
476
- ...normalized.split(/[.!?]\s+/).filter((item) => item.length > 12)
477
- ];
478
- for (const candidate of candidates) {
479
- const results = await searchProjectText(config, candidate, limit);
480
- if (results.length) return {
481
- query: candidate,
482
- results
483
- };
484
- }
485
- return {
486
- query: normalized,
487
- results: []
488
- };
489
- };
490
- const readJsonFile = async (rootDir, filePath) => {
491
- try {
492
- const content = await promises.readFile(path.join(rootDir, filePath), "utf-8");
493
- return JSON.parse(content);
494
- } catch {
495
- return null;
496
- }
497
- };
498
- const getProjectContext = async (config) => {
499
- const packageJson = await readJsonFile(config.rootDir, "package.json");
500
- const tsconfig = await readJsonFile(config.rootDir, "tsconfig.json");
501
- return {
502
- rootDir: config.rootDir,
503
- allowedDirs: config.allowedDirs,
504
- packageName: packageJson?.name,
505
- scripts: packageJson?.scripts,
506
- dependencies: packageJson?.dependencies,
507
- devDependencies: packageJson?.devDependencies,
508
- tsconfigPaths: (tsconfig?.compilerOptions)?.paths
509
- };
510
- };
511
- const buildCopilotProjectContext = async (rootDir, allowedDirs, params) => {
512
- const config = {
513
- rootDir,
514
- allowedDirs,
515
- ignoredDirs: [
516
- ".git",
517
- ".next",
518
- "node_modules",
519
- "coverage",
520
- "public",
521
- "dist",
522
- "build"
523
- ],
524
- maxReadBytes: 3e4,
525
- maxSearchResults: 20
526
- };
527
- const project = await getProjectContext(config);
528
- const selectedText = params.selectedText?.trim();
529
- const routeQuery = params.route?.startsWith("/") ? `app${params.route === "/" ? "" : params.route}/page.tsx` : params.route;
530
- const textMatches = selectedText ? await findComponentByText(config, selectedText, 8) : {
531
- query: "",
532
- results: []
533
- };
534
- const routeMatches = routeQuery ? await searchProjectText(config, routeQuery, 5) : [];
535
- return JSON.stringify({
536
- project,
537
- requestContext: {
538
- route: params.route,
539
- fileHints: params.fileHints
540
- },
541
- selectedTextLookup: textMatches,
542
- routeLookup: routeMatches,
543
- guidance: "이 컨텍스트는 로컬 프로젝트에서 수집한 실제 코드 검색 결과입니다. 수정 제안은 이 결과를 우선 근거로 삼고 path/oldText/newText 기반으로 작성하세요."
544
- }, null, 2);
545
- };
546
-
547
- //#endregion
548
- //#region src/bridge/lib/patch.ts
549
- const execFileAsync = promisify(execFile);
550
- const getChangedFiles = (patchPreview) => {
551
- const regex = /^\+\+\+ b\/(.+)$/gm;
552
- const changedFiles = /* @__PURE__ */ new Set();
553
- let match = regex.exec(patchPreview);
554
- while (match) {
555
- changedFiles.add(match[1]);
556
- match = regex.exec(patchPreview);
557
- }
558
- return [...changedFiles];
559
- };
560
- const normalizeUnifiedPatch = (patchPreview) => {
561
- const withoutFence = patchPreview.replace(/^```(?:diff|patch)?\s*/i, "").replace(/\s*```$/i, "").trim();
562
- const diffStartIndex = withoutFence.indexOf("diff --git ");
563
- return diffStartIndex >= 0 ? withoutFence.slice(diffStartIndex).trim() : withoutFence;
564
- };
565
- const validatePatchPaths = (patchPreview, validatePath) => {
566
- const changedFiles = getChangedFiles(patchPreview);
567
- if (!changedFiles.length) throw new Error("패치에서 변경 파일을 찾을 수 없습니다.");
568
- changedFiles.forEach(validatePath);
569
- return changedFiles;
570
- };
571
- const checkUnifiedPatch = async (patchPreview, cwd) => {
572
- const tempFilePath = path.join(os.tmpdir(), `dev-copilot-check-${Date.now()}-${Math.random().toString(16).slice(2)}.patch`);
573
- await promises.writeFile(tempFilePath, `${patchPreview.trimEnd()}\n`, "utf-8");
574
- try {
575
- await execFileAsync("git", [
576
- "apply",
577
- "--check",
578
- tempFilePath
579
- ], { cwd });
580
- } catch (error) {
581
- const detail = error instanceof Error && "stderr" in error ? String(error.stderr) : error instanceof Error ? error.message : "알 수 없는 patch 검증 오류";
582
- throw new Error(`적용 가능한 diff를 생성하지 못했습니다: ${detail.trim()}`);
583
- } finally {
584
- await promises.rm(tempFilePath, { force: true });
585
- }
586
- };
587
- const createGitPatch = async (filePath, oldContent, newContent) => {
588
- const tempDir = path.join(os.tmpdir(), `dev-copilot-diff-${Date.now()}-${Math.random().toString(16).slice(2)}`);
589
- const oldFilePath = path.join(tempDir, "old", filePath);
590
- const newFilePath = path.join(tempDir, "new", filePath);
591
- await promises.mkdir(path.dirname(oldFilePath), { recursive: true });
592
- await promises.mkdir(path.dirname(newFilePath), { recursive: true });
593
- await promises.writeFile(oldFilePath, oldContent, "utf-8");
594
- await promises.writeFile(newFilePath, newContent, "utf-8");
595
- try {
596
- const { stdout } = await execFileAsync("git", [
597
- "diff",
598
- "--no-index",
599
- "--no-ext-diff",
600
- "--src-prefix=a/",
601
- "--dst-prefix=b/",
602
- "--",
603
- path.join("old", filePath),
604
- path.join("new", filePath)
605
- ], { cwd: tempDir }).catch((error) => {
606
- const typedError = error;
607
- if (typedError.code === 1 && typedError.stdout) return { stdout: typedError.stdout };
608
- throw error;
609
- });
610
- return stdout.replaceAll(`a/old/${filePath}`, `a/${filePath}`).replaceAll(`b/new/${filePath}`, `b/${filePath}`).trim();
611
- } finally {
612
- await promises.rm(tempDir, {
613
- force: true,
614
- recursive: true
615
- });
616
- }
617
- };
618
- const createPatchFromTextReplacements = async (replacements, cwd, validatePath) => {
619
- const byPath = /* @__PURE__ */ new Map();
620
- for (const replacement of replacements) {
621
- const normalizedPath = validatePath(replacement.path);
622
- const current = byPath.get(normalizedPath) ?? [];
623
- current.push(replacement);
624
- byPath.set(normalizedPath, current);
625
- }
626
- const patches = [];
627
- for (const [filePath, fileReplacements] of byPath.entries()) {
628
- const absolutePath = path.join(cwd, filePath);
629
- const oldContent = await promises.readFile(absolutePath, "utf-8");
630
- let newContent = oldContent;
631
- for (const replacement of fileReplacements) {
632
- if (!newContent.includes(replacement.oldText)) throw new Error(`원문을 파일에서 찾을 수 없습니다: ${filePath}`);
633
- newContent = newContent.replace(replacement.oldText, replacement.newText);
634
- }
635
- if (newContent === oldContent) continue;
636
- patches.push(await createGitPatch(filePath, oldContent, newContent));
637
- }
638
- const patchPreview = patches.join("\n").trim();
639
- if (!patchPreview) throw new Error("변경할 내용이 없습니다.");
640
- return patchPreview;
641
- };
642
- const applyUnifiedPatch = async (patchPreview, cwd, actor) => {
643
- const tempFilePath = path.join(os.tmpdir(), `dev-copilot-${Date.now()}-${Math.random().toString(16).slice(2)}.patch`);
644
- await promises.writeFile(tempFilePath, `${patchPreview.trimEnd()}\n`, "utf-8");
645
- try {
646
- await execFileAsync("git", [
647
- "apply",
648
- "--check",
649
- tempFilePath
650
- ], { cwd });
651
- await execFileAsync("git", ["apply", tempFilePath], { cwd });
652
- return {
653
- actor,
654
- changedFiles: getChangedFiles(patchPreview)
655
- };
656
- } catch (error) {
657
- const detail = error instanceof Error && "stderr" in error ? String(error.stderr) : error instanceof Error ? error.message : "알 수 없는 patch 적용 오류";
658
- throw new Error(`patch 적용에 실패했습니다: ${detail.trim()}`);
659
- } finally {
660
- await promises.rm(tempFilePath, { force: true });
661
- }
662
- };
663
-
664
- //#endregion
665
- //#region src/bridge/lib/store.ts
666
- const patchStore = /* @__PURE__ */ new Map();
667
- const TTL_MS = 1e3 * 60 * 15;
668
- const pruneExpired = () => {
669
- const now = Date.now();
670
- for (const [key, value] of patchStore.entries()) if (now - value.createdAt > TTL_MS) patchStore.delete(key);
671
- };
672
- const createProposedPatch = (patchPreview, allowedPaths) => {
673
- pruneExpired();
674
- const patchId = randomUUID();
675
- const approvalToken = `approve:${patchId}`;
676
- patchStore.set(patchId, {
677
- patchId,
678
- approvalToken,
679
- patchPreview,
680
- allowedPaths,
681
- createdAt: Date.now()
682
- });
683
- return {
684
- patchId,
685
- approvalToken
686
- };
687
- };
688
- const getProposedPatch = (patchId, approvalToken) => {
689
- pruneExpired();
690
- const item = patchStore.get(patchId);
691
- if (!item || item.approvalToken !== approvalToken) return null;
692
- return item;
693
- };
694
- const deleteProposedPatch = (patchId) => {
695
- patchStore.delete(patchId);
696
- };
697
-
698
- //#endregion
699
- //#region src/bridge/server/http-server.ts
700
- const createCorsHeaders = (config) => ({
701
- "Access-Control-Allow-Origin": config.corsOrigin,
702
- "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
703
- "Access-Control-Allow-Headers": "Content-Type",
704
- "Content-Type": "application/json; charset=utf-8"
705
- });
706
- const sendJson = (response, config, statusCode, payload) => {
707
- response.writeHead(statusCode, createCorsHeaders(config));
708
- response.end(JSON.stringify(payload));
709
- };
710
- const readJsonBody = async (request) => {
711
- const chunks = [];
712
- for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
713
- const raw = Buffer.concat(chunks).toString("utf-8");
714
- return JSON.parse(raw);
715
- };
716
- const createProjectContext = async (config, payload, allowedPaths) => {
717
- return buildCopilotProjectContext(config.rootDir, allowedPaths, {
718
- selectedText: payload.selectedText,
719
- route: payload.context?.route,
720
- fileHints: payload.context?.fileHints
721
- });
722
- };
723
- const sanitizeAllowedPaths = (fileHints) => {
724
- if (!fileHints?.length) return null;
725
- const sanitized = fileHints.map((value) => value.trim()).filter(Boolean).filter((value) => value === "." || value === "*" || /^[A-Za-z0-9._-]+$/.test(value));
726
- return sanitized.length ? Array.from(new Set(sanitized)) : null;
727
- };
728
- const toPatchSummary = (changedFiles) => {
729
- return changedFiles.length === 1 ? "1개 파일에 수정이 반영되었습니다." : `${changedFiles.length}개 파일에 수정이 반영되었습니다.`;
730
- };
731
- const toEditPayload = (payload) => {
732
- return payload;
733
- };
734
- const isCopilotAgent = (value) => {
735
- return value === "codex" || value === "claude";
736
- };
737
- const resolveRequestedAgent = (config, requestAgent, queryAgent) => {
738
- if (requestAgent) return requestAgent;
739
- if (isCopilotAgent(queryAgent)) return queryAgent;
740
- return config.agent;
741
- };
742
- const createDevCopilotBridgeServer = (config) => {
743
- return createDevCopilotBridgeServerWithDependencies(config, { resolveAdapter: resolveAgentAdapter });
744
- };
745
- const createDevCopilotBridgeServerWithDependencies = (config, dependencies) => {
746
- return createServer(async (request, response) => {
747
- const method = request.method ?? "GET";
748
- const url = new URL(request.url ?? "/", `http://${config.host}:${config.port}`);
749
- if (method === "OPTIONS") {
750
- response.writeHead(204, createCorsHeaders(config));
751
- response.end();
752
- return;
753
- }
754
- try {
755
- if (method === "GET" && url.pathname === "/status") {
756
- const agent = resolveRequestedAgent(config, void 0, url.searchParams.get("agent"));
757
- sendJson(response, config, 200, await dependencies.resolveAdapter(agent).getStatus(config.rootDir));
758
- return;
759
- }
760
- if (method === "POST" && url.pathname === "/chat") {
761
- const payload = await readJsonBody(request);
762
- const effectiveAllowedPaths = sanitizeAllowedPaths(payload.context?.fileHints) ?? config.allowedPaths;
763
- const agent = resolveRequestedAgent(config, payload.context?.agent);
764
- const adapter = dependencies.resolveAdapter(agent);
765
- if (!payload.prompt?.trim()) {
766
- sendJson(response, config, 400, { error: "프롬프트를 입력해 주세요." });
767
- return;
768
- }
769
- const agentResponse = await adapter.run({
770
- selectedText: payload.selectedText ?? "",
771
- prompt: payload.prompt,
772
- mode: payload.mode,
773
- route: payload.context?.route,
774
- fileHints: payload.context?.fileHints,
775
- previousResponse: payload.context?.previousResponse,
776
- projectContext: await createProjectContext(config, payload, effectiveAllowedPaths),
777
- cwd: config.rootDir
778
- });
779
- if (payload.mode === "answer") {
780
- sendJson(response, config, 200, {
781
- message: agentResponse.message,
782
- warnings: agentResponse.warnings
783
- });
784
- return;
785
- }
786
- const parsed = toEditPayload(agentResponse);
787
- let patchPreview = "";
788
- try {
789
- if (parsed.changes?.length) patchPreview = await createPatchFromTextReplacements(parsed.changes, config.rootDir, (filePath) => resolveAndValidatePath(filePath, effectiveAllowedPaths, config.rootDir));
790
- else if (parsed.patchPreview) patchPreview = normalizeUnifiedPatch(parsed.patchPreview);
791
- } catch (error) {
792
- sendJson(response, config, 200, {
793
- message: parsed.message ?? "에이전트가 수정안을 만들었지만 실제 파일 내용과 매칭하지 못했습니다.",
794
- warnings: [...parsed.warnings ?? [], error instanceof Error ? error.message : "수정안 생성에 실패했습니다."]
795
- });
796
- return;
797
- }
798
- if (!patchPreview) {
799
- sendJson(response, config, 200, {
800
- message: parsed.message ?? "패치 제안을 생성하지 못했습니다.",
801
- warnings: [...parsed.warnings ?? [], "적용 가능한 변경 목록이 없어 패치 미리보기와 적용 버튼을 만들 수 없습니다."]
802
- });
803
- return;
804
- }
805
- try {
806
- validatePatchPaths(patchPreview, (filePath) => resolveAndValidatePath(filePath, effectiveAllowedPaths, config.rootDir));
807
- await checkUnifiedPatch(patchPreview, config.rootDir);
808
- } catch (error) {
809
- sendJson(response, config, 200, {
810
- message: parsed.message ?? "에이전트가 수정안을 만들었지만 적용 가능한 diff 형식이 아닙니다.",
811
- patchPreview,
812
- warnings: [...parsed.warnings ?? [], error instanceof Error ? error.message : "patch 검증에 실패했습니다."]
813
- });
814
- return;
815
- }
816
- const patch = createProposedPatch(patchPreview, effectiveAllowedPaths);
817
- sendJson(response, config, 200, {
818
- message: parsed.message ?? "에이전트가 패치 미리보기를 생성했습니다.",
819
- patchPreview,
820
- patchId: patch.patchId,
821
- warnings: parsed.warnings ?? []
822
- });
823
- return;
824
- }
825
- if (method === "POST" && url.pathname === "/apply") {
826
- const payload = await readJsonBody(request);
827
- if (!payload.patchId || !payload.approvalToken) {
828
- sendJson(response, config, 400, { error: "patchId와 approvalToken이 필요합니다." });
829
- return;
830
- }
831
- const proposedPatch = getProposedPatch(payload.patchId, payload.approvalToken);
832
- if (!proposedPatch) {
833
- sendJson(response, config, 400, { error: "유효하지 않거나 만료된 patchId입니다." });
834
- return;
835
- }
836
- validatePatchPaths(proposedPatch.patchPreview, (filePath) => resolveAndValidatePath(filePath, proposedPatch.allowedPaths?.length ? proposedPatch.allowedPaths : config.allowedPaths, config.rootDir));
837
- const result = await applyUnifiedPatch(proposedPatch.patchPreview, config.rootDir, "local-codex-bridge");
838
- deleteProposedPatch(payload.patchId);
839
- sendJson(response, config, 200, {
840
- applied: true,
841
- changedFiles: result.changedFiles,
842
- summary: toPatchSummary(result.changedFiles)
843
- });
844
- return;
845
- }
846
- sendJson(response, config, 404, { error: "지원하지 않는 경로입니다." });
847
- } catch (error) {
848
- sendJson(response, config, 500, { error: error instanceof Error ? error.message : "브리지 서버 처리 중 오류가 발생했습니다." });
849
- }
850
- });
851
- };
852
-
853
- //#endregion
854
- //#region src/bridge/cli/run-http.ts
855
- const resolveAgent = (value) => {
856
- if (value === "claude") return "claude";
857
- return "codex";
858
- };
859
- const runDevCopilotBridgeCli = async (argv) => {
860
- const portFlagIndex = argv.findIndex((value) => value === "-p");
861
- const portFlagValue = portFlagIndex >= 0 ? argv[portFlagIndex + 1] : void 0;
862
- const positionalPort = argv.find((value) => /^\d+$/.test(value));
863
- const resolvedPort = portFlagValue ? Number(portFlagValue) : positionalPort ? Number(positionalPort) : Number(process.env.DEV_COPILOT_BRIDGE_PORT ?? 3339);
864
- const positionalAgent = argv.find((value) => value === "codex" || value === "claude");
865
- const config = createDevCopilotBridgeConfig({
866
- rootDir: process.cwd(),
867
- host: process.env.DEV_COPILOT_BRIDGE_HOST,
868
- port: Number.isFinite(resolvedPort) ? resolvedPort : 3339,
869
- corsOrigin: process.env.DEV_COPILOT_BRIDGE_CORS_ORIGIN ?? "*",
870
- agent: resolveAgent(positionalAgent),
871
- allowedPaths: (process.env.DEV_COPILOT_ALLOWED_PATHS ?? "app,src,widgets,features,entities,shared,components").split(",").map((value) => value.trim()).filter(Boolean)
872
- });
873
- const server = createDevCopilotBridgeServer(config);
874
- await new Promise((resolve) => {
875
- server.listen(config.port, config.host, () => {
876
- process.stdout.write(`[dev-copilot-bridge] listening on http://${config.host}:${config.port}\n`);
877
- resolve();
878
- });
879
- });
880
- };
881
-
882
- //#endregion
883
4
  //#region src/bin/bridge.ts
884
5
  runDevCopilotBridgeCli(process.argv.slice(2)).catch((error) => {
6
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
885
7
  const message = error instanceof Error ? error.message : String(error);
886
- process.stderr.write(`[dev-copilot-bridge] ${message}\n`);
8
+ const normalized = code === "EADDRINUSE" ? "요청한 브리지 포트가 이미 사용 중입니다. 포트를 변경해서 다시 실행해 주세요." : message;
9
+ process.stderr.write(`[dev-copilot-bridge] ${normalized}\n`);
887
10
  process.exit(1);
888
11
  });
889
12