deel-local-cli 1.12.0 → 1.13.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.
Files changed (51) hide show
  1. package/README.ko.md +6 -5
  2. package/README.md +6 -5
  3. package/package.json +60 -60
  4. package/src/acp/map.js +30 -25
  5. package/src/acp/serve.js +810 -810
  6. package/src/agent/budget.js +156 -156
  7. package/src/agent/commit.js +32 -9
  8. package/src/agent/compact.js +441 -420
  9. package/src/agent/effort.js +308 -308
  10. package/src/agent/evolve.js +263 -263
  11. package/src/agent/loop.js +1728 -1707
  12. package/src/agent/memory.js +156 -156
  13. package/src/agent/mention.js +210 -210
  14. package/src/agent/recall.js +22 -9
  15. package/src/agent/session.js +1056 -1049
  16. package/src/backend/adapter.js +1355 -1159
  17. package/src/backend/cachemark.js +223 -150
  18. package/src/backend/detect.js +327 -327
  19. package/src/backend/http.js +3 -1
  20. package/src/backend/mcp.js +15 -3
  21. package/src/backend/price.js +69 -6
  22. package/src/backend/probe.js +17 -3
  23. package/src/backend/quota.js +274 -250
  24. package/src/backend/tokens.js +25 -3
  25. package/src/backend/toolfit.js +352 -352
  26. package/src/backend/wire.js +715 -600
  27. package/src/commands.js +3184 -3135
  28. package/src/config.js +39 -0
  29. package/src/i18n/en.js +545 -544
  30. package/src/i18n/ja.js +498 -497
  31. package/src/i18n/ko.js +592 -591
  32. package/src/i18n/zh.js +498 -497
  33. package/src/oneshot.js +625 -620
  34. package/src/pack/tar.js +23 -1
  35. package/src/pack/zip.js +19 -1
  36. package/src/plugins/manage.js +85 -5
  37. package/src/repl.js +2616 -2602
  38. package/src/safety/audit.js +22 -3
  39. package/src/safety/guard.js +124 -9
  40. package/src/safety/network.js +43 -0
  41. package/src/tools/fastgrep.js +15 -0
  42. package/src/tools/fsutil.js +291 -265
  43. package/src/tools/index.js +40 -3
  44. package/src/tools/jobs.js +87 -6
  45. package/src/tools/label.js +52 -0
  46. package/src/tools/spawn.js +114 -0
  47. package/src/tools/verify.js +358 -358
  48. package/src/tools/webfetch.js +28 -3
  49. package/src/ui/export.js +26 -10
  50. package/src/ui/pick.js +115 -115
  51. package/src/ui/status.js +617 -613
