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