seokang-sk 0.2.0 → 0.2.2
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.MD +41 -14
- package/docs/sk-cli-guide.md +68 -2
- package/docs/sk-notion-workspace.md +3 -0
- package/docs/sk-setup-architecture.md +9 -1
- package/package.json +1 -1
- package/scripts/sk.mjs +451 -10
package/README.MD
CHANGED
|
@@ -11,7 +11,18 @@
|
|
|
11
11
|
|
|
12
12
|
`sk`는 최종적으로 `~/.agents/skills`의 로컬 저장소를 기준으로 동작해야 한다.
|
|
13
13
|
|
|
14
|
-
즉, npm으로 배포한 뒤에도 `~/.agents/skills/sk-feature-workflow/SKILL.md` 같은 스킬 파일을 로컬에서 바로 수정할 수 있어야 한다. 배포 패키지는 설치 진입점과 디스패치만 담당하고, 실제 기준 원본은 로컬 저장소에 남는다.
|
|
14
|
+
즉, npm으로 배포한 뒤에도 `~/.agents/skills/skills/sk-feature-workflow/SKILL.md` 같은 스킬 파일을 로컬에서 바로 수정할 수 있어야 한다. 배포 패키지는 설치 진입점과 디스패치만 담당하고, 실제 기준 원본은 로컬 저장소에 남는다.
|
|
15
|
+
|
|
16
|
+
중요:
|
|
17
|
+
- `~/.agents/skills`는 이 저장소의 repo root다.
|
|
18
|
+
- 실제 skill 폴더는 `~/.agents/skills/skills/` 아래에 둔다.
|
|
19
|
+
- `docs/`, `codex-config/`, `scripts/`는 지원 폴더이고, `skills/`가 skill 묶음 폴더다.
|
|
20
|
+
|
|
21
|
+
## 30초 구조 요약
|
|
22
|
+
|
|
23
|
+
- repo root는 `~/.agents/skills`다.
|
|
24
|
+
- 실제 skill은 `~/.agents/skills/skills/sk-이름/SKILL.md` 형태로 둔다.
|
|
25
|
+
- `docs/`, `codex-config/`, `scripts/`는 지원 폴더이며, `skills/`만 skill 묶음 폴더다.
|
|
15
26
|
|
|
16
27
|
## 처음 설치
|
|
17
28
|
|
|
@@ -23,6 +34,12 @@ sk init
|
|
|
23
34
|
sk status
|
|
24
35
|
```
|
|
25
36
|
|
|
37
|
+
- DB safe-read login-path alias를 프로젝트 track 기준으로 맞추려면 아래 명령을 사용한다.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sk setup --configure-db-safe-read
|
|
41
|
+
```
|
|
42
|
+
|
|
26
43
|
- Notion 문서까지 함께 쓸 계획이면 설치 후 한 번 더 아래 명령을 실행한다.
|
|
27
44
|
|
|
28
45
|
```bash
|
|
@@ -31,7 +48,8 @@ sk setup --configure-notion
|
|
|
31
48
|
|
|
32
49
|
- `Records` / `Projects` 데이터베이스 URL에 Notion id가 들어 있으면 `data source id`는 setup 중 자동으로 추출한다.
|
|
33
50
|
|
|
34
|
-
- Notion 문서 기록이
|
|
51
|
+
- Notion 문서 기록이 `invalid_grant`, `unauthorized`, `forbidden` 같은 인증 오류로 실패하면 아래 명령으로 MCP 인증을 다시 연결한다.
|
|
52
|
+
- `Tool ... not found`처럼 특정 Notion tool만 빠진 경우는 로그인 문제가 아닐 수 있으니, exact error를 남기고 남아 있는 Notion MCP tool로 우회 가능한지 먼저 확인한다.
|
|
35
53
|
|
|
36
54
|
```bash
|
|
37
55
|
codex mcp login notion
|
|
@@ -53,15 +71,16 @@ codex mcp login notion
|
|
|
53
71
|
|
|
54
72
|
```text
|
|
55
73
|
.
|
|
56
|
-
|-
|
|
57
|
-
|- sk-
|
|
58
|
-
|- sk-
|
|
59
|
-
|- sk-
|
|
60
|
-
|- sk-
|
|
61
|
-
|- sk-
|
|
62
|
-
|- sk-
|
|
63
|
-
|- sk-
|
|
64
|
-
|- sk-
|
|
74
|
+
|- skills/
|
|
75
|
+
| |- sk-feature-workflow # 기능 작업 메인 오케스트레이터
|
|
76
|
+
| |- sk-bugfix-workflow # 버그 수정 워크플로
|
|
77
|
+
| |- sk-refactor # 리팩토링 워크플로
|
|
78
|
+
| |- sk-plan # 기획/설계 응답 워크플로
|
|
79
|
+
| |- sk-db # DB 변경 워크플로
|
|
80
|
+
| |- sk-design-system # 디자인 시스템 점검/구축
|
|
81
|
+
| |- sk-docs # 변경 문서 작성
|
|
82
|
+
| |- sk-git # git add/commit/push 워크플로
|
|
83
|
+
| |- sk-deploy # 배포 워크플로
|
|
65
84
|
|- codex-config/
|
|
66
85
|
| |- config/ # ~/.codex/config.toml 공용 원본
|
|
67
86
|
| |- agents/ # ~/.codex/agents 공용 원본
|
|
@@ -69,6 +88,14 @@ codex mcp login notion
|
|
|
69
88
|
|- scripts/ # 공용 설치 / 보조 스크립트
|
|
70
89
|
```
|
|
71
90
|
|
|
91
|
+
- 실제 skill 엔트리는 `skills/sk-*/SKILL.md`다.
|
|
92
|
+
|
|
93
|
+
## 어디를 수정하나
|
|
94
|
+
|
|
95
|
+
- skill 동작 변경: `skills/sk-*/SKILL.md`
|
|
96
|
+
- 설치/런타임 설정 변경: `codex-config/**`, `scripts/**`
|
|
97
|
+
- 사용법/운영 가이드 변경: `docs/**`, `README.MD`
|
|
98
|
+
|
|
72
99
|
---
|
|
73
100
|
|
|
74
101
|
## 목적
|
|
@@ -178,7 +205,7 @@ sh ./sk status
|
|
|
178
205
|
- `docs/**` 인덱스/개요 문서
|
|
179
206
|
- 현재 요청과 직접 관련된 상세 문서
|
|
180
207
|
- 문서가 많으면 전부 읽으려 하지 말고, 인덱스/개요 → 관련 상세 순서로 좁혀 읽는다.
|
|
181
|
-
- `.agents/skills`에서는 `README.MD`, `docs/**`, 수정 대상 `SKILL.md`, 관련 setup/config 문서를 기본 세트로 본다.
|
|
208
|
+
- `.agents/skills`에서는 `README.MD`, `docs/**`, `skills/**`, 수정 대상 `SKILL.md`, 관련 setup/config 문서를 기본 세트로 본다.
|
|
182
209
|
|
|
183
210
|
---
|
|
184
211
|
|
|
@@ -304,8 +331,8 @@ LOCAL_JAR_PATH=./build/libs/km-admin-api.jar
|
|
|
304
331
|
|
|
305
332
|
`seokang-skill` 프로젝트에서는 아래 스크립트/템플릿을 사용한다.
|
|
306
333
|
|
|
307
|
-
- `sk-deploy/deploy-npm.sh`
|
|
308
|
-
- `sk-deploy/deploy.local.conf.template.npm`
|
|
334
|
+
- `skills/sk-deploy/deploy-npm.sh`
|
|
335
|
+
- `skills/sk-deploy/deploy.local.conf.template.npm`
|
|
309
336
|
|
|
310
337
|
---
|
|
311
338
|
|
package/docs/sk-cli-guide.md
CHANGED
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
|
|
13
13
|
`sk`는 최종적으로 `~/.agents/skills`의 로컬 저장소를 기준으로 동작해야 한다.
|
|
14
14
|
|
|
15
|
-
그래서 npm으로 배포한 뒤에도 `~/.agents/skills/sk-feature-workflow/SKILL.md`를 바로 수정할 수 있다. 배포 패키지는 설치 진입점과 디스패치만 담당하고, 실제 기준 원본은 여전히 로컬 저장소다.
|
|
15
|
+
그래서 npm으로 배포한 뒤에도 `~/.agents/skills/skills/sk-feature-workflow/SKILL.md`를 바로 수정할 수 있다. 배포 패키지는 설치 진입점과 디스패치만 담당하고, 실제 기준 원본은 여전히 로컬 저장소다.
|
|
16
|
+
|
|
17
|
+
중요:
|
|
18
|
+
|
|
19
|
+
- `~/.agents/skills`는 repo root다.
|
|
20
|
+
- 실제 skill은 `~/.agents/skills/skills/` 아래에 둔다.
|
|
21
|
+
- `docs/`, `codex-config/`, `scripts/`는 지원 폴더이고, `skills/`가 skill 묶음 폴더다.
|
|
22
|
+
|
|
23
|
+
## 하지 말아야 할 구조 변경
|
|
24
|
+
|
|
25
|
+
- `sk-*` 디렉터리를 `skills/` 밖 여기저기에 흩어 놓지 않는다.
|
|
26
|
+
- `SKILL.md`를 루트 밖 공용 문서 폴더로 합치지 않는다.
|
|
27
|
+
- 프로젝트별 상태 폴더 `.sk/`와 이 저장소의 skill 디렉터리를 같은 개념으로 보지 않는다.
|
|
16
28
|
|
|
17
29
|
## 처음 설치
|
|
18
30
|
|
|
@@ -196,6 +208,11 @@ sk check
|
|
|
196
208
|
|
|
197
209
|
이 명령은 source와 실제 설치 대상이 일치하는지 확인한다.
|
|
198
210
|
|
|
211
|
+
- DB safe-read를 자주 쓰는 환경이면 `mysql`/`mysql_config_editor`의 PATH 또는 full path fallback, `~/.mylogin.cnf`, login-path alias 상태도 함께 보여준다.
|
|
212
|
+
- 출력이 `PATH 미노출`이더라도 full path fallback이 보이면 곧바로 `mysql 없음`으로 판단하지 않는다.
|
|
213
|
+
- 현재 프로젝트에서 legacy alias만 보이면 `sk setup --configure-db-safe-read`로 track alias migration을 바로 진행할 수 있다.
|
|
214
|
+
- `DB client` 경고가 뜨면 alias보다 먼저 client 설치/PATH를 해결해야 한다. 현재 메시지에서 바로 다음 액션까지 확인할 수 있다.
|
|
215
|
+
|
|
199
216
|
### 4-3. 현재 상태 보기
|
|
200
217
|
|
|
201
218
|
```bash
|
|
@@ -204,7 +221,56 @@ sk status
|
|
|
204
221
|
|
|
205
222
|
이 명령은 현재 프로젝트 기준 `.sk` 상태, 메모 파일 존재 여부, 최근 backup을 보여준다.
|
|
206
223
|
|
|
207
|
-
### 4-4.
|
|
224
|
+
### 4-4. DB safe-read alias 맞추기
|
|
225
|
+
|
|
226
|
+
현재 프로젝트 track 기준 DB login-path alias를 `sk` 안에서 맞추려면 아래처럼 실행한다.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
sk setup --configure-db-safe-read
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- 예: `slk-api` 프로젝트에서 실행하면 `codex-db-slk-server` alias를 기준으로 맞춘다.
|
|
233
|
+
- 이미 `codex_ro`만 있으면 host / port / user를 재사용하고, 비밀번호만 `mysql_config_editor` 프롬프트에서 다시 입력한다.
|
|
234
|
+
- track을 직접 지정하고 싶으면 `--track <name>`을 붙일 수 있다.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
sk setup --configure-db-safe-read --track slk-server
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 4-5. DB safe-read 선행 조건
|
|
241
|
+
|
|
242
|
+
`sk setup --configure-db-safe-read`가 바로 동작하려면 `mysql`과 `mysql_config_editor`가 먼저 준비돼 있어야 한다.
|
|
243
|
+
|
|
244
|
+
가장 짧은 확인 순서는 아래다.
|
|
245
|
+
|
|
246
|
+
1. `sk check`로 현재 상태가 `client 부재`, `PATH 미노출`, `alias 없음` 중 어디인지 먼저 본다.
|
|
247
|
+
2. `DB client` 경고면 client 설치/PATH를 먼저 해결한다.
|
|
248
|
+
3. 그다음 `sk check`를 다시 실행한다.
|
|
249
|
+
4. client가 잡히면 `sk setup --configure-db-safe-read`로 alias를 만든다.
|
|
250
|
+
|
|
251
|
+
#### macOS
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
brew install mysql-client
|
|
255
|
+
sk check
|
|
256
|
+
sk setup --configure-db-safe-read
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
PATH를 따로 추가하지 않아도 `sk`는 Homebrew 표준 full path fallback을 먼저 시도한다.
|
|
260
|
+
|
|
261
|
+
#### Windows Git Bash
|
|
262
|
+
|
|
263
|
+
MySQL client tools(`mysql`, `mysql_config_editor`)가 있어야 한다. MariaDB 설치본은 `mysql_config_editor`가 빠질 수 있으니 `sk check`로 먼저 확인한다. 설치 후 Git Bash에서 아래처럼 PATH를 맞춘다.
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
export PATH="/c/Program Files/MySQL/MySQL Server 8.0/bin:$PATH"
|
|
267
|
+
sk check
|
|
268
|
+
sk setup --configure-db-safe-read
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`sk check`에서 client fallback 경로가 보이면 PATH 미노출일 뿐이므로 즉시 alias 단계로 넘어가도 된다.
|
|
272
|
+
|
|
273
|
+
### 4-6. 현재 프로젝트 `.sk` 초기화
|
|
208
274
|
|
|
209
275
|
```bash
|
|
210
276
|
cd /path/to/project
|
|
@@ -24,6 +24,8 @@ sk setup --configure-notion
|
|
|
24
24
|
추출이 안 되는 특수 URL만 `workspace.json`에서 수동으로 보완하면 된다.
|
|
25
25
|
|
|
26
26
|
필요하면 이어서 아래 명령으로 Codex의 Notion MCP 인증도 연결한다.
|
|
27
|
+
단, 이 재로그인은 `invalid_grant`, `unauthorized`, `forbidden` 같은 인증/세션 오류일 때의 기본 대응이다.
|
|
28
|
+
`Tool ... not found`처럼 특정 MCP tool만 누락된 경우는 로그인 문제가 아닐 수 있으므로, exact error를 남기고 다른 Notion MCP tool로 우회 가능한지 먼저 본다.
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
31
|
codex mcp login notion
|
|
@@ -56,5 +58,6 @@ codex mcp login notion
|
|
|
56
58
|
- public skill 본문에 개인 Notion URL, DB id, collection id를 직접 적지 않는다.
|
|
57
59
|
- Notion 작업 전에는 먼저 local config 파일 존재 여부와 `configured` 상태를 확인한다.
|
|
58
60
|
- local config가 없거나 `configured=false`면 Notion 기록 시도를 생략하고 Markdown fallback을 기본 출력으로 사용한다.
|
|
61
|
+
- `Tool ... not found`처럼 특정 Notion MCP tool만 빠진 경우는 Notion 전체 장애로 단정하지 않는다. local config의 data source id와 project/history 위치가 있으면 `fetch`/`search`/`create_pages`/`update_page` 같은 생존 tool로 먼저 우회 시도한다.
|
|
59
62
|
- fallback Markdown를 나중에 Notion으로 옮길 때는 `Projects > History`의 `Date`를 이관 완료 시점의 로컬 datetime으로 다시 저장하고, `date:Date:is_datetime=1`을 유지한 뒤에만 fallback 파일을 제거한다.
|
|
60
63
|
- 사용자가 이후 Notion 기록도 원하면 그때 `sk setup --configure-notion`을 안내하고, Notion MCP 실패 여부와 구분해서 보고한다.
|
|
@@ -21,10 +21,16 @@ The most important rule is:
|
|
|
21
21
|
|
|
22
22
|
That is why editing a file like:
|
|
23
23
|
|
|
24
|
-
`~/.agents/skills/sk-feature-workflow/SKILL.md`
|
|
24
|
+
`~/.agents/skills/skills/sk-feature-workflow/SKILL.md`
|
|
25
25
|
|
|
26
26
|
stays simple. You are editing the real source file that `sk` will delegate to, not a hidden copy inside the npm install directory.
|
|
27
27
|
|
|
28
|
+
One more rule matters here:
|
|
29
|
+
|
|
30
|
+
- `~/.agents/skills` is the repo root
|
|
31
|
+
- actual skill folders live under `~/.agents/skills/skills`
|
|
32
|
+
- support files like `docs/`, `codex-config/`, and `scripts/` stay at repo root
|
|
33
|
+
|
|
28
34
|
## Install flow
|
|
29
35
|
|
|
30
36
|
Recommended first-time setup:
|
|
@@ -64,6 +70,8 @@ It only needs enough code to:
|
|
|
64
70
|
|
|
65
71
|
The full skills, config, agents, docs, and repo-local helpers stay in `~/.agents/skills`.
|
|
66
72
|
|
|
73
|
+
That includes the skill directories themselves under `~/.agents/skills/skills`, which Codex can still discover from the canonical user skills root.
|
|
74
|
+
|
|
67
75
|
## Editing model
|
|
68
76
|
|
|
69
77
|
After setup, improvements happen in one place only:
|
package/package.json
CHANGED
package/scripts/sk.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { createReadStream, existsSync } from 'node:fs';
|
|
4
|
+
import { createReadStream, existsSync, readdirSync } from 'node:fs';
|
|
5
5
|
import {
|
|
6
6
|
copyFile,
|
|
7
7
|
mkdir,
|
|
@@ -29,6 +29,63 @@ const NOTION_CONFIG_FILE = 'workspace.json';
|
|
|
29
29
|
const GUIDE_RELATIVE_PATH = join('docs', 'sk-cli-guide.md');
|
|
30
30
|
const DEFAULT_BOOTSTRAP_REPO = 'https://github.com/Seokang/seokang-codex-setting.git';
|
|
31
31
|
const CANONICAL_SKILLS_SEGMENTS = ['.agents', 'skills'];
|
|
32
|
+
function windowsMysqlCandidates(binaryName) {
|
|
33
|
+
if (process.platform !== 'win32') return [];
|
|
34
|
+
|
|
35
|
+
const candidates = [];
|
|
36
|
+
const programRoots = [
|
|
37
|
+
'C:\\Program Files',
|
|
38
|
+
'C:\\Program Files (x86)',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const programRoot of programRoots) {
|
|
42
|
+
const mysqlRoot = join(programRoot, 'MySQL');
|
|
43
|
+
if (existsSync(mysqlRoot)) {
|
|
44
|
+
for (const entry of readdirSync(mysqlRoot, { withFileTypes: true })) {
|
|
45
|
+
if (!entry.isDirectory()) continue;
|
|
46
|
+
candidates.push(join(mysqlRoot, entry.name, 'bin', binaryName));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!existsSync(programRoot)) continue;
|
|
51
|
+
for (const entry of readdirSync(programRoot, { withFileTypes: true })) {
|
|
52
|
+
if (!entry.isDirectory()) continue;
|
|
53
|
+
if (!entry.name.toLowerCase().startsWith('mariadb')) continue;
|
|
54
|
+
candidates.push(join(programRoot, entry.name, 'bin', binaryName));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return [...new Set(candidates)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const MYSQL_BIN_CANDIDATES = [
|
|
62
|
+
'/opt/homebrew/opt/mysql-client/bin/mysql',
|
|
63
|
+
'/usr/local/opt/mysql-client/bin/mysql',
|
|
64
|
+
'/c/Program Files/MySQL/MySQL Server 8.0/bin/mysql.exe',
|
|
65
|
+
'/c/Program Files/MySQL/MySQL Server 8.0/bin/mysql',
|
|
66
|
+
'/c/Program Files/MariaDB 11.4/bin/mysql.exe',
|
|
67
|
+
'/c/Program Files/MariaDB 11.4/bin/mysql',
|
|
68
|
+
...windowsMysqlCandidates('mysql.exe'),
|
|
69
|
+
];
|
|
70
|
+
const MYSQL_CONFIG_EDITOR_CANDIDATES = [
|
|
71
|
+
'/opt/homebrew/opt/mysql-client/bin/mysql_config_editor',
|
|
72
|
+
'/usr/local/opt/mysql-client/bin/mysql_config_editor',
|
|
73
|
+
'/c/Program Files/MySQL/MySQL Server 8.0/bin/mysql_config_editor.exe',
|
|
74
|
+
'/c/Program Files/MySQL/MySQL Server 8.0/bin/mysql_config_editor',
|
|
75
|
+
...windowsMysqlCandidates('mysql_config_editor.exe'),
|
|
76
|
+
];
|
|
77
|
+
const LEGACY_DB_LOGIN_PATH = 'codex_ro';
|
|
78
|
+
const DB_TRACK_RULES = [
|
|
79
|
+
{ track: 'slk-server', needles: ['slk-api', 'slk-server'] },
|
|
80
|
+
{ track: 'server', needles: ['km-admin-api', '/server', '\\server'] },
|
|
81
|
+
{ track: 'price-spinks', needles: ['price-advisor-spinks', 'price-spinks', 'spinks'] },
|
|
82
|
+
{ track: 'price-sealy', needles: ['price-advisor-sealy', 'price-sealy', 'sealy'] },
|
|
83
|
+
{ track: 'price', needles: ['price-advisor', '/price', '\\price'] },
|
|
84
|
+
{ track: 'office-web', needles: ['kwang-admin', 'admin-web', 'office-web'] },
|
|
85
|
+
{ track: 'office-app', needles: ['office-app', '/app', '\\app'] },
|
|
86
|
+
{ track: 'flow-frame', needles: ['flow-frame'] },
|
|
87
|
+
{ track: 'seokang-skill', needles: ['.agents/skills', 'seokang-skill', 'sellink docs'] },
|
|
88
|
+
];
|
|
32
89
|
|
|
33
90
|
function canonicalRepoRoot() {
|
|
34
91
|
return resolve(process.env.SK_REPO_ROOT || join(homedir(), ...CANONICAL_SKILLS_SEGMENTS));
|
|
@@ -58,6 +115,357 @@ function canRun(command, args = ['--version']) {
|
|
|
58
115
|
return !result.error && result.status === 0;
|
|
59
116
|
}
|
|
60
117
|
|
|
118
|
+
function detectExecutable(commandName, candidates = []) {
|
|
119
|
+
if (canRun(commandName)) {
|
|
120
|
+
return { source: 'path', path: commandName };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
if (candidate && canRun(candidate)) {
|
|
125
|
+
return { source: 'fallback', path: candidate };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function summarizeAliases(aliasNames) {
|
|
133
|
+
if (aliasNames.length === 0) return '없음';
|
|
134
|
+
if (aliasNames.length <= 3) return aliasNames.join(', ');
|
|
135
|
+
return `${aliasNames.slice(0, 3).join(', ')} 외 ${aliasNames.length - 3}개`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function inferTrackFromPath(cwd = process.cwd()) {
|
|
139
|
+
const normalized = resolve(cwd).replaceAll('\\', '/').toLowerCase();
|
|
140
|
+
for (const rule of DB_TRACK_RULES) {
|
|
141
|
+
if (rule.needles.some((needle) => normalized.includes(needle))) {
|
|
142
|
+
return rule.track;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function dbTargetAlias(track) {
|
|
149
|
+
return track ? `codex-db-${track}` : 'codex-db-default';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function dbSafeReadSetupCommand(track) {
|
|
153
|
+
return track ? `sk setup --configure-db-safe-read --track ${track}` : 'sk setup --configure-db-safe-read';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function mysqlClientInstallHint() {
|
|
157
|
+
if (process.platform === 'darwin') {
|
|
158
|
+
return 'macOS: 먼저 "brew install mysql-client"를 실행하고, 필요하면 "/opt/homebrew/opt/mysql-client/bin" PATH를 확인하세요.';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (process.platform === 'win32') {
|
|
162
|
+
return 'Windows Git Bash: MySQL client tools(mysql, mysql_config_editor)를 설치하고 "/c/Program Files/MySQL/.../bin"을 PATH에 추가하세요. MariaDB 설치본은 mysql_config_editor가 빠질 수 있으니 같이 확인하세요.';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return '먼저 mysql client tools(mysql, mysql_config_editor)를 설치하고 PATH를 맞추세요.';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function mysqlClientRecoveryHint(track) {
|
|
169
|
+
return `${mysqlClientInstallHint()} 그다음 "sk check"로 client 상태를 다시 확인하고, alias가 없으면 "${dbSafeReadSetupCommand(track)}"를 실행하세요.`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeMysqlConfigValue(rawValue = '') {
|
|
173
|
+
const value = String(rawValue).trim();
|
|
174
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
175
|
+
return value.slice(1, -1);
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseMysqlConfigEditorOutput(output = '') {
|
|
181
|
+
const profiles = {};
|
|
182
|
+
let currentAlias = null;
|
|
183
|
+
|
|
184
|
+
for (const line of String(output).split(/\r?\n/)) {
|
|
185
|
+
const trimmed = line.trim();
|
|
186
|
+
if (!trimmed) continue;
|
|
187
|
+
|
|
188
|
+
const aliasMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
189
|
+
if (aliasMatch) {
|
|
190
|
+
currentAlias = aliasMatch[1];
|
|
191
|
+
profiles[currentAlias] = {};
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!currentAlias) continue;
|
|
196
|
+
|
|
197
|
+
const fieldMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*(.+)$/);
|
|
198
|
+
if (!fieldMatch) continue;
|
|
199
|
+
|
|
200
|
+
profiles[currentAlias][fieldMatch[1]] = normalizeMysqlConfigValue(fieldMatch[2]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return profiles;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function chooseDbSourceProfile(profiles, targetAlias) {
|
|
207
|
+
const preferredAliases = ['codex-db-default', LEGACY_DB_LOGIN_PATH];
|
|
208
|
+
for (const alias of preferredAliases) {
|
|
209
|
+
if (alias === targetAlias) continue;
|
|
210
|
+
if (profiles[alias]?.host && profiles[alias]?.user) {
|
|
211
|
+
return { alias, ...profiles[alias] };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const [alias, profile] of Object.entries(profiles)) {
|
|
216
|
+
if (alias === targetAlias) continue;
|
|
217
|
+
if (profile?.host && profile?.user) {
|
|
218
|
+
return { alias, ...profile };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function promptForDbSafeReadProfile({ targetAlias, track }) {
|
|
226
|
+
const reader = createInterface({
|
|
227
|
+
input: process.stdin,
|
|
228
|
+
output: process.stdout,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
console.log('sk setup (user) - DB safe-read 설정');
|
|
232
|
+
console.log(` 🎯 대상 alias: ${targetAlias}${track ? ` (track=${track})` : ''}`);
|
|
233
|
+
console.log(' 기존 alias를 복사할 수 없어 host / port / user를 먼저 입력받습니다.');
|
|
234
|
+
console.log(' 비밀번호는 이어지는 mysql_config_editor 프롬프트에서만 입력합니다.');
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const host = (await promptWithInterface(reader, 'DB host: ')).trim();
|
|
238
|
+
const port = (await promptWithInterface(reader, 'DB port [3306]: ')).trim();
|
|
239
|
+
const user = (await promptWithInterface(reader, 'DB user: ')).trim();
|
|
240
|
+
|
|
241
|
+
if (!host || !user) {
|
|
242
|
+
fail('DB safe-read 설정을 진행하려면 host와 user가 필요합니다.');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
host,
|
|
247
|
+
port: port || '3306',
|
|
248
|
+
user,
|
|
249
|
+
sourceAlias: null,
|
|
250
|
+
};
|
|
251
|
+
} finally {
|
|
252
|
+
reader.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function ensureDbSafeReadConfig({
|
|
257
|
+
configure,
|
|
258
|
+
dryRun,
|
|
259
|
+
force,
|
|
260
|
+
verbose,
|
|
261
|
+
trackOverride,
|
|
262
|
+
contextCwd,
|
|
263
|
+
}) {
|
|
264
|
+
if (!configure) {
|
|
265
|
+
return { configured: false, changed: false, message: '요청 없음' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const track = trackOverride || inferTrackFromPath(contextCwd);
|
|
269
|
+
const targetAlias = dbTargetAlias(track);
|
|
270
|
+
const editor = detectExecutable('mysql_config_editor', MYSQL_CONFIG_EDITOR_CANDIDATES);
|
|
271
|
+
if (!editor) {
|
|
272
|
+
fail(`DB safe-read 설정을 진행하려면 mysql_config_editor가 필요합니다. ${mysqlClientRecoveryHint(track)}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const currentAliasesResult = spawnSync(editor.path, ['print', '--all'], { encoding: 'utf-8' });
|
|
276
|
+
if (currentAliasesResult.error) {
|
|
277
|
+
fail(`DB alias 목록을 읽지 못했습니다: ${currentAliasesResult.error.message}`);
|
|
278
|
+
}
|
|
279
|
+
if (currentAliasesResult.status !== 0) {
|
|
280
|
+
const stderr = typeof currentAliasesResult.stderr === 'string' ? currentAliasesResult.stderr.trim() : '';
|
|
281
|
+
fail(`DB alias 목록을 읽지 못했습니다.${stderr ? ` ${stderr}` : ''}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const profiles = parseMysqlConfigEditorOutput(currentAliasesResult.stdout);
|
|
285
|
+
if (profiles[targetAlias] && !force) {
|
|
286
|
+
return {
|
|
287
|
+
configured: true,
|
|
288
|
+
changed: false,
|
|
289
|
+
targetAlias,
|
|
290
|
+
track,
|
|
291
|
+
message: `${targetAlias} alias가 이미 있습니다.`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let profileInput = chooseDbSourceProfile(profiles, targetAlias);
|
|
296
|
+
if (!profileInput && dryRun) {
|
|
297
|
+
return {
|
|
298
|
+
configured: false,
|
|
299
|
+
changed: false,
|
|
300
|
+
targetAlias,
|
|
301
|
+
track,
|
|
302
|
+
message: `${targetAlias} alias를 만들려면 host / port / user 입력이 필요합니다.`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!profileInput) {
|
|
307
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
308
|
+
fail('기존 DB alias를 복사할 수 없어서 입력이 필요합니다. TTY에서 "sk setup --configure-db-safe-read"를 다시 실행하세요.');
|
|
309
|
+
}
|
|
310
|
+
profileInput = await promptForDbSafeReadProfile({ targetAlias, track });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const host = profileInput.host;
|
|
314
|
+
const port = profileInput.port || '3306';
|
|
315
|
+
const user = profileInput.user;
|
|
316
|
+
const sourceAlias = profileInput.alias || profileInput.sourceAlias || null;
|
|
317
|
+
|
|
318
|
+
if (dryRun) {
|
|
319
|
+
return {
|
|
320
|
+
configured: false,
|
|
321
|
+
changed: false,
|
|
322
|
+
targetAlias,
|
|
323
|
+
track,
|
|
324
|
+
message: `${targetAlias} alias를 준비합니다. ${sourceAlias ? `${sourceAlias}의 host/user/port를 재사용` : 'host/user/port를 입력받아 사용'}`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (verbose) {
|
|
329
|
+
console.log(` 🗄️ DB alias 생성: ${targetAlias}${sourceAlias ? ` (기준=${sourceAlias})` : ''}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 비밀번호는 mysql_config_editor 프롬프트에서만 입력해 user-local secret으로 유지한다.
|
|
333
|
+
runCommandOrFail(editor.path, [
|
|
334
|
+
'set',
|
|
335
|
+
`--login-path=${targetAlias}`,
|
|
336
|
+
`--host=${host}`,
|
|
337
|
+
`--port=${port}`,
|
|
338
|
+
`--user=${user}`,
|
|
339
|
+
'--password',
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
const mysql = detectExecutable('mysql', MYSQL_BIN_CANDIDATES);
|
|
343
|
+
if (!mysql) {
|
|
344
|
+
return {
|
|
345
|
+
configured: true,
|
|
346
|
+
changed: true,
|
|
347
|
+
targetAlias,
|
|
348
|
+
track,
|
|
349
|
+
message: `${targetAlias} alias를 만들었습니다. mysql PATH/fallback 확인은 다음 sk check에서 확인하세요.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const verifyResult = spawnSync(mysql.path, [
|
|
354
|
+
`--login-path=${targetAlias}`,
|
|
355
|
+
'--connect-timeout=5',
|
|
356
|
+
'-N',
|
|
357
|
+
'-B',
|
|
358
|
+
'-e',
|
|
359
|
+
'SELECT 1',
|
|
360
|
+
], { encoding: 'utf-8' });
|
|
361
|
+
|
|
362
|
+
if (verifyResult.error || verifyResult.status !== 0) {
|
|
363
|
+
const stderr = typeof verifyResult.stderr === 'string' ? verifyResult.stderr.trim() : '';
|
|
364
|
+
return {
|
|
365
|
+
configured: true,
|
|
366
|
+
changed: true,
|
|
367
|
+
targetAlias,
|
|
368
|
+
track,
|
|
369
|
+
message: `${targetAlias} alias를 만들었지만 즉시 연결 검증은 실패했습니다.${stderr ? ` ${stderr}` : ''}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
configured: true,
|
|
375
|
+
changed: true,
|
|
376
|
+
targetAlias,
|
|
377
|
+
track,
|
|
378
|
+
message: `${targetAlias} alias를 만들고 SELECT 1 검증까지 통과했습니다.`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function buildDbSafeReadChecks(cwd = process.cwd()) {
|
|
383
|
+
const mysql = detectExecutable('mysql', MYSQL_BIN_CANDIDATES);
|
|
384
|
+
const editor = detectExecutable('mysql_config_editor', MYSQL_CONFIG_EDITOR_CANDIDATES);
|
|
385
|
+
const checks = [];
|
|
386
|
+
const track = inferTrackFromPath(cwd);
|
|
387
|
+
const targetAlias = dbTargetAlias(track);
|
|
388
|
+
|
|
389
|
+
if (!mysql && !editor) {
|
|
390
|
+
checks.push(check('warn', 'DB client', `mysql, mysql_config_editor를 찾지 못했습니다. ${mysqlClientRecoveryHint(track)}`));
|
|
391
|
+
} else if (mysql?.source === 'path' && editor?.source === 'path') {
|
|
392
|
+
checks.push(check('pass', 'DB client', 'mysql, mysql_config_editor가 PATH에 있습니다.'));
|
|
393
|
+
} else if (mysql && editor) {
|
|
394
|
+
checks.push(check(
|
|
395
|
+
'pass',
|
|
396
|
+
'DB client',
|
|
397
|
+
`PATH 미노출이지만 fallback으로 사용 가능합니다. mysql=${mysql.path} | mysql_config_editor=${editor.path}`,
|
|
398
|
+
));
|
|
399
|
+
} else {
|
|
400
|
+
const parts = [];
|
|
401
|
+
if (mysql) {
|
|
402
|
+
parts.push(mysql.source === 'path'
|
|
403
|
+
? 'mysql=PATH'
|
|
404
|
+
: `mysql=fallback(${mysql.path})`);
|
|
405
|
+
} else {
|
|
406
|
+
parts.push('mysql=missing');
|
|
407
|
+
}
|
|
408
|
+
if (editor) {
|
|
409
|
+
parts.push(editor.source === 'path'
|
|
410
|
+
? 'mysql_config_editor=PATH'
|
|
411
|
+
: `mysql_config_editor=fallback(${editor.path})`);
|
|
412
|
+
} else {
|
|
413
|
+
parts.push('mysql_config_editor=missing');
|
|
414
|
+
}
|
|
415
|
+
const missingDetail = !mysql
|
|
416
|
+
? 'safe-read 실행에는 mysql이 필요합니다.'
|
|
417
|
+
: 'login-path alias setup에는 mysql_config_editor가 필요합니다.';
|
|
418
|
+
checks.push(check('warn', 'DB client', `${parts.join(' | ')}. ${missingDetail} ${mysqlClientRecoveryHint(track)}`));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const loginFile = join(homedir(), '.mylogin.cnf');
|
|
422
|
+
if (!editor) {
|
|
423
|
+
checks.push(check('warn', 'DB login-path', `mysql_config_editor를 찾지 못해 login-path alias를 확인할 수 없습니다. ${mysqlClientRecoveryHint(track)}`));
|
|
424
|
+
return checks;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!existsSync(loginFile)) {
|
|
428
|
+
checks.push(check('warn', 'DB login-path', `${loginFile} 파일이 없습니다. 먼저 "${dbSafeReadSetupCommand(track)}"로 login-path setup을 진행하세요.`));
|
|
429
|
+
return checks;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const result = spawnSync(editor.path, ['print', '--all'], { encoding: 'utf-8' });
|
|
433
|
+
if (result.error) {
|
|
434
|
+
checks.push(check('warn', 'DB login-path', `alias 확인 실패: ${result.error.message}`));
|
|
435
|
+
return checks;
|
|
436
|
+
}
|
|
437
|
+
if (result.status !== 0) {
|
|
438
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
439
|
+
checks.push(check('warn', 'DB login-path', `alias 확인 실패(종료 코드 ${result.status})${stderr ? `: ${stderr}` : ''}`));
|
|
440
|
+
return checks;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const aliasNames = (typeof result.stdout === 'string' ? result.stdout : '')
|
|
444
|
+
.split(/\r?\n/)
|
|
445
|
+
.map((line) => line.trim())
|
|
446
|
+
.filter((line) => line.startsWith('[') && line.endsWith(']'))
|
|
447
|
+
.map((line) => line.slice(1, -1));
|
|
448
|
+
|
|
449
|
+
const trackAliases = aliasNames.filter((alias) => alias.startsWith('codex-db-'));
|
|
450
|
+
if (trackAliases.includes(targetAlias)) {
|
|
451
|
+
checks.push(check('pass', 'DB login-path', `track alias 확인: ${targetAlias}`));
|
|
452
|
+
return checks;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (trackAliases.length > 0) {
|
|
456
|
+
checks.push(check('warn', 'DB login-path', `다른 track alias는 있지만 현재 track alias(${targetAlias})는 없습니다. 현재 프로젝트에서 "${dbSafeReadSetupCommand(track)}"로 맞출 수 있습니다.`));
|
|
457
|
+
return checks;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (aliasNames.includes(LEGACY_DB_LOGIN_PATH)) {
|
|
461
|
+
checks.push(check('warn', 'DB login-path', `legacy alias만 확인: ${LEGACY_DB_LOGIN_PATH}. 현재 프로젝트에서 "${dbSafeReadSetupCommand(track)}"로 ${targetAlias} alias를 만들 수 있습니다.`));
|
|
462
|
+
return checks;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
checks.push(check('warn', 'DB login-path', `사용 가능한 alias를 확인하지 못했습니다. 현재 alias=${summarizeAliases(aliasNames)}. 현재 프로젝트에서 "${dbSafeReadSetupCommand(track)}"를 실행하세요.`));
|
|
466
|
+
return checks;
|
|
467
|
+
}
|
|
468
|
+
|
|
61
469
|
function runCommandOrFail(command, args, options = {}) {
|
|
62
470
|
const result = spawnSync(command, args, {
|
|
63
471
|
stdio: 'inherit',
|
|
@@ -121,7 +529,7 @@ function ensureFullBootstrapHistory(repoDir, verbose) {
|
|
|
121
529
|
|
|
122
530
|
function resolveLocalDispatcher(command) {
|
|
123
531
|
if (process.env.SK_DISABLE_FORWARD === '1') return null;
|
|
124
|
-
if (command === 'setup
|
|
532
|
+
if (command === 'setup-internal') return null;
|
|
125
533
|
if (hasManagedSources(REPO_ROOT)) return null;
|
|
126
534
|
|
|
127
535
|
const localScript = canonicalScriptPath();
|
|
@@ -219,7 +627,7 @@ function printUsage() {
|
|
|
219
627
|
' sk search "<query>" 로컬 Codex 세션 기록 검색',
|
|
220
628
|
'',
|
|
221
629
|
'📝 사용법',
|
|
222
|
-
' sk setup [--dir <path>] [--repo <url>] [--dry-run] [--verbose] [--configure-notion] [--skip-notion]',
|
|
630
|
+
' sk setup [--dir <path>] [--cwd <path>] [--repo <url>] [--dry-run] [--verbose] [--configure-notion] [--configure-db-safe-read] [--track <name>] [--skip-notion]',
|
|
223
631
|
' sk init [--cwd <path>] [--dry-run] [--force] [--verbose]',
|
|
224
632
|
' sk check [--cwd <path>] [--json]',
|
|
225
633
|
' sk status [--cwd <path>] [--json]',
|
|
@@ -233,6 +641,7 @@ function printUsage() {
|
|
|
233
641
|
' - 배포된 npm 패키지는 설치 진입점과 디스패처 역할만 합니다.',
|
|
234
642
|
` - setup은 수정 가능한 원본 저장소를 ${canonicalRepoRoot()} 경로에 준비합니다.`,
|
|
235
643
|
' - setup은 로컬 원본 저장소를 기준으로 config, rules, 공용 agent를 동기화하고 check까지 실행합니다.',
|
|
644
|
+
' - setup에 --configure-db-safe-read를 붙이면 현재 프로젝트(track) 기준 DB login-path alias를 sk 안에서 맞출 수 있습니다.',
|
|
236
645
|
' - init은 현재 프로젝트 루트에 .sk 상태 파일만 준비합니다.',
|
|
237
646
|
' - setup은 ~/.codex/sk/notion/workspace.json 아래에 Notion 설정 파일도 만들 수 있습니다.',
|
|
238
647
|
` - 프로젝트 상태는 ${STATE_DIRNAME}/ 아래에 저장됩니다.`,
|
|
@@ -240,7 +649,7 @@ function printUsage() {
|
|
|
240
649
|
` - 공용 메모는 ${STATE_DIRNAME}/${NOTEPAD_FILE} 에 저장됩니다.`,
|
|
241
650
|
' - 다른 프로젝트를 대상으로 하려면 cd 대신 init/status/memory에 --cwd <path>를 사용할 수 있습니다.',
|
|
242
651
|
' - user 설정은 $CODEX_HOME 또는 ~/.codex를 대상으로 사용합니다.',
|
|
243
|
-
' - Notion 기록이
|
|
652
|
+
' - Notion 기록이 invalid_grant/unauthorized/forbidden 같은 인증 오류로 실패하면 "codex mcp login notion"으로 MCP 인증을 다시 연결하세요.',
|
|
244
653
|
` - 자세한 가이드: ${guidePath}`,
|
|
245
654
|
].join('\n'),
|
|
246
655
|
);
|
|
@@ -273,12 +682,12 @@ function parseFlags(args) {
|
|
|
273
682
|
continue;
|
|
274
683
|
}
|
|
275
684
|
|
|
276
|
-
if (token === '--json' || token === '--dry-run' || token === '--verbose' || token === '--case-sensitive' || token === '--force' || token === '--configure-notion' || token === '--skip-notion') {
|
|
685
|
+
if (token === '--json' || token === '--dry-run' || token === '--verbose' || token === '--case-sensitive' || token === '--force' || token === '--configure-notion' || token === '--configure-db' || token === '--configure-db-safe-read' || token === '--skip-notion') {
|
|
277
686
|
flags.set(token.slice(2), true);
|
|
278
687
|
continue;
|
|
279
688
|
}
|
|
280
689
|
|
|
281
|
-
if (token === '--scope' || token === '--cwd' || token === '--dir' || token === '--repo' || token === '--limit' || token === '--since' || token === '--project' || token === '--context' || token === '--session' || token === '--category' || token === '--priority') {
|
|
690
|
+
if (token === '--scope' || token === '--cwd' || token === '--dir' || token === '--repo' || token === '--limit' || token === '--since' || token === '--project' || token === '--context' || token === '--session' || token === '--category' || token === '--priority' || token === '--track' || token === '--db-cwd') {
|
|
282
691
|
const next = args[index + 1];
|
|
283
692
|
if (!next || next.startsWith('-')) {
|
|
284
693
|
fail(`${token} 뒤에 값이 필요합니다.`);
|
|
@@ -291,6 +700,10 @@ function parseFlags(args) {
|
|
|
291
700
|
fail(`알 수 없는 옵션입니다: ${token}`);
|
|
292
701
|
}
|
|
293
702
|
|
|
703
|
+
if (flags.get('configure-db-safe-read')) {
|
|
704
|
+
flags.set('configure-db', true);
|
|
705
|
+
}
|
|
706
|
+
|
|
294
707
|
return { flags, rest };
|
|
295
708
|
}
|
|
296
709
|
|
|
@@ -659,11 +1072,17 @@ async function writeScopeFile(scope, cwd, dryRun) {
|
|
|
659
1072
|
|
|
660
1073
|
function buildInternalSetupArgs(flags, targetDir) {
|
|
661
1074
|
const args = ['setup-internal', '--scope', 'user', '--cwd', targetDir];
|
|
662
|
-
for (const name of ['dry-run', 'verbose', 'configure-notion', 'skip-notion']) {
|
|
1075
|
+
for (const name of ['dry-run', 'verbose', 'configure-notion', 'configure-db', 'skip-notion']) {
|
|
663
1076
|
if (flags.get(name)) {
|
|
664
1077
|
args.push(`--${name}`);
|
|
665
1078
|
}
|
|
666
1079
|
}
|
|
1080
|
+
if (flags.get('track')) {
|
|
1081
|
+
args.push('--track', String(flags.get('track')));
|
|
1082
|
+
}
|
|
1083
|
+
if (flags.get('db-cwd')) {
|
|
1084
|
+
args.push('--db-cwd', String(flags.get('db-cwd')));
|
|
1085
|
+
}
|
|
667
1086
|
return args;
|
|
668
1087
|
}
|
|
669
1088
|
|
|
@@ -927,7 +1346,11 @@ async function runSetup(flags) {
|
|
|
927
1346
|
const dryRun = Boolean(flags.get('dry-run'));
|
|
928
1347
|
const verbose = Boolean(flags.get('verbose'));
|
|
929
1348
|
const configureNotion = Boolean(flags.get('configure-notion'));
|
|
1349
|
+
const configureDb = Boolean(flags.get('configure-db'));
|
|
930
1350
|
const skipNotion = Boolean(flags.get('skip-notion'));
|
|
1351
|
+
const force = Boolean(flags.get('force'));
|
|
1352
|
+
const dbContextCwd = resolve(String(flags.get('db-cwd') || requestedCwd));
|
|
1353
|
+
const trackOverride = flags.get('track') ? String(flags.get('track')) : null;
|
|
931
1354
|
const sources = await validateSources();
|
|
932
1355
|
const target = targetPaths(scope, cwd);
|
|
933
1356
|
const mergedConfig = await buildMergedConfig();
|
|
@@ -984,6 +1407,14 @@ async function runSetup(flags) {
|
|
|
984
1407
|
configure: configureNotion,
|
|
985
1408
|
skipNotion,
|
|
986
1409
|
});
|
|
1410
|
+
const dbSummary = await ensureDbSafeReadConfig({
|
|
1411
|
+
configure: configureDb,
|
|
1412
|
+
dryRun,
|
|
1413
|
+
force,
|
|
1414
|
+
verbose,
|
|
1415
|
+
trackOverride,
|
|
1416
|
+
contextCwd: dbContextCwd,
|
|
1417
|
+
});
|
|
987
1418
|
|
|
988
1419
|
const scopePath = await writeScopeFile(scope, cwd, dryRun);
|
|
989
1420
|
const stateSummary = await ensureProjectStateFiles({ cwd, dryRun, verbose });
|
|
@@ -996,6 +1427,9 @@ async function runSetup(flags) {
|
|
|
996
1427
|
console.log(` 📏 규칙 파일: ${target.rulesPath}`);
|
|
997
1428
|
console.log(` 🤖 에이전트: ${target.agentsDir}`);
|
|
998
1429
|
console.log(` 📝 Notion 설정: ${notionSummary.path}`);
|
|
1430
|
+
if (configureDb) {
|
|
1431
|
+
console.log(` 🗄️ DB safe-read: ${dbSummary.message}`);
|
|
1432
|
+
}
|
|
999
1433
|
console.log(` 🗂️ 상태 파일: ${scopePath}`);
|
|
1000
1434
|
console.log(` 🧠 프로젝트 메모리: ${projectMemoryPath(cwd)}`);
|
|
1001
1435
|
console.log(` 📒 메모장: ${notepadPath(cwd)}`);
|
|
@@ -1009,7 +1443,7 @@ async function runSetup(flags) {
|
|
|
1009
1443
|
);
|
|
1010
1444
|
if (notionSummary.configured) {
|
|
1011
1445
|
console.log(' ✅ Notion 워크스페이스 설정이 준비되었습니다.');
|
|
1012
|
-
console.log(' 🔐 Codex에서 Notion 문서 기록이 실패하면 아래 명령으로 MCP 인증을 다시 연결하세요.');
|
|
1446
|
+
console.log(' 🔐 Codex에서 Notion 문서 기록이 invalid_grant/unauthorized/forbidden 같은 인증 오류로 실패하면 아래 명령으로 MCP 인증을 다시 연결하세요.');
|
|
1013
1447
|
console.log(' codex mcp login notion');
|
|
1014
1448
|
} else {
|
|
1015
1449
|
console.log(' ⚠️ Notion 설정이 아직 완전하지 않습니다. 준비되면 "sk setup --configure-notion"을 다시 실행하세요.');
|
|
@@ -1017,6 +1451,7 @@ async function runSetup(flags) {
|
|
|
1017
1451
|
}
|
|
1018
1452
|
|
|
1019
1453
|
async function runSetupCommand(flags) {
|
|
1454
|
+
const invocationCwd = resolveWorkingDirectory(flags);
|
|
1020
1455
|
const targetDir = resolve(String(flags.get('dir') || canonicalRepoRoot()));
|
|
1021
1456
|
const repoUrl = String(flags.get('repo') || DEFAULT_BOOTSTRAP_REPO);
|
|
1022
1457
|
const verbose = Boolean(flags.get('verbose'));
|
|
@@ -1077,12 +1512,16 @@ async function runSetupCommand(flags) {
|
|
|
1077
1512
|
SK_DISABLE_FORWARD: '1',
|
|
1078
1513
|
SK_REPO_ROOT: targetDir,
|
|
1079
1514
|
};
|
|
1515
|
+
if (!flags.get('db-cwd')) {
|
|
1516
|
+
flags.set('db-cwd', invocationCwd);
|
|
1517
|
+
}
|
|
1080
1518
|
|
|
1081
1519
|
console.log(`sk setup (${repoStatus === 'cloned' ? '새로 복제됨' : repoStatus === 'reused' ? '기존 저장소 재사용' : repoStatus === 'current-source' ? '현재 저장소 사용' : repoStatus})`);
|
|
1082
1520
|
console.log(` 📁 기준 저장소: ${targetDir}`);
|
|
1083
1521
|
console.log(` 🔗 원본: ${repoUrl}`);
|
|
1084
1522
|
runCommandOrFail(process.execPath, [localScript, ...buildInternalSetupArgs(flags, targetDir)], { env: sharedEnv });
|
|
1085
|
-
|
|
1523
|
+
const verifyCwd = flags.get('configure-db') ? invocationCwd : targetDir;
|
|
1524
|
+
runCommandOrFail(process.execPath, [localScript, 'check', '--cwd', verifyCwd], { env: sharedEnv });
|
|
1086
1525
|
|
|
1087
1526
|
console.log(' ✅ 완료: setup이 끝났습니다.');
|
|
1088
1527
|
if (!hasManagedSources(REPO_ROOT)) {
|
|
@@ -1208,6 +1647,8 @@ async function buildDoctorResult(cwd = process.cwd()) {
|
|
|
1208
1647
|
? check('pass', '메모장', notepadPath(cwd))
|
|
1209
1648
|
: check('warn', '메모장', `${notepadPath(cwd)} 파일이 없습니다.`));
|
|
1210
1649
|
|
|
1650
|
+
checks.push(...buildDbSafeReadChecks(cwd));
|
|
1651
|
+
|
|
1211
1652
|
if (!existsSync(target.notionConfigPath)) {
|
|
1212
1653
|
checks.push(check('warn', 'Notion 설정', `${target.notionConfigPath} 파일이 없습니다. "sk setup --configure-notion"을 실행하세요.`));
|
|
1213
1654
|
} else if (!notionConfig || !notionWorkspaceConfigured(notionConfig)) {
|
|
@@ -1297,7 +1738,7 @@ async function runStatus(flags) {
|
|
|
1297
1738
|
console.log(` ${icon} ${entry.name}: ${entry.message}`);
|
|
1298
1739
|
}
|
|
1299
1740
|
if (status.notion_config_exists && status.notion_configured) {
|
|
1300
|
-
console.log(' 💡 Notion 기록이
|
|
1741
|
+
console.log(' 💡 Notion 기록이 invalid_grant/unauthorized/forbidden 같은 인증 오류로 실패하면 "codex mcp login notion"으로 MCP 인증을 다시 연결하세요.');
|
|
1301
1742
|
}
|
|
1302
1743
|
if (status.checks.some((entry) => entry.status === 'fail')) {
|
|
1303
1744
|
process.exitCode = 1;
|