deel-local-cli 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/slash.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * 슬래시 명령을 이름으로 찾는 규칙 — 대화 화면(commands.js)과 `deel run`(oneshot.js)이 같이 쓴다.
3
+ *
4
+ * 여기가 commands.js 안에 있었다. 그래서 `deel run` 은 이 셋을 쓰려고 대화 화면의 명령 전부
5
+ * (화면 · 설정 · 플러그인 · 스캔 …)를 읽었다. 규칙은 한 벌로 두되, 가벼운 자리로 뺀다 (2.1.3).
6
+ * commands.js 는 여기서 받아 그대로 내보낸다 — 부르던 자리는 안 바뀐다.
7
+ */
8
+ /** `ext:ReviewCode` 의 꼬리 — 언제나 낮춰서 돌려준다. 찾기와 「비슷한 것」 이 같은 잣대를 쓰게 한다. */
9
+ const 꼬리이름 = (이름) => String(이름 ?? '').split(':').pop().toLowerCase();
10
+
11
+ /*
12
+ * ── 이 PC 에서 찾은 슬래시 명령을 이름으로 찾는다 ───────────────────────
13
+ *
14
+ * 대화 화면(commands.js 의 default 갈래)과 배치(oneshot.js)가 **같은 규칙**을 써야 한다. 같은 글자를
15
+ * 쳤는데 창에 따라 되고 안 되면 사람은 어느 쪽을 믿을지 정할 수 없다. 그래서 두 벌로 적지
16
+ * 않고 여기 한 군데에 둔다.
17
+ *
18
+ * 찾는 차례는 셋이다 — 적은 그대로 · 대소문자 무시 · 꼬리 이름.
19
+ * 마지막 칸이 `x.name.split(':').pop() === 낮춘` 이었다 (8회차 그밖 명령2). 왼쪽은 파일에
20
+ * 적힌 그대로고 오른쪽은 낮춘 말이라 둘이 만날 수가 없다. `ext:ReviewCode` 는 `/ReviewCode`
21
+ * 로도 `/reviewcode` 로도 「모르는 명령」 이었고, 「비슷한 것」 에도 안 떴다 — 깔려 있는
22
+ * 명령이 어느 길로도 안 보이는 자리다. 배치에서는 그대로 1 로 선다.
23
+ */
24
+ export function 슬래시명령찾기(목록, 부른이름) {
25
+ const 것들 = 목록 ?? [];
26
+ const 낮춘 = String(부른이름 ?? '').toLowerCase();
27
+ return 것들.find((x) => x.name === 부른이름)
28
+ ?? 것들.find((x) => x.name.toLowerCase() === 낮춘)
29
+ ?? 것들.find((x) => 꼬리이름(x.name) === 낮춘)
30
+ ?? null;
31
+ }
32
+
33
+ /** 못 찾았을 때 「비슷한 것」 으로 늘어놓을 것. 찾기와 같은 잣대(낮춘 꼬리)를 쓴다. */
34
+ export function 비슷한슬래시명령(목록, 부른이름, 몇 = 5) {
35
+ const 낮춘 = String(부른이름 ?? '').toLowerCase();
36
+ if (!낮춘) return [];
37
+ return (목록 ?? [])
38
+ .filter((x) => x.name.toLowerCase().includes(낮춘) || 낮춘.includes(꼬리이름(x.name)))
39
+ .slice(0, 몇);
40
+ }
41
+
42
+ /*
43
+ * 프롬프트 인자를 가른다. 빈칸으로 가르되 따옴표("…" · '…')로 묶은 것은 한 인자다 — 파일 이름에 빈칸이
44
+ * 든다. 마지막 인자는 남은 글을 **그대로** 받는다 — 여러 줄 코드를 넘기면 줄바꿈 · 들여쓰기가 살아야 한다.
45
+ * 남은 것이 통째로 한 따옴표 묶음이면 따옴표만 벗긴다.
46
+ */
47
+ function 인자가르기(글, 몇) {
48
+ const s = String(글 ?? '');
49
+ const 값들 = [];
50
+ let i = 0;
51
+ for (let k = 0; k < 몇; k++) {
52
+ while (i < s.length && /\s/.test(s[i])) i++;
53
+ if (i >= s.length) break;
54
+ if (k === 몇 - 1) {
55
+ const 남은 = s.slice(i).trimEnd();
56
+ const 묶음 = /^(["'])((?:(?!\1)[\s\S])*)\1$/.exec(남은);
57
+ 값들.push(묶음 ? 묶음[2] : 남은);
58
+ break;
59
+ }
60
+ const 끝 = s[i] === '"' || s[i] === "'" ? s.indexOf(s[i], i + 1) : -1;
61
+ if (끝 > i) { 값들.push(s.slice(i + 1, 끝)); i = 끝 + 1; continue; }
62
+ const 시작 = i;
63
+ while (i < s.length && !/\s/.test(s[i])) i++;
64
+ 값들.push(s.slice(시작, i));
65
+ }
66
+ return 값들;
67
+ }
68
+
69
+ /**
70
+ * `/mcp__<서버>__<이름> 인자…` — MCP 서버가 낸 프롬프트를 받아 글로 편다 (2.1.3 · backend/mcp.js 의 프롬프트받기).
71
+ *
72
+ * 대화 화면과 `deel run` 이 같은 것을 쓴다 — 슬래시명령찾기 를 한 벌로 둔 것과 같은 까닭이다.
73
+ * 인자는 빈칸으로 갈라 적힌 차례대로 채우고, 마지막 인자가 나머지를 다 받는다(한 문장을 통째로 넘기게 ·
74
+ * 인자가르기).
75
+ *
76
+ * @returns null — 그런 프롬프트가 없다(부르는 쪽은 여느 슬래시 명령을 찾는다)
77
+ * { 탈 } — 있는데 못 폈다 · { text, 서버이름, 이름 } — 모델에게 보낼 글
78
+ */
79
+ export async function MCP프롬프트펴기(서버들, 부른이름, 인자글 = '', { signal = null } = {}) {
80
+ const { 이름풀기 } = await import('./backend/mcp.js');
81
+ const 갈린것 = 이름풀기(부른이름);
82
+ if (!갈린것) return null;
83
+ const 서버 = (서버들 ?? []).find((s) => s?.이름 === 갈린것.서버);
84
+ const 프롬프트 = (서버?.프롬프트 ?? []).find((p) => p?.name === 갈린것.도구);
85
+ if (!프롬프트) return null;
86
+ const 인자이름들 = (프롬프트.arguments ?? []).map((a) => a.name);
87
+ const 값들 = 인자가르기(인자글, 인자이름들.length);
88
+ const 인자 = {};
89
+ 인자이름들.forEach((n, i) => { if (값들[i]) 인자[n] = 값들[i]; });
90
+ // hasOwn — 인자 이름이 constructor · toString 이면 `in` 은 Object 원형에서 찾아 「있다」 고 한다.
91
+ const 빠진것 = (프롬프트.arguments ?? []).filter((a) => a.required && !Object.hasOwn(인자, a.name)).map((a) => a.name);
92
+ if (빠진것.length) {
93
+ return { 탈: `인자가 모자랍니다 — /${부른이름} ${인자이름들.map((n) => `<${n}>`).join(' ')} (빠진 것: ${빠진것.join(' · ')})` };
94
+ }
95
+ if (!서버.쓸수있나()) return { 탈: `${서버.이름} 서버가 죽었습니다: ${서버.죽음 ?? '이유 모름'}` };
96
+ try {
97
+ const text = await 서버.프롬프트받기(프롬프트.name, 인자, { signal });
98
+ if (!text.trim()) return { 탈: `${서버.이름} 서버가 빈 프롬프트를 줬습니다` };
99
+ return { text, 서버이름: 서버.이름, 이름: 프롬프트.name };
100
+ } catch (e) {
101
+ return { 탈: `프롬프트를 못 받았습니다 — ${String(e?.message ?? e)}` };
102
+ }
103
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 파일끼리 누가 누구를 불러 쓰나 — Outline 이 **많이 불리는 파일부터** 싣게 센다 (2.1.3).
3
+ *
4
+ * ── 왜 이게 필요한가 ───────────────────────────────────────────────────
5
+ *
6
+ * Outline 은 폴더의 뼈대를 최근에 손댄 순서로 싣고 창에 맞춰 뒤를 자른다. 그래서 오래 안 건드린 핵심 모듈 —
7
+ * 파일 수십 개가 불러 쓰는 것 — 이 큰 저장소에서 제일 먼저 잘렸다. 모델은 그 모듈이 있는 줄 모르고, 이미
8
+ * 있는 것을 새로 만든다.
9
+ *
10
+ * ── 무엇을 세나 ────────────────────────────────────────────────────────
11
+ *
12
+ * js · ts `import … from './x'` · `import './x'` · `import('./x')` · `require('./x')` · `export … from './x'`
13
+ * — 점으로 시작하는 것만. 꾸러미 이름(`react`)은 이 폴더의 파일이 아니다.
14
+ * 확장자를 빼고 적은 것 · 폴더(index) · `.js` 로 적고 `.ts` 인 것까지 짚는다.
15
+ * py `from a.b import c` · `from .b import c` · `import a.b` — 모듈 파일과 `__init__.py`.
16
+ *
17
+ * 한 파일이 같은 곳을 여러 번 불러도 한 번이다. 정규식이라 문자열 속 `require('x')` 를 셀 수도 있다 —
18
+ * Outline 머리말과 같은 까닭으로 괜찮다. 여기 셈은 **무엇을 먼저 보여 줄지**에만 쓰인다.
19
+ * 읽는 양에 상한을 둔다. 넘으면 넘은 파일은 안 세고(불린 수 0), 그 사실은 부르는 쪽이 적는다.
20
+ */
21
+ import { readFileSync, statSync } from 'node:fs';
22
+ import { dirname, extname, join, resolve } from 'node:path';
23
+
24
+ export const 셀파일상한 = 3000;
25
+ const 한개상한 = 256 * 1024;
26
+
27
+ const js확장 = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts', '.cts'];
28
+ const js꼴 = new Set(js확장);
29
+ const js무늬 = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)(['"])(\.{1,2}\/[^'"\n]*|\.{1,2})\1/gm;
30
+ const py무늬from = /^\s*from\s+(\.*)([\w.]*)\s+import\s+\(?([^\n#)]*)/gm;
31
+ const py무늬import = /^\s*import\s+([\w.]+(?:\s+as\s+\w+)?(?:\s*,\s*[\w.]+(?:\s+as\s+\w+)?)*)/gm;
32
+
33
+ /**
34
+ * @param {string} 뿌리 작업 폴더 (파이썬 절대 가져오기의 기준)
35
+ * @param {string[]} 파일들 절대 경로. 이 안에서만 짚고 센다.
36
+ * @returns {Map<string, number>} 절대 경로 → 그 파일을 불러 쓰는 **다른 파일** 수. 넘친 것이 있으면 `.넘침` 에 수.
37
+ */
38
+ export function 들여옴세기(뿌리, 파일들, { 상한 = 셀파일상한 } = {}) {
39
+ const 셈 = new Map();
40
+ // 윈도우 · 맥은 대소문자를 안 가린다 — 적힌 글자와 디스크 글자가 달라도 같은 파일로 짚는다.
41
+ const 안가림 = process.platform === 'win32' || process.platform === 'darwin';
42
+ const 열쇠 = (p) => (안가림 ? p.toLowerCase() : p);
43
+ const 있는것 = new Map();
44
+ for (const p of 파일들) 있는것.set(열쇠(resolve(p)), resolve(p));
45
+ const 짚기 = (후보들) => { for (const c of 후보들) { const 찾음 = 있는것.get(열쇠(c)); if (찾음) return 찾음; } return null; };
46
+
47
+ const js짚기 = (파일, 적힌) => {
48
+ const t = resolve(dirname(파일), 적힌);
49
+ const 후보 = [t, ...js확장.map((e) => t + e), ...js확장.map((e) => join(t, `index${e}`))];
50
+ // ts 는 `./x.js` 로 적고 `x.ts` 를 둔다 (NodeNext).
51
+ const e = extname(t);
52
+ if (e === '.js' || e === '.mjs' || e === '.cjs') {
53
+ const 몸 = t.slice(0, -e.length);
54
+ 후보.push(`${몸}.ts`, `${몸}.tsx`, `${몸}${e === '.mjs' ? '.mts' : e === '.cjs' ? '.cts' : '.ts'}`);
55
+ }
56
+ return 짚기(후보);
57
+ };
58
+ const py모듈 = (기준, 이름) => {
59
+ const 몸 = join(기준, ...이름.split('.').filter(Boolean));
60
+ return 짚기([`${몸}.py`, `${몸}.pyi`, join(몸, '__init__.py')]);
61
+ };
62
+
63
+ let 본수 = 0;
64
+ let 넘침 = 0;
65
+ for (const 파일 of 파일들) {
66
+ const e = extname(파일).toLowerCase();
67
+ const js = js꼴.has(e);
68
+ const py = e === '.py' || e === '.pyi';
69
+ if (!js && !py) continue;
70
+ if (본수 >= 상한) { 넘침++; continue; }
71
+ 본수++;
72
+ let 글;
73
+ try {
74
+ if (statSync(파일).size > 한개상한) continue;
75
+ 글 = readFileSync(파일, 'utf8');
76
+ } catch { continue; }
77
+ const 나 = resolve(파일);
78
+ const 부른것 = new Set();
79
+ if (js) {
80
+ for (const m of 글.matchAll(js무늬)) { const t = js짚기(나, m[2]); if (t) 부른것.add(t); }
81
+ } else {
82
+ for (const m of 글.matchAll(py무늬from)) {
83
+ const 점 = m[1].length;
84
+ const 모듈 = m[2];
85
+ let 기준 = resolve(뿌리);
86
+ if (점) { 기준 = dirname(나); for (let i = 1; i < 점; i++) 기준 = dirname(기준); }
87
+ if (모듈) { const t = py모듈(기준, 모듈); if (t) 부른것.add(t); }
88
+ // `from pkg import util` 의 util 이 하위 모듈일 수 있다.
89
+ for (const 이름 of m[3].split(',').map((x) => x.trim().split(/\s+/)[0]).filter((x) => /^\w+$/.test(x))) {
90
+ const t = py모듈(기준, 모듈 ? `${모듈}.${이름}` : 이름);
91
+ if (t) 부른것.add(t);
92
+ }
93
+ }
94
+ for (const m of 글.matchAll(py무늬import)) {
95
+ for (const 조각 of m[1].split(',')) {
96
+ const 이름 = 조각.trim().split(/\s+/)[0];
97
+ const t = py모듈(resolve(뿌리), 이름);
98
+ if (t) 부른것.add(t);
99
+ }
100
+ }
101
+ }
102
+ 부른것.delete(나);
103
+ for (const t of 부른것) 셈.set(t, (셈.get(t) ?? 0) + 1);
104
+ }
105
+ 셈.넘침 = 넘침;
106
+ return 셈;
107
+ }
@@ -18,13 +18,14 @@ import { TODO_TOOL } from './todo.js';
18
18
  import { TASK_TOOL } from './task.js';
19
19
  import { 고를말 } from '../agent/agents.js';
20
20
  import { OUTLINE_TOOL } from './outline.js';
21
- import { VERIFY_TOOL } from './verify.js';
21
+ import { VERIFY_TOOL, 한파일문법 } from './verify.js';
22
22
  import { DEF_TOOL, REFS_TOOL } from './lsp.js';
23
23
  import { 편집후진단, 붙이기 as 진단붙이기, 데우기 } from '../lsp/diag.js';
24
24
  import { 프로젝트갈래 } from '../lsp/servers.js';
25
25
  import { allow as allowedIn } from '../agent/modes.js';
26
26
  import { 도구정의, 이름풀기 } from '../backend/mcp.js';
27
27
  import { 셸환경 } from '../safety/shellenv.js';
28
+ import { 깃앞보기, 깃뒤적기 } from '../safety/gitsnap.js';
28
29
  import { isExcelPath, readExcel, toText as excelText, summarize as excelSummary } from './excel.js';
29
30
  import { isDocPath, readDoc, toText as docText, summarize as docSummary, looksOldHwp, 옛hwp안내, 문서는못고침 } from './docs.js';
30
31
  import { hwpx만들기, 만든말 } from './hwpxwrite.js';
@@ -725,6 +726,12 @@ function 나중에생긴것적기(떠본것, ctx) {
725
726
  for (const abs of 떠본것?.없던채뜬것 ?? []) {
726
727
  try { if (existsSync(abs) && !statSync(abs).isDirectory()) 떠본것.뜬것.push(ctx.scope.show(abs)); } catch { /* 넘어간다 */ }
727
728
  }
729
+ // 낱말로 못 짚은 것 — git 으로 견줘 안 것 (2.1.3 · safety/gitsnap.js). 이번 턴에 이미 뜬 것은 거기서 거른다.
730
+ if (떠본것?.깃앞) {
731
+ const 깃것 = 깃뒤적기(떠본것.깃앞, { history: ctx.history });
732
+ for (const abs of 깃것.뜬것) 떠본것.뜬것.push(ctx.scope.show(abs));
733
+ for (const x of 깃것.못뜬것) 떠본것.못뜬것.push(`${ctx.scope.show(x.abs)} (${x.왜})`);
734
+ }
728
735
  }
729
736
 
730
737
  /** 지금 파일이 몇 줄인가. 붙인 뒤 '얼마나 찼는지' 를 사실로 말해 주려고 센다. */
@@ -3109,6 +3116,12 @@ export const TOOLS = {
3109
3116
  * 그래도 손으로 옮기고 지우는 흔한 자리는 이걸로 덮인다.
3110
3117
  */
3111
3118
  const 떠본것 = 바꾸기전스냅샷(cmd, ctx);
3119
+ /*
3120
+ * git 저장소면 명령 앞 모습도 떠 둔다 (2.1.3 · safety/gitsnap.js). 낱말로는 못 짚는 자리 — 와일드카드 ·
3121
+ * 스크립트 · 포매터가 바꾼 파일 — 를 명령 뒤에 견줘 되돌리기에 올린다. 뒤에서 도는 명령은 언제 끝날지
3122
+ * 몰라 견줄 자리가 없으니 안 뜬다.
3123
+ */
3124
+ if (args.background !== true && ctx.history?.뒤늦은기록) 떠본것.깃앞 = 깃앞보기(ctx.scope.root);
3112
3125
  const 뜬것 = 떠본것.뜬것;
3113
3126
  /*
3114
3127
  * 못 뜬 것과 상한에 걸린 것은 **결과에 실어 보낸다.**
@@ -4118,6 +4131,16 @@ async function 고친뒤진단(name, r, ctx) {
4118
4131
  데우기(뿌리, 볼것[0]);
4119
4132
 
4120
4133
  const 것들 = await Promise.all(볼것.map((abs) => 편집후진단(뿌리, abs)));
4134
+ /*
4135
+ * 언어 서버가 말을 못 줬으면(없거나 · 아직 데우는 중) 문법만이라도 본다 (2.1.3 · verify.js 의 한파일문법).
4136
+ * 서버가 답했으면 안 본다 — 오류 0개도 답이다. 두 번 보면 같은 탈을 두 줄로 적는다.
4137
+ */
4138
+ if (것들.some((d) => d === null)) {
4139
+ const env = 셸환경(process.env, { 남길것: ctx.셸남길것 ?? [] }).env;
4140
+ await Promise.all(볼것.map(async (abs, i) => {
4141
+ if (것들[i] === null) 것들[i] = await 한파일문법(abs, { 뿌리, env });
4142
+ }));
4143
+ }
4121
4144
  let 답 = r;
4122
4145
  for (let i = 0; i < 볼것.length; i++) {
4123
4146
  답 = 진단붙이기(답, 것들[i], ctx.scope.show(볼것[i]));
@@ -35,6 +35,7 @@ import { walk, SKIP_DIRS, globToRegex, 내부살림 } from './fsutil.js';
35
35
  import { 건너뜀말 } from './ignore.js';
36
36
  import { decode, looksBinary } from './encoding.js';
37
37
  import { 찾을개수, 뼈대줄수 } from '../agent/budget.js';
38
+ import { 들여옴세기 } from './importgraph.js';
38
39
  import { 말, 세말 } from '../i18n/index.js';
39
40
 
40
41
  /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
@@ -289,10 +290,26 @@ export const OUTLINE_TOOL = {
289
290
 
290
291
  if (!파일들.length) return { content: '볼 파일이 없습니다.', summary: 세말('count', 0) };
291
292
 
292
- // 최근에 손댄 것부터. 지금 하는 일과 가까울 가능성이 높다.
293
+ const 파일상한 = 찾을개수(ctx.모델컨텍스트);
294
+ /*
295
+ * ── 싣는 차례 (2.1.3) ────────────────────────────────────────────────
296
+ *
297
+ * 여태는 최근에 손댄 것부터였다. 그러면 오래 안 건드린 **핵심 모듈** — 파일 수십 개가 불러 쓰는 것 —
298
+ * 이 큰 저장소에서 제일 먼저 잘렸다(tools/importgraph.js 머리말). 이제 둘을 섞는다.
299
+ *
300
+ * 1. 지금 손대는 파일 — 30분 안에 바뀐 것, 몇 자리만. 지금 하는 일과 제일 가깝다.
301
+ * 다만 폴더의 절반 넘게가 30분 안이면 막 받아 온(clone) 저장소라 시각이 뜻이 없다 — 이 자리를 안 둔다.
302
+ * 2. 나머지는 많이 불리는 것부터, 같으면 최근 것부터.
303
+ */
304
+ const 셈 = 하나인가 ? new Map() : 들여옴세기(ctx.scope.root, 파일들.map((f) => f.path));
305
+ const 불림 = (f) => 셈.get(f.path) ?? 0;
293
306
  파일들.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
307
+ const 제일늦은 = 파일들[0]?.mtime ?? 0;
308
+ const 지금것 = 파일들.filter((f) => (f.mtime ?? 0) >= 제일늦은 - 30 * 60 * 1000);
309
+ const 앞자리 = 지금것.length * 2 > 파일들.length ? [] : 지금것.slice(0, Math.max(2, Math.floor(파일상한 / 8)));
310
+ const 앞 = new Set(앞자리);
311
+ 파일들 = [...앞자리, ...파일들.filter((f) => !앞.has(f)).sort((a, b) => 불림(b) - 불림(a) || (b.mtime ?? 0) - (a.mtime ?? 0))];
294
312
 
295
- const 파일상한 = 찾을개수(ctx.모델컨텍스트);
296
313
  const 줄상한 = 뼈대줄수(ctx.모델컨텍스트);
297
314
  const 볼것 = 파일들.slice(0, 파일상한);
298
315
 
@@ -328,7 +345,8 @@ export const OUTLINE_TOOL = {
328
345
  }
329
346
 
330
347
  뼈대있는파일++;
331
- 줄들.push(`${보인이름} (${줄수}줄)`);
348
+ // 몇 곳에서 불러 쓰나 — 모델이 「여기가 중심이구나」 를 이름만 보고 안다 (2.1.3).
349
+ 줄들.push(`${보인이름} (${줄수}줄${불림(f) ? ` · ${불림(f)}곳에서 불러 씀` : ''})`);
332
350
  쓴줄++;
333
351
  // 파일 하나가 목록을 통째로 먹지 않게 한다. 500개짜리 파일 하나가
334
352
  // 나머지 마흔 개를 밀어내면 '구조를 본다' 는 뜻이 없어진다.
@@ -31,12 +31,13 @@
31
31
  import { existsSync, statSync, readFileSync } from 'node:fs';
32
32
  import { execFile } from 'node:child_process';
33
33
  import { tmpdir } from 'node:os';
34
- import { extname, dirname, resolve, join, delimiter, isAbsolute, relative, basename, sep } from 'node:path';
34
+ import { extname, dirname, resolve, join, isAbsolute, relative, basename, sep } from 'node:path';
35
35
  import { walk, SKIP_DIRS, 내부살림 } from './fsutil.js';
36
36
  import { 건너뜀말 } from './ignore.js';
37
37
  import { 확인법들 } from './checkmethods.js';
38
38
  import { decode, looksBinary } from './encoding.js';
39
39
  import { 셸환경 } from '../safety/shellenv.js';
40
+ import { 경로에서찾기 } from '../safety/which.js';
40
41
  import { 말 } from '../i18n/index.js';
41
42
 
42
43
  /* 결과 한 줄을 잇는다 — 빈 조각은 버린다(tools/index.js 의 이어 와 같은 것). */
@@ -271,9 +272,9 @@ export function json보기(글) {
271
272
  * 안 그러면 `-m py_compile` 이 작업 폴더를 sys.path 앞에 넣어 저장소의 `py_compile.py` 를
272
273
  * 가져와 돌린다(같은 구멍의 파이썬 쪽).
273
274
  */
274
- function 명령돌리기(파일, 인자, { cwd, 제한 = 60000, env } = {}) {
275
+ function 명령돌리기(파일, 인자, { cwd, 제한 = 60000, env, 입력 = null } = {}) {
275
276
  return new Promise((끝) => {
276
- execFile(파일, 인자, {
277
+ const 아이 = execFile(파일, 인자, {
277
278
  cwd, env, timeout: 제한, maxBuffer: 4 * 1024 * 1024,
278
279
  windowsHide: true, encoding: 'buffer',
279
280
  }, (err, so, se) => {
@@ -283,38 +284,13 @@ function 명령돌리기(파일, 인자, { cwd, 제한 = 60000, env } = {}) {
283
284
  const code = 시그널 ? null : (err?.code ?? 0);
284
285
  끝({ ok: !시그널 && code === 0, code, 시그널, out, 없음: err?.code === 'ENOENT' });
285
286
  });
287
+ // 표준입력으로 줄 글 (아래 ESM 다시 보기). 안 주면 바로 닫는다 — 열어 두면 읽는 쪽이 기다린다.
288
+ 아이.stdin?.on('error', () => {});
289
+ 아이.stdin?.end(입력 ?? undefined);
286
290
  });
287
291
  }
288
292
 
289
- /**
290
- * 이름으로 실행 파일을 PATH 에서 찾는다 — 셸이 찾게 두지 않는다 (위 머리말).
291
- *
292
- * 지금 폴더를 뜻하는 칸(빈 칸·`.`·상대 경로)과 작업 폴더 안의 칸은 건너뛴다. 그 자리의
293
- * 파일은 저장소가 심을 수 있다. 윈도우에서는 `.exe` 만 본다 — `.cmd`·`.bat` 은 결국
294
- * cmd.exe 를 부르는 것이라 셸을 안 거친다는 뜻이 없어진다.
295
- *
296
- * @returns {string|null} 절대 경로. 못 찾으면 null — 그러면 「도구가 없다」 로 말한다.
297
- */
298
- function 경로에서찾기(이름들, { env = process.env, 뿌리 = null } = {}) {
299
- const 윈 = process.platform === 'win32';
300
- // 윈도우 환경 이름은 대소문자를 안 가린다(Path · PATH). 거른 환경 객체는 평범한 객체라 직접 찾는다.
301
- const 칸 = Object.keys(env ?? {}).find((k) => (윈 ? k.toUpperCase() === 'PATH' : k === 'PATH'));
302
- const 목록 = String((칸 && env[칸]) ?? '').split(delimiter);
303
- const 뿌리abs = 뿌리 ? resolve(뿌리) : null;
304
- for (const 이름 of 이름들) {
305
- for (const 날것 of 목록) {
306
- const 폴더 = 날것.trim().replace(/^"(.*)"$/, '$1');
307
- if (!폴더 || !isAbsolute(폴더)) continue;
308
- if (뿌리abs) {
309
- const rel = relative(뿌리abs, resolve(폴더));
310
- if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) continue;
311
- }
312
- const 후보 = join(폴더, 윈 ? `${이름}.exe` : 이름);
313
- try { if (statSync(후보).isFile()) return 후보; } catch { /* 여기엔 없다 */ }
314
- }
315
- }
316
- return null;
317
- }
293
+ // 이름으로 실행 파일을 PATH 에서 찾는 자는 safety/which.js 로 옮겼다 (2.1.3) — 되돌리기의 git 뜨기도 같은 자를 쓴다.
318
294
 
319
295
  /**
320
296
  * 폴더를 통째로 볼 때 한 번에 보는 파일 수.
@@ -341,7 +317,8 @@ const 볼만한것인가 = (p) => 볼만한확장.has(extname(p).toLowerCase());
341
317
  function 돌릴명령(확장, { 뿌리 = null, env = process.env } = {}) {
342
318
  switch (확장) {
343
319
  case '.js': case '.mjs': case '.cjs':
344
- return { 파일: process.execPath, 인자: (p) => ['--check', p], 어떻게: 'node --check' };
320
+ // `.js` 만 모듈로 한 번 더 본다 — .mjs · .cjs 는 node 가 확장자로 정해 제대로 본다 (돌려보기 머리말).
321
+ return { 파일: process.execPath, 인자: (p) => ['--check', p], 어떻게: 'node --check', 모듈도볼까: 확장 === '.js' };
345
322
  case '.py':
346
323
  return {
347
324
  파일: 경로에서찾기(process.platform === 'win32' ? ['python', 'python3'] : ['python3', 'python'], { env, 뿌리 }),
@@ -363,6 +340,95 @@ const 짧게 = (s, n = 300) => {
363
340
  return t.length > n ? `${t.slice(0, n)}…` : t;
364
341
  };
365
342
 
343
+ /**
344
+ * 고른 명령으로 파일 하나를 돌려 본다. Verify 와 고친 직후 문법 검사(한파일문법)가 같이 쓴다.
345
+ *
346
+ * @returns {{ok:true} | {탈:string} | {못함:true}} 못함 — 이 컴퓨터에 그 도구가 없다
347
+ */
348
+ async function 돌려보기(만들기, abs, { 뿌리, env, 제한 }) {
349
+ const r = await 명령돌리기(만들기.파일, 만들기.인자(abs), { cwd: 뿌리, 제한, env });
350
+ // 도구 자체가 이 컴퓨터에 없으면 '틀렸다' 가 아니라 '못 봤다' 이다.
351
+ // 파이썬이 안 깔린 PC 에서 py 파일을 전부 빨갛게 칠하면 아무도 안 믿는다.
352
+ // (윈도우 스토어 자리표시 python.exe 는 「was not found」 를, 옛 python2 는 -I 에 「Unknown option」 을 낸다.)
353
+ // 그 말들은 **이 파일 이름이 안 나오는** 글에서만 믿는다 (2.0.0 6회차 검증6). 문법 오류 글에는 파일
354
+ // 경로와 원문 한 줄이 따라 나오는데, 원문에 `was not found` 가 있으면 깨진 파일이 「도구가 없다」 로 넘어갔다.
355
+ const 도구없다는말 = !r.out.includes(basename(abs))
356
+ && /not recognized|command not found|찾을 수 없|No such file|was not found|Unknown option/i.test(r.out);
357
+ if (!r.ok && (r.없음 || 도구없다는말)) return { 못함: true };
358
+ if (!r.ok) return { 탈: 짧게(r.out) || `종료코드 ${r.code}`, 끊김: !!r.시그널 };
359
+ /*
360
+ * ── `.js` 인데 import · export 가 있으면 모듈로 한 번 더 본다 (2.1.3) ─────────────
361
+ *
362
+ * package.json 에 "type" 이 없는 자리의 `.js` 는 node 가 CommonJS 로 읽어 보다가 ESM 꼴이면 모듈로
363
+ * 넘기는데(모듈 꼴 알아보기), `--check` 는 그 넘기는 자리에서 **괄호가 안 닫혀 있어도 0 으로 끝난다.**
364
+ * 재 보니 `export const a = 1;\nfunction f() {` 가 node 24 에서 초록이었다 — Verify 가 깨진 파일에
365
+ * 「✓ node --check」 를 적어 왔다. 표준입력으로 `--input-type=module` 을 주면 제대로 본다.
366
+ * 동적 `import(` · `import.meta` 는 CommonJS 에도 있어 안 센다 — 정적 import · export 꼴만.
367
+ *
368
+ * 꼴 찾기는 넉넉히 한다(줄 가운데 `; export { a }` · `export /* … *\/` 도). 넉넉해서 CommonJS 파일이 걸려도
369
+ * 탈을 붙이지 않는다 — 모듈로 틀렸으면 CommonJS 로 한 번 더 보고, 그쪽으로 맞으면 CommonJS 파일이다
370
+ * (틀 문자열 · 주석 속 import 줄. `const package = …` 는 모듈에서만 틀린다). 2차 눈이 짚은 셋이다.
371
+ */
372
+ if (만들기.모듈도볼까) {
373
+ let 글 = '';
374
+ try { 글 = decode(readFileSync(abs)).text; } catch { return { ok: true }; }
375
+ if (/\bexport(?:\s*[{*]|\s*\/\*|\s+(?:default|const|let|var|function|async|class)\b)|\bimport(?:\s*[{*'"]|\s*\/\*|\s+[\w$]+\s*(?:,|from\b))/.test(글)) {
376
+ const 넣어보기 = (꼴) => 명령돌리기(만들기.파일, [`--input-type=${꼴}`, '--check'], { cwd: 뿌리, 제한, env, 입력: 글 });
377
+ const m = await 넣어보기('module');
378
+ // 시간이 다 돼 끊긴 것은 본 것이 아니다 — 첫 검사가 끊겼을 때와 같이 적는다.
379
+ if (m.시그널) return { 탈: `모듈 검사가 ${Math.round(제한 / 1000)}초 안에 안 끝났습니다`, 끊김: true };
380
+ if (!m.ok && !m.없음 && !(await 넣어보기('commonjs')).ok) {
381
+ // 되받이 글($& 따위)로 읽히지 않게 함수로 바꾼다 — 경로에 $& 가 있으면 경로가 깨져 탈을 놓쳤다.
382
+ return { 탈: 짧게(m.out.replaceAll('[stdin]', () => abs)) || `종료코드 ${m.code}` };
383
+ }
384
+ }
385
+ }
386
+ return { ok: true };
387
+ }
388
+
389
+ /*
390
+ * ── 고친 직후 파일 하나의 문법 (2.1.3) ────────────────────────────────
391
+ *
392
+ * 고친 뒤 진단(lsp/diag.js)은 **이미 떠 있는 언어 서버**가 있을 때만 말한다. 로컬 모델을 쓰는 PC 대부분은
393
+ * 언어 서버가 없고, 있어도 첫 파일은 못 받는다. 그 자리에서 괄호 하나가 빠지면 아무 말이 없었다 — 모델은
394
+ * 고친 것으로 치고 다음 파일로 가고, 탈은 몇 걸음 뒤 돌려 볼 때에야 나왔다.
395
+ *
396
+ * 그래서 언어 서버가 말을 못 줬을 때 **싸게 볼 수 있는 것만** 본다. Verify 와 같은 명령 · 같은 거른 환경 ·
397
+ * 같은 셸 없는 길이다(명령돌리기 머리말). HTML 은 안 본다 — 빠진 참조를 찾느라 폴더를 뒤지고, 나눠 쓰는
398
+ * 중간(아직 안 쓴 짝)을 탈로 읽기 쉽다. 그건 일을 다 한 뒤 Verify 의 몫이다.
399
+ *
400
+ * @returns {Promise<{오류:number, 경고:number, 글:string, 출처:string}|null>} 못 봤으면 null — 「성하다」 가 아니다
401
+ */
402
+ export async function 한파일문법(abs, { 뿌리 = null, env = process.env, 제한 = 8000 } = {}) {
403
+ try {
404
+ const 확장 = extname(abs).toLowerCase();
405
+ const 탈하나 = (탈, 출처) => ({ 오류: 1, 경고: 0, 글: String(탈).split('\n').slice(0, 6).map((l) => ` ${l}`).join('\n'), 출처 });
406
+ const 읽는것 = { '.json': ['JSON 파싱', json보기], '.css': ['중괄호 짝', css보기], '.scss': ['중괄호 짝', css보기], '.less': ['중괄호 짝', css보기] }[확장];
407
+ if (읽는것) {
408
+ let buf;
409
+ try { buf = readFileSync(abs); } catch { return null; }
410
+ if (looksBinary(buf)) return null;
411
+ const 탈 = 읽는것[1](decode(buf).text);
412
+ return 탈.length ? 탈하나(탈.join('\n'), 읽는것[0]) : { 오류: 0, 경고: 0, 글: '', 출처: 읽는것[0] };
413
+ }
414
+ const 만들기 = 돌릴명령(확장, { 뿌리, env });
415
+ if (!만들기?.파일 || !existsSync(abs)) return null;
416
+ const r = await 돌려보기(만들기, abs, { 뿌리: 뿌리 ?? dirname(abs), env, 제한 });
417
+ // 없는 도구 · 시간이 다 돼 끊긴 것은 「틀렸다」 가 아니라 못 본 것이다. 고칠 때마다 붙는 자리라 짧게 기다린다.
418
+ if (r.못함 || r.끊김) return null;
419
+ /*
420
+ * 탈 글에 **이 파일 이름이 없으면** 이 파일의 문법 탈이 아니다 — 곁의 package.json 이 깨졌으면 node 는
421
+ * 그것을 읽다 넘어진 글을 낸다. 그걸 이 파일에 붙이면 멀쩡한 파일을 고치러 간다. 못 본 것으로 둔다.
422
+ */
423
+ // node 20 은 그 글에 「… while importing <이 파일>」 로 이 파일 이름까지 적는다 — 이름으로만 가르면 남의 탈이
424
+ // 붙는다(리눅스 CI 가 잡음). 꾸러미 설정 탈은 node 의 오류 번호로 먼저 가른다.
425
+ if (!r.ok && (/ERR_INVALID_PACKAGE_CONFIG|Invalid package config/.test(r.탈) || !r.탈.includes(basename(abs)))) return null;
426
+ return r.ok ? { 오류: 0, 경고: 0, 글: '', 출처: 만들기.어떻게 } : 탈하나(r.탈, 만들기.어떻게);
427
+ } catch {
428
+ return null; // 보다가 터져서 편집이 실패로 보이는 일은 없어야 한다
429
+ }
430
+ }
431
+
366
432
  export const VERIFY_TOOL = {
367
433
  schema: {
368
434
  name: 'Verify',
@@ -496,18 +562,10 @@ export const VERIFY_TOOL = {
496
562
  const 만들기 = 고른명령[확장];
497
563
  if (만들기) {
498
564
  if (!만들기.파일) { 못한것.push({ 이름, 왜: `${확장} 을 확인할 도구가 이 컴퓨터에 없습니다` }); continue; }
499
- const r = await 명령돌리기(만들기.파일, 만들기.인자(abs), { cwd: 뿌리, 제한: 30000, env: 자식환경 });
500
- // 도구 자체가 이 컴퓨터에 없으면 '틀렸다' 가 아니라 '못 봤다' 이다.
501
- // 파이썬이 안 깔린 PC 에서 py 파일을 전부 빨갛게 칠하면 아무도 안 믿는다.
502
- // (윈도우 스토어 자리표시 python.exe 는 「was not found」 를, 옛 python2 는 -I 에 「Unknown option」 을 낸다.)
503
- // 그 말들은 **이 파일 이름이 안 나오는** 글에서만 믿는다 (2.0.0 6회차 검증6). 문법 오류 글에는 파일
504
- // 경로와 원문 한 줄이 따라 나오는데, 원문에 `was not found` 가 있으면 깨진 파일이 「도구가 없다」 로 넘어갔다.
505
- const 도구없다는말 = !r.out.includes(basename(abs))
506
- && /not recognized|command not found|찾을 수 없|No such file|was not found|Unknown option/i.test(r.out);
507
- if (!r.ok && (r.없음 || 도구없다는말)) {
508
- 못한것.push({ 이름, 왜: `${확장} 을 확인할 도구가 이 컴퓨터에 없습니다` });
509
- } else if (r.ok) 된것.push({ 이름, 어떻게: 만들기.어떻게 });
510
- else 탈난것.push({ 이름, 탈: [짧게(r.out) || `종료코드 ${r.code}`] });
565
+ const r = await 돌려보기(만들기, abs, { 뿌리, env: 자식환경, 제한: 30000 });
566
+ if (r.못함) 못한것.push({ 이름, 왜: `${확장} 을 확인할 도구가 이 컴퓨터에 없습니다` });
567
+ else if (r.ok) 된것.push({ 이름, 어떻게: 만들기.어떻게 });
568
+ else 탈난것.push({ 이름, 탈: [r.탈] });
511
569
  continue;
512
570
  }
513
571
 
package/src/ui/banner.js CHANGED
@@ -39,7 +39,14 @@
39
39
  * 조각으로 남는다. 여기 쓰는 것들은 입력 상자가 이미 쓰고 있는 것이라
40
40
  * 이 프로그램이 도는 자리에서는 이미 한 번 검증된 셈이다.
41
41
  */
42
- import { c, 번호색, 색256가능, width } from './ansi.js';
42
+ import { c, say, 번호색, 색256가능, width } from './ansi.js';
43
+
44
+ /** 도움말·설정·진단 맨 위의 한 줄. */
45
+ export function banner() {
46
+ say('');
47
+ say(` ${c.cyan('deel')} ${c.gray('— 로컬 모델 코딩 에이전트')}`);
48
+ say('');
49
+ }
43
50
 
44
51
  /*
45
52
  * 글꼴. 한 글자가 일곱 줄이다.