deel-local-cli 1.20.12 → 2.0.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 +96 -77
- package/README.md +119 -80
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +62 -13
- package/src/acp/serve.js +338 -30
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +506 -32
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +300 -0
- package/src/commands/model.js +852 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +452 -39
- package/src/configexplain.js +125 -14
- package/src/doctor.js +164 -26
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +507 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +11 -1
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +444 -36
- package/src/tools/fig.js +40 -9
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1029 -145
- package/src/tools/jobs.js +325 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +366 -56
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +312 -43
- package/src/tools/webfetch.js +184 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/config.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
// (믿는 폴더일 때만 — safety/trust.js).
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, rmSync } from 'node:fs';
|
|
6
6
|
import { 프록시정하기 } from './backend/proxy.js';
|
|
7
7
|
import { 셸정하기 } from './tools/shell.js';
|
|
8
8
|
import { 애저정하기 } from './backend/azure.js';
|
|
9
9
|
import { 정책읽기 } from './safety/policy.js';
|
|
10
|
-
import { 잠그기, 풀기, 잠긴것인가, 쓸수있나,
|
|
11
|
-
import { 믿나,
|
|
10
|
+
import { 잠그기, 풀기, 잠긴것인가, 쓸수있나, 보관방식, 잠그다실패한까닭 } from './safety/keystore.js';
|
|
11
|
+
import { 믿나, 프로젝트거르기, 프로젝트설정줄들, 프로젝트금지칸, 연결칸, BOM떼기 } from './safety/trust.js';
|
|
12
|
+
import { 규칙모으기 } from './safety/policy.js';
|
|
13
|
+
import { mark } from './ui/ansi.js';
|
|
12
14
|
|
|
13
15
|
// 설정이 놓이는 자리.
|
|
14
16
|
//
|
|
@@ -22,8 +24,28 @@ import { 믿나, 프로젝트거르기 } from './safety/trust.js';
|
|
|
22
24
|
function userDir() {
|
|
23
25
|
return process.env.DEEL_HOME ? resolve(process.env.DEEL_HOME) : join(homedir(), '.deel');
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
/*
|
|
28
|
+
* ── 「이 폴더」 가 언제나 process.cwd() 였다 ───────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* 여기·configPath()·읽기() 셋이 다 `process.cwd()` 를 박아 놓고 있었다.
|
|
31
|
+
* 터미널에서는 그게 맞다 — 켠 자리가 곧 일하는 자리다. 그런데 문이 하나
|
|
32
|
+
* 더 있다. 에디터(ACP)는 **제가 연 프로젝트 폴더를 요청에 담아 준다**
|
|
33
|
+
* (`session/new` 의 `cwd`). 그 값은 에디터를 띄운 자리와 다를 수 있고,
|
|
34
|
+
* 창 하나에 폴더가 여럿인 구성에서는 세션마다도 다르다.
|
|
35
|
+
*
|
|
36
|
+
* 그래서 한 세션 안에서 **두 폴더가 섞였다** — 도구는 에디터가 준 폴더에서
|
|
37
|
+
* 파일을 고치는데, 설정과 신뢰와 승인 규칙은 에디터를 띄운 폴더 것을 봤다.
|
|
38
|
+
* 열어 둔 프로젝트의 `.deel/config.json` 은 아예 안 읽히고, 남의 폴더
|
|
39
|
+
* `permissions.deny` 가 이 프로젝트에 걸렸다. 아무 말도 안 난다 —
|
|
40
|
+
* 「안 읽었다」 를 알리는 프로젝트설정소식() 조차 다른 폴더를 기준으로 났다.
|
|
41
|
+
*
|
|
42
|
+
* acp/serve.js 머리말이 바로 이 실수를 「제일 무서운 종류」 라고 적어 뒀다.
|
|
43
|
+
* 도구 쪽은 그 말대로 고쳐져 있었고, 설정 쪽만 남아 있었다.
|
|
44
|
+
*
|
|
45
|
+
* 뿌리를 받는다. 안 주면 예전처럼 켠 자리다.
|
|
46
|
+
*/
|
|
47
|
+
function projectDir(root = process.cwd()) {
|
|
48
|
+
return join(root, '.deel');
|
|
27
49
|
}
|
|
28
50
|
|
|
29
51
|
/**
|
|
@@ -43,12 +65,54 @@ export function homeDir() { return userDir(); }
|
|
|
43
65
|
* 이 값을 적으면, 다음에 켤 때 그 값이 또 무시된다. 사람은 두 번 적고 두 번
|
|
44
66
|
* 다 안 먹는 것을 보는데, 그때 무엇 때문인지 알 길이 없다.
|
|
45
67
|
*/
|
|
46
|
-
export function configPath() {
|
|
47
|
-
const local = join(projectDir(), 'config.json');
|
|
48
|
-
if (existsSync(local) && 믿나(
|
|
68
|
+
export function configPath(root = process.cwd()) {
|
|
69
|
+
const local = join(projectDir(root), 'config.json');
|
|
70
|
+
if (existsSync(local) && 믿나(root)) return local;
|
|
49
71
|
return join(userDir(), 'config.json');
|
|
50
72
|
}
|
|
51
73
|
|
|
74
|
+
/**
|
|
75
|
+
* 이 파일이 **이 PC 설정 파일 그 자체인가.**
|
|
76
|
+
*
|
|
77
|
+
* ── 집 폴더에서 켜면 두 자리가 한 파일이다 ──────────────────────────────
|
|
78
|
+
*
|
|
79
|
+
* DEEL_HOME 을 안 주면 이 PC 설정은 `~/.deel/config.json` 이다. 그런데 집
|
|
80
|
+
* 폴더(`~`)에서 켜면 「이 폴더의 프로젝트 설정」 `<폴더>/.deel/config.json` 도 바로
|
|
81
|
+
* 그 파일이다. 그걸 따로 봤더니 세 가지가 어긋났다:
|
|
82
|
+
*
|
|
83
|
+
* · 켤 때마다 「이 폴더 프로젝트 설정을 안 읽었습니다 — deel trust」 가 떴다.
|
|
84
|
+
* 제 설정인데. doctor 는 같은 파일을 ✓설정 과 ⚠프로젝트 설정 으로 두 번 적었다.
|
|
85
|
+
* · 그 말대로 `deel trust` 를 치면 **집 폴더 전체**가 믿는 폴더가 된다 — 그 아래
|
|
86
|
+
* 받아 둔 남의 저장소가 전부 믿긴다.
|
|
87
|
+
* · 믿고 나면 제 설정의 permissions.allow 를 「저장소가 적은 것이라 걷어냈다」 고
|
|
88
|
+
* 거짓말했다. 실제로는 이 PC 파일로 읽혀 그대로 걸려 있었다.
|
|
89
|
+
*
|
|
90
|
+
* reset.js 의 같은자리와 같은 기준이다 — 윈도우는 대소문자를 안 가린다.
|
|
91
|
+
*/
|
|
92
|
+
export function 집설정파일인가(파일) {
|
|
93
|
+
const [x, y] = [resolve(String(파일 ?? '')), resolve(join(userDir(), 'config.json'))];
|
|
94
|
+
return process.platform === 'win32' ? x.toLowerCase() === y.toLowerCase() : x === y;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 이 PC 설정 파일에 **직접 적힌** 칸 하나. 없거나 못 읽으면 undefined.
|
|
99
|
+
*
|
|
100
|
+
* load() 가 주는 것은 저장소 설정까지 겹친 것이라, 「사람이 이 PC 에 적어 둔 값」 이
|
|
101
|
+
* 필요한 자리에는 못 쓴다. 승인 모드(mode)가 그렇다 — 저장소가 `"mode": "auto"` 를
|
|
102
|
+
* 적어 이 PC 의 strict 를 헐겁게 만드는 길을 열면 안 된다.
|
|
103
|
+
*
|
|
104
|
+
* 못 읽는 파일은 여기서 말하지 않는다. 곧이어 load() 가 어느 파일 몇째 줄인지까지
|
|
105
|
+
* 적어 멈춘다 — 두 자리에서 따로 말하면 같은 탈이 두 번 뜬다.
|
|
106
|
+
*/
|
|
107
|
+
export function 이PC설정값(칸) {
|
|
108
|
+
const p = join(userDir(), 'config.json');
|
|
109
|
+
if (!existsSync(p)) return undefined;
|
|
110
|
+
try {
|
|
111
|
+
const j = JSON.parse(BOM떼기(readFileSync(p, 'utf8')));
|
|
112
|
+
return j && typeof j === 'object' && Object.hasOwn(j, 칸) ? j[칸] : undefined;
|
|
113
|
+
} catch { return undefined; }
|
|
114
|
+
}
|
|
115
|
+
|
|
52
116
|
const EMPTY = { version: 1, active: null, profiles: [] };
|
|
53
117
|
|
|
54
118
|
/*
|
|
@@ -76,7 +140,7 @@ export function 열쇠보관(cfg = null) {
|
|
|
76
140
|
return 보관방식(p?.apiKey ?? null);
|
|
77
141
|
}
|
|
78
142
|
|
|
79
|
-
function 잠금옮기기(cfg) {
|
|
143
|
+
function 잠금옮기기(cfg, root = process.cwd()) {
|
|
80
144
|
if (옮겨봤나 || process.env.DEEL_KEYSTORE === 'off') return;
|
|
81
145
|
옮겨봤나 = true;
|
|
82
146
|
const 평문 = (cfg.profiles ?? []).filter((p) => p?.apiKey && !잠긴것인가(p.apiKey));
|
|
@@ -85,25 +149,52 @@ function 잠금옮기기(cfg) {
|
|
|
85
149
|
|
|
86
150
|
let 옮김 = 0;
|
|
87
151
|
for (const p of 평문) {
|
|
88
|
-
const 잠근것 = 잠그기(p.apiKey);
|
|
152
|
+
const 잠근것 = 잠그기(p.apiKey, { 프로필id: p.id });
|
|
89
153
|
if (잠근것) { p.apiKey = 잠근것; 옮김 += 1; }
|
|
90
154
|
}
|
|
91
155
|
if (!옮김) return;
|
|
156
|
+
/*
|
|
157
|
+
* 하나라도 못 잠갔으면 **파일은 그대로 둔다** (머리말).
|
|
158
|
+
*
|
|
159
|
+
* 여기가 `옮김` 이 0 만 아니면 적었다. 그러면 못 잠근 열쇠는 평문 그대로 그 파일에
|
|
160
|
+
* 실려 나가는데 화면에는 「잠갔습니다」 한 줄만 떴다. 그 파일이 무엇인지 — 잠긴
|
|
161
|
+
* 파일인지 평문이 섞인 파일인지 — 아무도 모르는 상태로 백업 폴더와 다른 PC 로 간다.
|
|
162
|
+
* 반쯤 잠근 파일을 안 남기는 쪽이, 이번 판에 안 잠기는 것보다 낫다.
|
|
163
|
+
*/
|
|
164
|
+
if (옮김 < 평문.length) {
|
|
165
|
+
const 못한것 = 평문.length - 옮김;
|
|
166
|
+
소식 = `게이트웨이 열쇠 ${평문.length}개 가운데 ${못한것}개를 못 잠갔습니다`
|
|
167
|
+
+ `${잠그다실패한까닭() ? ` (${잠그다실패한까닭()})` : ''}`
|
|
168
|
+
+ ' — 반쯤 잠근 파일을 안 남기려고 설정 파일은 그대로 두었습니다 (파일에는 평문 그대로입니다)';
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
92
171
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
/*
|
|
173
|
+
* 언제나 **이 PC 파일에** 적는다. 평문 열쇠는 이 PC 설정에서만 온다 —
|
|
174
|
+
* 저장소 설정의 apiKey 는 읽을 때 걷힌다(safety/trust.js).
|
|
175
|
+
*
|
|
176
|
+
* 여기는 「읽은 자리에 되쓴다」 며 믿는 폴더면 저장소의 `.deel/config.json`
|
|
177
|
+
* 에 적었다. 그런데 넘기는 cfg 는 **합친 것**이라, 이 PC 프로필 전부가 잠긴
|
|
178
|
+
* 열쇠까지 저장소 안 파일로 복사됐다. 그 파일은 대개 git 이 따라간다.
|
|
179
|
+
* 저장소가 얹은 것은 save() 가 벗긴다(방벗기기).
|
|
180
|
+
*/
|
|
181
|
+
const 적은곳 = save(cfg);
|
|
97
182
|
const 방식 = 쓸수있나().방식 === 'keychain' ? '키체인' : 'DPAPI';
|
|
98
|
-
|
|
183
|
+
// 어느 파일에 적었는지를 말한다. 「잠갔습니다」 한 줄로는 어디가 바뀌었는지 모른다.
|
|
184
|
+
소식 = (옮김 === 1
|
|
99
185
|
? `게이트웨이 열쇠를 이 PC 계정에서만 풀리게 잠갔습니다 (${방식})`
|
|
100
|
-
: `게이트웨이 열쇠 ${옮김}개를 이 PC 계정에서만 풀리게 잠갔습니다 (${방식})
|
|
101
|
-
|
|
186
|
+
: `게이트웨이 열쇠 ${옮김}개를 이 PC 계정에서만 풀리게 잠갔습니다 (${방식})`)
|
|
187
|
+
+ ` — ${적은곳}`;
|
|
188
|
+
} catch (err) {
|
|
189
|
+
// 못 썼으면 못 썼다고 한다. 메모리에만 잠긴 채로 이번 판은 돌지만,
|
|
190
|
+
// 파일은 평문 그대로다 — 그 말을 안 하면 잠긴 줄 알고 파일을 옮긴다.
|
|
191
|
+
소식 = `열쇠를 잠갔지만 설정 파일에 못 적었습니다 (${err?.message ?? err}) — 파일에는 평문 그대로입니다`;
|
|
192
|
+
}
|
|
102
193
|
}
|
|
103
194
|
|
|
104
|
-
export function load() {
|
|
105
|
-
const cfg = 읽기();
|
|
106
|
-
잠금옮기기(cfg);
|
|
195
|
+
export function load({ root = process.cwd() } = {}) {
|
|
196
|
+
const cfg = 읽기(root);
|
|
197
|
+
잠금옮기기(cfg, root);
|
|
107
198
|
// 프록시는 설정을 읽는 자리에서 정한다. run · acp · setup · diagnose 어느 문으로
|
|
108
199
|
// 들어와도 한 번은 여기를 지나므로, 여기가 빠뜨리지 않는 유일한 자리다.
|
|
109
200
|
프록시정하기({ config: cfg });
|
|
@@ -147,7 +238,8 @@ function 정책덮기(cfg) {
|
|
|
147
238
|
}
|
|
148
239
|
cfg.정책주소 = 정책.baseUrl.trim();
|
|
149
240
|
}
|
|
150
|
-
|
|
241
|
+
// 끄지는 못한다 — 켜기만. 글자 'true' 도 켠 것이다 (safety/runmode.js 의 봉인됐나).
|
|
242
|
+
if (정책.offline === true || 정책.offline === 'true') cfg.offline = true;
|
|
151
243
|
if (Array.isArray(정책.permissions?.deny)) {
|
|
152
244
|
cfg.permissions = cfg.permissions ?? {};
|
|
153
245
|
const 있던것 = Array.isArray(cfg.permissions.deny) ? cfg.permissions.deny : [];
|
|
@@ -188,9 +280,24 @@ function 정책벗기기(cfg) {
|
|
|
188
280
|
return 사본;
|
|
189
281
|
}
|
|
190
282
|
|
|
283
|
+
/*
|
|
284
|
+
* 한 장을 읽는다. 못 읽으면 **어느 파일이 왜** 인지를 달아서 던진다.
|
|
285
|
+
*
|
|
286
|
+
* BOM 을 먼저 뗀다(safety/trust.js 의 BOM떼기). 윈도우 파워셸 5.1 의
|
|
287
|
+
* `Set-Content -Encoding UTF8` 로 깐 설정은 앞에 U+FEFF 가 붙어 있고, 그 한 글자
|
|
288
|
+
* 때문에 `deel` 의 모든 명령이 「설정 파일을 읽지 못했습니다」 로 종료코드 1 이었다.
|
|
289
|
+
*
|
|
290
|
+
* 던지는 오류에 자리·까닭을 따로 싣는 것은 doctor 때문이다. 거기는 이 오류를
|
|
291
|
+
* `catch {}` 로 삼키고 「✓ 설정 파일」 을 찍었다 — 화면에서 까닭이 사라졌다.
|
|
292
|
+
*/
|
|
191
293
|
function 한장읽기(p) {
|
|
192
|
-
try { return JSON.parse(readFileSync(p, 'utf8')); }
|
|
193
|
-
catch (err) {
|
|
294
|
+
try { return JSON.parse(BOM떼기(readFileSync(p, 'utf8'))); }
|
|
295
|
+
catch (err) {
|
|
296
|
+
const e = new Error(`설정 파일을 읽지 못했습니다: ${p}\n ${err.message}`);
|
|
297
|
+
e.설정자리 = p;
|
|
298
|
+
e.까닭 = String(err?.message ?? err);
|
|
299
|
+
throw e;
|
|
300
|
+
}
|
|
194
301
|
}
|
|
195
302
|
|
|
196
303
|
/*
|
|
@@ -204,8 +311,27 @@ function 한장읽기(p) {
|
|
|
204
311
|
* 이제 겹친다. 규칙은 하나다 — **좁히는 쪽만 이긴다.** 금지(deny)는 두 장을
|
|
205
312
|
* 합치고, 허락(allow)은 애초에 프로젝트에서 안 읽는다(safety/trust.js).
|
|
206
313
|
*/
|
|
314
|
+
/*
|
|
315
|
+
* ── 「좁히는 쪽만 이긴다」 가 칸 단위 덮기에서 깨졌다 ─────────────────
|
|
316
|
+
*
|
|
317
|
+
* 최상위 칸은 `{ ...집, ...방 }` 한 줄로 저장소가 다 이겼고, 프로필은 칸 단위로
|
|
318
|
+
* 덮였다. 그래서 믿는 폴더의 저장소가 이 PC 프로필의 `baseUrl` 한 칸만 적으면
|
|
319
|
+
* 이 PC 열쇠가 그 주소로 붙었고, `offline:false` 로 봉인을 풀고, `active` 로
|
|
320
|
+
* 제가 더한 프로필에 연결을 돌렸다. 화면에는 한 줄도 안 나왔다.
|
|
321
|
+
*
|
|
322
|
+
* 값 하나로 가를 수 있는 칸은 safety/trust.js 의 프로젝트거르기가 걷는다.
|
|
323
|
+
* 이 PC 설정을 봐야 가를 수 있는 칸(연결 칸·active)은 여기서 걷고, 걷은 것을
|
|
324
|
+
* 돌려줘서 화면이 말하게 한다.
|
|
325
|
+
*/
|
|
326
|
+
const 방흔적 = Symbol('저장소가얹은것');
|
|
327
|
+
|
|
207
328
|
function 겹치기(집, 방) {
|
|
208
329
|
const cfg = { ...집, ...방 };
|
|
330
|
+
const 걸러낸것 = [];
|
|
331
|
+
const 적기 = (칸) => {
|
|
332
|
+
const 것 = 프로젝트금지칸.find((x) => x.칸 === 칸);
|
|
333
|
+
if (것 && !걸러낸것.some((x) => x.칸 === 칸)) 걸러낸것.push(것);
|
|
334
|
+
};
|
|
209
335
|
|
|
210
336
|
/*
|
|
211
337
|
* 프로필은 **id 로** 맞춘다. 통째로 갈아치우면 이 PC 프로필이 사라진다.
|
|
@@ -231,13 +357,73 @@ function 겹치기(집, 방) {
|
|
|
231
357
|
for (const p of 방.profiles ?? []) {
|
|
232
358
|
const 자리 = p?.id != null && 자리표.has(p.id) ? 자리표.get(p.id)
|
|
233
359
|
: (p?.name != null && 이름자리.has(p.name) ? 이름자리.get(p.name) : null);
|
|
234
|
-
if (자리 != null) {
|
|
360
|
+
if (자리 != null) {
|
|
361
|
+
/*
|
|
362
|
+
* 연결 칸은 이 PC 값이 남는다. 칸 단위로 덮으면 이 PC 열쇠(apiKey·열쇠받기)는
|
|
363
|
+
* 그대로인 채 주소만 저장소 것이 된다. 같은 값을 적은 것은 걷어도 달라질
|
|
364
|
+
* 것이 없으니 말하지 않는다 — 거짓 경고는 진짜 경고를 죽인다.
|
|
365
|
+
*/
|
|
366
|
+
const 덮을것 = { ...p };
|
|
367
|
+
/*
|
|
368
|
+
* 겹치는 프로필의 id 는 **언제나** 이 PC 것이 남는다. 바뀌면 active 와
|
|
369
|
+
* `DEEL_KEY_<id>` 가 다른 것을 가리킨다.
|
|
370
|
+
*
|
|
371
|
+
* 여기가 이 PC 프로필에 id 가 **있을 때만** 그렇게 했다. 그래서 id 를 안 적은
|
|
372
|
+
* 프로필(문서의 예시 모양이 그렇다)에는 저장소가 적은 id 가 그대로 붙었고, 그
|
|
373
|
+
* 순간 그 프로필의 열쇠 이름을 저장소가 고른 것이 된다 — 사람이 넣어 둔 다른
|
|
374
|
+
* 열쇠(`DEEL_KEY_<그이름>`)가 저장소가 고른 주소로 나간다. 저장소가 더한
|
|
375
|
+
* 프로필은 표식으로 막아 뒀는데(출처), 이쪽은 이 PC 프로필이라 그 막음을
|
|
376
|
+
* 통째로 비켜 갔다.
|
|
377
|
+
*/
|
|
378
|
+
delete 덮을것.id;
|
|
379
|
+
for (const 칸 of 연결칸) {
|
|
380
|
+
if (덮을것[칸] === undefined) continue;
|
|
381
|
+
if (덮을것[칸] !== 모음[자리]?.[칸]) 적기('profiles[].baseUrl·kind·auth·제공자');
|
|
382
|
+
delete 덮을것[칸];
|
|
383
|
+
}
|
|
384
|
+
모음[자리] = { ...모음[자리], ...덮을것 };
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
235
387
|
if (p?.id == null && p?.name == null) { 이름없는것.push(p); continue; }
|
|
236
|
-
|
|
388
|
+
/*
|
|
389
|
+
* 저장소가 더한 프로필에는 **글자 칸으로** 표식을 단다. 열쇠를 푸는 자
|
|
390
|
+
* (resolveKey)가 이걸 보고 DEEL_API_KEY 로 안 떨어진다. Symbol 로 달면 JSON
|
|
391
|
+
* 왕복·structuredClone 에서 사라지고, 사라지면 **새는 쪽으로** 실패한다.
|
|
392
|
+
* 저장소가 제 손으로 적은 `출처` 는 뒤에 펼쳐서 덮는다.
|
|
393
|
+
*/
|
|
394
|
+
모음.push({ ...p, 출처: '저장소' });
|
|
237
395
|
}
|
|
238
396
|
cfg.profiles = 모음;
|
|
239
397
|
if (이름없는것.length) 버린프로필 = 이름없는것.length;
|
|
240
398
|
|
|
399
|
+
/*
|
|
400
|
+
* active 는 이 PC 프로필을 고를 때만 받는다. 저장소가 더한 프로필을 가리키면
|
|
401
|
+
* 저장소가 연결을 제 주소로 돌리는 셈이다.
|
|
402
|
+
*
|
|
403
|
+
* 「이 PC 프로필인가」 는 **실제로 고를 것**으로 가른다. activeProfile 은 id 로
|
|
404
|
+
* 고르므로, 이 PC 프로필 이름(work)과 같은 id 로 저장소 프로필을 더하면 이름
|
|
405
|
+
* 목록으로는 통과하는데 붙는 것은 저장소 프로필이었다(2차 리뷰).
|
|
406
|
+
*
|
|
407
|
+
* 그 「실제로 고를 것」 이 반쪽이었다. 이름 쪽은 **집 프로필 중에 그 이름이
|
|
408
|
+
* 있기만 하면** 통과시켰는데, 고르는 자는 이름을 아예 안 본다. 그래서 이름으로
|
|
409
|
+
* 적은 active 는 통과해 놓고 아무 id 와도 안 맞아 **목록 첫 번째**로 떨어졌다 —
|
|
410
|
+
* 저장소가 적은 한 줄이 이 PC 의 엉뚱한 프로필로 연결을 돌리고, 걸러졌다는 말도
|
|
411
|
+
* 안 났다.
|
|
412
|
+
*
|
|
413
|
+
* 그래서 activeProfile 과 **똑같이** 골라 본 다음, 골라진 것이 정말 저장소가
|
|
414
|
+
* 부른 그것인지를 본다. id 가 아예 없는 설정(문서의 예시 모양이 그렇다)에서는
|
|
415
|
+
* 첫 번째가 골라지고, 그 이름이 맞으면 저장소의 뜻대로 된 것이라 받는다.
|
|
416
|
+
*/
|
|
417
|
+
if (방.active !== undefined) {
|
|
418
|
+
const 골라질것 = 모음.find((x) => x?.id != null && x.id === 방.active) ?? 모음[0] ?? null;
|
|
419
|
+
const 가리킨것 = 골라질것 && (골라질것.id === 방.active || 골라질것.name === 방.active) ? 골라질것 : null;
|
|
420
|
+
const 이PC것 = 방.active != null && !!가리킨것 && 가리킨것.출처 !== '저장소';
|
|
421
|
+
if (!이PC것) {
|
|
422
|
+
if (집.active === undefined) delete cfg.active; else cfg.active = 집.active;
|
|
423
|
+
if (방.active != null) 적기('active');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
241
427
|
// 금지는 합친다. 어느 쪽이 적었든 금지는 금지다.
|
|
242
428
|
const 금지 = [...(집.permissions?.deny ?? []), ...(방.permissions?.deny ?? [])];
|
|
243
429
|
cfg.permissions = { ...(집.permissions ?? {}), ...(방.permissions ?? {}) };
|
|
@@ -247,7 +433,7 @@ function 겹치기(집, 방) {
|
|
|
247
433
|
// allow 를 허용하도록 고쳤을 때 아무 데서도 안 걸린다.
|
|
248
434
|
if (Array.isArray(집.permissions?.allow)) cfg.permissions.allow = 집.permissions.allow;
|
|
249
435
|
else delete cfg.permissions.allow;
|
|
250
|
-
return cfg;
|
|
436
|
+
return { cfg, 걸러낸것 };
|
|
251
437
|
}
|
|
252
438
|
|
|
253
439
|
let 신뢰소식 = null; // 프로젝트 설정에 대해 할 말. 한 번 읽으면 지워진다.
|
|
@@ -266,13 +452,25 @@ let 버린프로필 = 0;
|
|
|
266
452
|
*/
|
|
267
453
|
export function 프로젝트설정소식() { const s = 신뢰소식; 신뢰소식 = null; return s; }
|
|
268
454
|
|
|
269
|
-
function 읽기() {
|
|
455
|
+
function 읽기(root = process.cwd()) {
|
|
270
456
|
const 집파일 = join(userDir(), 'config.json');
|
|
271
|
-
const 방파일 = join(projectDir(), 'config.json');
|
|
457
|
+
const 방파일 = join(projectDir(root), 'config.json');
|
|
272
458
|
const 집 = existsSync(집파일)
|
|
273
459
|
? { ...structuredClone(EMPTY), ...한장읽기(집파일) }
|
|
274
460
|
: structuredClone(EMPTY);
|
|
275
|
-
|
|
461
|
+
/*
|
|
462
|
+
* profiles 가 목록이 아니면 **프로필이 없는 것**으로 친다.
|
|
463
|
+
*
|
|
464
|
+
* `{"profiles":null}` 은 펼치기가 EMPTY 의 빈 목록을 null 로 덮어, 켜자마자
|
|
465
|
+
* activeProfile 의 `.length` 에서 「Cannot read properties of null」 로 죽었다.
|
|
466
|
+
* 그 말로는 무엇을 고칠지 알 길이 없다. 「저장된 연결이 없습니다 — deel setup」
|
|
467
|
+
* 이 사실에 맞는 말이다. 저장소 쪽은 프로젝트거르기가 이미 같은 것을 한다.
|
|
468
|
+
*/
|
|
469
|
+
if (!Array.isArray(집.profiles)) 집.profiles = [];
|
|
470
|
+
// 목록 안의 null·숫자 한 칸도 같다 — 손으로 고치다 남긴 `[null, {…}]` 가 모든 명령을 같은 말로 죽였다 (2.0.0 6회차 · Gemini 설정6).
|
|
471
|
+
집.profiles = 집.profiles.filter((p) => p && typeof p === 'object' && !Array.isArray(p));
|
|
472
|
+
// 집 폴더에서 켜면 방파일이 곧 집파일이다 — 프로젝트 설정이 아니다 (집설정파일인가 머리말).
|
|
473
|
+
if (!existsSync(방파일) || 집설정파일인가(방파일)) return 집;
|
|
276
474
|
|
|
277
475
|
/*
|
|
278
476
|
* 안 믿는 폴더의 설정은 **안 읽는다.**
|
|
@@ -281,16 +479,19 @@ function 읽기() {
|
|
|
281
479
|
* 다른 주소로 가고, 열쇠받기 명령이 이 계정 권한으로 도는 길이 여기다.
|
|
282
480
|
* 그 명령은 도구 승인 화면보다 **앞**이라 어떤 승인 정책으로도 안 걸린다.
|
|
283
481
|
*/
|
|
284
|
-
if (!믿나(
|
|
285
|
-
신뢰소식 = { 갈래: '안믿음', 자리: 방파일, 폴더:
|
|
482
|
+
if (!믿나(root)) {
|
|
483
|
+
신뢰소식 = { 갈래: '안믿음', 자리: 방파일, 폴더: root };
|
|
286
484
|
return 집;
|
|
287
485
|
}
|
|
288
486
|
|
|
289
487
|
const { 값: 방, 걸러낸것 } = 프로젝트거르기(한장읽기(방파일));
|
|
290
488
|
버린프로필 = 0;
|
|
291
|
-
const
|
|
489
|
+
const { cfg: 겹친것, 걸러낸것: 겹치며걸러낸것 } = 겹치기(집, 방);
|
|
490
|
+
// 저장할 때 벗기려고 둘 다 들고 있는다(방벗기기). Symbol 이라 JSON 에는 안 적힌다.
|
|
491
|
+
겹친것[방흔적] = { 집: structuredClone(집), 방: structuredClone(방) };
|
|
292
492
|
const 다걸러낸것 = [
|
|
293
493
|
...걸러낸것,
|
|
494
|
+
...겹치며걸러낸것,
|
|
294
495
|
// id 도 name 도 없으면 어느 프로필을 고치라는 것인지 알 수 없다. 버리되 말한다.
|
|
295
496
|
...(버린프로필 ? [{ 칸: 'profiles', 왜: `id 도 name 도 없는 것 ${버린프로필}개는 못 붙였습니다` }] : []),
|
|
296
497
|
];
|
|
@@ -298,8 +499,95 @@ function 읽기() {
|
|
|
298
499
|
return 겹친것;
|
|
299
500
|
}
|
|
300
501
|
|
|
301
|
-
|
|
302
|
-
|
|
502
|
+
/*
|
|
503
|
+
* ── 이 PC 파일에는 이 PC 것만 적는다 ────────────────────────────────────
|
|
504
|
+
*
|
|
505
|
+
* setup · /model · /ctx · /max · scan · 에디터의 「앞으로 묻지 않기」 가 모두
|
|
506
|
+
* `load()` 가 준 **합친 것**을 그대로 저장했다. 그래서 믿는 폴더에서 한 번
|
|
507
|
+
* 설정을 만지면 저장소가 더한 프로필·deny·mode 가 이 PC 설정이 되어, 그 뒤로
|
|
508
|
+
* **모든 폴더에서** 걸렸다. 정책벗기기와 같은 부류다 — 덮은 것을 제 것으로 적었다.
|
|
509
|
+
*
|
|
510
|
+
* 가르는 법: 저장소가 적은 칸인데 값이 **저장소 값 그대로**면 이 PC 원래 값으로
|
|
511
|
+
* 되돌린다. 값이 달라졌으면 프로그램이 바꾼 것이니 둔다. 저장소가 더한 프로필은
|
|
512
|
+
* 표식(출처)을 보고 통째로 뺀다 — 이쪽은 흔적이 사라져도 빠진다.
|
|
513
|
+
*
|
|
514
|
+
* 받아들인 틈: 사람이 이 PC 설정을 **일부러 저장소와 같은 값으로** 바꾸면 되돌려
|
|
515
|
+
* 진다. 그 폴더 안에서는 어차피 저장소 값이 이기므로 결과가 같고, 다른 폴더에서는
|
|
516
|
+
* 원래 값이 남는다.
|
|
517
|
+
*/
|
|
518
|
+
function 방벗기기(cfg) {
|
|
519
|
+
const 사본 = { ...cfg, profiles: (cfg?.profiles ?? []).filter((p) => p?.출처 !== '저장소') };
|
|
520
|
+
const 흔적 = cfg?.[방흔적];
|
|
521
|
+
if (!흔적) return 사본;
|
|
522
|
+
const { 집, 방 } = 흔적;
|
|
523
|
+
const 같다 = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
524
|
+
const 가진칸 = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
525
|
+
const 되돌리기 = (대상, 원래, k) => { if (가진칸(원래, k)) 대상[k] = 원래[k]; else delete 대상[k]; };
|
|
526
|
+
|
|
527
|
+
for (const k of Object.keys(방)) {
|
|
528
|
+
if (k === 'profiles' || k === 'permissions') continue;
|
|
529
|
+
if (같다(사본[k], 방[k])) 되돌리기(사본, 집, k);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (방.permissions && 사본.permissions) {
|
|
533
|
+
const 권한 = { ...사본.permissions };
|
|
534
|
+
for (const k of Object.keys(방.permissions)) {
|
|
535
|
+
if (k === 'deny') continue;
|
|
536
|
+
if (같다(권한[k], 방.permissions[k])) 되돌리기(권한, 집.permissions, k);
|
|
537
|
+
}
|
|
538
|
+
// deny 는 합친 것이라 값으로 못 가른다. 저장소에서 온 항목만 뺀다.
|
|
539
|
+
if (Array.isArray(권한.deny) && Array.isArray(방.permissions.deny)) {
|
|
540
|
+
const 집금지 = new Set(집.permissions?.deny ?? []);
|
|
541
|
+
const 방금지 = new Set(방.permissions.deny);
|
|
542
|
+
권한.deny = 권한.deny.filter((x) => 집금지.has(x) || !방금지.has(x));
|
|
543
|
+
if (!권한.deny.length && !Array.isArray(집.permissions?.deny)) delete 권한.deny;
|
|
544
|
+
}
|
|
545
|
+
if (Object.keys(권한).length || 집.permissions) 사본.permissions = 권한;
|
|
546
|
+
else delete 사본.permissions;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 이 PC 프로필에 저장소가 덮은 칸. 겹치기와 같은 차례로 맞춘다 — id 먼저, 없으면 이름.
|
|
550
|
+
const 집프로필 = 집.profiles ?? [];
|
|
551
|
+
const 집id = new Set(집프로필.map((x) => x?.id).filter((v) => v != null));
|
|
552
|
+
사본.profiles = 사본.profiles.map((p) => {
|
|
553
|
+
/*
|
|
554
|
+
* 한 이 PC 프로필에 저장소 프로필이 **여럿** 얹힐 수 있다 — 겹치기가 id 로도
|
|
555
|
+
* 이름으로도 맞춰 주기 때문이다(옛 저장소 설정이 이름만 적어서). 그래서
|
|
556
|
+
* 저장소 하나가 이름으로 한 번 · id 로 한 번, 같은 칸을 두 번 얹는다.
|
|
557
|
+
*
|
|
558
|
+
* 여기가 `find` 로 **처음 걸린 것 하나**만 보고 견줬다. 그러면 뒤엣것이 얹은
|
|
559
|
+
* 값은 「저장소 값과 다르니 프로그램이 바꾼 것」 으로 보여 안 걷히고, 이 PC
|
|
560
|
+
* 파일에 그대로 박힌다. 그 뒤로는 어느 폴더에서 켜도 그 값이 걸린다.
|
|
561
|
+
* 겹치기가 배열 차례로 덮으니, 여기서도 그 차례로 합쳐서 견준다.
|
|
562
|
+
*/
|
|
563
|
+
const 얹힌것 = (방.profiles ?? []).filter((x) => x && (
|
|
564
|
+
(x.id != null && x.id === p?.id)
|
|
565
|
+
|| (x.name != null && x.name === p?.name && !집id.has(x.id))));
|
|
566
|
+
if (!얹힌것.length) return p;
|
|
567
|
+
const q = Object.assign({}, ...얹힌것);
|
|
568
|
+
// id 로 먼저 찾는다. 한 번에 둘 다 보면 이름이 같은 **앞 프로필**이 먼저 걸려,
|
|
569
|
+
// 뒤 프로필을 앞 프로필의 id·값으로 되돌려 적었다(2차 리뷰).
|
|
570
|
+
const 원래 = (p.id != null && 집프로필.find((x) => x?.id === p.id))
|
|
571
|
+
|| 집프로필.find((x) => x?.name != null && x.name === p.name) || {};
|
|
572
|
+
const r = { ...p };
|
|
573
|
+
for (const k of Object.keys(q)) {
|
|
574
|
+
if (같다(r[k], q[k])) 되돌리기(r, 원래, k);
|
|
575
|
+
}
|
|
576
|
+
return r;
|
|
577
|
+
});
|
|
578
|
+
return 사본;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/*
|
|
582
|
+
* 저장은 **이 PC 파일 한 곳**뿐이다.
|
|
583
|
+
*
|
|
584
|
+
* 저장소 파일에 적는 갈래(`toProject`)가 있었는데, 부르는 자리는 잠금옮기기
|
|
585
|
+
* 하나였고 그마저 합친 것을 저장소로 옮기는 길이었다. 남겨 두면 다음에 누가
|
|
586
|
+
* 부르는 순간 이 PC 프로필과 잠긴 열쇠가 저장소 파일로 나간다(2차 리뷰).
|
|
587
|
+
* 저장소 설정은 사람이 손으로 적는다.
|
|
588
|
+
*/
|
|
589
|
+
export function save(cfg) {
|
|
590
|
+
const dir = userDir();
|
|
303
591
|
const p = join(dir, 'config.json');
|
|
304
592
|
mkdirSync(dir, { recursive: true });
|
|
305
593
|
// 평문 열쇠는 여기서 잠근다. 설정을 쓰는 길이 여기 하나뿐이라, 여기만
|
|
@@ -308,12 +596,30 @@ export function save(cfg, { toProject = false } = {}) {
|
|
|
308
596
|
if (process.env.DEEL_KEYSTORE !== 'off') {
|
|
309
597
|
for (const 프로필 of cfg?.profiles ?? []) {
|
|
310
598
|
if (!프로필?.apiKey || 잠긴것인가(프로필.apiKey)) continue;
|
|
311
|
-
const 잠근것 = 잠그기(프로필.apiKey);
|
|
599
|
+
const 잠근것 = 잠그기(프로필.apiKey, { 프로필id: 프로필.id });
|
|
312
600
|
if (잠근것) 프로필.apiKey = 잠근것;
|
|
313
601
|
}
|
|
314
602
|
}
|
|
315
|
-
// 정책이 얹은 값은 사람 파일에 안 적는다 (정책벗기기).
|
|
316
|
-
|
|
603
|
+
// 정책이 얹은 값은 사람 파일에 안 적는다 (정책벗기기). 이 PC 파일에는 저장소가
|
|
604
|
+
// 얹은 것도 안 적는다 (방벗기기). 정책 쪽이 먼저다 — 그쪽은 프로필 객체로 맞춘다.
|
|
605
|
+
const 적을것 = 방벗기기(정책벗기기(cfg));
|
|
606
|
+
/*
|
|
607
|
+
* 옆 임시 파일에 다 적고 **이름을 바꿔** 갈아 끼운다 (2.0.0 6회차 · Gemini 설정6).
|
|
608
|
+
*
|
|
609
|
+
* 제자리에서 덮어쓰면 적는 도중 끊겼을 때(전원 · 강제 종료 · 디스크 참) 프로필과 열쇠가 든 파일이
|
|
610
|
+
* 0바이트나 반쪽 JSON 으로 남고, 다음에 켜면 「설정 파일을 읽지 못했습니다」 로 모든 명령이 선다.
|
|
611
|
+
* 임시 파일은 처음부터 본인만 읽게 만든다(열쇠가 든다). 윈도우는 누가 그 파일을 읽는 순간 이름
|
|
612
|
+
* 바꾸기를 EPERM 으로 튕기므로 잠깐씩 다시 한다 — safety/trust.js 의 쓰기 와 같은 방식이다.
|
|
613
|
+
*/
|
|
614
|
+
const 임시 = `${p}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
615
|
+
writeFileSync(임시, JSON.stringify(적을것, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
616
|
+
for (let 번 = 0; ; 번++) {
|
|
617
|
+
try { renameSync(임시, p); break; }
|
|
618
|
+
catch (err) {
|
|
619
|
+
if (번 >= 20) { try { rmSync(임시, { force: true }); } catch { /* 남아도 원래 설정은 멀쩡하다 */ } throw err; }
|
|
620
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
317
623
|
// 키가 들어 있는 파일이므로 가능한 환경에서는 본인만 읽게 잠근다.
|
|
318
624
|
try { chmodSync(p, 0o600); } catch {}
|
|
319
625
|
return p;
|
|
@@ -391,6 +697,28 @@ export function 열쇠환경인가(이름) {
|
|
|
391
697
|
return n === 'DEEL_API_KEY' || n.startsWith('DEEL_KEY_');
|
|
392
698
|
}
|
|
393
699
|
|
|
700
|
+
/**
|
|
701
|
+
* 이 프로필의 열쇠를 담을 수 있는 **환경변수 이름.** 없으면 null.
|
|
702
|
+
*
|
|
703
|
+
* ── 왜 한 자리에 있어야 하나 ──────────────────────────────────────────
|
|
704
|
+
*
|
|
705
|
+
* 이 이름을 짓는 자가 셋이었다 — resolveKey(집는 자) · 열쇠출처(화면에 적는 자)
|
|
706
|
+
* · configexplain(어느 층이 이겼나를 그리는 자). 셋이 조금씩 달랐고, 그 차이가
|
|
707
|
+
* 그대로 두 가지 탈이 됐다:
|
|
708
|
+
*
|
|
709
|
+
* · 숫자 id. `profile.id.toUpperCase` 는 함수가 아니라 모든 명령이 TypeError 로
|
|
710
|
+
* 끝났다. 설정은 사람이 손으로 적는 JSON 이라 `"id": 7` 이 정말 들어온다.
|
|
711
|
+
* · **저장소가 더한 프로필.** `DEEL_KEY_<id>` 를 받아 준 까닭은 「사람이 그 이름을
|
|
712
|
+
* 골라 넣었다」 였다. 그런데 저장소가 더한 프로필은 그 id 를 **저장소가 적는다.**
|
|
713
|
+
* 사람이 쓰던 열쇠 이름을 맞히기만 하면 그 열쇠가 저장소가 고른 주소로 나갔다.
|
|
714
|
+
* DEEL_API_KEY 는 같은 까닭으로 이미 막혀 있었는데 이쪽만 열려 있었다.
|
|
715
|
+
*/
|
|
716
|
+
export function 열쇠환경이름(profile) {
|
|
717
|
+
if (!profile?.id) return null;
|
|
718
|
+
if (profile.출처 === '저장소') return null;
|
|
719
|
+
return `DEEL_KEY_${String(profile.id).toUpperCase()}`;
|
|
720
|
+
}
|
|
721
|
+
|
|
394
722
|
/** 환경에 실제로 들어 있는 **열쇠 값들.** 가릴 때 쓴다. */
|
|
395
723
|
export function 환경속열쇠들(env = process.env) {
|
|
396
724
|
const out = [];
|
|
@@ -400,9 +728,58 @@ export function 환경속열쇠들(env = process.env) {
|
|
|
400
728
|
return out;
|
|
401
729
|
}
|
|
402
730
|
|
|
731
|
+
/**
|
|
732
|
+
* 이 프로필의 열쇠는 **어디서 오나.** 값은 안 담는다 — 자리 이름만 준다.
|
|
733
|
+
*
|
|
734
|
+
* ── 왜 이 자가 있어야 하나 ────────────────────────────────────────────
|
|
735
|
+
*
|
|
736
|
+
* 열쇠는 네 자리에서 온다. 차례가 있고, 그 차례를 아는 자가 둘이다 —
|
|
737
|
+
* 아래 resolveKey 와 backend/adapter.js 의 머리말짓기. 그런데 **화면에 적는
|
|
738
|
+
* 자(doctor)는 제 차례를 따로 들고 있었다.**
|
|
739
|
+
*
|
|
740
|
+
* 진짜 열쇠받기(명령) → DEEL_KEY_<프로필> → DEEL_API_KEY → 설정 파일
|
|
741
|
+
* doctor DEEL_API_KEY → DEEL_KEY_<프로필> → 열쇠받기 → 설정 파일
|
|
742
|
+
*
|
|
743
|
+
* 둘 다 넣어 둔 사람에게 doctor 는 「환경변수 DEEL_API_KEY」 라고 적는데,
|
|
744
|
+
* 실제로 나가는 것은 `DEEL_KEY_<프로필>` 이다. 그 사람은 화면이 가리킨
|
|
745
|
+
* 자리만 백 번 고친다. 잰 값 자체는 맞으므로(열쇠는 있다) 아무도 안 이상해한다.
|
|
746
|
+
*
|
|
747
|
+
* 열쇠받기는 더 어긋나 있었다. 그건 요청 **직전**에 받아서 다른 무엇보다
|
|
748
|
+
* 먼저 실리는데(adapter.js 머리말짓기), doctor 목록에서는 환경변수 뒤였다.
|
|
749
|
+
*
|
|
750
|
+
* 차례를 아는 자를 하나로 둔다. 자리가 하나 늘면 여기만 고친다.
|
|
751
|
+
*
|
|
752
|
+
* @param {object|null} profile
|
|
753
|
+
* @param {object} o
|
|
754
|
+
* @param {boolean} o.받기쓰나 열쇠받기가 이번 자리에서 실제로 도는가.
|
|
755
|
+
* (봉인 중이거나 auth:'none' 이면 안 돈다 —
|
|
756
|
+
* 그 판단은 safety/authcmd.js 의 쓸수있나() 몫이라
|
|
757
|
+
* 부르는 쪽이 물어보고 넘긴다.)
|
|
758
|
+
* @returns {{갈래: '명령'|'환경변수'|'파일'|null, 이름: string, 말: string}}
|
|
759
|
+
*/
|
|
760
|
+
export function 열쇠출처(profile, { 받기쓰나 = false } = {}) {
|
|
761
|
+
if (받기쓰나) return { 갈래: '명령', 이름: '', 말: '명령으로 받아 옵니다' };
|
|
762
|
+
const 프로필이름 = 열쇠환경이름(profile);
|
|
763
|
+
if (프로필이름 && process.env[프로필이름]) {
|
|
764
|
+
return { 갈래: '환경변수', 이름: 프로필이름, 말: `환경변수 ${프로필이름}` };
|
|
765
|
+
}
|
|
766
|
+
if (process.env.DEEL_API_KEY && profile?.출처 !== '저장소') {
|
|
767
|
+
return { 갈래: '환경변수', 이름: 'DEEL_API_KEY', 말: '환경변수 DEEL_API_KEY' };
|
|
768
|
+
}
|
|
769
|
+
if (profile?.apiKey) return { 갈래: '파일', 이름: '', 말: '설정 파일' };
|
|
770
|
+
return { 갈래: null, 이름: '', 말: '' };
|
|
771
|
+
}
|
|
772
|
+
|
|
403
773
|
export function resolveKey(profile) {
|
|
404
|
-
const
|
|
405
|
-
const
|
|
774
|
+
const 이름 = 열쇠환경이름(profile);
|
|
775
|
+
const byName = 이름 ? process.env[이름] : null;
|
|
776
|
+
/*
|
|
777
|
+
* 저장소가 더한 프로필은 환경변수 열쇠를 **하나도** 안 집는다. DEEL_API_KEY 는
|
|
778
|
+
* 사람이 제 게이트웨이에 쓰라고 넣은 것이고, `DEEL_KEY_<id>` 도 그 id 를
|
|
779
|
+
* 저장소가 적는 순간 사람이 고른 이름이 아니다 (열쇠환경이름 머리말).
|
|
780
|
+
*/
|
|
781
|
+
const 전체열쇠 = profile?.출처 === '저장소' ? '' : process.env.DEEL_API_KEY;
|
|
782
|
+
const 값 = byName || 전체열쇠 || profile?.apiKey || '';
|
|
406
783
|
if (!잠긴것인가(값)) return 값;
|
|
407
784
|
// 푸는 데 파워셸을 한 번 부른다(0.2초쯤). 한마디마다 부르면 그게 다 사람이
|
|
408
785
|
// 기다리는 시간이라, 판에 한 번만 풀고 들고 있는다.
|
|
@@ -436,3 +813,39 @@ export function slug(name) {
|
|
|
436
813
|
.replace(/[^a-z0-9가-힣]+/g, '-').replace(/^-+|-+$/g, '');
|
|
437
814
|
return base || 'profile';
|
|
438
815
|
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* 진입점마다 한 번씩 비워야 하는 **모아 둔 소식** 전부.
|
|
819
|
+
*
|
|
820
|
+
* ── 왜 한 자리에 모아야 하나 ──────────────────────────────────────────
|
|
821
|
+
*
|
|
822
|
+
* 이 프로그램에는 「나중에 화면이 한 번 읽어 가면 지워지는」 소식이 넷
|
|
823
|
+
* 있다. 잠금소식(열쇠를 방금 잠갔다) · 프로젝트설정소식(이 폴더 설정을 안
|
|
824
|
+
* 읽었거나 걷어냈다) · 열쇠탈소식(잠근 열쇠를 못 풀었다) · 정책 탈(관리
|
|
825
|
+
* 정책 파일이 깨졌다).
|
|
826
|
+
*
|
|
827
|
+
* 넷 다 **당겨 가는 꼴**이다. 그래서 안 당기는 문은 아무 표시도 안 난다 —
|
|
828
|
+
* 「알릴 것이 없다」 와 화면에서 똑같이 생긴다. 당기는 문은 둘뿐이었다
|
|
829
|
+
* (대화 화면과 `deel run`). `deel setup` · `deel doctor` · `deel reset` ·
|
|
830
|
+
* 에디터(ACP)는 하나도 안 당겼다.
|
|
831
|
+
*
|
|
832
|
+
* 제일 값이 큰 것이 정책 탈이다. `policy.json` 이 깨지면 관리자가 걸어 둔
|
|
833
|
+
* offline·baseUrl·금지가 **통째로 안 걸린다.** 그런데 그 말을 하던 자
|
|
834
|
+
* (규칙말)는 src 안에 부르는 데가 한 곳도 없었다 — 죽은 코드였다. 관리자는
|
|
835
|
+
* 잠긴 줄 알고, 배치 작업은 아무 제한 없이 돈다.
|
|
836
|
+
*
|
|
837
|
+
* 문이 하나 늘면 이 함수만 부르면 된다.
|
|
838
|
+
*
|
|
839
|
+
* @returns {string[]} 그대로 찍으면 되는 줄들. 없으면 빈 배열.
|
|
840
|
+
*/
|
|
841
|
+
export function 소식줄들(cfg = null) {
|
|
842
|
+
const 줄 = [];
|
|
843
|
+
const 잠금 = 잠금소식();
|
|
844
|
+
if (잠금) 줄.push(` ${mark.ok} ${잠금}`);
|
|
845
|
+
줄.push(...프로젝트설정줄들(프로젝트설정소식()));
|
|
846
|
+
const 탈 = 규칙모으기(cfg ?? { permissions: {} }).탈;
|
|
847
|
+
if (탈) 줄.push(` ${mark.warn} ${탈}`);
|
|
848
|
+
const 열쇠탈 = 열쇠탈소식();
|
|
849
|
+
if (열쇠탈) 줄.push(` ${mark.warn} ${열쇠탈}`);
|
|
850
|
+
return 줄.filter((x) => x !== '' || 줄.length > 1);
|
|
851
|
+
}
|