deel-local-cli 1.7.0 → 1.10.0
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 +1166 -0
- package/README.md +1223 -1107
- package/bin/deel.js +78 -12
- package/package.json +4 -4
- package/src/acp/map.js +1 -1
- package/src/acp/serve.js +40 -3
- package/src/agent/compact.js +314 -296
- package/src/agent/effort.js +27 -7
- package/src/agent/evidence.js +2 -0
- package/src/agent/filemem.js +141 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +218 -40
- package/src/agent/session.js +136 -14
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +272 -23
- package/src/backend/learn.js +24 -0
- package/src/backend/mcp.js +96 -5
- package/src/backend/probe.js +131 -61
- package/src/backend/quota.js +29 -5
- package/src/backend/retry.js +3 -0
- package/src/backend/scanui.js +1 -1
- package/src/backend/toolfit.js +325 -0
- package/src/commands.js +170 -61
- package/src/completion.js +20 -2
- package/src/config.js +47 -2
- package/src/i18n/en.js +210 -3
- package/src/i18n/index.js +40 -0
- package/src/i18n/ja.js +204 -3
- package/src/i18n/ko.js +270 -3
- package/src/i18n/zh.js +204 -3
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +92 -5
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +26 -10
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/repl.js +344 -61
- package/src/reset.js +397 -0
- package/src/safety/audit.js +95 -8
- package/src/safety/authcmd.js +316 -0
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +114 -38
- package/src/safety/undo.js +25 -6
- package/src/tools/desc.en.js +19 -14
- package/src/tools/index.js +349 -75
- package/src/tools/jobs.js +175 -42
- package/src/tools/lsp.js +5 -4
- package/src/tools/outline.js +7 -3
- package/src/tools/task.js +10 -6
- package/src/tools/todo.js +9 -2
- package/src/tools/verify.js +42 -7
- package/src/tools/webfetch.js +94 -10
- package/src/ui/export.js +1 -1
- package/src/ui/md.js +201 -5
- package/src/ui/motion.js +0 -1
- package/src/ui/pastechip.js +50 -3
- package/src/ui/pick.js +115 -0
- package/src/ui/screen.js +23 -3
- package/README.en.md +0 -1117
package/src/reset.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// deel reset — 지우려고 다시 깔지 않게.
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 있나 ────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 연결을 바꿔 보다, 배운 것이 꼬여서, 남에게 넘기기 전에 — 되돌아갈 자리가
|
|
6
|
+
// 필요할 때가 있다. 지금까지는 그때마다 다시 깔았다. 그런데 다시 깔아도
|
|
7
|
+
// `~/.deel` 은 그대로 남으므로, 사실 다시 깔아도 초기화가 안 됐다.
|
|
8
|
+
//
|
|
9
|
+
// ── 이 파일이 조심하는 것 ──────────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// 이건 이 프로그램에서 **사람의 진짜 파일을 잃게 할 수 있는 유일한 기능**이다.
|
|
12
|
+
// 그래서 무엇을 지우는지보다 **무엇을 안 지우는지**를 먼저 정해 놓았다.
|
|
13
|
+
//
|
|
14
|
+
// 되돌리기 스냅샷(.deel/history) 이 도구가 승인 프롬프트 대신 내건 안전망
|
|
15
|
+
// 그 자체다. 이걸 지우면 여태 고친 파일을
|
|
16
|
+
// 되돌릴 길이 사라진다.
|
|
17
|
+
// 감사기록(.deel/audit.jsonl) 사내 심사에 낼 근거다.
|
|
18
|
+
// .deel/mcp.json · .deelignore 사람이 손으로 적은 것이다. 우리가 만든
|
|
19
|
+
// 것이 아니면 우리가 지우지 않는다.
|
|
20
|
+
//
|
|
21
|
+
// 앞의 둘은 `all --hard` 로만 지운다. 뒤의 둘은 **어떤 길로도 안 지운다.**
|
|
22
|
+
//
|
|
23
|
+
// ── 설정이 깨져 있어도 돌아야 한다 ─────────────────────────────────────
|
|
24
|
+
//
|
|
25
|
+
// 초기화를 찾는 까닭이 대개 「설정이 깨져서」 다. 그런데 평소에 켜는 길은
|
|
26
|
+
// 설정을 먼저 읽는다. 그래서 이 파일은 config.js 의 load() 를 안 쓴다 —
|
|
27
|
+
// 파일을 직접 읽고, 못 읽으면 못 읽었다고 적은 채로 계속 간다.
|
|
28
|
+
// (`deel --version` 을 연결 없이 답하게 만든 것과 같은 이유다.)
|
|
29
|
+
import { join, resolve, sep } from 'node:path';
|
|
30
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
31
|
+
import { c, say, mark, rule, pad } from './ui/ansi.js';
|
|
32
|
+
import { ask, confirm } from './ui/prompt.js';
|
|
33
|
+
import { homeDir } from './config.js';
|
|
34
|
+
import { 잠긴것인가, 잠금지우기, 보관방식 } from './safety/keystore.js';
|
|
35
|
+
|
|
36
|
+
/** 사람이 칠 수 있는 이름. bin/deel.js 의 도움말·완성 목록과 같아야 한다. */
|
|
37
|
+
export const 갈래들 = ['model', 'memory', 'sessions', 'learned', 'plugins', 'all'];
|
|
38
|
+
|
|
39
|
+
// ── 세는 것들 ───────────────────────────────────────────────────────────
|
|
40
|
+
//
|
|
41
|
+
// 세다가 나는 탈은 전부 삼킨다. 이건 '무엇이 있나' 를 보여 주려는 것이지
|
|
42
|
+
// 검사가 아니다 — 한 줄 못 셌다고 초기화 자체를 못 하게 하면 안 된다.
|
|
43
|
+
|
|
44
|
+
function 줄수(파일) {
|
|
45
|
+
try { return readFileSync(파일, 'utf8').split('\n').filter((l) => l.trim()).length; }
|
|
46
|
+
catch { return 0; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function 안에것(폴더, 거르기 = () => true) {
|
|
50
|
+
try { return readdirSync(폴더, { withFileTypes: true }).filter(거르기).length; }
|
|
51
|
+
catch { return 0; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 설정을 읽어 본다. **깨져 있어도 답한다.**
|
|
56
|
+
*
|
|
57
|
+
* @returns {{있나: boolean, 프로필: number|null, 잠긴열쇠: string[], 왜: string}}
|
|
58
|
+
*/
|
|
59
|
+
export function 설정살피기(파일) {
|
|
60
|
+
if (!existsSync(파일)) return { 있나: false, 프로필: 0, 잠긴열쇠: [], 왜: '' };
|
|
61
|
+
let j;
|
|
62
|
+
try { j = JSON.parse(readFileSync(파일, 'utf8')); }
|
|
63
|
+
catch (err) {
|
|
64
|
+
// 깨진 설정이야말로 지우려는 것이다. 몇 개인지 모른다고 해서 못 지우면 안 된다.
|
|
65
|
+
// 줄바꿈을 빼고 한 줄로 만든다. JSON 오류에는 깨진 그 줄이 그대로 실려
|
|
66
|
+
// 오는데, 그게 화면에 들어가면 표가 두 줄로 갈라진다.
|
|
67
|
+
const 한줄 = String(err.message).replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
68
|
+
return { 있나: true, 프로필: null, 잠긴열쇠: [], 왜: `읽을 수 없습니다 (${한줄})` };
|
|
69
|
+
}
|
|
70
|
+
const 목록 = Array.isArray(j?.profiles) ? j.profiles : [];
|
|
71
|
+
return {
|
|
72
|
+
있나: true,
|
|
73
|
+
프로필: 목록.length,
|
|
74
|
+
잠긴열쇠: 목록.map((p) => p?.apiKey).filter((k) => 잠긴것인가(k)),
|
|
75
|
+
왜: '',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 무엇이 어디에 얼마나 있나.
|
|
81
|
+
*
|
|
82
|
+
* 화면과 지우는 자리가 **같은 목록**을 본다. 두 벌이 되면 화면에는 안 뜨는
|
|
83
|
+
* 것이 지워지거나, 뜨는데 안 지워진다. 둘 다 겪으면 이 기능을 못 믿게 된다.
|
|
84
|
+
*
|
|
85
|
+
* @param {{home?: string, root?: string}} 어디
|
|
86
|
+
*/
|
|
87
|
+
export function 살펴보기({ home = homeDir(), root = process.cwd() } = {}) {
|
|
88
|
+
const 집 = (...n) => join(home, ...n);
|
|
89
|
+
const 일 = (...n) => join(root, '.deel', ...n);
|
|
90
|
+
|
|
91
|
+
const 집설정 = 설정살피기(집('config.json'));
|
|
92
|
+
const 일설정 = 설정살피기(일('config.json'));
|
|
93
|
+
const 열쇠들 = [...집설정.잠긴열쇠, ...일설정.잠긴열쇠];
|
|
94
|
+
|
|
95
|
+
const 항목 = [
|
|
96
|
+
{
|
|
97
|
+
키: 'model',
|
|
98
|
+
이름: '연결·프로필',
|
|
99
|
+
자리: [집('config.json'), 일('config.json')],
|
|
100
|
+
몇: 집설정.프로필 === null || 일설정.프로필 === null
|
|
101
|
+
? null : (집설정.프로필 ?? 0) + (일설정.프로필 ?? 0),
|
|
102
|
+
단위: '개',
|
|
103
|
+
탈: 집설정.왜 || 일설정.왜,
|
|
104
|
+
뒤: 'deel setup 을 다시 하게 됩니다',
|
|
105
|
+
all: true,
|
|
106
|
+
// 설정 파일을 지우기 **전에** 잠금장치를 먼저 손봐야 한다. 파일을
|
|
107
|
+
// 지우고 나면 어느 열쇠가 잠겨 있었는지 알 길이 없다.
|
|
108
|
+
열쇠: 열쇠들,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
키: 'memory',
|
|
112
|
+
이름: '기억',
|
|
113
|
+
자리: [일('memory.md')],
|
|
114
|
+
몇: 줄수(일('memory.md')),
|
|
115
|
+
단위: '줄',
|
|
116
|
+
뒤: c.yellow('돌아오지 않습니다'),
|
|
117
|
+
all: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
키: 'sessions',
|
|
121
|
+
이름: '대화 기록',
|
|
122
|
+
자리: [일('sessions')],
|
|
123
|
+
몇: 안에것(일('sessions'), (e) => e.isFile() && e.name.endsWith('.jsonl')),
|
|
124
|
+
단위: '개',
|
|
125
|
+
뒤: '이어하기(--continue · --resume)가 안 됩니다',
|
|
126
|
+
all: true,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
키: 'learned',
|
|
130
|
+
이름: '배운 것',
|
|
131
|
+
자리: [집('배운것.json'), 일('배운것.json')],
|
|
132
|
+
몇: [집('배운것.json'), 일('배운것.json')].filter((p) => existsSync(p)).length,
|
|
133
|
+
단위: '곳',
|
|
134
|
+
뒤: '다시 배웁니다',
|
|
135
|
+
all: true,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
// 우리가 만들어 쌓아 둔 것들. 사람이 적은 것이 아니라 안전하다.
|
|
139
|
+
키: '만든것',
|
|
140
|
+
이름: '증거·내보낸 것·임시',
|
|
141
|
+
자리: [일('증거'), 일('export'), 일('tmp'), 일('붙인그림')],
|
|
142
|
+
몇: [일('증거'), 일('export'), 일('tmp'), 일('붙인그림')].reduce((n, p) => n + 안에것(p), 0),
|
|
143
|
+
단위: '개',
|
|
144
|
+
뒤: '안전합니다',
|
|
145
|
+
all: true,
|
|
146
|
+
숨은것: true, // 따로 고를 수는 없고 all 에만 딸려 간다
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
키: 'plugins',
|
|
150
|
+
이름: '플러그인',
|
|
151
|
+
자리: [집('plugins')],
|
|
152
|
+
몇: 안에것(집('plugins'), (e) => e.isDirectory() && !e.name.startsWith('.')),
|
|
153
|
+
단위: '개',
|
|
154
|
+
뒤: c.yellow('다시 설치해야 합니다'),
|
|
155
|
+
// 따로 고를 때만 지운다. 다시 받는 데 시간이 걸리고, 사람이 초기화하려는
|
|
156
|
+
// 것은 대개 플러그인이 아니라 연결 쪽이다.
|
|
157
|
+
all: false,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
// `--hard` 로만 지우는 것.
|
|
162
|
+
const 굳은것 = [
|
|
163
|
+
{
|
|
164
|
+
키: 'history',
|
|
165
|
+
이름: '되돌리기 스냅샷',
|
|
166
|
+
자리: [일('history')],
|
|
167
|
+
몇: 줄수(일('history', 'edits.jsonl')),
|
|
168
|
+
단위: '자리',
|
|
169
|
+
뒤: c.red('고친 파일을 되돌릴 수 없게 됩니다'),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
키: 'audit',
|
|
173
|
+
이름: '감사기록',
|
|
174
|
+
자리: [일('audit.jsonl')],
|
|
175
|
+
몇: 줄수(일('audit.jsonl')),
|
|
176
|
+
단위: '줄',
|
|
177
|
+
뒤: c.red('사내 심사에 낼 근거가 사라집니다'),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
// 어떤 길로도 안 지우는 것. 화면에 이름을 적어 둔다 — 「왜 이건 안 지웠지」
|
|
182
|
+
// 를 나중에 묻게 하지 않으려고.
|
|
183
|
+
const 안건드림 = [
|
|
184
|
+
{ 이름: '.deel/mcp.json', 있나: existsSync(일('mcp.json')) },
|
|
185
|
+
{ 이름: '.deelignore', 있나: existsSync(join(root, '.deelignore')) },
|
|
186
|
+
{ 이름: 'DEEL.md', 있나: existsSync(join(root, 'DEEL.md')) },
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
return { home, root, 항목, 굳은것, 안건드림, 열쇠보관: 보관방식(열쇠들[0] ?? null) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 이 자리를 건드려도 되나.
|
|
194
|
+
*
|
|
195
|
+
* 작업 폴더와 살림 폴더 **밖은 절대 안 건드린다.** 이 프로그램의 다른
|
|
196
|
+
* 모든 자리가 지키는 규칙이고, 지우는 자리에서 특히 그렇다.
|
|
197
|
+
* 울타리 그 자체(`~/.deel` 이나 작업 폴더 통째로)도 안 준다 — 목록에 없는
|
|
198
|
+
* 것까지 딸려 가면 이 기능을 못 믿게 된다.
|
|
199
|
+
*/
|
|
200
|
+
export function 울타리안인가(경로, 울타리들) {
|
|
201
|
+
const p = resolve(경로);
|
|
202
|
+
return 울타리들.some((u) => {
|
|
203
|
+
const r = resolve(u);
|
|
204
|
+
return p !== r && p.startsWith(r + sep);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 고른 것을 지운다.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} 무엇 갈래들 중 하나
|
|
212
|
+
* @param {{home?: string, root?: string, hard?: boolean}} 옵션
|
|
213
|
+
* @returns {{지운것: Array, 못한것: Array, 열쇠: object|null}}
|
|
214
|
+
*/
|
|
215
|
+
export function 지우기(무엇, { home = homeDir(), root = process.cwd(), hard = false } = {}) {
|
|
216
|
+
const 본것 = 살펴보기({ home, root });
|
|
217
|
+
const 울타리 = [home, join(root, '.deel')];
|
|
218
|
+
|
|
219
|
+
const 고른것 = 무엇 === 'all'
|
|
220
|
+
? [...본것.항목.filter((x) => x.all), ...(hard ? 본것.굳은것 : [])]
|
|
221
|
+
: 본것.항목.filter((x) => x.키 === 무엇);
|
|
222
|
+
|
|
223
|
+
const 지운것 = [];
|
|
224
|
+
const 못한것 = [];
|
|
225
|
+
let 열쇠 = null;
|
|
226
|
+
|
|
227
|
+
for (const 것 of 고른것) {
|
|
228
|
+
// 설정을 지우기 전에 잠금장치부터. 순서가 반대면 어느 열쇠였는지 잃는다.
|
|
229
|
+
if (것.키 === 'model' && 것.열쇠?.length) {
|
|
230
|
+
열쇠 = 잠금지우기(것.열쇠[0]);
|
|
231
|
+
}
|
|
232
|
+
// 한 갈래가 여러 자리에 걸쳐 있다 (배운 것은 이 PC 와 이 폴더 둘 다).
|
|
233
|
+
// 화면에는 **갈래 하나로** 적는다 — 자리마다 한 줄씩 내면 「배운 것 2곳」
|
|
234
|
+
// 이 두 번 찍혀서, 네 곳을 지운 것처럼 읽힌다.
|
|
235
|
+
const 지운자리 = [];
|
|
236
|
+
for (const 자리 of 것.자리) {
|
|
237
|
+
if (!existsSync(자리)) continue;
|
|
238
|
+
if (!울타리안인가(자리, 울타리)) {
|
|
239
|
+
// 여기 오면 목록을 만드는 자리가 잘못된 것이다. 지우지 말고 말한다.
|
|
240
|
+
못한것.push({ 이름: 것.이름, 자리, 왜: '작업 폴더 밖이라 안 건드렸습니다' });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
rmSync(자리, { recursive: true, force: true });
|
|
245
|
+
지운자리.push(자리);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
못한것.push({ 이름: 것.이름, 자리, 왜: String(err.message).slice(0, 80) });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (지운자리.length) {
|
|
251
|
+
지운것.push({ 키: 것.키, 이름: 것.이름, 몇: 것.몇, 단위: 것.단위, 자리들: 지운자리 });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { 지운것, 못한것, 열쇠 };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── 화면 ────────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
function 몇자(것) {
|
|
260
|
+
if (것.몇 === null) return c.yellow('알 수 없음');
|
|
261
|
+
return 것.몇 ? `${것.몇.toLocaleString()}${것.단위}` : c.gray(`0${것.단위}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function 보여주기(본것) {
|
|
265
|
+
say('');
|
|
266
|
+
rule('지울 수 있는 것', 74);
|
|
267
|
+
say(` ${c.gray('살림 자리')} ${c.gray(본것.home)}`);
|
|
268
|
+
say(` ${c.gray('작업 폴더')} ${c.gray(본것.root)}`);
|
|
269
|
+
say('');
|
|
270
|
+
// 이름 칸은 pad() 로 맞춘다. padEnd 는 **글자 수**를 세는데 한글은 터미널에서
|
|
271
|
+
// 두 칸을 먹어서, 한글과 영문이 섞인 줄이 서로 어긋난다.
|
|
272
|
+
for (const 것 of 본것.항목) {
|
|
273
|
+
const 이름 = 것.숨은것 ? c.gray(것.이름) : c.bold(것.이름);
|
|
274
|
+
say(` ${pad(이름, 26)} ${pad(몇자(것), 10, 'right')}`);
|
|
275
|
+
if (것.탈) say(` ${c.yellow(것.탈)}`);
|
|
276
|
+
}
|
|
277
|
+
if (본것.항목.some((x) => x.키 === 'model' && x.열쇠?.length)) {
|
|
278
|
+
say(` ${pad(c.bold('잠긴 열쇠'), 26)} ${pad('', 10)} ${c.gray(본것.열쇠보관)}`);
|
|
279
|
+
}
|
|
280
|
+
say('');
|
|
281
|
+
rule('안 지웁니다', 74);
|
|
282
|
+
for (const 것 of 본것.굳은것) {
|
|
283
|
+
say(` ${pad(c.bold(것.이름), 26)} ${pad(몇자(것), 10, 'right')} ${c.gray('all --hard 를 줘야 지웁니다')}`);
|
|
284
|
+
}
|
|
285
|
+
const 남길것 = 본것.안건드림.filter((x) => x.있나).map((x) => x.이름);
|
|
286
|
+
say(` ${pad(c.bold('사람이 적은 것'), 26)} ${pad('', 10)} ${남길것.length ? c.gray(남길것.join(' · ')) : c.gray('없음')}`);
|
|
287
|
+
say(` ${c.gray(' 어떤 길로도 안 건드립니다.')}`);
|
|
288
|
+
say('');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function 지운뒤적기(결과) {
|
|
292
|
+
say('');
|
|
293
|
+
// 「완료」 한 줄은 확인이 안 된다. 무엇이 몇 개 없어졌는지 적는다.
|
|
294
|
+
if (!결과.지운것.length) {
|
|
295
|
+
say(` ${mark.warn} ${c.gray('지울 것이 없었습니다 — 이미 비어 있습니다.')}`);
|
|
296
|
+
}
|
|
297
|
+
for (const x of 결과.지운것) {
|
|
298
|
+
const 셈 = x.몇 === null ? '' : c.gray(` ${x.몇}${x.단위}`);
|
|
299
|
+
say(` ${mark.ok} ${c.bold(x.이름)}${셈}`);
|
|
300
|
+
for (const 자리 of x.자리들) say(` ${c.gray(자리)}`);
|
|
301
|
+
}
|
|
302
|
+
if (결과.열쇠) {
|
|
303
|
+
say(결과.열쇠.지움
|
|
304
|
+
? ` ${mark.ok} ${c.bold('잠금장치의 열쇠')} ${c.gray(결과.열쇠.방식)}`
|
|
305
|
+
: ` ${mark.warn} ${c.gray(`잠금장치 — ${결과.열쇠.왜}`)}`);
|
|
306
|
+
}
|
|
307
|
+
for (const x of 결과.못한것) {
|
|
308
|
+
say(` ${mark.no} ${x.이름} ${c.gray(x.자리)}`);
|
|
309
|
+
say(` ${c.yellow(x.왜)}`);
|
|
310
|
+
}
|
|
311
|
+
say('');
|
|
312
|
+
return 결과.못한것.length ? 1 : 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* `deel reset [갈래] [--hard] [--yes]`
|
|
317
|
+
*
|
|
318
|
+
* 인자 없이 부르면 **아무것도 안 지운다.** 보여 주고 묻는다.
|
|
319
|
+
*/
|
|
320
|
+
export async function runReset(args = [], flags = {}) {
|
|
321
|
+
const home = homeDir();
|
|
322
|
+
const root = flags.root ? String(flags.root) : process.cwd();
|
|
323
|
+
const hard = flags.hard === true || flags.hard === 'true';
|
|
324
|
+
const 묻지마 = flags.yes === true || flags.yes === 'true';
|
|
325
|
+
|
|
326
|
+
let 무엇 = String(args[0] ?? '').trim().toLowerCase();
|
|
327
|
+
if (무엇 && !갈래들.includes(무엇)) {
|
|
328
|
+
say('');
|
|
329
|
+
say(` ${mark.no} ${c.red('모르는 갈래')} ${c.bold(무엇)}`);
|
|
330
|
+
say(` ${c.gray(갈래들.join(' · '))}`);
|
|
331
|
+
say('');
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const 본것 = 살펴보기({ home, root });
|
|
336
|
+
보여주기(본것);
|
|
337
|
+
|
|
338
|
+
if (!무엇) {
|
|
339
|
+
/*
|
|
340
|
+
* 물어볼 사람이 없으면 안 묻는다.
|
|
341
|
+
*
|
|
342
|
+
* 파이프·CI 에서 `deel reset` 은 「무엇이 있나 보여 줘」 로 읽히는 것이
|
|
343
|
+
* 맞다. 여기서 답을 기다리면 스크립트가 그대로 멈춘다. 실제로 검사에서
|
|
344
|
+
* 한 번 멈춰 봤고, 멈춘 검사는 실패보다 나쁘다.
|
|
345
|
+
*/
|
|
346
|
+
if (!process.stdin.isTTY) {
|
|
347
|
+
say(` ${c.gray('무엇을 지울지 같이 주세요:')} ${c.cyan(`deel reset <${갈래들.join('|')}>`)}`);
|
|
348
|
+
say('');
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
const 답 = (await ask(`무엇을 지울까요? ${c.gray(갈래들.join(' · '))}`, { def: '' })).trim().toLowerCase();
|
|
352
|
+
if (!답) { say(` ${c.gray('그만둡니다. 아무것도 안 지웠습니다.')}`); say(''); return 0; }
|
|
353
|
+
if (!갈래들.includes(답)) {
|
|
354
|
+
say(` ${mark.no} ${c.red('모르는 갈래')} ${c.bold(답)} ${c.gray('— 아무것도 안 지웠습니다.')}`);
|
|
355
|
+
say('');
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
무엇 = 답;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 되돌릴 수 없다는 것을 **지우기 전에** 적는다.
|
|
362
|
+
const 볼것 = 무엇 === 'all'
|
|
363
|
+
? [...본것.항목.filter((x) => x.all), ...(hard ? 본것.굳은것 : [])]
|
|
364
|
+
: 본것.항목.filter((x) => x.키 === 무엇);
|
|
365
|
+
say(` ${c.bold(`지울 것 — ${무엇}${hard ? ' --hard' : ''}`)}`);
|
|
366
|
+
for (const 것 of 볼것) say(` ${c.gray('·')} ${것.이름} ${몇자(것)} ${c.gray('→')} ${것.뒤}`);
|
|
367
|
+
if (무엇 === 'all' && !hard) {
|
|
368
|
+
say(` ${c.gray('되돌리기 스냅샷·감사기록은 그대로 둡니다 (--hard 를 주면 그것도 지웁니다).')}`);
|
|
369
|
+
}
|
|
370
|
+
if (무엇 === 'all') {
|
|
371
|
+
say(` ${c.gray('플러그인은 안 지웁니다 —')} ${c.cyan('deel reset plugins')}`);
|
|
372
|
+
}
|
|
373
|
+
say(` ${c.yellow('되돌릴 수 없습니다.')}`);
|
|
374
|
+
say('');
|
|
375
|
+
|
|
376
|
+
if (!묻지마) {
|
|
377
|
+
if (!process.stdin.isTTY) {
|
|
378
|
+
say(` ${mark.no} ${c.yellow('물어볼 자리가 없습니다.')} ${c.gray('스크립트에서는')} ${c.cyan('--yes')} ${c.gray('를 주세요.')}`);
|
|
379
|
+
say('');
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
if (!await confirm('정말 지울까요?', false)) {
|
|
383
|
+
say(` ${c.gray('그만둡니다. 아무것도 안 지웠습니다.')}`);
|
|
384
|
+
say('');
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
// 되돌릴 안전망까지 지우는 것은 한 번 더 묻는다. 여기는 실수하면
|
|
388
|
+
// 「고친 파일을 되돌린다」 가 영영 안 된다.
|
|
389
|
+
if (무엇 === 'all' && hard && !await confirm(c.red('되돌리기 스냅샷·감사기록까지 지웁니다. 계속할까요?'), false)) {
|
|
390
|
+
say(` ${c.gray('그만둡니다. 아무것도 안 지웠습니다.')}`);
|
|
391
|
+
say('');
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return 지운뒤적기(지우기(무엇, { home, root, hard }));
|
|
397
|
+
}
|
package/src/safety/audit.js
CHANGED
|
@@ -1,35 +1,122 @@
|
|
|
1
1
|
// 감사 로그. 무엇을 언제 어떻게 했는지 전부 남긴다.
|
|
2
2
|
// 자율 실행을 사내에 설득할 때 이 파일이 근거가 된다.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { appendFileSync, mkdirSync, existsSync, readFileSync, chmodSync } from 'node:fs';
|
|
5
|
+
import { 가리기 } from './secrets.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 감사기록에 넘길 '아는 열쇠' 를 묻는 길. loop.js 가 대화 쪽에서 쓰는 것과 같은 값이다.
|
|
9
|
+
* 값이 아니라 길로 주는 까닭은 Audit 만들기 머리말에 적어 뒀다.
|
|
10
|
+
*/
|
|
11
|
+
export const 열쇠묻기 = (conn) => () => [conn?.key, process.env.DEEL_API_KEY].filter(Boolean);
|
|
5
12
|
|
|
6
13
|
export class Audit {
|
|
7
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} root 작업 폴더
|
|
16
|
+
* @param {object} o
|
|
17
|
+
* @param {string[]|(() => string[])} o.열쇠들 정확히 아는 비밀 (설정에 든 게이트웨이 열쇠).
|
|
18
|
+
* 값이 아니라 **묻는 길**로 받아도 된다 — /model 로 갈아타면 conn 이 통째로
|
|
19
|
+
* 바뀌므로, 만들 때 붙잡아 둔 값은 그 판이 끝나기 전에 옛것이 된다.
|
|
20
|
+
*/
|
|
21
|
+
constructor(root, { 열쇠들 = [] } = {}) {
|
|
8
22
|
const dir = join(root, '.deel');
|
|
9
23
|
mkdirSync(dir, { recursive: true });
|
|
10
24
|
this.file = join(dir, 'audit.jsonl');
|
|
11
25
|
this.session = `${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '')}`;
|
|
26
|
+
/** 적으려다 못 적은 건수와 첫 까닭. 0 이 아니면 이 세션의 기록은 불완전하다. */
|
|
27
|
+
this.못쓴수 = 0;
|
|
28
|
+
this.못쓴까닭 = null;
|
|
29
|
+
this.열쇠들 = 열쇠들;
|
|
30
|
+
/** 파일 권한을 건 결과. `{모드}` 면 걸었고, `{못함}` 이면 못 걸었다. */
|
|
31
|
+
this.잠금 = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* ── 적기 전에 가린다 ────────────────────────────────────────────────
|
|
36
|
+
*
|
|
37
|
+
* 여기 적히는 target 은 Bash 명령줄 그 자체다. 모델은 열쇠를 헤더로
|
|
38
|
+
* (`-H "Authorization: Bearer …"`), 환경변수로(`DEEL_API_KEY=… node x`),
|
|
39
|
+
* 주소 안에 박아서(`https://사람:토큰@github.com/…`) 넘긴다.
|
|
40
|
+
*
|
|
41
|
+
* 도구 **결과**는 loop.js 가 secrets.js 로 가려서 대화에 싣는데, 감사기록은
|
|
42
|
+
* 그 길을 안 지난다. 그래서 대화에서는 지운 값이 디스크에는 평문으로 남았다 —
|
|
43
|
+
* 그것도 `deel audit` 로 사내 심사 자료에 딸려 나가는 파일에.
|
|
44
|
+
*
|
|
45
|
+
* 가리는 것은 자유롭게 적히는 글뿐이다. 도구 이름·성패 같은 정해진 값은
|
|
46
|
+
* 안 건드린다 — 넓게 잡으면 「무엇을 했는지」 가 «가림» 으로 뭉개진다.
|
|
47
|
+
*/
|
|
48
|
+
#가린것(값) {
|
|
49
|
+
if (값 == null) return 값;
|
|
50
|
+
const 것들 = typeof this.열쇠들 === 'function' ? this.열쇠들() : this.열쇠들;
|
|
51
|
+
return 가리기(String(값), { 열쇠들: 것들 ?? [] }).글;
|
|
12
52
|
}
|
|
13
53
|
|
|
54
|
+
/*
|
|
55
|
+
* 못 적어도 던지지 않는다 — 기록을 남기려다 작업을 죽이면 본말이 뒤집힌다.
|
|
56
|
+
*
|
|
57
|
+
* 다만 **조용히 삼키면 안 된다.** 이 파일의 첫 줄이 파는 문장이
|
|
58
|
+
* 「무엇을 언제 어떻게 했는지 전부 남는다」 이고, /mcp 화면은 대놓고
|
|
59
|
+
* `.deel/audit.jsonl 에 남습니다` 라고 적어 준다. 디스크가 차거나 폴더가
|
|
60
|
+
* 읽기 전용이면 그 두 문장이 거짓이 되는데, 그때도 화면은 아무 말이 없었다.
|
|
61
|
+
* 더 나쁜 건 증거모으기다 — 기록이 안 적히면 「고쳤는데 확인 안 됨」이
|
|
62
|
+
* 아니라 아예 **안 고친 것처럼** 보고서가 짧아진다.
|
|
63
|
+
* 그래서 못 적은 건수를 세어 두고, 보고서와 화면이 그걸 말하게 한다.
|
|
64
|
+
*/
|
|
14
65
|
write(kind, data) {
|
|
15
66
|
const rec = { at: new Date().toISOString(), session: this.session, kind, ...data };
|
|
16
|
-
try {
|
|
67
|
+
try {
|
|
68
|
+
appendFileSync(this.file, JSON.stringify(rec) + '\n', 'utf8');
|
|
69
|
+
this.#잠그기();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.못쓴수 += 1;
|
|
72
|
+
if (!this.못쓴까닭) this.못쓴까닭 = err?.message ?? String(err);
|
|
73
|
+
}
|
|
17
74
|
return rec;
|
|
18
75
|
}
|
|
19
76
|
|
|
77
|
+
/*
|
|
78
|
+
* 만들자마자 본인만 읽게 잠근다 (config.js 가 설정 파일에 하는 것과 같다).
|
|
79
|
+
*
|
|
80
|
+
* 가리기를 지나도 여기에는 무엇을 언제 어디서 했는지가 남는다 — 파일 경로,
|
|
81
|
+
* 돌린 명령, 사람이 친 말. 같은 PC 를 여럿이 쓰거나 홈이 공유 폴더에 있으면
|
|
82
|
+
* 그게 그대로 남에게 읽힌다. 한 번만 건다 — 줄마다 걸 일이 아니다.
|
|
83
|
+
* 윈도우(NTFS)에서는 chmod 가 아무 일도 안 한다. 거기서는 못 잠근 것이지
|
|
84
|
+
* 잠근 척하지 않는다 — 보관방식 화면이 그 사실을 따로 말한다.
|
|
85
|
+
*
|
|
86
|
+
* 건 결과를 `잠금` 에 남긴다. "본인만 읽게 잠근다" 는 말은 지켜지는지
|
|
87
|
+
* **밖에서 볼 수 있어야** 뜻이 있다 (keystore.js 의 마지막명령줄 과 같은
|
|
88
|
+
* 까닭). 윈도우에서 값이 남는다고 실제로 잠겼다는 뜻은 아니다 —
|
|
89
|
+
* 우리가 걸어는 봤다는 뜻이다.
|
|
90
|
+
*/
|
|
91
|
+
#잠갔나 = false;
|
|
92
|
+
#잠그기() {
|
|
93
|
+
if (this.#잠갔나) return;
|
|
94
|
+
this.#잠갔나 = true;
|
|
95
|
+
try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
|
|
96
|
+
catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 기록이 새고 있나. 새고 있으면 `{수, 까닭}`, 멀쩡하면 null. */
|
|
100
|
+
못쓴것() {
|
|
101
|
+
return this.못쓴수 ? { 수: this.못쓴수, 까닭: this.못쓴까닭 } : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
20
104
|
tool(name, args, result) {
|
|
21
105
|
return this.write('tool', {
|
|
22
106
|
tool: name,
|
|
23
|
-
//
|
|
107
|
+
// purpose 는 하위 작업(Task)이 쓰는 이름이다. 이게 없으면 감사기록에
|
|
24
108
|
// '하위 작업을 돌렸다' 만 남고 **무엇을** 돌렸는지가 안 남는다.
|
|
25
|
-
|
|
109
|
+
// 옛 이름(목적)도 같이 본다 — 이름을 바꾼 판 이전의 기록이 남아 있고,
|
|
110
|
+
// 옛 이름으로 부르는 모델도 있다.
|
|
111
|
+
target: this.#가린것(args?.file_path ?? args?.path ?? args?.pattern ?? args?.command
|
|
112
|
+
?? args?.purpose ?? args?.목적 ?? null),
|
|
26
113
|
ok: !result?.error,
|
|
27
|
-
note: result?.error ?? result?.summary ?? null,
|
|
114
|
+
note: this.#가린것(result?.error ?? result?.summary ?? null),
|
|
28
115
|
});
|
|
29
116
|
}
|
|
30
117
|
|
|
31
|
-
turn(text) { return this.write('turn', { text: String(text).slice(0, 500) }); }
|
|
32
|
-
blocked(why, what) { return this.write('blocked', { why, what: String(what).slice(0, 300) }); }
|
|
118
|
+
turn(text) { return this.write('turn', { text: this.#가린것(String(text).slice(0, 500)) }); }
|
|
119
|
+
blocked(why, what) { return this.write('blocked', { why, what: this.#가린것(String(what).slice(0, 300)) }); }
|
|
33
120
|
undo(info) { return this.write('undo', info); }
|
|
34
121
|
|
|
35
122
|
recent(n = 20) {
|