deel-local-cli 1.20.10 → 1.20.12
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 +4 -2
- package/README.md +4 -2
- package/bin/deel.js +13 -3
- package/package.json +1 -1
- package/src/acp/serve.js +5 -3
- package/src/agent/commit.js +105 -21
- package/src/agent/loop.js +39 -4
- package/src/agent/session.js +279 -33
- package/src/agent/sessionui.js +14 -4
- package/src/agent/store.js +133 -33
- package/src/agent/threads.js +52 -1
- package/src/commands.js +146 -13
- package/src/config.js +87 -9
- package/src/i18n/en.js +7 -5
- package/src/i18n/ja.js +4 -2
- package/src/i18n/ko.js +7 -5
- package/src/i18n/zh.js +4 -2
- package/src/lsp/client.js +82 -19
- package/src/oneshot.js +3 -2
- package/src/plugins/manage.js +15 -6
- package/src/repl.js +55 -2
- package/src/reset.js +37 -6
- package/src/safety/hooks.js +4 -1
- package/src/safety/runmode.js +15 -0
- package/src/safety/undo.js +98 -8
- package/src/setup.js +26 -5
- package/src/skills/discover.js +10 -1
- package/src/tools/fsutil.js +7 -1
- package/src/tools/index.js +427 -30
- package/src/tools/webfetch.js +84 -20
- package/src/ui/intro.js +2 -1
- package/src/ui/md.js +123 -25
- package/src/ui/motion.js +40 -9
- package/src/ui/office.js +22 -1
- package/src/ui/screen.js +60 -4
- package/src/ui/working.js +4 -1
package/src/reset.js
CHANGED
|
@@ -31,6 +31,7 @@ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
|
31
31
|
import { c, say, mark, rule, pad } from './ui/ansi.js';
|
|
32
32
|
import { ask, confirm } from './ui/prompt.js';
|
|
33
33
|
import { homeDir } from './config.js';
|
|
34
|
+
import { pluginsDir } from './plugins/manage.js';
|
|
34
35
|
import { 잠긴것인가, 잠금지우기, 보관방식 } from './safety/keystore.js';
|
|
35
36
|
|
|
36
37
|
/** 사람이 칠 수 있는 이름. bin/deel.js 의 도움말·완성 목록과 같아야 한다. */
|
|
@@ -41,16 +42,28 @@ export const 갈래들 = ['model', 'memory', 'sessions', 'learned', 'plugins', '
|
|
|
41
42
|
// 세다가 나는 탈은 전부 삼킨다. 이건 '무엇이 있나' 를 보여 주려는 것이지
|
|
42
43
|
// 검사가 아니다 — 한 줄 못 셌다고 초기화 자체를 못 하게 하면 안 된다.
|
|
43
44
|
|
|
45
|
+
/*
|
|
46
|
+
* 다만 **없는 것과 못 읽은 것은 가른다.** 둘 다 0 으로 적으면, 지우기 전
|
|
47
|
+
* 확인 화면이 「기억 0줄 → 돌아오지 않습니다」 라고 말한다. 사람은 잃을 것이
|
|
48
|
+
* 없다고 읽고 넘기는데, 바로 다음 줄에서 폴더가 통째로 사라진다.
|
|
49
|
+
* 없으면 0, 못 읽으면 null 이다 — null 은 화면에서 「알 수 없음」 으로 뜬다
|
|
50
|
+
* (설정살피기 가 이미 그렇게 하고 있다).
|
|
51
|
+
*/
|
|
44
52
|
function 줄수(파일) {
|
|
53
|
+
if (!existsSync(파일)) return 0;
|
|
45
54
|
try { return readFileSync(파일, 'utf8').split('\n').filter((l) => l.trim()).length; }
|
|
46
|
-
catch { return
|
|
55
|
+
catch { return null; }
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
function 안에것(폴더, 거르기 = () => true) {
|
|
59
|
+
if (!existsSync(폴더)) return 0;
|
|
50
60
|
try { return readdirSync(폴더, { withFileTypes: true }).filter(거르기).length; }
|
|
51
|
-
catch { return
|
|
61
|
+
catch { return null; }
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
/** 여러 자리를 센 것을 하나로 모은다. 한 자리라도 못 셌으면 못 센 것이다. */
|
|
65
|
+
const 셈모으기 = (값들) => (값들.some((n) => n === null) ? null : 값들.reduce((a, n) => a + n, 0));
|
|
66
|
+
|
|
54
67
|
/**
|
|
55
68
|
* 설정을 읽어 본다. **깨져 있어도 답한다.**
|
|
56
69
|
*
|
|
@@ -68,10 +81,21 @@ export function 설정살피기(파일) {
|
|
|
68
81
|
return { 있나: true, 프로필: null, 잠긴열쇠: [], 왜: `읽을 수 없습니다 (${한줄})` };
|
|
69
82
|
}
|
|
70
83
|
const 목록 = Array.isArray(j?.profiles) ? j.profiles : [];
|
|
84
|
+
/*
|
|
85
|
+
* 연결 말고 **다른 것**이 이 파일에 적혀 있나.
|
|
86
|
+
*
|
|
87
|
+
* 저장소의 `.deel/config.json` 에는 프로필 대신 `mode` 나
|
|
88
|
+
* `permissions.deny` 만 적어 두는 쓰임이 있다 (config.js 가 권하는 그
|
|
89
|
+
* 쓰임이다). 그런 파일을 `deel reset model` 이 통째로 지우면, 사람이 손으로
|
|
90
|
+
* 적어 둔 **금지 규칙**이 초기화 한 번에 없어진다. 이 파일 머리말이
|
|
91
|
+
* 「사람이 손으로 적은 것은 어떤 길로도 안 지운다」 고 적어 둔 그 약속이다.
|
|
92
|
+
*/
|
|
93
|
+
const 연결칸 = new Set(['profiles', 'active', '정책주소', 'api-version']);
|
|
71
94
|
return {
|
|
72
95
|
있나: true,
|
|
73
96
|
프로필: 목록.length,
|
|
74
97
|
잠긴열쇠: 목록.map((p) => p?.apiKey).filter((k) => 잠긴것인가(k)),
|
|
98
|
+
다른것: Object.keys(j ?? {}).filter((k) => !연결칸.has(k)),
|
|
75
99
|
왜: '',
|
|
76
100
|
};
|
|
77
101
|
}
|
|
@@ -91,12 +115,15 @@ export function 살펴보기({ home = homeDir(), root = process.cwd() } = {}) {
|
|
|
91
115
|
const 집설정 = 설정살피기(집('config.json'));
|
|
92
116
|
const 일설정 = 설정살피기(일('config.json'));
|
|
93
117
|
const 열쇠들 = [...집설정.잠긴열쇠, ...일설정.잠긴열쇠];
|
|
118
|
+
// 저장소 설정에 연결 말고 다른 것이 적혀 있으면 그 파일은 안 건드린다.
|
|
119
|
+
const 일설정남길까 = 일설정.있나 && (일설정.프로필 === null || (일설정.다른것?.length ?? 0) > 0);
|
|
120
|
+
const 플러그인자리 = pluginsDir(home);
|
|
94
121
|
|
|
95
122
|
const 항목 = [
|
|
96
123
|
{
|
|
97
124
|
키: 'model',
|
|
98
125
|
이름: '연결·프로필',
|
|
99
|
-
자리: [집('config.json'), 일('config.json')],
|
|
126
|
+
자리: [집('config.json'), ...(일설정남길까 ? [] : [일('config.json')])],
|
|
100
127
|
몇: 집설정.프로필 === null || 일설정.프로필 === null
|
|
101
128
|
? null : (집설정.프로필 ?? 0) + (일설정.프로필 ?? 0),
|
|
102
129
|
단위: '개',
|
|
@@ -139,7 +166,7 @@ export function 살펴보기({ home = homeDir(), root = process.cwd() } = {}) {
|
|
|
139
166
|
키: '만든것',
|
|
140
167
|
이름: '증거·내보낸 것·임시',
|
|
141
168
|
자리: [일('증거'), 일('export'), 일('tmp'), 일('붙인그림')],
|
|
142
|
-
몇: [일('증거'), 일('export'), 일('tmp'), 일('붙인그림')].
|
|
169
|
+
몇: 셈모으기([일('증거'), 일('export'), 일('tmp'), 일('붙인그림')].map((p) => 안에것(p))),
|
|
143
170
|
단위: '개',
|
|
144
171
|
뒤: '안전합니다',
|
|
145
172
|
all: true,
|
|
@@ -148,8 +175,11 @@ export function 살펴보기({ home = homeDir(), root = process.cwd() } = {}) {
|
|
|
148
175
|
{
|
|
149
176
|
키: 'plugins',
|
|
150
177
|
이름: '플러그인',
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
// 깔리는 자리와 **같은 함수**로 고른다 (plugins/manage.js 의 pluginsDir).
|
|
179
|
+
// 두 자리가 갈리면 화면이 「0개 · 이미 비어 있습니다」 를 찍는데
|
|
180
|
+
// 플러그인은 그대로 살아 있다.
|
|
181
|
+
자리: [플러그인자리],
|
|
182
|
+
몇: 안에것(플러그인자리, (e) => e.isDirectory() && !e.name.startsWith('.')),
|
|
153
183
|
단위: '개',
|
|
154
184
|
뒤: c.yellow('다시 설치해야 합니다'),
|
|
155
185
|
// 따로 고를 때만 지운다. 다시 받는 데 시간이 걸리고, 사람이 초기화하려는
|
|
@@ -181,6 +211,7 @@ export function 살펴보기({ home = homeDir(), root = process.cwd() } = {}) {
|
|
|
181
211
|
// 어떤 길로도 안 지우는 것. 화면에 이름을 적어 둔다 — 「왜 이건 안 지웠지」
|
|
182
212
|
// 를 나중에 묻게 하지 않으려고.
|
|
183
213
|
const 안건드림 = [
|
|
214
|
+
{ 이름: '.deel/config.json (연결 말고 다른 것이 적혀 있습니다)', 있나: 일설정남길까 },
|
|
184
215
|
{ 이름: '.deel/mcp.json', 있나: existsSync(일('mcp.json')) },
|
|
185
216
|
{ 이름: '.deelignore', 있나: existsSync(join(root, '.deelignore')) },
|
|
186
217
|
{ 이름: 'DEEL.md', 있나: existsSync(join(root, 'DEEL.md')) },
|
package/src/safety/hooks.js
CHANGED
|
@@ -156,7 +156,10 @@ export function 훅펴기(raw, 출처) {
|
|
|
156
156
|
if (Array.isArray(것)) {
|
|
157
157
|
for (const x of 것) {
|
|
158
158
|
const 자리 = 자리풀기(x?.때 ?? x?.자리 ?? x?.event);
|
|
159
|
-
|
|
159
|
+
// 바로 위에서 `때 · 자리 · event` 셋을 다 보고 골랐는데, 무엇이 틀렸는지
|
|
160
|
+
// 적을 때는 `자리` 만 빠져 있었다. `{자리: '오타'}` 로 적어 둔 사람은
|
|
161
|
+
// 「모르는 자리: (없음)」 을 받는다 — 제가 적은 글자가 화면에 안 나온다.
|
|
162
|
+
if (!자리) { 버린것.push(`모르는 자리: ${x?.때 ?? x?.자리 ?? x?.event ?? '(없음)'}`); continue; }
|
|
160
163
|
const h = 한훅(자리, x?.도구 ?? x?.matcher ?? '*', x, 출처);
|
|
161
164
|
if (h) 훅들.push(h); else 버린것.push(`${자리}: 명령이나 무늬가 잘못 적혀 있습니다`);
|
|
162
165
|
}
|
package/src/safety/runmode.js
CHANGED
|
@@ -89,6 +89,21 @@ export function 지금모드({ online = false, offline = false } = {}) {
|
|
|
89
89
|
return 기본;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 봉인이 켜져 있나 — 깃발·프로필·설정 **셋 중 하나라도** 켜져 있으면 켜진 것이다.
|
|
94
|
+
*
|
|
95
|
+
* 여태 부르는 자리마다 `깃발 ?? prof.offline ?? cfg.offline` 으로 골랐다.
|
|
96
|
+
* `??` 는 null·undefined 에서만 다음 칸으로 넘어가는데, 부르는 쪽이 깃발을
|
|
97
|
+
* **언제나 boolean 으로** 넘겼다 (bin/deel.js 의 `flags.offline === true || …`).
|
|
98
|
+
* 그래서 첫 칸이 늘 false 로 차 있었고 뒤의 둘은 한 번도 안 읽혔다 —
|
|
99
|
+
* 관리자가 정책에 못 박아 둔 봉인도, 사람이 제 손으로 설정에 적은 봉인도
|
|
100
|
+
* 아무 데도 안 붙어 있었다. 정책은 켜기만 하므로 여기서는 OR 로 모은다.
|
|
101
|
+
*/
|
|
102
|
+
export function 봉인됐나({ 깃발 = undefined, prof = null, cfg = null } = {}) {
|
|
103
|
+
return 깃발 === true || 깃발 === 'true'
|
|
104
|
+
|| prof?.offline === true || cfg?.offline === true;
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
/**
|
|
93
108
|
* 이 주소가 바깥인가.
|
|
94
109
|
*
|
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/setup.js
CHANGED
|
@@ -12,7 +12,7 @@ import { 보관방식 } from './safety/keystore.js';
|
|
|
12
12
|
import { 애저풀기, 애저base } from './backend/azure.js';
|
|
13
13
|
import { allowEndpoint } from './safety/network.js';
|
|
14
14
|
import { 인증서설정, 인증서등록 } from './backend/clientcert.js';
|
|
15
|
-
import {
|
|
15
|
+
import { 바깥인가, 봉인됐나 } from './safety/runmode.js';
|
|
16
16
|
import { 제공자들, 제공자고르기, 어디것일까, 주소후보, 막힌까닭 } from './providers/index.js';
|
|
17
17
|
import { writeFileSync } from 'node:fs';
|
|
18
18
|
|
|
@@ -107,8 +107,21 @@ async function chooseModel(found) {
|
|
|
107
107
|
return items[i].label;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
/*
|
|
111
|
+
* 진단을 돌리고 결과를 화면에 그린다.
|
|
112
|
+
*
|
|
113
|
+
* **봉인은 진단에도 걸린다.** 여기가 빠져 있었다 — 관리자가 정책으로 밖을
|
|
114
|
+
* 막아 둔 PC 에서도 `deel diagnose` 는 그냥 나갔다. 자물쇠가 명령 하나로
|
|
115
|
+
* 열리면 자물쇠가 아니다. 사내망 주소는 봉인에서도 그대로 간다.
|
|
116
|
+
*/
|
|
117
|
+
export async function runProbe(conn, { out = null, 봉인 = false } = {}) {
|
|
118
|
+
if (봉인 && 바깥인가(conn.base)) {
|
|
119
|
+
say('');
|
|
120
|
+
say(` ${mark.no} ${c.red('봉인되어 있어 이 주소는 안 두드립니다.')} ${c.gray(conn.base)}`);
|
|
121
|
+
say(` ${c.gray('관리 정책이나 설정에 offline 이 켜져 있습니다. 사내망 주소는 그대로 됩니다.')}`);
|
|
122
|
+
say('');
|
|
123
|
+
return { facts: null, results: null, v: null, 봉인: true };
|
|
124
|
+
}
|
|
112
125
|
allowEndpoint(conn.base); // 진단도 이 주소 하나로만 나간다
|
|
113
126
|
renderHeader({ shape: conn.kind, base: conn.base, auth: conn.auth, model: conn.model });
|
|
114
127
|
const { facts, results } = await probe(conn, renderLine);
|
|
@@ -344,9 +357,11 @@ export async function runSetup() {
|
|
|
344
357
|
export async function runDiagnose(flags) {
|
|
345
358
|
banner();
|
|
346
359
|
let conn;
|
|
360
|
+
let 쓴설정 = null;
|
|
361
|
+
let 쓴프로필 = null;
|
|
347
362
|
|
|
348
363
|
if (flags.url) {
|
|
349
|
-
load(); // 설정의 api-version 을 먼저 읽는다 (runSetup 과 같은 이유)
|
|
364
|
+
쓴설정 = load(); // 설정의 api-version 을 먼저 읽는다 (runSetup 과 같은 이유)
|
|
350
365
|
const key = flags.key ?? process.env.DEEL_API_KEY ?? '';
|
|
351
366
|
const found = await connect(flags.url, key);
|
|
352
367
|
if (!found) return 1;
|
|
@@ -356,6 +371,8 @@ export async function runDiagnose(flags) {
|
|
|
356
371
|
} else {
|
|
357
372
|
const cfg = load();
|
|
358
373
|
const prof = activeProfile(cfg);
|
|
374
|
+
쓴설정 = cfg;
|
|
375
|
+
쓴프로필 = prof;
|
|
359
376
|
if (!prof) {
|
|
360
377
|
say(` ${mark.warn} 저장된 연결이 없습니다. ${c.cyan('deel setup')} 을 먼저 실행하세요.`);
|
|
361
378
|
say(` ${c.gray('또는')} deel diagnose --url <주소> --key <키> --model <모델>`);
|
|
@@ -377,7 +394,11 @@ export async function runDiagnose(flags) {
|
|
|
377
394
|
};
|
|
378
395
|
}
|
|
379
396
|
|
|
380
|
-
|
|
397
|
+
/*
|
|
398
|
+
* 봉인은 --url 로 주소를 직접 준 갈래에도 걸린다. 오히려 그쪽이 더 위험하다
|
|
399
|
+
* — 설정에 없는 주소를 그 자리에서 두드리는 길이기 때문이다.
|
|
400
|
+
*/
|
|
401
|
+
await runProbe(conn, { out: flags.out ?? null, 봉인: 봉인됐나({ 깃발: flags.offline, prof: 쓴프로필, cfg: 쓴설정 }) });
|
|
381
402
|
return 0;
|
|
382
403
|
}
|
|
383
404
|
|
package/src/skills/discover.js
CHANGED
|
@@ -11,6 +11,8 @@ import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
|
11
11
|
import { join, basename, dirname, resolve } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
+
import { homeDir } from '../config.js';
|
|
15
|
+
import { pluginsDir } from '../plugins/manage.js';
|
|
14
16
|
// 저장소에 딸려 온 스킬·명령이 시스템 글에 실리지 않게 한다 (discover 안구절 머리말).
|
|
15
17
|
import { 믿나 } from '../safety/trust.js';
|
|
16
18
|
|
|
@@ -163,6 +165,7 @@ function countSkillDirs(rootDir) {
|
|
|
163
165
|
*/
|
|
164
166
|
export function discover(root, opts = {}) {
|
|
165
167
|
const home = opts.home ?? homedir();
|
|
168
|
+
const 살림 = opts.살림 ?? (opts.home ? join(opts.home, '.deel') : homeDir());
|
|
166
169
|
const caps = { skills: opts.maxSkills ?? 400, commands: opts.maxCommands ?? 400 };
|
|
167
170
|
const skills = [];
|
|
168
171
|
const commands = [];
|
|
@@ -177,7 +180,13 @@ export function discover(root, opts = {}) {
|
|
|
177
180
|
// 같은 플러그인이 cache/ 와 marketplaces/ 양쪽에 있을 수 있다.
|
|
178
181
|
// 이름이 같으면 알맹이가 더 많은 쪽 하나만 쓴다.
|
|
179
182
|
const roots = [];
|
|
180
|
-
|
|
183
|
+
/*
|
|
184
|
+
* 우리 플러그인은 **살림 자리** 밑이다 (`~/.deel` 또는 `DEEL_HOME`).
|
|
185
|
+
* OS 집 폴더에 `.deel` 을 직접 붙이면 `DEEL_HOME` 을 쓰는 휴대용 설치에서
|
|
186
|
+
* 깔아 놓은 플러그인의 스킬이 안 보인다 — 깔리는 자리(plugins/manage.js)와
|
|
187
|
+
* 여기가 갈리기 때문이다. 클로드 것(.claude)은 그쪽 규칙이라 집 폴더 그대로다.
|
|
188
|
+
*/
|
|
189
|
+
for (const base of [pluginsDir(살림), join(home, '.claude', 'plugins')]) {
|
|
181
190
|
if (existsSync(base)) roots.push(...findPluginRoots(base));
|
|
182
191
|
}
|
|
183
192
|
const best = new Map();
|
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
|
// 사람을 한 번 더 보게 만들 뿐이고, 다 봤다고 말하는 것은 못 보게 만든다.
|