@@ -1,156 +1,156 @@
1
- /**
2
- * 상한을 **모델에 맞춰** 정한다.
3
- *
4
- * ── 왜 고정 숫자를 버렸나 ───────────────────────────────────────────────
5
- *
6
- * 전에는 전부 못 박혀 있었다. 걸음 24회, Read 2,000줄, Glob 200개,
7
- * WebFetch 20,000자. 그런데 이 값들이 맞는 모델은 **하나도 없다.**
8
- *
9
- * 8k 짜리 로컬 모델에 20,000자를 부어 넣으면 그 한 번으로 창이 넘친다.
10
- * 앞엣말을 잊은 채로 답하니, 사람 눈에는 "모델이 멍청해졌다" 로 보인다.
11
- *
12
- * 655k 짜리 모델은 24걸음 만에 끊긴다. 대시보드 하나를 만들려면 폴더를
13
- * 잡고 파일 몇 개를 만들고 돌려 보고 고쳐야 하는데, 만들다 만 채로 끝난다.
14
- * 여유가 96% 남아 있는데도 그렇다.
15
- *
16
- * 같은 숫자가 한쪽에선 너무 크고 다른 쪽에선 너무 작다. 그러면 그건 숫자를
17
- * 잘못 고른 게 아니라 **고정한 것 자체가 틀린 것**이다.
18
- *
19
- * 그래서 전부 컨텍스트에서 뽑아 쓴다. 큰 모델에는 크게 주고 작은 모델에는
20
- * 작게 준다. 사람이 `/ctx` 로 컨텍스트를 다시 재면 이 값들도 같이 따라온다.
21
- *
22
- * ── 걸음 수는 목표가 아니라 마지막 안전장치다 ──────────────────────────
23
- *
24
- * 헛도는 것은 걸음 수로 못 잡는다 — 그건 '잘 되고 있는 긴 작업' 과 '헛도는
25
- * 작업' 을 구분 못 한다. 헛도는 것은 같은 자리를 되풀이하는지로 잡고
26
- * (loop.js), 컨텍스트가 차면 접는다(compact.js). 여기 값은 그 둘이 다 놓쳤을
27
- * 때 끝없이 도는 것을 막는 **마지막 울타리**다. 그러니 넉넉해도 된다.
28
- */
29
-
30
- // 컨텍스트 1k 당 몇 걸음을 줄까. 일의 성격마다 다르다 —
31
- // '묻기' 는 설명 한 번이면 끝이고, '총괄' 은 여러 갈래를 끝까지 끌고 간다.
32
- const 걸음규칙 = {
33
- ask: { 배수: 0.25, 최소: 4, 최대: 12 },
34
- plan: { 배수: 0.6, 최소: 8, 최대: 40 },
35
- architect: { 배수: 0.7, 최소: 8, 최대: 48 },
36
- auto: { 배수: 1.5, 최소: 16, 최대: 200 },
37
- code: { 배수: 1.5, 최소: 16, 최대: 200 },
38
- debug: { 배수: 2, 최소: 16, 최대: 250 },
39
- orchestrator: { 배수: 3, 최소: 24, 최대: 400 },
40
- };
41
-
42
- const 사이 = (v, 최소, 최대) => Math.max(최소, Math.min(최대, Math.round(v)));
43
-
44
- /** 컨텍스트가 아직 안 잡혔을 때 쓰는 값. 못 알아냈다는 뜻이지 정답이 아니다. */
45
- const 모를때 = 32768;
46
-
47
- const k = (ctx) => (Number(ctx) > 0 ? Number(ctx) : 모를때) / 1024;
48
-
49
- /**
50
- * 이 모드·이 모델에서 한 턴에 몇 걸음까지 돌까.
51
- *
52
- * @param {string} 모드id auto · code · plan …
53
- * @param {number} ctx 모델 컨텍스트 길이(토큰)
54
- */
55
- export function 걸음수(모드id, ctx) {
56
- const r = 걸음규칙[모드id] ?? 걸음규칙.auto;
57
- return 사이(k(ctx) * r.배수, r.최소, r.최대);
58
- }
59
-
60
- /**
61
- * 한 번에 읽을 파일 줄 수.
62
- *
63
- * 8k 모델에 2,000줄을 부어 넣으면 그 한 번으로 창이 넘친다. 넘치면 접히고,
64
- * 접히면 앞엣말을 잊는다 — 그런데 화면에는 `2,000줄` 이라고만 떠서 사람은
65
- * 잘 읽은 줄 안다.
66
- */
67
- export function 읽을줄수(ctx) {
68
- return 사이(k(ctx) * 12, 200, 4000);
69
- }
70
-
71
- /** Glob 이 돌려줄 파일 개수. 목록은 한 줄에 한 개라 줄 수와 성격이 같다. */
72
- export function 찾을개수(ctx) {
73
- return 사이(k(ctx) * 6, 50, 1000);
74
- }
75
-
76
- /** Grep 이 돌려줄 줄 수. */
77
- export function 찾을줄수(ctx) {
78
- return 사이(k(ctx) * 8, 80, 1500);
79
- }
80
-
81
- /**
82
- * Outline 이 돌려줄 줄 수.
83
- *
84
- * Grep 보다 넉넉히 준다. 뼈대 한 줄은 ` 42 fn 접어쓰기` 처럼 짧은데,
85
- * 그 한 줄이 파일 하나를 통째로 안 읽어도 되게 해 준다 — 줄당 값이 제일 크다.
86
- * 8k 에서 120줄이면 파일 열댓 개의 모양이 들어온다. 그 정도면 "어디를 고칠지"
87
- * 는 정해진다.
88
- */
89
- export function 뼈대줄수(ctx) {
90
- return 사이(k(ctx) * 15, 120, 2500);
91
- }
92
-
93
- /**
94
- * WebFetch 가 한 번에 가져올 글자 수.
95
- *
96
- * 토큰이 아니라 **글자** 다. 영문 JSON 은 3.6자에 1토큰이지만 한글은 1자에
97
- * 1토큰이라, 같은 글자 수라도 한글이 세 배 넘게 무겁다. 무거운 쪽에 맞춘다 —
98
- * 넘쳐서 잊는 것보다 모자라서 한 번 더 부르는 편이 낫다.
99
- */
100
- export function 웹글자수(ctx) {
101
- return 사이(k(ctx) * 400, 4000, 120000);
102
- }
103
-
104
- /**
105
- * 하위 작업이 돌 걸음 수.
106
- *
107
- * 부모보다 적게 준다. 하위 작업은 '한 덩이' 를 맡는 것이지 전체를 맡는 게
108
- * 아니다. 부모만큼 주면 하위가 부모 일을 통째로 다시 하려 들고, 그러면
109
- * 창을 나눈 뜻이 없어진다. 대신 너무 적으면 맡긴 덩이도 못 끝내니
110
- * 8걸음은 무슨 일이 있어도 준다.
111
- */
112
- export function 하위걸음수(모드id, ctx) {
113
- return Math.max(8, Math.round(걸음수(모드id, ctx) / 2));
114
- }
115
-
116
- /**
117
- * 하위 작업이 부모에게 돌려줄 요약의 길이(글자).
118
- *
119
- * 이게 하위 작업의 **존재 이유**다. 하위가 파일 열 개를 읽고 만들어도 부모
120
- * 창에 들어가는 것은 이만큼뿐이다. 그러니 부모 창에 맞춰야지, 하위가 한 일의
121
- * 양에 맞추면 안 된다 — 그러면 나눈 것이 도로 합쳐진다.
122
- *
123
- * 부모 창의 약 5% 를 넘지 않게 잡는다. 8k 면 400자, 655k 면 4,000자다.
124
- */
125
- export function 요약길이(ctx) {
126
- return 사이(k(ctx) * 50, 400, 4000);
127
- }
128
-
129
- /**
130
- * 도구 설명을 몇 글자까지 실을까.
131
- *
132
- * ── 이게 왜 필요한가 ─────────────────────────────────────────────────
133
- *
134
- * 도구 정의는 **매 요청에 통째로 다시 나간다.** 접기로는 못 줄이는 몫이다.
135
- * 도구가 스무 종이 되면서 코드 모드에서만 그만큼이 매번 실린다. 시스템
136
- * 프롬프트까지 합치면 8k 모델은 대화 한 줄 전에 자리의 **절반**이 찬다.
137
- *
138
- * 여기에 잰 값을 박아 두지 않는다. 박아 두면 도구가 늘 때마다 낡고, 낡은
139
- * 숫자는 없는 숫자보다 나쁘다 — 그걸 믿고 예산을 짜기 때문이다.
140
- *
141
- * 그렇다고 도구를 뺄 수는 없다. 빼면 작은 모델만 할 수 있는 일이 달라져서,
142
- * "환경마다 다르게 동작" 하게 된다 — 사용자가 피하려는 바로 그것이다.
143
- * 그래서 **도구는 다 주고 설명만 줄인다.** 이름과 인자는 그대로 두므로
144
- * 할 수 있는 일은 똑같고, 없어지는 것은 '왜 이걸 쓰는지' 하는 설득뿐이다.
145
- *
146
- * 큰 창에서는 안 줄인다(Infinity). 그 설득이 실제로 값을 하기 때문이다 —
147
- * 모델이 Outline 을 Read 앞에 부르게 만드는 것은 그 두 문장이다.
148
- */
149
- export function 설명길이(ctx) {
150
- const n = Number(ctx) > 0 ? Number(ctx) : 모를때;
151
- if (n >= 64000) return Infinity; // 자리가 넉넉하다. 그대로 준다
152
- if (n >= 24000) return 220;
153
- if (n >= 12000) return 140;
154
- return 90; // 8k 언저리. 첫 문장 하나만 남는다
155
- }
156
-
1
+ /**
2
+ * 상한을 **모델에 맞춰** 정한다.
3
+ *
4
+ * ── 왜 고정 숫자를 버렸나 ───────────────────────────────────────────────
5
+ *
6
+ * 전에는 전부 못 박혀 있었다. 걸음 24회, Read 2,000줄, Glob 200개,
7
+ * WebFetch 20,000자. 그런데 이 값들이 맞는 모델은 **하나도 없다.**
8
+ *
9
+ * 8k 짜리 로컬 모델에 20,000자를 부어 넣으면 그 한 번으로 창이 넘친다.
10
+ * 앞엣말을 잊은 채로 답하니, 사람 눈에는 "모델이 멍청해졌다" 로 보인다.
11
+ *
12
+ * 655k 짜리 모델은 24걸음 만에 끊긴다. 대시보드 하나를 만들려면 폴더를
13
+ * 잡고 파일 몇 개를 만들고 돌려 보고 고쳐야 하는데, 만들다 만 채로 끝난다.
14
+ * 여유가 96% 남아 있는데도 그렇다.
15
+ *
16
+ * 같은 숫자가 한쪽에선 너무 크고 다른 쪽에선 너무 작다. 그러면 그건 숫자를
17
+ * 잘못 고른 게 아니라 **고정한 것 자체가 틀린 것**이다.
18
+ *
19
+ * 그래서 전부 컨텍스트에서 뽑아 쓴다. 큰 모델에는 크게 주고 작은 모델에는
20
+ * 작게 준다. 사람이 `/ctx` 로 컨텍스트를 다시 재면 이 값들도 같이 따라온다.
21
+ *
22
+ * ── 걸음 수는 목표가 아니라 마지막 안전장치다 ──────────────────────────
23
+ *
24
+ * 헛도는 것은 걸음 수로 못 잡는다 — 그건 '잘 되고 있는 긴 작업' 과 '헛도는
25
+ * 작업' 을 구분 못 한다. 헛도는 것은 같은 자리를 되풀이하는지로 잡고
26
+ * (loop.js), 컨텍스트가 차면 접는다(compact.js). 여기 값은 그 둘이 다 놓쳤을
27
+ * 때 끝없이 도는 것을 막는 **마지막 울타리**다. 그러니 넉넉해도 된다.
28
+ */
29
+
30
+ // 컨텍스트 1k 당 몇 걸음을 줄까. 일의 성격마다 다르다 —
31
+ // '묻기' 는 설명 한 번이면 끝이고, '총괄' 은 여러 갈래를 끝까지 끌고 간다.
32
+ const 걸음규칙 = {
33
+ ask: { 배수: 0.25, 최소: 4, 최대: 12 },
34
+ plan: { 배수: 0.6, 최소: 8, 최대: 40 },
35
+ architect: { 배수: 0.7, 최소: 8, 최대: 48 },
36
+ auto: { 배수: 1.5, 최소: 16, 최대: 200 },
37
+ code: { 배수: 1.5, 최소: 16, 최대: 200 },
38
+ debug: { 배수: 2, 최소: 16, 최대: 250 },
39
+ orchestrator: { 배수: 3, 최소: 24, 최대: 400 },
40
+ };
41
+
42
+ const 사이 = (v, 최소, 최대) => Math.max(최소, Math.min(최대, Math.round(v)));
43
+
44
+ /** 컨텍스트가 아직 안 잡혔을 때 쓰는 값. 못 알아냈다는 뜻이지 정답이 아니다. */
45
+ const 모를때 = 32768;
46
+
47
+ const k = (ctx) => (Number(ctx) > 0 ? Number(ctx) : 모를때) / 1024;
48
+
49
+ /**
50
+ * 이 모드·이 모델에서 한 턴에 몇 걸음까지 돌까.
51
+ *
52
+ * @param {string} 모드id auto · code · plan …
53
+ * @param {number} ctx 모델 컨텍스트 길이(토큰)
54
+ */
55
+ export function 걸음수(모드id, ctx) {
56
+ const r = 걸음규칙[모드id] ?? 걸음규칙.auto;
57
+ return 사이(k(ctx) * r.배수, r.최소, r.최대);
58
+ }
59
+
60
+ /**
61
+ * 한 번에 읽을 파일 줄 수.
62
+ *
63
+ * 8k 모델에 2,000줄을 부어 넣으면 그 한 번으로 창이 넘친다. 넘치면 접히고,
64
+ * 접히면 앞엣말을 잊는다 — 그런데 화면에는 `2,000줄` 이라고만 떠서 사람은
65
+ * 잘 읽은 줄 안다.
66
+ */
67
+ export function 읽을줄수(ctx) {
68
+ return 사이(k(ctx) * 12, 200, 4000);
69
+ }
70
+
71
+ /** Glob 이 돌려줄 파일 개수. 목록은 한 줄에 한 개라 줄 수와 성격이 같다. */
72
+ export function 찾을개수(ctx) {
73
+ return 사이(k(ctx) * 6, 50, 1000);
74
+ }
75
+
76
+ /** Grep 이 돌려줄 줄 수. */
77
+ export function 찾을줄수(ctx) {
78
+ return 사이(k(ctx) * 8, 80, 1500);
79
+ }
80
+
81
+ /**
82
+ * Outline 이 돌려줄 줄 수.
83
+ *
84
+ * Grep 보다 넉넉히 준다. 뼈대 한 줄은 ` 42 fn 접어쓰기` 처럼 짧은데,
85
+ * 그 한 줄이 파일 하나를 통째로 안 읽어도 되게 해 준다 — 줄당 값이 제일 크다.
86
+ * 8k 에서 120줄이면 파일 열댓 개의 모양이 들어온다. 그 정도면 "어디를 고칠지"
87
+ * 는 정해진다.
88
+ */
89
+ export function 뼈대줄수(ctx) {
90
+ return 사이(k(ctx) * 15, 120, 2500);
91
+ }
92
+
93
+ /**
94
+ * WebFetch 가 한 번에 가져올 글자 수.
95
+ *
96
+ * 토큰이 아니라 **글자** 다. 영문 JSON 은 3.6자에 1토큰이지만 한글은 1자에
97
+ * 1토큰이라, 같은 글자 수라도 한글이 세 배 넘게 무겁다. 무거운 쪽에 맞춘다 —
98
+ * 넘쳐서 잊는 것보다 모자라서 한 번 더 부르는 편이 낫다.
99
+ */
100
+ export function 웹글자수(ctx) {
101
+ return 사이(k(ctx) * 400, 4000, 120000);
102
+ }
103
+
104
+ /**
105
+ * 하위 작업이 돌 걸음 수.
106
+ *
107
+ * 부모보다 적게 준다. 하위 작업은 '한 덩이' 를 맡는 것이지 전체를 맡는 게
108
+ * 아니다. 부모만큼 주면 하위가 부모 일을 통째로 다시 하려 들고, 그러면
109
+ * 창을 나눈 뜻이 없어진다. 대신 너무 적으면 맡긴 덩이도 못 끝내니
110
+ * 8걸음은 무슨 일이 있어도 준다.
111
+ */
112
+ export function 하위걸음수(모드id, ctx) {
113
+ return Math.max(8, Math.round(걸음수(모드id, ctx) / 2));
114
+ }
115
+
116
+ /**
117
+ * 하위 작업이 부모에게 돌려줄 요약의 길이(글자).
118
+ *
119
+ * 이게 하위 작업의 **존재 이유**다. 하위가 파일 열 개를 읽고 만들어도 부모
120
+ * 창에 들어가는 것은 이만큼뿐이다. 그러니 부모 창에 맞춰야지, 하위가 한 일의
121
+ * 양에 맞추면 안 된다 — 그러면 나눈 것이 도로 합쳐진다.
122
+ *
123
+ * 부모 창의 약 5% 를 넘지 않게 잡는다. 8k 면 400자, 655k 면 4,000자다.
124
+ */
125
+ export function 요약길이(ctx) {
126
+ return 사이(k(ctx) * 50, 400, 4000);
127
+ }
128
+
129
+ /**
130
+ * 도구 설명을 몇 글자까지 실을까.
131
+ *
132
+ * ── 이게 왜 필요한가 ─────────────────────────────────────────────────
133
+ *
134
+ * 도구 정의는 **매 요청에 통째로 다시 나간다.** 접기로는 못 줄이는 몫이다.
135
+ * 도구가 스무 종이 되면서 코드 모드에서만 그만큼이 매번 실린다. 시스템
136
+ * 프롬프트까지 합치면 8k 모델은 대화 한 줄 전에 자리의 **절반**이 찬다.
137
+ *
138
+ * 여기에 잰 값을 박아 두지 않는다. 박아 두면 도구가 늘 때마다 낡고, 낡은
139
+ * 숫자는 없는 숫자보다 나쁘다 — 그걸 믿고 예산을 짜기 때문이다.
140
+ *
141
+ * 그렇다고 도구를 뺄 수는 없다. 빼면 작은 모델만 할 수 있는 일이 달라져서,
142
+ * "환경마다 다르게 동작" 하게 된다 — 사용자가 피하려는 바로 그것이다.
143
+ * 그래서 **도구는 다 주고 설명만 줄인다.** 이름과 인자는 그대로 두므로
144
+ * 할 수 있는 일은 똑같고, 없어지는 것은 '왜 이걸 쓰는지' 하는 설득뿐이다.
145
+ *
146
+ * 큰 창에서는 안 줄인다(Infinity). 그 설득이 실제로 값을 하기 때문이다 —
147
+ * 모델이 Outline 을 Read 앞에 부르게 만드는 것은 그 두 문장이다.
148
+ */
149
+ export function 설명길이(ctx) {
150
+ const n = Number(ctx) > 0 ? Number(ctx) : 모를때;
151
+ if (n >= 64000) return Infinity; // 자리가 넉넉하다. 그대로 준다
152
+ if (n >= 24000) return 220;
153
+ if (n >= 12000) return 140;
154
+ return 90; // 8k 언저리. 첫 문장 하나만 남는다
155
+ }
156
+
@@ -23,13 +23,15 @@
23
23
  // 남이 미리 담아 둔 것(index)을 풀지도 않는다 — 남의 준비를 말없이 흩는
