@kaydash9999/banker-plugins 0.23.3 → 0.24.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +14 -6
- package/codex/manifest.json +6 -2
- package/codex/transform-matrix.md +6 -2
- package/package.json +1 -1
- package/skills/build-testbox/SKILL.md +213 -0
- package/skills/build-testbox/scripts/args.mjs +156 -0
- package/skills/build-testbox/scripts/browser-cell.mjs +181 -0
- package/skills/build-testbox/scripts/browsers.mjs +164 -0
- package/skills/build-testbox/scripts/containers.mjs +237 -0
- package/skills/build-testbox/scripts/detect.mjs +166 -0
- package/skills/build-testbox/scripts/proc.mjs +91 -0
- package/skills/build-testbox/scripts/report.mjs +109 -0
- package/skills/build-testbox/scripts/setup.mjs +144 -0
- package/skills/build-testbox/scripts/testbox.mjs +148 -0
- package/skills/git-init/SKILL.md +152 -0
- package/skills/git-init/scripts/git-init.mjs +440 -0
- package/skills/git-init/scripts/marker-block.mjs +311 -0
- package/skills/git-init/scripts/payload-scan.mjs +186 -0
- package/skills/init-dir/SKILL.md +172 -0
- package/skills/init-dir/scripts/init-dir.mjs +308 -0
- package/skills/init-dir/scripts/marker-block.mjs +311 -0
- package/skills/init-dir/scripts/templates.mjs +100 -0
- package/skills/init-dir/templates/KB/README.md +15 -0
- package/skills/init-dir/templates/docs/README.md +14 -0
- package/skills/init-dir/templates/logs/README.md +14 -0
- package/skills/init-dir/templates/logs/gitignore +3 -0
- package/skills/init-dir/templates/mockup/README.md +7 -0
- package/skills/init-dir/templates/scripts/README.md +36 -0
- package/skills/init-dir/templates/scripts/lib/service.mjs +540 -0
- package/skills/init-dir/templates/scripts/restart.cmd +13 -0
- package/skills/init-dir/templates/scripts/restart.sh +10 -0
- package/skills/init-dir/templates/scripts/service.conf +5 -0
- package/skills/init-dir/templates/scripts/start.cmd +13 -0
- package/skills/init-dir/templates/scripts/start.sh +10 -0
- package/skills/init-dir/templates/scripts/status.cmd +13 -0
- package/skills/init-dir/templates/scripts/status.sh +10 -0
- package/skills/init-dir/templates/scripts/stop.cmd +13 -0
- package/skills/init-dir/templates/scripts/stop.sh +10 -0
- package/skills/init-dir/templates/tests/README.md +15 -0
- package/skills/init-dir/templates/tests/run.cmd +13 -0
- package/skills/init-dir/templates/tests/run.mjs +160 -0
- package/skills/init-dir/templates/tests/run.sh +10 -0
- package/skills/init-dir/templates/tests/tests.conf +7 -0
- package/skills/init-dir/templates/work/README.md +8 -0
- package/skills/progress-mon/README.md +10 -8
- package/skills/progress-mon/SKILL.md +53 -16
- package/skills/progress-mon/assets/fixture/README.md +1 -1
- package/skills/progress-mon/assets/fixture/adr.json +35 -0
- package/skills/progress-mon/assets/fixture/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/flow.json +2 -2
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/variants/legacy-init/rules.json +6 -0
- package/skills/progress-mon/assets/fixture/variants/legacy-init/state.json +5 -0
- package/skills/progress-mon/references/adr.md +177 -0
- package/skills/progress-mon/references/authoring.md +13 -5
- package/skills/progress-mon/scripts/pm-adr.mjs +585 -0
- package/skills/progress-mon/scripts/pm-check.mjs +48 -20
- package/skills/progress-mon/scripts/pm-data.mjs +1 -1
- package/skills/progress-mon/scripts/pm-html.mjs +1 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +113 -28
- package/skills/progress-mon/scripts/pm-render.mjs +6 -4
- package/skills/progress-mon/scripts/pm-server.mjs +2 -1
- package/skills/progress-mon/scripts/pm-sources.mjs +1 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +3 -2
- package/skills/tune-tmux/SKILL.md +235 -0
- package/skills/tune-tmux/scripts/app-notes.mjs +38 -0
- package/skills/tune-tmux/scripts/marker-block.mjs +311 -0
- package/skills/tune-tmux/scripts/psmux-caps.json +71 -0
- package/skills/tune-tmux/scripts/tmux-config.mjs +251 -0
- package/skills/tune-tmux/scripts/tune-tmux.mjs +284 -0
- package/skills/ultra-chain/SKILL.md +52 -22
- package/skills/ultra-chain/references/chain-ledger.mjs +93 -11
- package/skills/ultra-chain/references/chain-report.mjs +13 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: init-dir
|
|
3
|
+
description: "프로젝트에 관리 폴더 7개(logs, scripts, work, mockup, tests, docs, KB)를 만들고, 폴더별 목적을 지금 세션에 전달하며, AGENTS.md 와 CLAUDE.md 중 있는 것에 표지 블록으로 기록. 새로 만든 폴더에만 틀 파일(서비스 시작, 정지 스크립트와 로그 로테이션, 시험 실행기, README)을 넣고, 이미 있는 폴더는 만들지 않고 목적만 전달. 'init-dir'/'폴더 초기화'/'프로젝트 폴더 만들기'/'폴더 구조 잡기' 시 사용."
|
|
4
|
+
argument-hint: "[apply|status] [--only=<폴더,...>] [--follow-symlink] [--dry-run]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# init-dir: 프로젝트 관리 폴더 초기화
|
|
8
|
+
|
|
9
|
+
프로젝트 루트에 관리 폴더를 만들고 폴더마다 목적을 정한다.
|
|
10
|
+
목적은 두 곳에 남는다: 지금 세션(스크립트 출력의 "폴더 목적 요약")과 지시 파일(`AGENTS.md`, `CLAUDE.md`)의 표지 블록.
|
|
11
|
+
답변은 한글.
|
|
12
|
+
|
|
13
|
+
| 폴더 | 목적 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `logs/` | 서비스 로그. 표준 출력과 표준 오류를 나눠 저장, 100MB 로테이션, 스트림마다 최대 7개 |
|
|
16
|
+
| `scripts/` | `./scripts/start.sh`, `stop.sh`, `status.sh`, `restart.sh`. Windows 는 같은 이름의 `.cmd` |
|
|
17
|
+
| `work/` | 사용자와 상호작용하는 작업공간. 사용자가 에이전트에게 줄 파일을 둠 |
|
|
18
|
+
| `mockup/` | 목업 파일 관리 |
|
|
19
|
+
| `tests/` | 단위, 통합 시험 스크립트와 결과를 간결하게 출력하는 실행기 |
|
|
20
|
+
| `docs/` | 문서를 관리하는 영역. evidence와 ADR도 포함하여 보관한다. |
|
|
21
|
+
| `KB/` | knowledge base. OMC, OMX 위키 밖에서 사용자가 알아야 할 내용을 두는 2차 위키 |
|
|
22
|
+
|
|
23
|
+
## 언제 쓰나
|
|
24
|
+
|
|
25
|
+
- 새 프로젝트의 폴더 구조를 잡을 때
|
|
26
|
+
- 이미 있는 프로젝트에서 폴더 목적을 에이전트에게 알리고 지시 파일에 남길 때(있는 폴더는 건드리지 않음)
|
|
27
|
+
|
|
28
|
+
## 쓰지 않을 때
|
|
29
|
+
|
|
30
|
+
- git 저장소 초기화와 `.gitignore`: banker `git-init`
|
|
31
|
+
- 코드베이스 전체의 계층형 `AGENTS.md` 문서: banker `deep-init`
|
|
32
|
+
|
|
33
|
+
## 자리 표시
|
|
34
|
+
|
|
35
|
+
| 자리 | 뜻 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `<S>` | 이 스킬 디렉터리 절대경로(아래 런타임 표의 "스킬 디렉터리") |
|
|
38
|
+
| `<rt>` | `claude` 또는 `codex`(런타임 표의 `--runtime`) |
|
|
39
|
+
| `<root>` | 대상 폴더. 기본은 작업 폴더. 다르면 `--root <dir>` |
|
|
40
|
+
|
|
41
|
+
- 셸 변수는 도구 호출 사이에 남지 않는다. 명령마다 실제 경로를 적는다
|
|
42
|
+
|
|
43
|
+
## 런타임 표
|
|
44
|
+
|
|
45
|
+
| 항목 | Claude Code | Codex |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| 스킬 디렉터리 | 스킬 맨 위 "Base directory for this skill:" 경로 | Codex 스킬 목록이 보이는 이 SKILL.md 경로(file)의 폴더. npm 설치는 `~/.codex/skills/banker-init-dir/`(프로젝트 범위 설치면 `./.codex/skills/banker-init-dir/`), 플러그인 설치(`codex plugin add`)는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/init-dir/`. 절대경로로 씀 |
|
|
48
|
+
| `--runtime` | `claude` | `codex` |
|
|
49
|
+
| 지시 파일이 둘 다 없을 때 만드는 파일 | `CLAUDE.md` | `AGENTS.md` |
|
|
50
|
+
| 작업 폴더 안 실행 | 바로 실행 | 바로 실행(기본 샌드박스의 작업 폴더 쓰기 범위) |
|
|
51
|
+
| 작업 폴더 밖 `--root` | 바로 실행 | 명령에 `sandbox_permissions: "require_escalated"` 로 승인 요청. `--add-dir` 로 설정 폴더를 넓히지 않음 |
|
|
52
|
+
| 생성된 `./scripts/start.sh` 실행 | 바로 실행 | 서비스가 세션 뒤에도 남아야 하면 승인 요청. 세션을 닫으면 꺼질 수 있으므로 사용자 터미널 명령을 안내 |
|
|
53
|
+
|
|
54
|
+
## 실행
|
|
55
|
+
|
|
56
|
+
1. 인자 확인
|
|
57
|
+
- 하위 명령은 `apply`(기본)나 `status`. 그 밖은 실행하지 않고 사용법 안내
|
|
58
|
+
- `--only` 값은 `logs, scripts, work, mockup, tests, docs, KB` 의 이름만 쉼표로. 대소문자는 무시하고 `KB` 로 맞춤
|
|
59
|
+
- 사용자 표기 `Agents.md`, `Claude.md` 는 `AGENTS.md`, `CLAUDE.md` 로 해석
|
|
60
|
+
- 사용자 입력을 셸 명령에 그대로 붙이지 않음
|
|
61
|
+
2. 스크립트 실행
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
node "<S>/scripts/init-dir.mjs" apply --runtime <rt> [--root "<root>"] [--only=<폴더,...>]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- 계획만 보려면 `--dry-run`. 기계가 읽을 출력은 `--json`(JSON 한 줄)
|
|
68
|
+
- 상태만 보려면 `node "<S>/scripts/init-dir.mjs" status [--root "<root>"]`
|
|
69
|
+
3. 결과 처리
|
|
70
|
+
- 출력 표(폴더마다 `만듦` 또는 `있음(목적만 전달)`, 틀 파일마다 `만듦`, 지시 파일 결과)를 사용자에게 보고
|
|
71
|
+
- `경고:` 줄은 그대로 전함
|
|
72
|
+
- 출력 끝의 "폴더 목적 요약" 을 이 세션의 작업 규칙으로 바로 적용. 예: 새 로그는 `logs/`, 시험 스크립트는 `tests/`, 사용자가 준 파일은 `work/` 에서 찾음. 다음 세션은 지시 파일의 블록이 같은 내용을 전함
|
|
73
|
+
4. 종료 코드
|
|
74
|
+
|
|
75
|
+
| 코드 | 뜻 | 처리 |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| 0 | 완료. 바뀐 것 없음 포함 | 결과 보고와 목적 요약 적용 |
|
|
78
|
+
| 1 | 거부 또는 쓰기 실패. 거부는 아무것도 바꾸지 않음 | 메시지 그대로 전하고 아래 "문제 해결" |
|
|
79
|
+
| 2 | 사용 오류(`--runtime` 없음, 모르는 인자, 모르는 폴더 이름, 루트 폴더 없음) | 인자를 고쳐 다시 실행 |
|
|
80
|
+
| 3 | 환경(대소문자만 다른 지시 파일, 루트가 HOME 이나 파일 시스템 루트) | 메시지 전달. 이름을 바꿀지, 어느 폴더에서 실행할지 사용자에게 물음 |
|
|
81
|
+
|
|
82
|
+
- 출력이 비어 있으면 실행 실패. 성공으로 보고하지 않음
|
|
83
|
+
|
|
84
|
+
## 폴더와 틀
|
|
85
|
+
|
|
86
|
+
- 틀은 이번 실행에서 새로 만든 폴더에만 쓴다. 이미 있던 폴더에는 틀을 하나도 넣지 않고 목적만 전달한다(일부만 생겨 서로 안 맞는 상태 방지)
|
|
87
|
+
- 새 폴더는 틀을 다 갖추거나 아예 생기지 않는다. 같은 부모 아래 임시 폴더 `.<이름>.init-dir-<난수>` 에 틀을 다 쓴 뒤 이름을 바꿔 옮긴다. 쓰기나 이름 바꾸기가 실패하면 임시 폴더를 지우고 종료 1(`쓰기 실패:`). 대상 폴더가 없으니 다시 실행하면 완전한 폴더가 생긴다
|
|
88
|
+
- 강제 종료로 끊기면 임시 폴더가 남을 수 있다. `status` 와 `apply` 가 `남은 임시 폴더: <경로> (지워도 됨)` 로 보고한다. 자동으로 지우지 않는다. 관리 폴더가 아니므로 사용자 확인을 받아 지운다. git-init 은 이 꼴(`.*.init-dir-*/`)을 첫 커밋에서 뺀다
|
|
89
|
+
- 틀 파일은 새로 만들기만 한다. 같은 이름의 파일을 덮어쓰지 않는다
|
|
90
|
+
- `.sh` 는 POSIX sh, 권한 0755. `.cmd` 는 CRLF
|
|
91
|
+
|
|
92
|
+
| 폴더 | 만드는 틀 |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `logs/` | `README.md`(로그 규칙), `.gitignore`(`*`, `!.gitignore`, `!README.md`) |
|
|
95
|
+
| `scripts/` | `start.sh`, `stop.sh`, `status.sh`, `restart.sh` 와 같은 이름의 `.cmd`, `service.conf`(`name=app`, 빈 `command=`), `lib/service.mjs`, `README.md` |
|
|
96
|
+
| `work/` | `README.md`(비밀값을 두지 말 것. 커밋 전 git-init 이나 audit-security 스캔이 잡음) |
|
|
97
|
+
| `mockup/` | `README.md` |
|
|
98
|
+
| `tests/` | `run.sh`, `run.cmd`, `run.mjs`, `tests.conf`(`unit=`, `integration=`), `unit/`, `integration/`, `README.md` |
|
|
99
|
+
| `docs/` | `README.md`(evidence 와 ADR 보관 안내, 하위 폴더 뜻), `evidence/`, `adr/`(빈 `.gitkeep`) |
|
|
100
|
+
| `KB/` | `README.md`(2차 위키의 대상과 색인 규칙) |
|
|
101
|
+
|
|
102
|
+
- `work/`, `mockup/` 는 git 에서 빼지 않는다
|
|
103
|
+
|
|
104
|
+
### scripts 틀: `lib/service.mjs`
|
|
105
|
+
|
|
106
|
+
| 항목 | 내용 |
|
|
107
|
+
|---|---|
|
|
108
|
+
| 설정 | `scripts/service.conf` 의 `name=`(로그와 PID 파일 이름), `command=`(프로젝트 폴더에서 셸로 실행할 한 줄) |
|
|
109
|
+
| 구조 | `start` 가 분리된 감독 프로세스를 띄움. 감독이 `command` 를 자식으로 띄우고 표준 출력은 `logs/<이름>.out.log`, 표준 오류는 `logs/<이름>.err.log` 에 씀 |
|
|
110
|
+
| PID 파일 | `logs/<이름>.pid` 에 `{supervisor, child, startedAt}` |
|
|
111
|
+
| 로테이션 | 쓰기 전에 "현재 크기 + 쓸 양 > 100MB(104857600 바이트)" 면 돌림. `.log` 가 `.1.log` 로 밀리고 `.6.log` 는 지움. 현재 파일 포함 7개, 스트림당 최대 700MB |
|
|
112
|
+
| stop | 감독에 SIGTERM. 감독이 자식의 프로세스 그룹에 넘기고 10초 뒤 SIGKILL. 감독이 없으면 자식에 직접. Windows 는 `taskkill /T`, 10초 뒤 `/F` |
|
|
113
|
+
| status | 감독과 자식의 실행 여부를 따로, 로그 파일 수와 크기 |
|
|
114
|
+
| 시작 잠금 | `start` 는 `logs/<이름>.start.lock` 을 O_EXCL 로 만들어 잡고 끝나면 지움. 동시에 여러 번 실행해도 감독은 하나, 나중 실행은 기다린 뒤 `이미 실행 중`. 죽은 PID, 다른 프로그램, 30초 넘은 잠금은 넘겨받음. 링크나 폴더 자리는 거부 |
|
|
115
|
+
| 종료 코드 | 0 성공(status 는 둘 다 실행 중), 1 실패(시작 2초 안에 멈춤, 멈추지 못함, status 에서 실행 중 아님), 2 설정 없음(`command` 비어 있음), 3 Node 18 이상 없음(래퍼) |
|
|
116
|
+
|
|
117
|
+
- 대상 프로젝트에 Node 18 이상이 필요하다. 다른 Node 는 환경 변수 `NODE` 에 경로
|
|
118
|
+
- 조정용 환경 변수: `SERVICE_LOG_ROTATE_BYTES`, `SERVICE_STOP_GRACE_MS`, `SERVICE_START_CHECK_MS`
|
|
119
|
+
|
|
120
|
+
### tests 틀: `run.mjs`
|
|
121
|
+
|
|
122
|
+
- `tests.conf` 의 `<스위트>=<명령>` 을 차례로 실행하고 스위트마다 한 줄: `PASS unit (1.2s)`, `FAIL integration (0.3s, 종료 1)`
|
|
123
|
+
- 실패하면 그 출력의 마지막 20줄, 끝에 요약 한 줄
|
|
124
|
+
- 종료 코드: 0 모두 통과, 1 실패 있음, 2 설정 없음(실행할 명령 없음 포함), 3 Node 18 이상 없음
|
|
125
|
+
|
|
126
|
+
## 지시 파일 블록
|
|
127
|
+
|
|
128
|
+
- 대상: 정확한 이름의 `AGENTS.md`, `CLAUDE.md` 중 있는 것 전부. 둘 다 없으면 런타임 파일 하나를 만든다
|
|
129
|
+
- 표지: `<!-- banker:init-dir:start -->` 와 `<!-- banker:init-dir:end -->` 사이. 다시 실행하면 그 블록만 바꾼다
|
|
130
|
+
- `AGENTS.md` 에서는 자기 몫의 `<!-- USER:OMX:POLICY:START -->`, `<!-- USER:OMX:POLICY:END -->` 한 쌍으로 감싸 파일 끝에 둔다. 기존 정책 블록 안에 넣지 않는다. `omx setup --merge-agents` 가 이 블록을 지킨다
|
|
131
|
+
- 내용: 디스크에 있는 관리 폴더 전부의 한 줄 목적. 같은 디스크 상태면 같은 바이트(여러 번 실행해도 같음)
|
|
132
|
+
- 블록 밖 바이트, CRLF, BOM, 권한 비트는 그대로. 바이트가 바뀔 때만 첫 원본을 `<파일>.init-dir.bak` 에 둔다(이미 있으면 덮지 않음)
|
|
133
|
+
- `CLAUDE.md` 가 `AGENTS.md` 를 가리키는 링크면 기본은 거부(종료 1). `--follow-symlink` 면 같은 파일이므로 한 번만 쓰고, 블록은 `AGENTS.md` 꼴
|
|
134
|
+
- `AGENTS.override.md` 에 내용이 있으면 Codex 가 `AGENTS.md` 대신 그것을 읽는다고 경고한다. override 에는 쓰지 않는다
|
|
135
|
+
- 대소문자 구분 파일 시스템에서 대소문자만 다른 파일(`Agents.md`)만 있으면 경고하고 아무 파일도 만들지 않고 종료 3. 대소문자 무시 파일 시스템(macOS, Windows 기본)은 실제 이름 그대로 쓴다
|
|
136
|
+
|
|
137
|
+
## 오프라인
|
|
138
|
+
|
|
139
|
+
- 네트워크를 쓰지 않는다. 틀은 모두 스킬 안의 파일(`templates/`)에서 복사한다
|
|
140
|
+
|
|
141
|
+
## 함정
|
|
142
|
+
|
|
143
|
+
- 이미 있던 폴더에는 틀이 없다. 틀 파일이 필요하면 필요한 것만 사용자와 상의해 만든다. init-dir 은 기존 폴더를 지우거나 채우지 않는다
|
|
144
|
+
- 지시 파일의 블록을 손으로 고치면 다음 실행이 디스크 상태로 다시 쓴다. 손으로 쓸 내용은 블록 밖에 둔다
|
|
145
|
+
- 표지 줄 하나만 지우거나 문장 안에 표지를 옮기면 거부(종료 1)한다. 짝을 임의로 맞추지 않는다
|
|
146
|
+
- `scripts/` 의 감독이 죽으면 자식은 다음 출력 때 SIGPIPE 로 죽을 수 있다. `status` 가 감독과 자식을 따로 보이고, `stop` 이 남은 자식을 끝낸다
|
|
147
|
+
- 지금 세션은 지시 파일을 다시 읽지 않는다. 그래서 출력의 "폴더 목적 요약" 을 이 세션에 바로 적용한다
|
|
148
|
+
|
|
149
|
+
## 문제 해결
|
|
150
|
+
|
|
151
|
+
- 아래 표에서 파일을 보인다는 것은 `ls -l` 로 그 이름의 종류와 링크 대상 경로를 보이는 것이다. 링크가 아닌 일반 파일임을 본 뒤에만 내용을 연다
|
|
152
|
+
|
|
153
|
+
| 메시지 | 조치 |
|
|
154
|
+
|---|---|
|
|
155
|
+
| 심볼릭 링크라 그대로 둠 | 지시 파일이 링크. 링크와 대상 경로를 보이고, 대상에 쓸지 물은 뒤 `--follow-symlink` 로 다시 실행 |
|
|
156
|
+
| 끊어진 링크라 그대로 둠 | 링크가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 어떻게 할지 물음 |
|
|
157
|
+
| 하드 링크가 n개라 그대로 둠 | 같은 파일에 이름이 여럿. 쓰면 링크가 갈라짐. 어느 이름을 쓸지 물음 |
|
|
158
|
+
| 다른 사용자 소유라 그대로 둠 | 파일 소유자가 실행 사용자와 다름. 소유자 확인을 안내 |
|
|
159
|
+
| 표지 손상이라 그대로 둠 | 표지 줄이 한쪽만 있거나, 문장 안에 있거나, 블록이 둘 이상. 남은 표지 줄을 보이고 지울지 물은 뒤 다시 실행 |
|
|
160
|
+
| UTF-8 이 아니라 그대로 둠 | 지시 파일이 CP949, UTF-16 등. 인코딩을 보이고 UTF-8 로 바꿀지 물음 |
|
|
161
|
+
| 일반 파일이 아니라 그대로 둠 | 지시 파일 자리에 폴더나 FIFO 같은 특수 파일. 지우지 않고 무엇인지 보임 |
|
|
162
|
+
| 폴더 자리에 폴더가 아닌 것이 있어 거부 | `logs` 같은 이름이 파일. 옮기거나 이름을 바꿀지 물은 뒤 다시 실행 |
|
|
163
|
+
| 대소문자가 다른 지시 파일만 있음 | `Agents.md` 처럼 이름이 다름. `AGENTS.md` 로 바꿀지 물은 뒤 다시 실행 |
|
|
164
|
+
| 루트가 HOME 이라 거부 | 프로젝트 폴더에서 실행하거나 `--root` 로 지정 |
|
|
165
|
+
|
|
166
|
+
## 출처(조사한 것)
|
|
167
|
+
|
|
168
|
+
| 주제 | 확인한 사실 | 출처 |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| Claude Code 지시 파일 | 프로젝트 `CLAUDE.md` 를 세션 시작 때 읽음 | https://code.claude.com/docs/en/memory |
|
|
171
|
+
| Codex 지시 파일 | 폴더마다 `AGENTS.override.md` 가 있으면 그것을, 없으면 `AGENTS.md` 를 읽음 | https://developers.openai.com/codex/guides/agents-md |
|
|
172
|
+
| OMX 정책 블록 | `omx setup --merge-agents` 는 생성된 `AGENTS.md` 에서 `USER:OMX:POLICY` 구간만 옮김 | banker `tone-compact` 의 `instruction-toggle.mjs` 머리 주석(실측) |
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* init-dir: makes a project's managed folders (logs, scripts, work, mockup, tests, docs, KB), gives each newly
|
|
4
|
+
* made folder its template files, and records every managed folder's purpose in AGENTS.md and CLAUDE.md.
|
|
5
|
+
*
|
|
6
|
+
* node init-dir.mjs [apply] --runtime claude|codex [--root <dir>] [--only=<folder,...>] [--follow-symlink]
|
|
7
|
+
* [--dry-run] [--json]
|
|
8
|
+
* node init-dir.mjs status [--root <dir>] [--json]
|
|
9
|
+
*
|
|
10
|
+
* - A folder that is already there gets no template file, only its purpose passed on.
|
|
11
|
+
* - A new folder gets all its templates or does not appear: they are written into a temp folder beside it, which
|
|
12
|
+
* is then renamed into place. A failure removes the temp folder and exits 1, so the next run starts clean.
|
|
13
|
+
* A temp folder left by a killed run is reported by status and apply ("남은 임시 폴더"), never removed.
|
|
14
|
+
* - The block goes into each of AGENTS.md and CLAUDE.md that exists (exact names). With neither, the runtime's
|
|
15
|
+
* file is made (claude: CLAUDE.md, codex: AGENTS.md). In AGENTS.md the block sits inside its own
|
|
16
|
+
* USER:OMX:POLICY pair at the end, so `omx setup --merge-agents` keeps it.
|
|
17
|
+
* - The block lists every managed folder on disk, so the same disk gives the same bytes.
|
|
18
|
+
* - marker-block.mjs refuses links (unless --follow-symlink), hard links, other owners, broken markers and files
|
|
19
|
+
* that are not UTF-8; a refusal changes nothing.
|
|
20
|
+
*
|
|
21
|
+
* Exit status: 0 done (nothing to change included), 1 refused or a write failed, 2 usage error, 3 environment
|
|
22
|
+
* (only a differently cased instructions file on a case-sensitive file system, or the root is HOME or a file
|
|
23
|
+
* system root). No network is used.
|
|
24
|
+
*/
|
|
25
|
+
import { randomBytes } from "node:crypto";
|
|
26
|
+
import { lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
|
|
27
|
+
import { homedir } from "node:os";
|
|
28
|
+
import { join, parse, resolve } from "node:path";
|
|
29
|
+
import { pathToFileURL } from "node:url";
|
|
30
|
+
import { HTML, applyBlocks } from "./marker-block.mjs";
|
|
31
|
+
import { FOLDERS, PURPOSES, blockBody, summaryLines, writeTemplates } from "./templates.mjs";
|
|
32
|
+
|
|
33
|
+
export const USAGE = [
|
|
34
|
+
"init-dir: 프로젝트 관리 폴더(logs, scripts, work, mockup, tests, docs, KB)를 만들고 폴더 목적을 AGENTS.md, CLAUDE.md 에 기록",
|
|
35
|
+
"사용법: node init-dir.mjs [apply] --runtime claude|codex [--root <dir>] [--only=<폴더,...>] [--follow-symlink] [--dry-run] [--json]",
|
|
36
|
+
" node init-dir.mjs status [--root <dir>] [--json]",
|
|
37
|
+
"종료 코드: 0 성공(바뀐 것 없음 포함), 1 거부나 쓰기 실패, 2 사용 오류, 3 환경(대소문자 충돌, 루트가 HOME 이나 파일 시스템 루트)",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
const POLICY = { open: "<!-- USER:OMX:POLICY:START -->", close: "<!-- USER:OMX:POLICY:END -->" };
|
|
41
|
+
const INSTRUCTION_NAMES = ["AGENTS.md", "CLAUDE.md"];
|
|
42
|
+
const FLAGS = { "--follow-symlink": "follow", "--dry-run": "dryRun", "--json": "json", "--help": "help", "-h": "help" };
|
|
43
|
+
const VALUES = { "--runtime": "runtime", "--root": "root", "--only": "only" };
|
|
44
|
+
const SUBCOMMANDS = new Set(["apply", "status"]);
|
|
45
|
+
const fail = (code, error, extra = {}) => ({ code, error, ...extra });
|
|
46
|
+
|
|
47
|
+
function splitArg(a) {
|
|
48
|
+
const eq = a.indexOf("=");
|
|
49
|
+
return a.startsWith("--") && eq > 0 ? [a.slice(0, eq), a.slice(eq + 1)] : [a, undefined];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The options in argv, or { error } (exit 2). */
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const o = { cmd: "apply", runtime: null, root: process.cwd(), only: null, follow: false, dryRun: false, json: false, help: false };
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const [name, inline] = splitArg(argv[i]);
|
|
57
|
+
if (FLAGS[name] && inline === undefined) {
|
|
58
|
+
o[FLAGS[name]] = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (VALUES[name]) {
|
|
62
|
+
const value = inline ?? argv[++i];
|
|
63
|
+
if (!value) return { error: `${name} 에 값이 없음` };
|
|
64
|
+
o[VALUES[name]] = value;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (i === 0 && SUBCOMMANDS.has(name)) o.cmd = name;
|
|
68
|
+
else return { error: `모르는 인자: ${argv[i]}` };
|
|
69
|
+
}
|
|
70
|
+
return o;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The folder names in an --only value, case-insensitive, as FOLDERS spells them; or { error }. */
|
|
74
|
+
function normalizeOnly(value) {
|
|
75
|
+
const names = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
76
|
+
const unknown = names.filter((n) => !FOLDERS.some((f) => f.toLowerCase() === n.toLowerCase()));
|
|
77
|
+
if (unknown.length > 0) return { error: `모르는 폴더 이름: ${unknown.join(", ")} (쓸 수 있는 이름: ${FOLDERS.join(", ")})` };
|
|
78
|
+
if (names.length === 0) return { error: "--only 에 폴더 이름이 없음" };
|
|
79
|
+
return { folders: FOLDERS.filter((f) => names.some((n) => n.toLowerCase() === f.toLowerCase())) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validate(o) {
|
|
83
|
+
if (o.runtime !== null && o.runtime !== "claude" && o.runtime !== "codex") return `--runtime 은 claude 나 codex: ${o.runtime}`;
|
|
84
|
+
if (o.cmd === "apply" && o.runtime === null) return "--runtime claude|codex 가 필요함";
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const real = (p) => {
|
|
89
|
+
try {
|
|
90
|
+
return realpathSync(p);
|
|
91
|
+
} catch {
|
|
92
|
+
return resolve(p);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// The root folder, or a failure: missing (2), HOME or a file system root (3).
|
|
97
|
+
function checkRoot(rootArg) {
|
|
98
|
+
const root = resolve(rootArg);
|
|
99
|
+
if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) return fail(2, `루트 폴더가 없음: ${root}`);
|
|
100
|
+
const r = real(root);
|
|
101
|
+
if (r === real(homedir())) return fail(3, `루트가 HOME 이라 거부: ${root}. 프로젝트 폴더에서 실행하거나 --root 로 지정`);
|
|
102
|
+
if (parse(r).root === r) return fail(3, `루트가 파일 시스템 루트라 거부: ${root}`);
|
|
103
|
+
return { root };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The instructions files to write, AGENTS.md first: the exact names present, or on a case-insensitive file system
|
|
108
|
+
* the name as it is on disk. A differently cased name alone on a case-sensitive file system is a conflict.
|
|
109
|
+
*/
|
|
110
|
+
function instructionFiles(root, runtime) {
|
|
111
|
+
const listing = readdirSync(root);
|
|
112
|
+
const files = [];
|
|
113
|
+
const conflicts = [];
|
|
114
|
+
for (const exact of INSTRUCTION_NAMES) {
|
|
115
|
+
const variant = listing.find((n) => n !== exact && n.toLowerCase() === exact.toLowerCase());
|
|
116
|
+
if (listing.includes(exact)) files.push(exact);
|
|
117
|
+
else if (variant && lstatSync(join(root, exact), { throwIfNoEntry: false })) files.push(variant);
|
|
118
|
+
else if (variant) conflicts.push(variant);
|
|
119
|
+
}
|
|
120
|
+
if (conflicts.length > 0) return { conflicts };
|
|
121
|
+
if (files.length === 0 && runtime) files.push(runtime === "codex" ? "AGENTS.md" : "CLAUDE.md");
|
|
122
|
+
return { files };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Each managed folder: there (a folder, or a link to one), blocked (something else at the name), wanted.
|
|
126
|
+
function folderStates(root, wanted) {
|
|
127
|
+
return FOLDERS.map((name) => {
|
|
128
|
+
const path = join(root, name);
|
|
129
|
+
const st = statSync(path, { throwIfNoEntry: false });
|
|
130
|
+
const blocked = st ? !st.isDirectory() : lstatSync(path, { throwIfNoEntry: false }) !== undefined;
|
|
131
|
+
return { name, exists: st?.isDirectory() === true, blocked, wanted: wanted.includes(name) };
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function overrideWarning(root) {
|
|
136
|
+
const path = join(root, "AGENTS.override.md");
|
|
137
|
+
const st = lstatSync(path, { throwIfNoEntry: false });
|
|
138
|
+
if (!st) return null;
|
|
139
|
+
const why = "Codex 는 이 폴더의 AGENTS.md 대신 AGENTS.override.md 를 읽을 수 있음. init-dir 은 override 에 쓰지 않음";
|
|
140
|
+
if (!st.isFile()) return `경고: AGENTS.override.md 가 일반 파일이 아님(내용 확인 안 함). ${why}`;
|
|
141
|
+
if (st.size > 32 * 1024 * 1024) return `경고: AGENTS.override.md 가 32MiB 보다 큼(내용 확인 안 함). ${why}`;
|
|
142
|
+
return /\S/.test(readFileSync(path, "utf8")) ? `경고: AGENTS.override.md 에 내용이 있음. ${why}` : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const LEFTOVER_RE = new RegExp(`^\\.(?:${FOLDERS.join("|")})\\.init-dir-[0-9a-f]+$`);
|
|
146
|
+
|
|
147
|
+
// The temp folders (.<name>.init-dir-<random>) a killed run left in root, one warning line each.
|
|
148
|
+
function leftoverWarnings(root) {
|
|
149
|
+
return readdirSync(root, { withFileTypes: true })
|
|
150
|
+
.filter((e) => e.isDirectory() && LEFTOVER_RE.test(e.name))
|
|
151
|
+
.map((e) => `남은 임시 폴더: ${join(root, e.name)} (지워도 됨)`)
|
|
152
|
+
.sort();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const blockEntries = (root, files, present, follow) =>
|
|
156
|
+
files.map((name) => ({
|
|
157
|
+
path: join(root, name),
|
|
158
|
+
name,
|
|
159
|
+
comment: HTML,
|
|
160
|
+
id: "banker:init-dir",
|
|
161
|
+
body: blockBody(present),
|
|
162
|
+
skill: "init-dir",
|
|
163
|
+
wrap: name.toLowerCase() === "agents.md" ? POLICY : null,
|
|
164
|
+
followSymlink: follow,
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
function instructionStatus(r) {
|
|
168
|
+
if (r.sameAs) return "same-file";
|
|
169
|
+
if (!r.changed) return "unchanged";
|
|
170
|
+
return r.created || r.before === null ? "created" : "updated";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Everything decided before anything is written: a refusal here leaves the disk as it was.
|
|
174
|
+
function prepare(o) {
|
|
175
|
+
const at = checkRoot(o.root);
|
|
176
|
+
if (at.error) return at;
|
|
177
|
+
const { root } = at;
|
|
178
|
+
const inst = instructionFiles(root, o.runtime);
|
|
179
|
+
if (inst.conflicts) return fail(3, `경고: 대소문자가 다른 지시 파일만 있음: ${inst.conflicts.join(", ")}. 이 파일 시스템은 대소문자를 구분하므로 AGENTS.md, CLAUDE.md 와 다른 파일임. 아무 파일도 만들지 않음. 이름을 바꾼 뒤 다시 실행`);
|
|
180
|
+
const wanted = o.only === null ? FOLDERS : o.only;
|
|
181
|
+
const folders = folderStates(root, wanted);
|
|
182
|
+
const blocked = folders.filter((f) => f.wanted && f.blocked).map((f) => f.name);
|
|
183
|
+
if (blocked.length > 0) return fail(1, `폴더 자리에 폴더가 아닌 것이 있어 거부: ${blocked.join(", ")}. 아무것도 바꾸지 않음`);
|
|
184
|
+
const present = folders.filter((f) => f.exists || f.wanted).map((f) => f.name);
|
|
185
|
+
const entries = blockEntries(root, inst.files, present, o.follow);
|
|
186
|
+
const plan = applyBlocks(entries, { dryRun: true });
|
|
187
|
+
if (!plan.ok) return fail(1, `지시 파일 거부: ${plan.refused.map((r) => r.message).join("; ")}. 아무것도 바꾸지 않음`);
|
|
188
|
+
return { root, folders, present, entries, plan, warnings: [overrideWarning(root)].filter(Boolean) };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// One new folder with every template, or none of it. The templates go into root/.<name>.init-dir-<random> (same
|
|
192
|
+
// parent, so same volume) and that folder is renamed to root/<name>. Rename of a folder whose target is absent
|
|
193
|
+
// works on every OS: on Windows libuv calls MoveFileExW(MOVEFILE_REPLACE_EXISTING), which moves a directory when
|
|
194
|
+
// the target does not exist and fails with EPERM when a directory is there (directories are never replaced). On
|
|
195
|
+
// any failure the temp folder is removed and the error rethrown. Returns the rel paths written, or null when a
|
|
196
|
+
// folder appeared at the name meanwhile (then it is left alone).
|
|
197
|
+
function makeFolder(root, name) {
|
|
198
|
+
const tmp = join(root, `.${name}.init-dir-${randomBytes(6).toString("hex")}`);
|
|
199
|
+
mkdirSync(tmp);
|
|
200
|
+
try {
|
|
201
|
+
const files = writeTemplates(root, name, tmp);
|
|
202
|
+
renameSync(tmp, join(root, name));
|
|
203
|
+
return files;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
206
|
+
if (statSync(join(root, name), { throwIfNoEntry: false })?.isDirectory()) return null;
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Makes the wanted folders that are not there, each with its templates.
|
|
212
|
+
function makeFolders(root, folders) {
|
|
213
|
+
return folders
|
|
214
|
+
.filter((f) => f.exists || f.wanted)
|
|
215
|
+
.map((f) => {
|
|
216
|
+
const files = f.exists ? null : makeFolder(root, f.name);
|
|
217
|
+
return files ? { name: f.name, status: "created", files } : { name: f.name, status: "exists", files: [] };
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function plannedFolders(folders) {
|
|
222
|
+
return folders.filter((f) => f.exists || f.wanted).map((f) => ({ name: f.name, status: f.exists ? "exists" : "planned", files: [] }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function apply(o) {
|
|
226
|
+
const p = prepare(o);
|
|
227
|
+
if (p.error) return p;
|
|
228
|
+
const folders = o.dryRun ? plannedFolders(p.folders) : makeFolders(p.root, p.folders);
|
|
229
|
+
const done = o.dryRun ? p.plan : applyBlocks(p.entries);
|
|
230
|
+
if (!done.ok) return fail(1, `지시 파일 거부: ${done.refused.map((r) => r.message).join("; ")}. 폴더는 만들었음`);
|
|
231
|
+
const instructions = done.results.map((r, i) => ({ file: p.entries[i].name, status: instructionStatus(r) }));
|
|
232
|
+
const warnings = [...p.warnings, ...leftoverWarnings(p.root)];
|
|
233
|
+
return { code: 0, report: { ok: true, command: "apply", root: p.root, runtime: o.runtime, dryRun: o.dryRun, folders, instructions, warnings, present: p.present } };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function status(o) {
|
|
237
|
+
const at = checkRoot(o.root);
|
|
238
|
+
if (at.error) return at;
|
|
239
|
+
const inst = instructionFiles(at.root, null);
|
|
240
|
+
const conflict = inst.conflicts ? `경고: 대소문자가 다른 지시 파일만 있음: ${inst.conflicts.join(", ")}` : null;
|
|
241
|
+
const warnings = [overrideWarning(at.root), conflict, ...leftoverWarnings(at.root)].filter(Boolean);
|
|
242
|
+
const present = folderStates(at.root, []).filter((f) => f.exists).map((f) => f.name);
|
|
243
|
+
const folders = FOLDERS.map((name) => ({ name, status: present.includes(name) ? "exists" : "missing", files: [] }));
|
|
244
|
+
const instructions = (inst.files ?? []).map((name) => {
|
|
245
|
+
const r = applyBlocks(blockEntries(at.root, [name], present, o.follow), { dryRun: true }).results[0];
|
|
246
|
+
return { file: name, status: r.ok ? (r.changed ? "stale" : "current") : `refused:${r.refused}` };
|
|
247
|
+
});
|
|
248
|
+
return { code: 0, report: { ok: true, command: "status", root: at.root, folders, instructions, warnings, present } };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const FOLDER_WORDS = { created: "만듦", exists: "있음(목적만 전달)", planned: "만들 예정", missing: "없음" };
|
|
252
|
+
const INSTRUCTION_WORDS = { created: "블록 씀(새로 만듦)", updated: "블록 씀", unchanged: "변경 없음", "same-file": "같은 파일이라 한 번만 씀", current: "블록 최신", stale: "블록 없음 또는 낡음(apply 실행)" };
|
|
253
|
+
|
|
254
|
+
function textLines(rep) {
|
|
255
|
+
const head = `init-dir ${rep.command}${rep.dryRun ? " (dry-run, 바꾸지 않음)" : ""}: ${rep.root}`;
|
|
256
|
+
const rows = [];
|
|
257
|
+
for (const f of rep.folders) {
|
|
258
|
+
rows.push(`| \`${f.name}/\` | ${rep.command === "status" && f.status === "exists" ? "있음" : FOLDER_WORDS[f.status]} |`);
|
|
259
|
+
for (const file of f.files) rows.push(`| \`${file}\` | 만듦 |`);
|
|
260
|
+
}
|
|
261
|
+
for (const i of rep.instructions) rows.push(`| \`${i.file}\` | ${INSTRUCTION_WORDS[i.status] ?? `거부(${i.status})`} |`);
|
|
262
|
+
const summary = summaryLines(rep.present);
|
|
263
|
+
return [head, "", "| 대상 | 결과 |", "|---|---|", ...rows, "", ...rep.warnings, ...(rep.warnings.length ? [""] : []), "폴더 목적 요약", ...(summary.length ? summary : ["- (관리 폴더 없음)"])];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function jsonOf(rep) {
|
|
267
|
+
const summary = FOLDERS.filter((f) => rep.present.includes(f)).map((f) => ({ folder: f, purpose: PURPOSES[f] }));
|
|
268
|
+
const { present, ...rest } = rep;
|
|
269
|
+
return JSON.stringify({ ...rest, summary });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The options checked: { error } for a usage error, --only turned into folder names.
|
|
273
|
+
function options(argv) {
|
|
274
|
+
const o = parseArgs(argv);
|
|
275
|
+
if (o.error || o.help) return o;
|
|
276
|
+
const bad = validate(o);
|
|
277
|
+
if (bad) return { ...o, error: bad };
|
|
278
|
+
if (o.only === null) return o;
|
|
279
|
+
const only = normalizeOnly(o.only);
|
|
280
|
+
return only.error ? { ...o, error: only.error } : { ...o, only: only.folders };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Runs init-dir with argv; returns { code, stdout, stderr } and writes nothing to the console. */
|
|
284
|
+
export function run(argv) {
|
|
285
|
+
const o = options(argv);
|
|
286
|
+
if (o.help) return { code: 0, stdout: USAGE, stderr: "" };
|
|
287
|
+
if (o.error) return finish(o, fail(2, `${o.error}\n${USAGE}`));
|
|
288
|
+
try {
|
|
289
|
+
return finish(o, o.cmd === "status" ? status(o) : apply(o));
|
|
290
|
+
} catch (e) {
|
|
291
|
+
return finish(o, fail(1, `쓰기 실패: ${e.message}`));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function finish(o, r) {
|
|
296
|
+
if (r.error) {
|
|
297
|
+
const out = o.json ? JSON.stringify({ ok: false, exit: r.code, error: r.error.split("\n")[0] }) : "";
|
|
298
|
+
return { code: r.code, stdout: out, stderr: `init-dir: ${r.error}` };
|
|
299
|
+
}
|
|
300
|
+
return { code: r.code, stdout: o.json ? jsonOf(r.report) : textLines(r.report).join("\n"), stderr: "" };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(real(process.argv[1])).href) {
|
|
304
|
+
const r = run(process.argv.slice(2));
|
|
305
|
+
if (r.stdout) process.stdout.write(`${r.stdout}\n`);
|
|
306
|
+
if (r.stderr) process.stderr.write(`${r.stderr}\n`);
|
|
307
|
+
process.exitCode = r.code;
|
|
308
|
+
}
|