deel-local-cli 1.20.10 → 1.20.11
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 +3 -2
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/agent/loop.js +39 -4
- package/src/agent/session.js +279 -33
- package/src/agent/threads.js +52 -1
- package/src/commands.js +124 -10
- package/src/i18n/en.js +3 -1
- package/src/i18n/ja.js +3 -1
- package/src/i18n/ko.js +3 -1
- package/src/i18n/zh.js +3 -1
- package/src/repl.js +52 -0
- package/src/safety/undo.js +98 -8
- package/src/tools/fsutil.js +7 -1
- package/src/tools/index.js +427 -30
- package/src/ui/intro.js +2 -1
- package/src/ui/motion.js +40 -9
- package/src/ui/office.js +22 -1
- package/src/ui/screen.js +47 -4
- package/src/ui/working.js +4 -1
package/src/agent/threads.js
CHANGED
|
@@ -68,8 +68,53 @@ export class Threads {
|
|
|
68
68
|
}];
|
|
69
69
|
this.자리 = 0;
|
|
70
70
|
this.센것 = 1;
|
|
71
|
+
/*
|
|
72
|
+
* ── 닫은 갈래가 **쓴 돈**은 어디로 가나 ────────────────────────
|
|
73
|
+
*
|
|
74
|
+
* 여태 아무 데도 안 갔다. 닫기() 는 갈래를 splice 로 빼기만 했고,
|
|
75
|
+
* 그 갈래가 태운 토큰·호출·시간은 그 자리에서 사라졌다. 곁가지에서
|
|
76
|
+
* 오래 헤매다 닫고 본줄기로 돌아오면 `/cost` 는 그 값을 못 본다 —
|
|
77
|
+
* **실제로 낸 돈보다 적은 금액**이 화면에 뜬다.
|
|
78
|
+
*
|
|
79
|
+
* 갈래를 지운다고 청구서가 지워지지는 않는다. 그래서 여기에 쌓는다.
|
|
80
|
+
*/
|
|
81
|
+
this.닫힌셈 = { in: 0, out: 0, prompt: 0, calls: 0, ms: 0, retries: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, 못잰것: 0 };
|
|
82
|
+
this.닫힌수 = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* **모든 갈래**가 지금까지 쓴 것을 합친다. 닫은 갈래 것까지 센다.
|
|
87
|
+
*
|
|
88
|
+
* `/cost` 가 session.usage 하나만 보고 있었다. 그건 지금 갈래 것이다.
|
|
89
|
+
* 갈래를 셋 굴리면 화면의 금액은 그중 하나 몫이고, 갈래를 바꾸는
|
|
90
|
+
* 것만으로 숫자가 내려간다 — 돈은 그대로 나가는데.
|
|
91
|
+
*
|
|
92
|
+
* 지금 갈래 값은 **session 쪽**에서 가져온다.
|
|
93
|
+
*
|
|
94
|
+
* 처음에 여기 「갈래 표에 담긴 것은 마지막으로 옮길 때 담아 둔 값이라
|
|
95
|
+
* 이번 턴 것이 빠져 있다」 라고 적어 뒀는데, **그건 사실이 아니다.**
|
|
96
|
+
* #꺼내기() 가 `session.usage = g.usage` 로 **같은 객체를 물려** 주고,
|
|
97
|
+
* session 쪽에서 그 객체를 갈아 끼우는 자리는 생성자 말고는 없다.
|
|
98
|
+
* 그러니 지금은 둘이 늘 같은 값이다 — 어긋내기 판이 이 줄을 표 쪽으로
|
|
99
|
+
* 바꿔도 검사가 안 빨개져서 그걸로 알았다.
|
|
100
|
+
*
|
|
101
|
+
* 그래도 session 쪽을 읽는다. 언젠가 누가 usage 를 통째로 갈아 끼우면
|
|
102
|
+
* (되감기·이어받기가 그럴 만한 자리다) 표 쪽만 읽는 코드는 그 순간부터
|
|
103
|
+
* 조용히 적은 금액을 적는다. 값을 두 군데서 들고 있을 때는 **살아 있는
|
|
104
|
+
* 쪽**을 읽는 것이 맞다.
|
|
105
|
+
*/
|
|
106
|
+
전체usage() {
|
|
107
|
+
const 합 = { ...this.닫힌셈 };
|
|
108
|
+
for (let i = 0; i < this.갈래들.length; i++) {
|
|
109
|
+
const u = (i === this.자리 ? this.session.usage : this.갈래들[i].usage) ?? {};
|
|
110
|
+
for (const k of Object.keys(합)) 합[k] += Number(u[k] ?? 0);
|
|
111
|
+
}
|
|
112
|
+
return 합;
|
|
71
113
|
}
|
|
72
114
|
|
|
115
|
+
/** 지금 갈래 것과 전부의 것이 다른가. 다를 때만 화면에 두 줄이 필요하다. */
|
|
116
|
+
갈래여럿인가() { return this.갈래들.length > 1 || this.닫힌수 > 0; }
|
|
117
|
+
|
|
73
118
|
현재() { return this.갈래들[this.자리]; }
|
|
74
119
|
현재store() { return this.현재().store; }
|
|
75
120
|
개수() { return this.갈래들.length; }
|
|
@@ -128,7 +173,10 @@ export class Threads {
|
|
|
128
173
|
const g = {
|
|
129
174
|
이름: String(이름 ?? '').trim() || 기본이름(this.센것),
|
|
130
175
|
messages: 물려줄것 ? [...물려줄것] : [],
|
|
131
|
-
usage
|
|
176
|
+
// session.usage 와 **같은 모양**이어야 한다. 여태 다섯 칸만 있어서
|
|
177
|
+
// 갈래에서는 캐시·생각·못잰것 칸이 아예 없었다. 더하는 자리들이 전부
|
|
178
|
+
// `?? 0` 을 써서 터지지는 않았지만, 모양이 둘인 것 자체가 다음 사람의 함정이다.
|
|
179
|
+
usage: { in: 0, out: 0, prompt: 0, calls: 0, ms: 0, retries: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, 못잰것: 0 },
|
|
132
180
|
todos: null,
|
|
133
181
|
// 할 일도 todos 와 같이 비워서 시작한다. 곁가지는 다른 일을 하러 나가는
|
|
134
182
|
// 것이고, 물려받으면 본줄기의 남은 일을 곁가지 파일에도 적게 된다.
|
|
@@ -191,6 +239,9 @@ export class Threads {
|
|
|
191
239
|
if (i < 0) return { ok: false, why: '그런 갈래가 없습니다.' };
|
|
192
240
|
if (i === this.자리) this.#담아두기();
|
|
193
241
|
const [닫은것] = this.갈래들.splice(i, 1);
|
|
242
|
+
// 갈래는 지워도 그 갈래가 낸 돈은 안 지워진다 (생성자의 닫힌셈).
|
|
243
|
+
for (const k of Object.keys(this.닫힌셈)) this.닫힌셈[k] += Number(닫은것.usage?.[k] ?? 0);
|
|
244
|
+
this.닫힌수 += 1;
|
|
194
245
|
// 닫힌 자리보다 뒤에 있었으면 번호가 하나 당겨진다.
|
|
195
246
|
const 다음 = i === this.자리 ? Math.max(0, i - 1) : (this.자리 > i ? this.자리 - 1 : this.자리);
|
|
196
247
|
this.자리 = 다음;
|
package/src/commands.js
CHANGED
|
@@ -42,7 +42,7 @@ import { 보고서적기 } from './ui/export.js';
|
|
|
42
42
|
import { loadCommand, discover } from './skills/discover.js';
|
|
43
43
|
import { install, list, remove, pack } from './plugins/manage.js';
|
|
44
44
|
import { spin } from './ui/spinner.js';
|
|
45
|
-
import { 그림고르기설정,
|
|
45
|
+
import { 그림고르기설정, 끔설정, 환경그림, 환경으로껐나 } from './ui/motion.js';
|
|
46
46
|
import { 사무실설정, 최소높이, 최소폭 } from './ui/office.js';
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -63,6 +63,28 @@ export function 적용하기(고른것) {
|
|
|
63
63
|
// 지워 버리면, 껐다고 믿는 자리에서 그림이 다시 돈다. 설정용 값은 따로 둔다.
|
|
64
64
|
끔설정(값 === '끔');
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `/motion` 이 환경변수에 대해 해야 하는 말. 없으면 빈 배열.
|
|
69
|
+
*
|
|
70
|
+
* 여태 이 자리는 「DEEL_MOTION·DEEL_OFFICE 가 켜져 있으면 그쪽이 이깁니다」
|
|
71
|
+
* 한 줄이 전부였다. 그래서 두 가지를 틀리게 말했다.
|
|
72
|
+
*
|
|
73
|
+
* · **DEEL_NO_MOTION 을 통째로 안 봤다.** 그게 켜져 있으면 무엇을 골라도
|
|
74
|
+
* 한 칸짜리 돌림표가 도는데, 화면은 「기사로 바꿨습니다 · 그 자리에서
|
|
75
|
+
* 바뀝니다」 라고 말한다. 아무것도 안 바뀐다.
|
|
76
|
+
* · **모르는 값도 이긴다고 말했다.** `DEEL_MOTION=cat` 은 무시되고 설정
|
|
77
|
+
* 파일이 그대로 쓰이는데, 「환경변수가 이깁니다」 라고 적혔다. 사람은
|
|
78
|
+
* 설정을 바꿔도 소용없다고 믿는다 — 사실은 그 설정이 이기고 있는데.
|
|
79
|
+
*/
|
|
80
|
+
function 환경이하는말() {
|
|
81
|
+
const 말들 = [];
|
|
82
|
+
const 그림 = 환경그림();
|
|
83
|
+
if (그림?.이김 || process.env.DEEL_OFFICE) 말들.push(c.gray(말('motion.envWins')));
|
|
84
|
+
if (그림 && !그림.이김) 말들.push(c.gray(말('motion.envUnknown', { 값: 그림.값 })));
|
|
85
|
+
if (환경으로껐나()) 말들.push(c.gray(말('motion.envOff')));
|
|
86
|
+
return 말들;
|
|
87
|
+
}
|
|
66
88
|
import { PROFILES, LEVELS as THINK_LEVELS, normalizeProfile, table as effortTable, 가벼운강도, shiftLevel } from './agent/effort.js';
|
|
67
89
|
import { 전선붙이기, 전선말 } from './backend/wire.js';
|
|
68
90
|
import { scanLocal, toProfiles } from './backend/scan.js';
|
|
@@ -607,10 +629,8 @@ export async function handle(line, session, ctx) {
|
|
|
607
629
|
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
|
|
608
630
|
say('');
|
|
609
631
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
say('');
|
|
613
|
-
}
|
|
632
|
+
const 환경말 = 환경이하는말();
|
|
633
|
+
if (환경말.length) { for (const 줄 of 환경말) say(` ${줄}`); say(''); }
|
|
614
634
|
return { handled: true };
|
|
615
635
|
}
|
|
616
636
|
|
|
@@ -652,10 +672,17 @@ export async function handle(line, session, ctx) {
|
|
|
652
672
|
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.appleTerminal'))}`);
|
|
653
673
|
}
|
|
654
674
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
675
|
+
for (const 줄 of 환경이하는말()) say(` ${c.yellow(mark.warn)} ${줄}`);
|
|
676
|
+
/*
|
|
677
|
+
* 「그 자리에서 바뀝니다」 는 **정말로 바뀔 때만** 적는다.
|
|
678
|
+
*
|
|
679
|
+
* DEEL_NO_MOTION 이 켜져 있으면 무엇을 골라도 한 칸짜리 돌림표가 돈다.
|
|
680
|
+
* 그런데 이 자리는 그걸 안 보고 늘 「바뀝니다」 를 적고 있었다. 바로
|
|
681
|
+
* 위에서 사무실이 작은 터미널에 안 뜬다고 굳이 말해 주는 것과 같은
|
|
682
|
+
* 까닭인데(「켰다고 말해 놓고 안 보이면 그게 고장이다」), 정작 같은
|
|
683
|
+
* 일이 환경변수로 일어날 때는 아무 말도 안 했다.
|
|
684
|
+
*/
|
|
685
|
+
if (!환경으로껐나()) say(` ${c.gray(말('motion.appliesNow'))}`);
|
|
659
686
|
say('');
|
|
660
687
|
return { handled: true };
|
|
661
688
|
}
|
|
@@ -929,9 +956,40 @@ export async function handle(line, session, ctx) {
|
|
|
929
956
|
// 사람도 되돌아간 줄 모른다. **되돌아간 것만** 뺀다 — 못 되돌린 파일을
|
|
930
957
|
// 여기서 빼면 진짜로 바뀌어 있는 파일이 /diff 에서도 사라진다.
|
|
931
958
|
for (const f of r.restored) if (f.ok === true) session.changes.delete(f.path);
|
|
959
|
+
/*
|
|
960
|
+
* ── 되돌리기는 됐는데 **이력을 못 지웠으면** 그것도 말한다 ──────────
|
|
961
|
+
*
|
|
962
|
+
* 파일은 이미 되돌아가 있다. 그런데 그 턴이 이력에 그대로 남으면,
|
|
963
|
+
* 사람이 /undo 를 한 번 더 쳤을 때 **같은 턴**이 또 되돌아간다.
|
|
964
|
+
* 두 턴을 되돌린 줄 알지만 한 턴이다 (safety/undo.js 의 이력줄임).
|
|
965
|
+
*/
|
|
966
|
+
if (r.이력줄임 && r.이력줄임.ok === false) {
|
|
967
|
+
say(` ${mark.warn} ${c.gray('파일은 되돌렸지만 되돌리기 기록을 못 지웠습니다')} ${c.gray(`— ${r.이력줄임.왜}`)}`);
|
|
968
|
+
say(` ${c.gray('그래서 이 턴이 기록에 그대로 남아 있습니다. 여기서')} ${c.cyan('/undo')} ${c.gray('를 또 치면 같은 턴을 또 되돌립니다.')}`);
|
|
969
|
+
}
|
|
970
|
+
/*
|
|
971
|
+
* 깨진 줄은 **되돌릴 수 없는 파일**이다.
|
|
972
|
+
*
|
|
973
|
+
* 이력 한 줄이 곧 한 파일의 원문이라, 줄이 깨졌다는 것은 그 파일을
|
|
974
|
+
* 되돌릴 길이 사라졌다는 뜻이다. 안 말하면 「파일 3개를 되돌렸습니다」
|
|
975
|
+
* 만 남고 넷째가 왜 안 돌아왔는지는 아무 데도 안 적힌다.
|
|
976
|
+
*/
|
|
977
|
+
if (r.깨진줄) {
|
|
978
|
+
say(` ${mark.warn} ${c.gray(`되돌리기 기록에서 ${r.깨진줄}줄을 못 읽었습니다 — 그 줄에 걸린 파일은 되돌릴 길이 없습니다.`)}`);
|
|
979
|
+
}
|
|
980
|
+
|
|
932
981
|
// 상태줄의 '되돌릴 턴' 도 여기서 줄여 준다. 안 줄이면 방금 되돌린 것을
|
|
933
982
|
// 아직 되돌릴 수 있는 것처럼 세고 있게 된다.
|
|
934
|
-
|
|
983
|
+
/*
|
|
984
|
+
* 못 세면 **모른다고 말한다.** 그냥 두면 방금 되돌린 턴이 상태줄에
|
|
985
|
+
* 그대로 남아, 아직 되돌릴 수 있는 것처럼 보인다 (repl.js 에서 같은
|
|
986
|
+
* 자리를 이미 고쳤다 — 안전망 숫자가 조용히 틀리면 안 된다).
|
|
987
|
+
*/
|
|
988
|
+
try { session.되돌릴턴 = ctx.history.turns().length; }
|
|
989
|
+
catch (e) {
|
|
990
|
+
session.되돌릴턴 = null;
|
|
991
|
+
say(` ${mark.warn} ${c.gray(`되돌릴 턴이 몇 개 남았는지 못 셌습니다 — ${e?.message ?? e}`)}`);
|
|
992
|
+
}
|
|
935
993
|
|
|
936
994
|
const 되감음 = session.되감기(r.turnIds ?? []);
|
|
937
995
|
if (되감음.걷은것) {
|
|
@@ -1037,6 +1095,22 @@ export async function handle(line, session, ctx) {
|
|
|
1037
1095
|
// 서버가 잠깐 막아 다시 부른 횟수. 0 이면 안 적는다 — 없는 일을 줄로 남기면 표만 길어진다.
|
|
1038
1096
|
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${말('unit.calls', { n: session.usage.retries })}`);
|
|
1039
1097
|
say(` ${c.gray(pad(말('cost.elapsed'), 14))} ${말('unit.min', { n: mins })}`);
|
|
1098
|
+
/*
|
|
1099
|
+
* ── 갈래를 쓰면 이 위의 숫자는 **한 갈래 것**이다 ────────────────
|
|
1100
|
+
*
|
|
1101
|
+
* session.usage 는 지금 갈래의 셈이다. 갈래를 셋 굴리면 화면의
|
|
1102
|
+
* 금액은 그중 하나 몫이고, 갈래를 바꾸는 것만으로 숫자가 내려간다 —
|
|
1103
|
+
* 돈은 그대로 나가는데. 닫은 갈래 것은 아예 안 보였다.
|
|
1104
|
+
*
|
|
1105
|
+
* 위 줄들을 갈래 합으로 바꾸지는 않는다. 「이 갈래에서 뭘 했나」 도
|
|
1106
|
+
* 봐야 하는 값이라서다. 대신 **다를 때만** 한 줄을 더 붙인다.
|
|
1107
|
+
*/
|
|
1108
|
+
const 갈래셈 = ctx?.갈래?.갈래여럿인가?.() ? ctx.갈래.전체usage() : null;
|
|
1109
|
+
if (갈래셈) {
|
|
1110
|
+
const 갈래수 = (ctx.갈래.개수?.() ?? 1) + (ctx.갈래.닫힌수 ?? 0);
|
|
1111
|
+
say(` ${c.gray(pad('갈래 전부', 14))} ${c.gray(`${갈래수}갈래 · 호출 ${갈래셈.calls.toLocaleString()} · 입력 ${갈래셈.in.toLocaleString()} · 출력 ${갈래셈.out.toLocaleString()}`)}`);
|
|
1112
|
+
say(` ${c.gray(pad('', 14))} ${c.gray('위 숫자는 지금 갈래 것입니다.')}`);
|
|
1113
|
+
}
|
|
1040
1114
|
/*
|
|
1041
1115
|
* 돈.
|
|
1042
1116
|
*
|
|
@@ -1153,6 +1227,29 @@ export async function handle(line, session, ctx) {
|
|
|
1153
1227
|
이름: session.규칙못읽음.이름, 까닭: session.규칙못읽음.까닭,
|
|
1154
1228
|
}))}`);
|
|
1155
1229
|
}
|
|
1230
|
+
/*
|
|
1231
|
+
* 반만 실린 것도 같은 자리에서 말한다.
|
|
1232
|
+
*
|
|
1233
|
+
* 이름만 적으면 사람은 규칙이 통째로 걸려 있다고 믿는다. 2만 자를
|
|
1234
|
+
* 넘긴 규칙 파일의 뒷부분은 모델에게 한 글자도 안 간다 — 「운영 DB 는
|
|
1235
|
+
* 건드리지 마라」 가 하필 그 뒤에 있으면 그게 제일 나쁜 모양이다.
|
|
1236
|
+
*/
|
|
1237
|
+
if (session.규칙잘림) {
|
|
1238
|
+
const j = session.규칙잘림;
|
|
1239
|
+
say(` ${c.gray(pad('', 10))} ${mark.warn} ${c.yellow(`${j.이름} 은 ${j.원본.toLocaleString()}자라 앞 ${j.실린.toLocaleString()}자만 실렸습니다`)}`);
|
|
1240
|
+
say(` ${c.gray(pad('', 10))} ${c.gray('그 뒤에 적은 규칙은 모델에게 안 갑니다. 중요한 것을 앞으로 옮기세요.')}`);
|
|
1241
|
+
}
|
|
1242
|
+
/*
|
|
1243
|
+
* 도구 정의를 못 쟀으면 그것도 적는다.
|
|
1244
|
+
*
|
|
1245
|
+
* 도구 정의는 매 요청에 통째로 실려 나간다. 못 재면 /context 는 그만큼
|
|
1246
|
+
* 빈자리가 있다고 말하고, 그 값으로 출력 상한이 잡혀 답이 이유 없이
|
|
1247
|
+
* 짧아진다. MCP 서버가 이상한 스키마를 주면 실제로 나는 일이다.
|
|
1248
|
+
*/
|
|
1249
|
+
if (session.도구못쟀나) {
|
|
1250
|
+
say(` ${c.gray(pad('', 10))} ${mark.warn} ${c.yellow(`도구 정의 크기를 못 쟀습니다 — ${String(session.도구못쟀나).slice(0, 60)}`)}`);
|
|
1251
|
+
say(` ${c.gray(pad('', 10))} ${c.gray('/context 의 도구 칸이 0 으로 나오고, 답이 이유 없이 짧아질 수 있습니다.')}`);
|
|
1252
|
+
}
|
|
1156
1253
|
const caps = [
|
|
1157
1254
|
k.tools ? c.green(말('status.capTools')) : c.red(말('status.capTools')),
|
|
1158
1255
|
k.streaming ? c.green(말('status.capStream')) : c.gray(말('status.capStream')),
|
|
@@ -2812,6 +2909,23 @@ function showContext(session) {
|
|
|
2812
2909
|
} else {
|
|
2813
2910
|
say(` ${c.gray(말('ctx.estimate'))}`);
|
|
2814
2911
|
}
|
|
2912
|
+
/*
|
|
2913
|
+
* 재 봤는데 **못 믿어서 안 쓴** 것도 말한다.
|
|
2914
|
+
*
|
|
2915
|
+
* session.배운다() 는 서버가 알려 준 값이 우리 추정의 절반 아래거나 두 배
|
|
2916
|
+
* 위면 표본을 버린다. 튄 값 하나에 게이지가 휘둘리면 안 되니 버리는 것은
|
|
2917
|
+
* 맞다. 그런데 버린 것을 아무 데도 안 남기면, 「아직 한 번도 못 재 봤다」 와
|
|
2918
|
+
* 「재 봤는데 세 배가 나와서 못 믿었다」 가 화면에서 똑같아진다.
|
|
2919
|
+
*
|
|
2920
|
+
* 뒤엣것은 우리 추정이 크게 틀렸다는 단서다. 그 말을 안 하면 게이지는 영영
|
|
2921
|
+
* 짐작인 채로 도는데 사람은 그저 「추정입니다」 만 읽는다.
|
|
2922
|
+
*/
|
|
2923
|
+
if (session?.보정버림 > 0) {
|
|
2924
|
+
const 배 = session.보정마지막버린비율;
|
|
2925
|
+
say(` ${c.gray(`서버가 알려 준 값이 우리 추정과 너무 달라 ${session.보정버림}번은 안 썼습니다`
|
|
2926
|
+
+ (배 ? ` (마지막에 잰 것은 추정의 ${배.toFixed(1)}배)` : ''))}`);
|
|
2927
|
+
say(` ${c.gray('이 창구에서는 위 숫자가 실제와 크게 다를 수 있습니다.')}`);
|
|
2928
|
+
}
|
|
2815
2929
|
say('');
|
|
2816
2930
|
}
|
|
2817
2931
|
|
package/src/i18n/en.js
CHANGED
|
@@ -240,6 +240,8 @@ export const en = {
|
|
|
240
240
|
'motion.unknown': '"{값}" is not one of the drawings.',
|
|
241
241
|
'motion.pickOne': 'Pick one of:',
|
|
242
242
|
'motion.envWins': 'DEEL_MOTION / DEEL_OFFICE is set, and the environment wins.',
|
|
243
|
+
'motion.envUnknown': 'DEEL_MOTION is set to "{값}", which is not a name we know — it was ignored, and your saved choice is what applies.',
|
|
244
|
+
'motion.envOff': 'DEEL_NO_MOTION is set, so nothing is drawn. Your choice is saved and takes effect once you clear that variable.',
|
|
243
245
|
'motion.tooSmall': 'This terminal is {줄} x {칸} — the office needs at least {최소줄} rows and {최소칸} columns.',
|
|
244
246
|
'motion.appleTerminal': "macOS Terminal spaces lines apart, so the room gets horizontal seams — Terminal › Settings › Profiles › Text › Font \"Change\" › set Line Spacing to 1.0. (iTerm2 defaults to 1.0, which is why it looks right there)",
|
|
245
247
|
|
|
@@ -575,7 +577,7 @@ export const en = {
|
|
|
575
577
|
'ctx.skills': 'Skill list ({실림}/{전체})',
|
|
576
578
|
'ctx.tools': 'Tool definitions',
|
|
577
579
|
'ctx.history': 'Conversation',
|
|
578
|
-
'ctx.toolResults': 'Tool results ({n}
|
|
580
|
+
'ctx.toolResults': 'Tool results ({n})',
|
|
579
581
|
'ctx.left': 'Left',
|
|
580
582
|
'ctx.compactHint': 'shrink the conversation',
|
|
581
583
|
'ctx.clearHint': 'clear it all',
|
package/src/i18n/ja.js
CHANGED
|
@@ -213,6 +213,8 @@ export const ja = {
|
|
|
213
213
|
'motion.unknown': '"{값}" はその中にありません。',
|
|
214
214
|
'motion.pickOne': 'この中から選んでください:',
|
|
215
215
|
'motion.envWins': 'DEEL_MOTION / DEEL_OFFICE が設定されていて、そちらが優先されます。',
|
|
216
|
+
'motion.envUnknown': 'DEEL_MOTION の "{값}" は知らない名前なので無視しました — 設定で選んだものがそのまま使われます。',
|
|
217
|
+
'motion.envOff': 'DEEL_NO_MOTION が設定されているので何も描きません。選んだものは保存済みで、その環境変数を消せば効きます。',
|
|
216
218
|
'motion.tooSmall': 'この端末は {줄} x {칸} です — オフィスには少なくとも {최소줄} 行 {최소칸} 桁が要ります。',
|
|
217
219
|
'motion.appleTerminal': 'macOS の Terminal は行間を空けるので部屋に横筋が入ります — Terminal › 設定 › プロファイル › テキスト › フォント「変更」› 行間を 1.0 に。(iTerm2 は既定が 1.0 なので、あちらでは綺麗に出ます)',
|
|
218
220
|
'lang.now': '画面の言葉は {이름} です。',
|
|
@@ -529,7 +531,7 @@ export const ja = {
|
|
|
529
531
|
'ctx.skills': 'スキル一覧 ({실림}/{전체})',
|
|
530
532
|
'ctx.tools': '道具の定義',
|
|
531
533
|
'ctx.history': '会話の履歴',
|
|
532
|
-
'ctx.toolResults': '道具の結果 (
|
|
534
|
+
'ctx.toolResults': '道具の結果 ({n}件)',
|
|
533
535
|
'ctx.left': '残り',
|
|
534
536
|
'ctx.compactHint': '会話を畳む',
|
|
535
537
|
'ctx.clearHint': 'まるごと消す',
|
package/src/i18n/ko.js
CHANGED
|
@@ -229,6 +229,8 @@ export const ko = {
|
|
|
229
229
|
'motion.unknown': '"{값}"{~값:은는} 모르는 그림입니다.',
|
|
230
230
|
'motion.pickOne': '고를 수 있는 것:',
|
|
231
231
|
'motion.envWins': '환경변수(DEEL_MOTION·DEEL_OFFICE)가 켜져 있어 그쪽이 이깁니다.',
|
|
232
|
+
'motion.envUnknown': 'DEEL_MOTION 에 적힌 "{값}" — 모르는 이름이라 무시했습니다. 설정에 고른 것이 그대로 쓰입니다.',
|
|
233
|
+
'motion.envOff': 'DEEL_NO_MOTION 이 켜져 있어 그림은 안 돕니다. 고른 것은 적어 뒀고, 그 환경변수를 치우면 그때 살아납니다.',
|
|
232
234
|
'motion.tooSmall': '이 터미널은 {줄}줄 × {칸}칸입니다 — 사무실은 {최소줄}줄 × {최소칸}칸부터 뜹니다.',
|
|
233
235
|
'motion.appleTerminal': '맥 기본 터미널은 줄 간격이 넓어 방에 가로줄이 그어집니다 — 터미널 › 설정 › 프로파일 › 텍스트 › 글꼴 「변경」 › 행간을 1.0 으로. (iTerm2 는 기본이 1.0 이라 멀쩡합니다)',
|
|
234
236
|
|
|
@@ -622,7 +624,7 @@ export const ko = {
|
|
|
622
624
|
'ctx.skills': '스킬 목록 ({실림}/{전체}개)',
|
|
623
625
|
'ctx.tools': '도구 정의',
|
|
624
626
|
'ctx.history': '대화 이력',
|
|
625
|
-
'ctx.toolResults': '도구 결과 (
|
|
627
|
+
'ctx.toolResults': '도구 결과 ({n}개)',
|
|
626
628
|
'ctx.left': '남음',
|
|
627
629
|
'ctx.compactHint': '대화 줄이기',
|
|
628
630
|
'ctx.clearHint': '통째로 비우기',
|
package/src/i18n/zh.js
CHANGED
|
@@ -213,6 +213,8 @@ export const zh = {
|
|
|
213
213
|
'motion.unknown': '"{값}" 不在其中。',
|
|
214
214
|
'motion.pickOne': '从这些里选一个:',
|
|
215
215
|
'motion.envWins': '设置了 DEEL_MOTION / DEEL_OFFICE,环境变量优先。',
|
|
216
|
+
'motion.envUnknown': 'DEEL_MOTION 里的 "{값}" 不是我们认识的名字,已忽略 — 实际生效的是配置里选的那个。',
|
|
217
|
+
'motion.envOff': '设置了 DEEL_NO_MOTION,所以什么都不画。选择已保存,清掉那个环境变量后才会生效。',
|
|
216
218
|
'motion.tooSmall': '这个终端是 {줄} x {칸} — 办公室至少要 {최소줄} 行 {최소칸} 列。',
|
|
217
219
|
'motion.appleTerminal': 'macOS 的 Terminal 会拉开行距,房间里会出现横缝 — Terminal › 设置 › 描述文件 › 文本 › 字体「更改」› 把行距设为 1.0。(iTerm2 默认就是 1.0,所以那边看着正常)',
|
|
218
220
|
'lang.now': '屏幕语言是 {이름}。',
|
|
@@ -529,7 +531,7 @@ export const zh = {
|
|
|
529
531
|
'ctx.skills': '技能列表 ({실림}/{전체})',
|
|
530
532
|
'ctx.tools': '工具定义',
|
|
531
533
|
'ctx.history': '对话历史',
|
|
532
|
-
'ctx.toolResults': '工具结果 (
|
|
534
|
+
'ctx.toolResults': '工具结果 ({n}个)',
|
|
533
535
|
'ctx.left': '剩余',
|
|
534
536
|
'ctx.compactHint': '折叠对话',
|
|
535
537
|
'ctx.clearHint': '全部清空',
|
package/src/repl.js
CHANGED
|
@@ -455,6 +455,8 @@ export async function chatLoop(opts = {}) {
|
|
|
455
455
|
// 되돌리기 이력을 못 읽는다는 말은 한 판에 한 번만 한다 — 턴마다 세는 자리라
|
|
456
456
|
// 안 막으면 같은 줄이 턴 수만큼 쌓인다.
|
|
457
457
|
let 되돌리기못읽음말했나 = false;
|
|
458
|
+
// 이력이 커졌는데 못 줄인 것·깨진 줄. 둘 다 한 판에 한 번만 말한다.
|
|
459
|
+
let 이력탈말했나 = false;
|
|
458
460
|
/*
|
|
459
461
|
* 정리가 실패해도 켜는 것은 막지 않는다. 다만 **삼키지는 않는다.**
|
|
460
462
|
* 계속 실패하면 .deel/sessions 가 끝없이 커지는데 화면에는 영영 안 떴다.
|
|
@@ -2335,6 +2337,21 @@ export async function chatLoop(opts = {}) {
|
|
|
2335
2337
|
const 것들 = ev.result.되돌릴것;
|
|
2336
2338
|
say(` ${c.gray(`↩ ${것들.slice(0, 3).join(' · ')}${것들.length > 3 ? ` 외 ${것들.length - 3}개` : ''} 는 떠 뒀습니다 — /undo 로 되돌아갑니다`)}`);
|
|
2337
2339
|
}
|
|
2340
|
+
/*
|
|
2341
|
+
* ── 못 뜬 것은 **되돌아가지 않는다** ──────────────────────
|
|
2342
|
+
*
|
|
2343
|
+
* 위 한 줄만 있을 때, `rm 로고.png` 는 「로고.png 는 떠 뒀습니다
|
|
2344
|
+
* — /undo 로 되돌아갑니다」 를 찍었다. 그림·xlsx·zip 은 내용을
|
|
2345
|
+
* 못 떠서 /undo 가 손도 안 대는데도 그랬다. 파일은 이미 없고,
|
|
2346
|
+
* 안전망이 있다는 말만 남는다.
|
|
2347
|
+
*/
|
|
2348
|
+
if (ev.result?.못뜬것?.length) {
|
|
2349
|
+
const 못 = ev.result.못뜬것;
|
|
2350
|
+
say(` ${mark.warn} ${c.gray(`${못.slice(0, 3).join(' · ')}${못.length > 3 ? ` 외 ${못.length - 3}개` : ''} 는 못 떠 뒀습니다 — /undo 로 안 돌아옵니다`)}`);
|
|
2351
|
+
}
|
|
2352
|
+
if (ev.result?.스냅샷상한걸림) {
|
|
2353
|
+
say(` ${mark.warn} ${c.gray(`대상이 많아 앞 ${ev.result.되돌릴것?.length ?? 0}개만 떠 뒀습니다 — 나머지는 /undo 로 안 돌아옵니다`)}`);
|
|
2354
|
+
}
|
|
2338
2355
|
// 파일을 고쳤으면 무엇이 바뀌었는지 바로 보여 준다.
|
|
2339
2356
|
//
|
|
2340
2357
|
// auto 모드는 안 물어보고 고친다. 여기서 안 보여주면 사람이
|
|
@@ -2383,6 +2400,20 @@ export async function chatLoop(opts = {}) {
|
|
|
2383
2400
|
* 줄 수는 0 이다 — 옮긴 것이지 고친 것이 아니다.
|
|
2384
2401
|
*/
|
|
2385
2402
|
for (const f of ev.result.바뀐것들) session.noteChange(f, { added: 0, removed: 0 });
|
|
2403
|
+
} else if (ev.result?.changed) {
|
|
2404
|
+
/*
|
|
2405
|
+
* ── 바뀐 **경로만** 주는 도구가 있다 ─────────────────────
|
|
2406
|
+
*
|
|
2407
|
+
* 파일 하나짜리 Move 와 hwpx Write 가 그렇다. 옮긴 것·새로 만든
|
|
2408
|
+
* 문서라 「몇 줄 바뀌었나」 가 없다. 그런데 위 세 갈래가 전부
|
|
2409
|
+
* diff·여럿·바뀐것들 을 보므로 셋 다 안 걸리고, 그러면
|
|
2410
|
+
* noteChange 가 **아예 안 불린다.**
|
|
2411
|
+
*
|
|
2412
|
+
* 파일은 진짜로 옮겨졌는데 /diff 목록에 없고, 상태줄 ✎ 에도 안
|
|
2413
|
+
* 들고, /commit 이 담는 목록에도 없다. 커밋은 초록으로 끝나고
|
|
2414
|
+
* 옮긴 파일만 안 담긴다 — 나중에 원격에서만 깨진다.
|
|
2415
|
+
*/
|
|
2416
|
+
session.noteChange(ev.result.changed, { added: 0, removed: 0 });
|
|
2386
2417
|
}
|
|
2387
2418
|
}
|
|
2388
2419
|
flush(); // 도구가 하나 끝날 때마다 적어 둔다
|
|
@@ -2899,6 +2930,27 @@ export async function chatLoop(opts = {}) {
|
|
|
2899
2930
|
*/
|
|
2900
2931
|
try {
|
|
2901
2932
|
session.되돌릴턴 = ctx.history.turns().length;
|
|
2933
|
+
/*
|
|
2934
|
+
* ── 안전망이 **제 무게로 무너지는** 것도 여기서 잡힌다 ──────────
|
|
2935
|
+
*
|
|
2936
|
+
* 되돌리기 이력은 파일 내용을 통째로 담아서 금방 수십 MB 가 된다.
|
|
2937
|
+
* 32MB 를 넘으면 오래된 턴부터 버리는데, 그 버리기가 실패하면
|
|
2938
|
+
* (읽기 전용·디스크 참) 이력은 끝없이 자라고 화면에는 영영 안 떴다.
|
|
2939
|
+
* 실패는 대개 이어지는 종류라 저절로 낫지도 않는다.
|
|
2940
|
+
*
|
|
2941
|
+
* 깨진 줄도 같다. 이력 한 줄이 곧 한 파일의 원문이라, 줄이 깨졌다는
|
|
2942
|
+
* 것은 그 파일을 되돌릴 길이 사라졌다는 뜻이다. 상태줄의 `↩ 3` 은
|
|
2943
|
+
* 그 셋이 다 되돌아간다는 말인데, 깨진 줄이 있으면 그게 아니다.
|
|
2944
|
+
*/
|
|
2945
|
+
if (!이력탈말했나 && (ctx.history.줄이기못함 || ctx.history.깨진줄)) {
|
|
2946
|
+
이력탈말했나 = true;
|
|
2947
|
+
if (ctx.history.줄이기못함) {
|
|
2948
|
+
say(` ${mark.warn} ${c.gray('되돌리기 이력이 커졌는데 못 줄였습니다')} ${c.gray('— ' + clip(String(ctx.history.줄이기못함), 60))}`);
|
|
2949
|
+
}
|
|
2950
|
+
if (ctx.history.깨진줄) {
|
|
2951
|
+
say(` ${mark.warn} ${c.gray(`되돌리기 이력에서 ${ctx.history.깨진줄}줄을 못 읽었습니다 — 그만큼은 ${'/undo'} 로 안 돌아옵니다.`)}`);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2902
2954
|
} catch (e) {
|
|
2903
2955
|
session.되돌릴턴 = 0;
|
|
2904
2956
|
if (!되돌리기못읽음말했나) {
|
package/src/safety/undo.js
CHANGED
|
@@ -122,18 +122,54 @@ export class History {
|
|
|
122
122
|
#maybePrune() {
|
|
123
123
|
this.#writes = (this.#writes ?? 0) + 1;
|
|
124
124
|
if (this.#writes % 20 !== 0) return;
|
|
125
|
+
// 앞서 못 줄였으면 한참 쉬었다가 다시 본다 (prune 의 주석).
|
|
126
|
+
if (this.#writes < this.#다음줄이기) return;
|
|
125
127
|
try {
|
|
126
128
|
if (statSync(this.file).size < MAX_BYTES) return;
|
|
127
129
|
} catch { return; }
|
|
128
|
-
|
|
130
|
+
/*
|
|
131
|
+
* 줄이다 터져도 **고치던 파일까지 말려들면 안 된다.**
|
|
132
|
+
*
|
|
133
|
+
* 이 함수는 snapshot() 안에서 돈다 — 즉 Write·Edit 이 파일을 쓰기
|
|
134
|
+
* 직전이다. prune() 이 이력을 읽다 던지면(EACCES·EBUSY) 그 예외가
|
|
135
|
+
* 도구 밖으로 새어 나가, 사람은 고치려던 파일과 아무 상관 없는
|
|
136
|
+
* 「EACCES: …/edits.jsonl」 을 보게 된다. 이력을 못 줄인 것은
|
|
137
|
+
* 위쪽(줄이기못함)에 남기고, 고치는 일은 그대로 이어 간다.
|
|
138
|
+
*/
|
|
139
|
+
try { this.prune(); }
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.줄이기못함 = err?.message ?? String(err);
|
|
142
|
+
this.#다음줄이기 = this.#writes + 500;
|
|
143
|
+
}
|
|
129
144
|
}
|
|
130
145
|
|
|
146
|
+
#다음줄이기 = 0;
|
|
147
|
+
/** 이력을 못 줄였으면 그 까닭. 못 줄이면 안전망이 제 무게로 무너진다. */
|
|
148
|
+
줄이기못함 = null;
|
|
149
|
+
|
|
131
150
|
#writes = 0;
|
|
132
151
|
|
|
133
152
|
/** 최근 keep 개 턴만 남기고 자른다. 버린 줄 수를 돌려준다. */
|
|
134
153
|
prune({ keep = KEEP_TURNS } = {}) {
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
/*
|
|
155
|
+
* ── **읽다 터지는 것**도 못 줄인 것이다 ────────────────────────────
|
|
156
|
+
*
|
|
157
|
+
* 아래 쓰기만 감싸 두고 여기 읽기는 맨몸이었다. 그런데 이력을 못 읽는
|
|
158
|
+
* 판(권한·잠김·자리가 폴더로 바뀜)은 못 쓰는 판만큼이나 흔하고, 그때
|
|
159
|
+
* 예외가 여기를 뚫고 나가면 부르는 쪽 — 파일을 쓰던 Write·Edit — 이
|
|
160
|
+
* 고치려던 파일과 아무 상관 없는 오류를 뒤집어쓴다.
|
|
161
|
+
*
|
|
162
|
+
* 못 줄인 것은 못 줄였다고 위쪽(줄이기못함)에 남기고 0을 돌려준다.
|
|
163
|
+
* 그게 이 함수가 실패를 말하는 유일한 방식이라 여기서도 같아야 한다.
|
|
164
|
+
*/
|
|
165
|
+
let recs;
|
|
166
|
+
let turns;
|
|
167
|
+
try { recs = this.all(); turns = this.turns(); }
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.줄이기못함 = err?.message ?? String(err);
|
|
170
|
+
this.#다음줄이기 = (this.#writes ?? 0) + 500;
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
137
173
|
if (turns.length <= keep) return 0;
|
|
138
174
|
const 남길턴 = new Set(turns.slice(-keep));
|
|
139
175
|
const 남길것 = recs.filter((r) => 남길턴.has(r.turn));
|
|
@@ -143,7 +179,22 @@ export class History {
|
|
|
143
179
|
writeFileSync(this.file, 남길것.map((r) => JSON.stringify(r)).join('\n') + (남길것.length ? '\n' : ''), 'utf8');
|
|
144
180
|
// 통째로 다시 쓰면 새 파일일 수 있다 — 빗장을 다시 건다 (agent/store.js 도 같다).
|
|
145
181
|
this.#잠갔나 = false; this.#잠그기();
|
|
146
|
-
|
|
182
|
+
this.줄이기못함 = null;
|
|
183
|
+
} catch (err) {
|
|
184
|
+
/*
|
|
185
|
+
* 못 줄인 것을 **0개 버렸다**와 같은 값으로 돌려주고 있었다.
|
|
186
|
+
*
|
|
187
|
+
* 부르는 쪽은 둘을 구별할 길이 없다. 그리고 실패는 대개 이어진다 —
|
|
188
|
+
* 읽기 전용이거나 디스크가 찬 것이라 다음 번에도 같다. 그러면 이력은
|
|
189
|
+
* 32MB 를 넘긴 채 끝없이 자라는데 화면에는 영영 안 뜬다.
|
|
190
|
+
*
|
|
191
|
+
* 곧바로 또 시도하지도 않는다. 32MB 짜리 파일을 스무 번 쓸 때마다
|
|
192
|
+
* 통째로 다시 읽는 셈이라, 실패가 이어지면 그 자체가 느려지는 원인이 된다.
|
|
193
|
+
*/
|
|
194
|
+
this.줄이기못함 = err?.message ?? String(err);
|
|
195
|
+
this.#다음줄이기 = (this.#writes ?? 0) + 500;
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
147
198
|
return 버린수;
|
|
148
199
|
}
|
|
149
200
|
|
|
@@ -152,12 +203,30 @@ export class History {
|
|
|
152
203
|
try { return statSync(this.file).size; } catch { return 0; }
|
|
153
204
|
}
|
|
154
205
|
|
|
206
|
+
/*
|
|
207
|
+
* ── 깨진 줄은 **되돌릴 수 없는 파일**이다 ────────────────────────────
|
|
208
|
+
*
|
|
209
|
+
* 여태 `catch { return null }` 로 조용히 버렸다. 그런데 이 파일의 한 줄은
|
|
210
|
+
* 곧 한 파일의 원문이다 — 줄이 깨졌다는 것은 그 파일을 되돌릴 길이
|
|
211
|
+
* 사라졌다는 뜻이다. 그걸 안 세면 `/undo` 는 「파일 3개를 되돌렸습니다」
|
|
212
|
+
* 라고만 하고, 넷째 파일이 왜 안 돌아왔는지는 아무 데도 안 남는다.
|
|
213
|
+
*
|
|
214
|
+
* 줄은 실제로 깨진다. 쓰다 죽으면 마지막 줄이 반만 적히고, 같은 폴더에서
|
|
215
|
+
* 창을 둘 띄워 놓으면 큰 줄 둘이 서로 끼어든다(appendFileSync 는 긴 줄을
|
|
216
|
+
* 통째로 보장하지 않는다).
|
|
217
|
+
*/
|
|
218
|
+
/** 마지막으로 읽을 때 깨져 있던 줄 수. 0 이 아니면 그만큼 못 되돌린다. */
|
|
219
|
+
깨진줄 = 0;
|
|
220
|
+
|
|
155
221
|
all() {
|
|
156
222
|
if (!existsSync(this.file)) return [];
|
|
157
|
-
|
|
223
|
+
let 깨진 = 0;
|
|
224
|
+
const 것들 = readFileSync(this.file, 'utf8')
|
|
158
225
|
.split('\n').filter(Boolean)
|
|
159
|
-
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
226
|
+
.map((l) => { try { return JSON.parse(l); } catch { 깨진 += 1; return null; } })
|
|
160
227
|
.filter(Boolean);
|
|
228
|
+
this.깨진줄 = 깨진;
|
|
229
|
+
return 것들;
|
|
161
230
|
}
|
|
162
231
|
|
|
163
232
|
turns() {
|
|
@@ -232,10 +301,31 @@ export class History {
|
|
|
232
301
|
*/
|
|
233
302
|
const 못한것 = new Set(restored.filter((x) => x.ok === false).map((x) => x.path));
|
|
234
303
|
const keep = recs.filter((r) => !turns.includes(r.turn) || 못한것.has(r.path));
|
|
235
|
-
|
|
236
|
-
|
|
304
|
+
/*
|
|
305
|
+
* ── 여기가 터지면 **파일은 이미 되돌아가 있다** ────────────────────
|
|
306
|
+
*
|
|
307
|
+
* 감싸지 않은 쓰기였다. 던지면 위에서 받아 「명령을 처리하다 막혔습니다」
|
|
308
|
+
* 한 줄만 남는다 — 그런데 디스크의 파일들은 방금 되돌아갔다. 화면이
|
|
309
|
+
* 실패라고 말하는데 실제로는 성공한, 이 저장소가 계속 잡아 온 어긋남이
|
|
310
|
+
* 방향만 뒤집힌 꼴이다.
|
|
311
|
+
*
|
|
312
|
+
* 게다가 이력을 못 줄였으니 그 턴이 그대로 남는다. 사람이 「실패했으니
|
|
313
|
+
* 다시」 하고 /undo 를 또 치면 **같은 턴**을 또 되돌린다 — 두 턴을
|
|
314
|
+
* 되돌린 줄 알지만 한 턴이다.
|
|
315
|
+
*/
|
|
316
|
+
let 이력줄임 = { ok: true };
|
|
317
|
+
try {
|
|
318
|
+
writeFileSync(this.file, keep.map((r) => JSON.stringify(r)).join('\n') + (keep.length ? '\n' : ''), 'utf8');
|
|
319
|
+
this.#잠갔나 = false; this.#잠그기();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
이력줄임 = { ok: false, 왜: err?.message ?? String(err) };
|
|
322
|
+
}
|
|
237
323
|
return {
|
|
238
324
|
restored,
|
|
325
|
+
// 이력에서 그 턴을 지웠나. 못 지웠으면 같은 턴이 그대로 남아 있다.
|
|
326
|
+
이력줄임,
|
|
327
|
+
// 이력에서 못 읽은 줄 수. 그만큼은 되돌릴 길이 애초에 없었다.
|
|
328
|
+
깨진줄: this.깨진줄,
|
|
239
329
|
// 화면·감사기록이 쓰는 수. **진짜로 되돌아간 것만** 센다.
|
|
240
330
|
되돌린수: restored.filter((x) => x.ok === true).length,
|
|
241
331
|
못한것: restored.filter((x) => x.ok === false),
|
package/src/tools/fsutil.js
CHANGED
|
@@ -297,6 +297,8 @@ const 한숨 = () => new Promise((풀기) => setImmediate(풀기));
|
|
|
297
297
|
export async function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true, signal = null } = {}) {
|
|
298
298
|
const out = [];
|
|
299
299
|
const 건너뜀 = { 폴더: 0, 파일: 0 };
|
|
300
|
+
// .gitignore 로 건너뛴 것과 다르다 — 이쪽은 우리가 늘 안 보는 살림 폴더다.
|
|
301
|
+
const 건너뛴살림 = [];
|
|
300
302
|
let 끊김 = false;
|
|
301
303
|
let 본것 = 0;
|
|
302
304
|
const stack = [{ dir: root, rel: '', 규칙: ignore ? 뿌리규칙읽기(root) : [] }];
|
|
@@ -315,7 +317,10 @@ export async function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS,
|
|
|
315
317
|
const full = join(dir, e.name);
|
|
316
318
|
const erel = rel ? `${rel}/${e.name}` : e.name;
|
|
317
319
|
if (e.isDirectory()) {
|
|
318
|
-
|
|
320
|
+
// 살림 폴더(node_modules·.git·dist…)는 안 훑는다. 다만 **몇 개를
|
|
321
|
+
// 안 봤는지는 센다** — Move 가 폴더를 통째로 옮길 때 이 수를 모르면
|
|
322
|
+
// 「폴더 12개 파일」 이라고 말해 놓고 3만 개를 옮기게 된다.
|
|
323
|
+
if (skipDirs.has(e.name)) { 건너뛴살림.push(erel); continue; }
|
|
319
324
|
if (여기규칙.length && 걸리나(erel, true, 여기규칙)) { 건너뜀.폴더 += 1; continue; }
|
|
320
325
|
stack.push({ dir: full, rel: erel, 규칙: 여기규칙 });
|
|
321
326
|
} else if (e.isFile()) {
|
|
@@ -335,6 +340,7 @@ export async function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS,
|
|
|
335
340
|
if (끊김) break;
|
|
336
341
|
}
|
|
337
342
|
Object.defineProperty(out, '건너뜀', { value: 건너뜀, enumerable: false });
|
|
343
|
+
Object.defineProperty(out, '건너뛴살림', { value: 건너뛴살림, enumerable: false });
|
|
338
344
|
// 상한까지 찼으면 "여기서 멈췄다" 고 표시한다. 딱 맞아떨어져 끝난 경우까지
|
|
339
345
|
// 잘렸다고 하게 되지만, 그쪽으로 틀리는 편이 낫다 — 덜 봤다고 말하는 것은
|
|
340
346
|
// 사람을 한 번 더 보게 만들 뿐이고, 다 봤다고 말하는 것은 못 보게 만든다.
|