@kaydash9999/banker-plugins 0.24.1 → 0.25.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 +32 -0
- package/README.md +5 -4
- package/bin/lib/codex-hooks.mjs +5 -1
- package/codex/hooks.json +4 -4
- package/codex/manifest.json +1 -0
- package/codex/skills/teamwork/scripts/teamwork.mjs +18 -8
- package/codex/transform-matrix.md +1 -0
- package/hooks/telemetry-count-codex.mjs +3 -2
- package/package.json +1 -1
- package/skills/anti-drm/SKILL.md +2 -2
- package/skills/anti-drm/scripts/anti-drm.mjs +8 -5
- package/skills/anti-drm/scripts/instruction-toggle.mjs +6 -3
- package/skills/build-testbox/scripts/browser-cell.mjs +10 -2
- package/skills/build-testbox/scripts/containers.mjs +8 -3
- package/skills/build-testbox/scripts/proc.mjs +32 -8
- package/skills/build-testbox/scripts/testbox.mjs +10 -2
- package/skills/git-init/scripts/git-init.mjs +53 -5
- package/skills/git-init/scripts/marker-block.mjs +131 -19
- package/skills/init-dir/scripts/init-dir.mjs +29 -2
- package/skills/init-dir/scripts/marker-block.mjs +131 -19
- package/skills/init-dir/templates/scripts/lib/service.mjs +57 -6
- package/skills/lineage/SKILL.md +1 -1
- package/skills/lineage/lineage.py +11 -11
- package/skills/motion-sites/SKILL.md +79 -0
- package/skills/motion-sites/scripts/motion-sites.mjs +331 -0
- package/skills/omc-patch/scripts/omc-patch.mjs +2 -1
- package/skills/payload-mon/scripts/payload-mon.mjs +2 -1
- package/skills/payload-mon/scripts/payload-size.mjs +2 -2
- package/skills/progress-mon/scripts/pm-adr.mjs +6 -3
- package/skills/progress-mon/scripts/pm-config.mjs +133 -7
- package/skills/progress-mon/scripts/pm-data.mjs +35 -29
- package/skills/progress-mon/scripts/pm-memory.mjs +14 -7
- package/skills/progress-mon/scripts/pm-server.mjs +3 -3
- package/skills/progress-mon/scripts/pm-sources.mjs +2 -1
- package/skills/remains/SKILL.md +30 -3
- package/skills/remains/scripts/boxes.mjs +14 -4
- package/skills/setup-bypass-permissions/scripts/bypass-permissions.mjs +2 -1
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +12 -7
- package/skills/setup-omc-hud/assets/apply-hud.mjs +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +12 -7
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +2 -1
- package/skills/smart-compact/scripts/codex-gate.mjs +25 -4
- package/skills/tone-compact/scripts/instruction-toggle.mjs +6 -3
- package/skills/tune-tmux/scripts/marker-block.mjs +131 -19
- package/skills/tune-tmux/scripts/tmux-config.mjs +35 -3
- package/skills/tune-tmux/scripts/tune-tmux.mjs +15 -9
- package/skills/ultra-chain/references/chain-ledger.mjs +8 -1
- package/skills/vertical-pptx/verify_a4p.mjs +11 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Claude Code plugin: QA/audit/docs/architecture skills + commands, a /banker:setup installer for OS dependencies (playwright, etc.), and wiki compaction.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "smallOpenSource",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.0] - 2026-11-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- 새 스킬 `motion-sites`: MotionSites 의 `motionsites-web-builder` 스킬(https://github.com/Motionsites-AI/claude-skill)을 짧은 이름으로 부른다. 원본은 banker 에 복사하지 않는다(원본의 예시와 미디어는 재배포 금지). 첫 실행 때 사용자의 Claude Code 나 Codex 가 MotionSites 마켓플레이스에서 원본 플러그인을 설치하고, `pip install playwright` 와 `python -m playwright install chromium` 으로 스크린숏 검사를 갖춘 뒤 프롬프트를 그대로 넘긴다. 그다음부터는 넘기기만 한다.
|
|
7
|
+
- 받기 전에 원천(github.com, PyPI, Playwright 내려받기 서버, 환경 변수나 git, pip 설정의 미러)에 닿는지 본다. 닿지 않으면 "오프라인이라 설치하지 않음" 으로 끝난다.
|
|
8
|
+
- pip 이 시스템 Python 을 거부하면(externally managed) `~/.cache/banker/motion-sites/venv` 에 설치한다.
|
|
9
|
+
- 원본의 유료 상품 안내 블록은 넣지 않고 결과 끝에 출처 한 줄을 남긴다.
|
|
10
|
+
- `remains`: "같은 종류 전체 찾기" 단계(2-1). 지적마다 원인을 한 문장으로 정하고, 같은 원인의 코드 꼴을 배포 코드 전체(사본 포함)에서 찾아 자리마다 판정한다. CHANGELOG, 위키, 메모리에 기록된 예전에 고친 결함도 종류로 삼는다. 지적 하나를 그 자리에서만 고쳐 다음 검토에서 같은 종류가 또 나오던 것을 막는다.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Windows 에서 추측할 수 있는 이름에 새 파일을 만들 때(`wx`, `O_EXCL`), 그 자리에 심어 둔 끊어진 링크를 따라가 프로젝트 밖에 파일을 만들던 자리를 전부 고친다. 임시 이름은 난수를 붙이고, 고정 이름(잠금, 정리 잠금, 백업, 처음 만드는 설정)은 열기 전후로 이름을 확인한다.
|
|
14
|
+
- 고친 스킬: `init-dir` 틀의 `service.mjs`, `progress-mon`, `smart-compact`, `setup-bypass-permissions`, `setup-omc-hud`, `payload-mon`, `omc-patch`, `vertical-pptx`, `lineage`, 사용량 기록 훅
|
|
15
|
+
- Codex 설정 백업(`config.toml` 의 `.bak`)은 백업 자리의 링크를 모든 OS 에서 따라가 덮어쓰던 것도 함께 고친다.
|
|
16
|
+
- 대소문자, Windows 8.3 짧은 이름, 링크인 부모 폴더, macOS `/var` 처럼 철자만 다른 같은 폴더를 다른 폴더로 보던 판정을 고친다.
|
|
17
|
+
- `init-dir`, `git-init`: `--root` 를 HOME 의 다른 철자(예: `c:\users\me`)로 주면 HOME 거부를 통과하던 것
|
|
18
|
+
- 훅 명령을 쓸 때 임시 폴더의 node 를 거부하는 판정(`smart-compact`, Codex 훅 설치)
|
|
19
|
+
- `progress-mon`: 자료 폴더 링크 검사, git 제외 목록, Codex 지시 합계
|
|
20
|
+
- `tune-tmux`: 같은 설정 파일을 다른 파일로 보아 내던 거짓 경고
|
|
21
|
+
- 표지 블록 모듈(`init-dir`, `git-init`, `tune-tmux`): 아직 없는 파일을 두 철자로 받으면 예외로 끝나던 것
|
|
22
|
+
- Windows 에서 이름만 준 프로그램(`git`, `ssh`, `scp`, `psmux`, `claude`, `codex`, `python`)을 일하는 폴더에서 먼저 찾아, 그 폴더에 심은 같은 이름의 프로그램이 실행되던 자리를 고친다. 이제 홈 폴더에서 띄우고 대상 폴더는 `git -C` 로 준다(`git-init`, `progress-mon`, `ultra-chain`, `remains`, `build-testbox`, `tune-tmux`, `motion-sites`).
|
|
23
|
+
- `build-testbox`: 시간 제한이 직접 자식만 끝내고 손자가 쥔 파이프를 기다려 멈추던 것. 이제 프로세스 그룹(Windows 는 트리)을 끝내고 곧 돌아온다. 링크가 낀 경로로 부르면 아무것도 하지 않고 종료 0 으로 끝나던 것도 고친다.
|
|
24
|
+
- `git-init`(Windows): `git commit` 이 시간 제한에 걸리면 래퍼만 끝나고 실제 git 이 남아, 실패로 보고한 뒤 커밋이 생길 수 있던 것.
|
|
25
|
+
- 잠금과 백업을 새로 만들 때 연 파일과 이름을 대조한 뒤에만 쓴다. 이름이 사라졌거나 다른 것이면 쓰지 않고, 쓰기가 실패하면 빈 파일을 지운다(빈 백업이 첫 원본처럼 남지 않게). 확인할 수 없을 때(ESTALE 등)는 예전처럼 쓴다.
|
|
26
|
+
- 백업 자리에 링크나 폴더가 있으면 원본을 바꾸지 않고 거부한다(`init-dir`, `git-init`, `tune-tmux` 는 계획 단계에서, `progress-mon` 규칙 줄, `anti-drm` 설정). 전에는 백업을 건너뛰고 원본을 바꿔 첫 원본을 잃었다.
|
|
27
|
+
- `anti-drm`: `anti-drm.json.bak` 자리에 일반 파일이 아닌 것이 있으면 설정을 저장하지 않고 거부한다.
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
- 같은 종류가 다시 들어오지 않게 저장소 시험 `scripts/class-guards.test.mjs` 가 배포 코드에서 pid 나 시각만으로 만든 임시, 잠금 이름을 잡는다.
|
|
31
|
+
- 사용량 기록 훅(`hooks/telemetry-count-codex.mjs`)이 바뀌어 Codex 플러그인 사용자는 다음 세션에서 `/hooks` 신뢰를 다시 묻는 화면을 본다.
|
|
32
|
+
- `lineage` 3.1.2.
|
|
33
|
+
- 실측: 테스트박스 3대에서 d4a1242 를 시험했다(통과/실패/건너뜀). Rocky Linux 8 2856/0/39, macOS 2852/0/43, Windows 2543/2/345. Windows 의 2건은 절대 경로를 다시 푸는 시험의 가정이었고 211ae51 에서 고쳐 그 두 시험 파일이 Windows 에서 56/56 통과했다.
|
|
34
|
+
|
|
3
35
|
## [0.24.1] - 2026-11-02
|
|
4
36
|
|
|
5
37
|
### Fixed
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[빠른 시작](#빠른-시작) · [워크플로 예시](#워크플로-사용-예시) · [구성](#구성) · [설치 상세](#설치-상세-npm--codex) · [설정 변경 지점](#설정-변경-지점-claude-code--codex) · [요구사항](#요구사항) · [업데이트 / 제거](#업데이트--제거) · [업데이트 확인](#업데이트-확인-및-사용량-카운팅) · [오프라인](#오프라인에서-쓰기) · [라이선스 / 서드파티](#라이선스--서드파티)
|
|
10
10
|
|
|
11
11
|
□ banker는 다음을 포함하는 Claude Code 및 Codex CLI 플러그인입니다.
|
|
12
|
-
- QA·보안 감사·문서·아키텍처·위키·미디어 작업과 의존성·개발환경(OS별) 설치를 아우르는 **스킬
|
|
12
|
+
- QA·보안 감사·문서·아키텍처·위키·미디어 작업과 의존성·개발환경(OS별) 설치를 아우르는 **스킬 71개 + 커맨드 2개**(총 73개 구성요소. Codex 는 Claude Code 전용 `ultraqa` 를 뺀 스킬 70개와 Codex 전용 스킬 3개)
|
|
13
13
|
- Claude Code 즉시 명령 `/graceful-pause`, `/progress`
|
|
14
14
|
|
|
15
15
|
□ 설치하면 스킬과 커맨드가 `/banker:<이름>` 네임스페이스로 노출됩니다.
|
|
@@ -180,6 +180,7 @@ Claude Code 2.1.296 은 Claude 5 모델(Opus 5.5 등)에 작업 목록 도구(Ta
|
|
|
180
180
|
| 스킬 | 설명 |
|
|
181
181
|
|---|---|
|
|
182
182
|
| `motion-graphic-make` | 10초 내외 내레이션 없는 무료 모션 그래픽 제작 (hyperframes 위임). 계획 승인, 스냅숏과 렌더 검토 페이지, 독립 검증, 사이트 배치는 선택 |
|
|
183
|
+
| `motion-sites` | MotionSites 의 `motionsites-web-builder` 스킬을 짧은 이름으로 부름. 첫 실행 때 원본 플러그인을 MotionSites 마켓플레이스에서 설치하고 Playwright 와 Chromium 을 갖춘 뒤 프롬프트를 그대로 넘김. 원본은 banker 에 복사하지 않음(예시와 미디어 재배포 금지), 첫 실행에 네트워크 필요 |
|
|
183
184
|
| `3d-intro-build` | 스크롤-스크럽 3D 인트로 사이트 제작 (Azure gpt-image 스틸 + WAN 키 풀 영상, 소진 시 Sora-2 폴백, 유료). 단계별 검토 페이지, 엄격한 CSP 에서 도는 페이지, 실제 브라우저 독립 검증. 영상 모드는 chain, dive 나가기 없음(기본), dive 나가기 있음 가운데 고름. 장면 단위 이동(장면 시작점에 멈춤, Home, End, 장면 점은 바로 이동)은 기본 켬이고 끌 수 있음, 기존 사이트 통합은 선택 |
|
|
184
185
|
|
|
185
186
|
### 스킬: 개발환경 setup (OS별 · 런타임별)
|
|
@@ -231,14 +232,14 @@ banker uninstall # 제거
|
|
|
231
232
|
|
|
232
233
|
- `--scope project` 로 프로젝트 로컬(`./.codex`)에 설치하고, `--dry-run` 으로 미리 볼 수 있습니다.
|
|
233
234
|
- non-root 전용입니다(전역 sudo 설치 시 root 소유 파일을 방지). postinstall이 없으므로 `banker setup` 을 직접 실행합니다.
|
|
234
|
-
- Codex에는 스킬
|
|
235
|
+
- Codex에는 스킬 70개와 Codex 전용 스킬 3개(`graceful-pause`, `progress`, `teamwork`)가 `~/.codex/skills/banker-<name>/` 에, 커맨드 2개가 `~/.codex/prompts/banker-<name>.md` 에 설치됩니다(`codex/manifest.json`). \
|
|
235
236
|
`CODEX_HOME` 을 설정했으면 `~/.codex` 대신 그 폴더에 설치합니다(Codex 가 읽는 곳). \
|
|
236
237
|
`lineage`, `progress`, 사용량 카운팅은 Codex 의 대화 기록(rollout)을 읽습니다. Codex 는 이 형식을 공개 약속으로 내놓지 않아 업데이트로 바뀔 수 있습니다. \
|
|
237
238
|
Codex 를 업데이트한 뒤에는 `banker doctor` 로 형식을 점검합니다. 읽지 못하면 lineage 는 빈 페이지 대신 오류로 멈추고, progress 는 경고 줄을 보이며, 카운팅은 다음 세션 시작 때 경고를 한 번 보입니다. \
|
|
238
239
|
Codex 0.144 이상에는 사용자 프롬프트가 없으므로 커맨드 2개는 스킬로도 설치되어 `$banker-front-qa`, `$banker-setup` 으로 부릅니다. \
|
|
239
240
|
디렉터리명과 일치하도록 프론트매터 `name:` 이 `banker-<name>` 으로 재작성되어 Codex가 `banker-<name>` 으로 인식합니다.
|
|
240
241
|
- Codex 플러그인 설치(`codex plugin add banker@banker-plugins`)는 저장소를 `~/.codex/plugins/cache/banker-plugins/banker/<판>/` 에 두고 `.codex-plugin/plugin.json` 을 읽습니다. \
|
|
241
|
-
스킬
|
|
242
|
+
스킬 70개, Codex 전용 스킬 3개, 커맨드 2개(스킬로)가 `$banker:<name>` 으로 보이고, 업데이트 알림과 사용량 카운팅 훅은 `codex/hooks.json` 에서 옵니다. \
|
|
242
243
|
Codex 는 세션을 시작할 때 마켓플레이스를 스스로 올려 새 판을 설치합니다. 훅 명령 앞에는 훅이 실행하는 파일의 해시가 붙어 있어, 훅 코드가 바뀐 판에서는 `/hooks` 가 다시 신뢰를 묻고 신뢰하기 전에는 훅이 돌지 않습니다. 훅 코드가 그대로인 판은 신뢰도 그대로입니다. 스킬 파일은 신뢰 확인 없이 새 판으로 바뀝니다. \
|
|
243
244
|
obsidizer 훅은 npm 설치와 같이 `obsidizer --enable` 할 때만 사용자 범위에 설치됩니다(셸 호출마다 node 를 띄우므로). \
|
|
244
245
|
훅은 Codex 샌드박스 밖에서 돌므로, PATH 의 node 가 임시 폴더나 작업 폴더 안에 있으면(샌드박스가 바꿀 수 있는 곳) 아무것도 돌리지 않고 그 이유를 세션 시작 알림으로 보입니다. 작업 폴더가 홈 폴더 자체이면 홈 아래의 node(nvm 등)는 그대로 씁니다. 훅 스크립트, 플러그인 폴더, node 가 `config.toml` 의 `[sandbox_workspace_write] writable_roots` 아래(그 항목 맨 위 `.codex` 제외)면 훅은 다른 일을 하지 않고 그 이유를 한 줄로 알립니다. 이 검사는 훅 파일 안에서 하므로, 샌드박스가 이미 그 파일을 고친 뒤에는 막지 못합니다. writable_roots 에 Codex 홈이나 node 가 든 폴더를 넣지 마십시오. \
|
|
@@ -449,7 +450,7 @@ banker setup
|
|
|
449
450
|
- 세션 시작 훅의 업데이트 조회와 체크인은 따로 돌아 세션을 기다리게 하지 않습니다. 실패하면 24시간 동안 다시 시도하지 않고, 아예 시도하지 않게 하려면 `BANKER_NO_UPDATE_CHECK=1` 을 둡니다.
|
|
450
451
|
- 훅은 `HTTPS_PROXY`, `HTTP_PROXY` 를 따르지 않습니다(Node 가 `NODE_USE_ENV_PROXY=1` 일 때만 따름). 프록시를 거쳐야만 밖에 닿는 망에서는 훅의 확인이 실패로 기록됩니다(`banker telemetry status` 의 "마지막 시도").
|
|
451
452
|
- 네트워크가 필요한 스킬과 네트워크 없이 끝나는 모습
|
|
452
|
-
- 내려받는 설치 스킬(`setup-omc-hud`, `setup-bypass-permissions` 를 뺀 `setup-*`, `docs-setup`, `vertical-pptx-setup`, `motion-graphic-setup`, `3d-intro-setup`): 웹 원천(npm, PyPI, GitHub 등)은 먼저 닿는지 보고, 닿지 못하면 받지 않고 "오프라인이라 설치하지 않음" 으로 알립니다. OS 패키지 관리자(dnf, apt, brew, winget)의 단계는 그 관리자의 미러 설정을 따르므로 미리 판정하지 않고 시도합니다. 단 `setup-pwsh` 는 winget 이 유일한 원천이라 그 원천을 먼저 봅니다.
|
|
453
|
+
- 내려받는 설치 스킬(`setup-omc-hud`, `setup-bypass-permissions` 를 뺀 `setup-*`, `docs-setup`, `vertical-pptx-setup`, `motion-graphic-setup`, `3d-intro-setup`, `motion-sites` 의 첫 실행 구성): 웹 원천(npm, PyPI, GitHub 등)은 먼저 닿는지 보고, 닿지 못하면 받지 않고 "오프라인이라 설치하지 않음" 으로 알립니다. OS 패키지 관리자(dnf, apt, brew, winget)의 단계는 그 관리자의 미러 설정을 따르므로 미리 판정하지 않고 시도합니다. 단 `setup-pwsh` 는 winget 이 유일한 원천이라 그 원천을 먼저 봅니다.
|
|
453
454
|
- `update-banker`: 레지스트리에 닿지 못하면 어느 채널도 바꾸지 않고 멈춥니다.
|
|
454
455
|
- `audit-security` 의 의존성 취약점 조회: 결과로 읽을 수 없는 도구는 0건이 아니라 "미커버" 로 적습니다.
|
|
455
456
|
- `deep-research`, `lfm`, 이를 부르는 `ultra-chain` 1단계, `ultra-interview` 와 `rfp-author` 의 조사: 웹 도구가 모두 실패하면 조사하지 못했다고 알립니다. `lfm` 은 모델이 아는 관행으로 표를 채우지 않고 "웹 조사 불가" 로 끝납니다.
|
package/bin/lib/codex-hooks.mjs
CHANGED
|
@@ -119,7 +119,11 @@ export function safeNode(node, { win = process.platform === 'win32', cwd = proce
|
|
|
119
119
|
// absolute home names one (HOME=. would make every folder the home); path.relative compares as the OS does (case on Windows).
|
|
120
120
|
const isHome = P.isAbsolute(home) && P.relative(real(home), real(cwd)) === '';
|
|
121
121
|
const inWork = codex && !isHome;
|
|
122
|
-
|
|
122
|
+
// Both sides as given and as the file system spells them (as hookGuard): node comes from process.execPath, which is
|
|
123
|
+
// the real path (/private/var on macOS, the long name on Windows), while TMPDIR or TEMP may be a link or an 8.3 name.
|
|
124
|
+
const temps = [os.tmpdir(), ...(win ? [] : ['/tmp'])].flatMap((d) => [d, real(d)]);
|
|
125
|
+
const works = [cwd, real(cwd)];
|
|
126
|
+
return !places.some((f) => temps.some((d) => under(f, d)) || (inWork && works.some((d) => under(f, d))));
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
// 실제 경로. 아직 없는 절대 경로는 있는 조상까지만 풀고 나머지를 붙인다(macOS 의 /var 는 /private/var 로 가는 링크라,
|
package/codex/hooks.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": ": banker-hooks-rev
|
|
10
|
-
"commandWindows": "$null = 'banker-hooks-rev
|
|
9
|
+
"command": ": banker-hooks-rev 59265eebca3c; x() { m=$(printf '%s' \"banker hooks skipped: $1\" | command -p tr -d '\\000-\\037\"\\\\'); printf '{\"systemMessage\":\"%s\"}\\n' \"$m\"; exit 0; }; [ -n \"$PLUGIN_ROOT\" ] || x \"PLUGIN_ROOT is not set\"; p=$(pwd -P); [ -n \"$p\" ] && [ \"$p\" != / ] || x \"the session folder is /\"; u=$(cd /tmp 2>/dev/null && pwd -P); t=$(cd \"${TMPDIR:-/tmp}\" 2>/dev/null && pwd -P); h=; case \"$HOME\" in /?*) h=$(cd \"$HOME\" 2>/dev/null && pwd -P);; esac; w() { for z in \"$u\" \"$t\"; do [ -n \"$z\" ] && case \"$1/\" in \"$z\"/*) return 0;; esac; done; [ \"$p\" = \"$h\" ] && return 1; case \"$1/\" in \"$p\"/*) return 0;; esac; return 1; }; pr=$(cd -P \"$PLUGIN_ROOT\" 2>/dev/null && pwd -P) || x \"cannot enter the plugin folder\"; w \"$pr\" && x \"the plugin folder is in a temp folder or the session folder ($pr)\"; A='function dec(s, o, i, c) { o = \"\"; for (i = 1; i <= length(s); i++) { c = substr(s, i, 1); if (c == \"\\\\\" && substr(s, i + 1, 3) ~ /^[0-7][0-7][0-7]$/) { o = o sprintf(\"%c\", substr(s, i + 1, 1) * 64 + substr(s, i + 2, 1) * 8 + substr(s, i + 3, 1)); i += 3 } else o = o c }; return o } mac { i = index($0, \" on \"); j = 0; for (k = length($0); k > i; k--) if (substr($0, k, 2) == \" (\") { j = k; break }; if (!i || !j) next; m = substr($0, i + 4, j - i - 4); t = substr($0, j + 2); sub(/[,)].*/, \"\", t) } !mac { m = dec($2); t = $3 } (t ~ /^(nfs|nfs4|cifs|smbfs|smb3|afpfs|webdav|davfs|autofs|ceph|glusterfs|lustre|9p)$/ || t ~ /^fuse\\./) && m !~ /:/ { printf \"%s:\", m }'; if [ -r '/proc/self/mounts' ]; then M=$(LC_ALL=C command -p awk \"$A\" '/proc/self/mounts' 2>/dev/null); else M=$('/sbin/mount' 2>/dev/null | LC_ALL=C command -p awk -v mac=1 \"$A\" 2>/dev/null); fi; nm() { for z in $M; do case \"$1/\" in \"$z\"/*) return 0;; esac; done; return 1; }; hm() { for z in \"$h\" \"$HOME\"; do case \"$z\" in /?*) case \"$1/\" in \"$z\"/*) return 0;; esac;; esac; done; return 1; }; set -f; o=$IFS; IFS=:; a=; q=; for v in $PATH; do case \"$v\" in /*) ;; *) a=$a${a:+:}$v; continue;; esac; nm \"$v\" && ! hm \"$v\" && continue; a=$a${a:+:}$v; y=$(cd \"$v\" 2>/dev/null && pwd -P) || continue; w \"$y\" || q=$q${q:+:}$v; done; n=; [ -n \"$a\" ] && n=$(PATH=$a; command -v node) || for d in /opt/homebrew/bin /usr/local/bin; do nm \"$d\" && continue; [ -x \"$d/node\" ] && n=$d/node && break; done; IFS=$o; set +f; [ -n \"$n\" ] || x \"node is not on PATH (a Codex started from the desktop may not have the shell's PATH)\"; case \"$n\" in /*) ;; *) x \"node on PATH is not an absolute path ($n)\";; esac; PATH=${q:-/usr/bin:/bin}; export PATH; cd \"$pr\" || x \"cannot enter the plugin folder\"; rp() { realpath \"$1\" 2>/dev/null || readlink -f \"$1\" 2>/dev/null || { f=$1; i=0; while [ -L \"$f\" ]; do i=$((i+1)); [ \"$i\" -le 40 ] || return 1; l=$(readlink \"$f\") || return 1; case \"$l\" in /*) f=$l;; *) f=${f%/*}/$l;; esac; done; [ -e \"$f\" ] || return 1; d=$(cd -P \"${f%/*}/\" 2>/dev/null && pwd -P) || return 1; printf '%s\\n' \"${d%/}/${f##*/}\"; }; }; r=$(rp \"$n\") || x \"cannot resolve the real path of $n\"; k() { case \"$1\" in /*) ;; *) x \"node path is not absolute ($1)\";; esac; for d in \"$u\" \"$t\"; do [ -n \"$d\" ] && case \"$1\" in \"$d\"/*) x \"node is in a temp folder ($1)\";; esac; done; [ \"$p\" = \"$h\" ] || case \"$1\" in \"$p\"/*) x \"node is in the session folder ($1)\";; esac; }; case \"${r##*/}\" in node) k \"$r\"; e=$r;; *) k \"$n\"; k \"$r\"; e=$(\"$n\" -p 'require(\"fs\").realpathSync(process.execPath)' 2>/dev/null) || x \"the node shim $n did not name its node\"; k \"$e\";; esac; \"$e\" \"$pr/hooks/run.cjs\" \"$pr/hooks/update-notify.mjs\" --codex --plugin",
|
|
10
|
+
"commandWindows": "$null = 'banker-hooks-rev 59265eebca3c'; function x($m) { Write-Output (ConvertTo-Json -Compress @{ systemMessage = \"banker hooks skipped: $m\" }); exit 0 }; if (-not $env:PLUGIN_ROOT) { x 'PLUGIN_ROOT is not set' }; function G($p) { try { (Get-Item -LiteralPath $p -Force -ErrorAction Stop).FullName } catch { try { [IO.Path]::GetFullPath($p) } catch { $p } } }; $sb = @{}; foreach ($l in @(& (Join-Path ([Environment]::SystemDirectory) 'subst.exe') 2>$null)) { if ($l -match '^([A-Za-z]:)\\\\: => (.+)$') { $sb[$matches[1].ToUpper()] = $matches[2] } }; function L($p) { if (-not $p) { return '' }; $p = G $p; for ($i = 0; $i -lt 32; $i++) { $d2 = if ($p.Length -ge 2) { $p.Substring(0, 2).ToUpper() } else { '' }; if ($sb.Contains($d2)) { $p = G ($sb[$d2].TrimEnd('\\') + $p.Substring(2)); continue }; $c = $p; $t = $null; while ($c) { $it = Get-Item -LiteralPath $c -Force -ErrorAction SilentlyContinue; if ($it -and $it.LinkType -in 'SymbolicLink', 'Junction' -and @($it.Target)[0]) { $t = [string]@($it.Target)[0]; break }; $c = Split-Path -LiteralPath $c }; if (-not $t) { break }; if (-not [IO.Path]::IsPathRooted($t)) { $t = Join-Path (Split-Path -LiteralPath $c) $t }; $p = G ($t.TrimEnd('\\') + $p.Substring($c.Length)) }; $p.TrimEnd('\\') }; $ds = @($env:TEMP, $env:TMP); if ($env:LOCALAPPDATA) { $ds += \"$env:LOCALAPPDATA\\Temp\" }; $w = L $PWD.Path; if ($w -ne (L $env:USERPROFILE)) { $ds += $w }; $ds = @($ds | Where-Object { $_ } | ForEach-Object { L $_ } | Where-Object { $_ }); function u($f) { $f = L $f; foreach ($d in $ds) { if ($f -eq $d -or $f.StartsWith($d + '\\', 'OrdinalIgnoreCase')) { return $true } }; $false }; function k($f) { if (u $f) { x \"node is in a temp folder or the session folder ($(L $f))\" }; L $f }; $pr = L $env:PLUGIN_ROOT; if (u $pr) { x \"the plugin folder is in a temp folder or the session folder ($pr)\" }; $nd = @(@($env:PATH -split ';') | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('\"')) } | Where-Object { $_ -match '^[A-Za-z]:\\\\' -and ([IO.DriveInfo]::new($_.Substring(0, 1))).DriveType -ne 'Network' }); $n = $null; foreach ($d in $nd) { foreach ($e in 'node.exe', 'node.bat', 'node.cmd') { $f = Join-Path $d $e; if (Test-Path -LiteralPath $f -PathType Leaf) { $n = $f; break } }; if ($n) { break } }; if (-not $n) { x 'node is not on PATH' }; $env:PATH = ($nd | Where-Object { -not (u $_) }) -join ';'; $r = k $n; if (-not (Set-Location -LiteralPath $pr -PassThru -ErrorAction SilentlyContinue)) { x 'cannot enter the plugin folder' }; if ($r -notmatch '\\.exe$') { $e = & $r -p process.execPath 2>$null | Select-Object -Last 1; if (-not $e -or $e -notmatch '\\.exe$' -or -not (Test-Path -LiteralPath $e -PathType Leaf)) { x \"the node shim $r did not name its node\" }; $r = k $e }; & $r \"$pr\\hooks\\run.cjs\" \"$pr\\hooks\\update-notify.mjs\" --codex --plugin",
|
|
11
11
|
"timeout": 5
|
|
12
12
|
}
|
|
13
13
|
]
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"hooks": [
|
|
19
19
|
{
|
|
20
20
|
"type": "command",
|
|
21
|
-
"command": ": banker-hooks-rev
|
|
22
|
-
"commandWindows": "$null = 'banker-hooks-rev
|
|
21
|
+
"command": ": banker-hooks-rev 59265eebca3c; x() { printf '%s\\n' \"banker hooks skipped: $1\" >&2; exit 0; }; [ -n \"$PLUGIN_ROOT\" ] || x \"PLUGIN_ROOT is not set\"; p=$(pwd -P); [ -n \"$p\" ] && [ \"$p\" != / ] || x \"the session folder is /\"; u=$(cd /tmp 2>/dev/null && pwd -P); t=$(cd \"${TMPDIR:-/tmp}\" 2>/dev/null && pwd -P); h=; case \"$HOME\" in /?*) h=$(cd \"$HOME\" 2>/dev/null && pwd -P);; esac; w() { for z in \"$u\" \"$t\"; do [ -n \"$z\" ] && case \"$1/\" in \"$z\"/*) return 0;; esac; done; [ \"$p\" = \"$h\" ] && return 1; case \"$1/\" in \"$p\"/*) return 0;; esac; return 1; }; pr=$(cd -P \"$PLUGIN_ROOT\" 2>/dev/null && pwd -P) || x \"cannot enter the plugin folder\"; w \"$pr\" && x \"the plugin folder is in a temp folder or the session folder ($pr)\"; A='function dec(s, o, i, c) { o = \"\"; for (i = 1; i <= length(s); i++) { c = substr(s, i, 1); if (c == \"\\\\\" && substr(s, i + 1, 3) ~ /^[0-7][0-7][0-7]$/) { o = o sprintf(\"%c\", substr(s, i + 1, 1) * 64 + substr(s, i + 2, 1) * 8 + substr(s, i + 3, 1)); i += 3 } else o = o c }; return o } mac { i = index($0, \" on \"); j = 0; for (k = length($0); k > i; k--) if (substr($0, k, 2) == \" (\") { j = k; break }; if (!i || !j) next; m = substr($0, i + 4, j - i - 4); t = substr($0, j + 2); sub(/[,)].*/, \"\", t) } !mac { m = dec($2); t = $3 } (t ~ /^(nfs|nfs4|cifs|smbfs|smb3|afpfs|webdav|davfs|autofs|ceph|glusterfs|lustre|9p)$/ || t ~ /^fuse\\./) && m !~ /:/ { printf \"%s:\", m }'; if [ -r '/proc/self/mounts' ]; then M=$(LC_ALL=C command -p awk \"$A\" '/proc/self/mounts' 2>/dev/null); else M=$('/sbin/mount' 2>/dev/null | LC_ALL=C command -p awk -v mac=1 \"$A\" 2>/dev/null); fi; nm() { for z in $M; do case \"$1/\" in \"$z\"/*) return 0;; esac; done; return 1; }; hm() { for z in \"$h\" \"$HOME\"; do case \"$z\" in /?*) case \"$1/\" in \"$z\"/*) return 0;; esac;; esac; done; return 1; }; set -f; o=$IFS; IFS=:; a=; q=; for v in $PATH; do case \"$v\" in /*) ;; *) a=$a${a:+:}$v; continue;; esac; nm \"$v\" && ! hm \"$v\" && continue; a=$a${a:+:}$v; y=$(cd \"$v\" 2>/dev/null && pwd -P) || continue; w \"$y\" || q=$q${q:+:}$v; done; n=; [ -n \"$a\" ] && n=$(PATH=$a; command -v node) || for d in /opt/homebrew/bin /usr/local/bin; do nm \"$d\" && continue; [ -x \"$d/node\" ] && n=$d/node && break; done; IFS=$o; set +f; [ -n \"$n\" ] || x \"node is not on PATH (a Codex started from the desktop may not have the shell's PATH)\"; case \"$n\" in /*) ;; *) x \"node on PATH is not an absolute path ($n)\";; esac; PATH=${q:-/usr/bin:/bin}; export PATH; cd \"$pr\" || x \"cannot enter the plugin folder\"; rp() { realpath \"$1\" 2>/dev/null || readlink -f \"$1\" 2>/dev/null || { f=$1; i=0; while [ -L \"$f\" ]; do i=$((i+1)); [ \"$i\" -le 40 ] || return 1; l=$(readlink \"$f\") || return 1; case \"$l\" in /*) f=$l;; *) f=${f%/*}/$l;; esac; done; [ -e \"$f\" ] || return 1; d=$(cd -P \"${f%/*}/\" 2>/dev/null && pwd -P) || return 1; printf '%s\\n' \"${d%/}/${f##*/}\"; }; }; r=$(rp \"$n\") || x \"cannot resolve the real path of $n\"; k() { case \"$1\" in /*) ;; *) x \"node path is not absolute ($1)\";; esac; for d in \"$u\" \"$t\"; do [ -n \"$d\" ] && case \"$1\" in \"$d\"/*) x \"node is in a temp folder ($1)\";; esac; done; [ \"$p\" = \"$h\" ] || case \"$1\" in \"$p\"/*) x \"node is in the session folder ($1)\";; esac; }; case \"${r##*/}\" in node) k \"$r\"; e=$r;; *) k \"$n\"; k \"$r\"; e=$(\"$n\" -p 'require(\"fs\").realpathSync(process.execPath)' 2>/dev/null) || x \"the node shim $n did not name its node\"; k \"$e\";; esac; \"$e\" \"$pr/hooks/run.cjs\" \"$pr/hooks/telemetry-count-codex.mjs\"",
|
|
22
|
+
"commandWindows": "$null = 'banker-hooks-rev 59265eebca3c'; function x($m) { [Console]::Error.WriteLine(\"banker hooks skipped: $m\"); exit 0 }; if (-not $env:PLUGIN_ROOT) { x 'PLUGIN_ROOT is not set' }; function G($p) { try { (Get-Item -LiteralPath $p -Force -ErrorAction Stop).FullName } catch { try { [IO.Path]::GetFullPath($p) } catch { $p } } }; $sb = @{}; foreach ($l in @(& (Join-Path ([Environment]::SystemDirectory) 'subst.exe') 2>$null)) { if ($l -match '^([A-Za-z]:)\\\\: => (.+)$') { $sb[$matches[1].ToUpper()] = $matches[2] } }; function L($p) { if (-not $p) { return '' }; $p = G $p; for ($i = 0; $i -lt 32; $i++) { $d2 = if ($p.Length -ge 2) { $p.Substring(0, 2).ToUpper() } else { '' }; if ($sb.Contains($d2)) { $p = G ($sb[$d2].TrimEnd('\\') + $p.Substring(2)); continue }; $c = $p; $t = $null; while ($c) { $it = Get-Item -LiteralPath $c -Force -ErrorAction SilentlyContinue; if ($it -and $it.LinkType -in 'SymbolicLink', 'Junction' -and @($it.Target)[0]) { $t = [string]@($it.Target)[0]; break }; $c = Split-Path -LiteralPath $c }; if (-not $t) { break }; if (-not [IO.Path]::IsPathRooted($t)) { $t = Join-Path (Split-Path -LiteralPath $c) $t }; $p = G ($t.TrimEnd('\\') + $p.Substring($c.Length)) }; $p.TrimEnd('\\') }; $ds = @($env:TEMP, $env:TMP); if ($env:LOCALAPPDATA) { $ds += \"$env:LOCALAPPDATA\\Temp\" }; $w = L $PWD.Path; if ($w -ne (L $env:USERPROFILE)) { $ds += $w }; $ds = @($ds | Where-Object { $_ } | ForEach-Object { L $_ } | Where-Object { $_ }); function u($f) { $f = L $f; foreach ($d in $ds) { if ($f -eq $d -or $f.StartsWith($d + '\\', 'OrdinalIgnoreCase')) { return $true } }; $false }; function k($f) { if (u $f) { x \"node is in a temp folder or the session folder ($(L $f))\" }; L $f }; $pr = L $env:PLUGIN_ROOT; if (u $pr) { x \"the plugin folder is in a temp folder or the session folder ($pr)\" }; $nd = @(@($env:PATH -split ';') | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_.Trim().Trim('\"')) } | Where-Object { $_ -match '^[A-Za-z]:\\\\' -and ([IO.DriveInfo]::new($_.Substring(0, 1))).DriveType -ne 'Network' }); $n = $null; foreach ($d in $nd) { foreach ($e in 'node.exe', 'node.bat', 'node.cmd') { $f = Join-Path $d $e; if (Test-Path -LiteralPath $f -PathType Leaf) { $n = $f; break } }; if ($n) { break } }; if (-not $n) { x 'node is not on PATH' }; $env:PATH = ($nd | Where-Object { -not (u $_) }) -join ';'; $r = k $n; if (-not (Set-Location -LiteralPath $pr -PassThru -ErrorAction SilentlyContinue)) { x 'cannot enter the plugin folder' }; if ($r -notmatch '\\.exe$') { $e = & $r -p process.execPath 2>$null | Select-Object -Last 1; if (-not $e -or $e -notmatch '\\.exe$' -or -not (Test-Path -LiteralPath $e -PathType Leaf)) { x \"the node shim $r did not name its node\" }; $r = k $e }; & $r \"$pr\\hooks\\run.cjs\" \"$pr\\hooks\\telemetry-count-codex.mjs\"",
|
|
23
23
|
"timeout": 5
|
|
24
24
|
}
|
|
25
25
|
]
|
package/codex/manifest.json
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
{ "name": "cleansing-memory", "type": "skill", "target": "both", "reason": "keeps agent memory files inside their documented thresholds by de-duping to latest, condensing losslessly, and converting append-style records into replace-style", "supportingFiles": [], "caveat": "only MEMORY.md has a documented hard gate (200 lines or 25KB, whichever comes first); CLAUDE.md has NO documented size limit, so asserting a byte cap for it would be fabrication. Codex's 32768-byte project_doc_max_bytes covers project-scope AGENTS.md only, not the global one, and truncates raw bytes before a lossy UTF-8 decode, so cutting to an exact byte count corrupts the boundary character. Codebase-derivable CLAUDE.md trimming is left to Claude's own /doctor." },
|
|
64
64
|
|
|
65
65
|
{ "name": "motion-graphic-setup", "type": "skill", "target": "both", "reason": "installs hyperframes prereqs (Node22+ffmpeg per-OS) then npx hyperframes init/skills update/doctor; ffmpeg never bundled; runtime-agnostic installer", "supportingFiles": [], "caveat": "needs Node+ffmpeg; hyperframes npx-installed (Apache-2.0)" },
|
|
66
|
+
{ "name": "motion-sites", "type": "skill", "target": "both", "reason": "a short name for MotionSites' own motionsites-web-builder skill (Motionsites-AI/claude-skill): the first run installs that plugin from MotionSites' marketplace in the runtime itself (claude plugin marketplace add and install, or codex plugin marketplace add and add), then pip install playwright and python -m playwright install chromium, and hands the user's prompt over unchanged; later runs only hand it over; nothing of the original is copied into banker, since its examples and media may not be redistributed", "supportingFiles": ["scripts/motion-sites.mjs"], "caveat": "needs the network on the first run (the plugin, Playwright and Chromium); pip refusing an externally managed Python puts Playwright in a virtual environment under ~/.cache/banker/motion-sites; on Codex setup asks for sandbox_permissions require_escalated; a plugin installed in this session shows in the skill list from the next session, so until then the original SKILL.md is read and followed; the original's sponsored upgrade block is left out and one line of credit kept" },
|
|
66
67
|
{ "name": "motion-graphic-make", "type": "skill", "target": "both", "reason": "thin wrapper delegating to hyperframes /motion-graphics (free form categories); owns no render pipeline, only the stage gates (plan approval, snapshot and render review, independent check) and the review page and preview server it shares with 3d-intro-build; runtime-agnostic", "supportingFiles": ["references/preview-lib.mjs", "references/serve.mjs", "references/curate.mjs"], "caveat": "needs hyperframes via motion-graphic-setup (npx, Apache-2.0); the three references are byte-identical copies of 3d-intro-build's (scripts/sync-adapter.js)" },
|
|
67
68
|
{ "name": "3d-intro-setup", "type": "skill", "target": "both", "reason": "collects Azure creds (direct or console-sample parse) to untracked env + FREE preflight; installs Node/ffmpeg; runtime-agnostic", "supportingFiles": ["references/azure-adapter.mjs", "references/video-pool.mjs"] },
|
|
68
69
|
{ "name": "3d-intro-build", "type": "skill", "target": "both", "reason": "scroll-scrub 3D intro: gpt-image stills, WAN key pool video with Sora-2 fallback (dive per scene by default, forward-chaining, or the optional hold-and-flight mode), a review page for every paid stage, a page with no inline script or style (strict CSP), scene-by-scene navigation on by default (can be turned off), an optional glass panel, and a Playwright measurement for the independent check; vendored scroll-world engine (MIT) left unmodified; cross-platform Node", "supportingFiles": ["references/azure-adapter.mjs", "references/video-pool.mjs", "references/scrub-engine.js", "references/index-template.html", "references/prompts.md", "references/LICENSE", "references/NOTICE.md", "references/preview-lib.mjs", "references/serve.mjs", "references/assemble.mjs", "references/curate.mjs", "references/intro-fixes.css", "references/panel-glass.css", "references/step-nav.js", "references/step-nav.css", "references/verify-intro.mjs", "references/verify.md", "references/clip-post.mjs", "references/dive-mode.mjs"], "caveat": "the independent check (verify-intro.mjs) needs Node Playwright with Chromium (setup-playwright); the hold-and-flight mode needs WAN keys and is not yet confirmed by a paid smoke" },
|
|
@@ -86,7 +86,11 @@ function err(s) { process.stderr.write(s.endsWith('\n') ? s : s + '\n'); }
|
|
|
86
86
|
// POSIX sh 용 따옴표. 홑따옴표 안에서는 확장이 일어나지 않는다
|
|
87
87
|
function shq(s) { return /^[A-Za-z0-9_./:@%+=-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`; }
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// 실제 경로. native 먼저: macOS 에서 디스크의 대소문자로 돌려줘, 세션 폴더나 writable_roots 를 다른 대소문자로 적어도
|
|
90
|
+
// 같은 폴더로 판정한다(JS 판은 받은 철자를 그대로 둔다).
|
|
91
|
+
function realOr(p) {
|
|
92
|
+
try { return fs.realpathSync.native(p); } catch { try { return fs.realpathSync(p); } catch { return p; } }
|
|
93
|
+
}
|
|
90
94
|
|
|
91
95
|
function isExec(f) { try { fs.accessSync(f, fs.constants.X_OK); return fs.statSync(f).isFile(); } catch { return false; } }
|
|
92
96
|
|
|
@@ -99,12 +103,18 @@ const verOf = (s) => String(s).split('.').map(Number);
|
|
|
99
103
|
|
|
100
104
|
// ---------- 샌드박스가 쓸 수 있는 곳 ----------
|
|
101
105
|
|
|
102
|
-
function under(p, dir) {
|
|
106
|
+
function under(p, dir, fold = (x) => x) {
|
|
103
107
|
if (!dir) return false;
|
|
104
|
-
const r = path.relative(dir, p);
|
|
108
|
+
const r = path.relative(fold(dir), fold(p));
|
|
105
109
|
return r === '' || (r.split(path.sep)[0] !== '..' && !path.isAbsolute(r));
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
// 위험한 곳 판정에만 쓰는 접기: macOS 는 대소문자를 가리지 않는 볼륨이 기본이라, 아직 없어 실제 철자로 못 푼 경로도
|
|
113
|
+
// 같은 폴더로 본다. 대소문자를 가리는 볼륨에서는 다른 폴더를 위험한 곳으로 볼 수 있지만 그쪽은 거부일 뿐이다.
|
|
114
|
+
// 파일 보내기 같은 허용 판정(아래 roots)은 접지 않는다.
|
|
115
|
+
const foldMac = (p) => (process.platform === 'darwin' ? p.toLowerCase() : p);
|
|
116
|
+
const unsafeUnder = (p, dir) => under(p, dir, foldMac);
|
|
117
|
+
|
|
108
118
|
// p 가 샌드박스 안 세션이 쓸 수 있는 곳이면 그 까닭, 아니면 ''. 순수 함수: 경로는 부르는 쪽이 실제 경로로 푼다.
|
|
109
119
|
// 세션 폴더(와 그 git 최상위)가 홈 폴더 자체나 파일 시스템 루트면 그 규칙은 쓰지 않는다
|
|
110
120
|
const NO_CTX = { cwd: '', gitTop: '', tmpRoots: [], writableRoots: [], home: '' };
|
|
@@ -114,10 +124,10 @@ export function unsafePath(p, ctx = {}) {
|
|
|
114
124
|
if (typeof p !== 'string' || !path.isAbsolute(p)) return `절대 경로가 아님(${p})`;
|
|
115
125
|
const c = { ...NO_CTX, ...ctx };
|
|
116
126
|
const hit = [
|
|
117
|
-
[c.tmpRoots.some((d) =>
|
|
118
|
-
[isWork(c.cwd, c.home) &&
|
|
119
|
-
[isWork(c.gitTop, c.home) &&
|
|
120
|
-
[c.writableRoots.some((d) =>
|
|
127
|
+
[c.tmpRoots.some((d) => unsafeUnder(p, d)), '임시 폴더 아래'],
|
|
128
|
+
[isWork(c.cwd, c.home) && unsafeUnder(p, c.cwd), '세션 폴더 아래'],
|
|
129
|
+
[isWork(c.gitTop, c.home) && unsafeUnder(p, c.gitTop), '세션 폴더의 git 저장소 아래'],
|
|
130
|
+
[c.writableRoots.some((d) => unsafeUnder(p, d)), '샌드박스 쓰기 허용 폴더(writable_roots) 아래'],
|
|
121
131
|
].find(([yes]) => yes);
|
|
122
132
|
return hit ? `${hit[1]}(${p})` : '';
|
|
123
133
|
}
|
|
@@ -922,7 +932,7 @@ function readChecked(real, roots) {
|
|
|
922
932
|
// 래퍼는 샌드박스 밖에서 돈다. 이 세션의 작업 폴더와 임시 폴더 안의 파일만 보낸다
|
|
923
933
|
function readFileBody(file) {
|
|
924
934
|
let real;
|
|
925
|
-
try { real = fs.realpathSync(path.resolve(file)); } catch (e) { usageExit(`파일을 읽지 못했습니다: ${e.message}`); }
|
|
935
|
+
try { real = fs.realpathSync.native(path.resolve(file)); } catch (e) { usageExit(`파일을 읽지 못했습니다: ${e.message}`); }
|
|
926
936
|
const base = sessionCwd();
|
|
927
937
|
const roots = [base, os.tmpdir()].filter(Boolean).map(realOr);
|
|
928
938
|
if (!roots.some((r) => under(real, r))) {
|
|
@@ -52,6 +52,7 @@ The generator (`banker setup --codex`) applies the rules below for `target: both
|
|
|
52
52
|
- `theme-selection` — the same procedure in both runtimes; the engine is `scripts/themes.mjs` (Codex: `~/.codex/skills/banker-theme-selection/scripts/themes.mjs`), which reads `assets/palettes.json` beside it. Pure Node with no network; it writes only the file `--out` names (refused when that file itself is a symbolic link), and the skill asks before it overwrites one that exists.
|
|
53
53
|
- `progress-mon` — the same procedure in both runtimes; the engine is `scripts/progress-mon.mjs` (Codex npm install: `~/.codex/skills/banker-progress-mon/scripts/progress-mon.mjs`; plugin install: `~/.codex/plugins/cache/<marketplace>/banker/<version>/skills/progress-mon/scripts/progress-mon.mjs`), every command with `--runtime claude|codex`. The data folder is `.omc/progress-mon/` for Claude Code and `.omx/progress-mon/` for Codex. Pure Node with no network; the server binds 127.0.0.1 by default (`--host` takes a private IPv4 only). On Codex `start`, `stop`, `status` and `init` each ask for `sandbox_permissions: "require_escalated"` (`init` writes the git exclude list), and since closing the session may stop the server, a long-lived screen runs from the user's terminal (`serve`). `init --memory agents-md` writes its rule to `AGENTS.override.md` when that file has content of its own, as Codex then reads it instead of `AGENTS.md`. The fifth tab, ADR, reads `adr.json` only (the agent cites `docs/adr/` and `docs/evidence/` from it); a project set up before the ADR tab gets, on the next bare call and after one confirmation (Claude Code AskUserQuestion, Codex request_user_input), a rerun of `init` with the memory kind from `state.json` (auto-memory passes the folder of its `MEMORY.md` as `--memory-dir`) that replaces the old rule line in place with a `.progress-mon.bak` backup, then the ADR step. Not the same as `/progress` (the session's own task panel).
|
|
54
54
|
- `init-dir` — the same procedure in both runtimes; the engine is `scripts/init-dir.mjs` (Codex npm install: `~/.codex/skills/banker-init-dir/scripts/init-dir.mjs`; plugin install: `~/.codex/plugins/cache/<marketplace>/banker/<version>/skills/init-dir/scripts/init-dir.mjs`), run with `--runtime claude|codex`. It writes one marked block (`<!-- banker:init-dir:start -->` to `<!-- banker:init-dir:end -->`) into whichever of `AGENTS.md` and `CLAUDE.md` exist (Codex with neither: `AGENTS.md`), the `AGENTS.md` block inside its own `USER:OMX:POLICY` pair; `AGENTS.override.md` is never written, only warned about when it has content of its own. The block editor `scripts/marker-block.mjs` is the source of truth for its future copies (`scripts/sync-adapter.js`). Pure Node with no network. On Codex a `--root` outside the workspace, or starting a generated service that must outlive the session, asks for `sandbox_permissions: "require_escalated"`.
|
|
55
|
+
- `motion-sites` — the same procedure in both runtimes; the engine is `scripts/motion-sites.mjs` (Codex npm install: `~/.codex/skills/banker-motion-sites/scripts/motion-sites.mjs`; plugin install: `~/.codex/plugins/cache/<marketplace>/banker/<version>/skills/motion-sites/scripts/motion-sites.mjs`). `status --runtime codex` finds the original plugin through `config.toml` (`[plugins."motionsites-web-builder@motionsites"]`) and `plugins/cache/motionsites/`; `setup --runtime codex` runs `codex plugin marketplace add Motionsites-AI/claude-skill` and `codex plugin add motionsites-web-builder@motionsites` where Claude Code runs `claude plugin marketplace add` and `claude plugin install`. The hand-over is `$motionsites-web-builder:motionsites-web-builder` once the plugin shows in the session, else the original `SKILL.md` read and followed. `setup` needs the network and asks for `sandbox_permissions: "require_escalated"`.
|
|
55
56
|
- `git-init` — the same procedure in both runtimes; the engine is `scripts/git-init.mjs` (Codex npm install: `~/.codex/skills/banker-git-init/scripts/git-init.mjs`; plugin install: `~/.codex/plugins/cache/<marketplace>/banker/<version>/skills/git-init/scripts/git-init.mjs`). `check` (the default) writes nothing; `run` writes the `.gitignore` block (`# banker:git-init:start` to `# banker:git-init:end`, through `scripts/marker-block.mjs`, a byte-identical copy of init-dir's), scans the files to track with `scripts/payload-scan.mjs` (a byte-identical copy of ralph-qa's; both kept by `scripts/sync-adapter.js`) and only then runs `git init`, `git add .` and `git commit -m "init"`. Needs `git`; no network, no remote, no push. On Codex `run` asks for `sandbox_permissions: "require_escalated"`, as the default sandbox may block writes to `.git`.
|
|
56
57
|
- `build-testbox` — the same procedure in both runtimes; the engine is `scripts/testbox.mjs` (Codex npm install: `~/.codex/skills/banker-build-testbox/scripts/testbox.mjs`; plugin install: `~/.codex/plugins/cache/<marketplace>/banker/<version>/skills/build-testbox/scripts/testbox.mjs`) with `detect`, `run` and `setup`; `scripts/browser-cell.mjs` is copied into the Playwright image and runs there alone for `--browser-in-container`. Playwright and docker or podman are optional: a missing one leaves its cells `unverified` with the reason. `run` downloads nothing; `setup` without `--yes` only lists what it would fetch, and the skill confirms with the user (Claude Code `AskUserQuestion`, Codex `request_user_input`) before running it again with `--yes`. macOS runs only as a Tart VM on an Apple Silicon host; other hosts get `unsupported(apple-sla)`. On Codex `detect` and `run` ask for `sandbox_permissions: "require_escalated"` (container engine, headless browsers, copies under the OS temp folder), `setup --yes` and container commands that use the network ask for network access, and the server under test is the user's own on a 127.0.0.1 port; a cell stopped by a permission error is `unverified(sandbox)`.
|
|
57
58
|
- `tune-tmux` — the same procedure in both runtimes; the engine is `scripts/tune-tmux.mjs` (Codex npm install: `~/.codex/skills/banker-tune-tmux/scripts/tune-tmux.mjs`; plugin install: `~/.codex/plugins/cache/<marketplace>/banker/<version>/skills/tune-tmux/scripts/tune-tmux.mjs`) with `status` (the default), `on` and `off`. It writes one marked block (`# banker:tune-tmux:start` to `# banker:tune-tmux:end`) through `scripts/marker-block.mjs`, a byte-identical copy of init-dir's kept by `scripts/sync-adapter.js`, into the user's tmux config (`~/.tmux.conf` or the XDG `tmux.conf`) or psmux config (`~/.psmux.conf` and the rest of psmux's order); psmux gets zero lines today, as `scripts/psmux-caps.json` verifies nothing beyond its defaults. The conditional `#{||:}` is probed on a private `tmux -L` server, never the user's. A symbolic-link config is refused unless `--follow-symlink`. No network. On Codex `on`, `off` and `--reload` ask for `sandbox_permissions: "require_escalated"`, as the config file and the tmux socket are outside the workspace; Claude Code and Codex settings are never changed, and Codex Fullscreen history cannot be scrolled by tmux copy-mode (only the user's `--no-alt-screen` or `tui.alternate_screen = "never"` changes that).
|
|
@@ -253,7 +253,8 @@ function readTurns() {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// 링크나 일반 파일이 아닌 자리에는 쓰지 않는다. 새 파일을 만들어 바꿔 넣는다.
|
|
256
|
+
// 링크나 일반 파일이 아닌 자리에는 쓰지 않는다. 새 파일을 만들어 바꿔 넣는다. 임시 이름에 난수를 붙인다: Windows 의
|
|
257
|
+
// wx 는 끊어진 링크를 따라가 그 너머에 파일을 만들므로, 이름을 미리 알 수 없어야 링크를 심어 둘 수 없다.
|
|
257
258
|
function writeTurns(lines) {
|
|
258
259
|
const file = TURNS_FILE();
|
|
259
260
|
try {
|
|
@@ -261,7 +262,7 @@ function writeTurns(lines) {
|
|
|
261
262
|
} catch (e) {
|
|
262
263
|
if (e?.code !== 'ENOENT') return;
|
|
263
264
|
}
|
|
264
|
-
const tmp = `${file}.${process.pid}.${
|
|
265
|
+
const tmp = `${file}.${process.pid}.${randomHex()}.tmp`;
|
|
265
266
|
try {
|
|
266
267
|
writeFileSync(tmp, `${lines.slice(-KEEP_TURNS).join('\n')}\n`, { flag: 'wx', mode: 0o600 });
|
|
267
268
|
renameSync(tmp, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaydash9999/banker-plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "banker — Claude Code + Codex CLI plugin (QA, audit, docs, architecture, wiki skills). Install via Claude Code marketplace, or `npm i -g @kaydash9999/banker-plugins` then `banker setup`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"banker": "bin/banker.js"
|
package/skills/anti-drm/SKILL.md
CHANGED
|
@@ -217,7 +217,7 @@ argument-hint: "on | off | status | config"
|
|
|
217
217
|
- 설정 자리가 링크면 `status` 는 설정 줄에 링크가 가리키는 곳을 함께 보이고, 그 파일의 틀린 값은 글 대신 형식과 길이만 보임(`(string, 12자)`)
|
|
218
218
|
- 폴더, FIFO, 64KiB 를 넘는 파일, 읽을 수 없는 파일은 덮어쓰지 않음(링크와 하드 링크는 다음 줄)
|
|
219
219
|
- 저장할 때 링크와 하드 링크(이름이 둘 이상인 일반 파일)는 그 이름만 새 파일로 바꿈. 가리키던 파일은 읽지 않고 그대로 둠
|
|
220
|
-
- anti-drm 이 쓰지 않은 파일이 그 자리에 있으면 `anti-drm.json.bak` 에 한 번 남김 (이 계정만 읽기 가능). 링크와 하드 링크는 남기지
|
|
220
|
+
- anti-drm 이 쓰지 않은 파일이 그 자리에 있으면 `anti-drm.json.bak` 에 한 번 남김 (이 계정만 읽기 가능). 링크와 하드 링크는 남기지 않음. `.bak` 자리에 일반 파일이 아닌 것(폴더, 링크)이 있으면 설정을 저장하지 않고 거부
|
|
221
221
|
- 설정 폴더
|
|
222
222
|
- Claude Code: `$CLAUDE_CONFIG_DIR`, 없으면 `~/.claude`
|
|
223
223
|
- Codex: `$CODEX_HOME`, 없으면 `~/.codex`
|
|
@@ -243,7 +243,7 @@ argument-hint: "on | off | status | config"
|
|
|
243
243
|
| 설정이 없습니다 | "질문" 으로 설정을 정해 `on` 에 넘김 |
|
|
244
244
|
| 표준 입력에 설정 줄이 없습니다 | heredoc 이 비었음. "설정 넘기기" 대로 줄을 넣어 다시 실행 |
|
|
245
245
|
| 표준 입력이 64KiB 보다 큽니다 | "답 처리" 대로 정규화한 값만 넘김 |
|
|
246
|
-
| 설정 파일이 올바르지 않습니다 (뒤에 `--stdin 이나 --deny, --alt 로 함께 주세요` 가 붙음) | 이유를 전하고 설정을 다시 정할지 물은 뒤 진행. 정하면 "질문" 뒤 켜는 중이면 `on --runtime <런타임> --stdin`, 설정만 바꿀 때는 `config --stdin` 실행. 그 파일은 새 파일로 바뀌고(그 자리가 폴더나 특수 파일이거나, 64KiB 보다 크거나, 읽을 수 없는 파일이면 바꾸지 않고 거부함. 링크와 하드 링크(이름이 둘 이상인 일반 파일)는 거부하지 않고 그 이름만 일반 파일로 바꾸며, 가리키던 파일은 그대로 두고 `.bak` 에 남기지 않음), anti-drm 형식이 아니면 `anti-drm.json.bak` 에 한 번 남음(먼저 남긴
|
|
246
|
+
| 설정 파일이 올바르지 않습니다 (뒤에 `--stdin 이나 --deny, --alt 로 함께 주세요` 가 붙음) | 이유를 전하고 설정을 다시 정할지 물은 뒤 진행. 정하면 "질문" 뒤 켜는 중이면 `on --runtime <런타임> --stdin`, 설정만 바꿀 때는 `config --stdin` 실행. 그 파일은 새 파일로 바뀌고(그 자리가 폴더나 특수 파일이거나, 64KiB 보다 크거나, 읽을 수 없는 파일이면 바꾸지 않고 거부함. 링크와 하드 링크(이름이 둘 이상인 일반 파일)는 거부하지 않고 그 이름만 일반 파일로 바꾸며, 가리키던 파일은 그대로 두고 `.bak` 에 남기지 않음), anti-drm 형식이 아니면 `anti-drm.json.bak` 에 한 번 남음(먼저 남긴 파일이 있으면 남기지 않고 결과에 알림. 그 자리에 일반 파일이 아닌 것이 있으면 원래 파일을 잃지 않게 설정을 저장하지 않고 거부). 에이전트가 그 파일을 직접 지우거나 옮기지 않음 |
|
|
247
247
|
| 설정 파일이 올바르지 않습니다 (뒤에 `anti-drm 은 이 자리를 덮어쓰지 않습니다` 가 붙음) | 설정 자리가 폴더, 특수 파일, 64KiB 보다 큰 파일, 읽을 수 없는 파일임(링크면 이 말이 붙지 않고, 설정을 다시 주면 그 이름이 새 파일로 바뀜). 지우지 않음. 무엇인지 보이고, 옮길지 권한을 고칠지(다른 경로는 `BANKER_ANTI_DRM_CONFIG`) 물은 뒤 다시 실행 |
|
|
248
248
|
| 일반 파일이 아니라 설정을 저장하지 않았습니다 | 설정 경로의 폴더나 특수 파일은 지우지 않음. 무엇인지 보이고, 옮길지 다른 경로(`BANKER_ANTI_DRM_CONFIG`)를 쓸지 물은 뒤 다시 실행 |
|
|
249
249
|
| 64KiB 보다 큰 파일이라, 읽을 수 없는 파일이라 설정을 저장하지 않았습니다 | 그 파일이 무엇인지 보이고, 권한을 고칠지 옮길지 물은 뒤 다시 실행 |
|
|
@@ -358,11 +358,13 @@ function priorSettings(file) {
|
|
|
358
358
|
* rename. keep, when given, is a file anti-drm did not write; its bytes go to
|
|
359
359
|
* <file>.bak, readable by this account only, once: a .bak already there is the
|
|
360
360
|
* first such file and stays. Gives back what became of keep: null when none was
|
|
361
|
-
* given, else "kept", "earlier" or "other" (see keepOnce).
|
|
361
|
+
* given, else "kept", "earlier" or "other" (see keepOnce). With "other" the settings are not written: the
|
|
362
|
+
* original would be lost.
|
|
362
363
|
*/
|
|
363
364
|
export function writeConfig(file, config, keep = null) {
|
|
364
365
|
mkdirSync(dirname(file), { recursive: true, mode: 0o755 });
|
|
365
366
|
const kept = keep === null ? null : keepOnce(`${file}.bak`, keep);
|
|
367
|
+
if (kept === "other") return kept; // the original has nowhere to go: the settings are not written over it
|
|
366
368
|
writeFresh(file, `${JSON.stringify(config, null, 2)}\n`, 0o644);
|
|
367
369
|
return kept;
|
|
368
370
|
}
|
|
@@ -472,12 +474,11 @@ export function status(opts = {}) {
|
|
|
472
474
|
|
|
473
475
|
// What a save does beyond writing the settings, as lines to report: a link or a hard link at the settings path becomes
|
|
474
476
|
// a regular file (what it led to stays, and nothing of it goes to .bak), and a file anti-drm did not write goes to
|
|
475
|
-
// .bak, or is not kept when an earlier one
|
|
476
|
-
// writeConfig gave back.
|
|
477
|
+
// .bak, or is not kept when an earlier one is there already (something that is not a regular file there refuses the
|
|
478
|
+
// save before this). kept is what writeConfig gave back.
|
|
477
479
|
const KEPT_NOTES = {
|
|
478
480
|
kept: (bak) => `참고: anti-drm 이 쓰지 않은 원래 파일은 ${bak} 에 남겼습니다.`,
|
|
479
481
|
earlier: (bak) => `참고: anti-drm 이 쓰지 않은 원래 파일은 남기지 않았습니다. 먼저 남긴 파일이 있습니다: ${bak}`,
|
|
480
|
-
other: (bak) => `참고: anti-drm 이 쓰지 않은 원래 파일은 남기지 않았습니다. 그 자리에 일반 파일이 아닌 것이 있습니다: ${bak}`,
|
|
481
482
|
};
|
|
482
483
|
const REPLACED_NOTES = {
|
|
483
484
|
link: LINK_REPLACED,
|
|
@@ -496,7 +497,9 @@ function save(o, config) {
|
|
|
496
497
|
const prior = priorSettings(o.configFile);
|
|
497
498
|
if (prior.refusal) return refuse(`${prior.refusal}: ${where(o.configFile)}`);
|
|
498
499
|
const replaced = pathKind(o.configFile);
|
|
499
|
-
|
|
500
|
+
const kept = writeConfig(o.configFile, config, prior.keep);
|
|
501
|
+
if (kept === "other") return refuse(`원래 파일을 남길 자리에 일반 파일이 아닌 것이 있어 설정을 저장하지 않았습니다: ${where(`${o.configFile}.bak`)}`);
|
|
502
|
+
notes = saveNotes(o.configFile, replaced, kept);
|
|
500
503
|
} catch (e) {
|
|
501
504
|
if (denied(e)) return refuse(`쓸 권한이 없어 설정을 저장하지 못했습니다 (${e.code}): ${where(o.configFile)}`);
|
|
502
505
|
throw e;
|
|
@@ -198,7 +198,8 @@ export function writeFresh(file, data, mode = 0o644) {
|
|
|
198
198
|
// any name that is there, a link to nothing among them (EEXIST). Windows follows a link to nothing and makes the file
|
|
199
199
|
// it leads to: then that file is removed again when it is still the empty one made, and the open is refused with
|
|
200
200
|
// EEXIST as elsewhere. A look before the open catches such a link; this catches one planted between the look and the
|
|
201
|
-
// open. A
|
|
201
|
+
// open. A name gone after the open is EEXIST too. A look that cannot be made (ESTALE, EPERM) leaves the file to the
|
|
202
|
+
// caller, as a plain "wx" would.
|
|
202
203
|
export function openNew(path, mode) {
|
|
203
204
|
const fd = openSync(path, "wx", mode);
|
|
204
205
|
let made = null;
|
|
@@ -206,8 +207,10 @@ export function openNew(path, mode) {
|
|
|
206
207
|
try {
|
|
207
208
|
made = fstatSync(fd, { bigint: true });
|
|
208
209
|
st = lstatSync(path, { bigint: true });
|
|
209
|
-
} catch {
|
|
210
|
-
return fd;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (e?.code !== "ENOENT") return fd; // cannot look (ESTALE, EPERM): the file is the caller's, as a plain "wx" leaves it
|
|
212
|
+
closeSync(fd); // the name is gone: nothing is written through a name not proved
|
|
213
|
+
throw Object.assign(new Error(`EEXIST: file already exists, open '${path}'`), { code: "EEXIST", syscall: "open", path });
|
|
211
214
|
}
|
|
212
215
|
if (!st.isSymbolicLink() && (made.ino === 0n || sameInode(made, st))) return fd;
|
|
213
216
|
closeSync(fd);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// spec: { url, jobs, timeoutMs, settleMs, outDir, cells: [{ id, device, browser, png }] }
|
|
9
9
|
// Prints one line "BANKER_TESTBOX_RESULT <json>": { cells: [...] } or { fatal, detail }.
|
|
10
|
-
import { mkdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
11
11
|
import { join, resolve } from "node:path";
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
13
|
|
|
@@ -173,7 +173,15 @@ async function main(argv) {
|
|
|
173
173
|
return { cells: await runGroup(pw, spec) };
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
// Run directly, also through a linked path or another spelling: both sides as real paths.
|
|
177
|
+
const realOr = (p) => {
|
|
178
|
+
try {
|
|
179
|
+
return realpathSync(p);
|
|
180
|
+
} catch {
|
|
181
|
+
return resolve(p);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const isMain = process.argv[1] && realOr(process.argv[1]) === realOr(fileURLToPath(import.meta.url));
|
|
177
185
|
if (isMain) {
|
|
178
186
|
main(process.argv.slice(2))
|
|
179
187
|
.then((r) => { process.stdout.write(`${RESULT_TAG}${JSON.stringify(r)}\n`); process.exit(0); })
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
// No bind mount (SELinux relabel, Windows paths). Containers are named banker-testbox-<random>-<os> and removed on
|
|
4
4
|
// success, failure, time-out and signal.
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { copyFileSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
6
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
8
8
|
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
9
9
|
import { imageFor } from "./args.mjs";
|
|
10
10
|
import { pool } from "./browser-cell.mjs";
|
|
11
11
|
import { firstLine, runProc, which } from "./proc.mjs";
|
|
12
12
|
import { cellOf } from "./report.mjs";
|
|
13
13
|
|
|
14
|
+
// The folder programs start in: the home folder (Windows looks for a program in the working folder first, so a
|
|
15
|
+
// program planted in the folder worked on never runs), or the temp folder when there is no home folder.
|
|
16
|
+
const runDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
17
|
+
|
|
14
18
|
export const NAME_PREFIX = "banker-testbox";
|
|
15
19
|
const iso = () => new Date().toISOString();
|
|
16
20
|
export const randomTag = () => Math.random().toString(16).slice(2, 10).padEnd(8, "0");
|
|
@@ -66,7 +70,8 @@ export function removeAllSync(engine, names) {
|
|
|
66
70
|
|
|
67
71
|
function gitFiles(src) {
|
|
68
72
|
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_/i.test(k)));
|
|
69
|
-
|
|
73
|
+
// from the home folder: Windows looks for git.exe in the working folder first
|
|
74
|
+
const r = spawnSync("git", ["-C", src, "ls-files", "-z", "-co", "--exclude-standard"], { cwd: runDir(), encoding: "utf8", env, maxBuffer: 256 * 1024 * 1024 });
|
|
70
75
|
return r.status === 0 ? r.stdout.split("\0").filter(Boolean) : null;
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Child processes for build-testbox: PATH lookup (PATHEXT on Windows), a promise-returning runner with a time limit
|
|
2
2
|
// and a set of live children, so a signal handler can stop them all. No shell: arguments go to the program as given.
|
|
3
3
|
// On Windows a .cmd or .bat goes through cmd.exe with each argument quoted (Node refuses to spawn them directly).
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { accessSync, constants, statSync } from "node:fs";
|
|
6
|
-
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { delimiter, extname, isAbsolute, join, win32 } from "node:path";
|
|
7
7
|
|
|
8
8
|
const LIVE = new Set();
|
|
9
9
|
const MAX_OUT = 8 * 1024 * 1024;
|
|
@@ -51,6 +51,21 @@ function collector() {
|
|
|
51
51
|
return { parts, add };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Ends a child and everything it started: on POSIX its process group (the child leads one, spawned detached), on
|
|
55
|
+
// Windows the tree through System32's taskkill /T /F (not a taskkill found in the working folder).
|
|
56
|
+
function killTree(child) {
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
const taskkill = win32.join(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe");
|
|
59
|
+
spawnSync(taskkill, ["/T", "/F", "/PID", String(child.pid)], { stdio: "ignore", windowsHide: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
process.kill(-child.pid, "SIGKILL");
|
|
64
|
+
} catch {
|
|
65
|
+
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
// Runs a program and resolves { code, signal, stdout, stderr, out, timedOut, error }. Never rejects: a missing
|
|
55
70
|
// program is { code: null, error: "ENOENT" }. out holds stdout and stderr in arrival order.
|
|
56
71
|
export function runProc(cmd, args = [], opts = {}) {
|
|
@@ -61,13 +76,19 @@ export function runProc(cmd, args = [], opts = {}) {
|
|
|
61
76
|
let timedOut = false;
|
|
62
77
|
let child;
|
|
63
78
|
try {
|
|
64
|
-
|
|
79
|
+
const detached = process.platform !== "win32"; // its own process group, so a time-out ends what it started too
|
|
80
|
+
child = spawn(bin, argv, { cwd: opts.cwd, env: opts.env || process.env, stdio: ["ignore", "pipe", "pipe"], detached, ...extra });
|
|
65
81
|
} catch (e) {
|
|
66
82
|
resolveRun({ code: null, signal: null, ...parts, timedOut, error: e.code || String(e.message) });
|
|
67
83
|
return;
|
|
68
84
|
}
|
|
69
85
|
LIVE.add(child);
|
|
70
|
-
const timer = opts.timeoutMs ? setTimeout(() => { timedOut = true; child
|
|
86
|
+
const timer = opts.timeoutMs ? setTimeout(() => { timedOut = true; killTree(child); }, opts.timeoutMs) : null;
|
|
87
|
+
// After a time-out, a process that left the tree may still hold the pipes: they are closed a second after the exit,
|
|
88
|
+
// so the run ends instead of waiting for it.
|
|
89
|
+
child.on("exit", () => {
|
|
90
|
+
if (timedOut) setTimeout(() => { child.stdout.destroy(); child.stderr.destroy(); }, 1000).unref();
|
|
91
|
+
});
|
|
71
92
|
child.stdout.on("data", add("stdout"));
|
|
72
93
|
child.stderr.on("data", add("stderr"));
|
|
73
94
|
let error = null;
|
|
@@ -80,12 +101,15 @@ export function runProc(cmd, args = [], opts = {}) {
|
|
|
80
101
|
});
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
// Stops every child still running (used by the SIGINT and SIGTERM handlers).
|
|
104
|
+
// Stops every child still running (used by the SIGINT and SIGTERM handlers, and when this process exits). On POSIX a
|
|
105
|
+
// child leads its own process group, so it no longer gets the terminal's signals: an entry point that uses runProc
|
|
106
|
+
// keeps signal handlers that call killAll (testbox.mjs does).
|
|
84
107
|
export function killAll() {
|
|
85
|
-
for (const child of LIVE)
|
|
86
|
-
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
87
|
-
}
|
|
108
|
+
for (const child of LIVE) killTree(child);
|
|
88
109
|
LIVE.clear();
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
export const firstLine = (s) => String(s || "").split(/\r?\n/).map((l) => l.trim()).find(Boolean) || "";
|
|
113
|
+
|
|
114
|
+
// A child group left when this process ends some other way (an uncaught error) is ended too.
|
|
115
|
+
process.on("exit", killAll);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// build-testbox: a test bed each PC builds for itself. Browser matrix (Playwright Chromium and WebKit, desktop and
|
|
3
3
|
// mobile emulation) and container OS (Rocky Linux; macOS only as a Tart VM on Apple Silicon) run in parallel.
|
|
4
4
|
// run downloads nothing; setup downloads only after --yes. Pure Node 18+, no dependency.
|
|
5
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs";
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { UsageError, parseArgs } from "./args.mjs";
|
|
@@ -144,5 +144,13 @@ export async function main(argv) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
// Run directly, also through a linked path or another spelling: both sides as real paths.
|
|
148
|
+
const realOr = (p) => {
|
|
149
|
+
try {
|
|
150
|
+
return realpathSync(p);
|
|
151
|
+
} catch {
|
|
152
|
+
return resolve(p);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const isMain = process.argv[1] && realOr(process.argv[1]) === realOr(fileURLToPath(import.meta.url));
|
|
148
156
|
if (isMain) main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|