24
24
  // 것이 커밋을 하나 더 만드는 것보다 나쁘다. 대신 화면에 같이 적는다.
25
25
  import { spawnSync } from 'node:child_process';
26
- import { writeFileSync, unlinkSync, realpathSync, statSync } from 'node:fs';
26
+ import { writeFileSync, unlinkSync, statSync } from 'node:fs';
27
27
  import { join, relative, isAbsolute } from 'node:path';
28
+ import { 진짜자리 } from '../safety/guard.js';
28
29
  import { tmpdir } from 'node:os';
29
30
  import { randomBytes } from 'node:crypto';
30
31
  import { chat } from '../backend/adapter.js';
31
32
  import { 증거모으기 } from './evidence.js';
32
33
  import { VERSION } from '../version.js';
34
+ import { homeDir } from '../config.js';
33
35
 
34
36
  /** 제목 길이 상한. git 관례(50~72)의 넉넉한 쪽. 한글은 글자 수로 센다. */
35
37
  export const 제목상한 = 72;
@@ -107,25 +109,45 @@ const 살림꼴 = /(^|[\\/])\.deel([\\/]|$)/i;
107
109
  /** 이 경로가 살림을 가리키나 — 글자로 본다. */
108
110
  export function 살림경로인가(rel) { return 살림꼴.test(String(rel ?? '')); }
