deel-local-cli 1.20.13 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +97 -78
- package/README.md +120 -81
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +23 -3
- package/src/acp/serve.js +333 -25
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +253 -26
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +299 -0
- package/src/commands/model.js +851 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +454 -41
- package/src/configexplain.js +127 -15
- package/src/doctor.js +164 -27
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +531 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +36 -4
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +501 -38
- package/src/tools/fig.js +21 -4
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1083 -148
- package/src/tools/jobs.js +328 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +367 -59
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +328 -45
- package/src/tools/webfetch.js +194 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/tools/shell.js
CHANGED
|
@@ -29,9 +29,16 @@ export function 배시후보(env = process.env, platform = process.platform) {
|
|
|
29
29
|
}
|
|
30
30
|
if (env.LOCALAPPDATA) 후보.push(P.join(env.LOCALAPPDATA, 'Programs', 'Git', 'bin', 'bash.exe'));
|
|
31
31
|
const 구분 = platform === 'win32' ? ';' : ':';
|
|
32
|
-
for (const
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
for (const 칸 of String(env.PATH ?? env.Path ?? '').split(구분)) {
|
|
33
|
+
// PATH 칸은 따옴표로 쌀 수 있다(cmd 가 그렇게 읽는다). 안 벗기면 `.\"C:\…\bin"\bash.exe` 가 되어 Git Bash 를
|
|
34
|
+
// 놓쳤고, 싼 System32 는 아래 거르기도 비켰다 (6회차 Gemini 셸고르기6).
|
|
35
|
+
const dir = 칸.trim().replace(/^"(.*)"$/, '$1').trim();
|
|
36
|
+
// 상대 칸(`.` · `bin` · 안 풀린 `%CD%`)은 **지금 폴더**, 곧 받아 온 저장소를 가리킨다.
|
|
37
|
+
// 남겨 두면 후보가 `bash.exe` 라는 맨 이름이 되고, existsSync 가 그것을 작업 폴더에서
|
|
38
|
+
// 찾아 준다 — 저장소가 심어 둔 bash.exe 가 Bash 도구와 Jobs 의 모든 명령을 받는다.
|
|
39
|
+
// tools/verify.js 의 경로에서찾기 가 같은 위협을 같은 자로 이미 막고 있다.
|
|
40
|
+
if (!dir || !P.isAbsolute(dir) || /[\\/]system32[\\/]?$/i.test(dir)) continue;
|
|
41
|
+
후보.push(P.join(dir, 'bash.exe'));
|
|
35
42
|
}
|
|
36
43
|
return [...new Set(후보)];
|
|
37
44
|
}
|
|
@@ -73,7 +80,8 @@ export function 셸고르기({ env = process.env, platform = process.platform, c
|
|
|
73
80
|
* Node 의 exec() 가 안에서 하는 것과 똑같이 맞춘다. /s 는 그 감싼 따옴표 한 쌍을
|
|
74
81
|
* 벗기라는 뜻이라 짝이 맞는다.
|
|
75
82
|
*/
|
|
76
|
-
|
|
83
|
+
// 빈 COMSPEC 은 안 정한 것과 같다 — `??` 는 빈 글을 지나보내 명령마다 ENOENT 였다 (6회차 Gemini 셸고르기6).
|
|
84
|
+
const file = env.COMSPEC || 'cmd.exe';
|
|
77
85
|
return { id: 'cmd', file, 명령: (cmd) => ['/d', '/s', '/c', `"${cmd}"`], verbatim: true, 표시: `cmd.exe · ${file}`, 경고 };
|
|
78
86
|
}
|
|
79
87
|
|
package/src/tools/spawn.js
CHANGED
|
@@ -39,6 +39,15 @@ import { spawn } from 'node:child_process';
|
|
|
39
39
|
*/
|
|
40
40
|
export function 돌려보기(이름, 인자, {
|
|
41
41
|
timeout = 20000, maxBuffer = 32 * 1024 * 1024, signal = null, 넣을것 = null, 덤 = {},
|
|
42
|
+
/*
|
|
43
|
+
* 넘치면 **자르고 계속 기다린다** — 죽이지 않는다.
|
|
44
|
+
*
|
|
45
|
+
* 기본은 spawnSync 처럼 넘치면 죽이고 실패로 돌려준다(아래 머리말). 그런데 훅은 그러면
|
|
46
|
+
* 안 된다: 0 으로 끝날 훅이 경고를 16KB 넘게 뱉는 순간 「못 돌렸다」 가 되고, 막는 자리는
|
|
47
|
+
* 못 돌린 훅을 막힘으로 친다(safety/hooks.js). 끝난 모양(종료코드)이 판정인 부르개는
|
|
48
|
+
* 이걸 켜고, 잘랐는지는 `잘림` 으로 받는다.
|
|
49
|
+
*/
|
|
50
|
+
넘치면자르기 = false,
|
|
42
51
|
} = {}) {
|
|
43
52
|
return new Promise((resolve) => {
|
|
44
53
|
// 이미 눌렸으면 띄우지도 않는다. 여럿을 함께 돌릴 때 뒤엣것이 여기로 온다.
|
|
@@ -52,7 +61,9 @@ export function 돌려보기(이름, 인자, {
|
|
|
52
61
|
return;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
let 밖 = ''; let 탈 = ''; let 끝났나 = false;
|
|
64
|
+
let 밖 = ''; let 탈 = ''; let 끝났나 = false; let 잘림 = false; let 탈잘림 = false;
|
|
65
|
+
/** 탈 글을 담아 두는 한도. 넘으면 자르되 자른 자리를 글에 적는다. */
|
|
66
|
+
const 탈상한 = 65536;
|
|
56
67
|
const 끝내기 = (것) => {
|
|
57
68
|
if (끝났나) return;
|
|
58
69
|
끝났나 = true;
|
|
@@ -67,10 +78,15 @@ export function 돌려보기(이름, 인자, {
|
|
|
67
78
|
* 여기가 없으면 반쪽짜리다. 비동기로 바꾼 덕에 ESC 는 들리지만, 정작
|
|
68
79
|
* soffice 는 남은 시간을 마저 돈다 — 사람 눈에는 여전히 "눌렀는데 안
|
|
69
80
|
* 멈춘다". 듣는 것과 멈추는 것은 다른 일이라, 둘 다 해야 한 가지가 끝난다.
|
|
81
|
+
*
|
|
82
|
+
* **여태 받은 것은 버리지 않는다.** 바로 위 시한 갈래는 `stdout: 밖` 으로
|
|
83
|
+
* 실어 주는데 여기만 빈 글이었다. 훅(safety/hooks.js)은 못 돌린 판에도
|
|
84
|
+
* 자식이 뱉은 말을 그대로 보여 주는 자리라, 중단한 판에서만 「어디까지
|
|
85
|
+
* 하다 멈췄는지」 가 통째로 사라졌다. 멈춘 것과 아무 말도 없던 것은 다르다.
|
|
70
86
|
*/
|
|
71
87
|
const 중단하기 = () => {
|
|
72
88
|
try { 아이.kill(); } catch { /* 이미 죽었다 */ }
|
|
73
|
-
끝내기({ error: new Error('중단했습니다'), status: null, stdout:
|
|
89
|
+
끝내기({ error: new Error('중단했습니다'), status: null, stdout: 밖, stderr: 탈 });
|
|
74
90
|
};
|
|
75
91
|
signal?.addEventListener?.('abort', 중단하기, { once: true });
|
|
76
92
|
|
|
@@ -91,28 +107,56 @@ export function 돌려보기(이름, 인자, {
|
|
|
91
107
|
* 자식은 제 할 말을 이미 했고, 그 결과는 status 로 온다. 여기서 던지면
|
|
92
108
|
* 프로그램이 통째로 죽으므로 삼킨다.
|
|
93
109
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
110
|
+
아이.stdin?.on('error', () => { /* 자식이 먼저 닫았다 — 결과는 status 로 온다 */ });
|
|
111
|
+
// 넣을 것이 없어도 **닫는다.** 안 닫으면 위 머리말 그대로 — rg 든 훅이든
|
|
112
|
+
// stdin 을 끝까지 읽는 자식은 시한에 걸려 죽을 때까지 기다린다.
|
|
113
|
+
try { 아이.stdin?.end(넣을것 == null ? undefined : String(넣을것)); } catch { /* 이미 닫혔다 */ }
|
|
98
114
|
|
|
99
115
|
아이.stdout?.setEncoding('utf8');
|
|
100
116
|
아이.stderr?.setEncoding('utf8');
|
|
101
117
|
아이.stdout?.on('data', (조각) => {
|
|
118
|
+
// 이미 잘랐으면 흘려 버리기만 한다. 안 읽으면 자식이 가득 찬 파이프에 막혀 안 끝난다.
|
|
119
|
+
if (잘림) return;
|
|
102
120
|
밖 += 조각;
|
|
103
121
|
/*
|
|
104
122
|
* spawnSync 는 이 자리에서 ENOBUFS 를 냈다. 조용히 잘라 버리면 사람
|
|
105
123
|
* 눈에는 '결과가 적다' 로만 보인다 — 오류도 안 나고 아무도 눈치 못 챈다.
|
|
106
124
|
* 같은 자리에서 같은 실패를 내야 부르는 쪽이 예전 길로 내려간다.
|
|
125
|
+
* (넘치면자르기 를 켠 부르개는 자르되 `잘림` 으로 **말한다** — 조용히가 아니다.)
|
|
107
126
|
*/
|
|
127
|
+
if (밖.length > maxBuffer && 넘치면자르기) { 밖 = 밖.slice(0, maxBuffer); 잘림 = true; return; }
|
|
108
128
|
if (밖.length > maxBuffer) {
|
|
109
129
|
try { 아이.kill(); } catch { /* 이미 죽었다 */ }
|
|
110
|
-
|
|
130
|
+
// `code` 를 단다 — 이 탈에는 그게 없어서 부르는 쪽이 우리말 한 줄을 글자로 맞춰 보는
|
|
131
|
+
// 수밖에 없었다. 말이 바뀌면 조용히 안 걸린다. spawnSync 가 내던 그 코드를 그대로 쓴다.
|
|
132
|
+
끝내기({
|
|
133
|
+
error: Object.assign(new Error('결과가 너무 많습니다'), { code: 'ENOBUFS', killed: true }),
|
|
134
|
+
status: null, stdout: '', stderr: 탈,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
/*
|
|
139
|
+
* 탈 글도 자른다 — 다만 **자른 자리를 글 안에 적어 둔다.**
|
|
140
|
+
*
|
|
141
|
+
* 여태 `if (탈.length < 65536) 탈 += 조각` 한 줄이라 64KB 를 넘는 순간부터 소리 없이
|
|
142
|
+
* 사라졌다. 바로 위 stdout 머리말이 「조용히 잘라 버리면 사람 눈에는 '결과가 적다' 로만
|
|
143
|
+
* 보인다 — 오류도 안 나고 아무도 눈치 못 챈다」 고 못박은 그 자리인데 여기만 안 고쳐졌다.
|
|
144
|
+
* 경고를 stderr 로 쏟는 훅(린터·형 검사 감싸개)이 정확히 이 꼴이다 — 뒤쪽 경고가 통째로
|
|
145
|
+
* 없어지고 종료코드는 0 이라, 안 본 경고가 「경고 없음」 으로 올라간다.
|
|
146
|
+
*
|
|
147
|
+
* 표를 따로 다는 대신 글 끝에 한 줄로 적는다. 그러면 stderr 를 그대로 보여 주는
|
|
148
|
+
* 모든 자리가 고칠 것 없이 잘린 사실을 같이 보여 준다.
|
|
149
|
+
*/
|
|
150
|
+
아이.stderr?.on('data', (조각) => {
|
|
151
|
+
if (탈잘림) return;
|
|
152
|
+
탈 += 조각;
|
|
153
|
+
if (탈.length > 탈상한) {
|
|
154
|
+
탈 = `${탈.slice(0, 탈상한)}\n(탈 글이 ${탈상한}자에서 잘렸습니다 — 뒷부분은 안 실렸습니다)`;
|
|
155
|
+
탈잘림 = true;
|
|
111
156
|
}
|
|
112
157
|
});
|
|
113
|
-
아이.stderr?.on('data', (조각) => { if (탈.length < 65536) 탈 += 조각; });
|
|
114
158
|
아이.on('error', (탈난것) => 끝내기({ error: 탈난것, status: null, stdout: 밖, stderr: 탈 }));
|
|
115
|
-
아이.on('close', (코드) => 끝내기({ error: null, status: 코드, stdout: 밖, stderr:
|
|
159
|
+
아이.on('close', (코드) => 끝내기({ error: null, status: 코드, stdout: 밖, stderr: 탈, ...(잘림 ? { 잘림 } : {}) }));
|
|
116
160
|
});
|
|
117
161
|
}
|
|
118
162
|
|
|
@@ -165,10 +209,60 @@ export function 무리로돌리기(파일, 인자, 옵션 = {}, 끝나면 = () =
|
|
|
165
209
|
cwd = undefined, env = undefined, timeout = 0, maxBuffer = 32 * 1024 * 1024,
|
|
166
210
|
windowsHide = true, windowsVerbatimArguments = false, detached = false,
|
|
167
211
|
killSignal = 'SIGTERM',
|
|
212
|
+
/*
|
|
213
|
+
* 넘쳤을 때 **나무째** 끊는 법. 부르는 쪽이 준다 (사냥5 M3).
|
|
214
|
+
*
|
|
215
|
+
* 여기는 넘치면 `아이.kill()` 만 했다. 아이는 셸이라 셸만 죽고, 정작 쏟아내는 손자는
|
|
216
|
+
* 살아서 파이프를 계속 물었다. 그러면 'close' 가 안 와서 넘친 즉시가 아니라
|
|
217
|
+
* **시간 초과에** 돌아왔고, 그동안 여기는 넘친 뒤에도 조각을 계속 모아 메모리가
|
|
218
|
+
* 기가 단위로 불었다(검사가 `Array buffer allocation failed` 로 쓰러졌다).
|
|
219
|
+
* 나무를 끊는 판단(taskkill /t · 무리)은 부르는 쪽(tools/jobs.js)에 있으므로 받아 쓴다.
|
|
220
|
+
* 셸이 살아 있을 때 끊어야 한다 — 먼저 셸을 죽이면 나무의 뿌리가 없어진다.
|
|
221
|
+
*/
|
|
222
|
+
넘치면 = null,
|
|
223
|
+
/*
|
|
224
|
+
* 뿌리(셸)가 끝난 뒤 'close' 를 얼마나 기다리나. 0 이면 예전처럼 끝까지 (사냥5 M2).
|
|
225
|
+
*
|
|
226
|
+
* `node gc.cjs & echo done` 처럼 셸이 뒤로 띄운 손자가 stdout 을 물면 셸은 끝났는데
|
|
227
|
+
* 'close' 가 영영 안 온다. 그러면 시간 초과까지 섰고, 사람 눈에는 끝난 명령이 멈춘
|
|
228
|
+
* 것으로 보였다. 셸이 끝나면 파이프에 남은 것은 곧바로 읽히므로 짧게만 기다리고,
|
|
229
|
+
* 그래도 안 닫히면 남은 것을 끊으러 가고(남은것끊기) 우리 파이프를 놓고 끝맺는다.
|
|
230
|
+
* 끝맺을 때 넷째 인자로 {파이프남음, 남은것끊음} 을 준다 — 부르는 쪽이 사실대로 적게.
|
|
231
|
+
*/
|
|
232
|
+
뿌리끝나면기다림 = 0,
|
|
233
|
+
남은것끊기 = null,
|
|
234
|
+
/*
|
|
235
|
+
* 죽이라고 한 뒤 'close' 를 얼마나 기다리나 (8회차 · 끊긴출력).
|
|
236
|
+
*
|
|
237
|
+
* 여기는 오직 'close' 가 와야 여태 모은 것을 넘겨 줬다. 그런데 셸이 뒤로 띄운
|
|
238
|
+
* 손자가 파이프를 물고 있으면 자식을 죽여도 close 는 **영영 안 온다** — 자식은
|
|
239
|
+
* exit 하는데 파이프가 안 닫혀서다.
|
|
240
|
+
*
|
|
241
|
+
* 부르는 쪽(tools/index.js 의 Bash)은 ESC 를 받으면 자식을 죽이고 400ms 짜리
|
|
242
|
+
* 그물을 두고 「죽기 직전에 뱉은 줄」 을 기다린다. 그 줄은 여기 밖조각 에 이미
|
|
243
|
+
* 들어 있는데, 그물 갈래에는 그것을 실을 길이 없다. 그래서 그물이 먼저 울면
|
|
244
|
+
* 결과가 통째로 빈 채 나갔다 — 여덟 번 재면 절반쯤. 사람도 모델도
|
|
245
|
+
* 「사용자가 중단했습니다」 한 줄만 받고, 서버가 뻗으며 남긴 스택 트레이스는
|
|
246
|
+
* 아무 데도 안 남는다.
|
|
247
|
+
*
|
|
248
|
+
* 그래서 **죽인 뒤에는 오래 안 기다린다.** 이만큼 기다려도 close 가 안 오면
|
|
249
|
+
* 여태 모은 것을 들고 나온다. 그물(400ms)보다 넉넉히 짧게 잡는다 — 여기가
|
|
250
|
+
* 늦으면 그물이 먼저 울어 고친 것이 아무 일도 안 한 것이 된다.
|
|
251
|
+
* 0 이면 예전처럼 close 만 기다린다.
|
|
252
|
+
*/
|
|
253
|
+
죽인뒤기다림 = 200,
|
|
168
254
|
} = 옵션;
|
|
169
255
|
|
|
170
256
|
const 아이 = spawn(파일, 인자, {
|
|
171
257
|
cwd, env, windowsHide, windowsVerbatimArguments, detached,
|
|
258
|
+
/*
|
|
259
|
+
* 입력은 안 물려 준다 (사냥5 M1).
|
|
260
|
+
*
|
|
261
|
+
* stdio 를 안 적으면 stdin 이 **열린 파이프**가 된다. 아무도 안 쓰고 안 닫는 파이프라,
|
|
262
|
+
* 입력을 끝까지 읽는 명령(`cat` · `sort` · 입력을 기다리는 스크립트)이 그 앞에서
|
|
263
|
+
* 시간 초과까지 섰다. Jobs 는 처음부터 'ignore' 였다(jobs.js 띄우기옵션) — 같은 판단이다.
|
|
264
|
+
*/
|
|
265
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
172
266
|
});
|
|
173
267
|
|
|
174
268
|
const 밖조각 = [];
|
|
@@ -177,48 +271,139 @@ export function 무리로돌리기(파일, 인자, 옵션 = {}, 끝나면 = () =
|
|
|
177
271
|
let 탈크기 = 0;
|
|
178
272
|
let 끝났나 = false;
|
|
179
273
|
let 미리잡은탈 = null;
|
|
274
|
+
let 넘침 = false;
|
|
275
|
+
let 뿌리시계 = null;
|
|
276
|
+
let 죽인시계 = null;
|
|
277
|
+
let 죽였나 = false;
|
|
278
|
+
let 죽인신호 = null;
|
|
279
|
+
// 'exit' 에서 본 것. 파이프가 안 닫혀 close 를 못 받고 끝맺을 때 이걸로 탈을 짓는다.
|
|
280
|
+
let 나갔나 = false;
|
|
281
|
+
let 나간코드 = null;
|
|
282
|
+
let 나간신호 = null;
|
|
180
283
|
|
|
181
284
|
const 모으기 = (스트림, 조각들, 더할것) => {
|
|
182
285
|
조각들.push(더할것);
|
|
183
286
|
return 스트림 + 더할것.length;
|
|
184
287
|
};
|
|
185
288
|
|
|
186
|
-
const 끝내기 = (
|
|
289
|
+
const 끝내기 = (탈, 덤 = {}) => {
|
|
187
290
|
if (끝났나) return;
|
|
188
291
|
끝났나 = true;
|
|
189
292
|
clearTimeout(시계);
|
|
190
|
-
|
|
293
|
+
clearTimeout(뿌리시계);
|
|
294
|
+
clearTimeout(죽인시계);
|
|
295
|
+
끝나면(탈 ?? null, Buffer.concat(밖조각), Buffer.concat(탈조각), 덤);
|
|
191
296
|
};
|
|
192
297
|
|
|
193
298
|
/** execFile 이 만드는 것과 같은 모양의 탈. */
|
|
194
299
|
const 탈만들기 = (글, 더할것) => Object.assign(new Error(글), 더할것);
|
|
195
300
|
|
|
196
|
-
|
|
197
|
-
|
|
301
|
+
/**
|
|
302
|
+
* close 를 못 받고 끝맺을 때 쓸 탈. close 갈래가 짓는 것과 **같은 모양**이다.
|
|
303
|
+
*
|
|
304
|
+
* 먼저 잡아 둔 탈(넘침·시한)이 있으면 그것이 이긴다 — 여기서 덮으면 넘침이
|
|
305
|
+
* 시간 초과로 바뀌어 나가던 그 자리로 되돌아간다(시계 머리말).
|
|
306
|
+
*/
|
|
307
|
+
const 탈고르기 = () => 미리잡은탈 ?? (나갔나
|
|
308
|
+
? (나간신호 ? 탈만들기(`${나간신호} 시그널로 죽었습니다`, { killed: true, signal: 나간신호 })
|
|
309
|
+
: 나간코드 !== 0 ? 탈만들기(`종료코드 ${나간코드}`, { code: 나간코드, killed: false }) : null)
|
|
310
|
+
// 나가지도 않았다 — 죽이라고 했는데 안 죽는 판이다. 죽인 것은 사실대로 적는다.
|
|
311
|
+
: 탈만들기(`${죽인신호 ?? killSignal} 로 끊었습니다`,
|
|
312
|
+
{ killed: true, signal: 죽인신호 ?? killSignal }));
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 'close' 를 그만 기다리고 **여태 모은 것으로** 끝맺는다.
|
|
316
|
+
*
|
|
317
|
+
* 우리 파이프는 놓는다. 안 놓으면 손자가 물고 있는 동안 이 프로세스가 안 끝난다.
|
|
318
|
+
*
|
|
319
|
+
* @param {object} [o]
|
|
320
|
+
* @param {boolean} [o.남은것도] 남은 무리까지 끊어 보고, 끊었는지를 덤 에 적는다.
|
|
321
|
+
*/
|
|
322
|
+
const 그만기다리기 = ({ 남은것도 = false } = {}) => {
|
|
323
|
+
if (끝났나) return;
|
|
324
|
+
let 끊음 = false;
|
|
325
|
+
if (남은것도) { try { 끊음 = 남은것끊기?.(아이) === true; } catch { /* 못 끊었다 — 그대로 말한다 */ } }
|
|
326
|
+
try { 아이.stdout?.destroy(); 아이.stderr?.destroy(); } catch { /* 이미 닫힘 */ }
|
|
327
|
+
끝내기(탈고르기(), { 파이프남음: true, 남은것끊음: 끊음 });
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/*
|
|
331
|
+
* ── 죽이라고 한 순간부터 센다 ──────────────────────────────────────────
|
|
332
|
+
*
|
|
333
|
+
* 죽이는 손은 **부르는 쪽에도** 있다. tools/index.js 의 Bash 는 ESC·시간 초과·
|
|
334
|
+
* 넘침에서 taskkill(또는 무리끊기)로 나무를 끊고 나서 `kid.kill()` 을 부른다.
|
|
335
|
+
* 그 부름이 여기 안 걸리면, 정작 사람이 멈춘 그 판에서만 이 기다림이 안 선다 —
|
|
336
|
+
* 고쳐야 할 자리가 딱 거기다. 그래서 kill 을 지나가며 한 번 본다.
|
|
337
|
+
*
|
|
338
|
+
* 우리가 죽이는 자리(시계·넘쳤다)도 같은 문을 지난다. 두 벌로 적어 두면 늘
|
|
339
|
+
* 한쪽만 고쳐진다 — 이 파일이 애초에 한 벌만 두려고 생긴 파일이다.
|
|
340
|
+
*/
|
|
341
|
+
const 죽은뒤부터재기 = () => {
|
|
342
|
+
if (죽였나 || 끝났나 || !(죽인뒤기다림 > 0)) return;
|
|
343
|
+
죽였나 = true;
|
|
344
|
+
죽인시계 = setTimeout(() => 그만기다리기(), 죽인뒤기다림);
|
|
345
|
+
};
|
|
346
|
+
const 본래죽이기 = 아이.kill.bind(아이);
|
|
347
|
+
아이.kill = (신호) => {
|
|
348
|
+
죽인신호 = 신호 ?? killSignal;
|
|
349
|
+
const 됐나 = 본래죽이기(신호);
|
|
350
|
+
죽은뒤부터재기();
|
|
351
|
+
return 됐나;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// 넘친 쪽을 **사실대로** 적는다. 여태 stderr 가 넘쳐도 'stdout' 이라고 적었다 —
|
|
355
|
+
// 어느 관을 좁혀야 하는지가 이 한 낱말에 달렸는데 늘 엉뚱한 쪽을 가리켰다.
|
|
356
|
+
const 넘쳤다 = (어느쪽) => {
|
|
357
|
+
넘침 = true;
|
|
358
|
+
미리잡은탈 = 탈만들기(`${어느쪽} maxBuffer length exceeded`,
|
|
198
359
|
{ code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER', killed: true });
|
|
199
|
-
try {
|
|
360
|
+
if (넘치면) { try { 넘치면(아이); } catch { /* 끊다 말았다 — 아래 뿌리 기다림이 끝맺는다 */ } }
|
|
361
|
+
else { try { 아이.kill(killSignal); } catch { /* 이미 죽었다 */ } }
|
|
200
362
|
};
|
|
201
363
|
|
|
364
|
+
// 넘친 뒤로는 안 모은다 — 받아 봐야 버릴 것이고, 모으면 메모리만 분다(넘치면 머리말).
|
|
202
365
|
아이.stdout?.on('data', (조각) => {
|
|
366
|
+
if (넘침) return;
|
|
203
367
|
밖크기 = 모으기(밖크기, 밖조각, 조각);
|
|
204
|
-
if (밖크기 > maxBuffer && !미리잡은탈) 넘쳤다();
|
|
368
|
+
if (밖크기 > maxBuffer && !미리잡은탈) 넘쳤다('stdout');
|
|
205
369
|
});
|
|
206
370
|
아이.stderr?.on('data', (조각) => {
|
|
371
|
+
if (넘침) return;
|
|
207
372
|
탈크기 = 모으기(탈크기, 탈조각, 조각);
|
|
208
|
-
if (탈크기 > maxBuffer && !미리잡은탈) 넘쳤다();
|
|
373
|
+
if (탈크기 > maxBuffer && !미리잡은탈) 넘쳤다('stderr');
|
|
209
374
|
});
|
|
210
375
|
|
|
211
376
|
/*
|
|
212
377
|
* 시한은 **마지막 그물**이다. 부르는 쪽이 제 시계로 먼저 끊는 것이 보통이고,
|
|
213
378
|
* 이건 그것마저 못 돌았을 때 선다. unref 하지 않는다 — 여기서 놓아 버리면
|
|
214
379
|
* 그물이 아니게 된다.
|
|
380
|
+
*
|
|
381
|
+
* `시한: true` 를 붙인다. 부르는 쪽이 killed 만 보고 「시간 초과」 라고 적어서, 넘침도
|
|
382
|
+
* 시그널 죽음도 전부 시간 초과로 나갔다 (사냥5 M3).
|
|
215
383
|
*/
|
|
216
384
|
const 시계 = timeout > 0 ? setTimeout(() => {
|
|
217
|
-
|
|
218
|
-
|
|
385
|
+
/*
|
|
386
|
+
* **먼저 잡아 둔 탈은 안 덮는다** (8회차 확인). 여기서 그냥 대입해서, 넘쳐서 죽인 자식이
|
|
387
|
+
* 시한 안에 안 닫히면(끊는 손이 못 끊는 판) 넘침이 시간 초과로 바뀌어 나갔다. 부르는 쪽은
|
|
388
|
+
* timeout 을 늘려 같은 명령을 또 부른다 — 바로 위 머리말이 고쳤다던 그 자리로 되돌아간다.
|
|
389
|
+
*/
|
|
390
|
+
미리잡은탈 ??= 탈만들기(`명령이 ${timeout}ms 안에 안 끝났습니다`,
|
|
391
|
+
{ killed: true, signal: killSignal, 시한: true });
|
|
219
392
|
try { 아이.kill(killSignal); } catch { /* 이미 죽었다 */ }
|
|
220
393
|
}, timeout) : null;
|
|
221
394
|
|
|
395
|
+
// 나간 자리를 적어 둔다. close 를 못 받고 끝맺을 때(그만기다리기) 이걸로 같은 모양의 탈을 짓는다.
|
|
396
|
+
아이.on('exit', (코드, 신호) => { 나갔나 = true; 나간코드 = 코드; 나간신호 = 신호; });
|
|
397
|
+
|
|
398
|
+
if (뿌리끝나면기다림 > 0) {
|
|
399
|
+
아이.on('exit', () => {
|
|
400
|
+
if (끝났나) return;
|
|
401
|
+
// 셸은 끝났는데 파이프가 안 닫혔다 — 뒤로 띄운 것이 물고 있다(뿌리끝나면기다림 머리말).
|
|
402
|
+
// 여기는 **우리가 죽인 판이 아니라** 셸이 제 발로 끝난 판이라, 남은 무리도 끊으러 간다.
|
|
403
|
+
뿌리시계 = setTimeout(() => 그만기다리기({ 남은것도: true }), 뿌리끝나면기다림);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
222
407
|
아이.on('error', (탈) => 끝내기(탈));
|
|
223
408
|
아이.on('close', (코드, 신호) => {
|
|
224
409
|
if (미리잡은탈) return 끝내기(미리잡은탈);
|
package/src/tools/todo.js
CHANGED
|
@@ -17,11 +17,26 @@ export const STATES = ['todo', 'doing', 'done'];
|
|
|
17
17
|
|
|
18
18
|
const 표시 = { todo: '☐', doing: '▶', done: '☑' };
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 받은 줄을 다듬는다. **버린 줄은 세어서 같이 돌려준다.**
|
|
22
|
+
*
|
|
23
|
+
* 여태 text 없는 줄을 여기서 조용히 버렸다. 그게 이 도구가 막으라고 있는 바로 그 일이다 —
|
|
24
|
+
* 세 줄을 보냈는데 두 줄이 저장되고, 사라진 한 줄에 대해 아무도 아무 말을 안 했다.
|
|
25
|
+
* 무엇을 못 받았는지는 부르는 쪽이 사람에게 말해야 하므로, 버린 줄의 칸 이름까지 들려 보낸다.
|
|
26
|
+
*
|
|
27
|
+
* @returns {{목록: object[], 버린칸: string[], 버린수: number}}
|
|
28
|
+
*/
|
|
20
29
|
function 정리(items) {
|
|
21
30
|
const out = [];
|
|
31
|
+
const 버린칸 = [];
|
|
32
|
+
let 버린수 = 0;
|
|
22
33
|
for (const [i, x] of (items ?? []).entries()) {
|
|
23
34
|
const text = String(x?.text ?? x?.content ?? '').trim();
|
|
24
|
-
if (!text)
|
|
35
|
+
if (!text) {
|
|
36
|
+
버린수 += 1;
|
|
37
|
+
if (x && typeof x === 'object') 버린칸.push(...Object.keys(x));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
25
40
|
let state = String(x?.state ?? x?.status ?? 'todo').toLowerCase();
|
|
26
41
|
if (state === 'in_progress' || state === 'in-progress') state = 'doing';
|
|
27
42
|
if (state === 'completed' || state === 'complete') state = 'done';
|
|
@@ -29,7 +44,7 @@ function 정리(items) {
|
|
|
29
44
|
if (!STATES.includes(state)) state = 'todo';
|
|
30
45
|
out.push({ id: i + 1, text: text.slice(0, 200), state });
|
|
31
46
|
}
|
|
32
|
-
return out;
|
|
47
|
+
return { 목록: out, 버린칸, 버린수 };
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
export function render(items) {
|
|
@@ -84,8 +99,62 @@ export const TODO_TOOL = {
|
|
|
84
99
|
},
|
|
85
100
|
|
|
86
101
|
run(args, ctx) {
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
/*
|
|
103
|
+
* ── 배열이 아니면 **무엇을 보내야 하는지** 말한다 (사냥5 L4) ────────────
|
|
104
|
+
*
|
|
105
|
+
* 여기는 받은 것을 그대로 정리() 에 넘겼다. 그래서 `todos: "할 일"` 이 오면
|
|
106
|
+
* `(items ?? []).entries is not a function` 이라는 날것 TypeError 가 도구 결과로
|
|
107
|
+
* 나갔다. 모델은 그 말에서 고칠 자리를 못 찾고 같은 모양으로 또 보낸다.
|
|
108
|
+
*
|
|
109
|
+
* 글자로 싼 JSON 배열(`"[{\"text\":…}]"`)은 풀어서 받는다 — 모델이 인자를 한 겹
|
|
110
|
+
* 더 싸서 보내는 일은 흔하고, 뜻은 분명하다. 그 밖의 것은 꼴을 적어 돌려보낸다.
|
|
111
|
+
*/
|
|
112
|
+
let 받은것 = args?.todos;
|
|
113
|
+
if (typeof 받은것 === 'string') {
|
|
114
|
+
try { const 푼것 = JSON.parse(받은것); if (Array.isArray(푼것)) 받은것 = 푼것; } catch { /* 아래에서 꼴을 알려 준다 */ }
|
|
115
|
+
}
|
|
116
|
+
if (받은것 != null && !Array.isArray(받은것)) {
|
|
117
|
+
return {
|
|
118
|
+
error: `todos 는 배열이어야 합니다 (받은 것: ${typeof 받은것}). `
|
|
119
|
+
+ '[{"text": "할 일 한 줄", "state": "todo"}, …] 꼴로 목록 전체를 보내세요.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const { 목록: items, 버린칸, 버린수 } = 정리(받은것);
|
|
123
|
+
/*
|
|
124
|
+
* 「할 일이 비어 있습니다」 한 줄은 셋을 못 가른다 — todos 칸을 빼먹었나
|
|
125
|
+
* (`{tasks: […]}`), 줄마다 text 가 없나(`{title: …}`), 정말 빈 목록인가.
|
|
126
|
+
* 앞 둘은 위 L4 와 같은 자리다: 모델은 고칠 곳을 못 찾고 같은 꼴로 또 보낸다.
|
|
127
|
+
* 그래서 받은 칸 이름과 보낼 꼴을 같이 돌려준다. 칸 이름은 모델이 지은 것이라
|
|
128
|
+
* 여덟 개·마흔 자에서 자른다. (6회차 할일6aq-b T2)
|
|
129
|
+
*/
|
|
130
|
+
const 꼴 = 'todos 에 [{"text": "할 일 한 줄", "state": "todo"}, …] 꼴로 목록 전체를 보내세요.';
|
|
131
|
+
const 칸들 = (것들) => [...new Set(것들)].slice(0, 8).map((k) => String(k).slice(0, 40)).join(', ') || '없음';
|
|
132
|
+
if (!items.length) {
|
|
133
|
+
// ── 비었으면 **왜 비었는지** 가른다 (6회차 할일6aq-b T2) ─────────────────
|
|
134
|
+
if (받은것 == null) {
|
|
135
|
+
return { error: `todos 칸이 없습니다 (받은 칸: ${칸들(Object.keys(args ?? {}))}). ${꼴}` };
|
|
136
|
+
}
|
|
137
|
+
if (받은것.length) {
|
|
138
|
+
return { error: `할 일 줄마다 비어 있지 않은 text 가 있어야 합니다 (받은 칸: ${칸들(버린칸)}). ${꼴}` };
|
|
139
|
+
}
|
|
140
|
+
return { error: `할 일이 비어 있습니다. ${꼴}` };
|
|
141
|
+
}
|
|
142
|
+
/*
|
|
143
|
+
* ── 버린 줄이 있으면 **말한다** (8회차 확인) ──────────────────────────────
|
|
144
|
+
*
|
|
145
|
+
* 줄 전부에 text 가 없으면 위에서 말해 줬는데, **하나라도 성했으면** 그냥 성공이었다.
|
|
146
|
+
* 세 줄을 보냈는데 두 줄짜리 목록이 아무 말 없이 저장되고, 빠진 할 일은 사라졌다.
|
|
147
|
+
* 할 일이 사라지는 걸 막으려고 있는 도구가 할 일을 사라지게 하는 자리다.
|
|
148
|
+
*
|
|
149
|
+
* 반쪽짜리 목록을 저장하지 않는다. 저장해 두면 다음 갱신은 그 반쪽을 기준으로 삼고,
|
|
150
|
+
* 사라진 줄은 영영 안 돌아온다. 몇 줄을 왜 못 받았는지 적어 통째로 다시 받는다.
|
|
151
|
+
*/
|
|
152
|
+
if (버린수) {
|
|
153
|
+
return {
|
|
154
|
+
error: `할 일 ${버린수}줄에 비어 있지 않은 text 가 없어 받지 못했습니다 (그 줄들이 보낸 칸: ${칸들(버린칸)}).`
|
|
155
|
+
+ ` 반쪽짜리 목록은 저장하지 않았습니다 — ${꼴}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
89
158
|
|
|
90
159
|
const 하는중 = items.filter((x) => x.state === 'doing');
|
|
91
160
|
if (하는중.length > 1) {
|