deel-local-cli 1.6.1 → 1.6.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/README.md +2 -0
- package/package.json +2 -2
- package/src/agent/askcheck.js +281 -0
- package/src/agent/asks.js +186 -0
- package/src/agent/compact.js +28 -1
- package/src/agent/loop.js +128 -3
- package/src/agent/route.js +54 -3
- package/src/repl.js +183 -10
- package/src/tools/convert.js +38 -10
- package/src/tools/desc.en.js +7 -3
- package/src/tools/fastgrep.js +8 -11
- package/src/tools/fsutil.js +13 -1
- package/src/tools/index.js +127 -19
- package/src/tools/outline.js +2 -1
- package/src/tools/spawn.js +99 -0
- package/src/tools/todo.js +28 -0
- package/src/ui/inputbox.js +11 -2
- package/src/ui/motion.js +30 -0
- package/src/ui/pastechip.js +87 -0
- package/src/ui/working.js +14 -0
package/src/agent/loop.js
CHANGED
|
@@ -17,6 +17,7 @@ import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
|
17
17
|
import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴도구 } from '../safety/secrets.js';
|
|
18
18
|
import { get as workMode } from './modes.js';
|
|
19
19
|
import { 지시말 } from '../i18n/index.js';
|
|
20
|
+
import { 빠진것, 빠졌다는말 } from './asks.js';
|
|
20
21
|
|
|
21
22
|
/*
|
|
22
23
|
* 콜백으로만 소식을 주는 부름을, 제너레이터가 중간에 내보낼 수 있는 모양으로 바꾼다.
|
|
@@ -60,6 +61,36 @@ function 빈답인가(msg) {
|
|
|
60
61
|
return !String(msg.content ?? '').trim();
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 도구 결과 중에서 **대화에 실을 글**을 고른다.
|
|
66
|
+
*
|
|
67
|
+
* ── 왜 따로 두나 ────────────────────────────────────────────────────────
|
|
68
|
+
*
|
|
69
|
+
* 여태 여기는 `result.content ?? ''` 한 줄이었다. 그런데 도구가 `summary` 만
|
|
70
|
+
* 돌려주는 자리가 있다 — Recall 이 지난 대화를 다 뒤졌지만 못 찾았을 때가
|
|
71
|
+
* 그렇다. 그 경우 모델에게 가는 것은 **빈 글**이다.
|
|
72
|
+
*
|
|
73
|
+
* 사람 화면에는 「지난 대화 12개를 다 뒤졌지만 없습니다」 가 멀쩡히 찍힌다.
|
|
74
|
+
* 그래서 아무도 눈치를 못 챈다. 정작 모델은 아무 말도 못 들었으므로,
|
|
75
|
+
* 못 찾았다는 것도 모르고 **찾아본 적이 있다는 것조차** 모른다. 그 자리에서
|
|
76
|
+
* 모델이 하는 일은 둘 중 하나다 — 없는 것을 지어내거나, 사람에게 엉뚱한
|
|
77
|
+
* 선택지를 들이민다. 실제 제보가 딱 그 모양이었다.
|
|
78
|
+
*
|
|
79
|
+
* 그래서 규칙을 셋으로 못 박는다.
|
|
80
|
+
* 1) 글이 있으면 그대로.
|
|
81
|
+
* 2) 글이 없고 요약이 있으면 요약을 싣는다 — 사람이 본 것과 같은 말이다.
|
|
82
|
+
* 3) 둘 다 없으면 **없다고 말한다.** 침묵은 지어내기를 부른다.
|
|
83
|
+
*/
|
|
84
|
+
export function 실을글(result) {
|
|
85
|
+
if (result?.error) return `오류: ${result.error}`;
|
|
86
|
+
const 글 = result?.content;
|
|
87
|
+
if (String(글 ?? '').trim()) return String(글);
|
|
88
|
+
const 요약 = String(result?.summary ?? '').trim();
|
|
89
|
+
if (요약) return 요약;
|
|
90
|
+
return '도구는 돌았지만 돌려준 것이 없습니다.'
|
|
91
|
+
+ ' 없는 것을 있다고 지어내지 말고, 못 얻었다는 것을 그대로 두고 다음으로 넘어가세요.';
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
// think 값을 규격에 맞게. 'off' 는 사고를 끈다.
|
|
64
95
|
function thinkFor(conn, level) {
|
|
65
96
|
if (level === 'off') return conn.kind === 'ollama' ? false : undefined;
|
|
@@ -122,6 +153,22 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
122
153
|
*/
|
|
123
154
|
ctx.signal = signal;
|
|
124
155
|
|
|
156
|
+
/*
|
|
157
|
+
* 이번에 사람이 실제로 친 말. 도구가 볼 수 있게 여기 걸어 둔다.
|
|
158
|
+
*
|
|
159
|
+
* Ask 관문(agent/askcheck.js)이 이걸 본다 — "사용자가 이미 적어 준 것을
|
|
160
|
+
* 되묻고 있나" 는 원문 없이는 판단할 수 없다. 프롬프트에는 들어 있지만
|
|
161
|
+
* 그건 모델이 보는 것이고, 우리 코드가 볼 수 있는 자리가 없었다.
|
|
162
|
+
*
|
|
163
|
+
* 하위 작업에서는 그 하위가 받은 할일이 들어온다. 그게 맞다 — 하위에게
|
|
164
|
+
* '사용자의 말' 은 자기가 받은 할일이다.
|
|
165
|
+
*/
|
|
166
|
+
ctx.요청 = String(userText ?? '');
|
|
167
|
+
// 이번 턴에 물어본 것. 관문이 "같은 것을 또 묻나" 를 보는 자리다 (agent/askcheck.js).
|
|
168
|
+
ctx.물은것 = [];
|
|
169
|
+
// 접힐 때 요약에 뭉개지지 않게 원문을 세션에도 박아 둔다 (compact.js 의 못박은요청).
|
|
170
|
+
session.이번요청 = ctx.요청;
|
|
171
|
+
|
|
125
172
|
const conn = session.conn;
|
|
126
173
|
|
|
127
174
|
/**
|
|
@@ -200,6 +247,13 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
200
247
|
* 결과가 달라졌으면 그대로 싣는다. 고치고 나서 다시 읽는 것은 정상이다.
|
|
201
248
|
*/
|
|
202
249
|
const 부른것 = new Map();
|
|
250
|
+
/*
|
|
251
|
+
* 그 부름을 마지막으로 봤을 때, 이 턴이 파일을 몇 개나 손댔었나.
|
|
252
|
+
*
|
|
253
|
+
* 되풀이를 셀 때 이 값과 견준다. 그 사이에 파일이 늘었으면 일이 되고
|
|
254
|
+
* 있는 것이라 되풀이 셈을 처음부터 다시 센다 — 까닭은 세는 자리에.
|
|
255
|
+
*/
|
|
256
|
+
const 반복진척 = new Map();
|
|
203
257
|
/*
|
|
204
258
|
* 서명은 한 모양으로 맞춰 놓고 견준다 (NFC).
|
|
205
259
|
*
|
|
@@ -354,6 +408,34 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
354
408
|
return null;
|
|
355
409
|
};
|
|
356
410
|
|
|
411
|
+
/*
|
|
412
|
+
* ── 시킨 것을 다 했나 (agent/asks.js) ────────────────────────────────
|
|
413
|
+
*
|
|
414
|
+
* 위의 되밀기는 **아무것도 안 바꿨을 때**만 본다(손댄파일.size 로 빠져나간다).
|
|
415
|
+
* 그런데 제보받은 것은 그게 아니었다 — 네 가지를 시키면 셋을 고치고 끝냈다.
|
|
416
|
+
* 뭐라도 고쳤으니 되밀기는 안 걸리고, 안 한 하나는 말없이 사라진다.
|
|
417
|
+
*
|
|
418
|
+
* 그래서 여기서 따로 대조한다. 시킨 것을 항목으로 쪼개 두고, 항목마다
|
|
419
|
+
* 자국이 남았는지 본다 — 할 일 목록에 있나, 손댄 파일 이름에 있나,
|
|
420
|
+
* 마지막 보고에 있나. 하나도 없으면 잊은 것이다.
|
|
421
|
+
*
|
|
422
|
+
* 한 번만 민다. 밀고도 그대로면 사람에게 말한다 — 두 번 세 번 미는 것은
|
|
423
|
+
* 모델이 못 하는 일을 억지로 시키는 꼴이고, 그 왕복은 사람이 낸다.
|
|
424
|
+
*/
|
|
425
|
+
let 빠뜨림민적 = false;
|
|
426
|
+
const 빠뜨린것 = (글) => {
|
|
427
|
+
// 하위 작업은 사람이 시킨 말이 아니다. 거기 할일은 부모가 이미 쪼개 준 것이다.
|
|
428
|
+
if (깊이) return [];
|
|
429
|
+
return 빠진것({
|
|
430
|
+
요청: userText,
|
|
431
|
+
자국: [
|
|
432
|
+
...(ctx.todos ?? []).map((x) => x.text),
|
|
433
|
+
...손댄파일,
|
|
434
|
+
String(글 ?? ''),
|
|
435
|
+
],
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
357
439
|
const 되미는말 = () => {
|
|
358
440
|
const 시킨 = String(userText ?? '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
359
441
|
return 지시말() === 'en'
|
|
@@ -644,7 +726,15 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
644
726
|
yield { type: 'nudge', why: 왜, text: msg.content };
|
|
645
727
|
continue;
|
|
646
728
|
}
|
|
647
|
-
|
|
729
|
+
const 빠진 = 빠뜨린것(msg.content);
|
|
730
|
+
if (빠진.length && !빠뜨림민적) {
|
|
731
|
+
빠뜨림민적 = true;
|
|
732
|
+
session.push({ role: 'user', content: 빠졌다는말(빠진, { 영어: 지시말() === 'en' }) });
|
|
733
|
+
yield { type: 'nudge', why: '요청누락', 빠진, text: msg.content };
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
// 밀고도 그대로면 조용히 넘어가지 않는다. 사람이 알아야 다음을 정한다.
|
|
737
|
+
yield { type: 'done', steps, text: msg.content, files: 마무리(), 빠진 };
|
|
648
738
|
return;
|
|
649
739
|
}
|
|
650
740
|
|
|
@@ -1056,9 +1146,17 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1056
1146
|
// 한 번에 여러 개를 쓴 경우. changed 하나만 보면 나머지가 조용히 빠져서,
|
|
1057
1147
|
// 턴 끝에 "만들어졌다" 고 확인해 주는 파일이 넷 중 하나만 나온다.
|
|
1058
1148
|
for (const f of result.여럿 ?? []) if (f.ok && f.path) 손댄파일.add(f.path);
|
|
1149
|
+
/*
|
|
1150
|
+
* 옮긴 것도 손댄 것이다.
|
|
1151
|
+
*
|
|
1152
|
+
* Move 는 파일을 고치는 게 아니라 자리를 바꾸므로 changed·여럿 이
|
|
1153
|
+
* 아니라 바뀐것들 로 온다. 이걸 안 세면 스물두 개를 옮겨 놓고도
|
|
1154
|
+
* 이 턴은 "아무것도 안 했다" 고 알고 있게 된다.
|
|
1155
|
+
*/
|
|
1156
|
+
for (const f of result.바뀐것들 ?? []) 손댄파일.add(f);
|
|
1059
1157
|
|
|
1060
1158
|
// 앞에서 똑같이 부른 적이 있고 결과도 같으면, 결과를 다시 싣지 않는다.
|
|
1061
|
-
let 실을것 = result
|
|
1159
|
+
let 실을것 = 실을글(result);
|
|
1062
1160
|
if (!result.error) {
|
|
1063
1161
|
const 서명 = 서명만들기(call);
|
|
1064
1162
|
// 결과도 한 모양으로 맞춰 견준다. 여러 도구가 받은 경로를 답에 그대로
|
|
@@ -1067,13 +1165,40 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1067
1165
|
const 앞것 = 부른것.get(서명);
|
|
1068
1166
|
if (앞것 !== undefined && 앞것 === 같은꼴) {
|
|
1069
1167
|
session.본것?.본것('되풀이');
|
|
1070
|
-
|
|
1168
|
+
/*
|
|
1169
|
+
* ── 되풀이와 「일하면서 장부를 다시 쓴 것」 은 다르다 ──────────
|
|
1170
|
+
*
|
|
1171
|
+
* 「폴더 정리 해줘」 에서 이 자리가 사람의 턴을 죽였다. 화면은
|
|
1172
|
+
* 이랬다.
|
|
1173
|
+
*
|
|
1174
|
+
* Move 7개 옮김 → TodoWrite → Move 22개 옮김 → TodoWrite
|
|
1175
|
+
* → ⊘ 같은 자리에서 헛돌고 있어 멈췄습니다
|
|
1176
|
+
*
|
|
1177
|
+
* 파일은 스물아홉 개가 실제로 움직이고 있었다. 헛돈 것은 할 일
|
|
1178
|
+
* 목록을 같은 값으로 다시 쓴 것뿐이다. 그런데 세는 자리는 그
|
|
1179
|
+
* 사이에 무슨 일이 있었는지를 안 봤다 — 같은 부름이 세 번이면
|
|
1180
|
+
* 그것으로 끝이었다.
|
|
1181
|
+
*
|
|
1182
|
+
* 이 파일 머리말에 스스로 적어 둔 원칙이 있다: 헛도는 것은
|
|
1183
|
+
* '잘 되고 있는 긴 작업' 과 구분해야 한다. 그 구분을 여기에
|
|
1184
|
+
* 넣는다 — **그 사이에 파일이 진짜로 바뀌었으면 처음부터 다시
|
|
1185
|
+
* 센다.** 아무것도 안 바뀐 채로 되풀이하는 것만 헛도는 것이다.
|
|
1186
|
+
*
|
|
1187
|
+
* 이러면 끝없이 돌 수 있지 않나: 아니다. 손댄파일 은 서로 다른
|
|
1188
|
+
* 파일만 세므로, 같은 자리를 맴돌면 안 늘어난다. 늘어난다는 것은
|
|
1189
|
+
* 매번 새 파일이 움직였다는 뜻이고, 그건 일이 되고 있는 것이다.
|
|
1190
|
+
* 그래도 걸음 수 상한이 마지막 울타리로 남아 있다.
|
|
1191
|
+
*/
|
|
1192
|
+
const 그새한일 = 손댄파일.size > (반복진척.get(서명) ?? 0);
|
|
1193
|
+
const n = 그새한일 ? 1 : (막힘.get(`반복|${서명}`) ?? 0) + 1;
|
|
1071
1194
|
막힘.set(`반복|${서명}`, n);
|
|
1195
|
+
반복진척.set(서명, 손댄파일.size);
|
|
1072
1196
|
실을것 = `앞에서 부른 ${call.name} 과 인자도 결과도 같습니다. 결과는 그대로라 다시 싣지 않습니다.`
|
|
1073
1197
|
+ ' 같은 것을 또 부르지 말고 다음 일로 넘어가세요.';
|
|
1074
1198
|
if (n + 1 >= MAX_SAME) 멈출까 = `${call.name} 을 같은 인자로 계속 부르고 있습니다 — 결과가 매번 같습니다`;
|
|
1075
1199
|
} else {
|
|
1076
1200
|
부른것.set(서명, 같은꼴);
|
|
1201
|
+
반복진척.set(서명, 손댄파일.size);
|
|
1077
1202
|
}
|
|
1078
1203
|
}
|
|
1079
1204
|
|
package/src/agent/route.js
CHANGED
|
@@ -154,6 +154,38 @@ export function 겹친요청(text) {
|
|
|
154
154
|
return { 겹침: true, why: `'${앞[0].trim()}' 뒤에 '${뒤[0].trim()}'` };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/*
|
|
158
|
+
* ── 손대라는 말 ─────────────────────────────────────────────────────────
|
|
159
|
+
*
|
|
160
|
+
* "폴더 구조 개선해줘" 가 설계 모드로 갔다. 설계는 **읽기만 하는 모드**라,
|
|
161
|
+
* 사람은 고쳐 달라고 했는데 설계안 한 장을 받고 끝났다. 이 파일 맨 위에
|
|
162
|
+
* 적어 둔 것이 정확히 그 경우다 — "읽기 전용 모드로 잘못 보내면 사용자는
|
|
163
|
+
* '왜 파일을 안 고쳐?' 하고 막힌다."
|
|
164
|
+
*
|
|
165
|
+
* 낱말 점수로는 못 막는다. '구조' 와 '개선' 이 둘 다 설계의 강한 신호라서,
|
|
166
|
+
* 뒤에 붙은 '해줘' 를 아무리 봐도 점수로는 안 뒤집힌다.
|
|
167
|
+
*
|
|
168
|
+
* 그래서 **동사를 따로 본다.** 손대라는 말이 있으면 읽기만 하는 모드
|
|
169
|
+
* (설계·계획·묻기)는 후보에서 뺀다. 남은 것 중에 고르고, 없으면 종합에
|
|
170
|
+
* 그대로 둔다 — 종합은 파일을 고칠 수 있다.
|
|
171
|
+
*
|
|
172
|
+
* 가르는 자리가 미묘하다. '설명해줘' 와 '개선해줘' 는 둘 다 '…해줘' 지만
|
|
173
|
+
* 하나는 읽고 말하라는 말이고 하나는 고치라는 말이다. 그래서 어미가 아니라
|
|
174
|
+
* **동사 목록**으로 본다. 목록에 없으면 손대라는 말이 아니다.
|
|
175
|
+
*/
|
|
176
|
+
const 손대는동사 = '만들|고쳐|고치|바꿔|바꾸|추가|넣어|지워|삭제|옮겨|나눠|나누|합쳐|정리|개선|구현|작성|적용|설치|배포|리팩터링|리팩터|분리|통일|세팅|갈아|손봐|손보';
|
|
177
|
+
// 동사 바로 뒤에 시킴꼴이 붙어야 한다. 빈칸을 안 넘으므로 "나누는 설계만 봐줘"
|
|
178
|
+
// 는 안 걸린다 — 그건 나누라는 말이 아니라 나누는 안을 보자는 말이다.
|
|
179
|
+
const 손대라는말 = new RegExp(`(${손대는동사})\\S{0,3}(줘|주세요|주라|해라|하라|하자|해다오|해줄래|할래)`);
|
|
180
|
+
|
|
181
|
+
// 파일을 못 고치는 모드. modes.js 에서 쓰기 도구를 안 주는 것들과 같아야 한다.
|
|
182
|
+
const 읽기만하는모드 = new Set(['architect', 'plan', 'ask']);
|
|
183
|
+
|
|
184
|
+
/** 파일을 손대라는 말이 들었나. */
|
|
185
|
+
export function 손대라했나(text) {
|
|
186
|
+
return 손대라는말.test(String(text ?? ''));
|
|
187
|
+
}
|
|
188
|
+
|
|
157
189
|
/**
|
|
158
190
|
* 이 한마디가 무슨 일인지 고른다.
|
|
159
191
|
*
|
|
@@ -186,10 +218,28 @@ export function route(text) {
|
|
|
186
218
|
근거[mode] = 걸린것;
|
|
187
219
|
}
|
|
188
220
|
|
|
189
|
-
|
|
221
|
+
/*
|
|
222
|
+
* 손대라는 말이 있으면 읽기만 하는 모드는 후보에서 뺀다.
|
|
223
|
+
*
|
|
224
|
+
* 점수를 깎지 않고 **줄에서 뺀다.** 깎으면 얼마나 깎을지가 또 하나의 눈금이
|
|
225
|
+
* 되고, 그 눈금은 아무도 못 지킨다. 여기서 가르는 것은 정도의 문제가 아니라
|
|
226
|
+
* 종류의 문제다 — 고칠 수 있는 자리인가 아닌가.
|
|
227
|
+
*/
|
|
228
|
+
const 손대라 = 손대라했나(s);
|
|
229
|
+
const 뺀것 = [];
|
|
230
|
+
let 순위 = Object.entries(점수들).sort((a, b) => b[1] - a[1]);
|
|
231
|
+
if (손대라) {
|
|
232
|
+
for (const [m, n] of 순위) if (읽기만하는모드.has(m) && n > 0) 뺀것.push(m);
|
|
233
|
+
순위 = 순위.filter(([m]) => !읽기만하는모드.has(m));
|
|
234
|
+
}
|
|
190
235
|
const [으뜸, 점수] = 순위[0] ?? [null, 0];
|
|
191
236
|
if (!으뜸 || 점수 < (문턱[으뜸] ?? 4)) {
|
|
192
|
-
|
|
237
|
+
// 왜 안 바꿨는지는 화면에 그대로 뜬다. "설계로 갈 뻔했는데 고치라는 말이라
|
|
238
|
+
// 안 보냈다" 와 "그냥 모르겠다" 는 사람에게 아주 다른 말이다.
|
|
239
|
+
const 왜 = 뺀것.length
|
|
240
|
+
? `${뺀것.join('·')} 신호가 있었지만 고치라는 말이라 읽기 전용 모드로 안 보냄`
|
|
241
|
+
: '무슨 일인지 뚜렷하지 않음';
|
|
242
|
+
return { mode: null, score: 점수, why: 왜, 겹침: false, 점수들, 뺀것 };
|
|
193
243
|
}
|
|
194
244
|
|
|
195
245
|
// 1·2등이 붙으면 고르지 않는다. 반반인 것을 억지로 정하면 절반은 틀린다.
|
|
@@ -201,7 +251,8 @@ export function route(text) {
|
|
|
201
251
|
return {
|
|
202
252
|
mode: 으뜸,
|
|
203
253
|
score: 점수,
|
|
204
|
-
why: 근거[으뜸].slice(0, 2).map((x) => `'${x}'`).join(', '),
|
|
254
|
+
why: [근거[으뜸].slice(0, 2).map((x) => `'${x}'`).join(', '),
|
|
255
|
+
뺀것.length ? `(${뺀것.join('·')} 는 고치라는 말이라 뺐습니다)` : ''].filter(Boolean).join(' '),
|
|
205
256
|
겹침: false,
|
|
206
257
|
점수들,
|
|
207
258
|
};
|
package/src/repl.js
CHANGED
|
@@ -36,6 +36,7 @@ import { explain, shows as levelShows } from './ui/level.js';
|
|
|
36
36
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
37
37
|
import { 추천, 채울글 } from './ui/complete.js';
|
|
38
38
|
import { 접어쓰기 } from './ui/wrap.js';
|
|
39
|
+
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들 } from './ui/pastechip.js';
|
|
39
40
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
40
41
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
41
42
|
import { expand as expandMentions } from './agent/mention.js';
|
|
@@ -430,9 +431,39 @@ export async function chatLoop(opts = {}) {
|
|
|
430
431
|
* (TDZ). 켜자마자 아무 키나 누르면 죽는 프로그램이 되는 셈이다.
|
|
431
432
|
*/
|
|
432
433
|
let turn = null;
|
|
434
|
+
|
|
435
|
+
/*
|
|
436
|
+
* 멈추라는 것이 닿았다고 **곧바로** 화면에 적는다.
|
|
437
|
+
*
|
|
438
|
+
* 멈추기는 즉시가 아니다. 돌던 자식 프로그램(rg·soffice)을 죽이고, 안 돈
|
|
439
|
+
* 도구 자리를 채우고, 대화를 성하게 닫는 데 잠깐이 걸린다. 그 잠깐 동안
|
|
440
|
+
* 화면이 아까 하던 말을 그대로 하고 있으면 사람은 안 먹었다고 판단한다.
|
|
441
|
+
* 그러면 또 누르고, 그러다 Ctrl+C 로 손이 가고, 두 번 누르면 대화가 닫힌다.
|
|
442
|
+
*
|
|
443
|
+
* "ESC 를 눌러도 안 멈춘다" 는 제보의 절반은 진짜로 안 멈춘 것이었고
|
|
444
|
+
* (동기 호출이 키를 안 배달했다), 나머지 절반이 이것이다 — 멈추고는
|
|
445
|
+
* 있는데 그렇게 보이지 않았다.
|
|
446
|
+
*/
|
|
447
|
+
const 멈추는중 = () => { try { 화면.일바꿈('멈춤'); } catch { /* 상자가 없는 화면도 있다 */ } };
|
|
448
|
+
|
|
433
449
|
// 붙여넣는 중인가 (bracketed paste). 키 처리와 'line' 이 같이 본다.
|
|
434
450
|
let 붙여넣는중 = false;
|
|
435
451
|
|
|
452
|
+
/*
|
|
453
|
+
* ── 붙여넣은 덩이를 접어 둔다 ────────────────────────────────────────
|
|
454
|
+
*
|
|
455
|
+
* 상자에 남는 것은 `[붙여넣기 #1 · 47줄 · 2.1KB]` 한 줄이고, 원문은
|
|
456
|
+
* 여기 그대로 있다가 보낼 때 되돌아간다. 왜 접는지는 pastechip.js 에.
|
|
457
|
+
*
|
|
458
|
+
* 접기 전 화면이 어디까지였는지도 같이 적어 둔다. 붙여넣기는 커서
|
|
459
|
+
* 자리에 끼어 들어오므로, 무엇이 새로 온 것인지는 시작 표를 받은
|
|
460
|
+
* 순간을 기억해 두어야만 안다.
|
|
461
|
+
*/
|
|
462
|
+
const 붙인것들 = new Map();
|
|
463
|
+
let 붙임번호 = 0;
|
|
464
|
+
let 붙임앞줄수 = 0;
|
|
465
|
+
let 붙임앞입력 = '';
|
|
466
|
+
|
|
436
467
|
/** 줄을 쌓아 두고 "다음 줄" 이라고 알린다. 백틱 표시와 Alt+Enter 가 같이 쓴다. */
|
|
437
468
|
const 줄쌓기 = (줄) => {
|
|
438
469
|
const 첫줄 = 이어쓰기줄들 === null;
|
|
@@ -529,8 +560,22 @@ export async function chatLoop(opts = {}) {
|
|
|
529
560
|
화면.대기갱신('', queue.length + 1);
|
|
530
561
|
}
|
|
531
562
|
}
|
|
532
|
-
|
|
533
|
-
|
|
563
|
+
/*
|
|
564
|
+
* ── 접어 둔 붙여넣기를 여기서 편다 ──────────────────────────────────
|
|
565
|
+
*
|
|
566
|
+
* 화면에 남는 것과 모델에게 가는 것이 여기서만 일부러 다르다. 화면에는
|
|
567
|
+
* `[붙여넣기 #1 · 47줄 · 2.1KB]` 이 남고, 모델에게는 그 47줄이 그대로
|
|
568
|
+
* 간다. 표가 무엇을 담고 있었는지를 말해 주므로 나중에 스크롤을 올려도
|
|
569
|
+
* 무엇을 시켰는지 알아볼 수 있다.
|
|
570
|
+
*
|
|
571
|
+
* 펴는 자리는 여기 한 군데다. 미리 쳐 둔 것이든 기다리다 보낸 것이든
|
|
572
|
+
* 결국 이 줄을 지나가므로, 여기서 펴면 어느 길로 와도 원문이 간다.
|
|
573
|
+
*/
|
|
574
|
+
const 펴진것 = 붙임펼치기(보낼것, 붙인것들);
|
|
575
|
+
// 다 쓴 것은 치운다. 안 치우면 대화가 길어질수록 붙인 원문이 계속 쌓인다.
|
|
576
|
+
for (const 번호 of 붙임쓴번호들(보낼것)) 붙인것들.delete(번호);
|
|
577
|
+
if (waiter) { const w = waiter; waiter = null; w(펴진것); }
|
|
578
|
+
else queue.push(펴진것);
|
|
534
579
|
});
|
|
535
580
|
rl.on('close', () => {
|
|
536
581
|
closed = true;
|
|
@@ -592,11 +637,53 @@ export async function chatLoop(opts = {}) {
|
|
|
592
637
|
process.on('exit', () => { try { process.stdout.write('\x1b[?2004l'); } catch { /* 이미 닫혔다 */ } });
|
|
593
638
|
|
|
594
639
|
process.stdin.on('keypress', (_ch, key) => {
|
|
595
|
-
if (key?.sequence === '\x1b[200~') {
|
|
640
|
+
if (key?.sequence === '\x1b[200~') {
|
|
641
|
+
붙여넣는중 = true;
|
|
642
|
+
붙임앞줄수 = 이어쓰기줄들?.length ?? 0;
|
|
643
|
+
붙임앞입력 = rl.line ?? '';
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
596
646
|
if (key?.sequence === '\x1b[201~') {
|
|
597
647
|
붙여넣는중 = false;
|
|
598
|
-
|
|
599
|
-
|
|
648
|
+
/*
|
|
649
|
+
* 이번에 들어온 것만 골라낸다.
|
|
650
|
+
*
|
|
651
|
+
* 붙여넣기에 줄바꿈이 있었으면 readline 이 줄마다 'line' 을 쐈고,
|
|
652
|
+
* 그 줄들은 붙임앞줄수 뒤에 쌓여 있다. 마지막 조각은 아직 입력칸에
|
|
653
|
+
* 남아 있다. 그리고 붙이기 전에 치고 있던 글(앞머리)은 **첫 줄
|
|
654
|
+
* 앞에 붙어서** 같이 나갔으므로 떼어 내야 한다.
|
|
655
|
+
*
|
|
656
|
+
* 커서를 글 가운데 두고 붙이면 어디까지가 붙인 것인지 가릴 수
|
|
657
|
+
* 없다. 그때는 접지 않는다 — 접기는 되돌릴 수 있어야 하고, 잘못
|
|
658
|
+
* 가른 접기는 못 되돌린다.
|
|
659
|
+
*/
|
|
660
|
+
const 새줄들 = (이어쓰기줄들 ?? []).slice(붙임앞줄수);
|
|
661
|
+
const 지금입력 = rl.line ?? '';
|
|
662
|
+
const 앞머리 = 붙임앞입력;
|
|
663
|
+
let 붙인줄들 = null;
|
|
664
|
+
if (새줄들.length) {
|
|
665
|
+
if (!앞머리 || 새줄들[0].startsWith(앞머리)) {
|
|
666
|
+
붙인줄들 = [새줄들[0].slice(앞머리.length), ...새줄들.slice(1), 지금입력];
|
|
667
|
+
}
|
|
668
|
+
} else if (지금입력.startsWith(앞머리)) {
|
|
669
|
+
붙인줄들 = [지금입력.slice(앞머리.length)];
|
|
670
|
+
}
|
|
671
|
+
const 붙인것 = 붙인줄들 ? 펴기(붙인줄들.join('\n')) : '';
|
|
672
|
+
const 폭 = Math.max(20, (process.stdout.columns ?? 80) - 8);
|
|
673
|
+
|
|
674
|
+
if (붙인줄들 && 입력기다림 && 묻는중 === null && 붙임접을까(붙인것, { 폭 })) {
|
|
675
|
+
const 번호 = ++붙임번호;
|
|
676
|
+
붙인것들.set(번호, 붙인것);
|
|
677
|
+
이어쓰기줄들 = 붙임앞줄수 ? (이어쓰기줄들 ?? []).slice(0, 붙임앞줄수) : null;
|
|
678
|
+
if (이어쓰기줄들 === null) 이어쓴것 = false;
|
|
679
|
+
const 표 = 붙임표(번호, 붙인것);
|
|
680
|
+
rl.line = 앞머리 + 표;
|
|
681
|
+
rl.cursor = rl.line.length;
|
|
682
|
+
화면.입력지움();
|
|
683
|
+
say(` ${c.gray(`${표} — 접어 뒀습니다. 보낼 때 그대로 펴집니다.`)}`);
|
|
684
|
+
} else if (이어쓰기줄들?.length && 입력기다림) {
|
|
685
|
+
// 접지 않은 덩이는 몇 줄인지라도 알려 준다. 안 보이면 사라진 줄
|
|
686
|
+
// 알고 다시 붙인다.
|
|
600
687
|
화면.입력지움();
|
|
601
688
|
say(` ${c.gray(`${이어쓰기줄들.length + 1}줄을 붙여넣었습니다 — Enter 로 한 번에 보냅니다`)}`);
|
|
602
689
|
}
|
|
@@ -615,7 +702,7 @@ export async function chatLoop(opts = {}) {
|
|
|
615
702
|
* 치던 글이 날아가면 그게 더 놀랍다.
|
|
616
703
|
*/
|
|
617
704
|
if (key?.name === 'escape' && !key.ctrl && !key.meta && !key.shift) {
|
|
618
|
-
if (turn && !turn.signal.aborted) { turn.abort(); return; }
|
|
705
|
+
if (turn && !turn.signal.aborted) { turn.abort(); 멈추는중(); return; }
|
|
619
706
|
return;
|
|
620
707
|
}
|
|
621
708
|
// Shift+Tab — 승인 방식 (자동 → 위험만 → 모두)
|
|
@@ -909,11 +996,26 @@ export async function chatLoop(opts = {}) {
|
|
|
909
996
|
say('');
|
|
910
997
|
},
|
|
911
998
|
|
|
912
|
-
ask물음: async (물음, 고를것 = []) => {
|
|
999
|
+
ask물음: async (물음, 고를것 = [], 이해 = '') => {
|
|
913
1000
|
if (closed) return null;
|
|
1001
|
+
const 폭 = Math.max(40, Math.min(88, (process.stdout.columns || 80) - 6)) - 4;
|
|
914
1002
|
say('');
|
|
915
1003
|
say(` ${c.hcyan('┌')} ${c.bold(옮긴말('ask.title'))}`);
|
|
916
|
-
|
|
1004
|
+
/*
|
|
1005
|
+
* 물음보다 **이해를 먼저** 보여 준다.
|
|
1006
|
+
*
|
|
1007
|
+
* 사람이 이 상자에서 제일 먼저 알고 싶은 것은 "얘가 내 말을 알아듣긴
|
|
1008
|
+
* 했나" 이다. 그걸 모르는 채 선택지부터 보면, 답을 고르는 것이 아니라
|
|
1009
|
+
* 딴소리를 하는지 아닌지를 먼저 가늠해야 한다. 순서가 뒤바뀌면
|
|
1010
|
+
* 물음 자체가 짐이 된다.
|
|
1011
|
+
*/
|
|
1012
|
+
if (String(이해 ?? '').trim()) {
|
|
1013
|
+
for (const 줄 of 접어쓰기(`알아들은 것: ${String(이해).trim()}`, 폭)) {
|
|
1014
|
+
say(` ${c.hcyan('│')} ${c.gray(줄)}`);
|
|
1015
|
+
}
|
|
1016
|
+
say(` ${c.hcyan('│')}`);
|
|
1017
|
+
}
|
|
1018
|
+
for (const 줄 of 접어쓰기(물음, 폭)) {
|
|
917
1019
|
say(` ${c.hcyan('│')} ${c.white(줄)}`);
|
|
918
1020
|
}
|
|
919
1021
|
if (고를것.length) {
|
|
@@ -1138,6 +1240,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1138
1240
|
}
|
|
1139
1241
|
if (turn && !turn.signal.aborted) {
|
|
1140
1242
|
turn.abort();
|
|
1243
|
+
멈추는중();
|
|
1141
1244
|
return; // 화면 정리는 루프 쪽 'aborted' 이벤트가 한다
|
|
1142
1245
|
}
|
|
1143
1246
|
if (interrupted) { rl.close(); return; }
|
|
@@ -1298,6 +1401,15 @@ export async function chatLoop(opts = {}) {
|
|
|
1298
1401
|
// 이 턴이 탈 없이 끝났나. 끊겼거나 터진 뒤에 승인 창을 띄우면 안 된다 —
|
|
1299
1402
|
// 계획이 반만 나온 것을 두고 "이대로 진행할까요?" 를 묻는 꼴이 된다.
|
|
1300
1403
|
let 턴탈났나 = false;
|
|
1404
|
+
/*
|
|
1405
|
+
* 걸음을 다 써서 중간에 끊긴 턴인가. 끊겼을 때 남은 할 일을 여기 담는다.
|
|
1406
|
+
*
|
|
1407
|
+
* 「중간에 끊기는 이슈가 생기는거 같은데」 — 여기가 그 자리다. 여태는
|
|
1408
|
+
* "이어서 하려면 다시 말씀하세요" 라고만 하고 끝났다. 사람은 방금 화면에
|
|
1409
|
+
* 다 적혀 있는 남은 할 일을 손으로 다시 옮겨 적거나, "이어서 해줘" 라고만
|
|
1410
|
+
* 쳤다. 그러면 모델은 무엇을 잇는지 몰라서 처음부터 다시 뒤진다.
|
|
1411
|
+
*/
|
|
1412
|
+
let 끊긴할일 = null;
|
|
1301
1413
|
let thinkChars = 0;
|
|
1302
1414
|
let streamed = false;
|
|
1303
1415
|
/*
|
|
@@ -1720,8 +1832,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1720
1832
|
break;
|
|
1721
1833
|
|
|
1722
1834
|
case 'limit':
|
|
1835
|
+
끊긴할일 = ev.남은할일 ?? [];
|
|
1723
1836
|
say('');
|
|
1724
|
-
say(` ${mark.warn} 도구 호출 ${ev.steps}회에서
|
|
1837
|
+
say(` ${mark.warn} 도구 호출 ${ev.steps}회에서 멈췄습니다.`);
|
|
1725
1838
|
// 무엇이 안 끝났는지 그 자리에 적는다. 위로 스크롤해 도구 줄을
|
|
1726
1839
|
// 세어 보게 하면, 이어서 시킬 때 무엇을 시켜야 할지 알 수 없다.
|
|
1727
1840
|
if (ev.남은할일?.length) {
|
|
@@ -1788,13 +1901,28 @@ export async function chatLoop(opts = {}) {
|
|
|
1788
1901
|
clearThinking();
|
|
1789
1902
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1790
1903
|
say('');
|
|
1791
|
-
say(
|
|
1904
|
+
say(ev.why === '요청누락'
|
|
1905
|
+
? ` ${c.gray(`↺ 시킨 것 ${ev.빠진?.length ?? 0}가지를 빼놓고 끝내려고 해서 되밀었습니다`)}`
|
|
1906
|
+
: ` ${c.gray('↺ 읽기만 하고 끝내려고 해서 한 번 되밀었습니다')}`);
|
|
1792
1907
|
break;
|
|
1793
1908
|
|
|
1794
1909
|
case 'done':
|
|
1795
1910
|
clearThinking();
|
|
1796
1911
|
if (streamed) { 답비우기(); say(''); }
|
|
1797
1912
|
만든파일보이기(ev.files);
|
|
1913
|
+
/*
|
|
1914
|
+
* 되밀고도 그대로면 사람에게 말한다.
|
|
1915
|
+
*
|
|
1916
|
+
* 조용히 넘어가면 사람은 다 된 줄 알고 그 위에 다음 것을 쌓는다.
|
|
1917
|
+
* 나중에 발견했을 때는 이미 늦다. 「몇 번 까먹거나 누락되거나」라는
|
|
1918
|
+
* 제보가 정확히 그 모양이었다 — 빠진 것 자체보다, 빠졌다는 말을
|
|
1919
|
+
* 안 한 것이 문제다.
|
|
1920
|
+
*/
|
|
1921
|
+
if (ev.빠진?.length) {
|
|
1922
|
+
say('');
|
|
1923
|
+
say(` ${c.yellow('!')} ${c.white(`시킨 것 중 ${ev.빠진.length}가지가 안 다뤄졌습니다`)}`);
|
|
1924
|
+
for (const x of ev.빠진) say(` ${c.gray(`${x.번호}. ${clip(x.글, 68)}`)}`);
|
|
1925
|
+
}
|
|
1798
1926
|
break;
|
|
1799
1927
|
}
|
|
1800
1928
|
}
|
|
@@ -1900,6 +2028,51 @@ export async function chatLoop(opts = {}) {
|
|
|
1900
2028
|
say('');
|
|
1901
2029
|
say(` ${c.hcyan('☰')} ${c.gray('그 점을 반영해 계획을 다시 냅니다.')}`);
|
|
1902
2030
|
}
|
|
2031
|
+
} else if (끊긴할일) {
|
|
2032
|
+
/*
|
|
2033
|
+
* ── 끊긴 자리에서 ⏎ 하나로 잇는다 ────────────────────────────────
|
|
2034
|
+
*
|
|
2035
|
+
* 「중간에 끊기는 이슈가 생기는거 같은데 해당 문제점들이 극복되면 좋겠어」
|
|
2036
|
+
*
|
|
2037
|
+
* 걸음 상한은 안전장치라 없앨 수 없다 — 그게 없으면 헛도는 턴이
|
|
2038
|
+
* 컨텍스트를 다 먹을 때까지 안 멈춘다. 없앨 게 아니라 **잇는 길**을
|
|
2039
|
+
* 놔야 한다.
|
|
2040
|
+
*
|
|
2041
|
+
* 이어갈 말에는 남은 단계를 **글자 그대로 싣는다.** 계획 승인에서
|
|
2042
|
+
* 배운 것과 같은 이유다 — "아까 하던 것 이어서 해라" 는 가리키기만
|
|
2043
|
+
* 하고 안 주는 말이라, 작은 모델은 그 '아까' 를 찾으러 지난 대화를
|
|
2044
|
+
* 뒤지기 시작한다.
|
|
2045
|
+
*/
|
|
2046
|
+
// 끝난 것을 빼는 것은 loop.js 가 이미 한 번 한다. 여기서 또 하는 것은
|
|
2047
|
+
// 이 말이 **끝난 일을 다시 시키는 말**이 되면 안 되기 때문이다 —
|
|
2048
|
+
// 저쪽 걸러내기가 언젠가 바뀌어도 이 자리는 안 흔들려야 한다.
|
|
2049
|
+
const 남은 = 끊긴할일.filter((t) => t && t.state !== 'done');
|
|
2050
|
+
say('');
|
|
2051
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
2052
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
2053
|
+
const 답 = String(await ask(
|
|
2054
|
+
`이어서 할까요? ${c.gray('⏎ 이어서 · n 그만')}`,
|
|
2055
|
+
{ def: 'y' },
|
|
2056
|
+
)).trim().toLowerCase();
|
|
2057
|
+
|
|
2058
|
+
if (['n', 'no', 'ㄴ', '취소', '그만', '아니', '아니요', '아니오'].includes(답)) {
|
|
2059
|
+
say(` ${c.gray('여기서 멈췄습니다. 나중에 「이어서 해줘」 라고 하셔도 됩니다.')}`);
|
|
2060
|
+
} else {
|
|
2061
|
+
const 단계글 = 남은.map((t, i) => `${i + 1}. ${String(t.text ?? '')}`).join('\n');
|
|
2062
|
+
이어갈것 = (남은.length
|
|
2063
|
+
? '걸음 수를 다 써서 중간에 멈췄다. 사람이 이어서 하라고 했다.\n\n'
|
|
2064
|
+
+ `아직 안 끝난 것은 아래 그대로다.\n\n${단계글}\n\n`
|
|
2065
|
+
+ '이것부터 이어서 실제로 해라.'
|
|
2066
|
+
: '걸음 수를 다 써서 중간에 멈췄다. 사람이 이어서 하라고 했다.'
|
|
2067
|
+
+ ' 하던 자리에서 이어서 해라.')
|
|
2068
|
+
+ ' **이미 끝낸 것은 다시 하지 마라.** 지난 대화를 뒤지지 마라 —'
|
|
2069
|
+
+ ' 필요한 것은 이 대화 안에 다 있다. 처음부터 다시 시작하지도 마라.';
|
|
2070
|
+
// 모드를 다시 고르지 않는다. 이 말에는 '해라' 가 들어 있어서 다시
|
|
2071
|
+
// 고르면 엉뚱한 데로 가고, 애초에 하던 일을 잇는 것이라 바뀔 이유가 없다.
|
|
2072
|
+
이어갈모드 = session.routed ?? (session.work === 'auto' ? 'code' : session.work);
|
|
2073
|
+
say('');
|
|
2074
|
+
say(` ${c.hgreen('▶')} ${c.gray(`끊긴 자리에서 이어서 합니다${남은.length ? ` (남은 ${남은.length}가지)` : ''}.`)}`);
|
|
2075
|
+
}
|
|
1903
2076
|
}
|
|
1904
2077
|
}
|
|
1905
2078
|
|
package/src/tools/convert.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
//
|
|
26
26
|
// 바뀐 글만 쓰고 원본은 한 글자도 안 건드린다. 변환은 읽기의 곁길이다.
|
|
27
27
|
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { 돌려보기 } from './spawn.js';
|
|
28
29
|
import {
|
|
29
30
|
existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync,
|
|
30
31
|
} from 'node:fs';
|
|
@@ -90,17 +91,44 @@ const soffice자리 = [
|
|
|
90
91
|
* `cmd /s /c` 는 뒤엣것이 따옴표로 시작해 따옴표로 끝나면 바깥 한 쌍을 떼어
|
|
91
92
|
* 내므로, 한 겹 더 둘러서 넘긴다.
|
|
92
93
|
*/
|
|
93
|
-
function
|
|
94
|
+
function 명령짓기(cmd, 인자) {
|
|
94
95
|
const 셸필요 = process.platform === 'win32' && /\.(cmd|bat)$/i.test(String(cmd));
|
|
95
|
-
if (!셸필요) return
|
|
96
|
+
if (!셸필요) return { 실행: cmd, 인자, 덤: {} };
|
|
96
97
|
const 몰아쓰기 = `""${cmd}" ${인자.map((a) => `"${a}"`).join(' ')}"`;
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
return {
|
|
99
|
+
실행: process.env.ComSpec || 'cmd.exe',
|
|
100
|
+
인자: ['/d', '/s', '/c', 몰아쓰기],
|
|
101
|
+
덤: { windowsVerbatimArguments: true },
|
|
102
|
+
};
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/*
|
|
106
|
+
* 변환은 **비동기로** 부른다.
|
|
107
|
+
*
|
|
108
|
+
* soffice 는 처음 뜰 때 20초, 큰 문서면 그 이상 걸린다. 전에는 이 자리가
|
|
109
|
+
* spawnSync 였고, 그동안 Node 는 이벤트 루프를 통째로 멈췄다 — 그 90초 내내
|
|
110
|
+
* ESC 가 배달되지 않았다. 화면에는 도는 그림이 보이는데 키가 안 먹으니
|
|
111
|
+
* 사람 눈에는 프로그램이 고장 난 것처럼 보인다.
|
|
112
|
+
*
|
|
113
|
+
* 자식을 띄우고 기다리는 일은 tools/spawn.js 한 벌만 안다. 찾기(fastgrep)와
|
|
114
|
+
* 여기가 같은 것을 쓴다 — 따로 적어 두면 언젠가 한쪽만 고쳐진다.
|
|
115
|
+
*/
|
|
116
|
+
function 부르기(cmd, 인자, opts = {}) {
|
|
117
|
+
const c = 명령짓기(cmd, 인자);
|
|
118
|
+
return 돌려보기(c.실행, c.인자, { ...opts, 덤: c.덤 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
* 있는지 본다. 여기는 **동기로 둔다.**
|
|
123
|
+
*
|
|
124
|
+
* `--version` 은 곧바로 답하고, 한 세션에 한 번만 부른다(변환기찾기가 기억한다).
|
|
125
|
+
* 이걸 비동기로 만들면 변환기찾기·변환기말·못바꿈말이 줄줄이 비동기가 되고,
|
|
126
|
+
* 그 함수들은 안내문을 만드는 자리라 부르는 데가 많다. 90초를 없애려다
|
|
127
|
+
* 열 군데를 흔드는 것은 값이 안 맞는다.
|
|
128
|
+
*/
|
|
102
129
|
function 돌아가나(cmd, 인자) {
|
|
103
|
-
const
|
|
130
|
+
const c = 명령짓기(cmd, 인자);
|
|
131
|
+
const r = spawnSync(c.실행, c.인자, { encoding: 'utf8', timeout: 15000, windowsHide: true, ...c.덤 });
|
|
104
132
|
return !r.error && r.status === 0;
|
|
105
133
|
}
|
|
106
134
|
|
|
@@ -189,7 +217,7 @@ export function 임시자리(root) {
|
|
|
189
217
|
* @param {string} root 작업 폴더 — 바꾼 것을 여기 안에 떨군다
|
|
190
218
|
* @returns {{ok:true, text:string, 쓴것:string} | {ok:false, 왜:string, 없음?:boolean}}
|
|
191
219
|
*/
|
|
192
|
-
export function 글로바꾸기(abs, root, { timeout = 90000, 찾은것 = null } = {}) {
|
|
220
|
+
export async function 글로바꾸기(abs, root, { timeout = 90000, 찾은것 = null, signal = null } = {}) {
|
|
193
221
|
const 있는것 = 찾은것 ?? 변환기찾기();
|
|
194
222
|
if (!있는것.soffice && !있는것.textutil) {
|
|
195
223
|
return { ok: false, 없음: true, 왜: 있는것.왜 ?? '이 PC 에 변환기가 없습니다' };
|
|
@@ -210,7 +238,7 @@ export function 글로바꾸기(abs, root, { timeout = 90000, 찾은것 = null }
|
|
|
210
238
|
const 확장자 = extname(abs).toLowerCase();
|
|
211
239
|
if (있는것.textutil && ['.doc', '.rtf', '.odt', '.docx'].includes(확장자)) {
|
|
212
240
|
const 나온것 = join(받을곳, `${basename(abs, 확장자)}.txt`);
|
|
213
|
-
const r = 부르기('textutil', ['-convert', 'txt', '-output', 나온것, abs], { timeout });
|
|
241
|
+
const r = await 부르기('textutil', ['-convert', 'txt', '-output', 나온것, abs], { timeout, signal });
|
|
214
242
|
if (!r.error && r.status === 0 && existsSync(나온것)) {
|
|
215
243
|
const text = readFileSync(나온것, 'utf8');
|
|
216
244
|
// soffice 쪽과 같은 규칙 — 읽었으면 사본을 남기지 않는다.
|
|
@@ -234,13 +262,13 @@ export function 글로바꾸기(abs, root, { timeout = 90000, 찾은것 = null }
|
|
|
234
262
|
*/
|
|
235
263
|
const 프로필 = join(받을곳, '.soffice-profile');
|
|
236
264
|
const 전 = new Set(existsSync(받을곳) ? readdirSync(받을곳) : []);
|
|
237
|
-
const r = 부르기(있는것.soffice, [
|
|
265
|
+
const r = await 부르기(있는것.soffice, [
|
|
238
266
|
`-env:UserInstallation=file:///${프로필.replace(/\\/g, '/').replace(/^\/+/, '')}`,
|
|
239
267
|
'--headless', '--norestore',
|
|
240
268
|
'--convert-to', 'txt:Text',
|
|
241
269
|
'--outdir', 받을곳,
|
|
242
270
|
abs,
|
|
243
|
-
], { timeout });
|
|
271
|
+
], { timeout, signal });
|
|
244
272
|
|
|
245
273
|
if (r.error) {
|
|
246
274
|
return { ok: false, 왜: `변환기를 못 돌렸습니다: ${r.error.message}` };
|