109
111
 
110
- /** 이 경로가 **진짜로** 살림 안에 닿나 — 링크를 다 풀고 본다. */
112
+ /**
113
+ * 이 경로가 **진짜로** 살림 안에 닿나 — 링크를 다 풀고 본다.
114
+ *
115
+ * 푸는 자는 울타리와 같은 것을 쓴다(진짜자리). 여기서 따로 realpathSync 를
116
+ * 부르면 없는 파일에서 그냥 포기하고, 그러면 링크로 들어온 살림이 글자
117
+ * 검사만 거쳐 통과한다 — 열쇠가 담기는 길이 그렇게 열린다.
118
+ */
111
119
  export function 살림에닿나(abs) {
112
- try { return 살림꼴.test(realpathSync(abs)); }
113
- catch { return 살림경로인가(abs); } // 못 풀면(지워진 파일 등) 글자로만 본다
120
+ const 푼것 = 진짜자리(abs);
121
+ if (살림꼴.test(푼것) || 살림경로인가(abs)) return true;
122
+ /*
123
+ * 이름이 `.deel` 이 아닐 수도 있다 — `DEEL_HOME` 으로 옮기면 그렇다.
124
+ * 살림이 어디인지는 config.js 만 안다. 여기서 또 적으면 한쪽이 낡는다.
125
+ */
126
+ try {
127
+ const 집 = String(homeDir()).replace(/\\/g, '/').replace(/[/]+$/, '').toLowerCase();
128
+ const 낮은 = String(푼것 ?? '').replace(/\\/g, '/').toLowerCase();
129
+ return !!집 && (낮은 === 집 || 낮은.startsWith(집 + '/'));
130
+ } catch { return false; }
114
131
  }
115
132
 
116
133
  /** 담을 때 git 에게도 빼 달라고 하는 자리. 어느 깊이든, 대소문자 상관없이. */
117
134
  const 살림빼기 = [':(exclude,icase,glob)**/.deel/**', ':(exclude,icase,glob)**/.deel'];
118
135
 
119
136
  /**
120
- * 링크를 다 푼 진짜 자리. 풀면(아직 없는 파일 등) 준 것을 그대로.
137
+ * 링크를 다 푼 진짜 자리 자는 울타리와 **같은 것**을 쓴다.
121
138
  *
122
139
  * 저장소 뿌리를 링크로 열어 둔 사람이 있다(윈도우 junction, 맥의 /tmp).
123
140
  * git 은 링크를 풀어서 답하고 우리는 안 풀면, 같은 자리를 두 이름으로 부르게
124
- * 되어 "이번에 바꾼 것이 없습니다" 가 된다 — 바꾼 것이 있는데도.
141
+ * 되어 "이번에 바꾼 것이 없습니다" 가 된다 — 바꾼 것이 있는데도. 그리고 그
142
+ * 「없다」 는 **작업 폴더 전부**로 물러서는 신호다. 이 파일이 없애려고 만들어진
143
+ * 바로 그 `git add -A` 사고가, 경고 한 줄 없이 난다.
144
+ *
145
+ * 여기 자를 따로 두고 있었다. 없는 파일에서 그냥 포기하는 자였고(지운 파일은
146
+ * 이 명령이 제일 자주 다루는 것이다), 윈도우 8.3 단축명도 안 폈다. 울타리
147
+ * (safety/guard.js)에는 그 둘을 다 하는 자가 이미 있었는데도 따로 있었다.
148
+ * 자가 둘이면 언젠가 어긋나고, 어긋나는 날 이 명령은 남의 것까지 담는다.
125
149
  */
126
- function 진짜(p) {
127
- try { return realpathSync(p); } catch { return p; }
128
- }
150
+ const 진짜 = 진짜자리;
129
151
 
130
152
  /**
131
153
  * 이번 대화가 건드린 파일 — 저장소 안의 것만, 저장소 기준 상대경로로.
@@ -143,6 +165,7 @@ export function 이번에바꾼것(session, 뿌리) {
143
165
  const abs = 진짜(isAbsolute(p) ? p : join(기준, p));
144
166
  const rel = relative(기준, abs).replace(/\\/g, '/');
145
167
  // 저장소 밖은 담지 않는다. `..` 로 시작하면 밖이다.
168
+ // (지워진 파일도 여기서 안 새어 나간다 — 진짜() 가 있는 데까지 풀어 준다.)
146
169
  if (!rel || rel.startsWith('../') || rel === '..') continue;
147
170
  if (살림경로인가(rel) || 살림에닿나(abs)) continue;
148
171
  let 폴더인가 = false;