@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
|
@@ -34,13 +34,15 @@
|
|
|
34
34
|
* pid and time, removed when it ends). A later start waits for it, then sees the service running. A lock whose pid
|
|
35
35
|
* is dead, is another program, or is older than 30 seconds plus the start check is removed, by one start at a time:
|
|
36
36
|
* the one holding logs/<name>.start.lock.break (O_EXCL), which reads the lock again and removes only the lock it
|
|
37
|
-
* judged stale. A link or a folder at either name is refused (exit 1)
|
|
37
|
+
* judged stale. A link or a folder at either name is refused (exit 1), a link to nothing among them: Windows opens
|
|
38
|
+
* O_EXCL through such a link and makes the file it leads to, so the name is looked at before the open and again after.
|
|
38
39
|
*
|
|
39
40
|
* Environment (tuning): SERVICE_LOG_ROTATE_BYTES (default 104857600), SERVICE_STOP_GRACE_MS (10000),
|
|
40
41
|
* SERVICE_START_CHECK_MS (2000).
|
|
41
42
|
*/
|
|
42
43
|
import { spawn, spawnSync } from "node:child_process";
|
|
43
|
-
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
44
|
+
import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
45
|
+
import { randomBytes } from "node:crypto";
|
|
44
46
|
import { uptime } from "node:os";
|
|
45
47
|
import { dirname, join } from "node:path";
|
|
46
48
|
import { fileURLToPath } from "node:url";
|
|
@@ -105,10 +107,16 @@ function readPid(file) {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// A fresh temp file with a random name (no one can plant a link at it first), renamed over the pid file.
|
|
108
111
|
function writePid(file, value) {
|
|
109
|
-
const tmp = `${file}.${
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
const tmp = `${file}.${randomBytes(6).toString("hex")}.tmp`;
|
|
113
|
+
try {
|
|
114
|
+
writeFileSync(tmp, `${JSON.stringify(value)}\n`, { flag: "wx" });
|
|
115
|
+
renameSync(tmp, file);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
rmSync(tmp, { force: true });
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
// Removes the pid file when it still names this supervisor.
|
|
@@ -356,12 +364,30 @@ function lockBusy(text, st) {
|
|
|
356
364
|
}
|
|
357
365
|
|
|
358
366
|
// One file made with O_EXCL holding this start's pid and time: { text } made, { code } not (EEXIST: there is one).
|
|
367
|
+
// Anything at the name, a link to nothing among them, is EEXIST: Windows opens O_EXCL through a link to nothing and
|
|
368
|
+
// makes the file it leads to (macOS and Linux answer EEXIST), so the name is looked at first, and after the open,
|
|
369
|
+
// before anything is written, the opened file is compared with the name: a name gone, a link or another file is EEXIST
|
|
370
|
+
// (an empty file made through a link is taken back). A look that cannot be made (ESTALE, EPERM) writes as a plain
|
|
371
|
+
// O_EXCL open would. A write that fails takes the empty lock away again.
|
|
359
372
|
function makeLockFile(file) {
|
|
360
373
|
const text = `${JSON.stringify({ pid: process.pid, at: new Date().toISOString() })}\n`;
|
|
361
374
|
try {
|
|
375
|
+
if (lstatSync(file, { throwIfNoEntry: false }) !== undefined) return { code: "EEXIST" };
|
|
362
376
|
const fd = openSync(file, "wx");
|
|
363
377
|
try {
|
|
364
|
-
|
|
378
|
+
let made = null;
|
|
379
|
+
try {
|
|
380
|
+
made = fstatSync(fd, { bigint: true });
|
|
381
|
+
} catch {
|
|
382
|
+
// cannot look: the name is not judged
|
|
383
|
+
}
|
|
384
|
+
if (namesOpened(file, made) === false) return { code: "EEXIST" };
|
|
385
|
+
try {
|
|
386
|
+
writeSync(fd, text);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
if (namesOpened(file, made) === true) rmSync(file, { force: true });
|
|
389
|
+
throw e;
|
|
390
|
+
}
|
|
365
391
|
} finally {
|
|
366
392
|
closeSync(fd);
|
|
367
393
|
}
|
|
@@ -371,6 +397,31 @@ function makeLockFile(file) {
|
|
|
371
397
|
}
|
|
372
398
|
}
|
|
373
399
|
|
|
400
|
+
// Whether the name is the opened file (made): true, false (gone, a link, another file), or null when it cannot be
|
|
401
|
+
// told (a look that fails, no file ids). A link: the empty file made through it is taken back, and false.
|
|
402
|
+
function namesOpened(file, made) {
|
|
403
|
+
let st;
|
|
404
|
+
try {
|
|
405
|
+
st = lstatSync(file, { bigint: true, throwIfNoEntry: false });
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
if (!st) return false;
|
|
410
|
+
if (st.isSymbolicLink()) {
|
|
411
|
+
if (!made) return false;
|
|
412
|
+
try {
|
|
413
|
+
const target = realpathSync(file);
|
|
414
|
+
const at = statSync(target, { bigint: true });
|
|
415
|
+
if (at.dev === made.dev && at.ino === made.ino && at.size === 0n) rmSync(target, { force: true });
|
|
416
|
+
} catch {
|
|
417
|
+
// the link changed again: nothing of ours to find
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
if (!made || made.ino === 0n) return null;
|
|
422
|
+
return st.dev === made.dev && st.ino === made.ino;
|
|
423
|
+
}
|
|
424
|
+
|
|
374
425
|
// lstat that answers undefined for a missing file and null for one busy being deleted (the next try sees).
|
|
375
426
|
function lstatNow(file) {
|
|
376
427
|
try {
|
package/skills/lineage/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: lineage
|
|
3
3
|
description: "현재 세션 대화를 카카오톡 스타일의 단일 HTML 로 내보냄. Claude 답변은 한 줄 요약 아래에 원문을 접어 두고, 펼치면 마크다운으로 렌더해 보여 줌. 기본은 세션 모델이 요약을 쓰고 노이즈 턴을 걸러 내며, `--rulebase` 는 모델 없이 규칙만 씀. 여러 세션을 합칠 수 있음. 'lineage'/'대화 export'/'카톡 스타일 html' 시 사용."
|
|
4
4
|
invocation: /lineage
|
|
5
|
-
version: 3.1.
|
|
5
|
+
version: 3.1.2
|
|
6
6
|
schema_version: 1
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -1590,24 +1590,24 @@ def _stdin_id_key():
|
|
|
1590
1590
|
if key is not None and len(key) == 32:
|
|
1591
1591
|
return key
|
|
1592
1592
|
key = os.urandom(32)
|
|
1593
|
-
tmp =
|
|
1593
|
+
tmp = None
|
|
1594
1594
|
try:
|
|
1595
1595
|
CACHE_BASE.mkdir(parents=True, exist_ok=True)
|
|
1596
1596
|
os.chmod(str(CACHE_BASE), 0o700)
|
|
1597
|
-
#
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
0o600,
|
|
1597
|
+
# mkstemp: a fresh random name made exclusively (0600), so no link can be planted at it
|
|
1598
|
+
# first (Windows opens O_EXCL through a link to nothing and makes the file it leads to)
|
|
1599
|
+
fd, tmp = tempfile.mkstemp(
|
|
1600
|
+
dir=str(CACHE_BASE), prefix=f".{path.name}.", suffix=".tmp"
|
|
1602
1601
|
)
|
|
1603
1602
|
with os.fdopen(fd, "wb") as f:
|
|
1604
1603
|
f.write(key)
|
|
1605
|
-
os.replace(
|
|
1604
|
+
os.replace(tmp, str(path)) # whole or not at all
|
|
1606
1605
|
except OSError:
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1606
|
+
if tmp is not None:
|
|
1607
|
+
try:
|
|
1608
|
+
os.unlink(tmp)
|
|
1609
|
+
except OSError:
|
|
1610
|
+
pass
|
|
1611
1611
|
return key
|
|
1612
1612
|
|
|
1613
1613
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: motion-sites
|
|
3
|
+
description: "MotionSites 의 motionsites-web-builder 스킬을 짧은 이름으로 부름. 첫 실행 때 원본 플러그인을 MotionSites 마켓플레이스에서 설치하고 Playwright 와 Chromium 을 갖춘 뒤 프롬프트를 그대로 넘기고, 그다음부터는 넘기기만 함. 'motion-sites'/'모션 사이트'/'시네마틱 랜딩 페이지' 시 사용."
|
|
4
|
+
argument-hint: "<만들 사이트나 프롬프트>"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# motion-sites - motionsites-web-builder 를 짧은 이름으로
|
|
8
|
+
|
|
9
|
+
MotionSites 의 `motionsites-web-builder` 스킬(https://github.com/Motionsites-AI/claude-skill)을 `/motion-sites` 로 부른다. 원본 이름이 길어서 둔 이름이다.
|
|
10
|
+
banker 는 원본을 복사하지 않는다. 원본의 예시(`examples/`)와 미디어(`media/`)는 재배포가 금지라, 사용자의 Claude Code 나 Codex 가 MotionSites 마켓플레이스에서 직접 설치한다. 답변은 한글.
|
|
11
|
+
|
|
12
|
+
## 엔진 위치
|
|
13
|
+
|
|
14
|
+
`scripts/motion-sites.mjs` 가 상태 확인과 설치를 한다.
|
|
15
|
+
|
|
16
|
+
| 설치 | 경로 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Claude Code 플러그인 | 이 스킬 폴더의 `scripts/motion-sites.mjs` |
|
|
19
|
+
| Codex npm | `~/.codex/skills/banker-motion-sites/scripts/motion-sites.mjs` |
|
|
20
|
+
| Codex 플러그인 | `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/motion-sites/scripts/motion-sites.mjs` |
|
|
21
|
+
|
|
22
|
+
`--runtime` 은 지금 런타임이다(Claude Code 는 `claude`, Codex 는 `codex`).
|
|
23
|
+
|
|
24
|
+
## 1. 상태 확인
|
|
25
|
+
|
|
26
|
+
`node <엔진> status --runtime <claude|codex>` 를 실행한다. 출력은 JSON 이다.
|
|
27
|
+
|
|
28
|
+
| 칸 | 뜻 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `skillFile` | 설치된 원본 `SKILL.md` 경로. 없거나 꺼져 있으면 `null` |
|
|
31
|
+
| `disabled` | 설치는 됐지만 꺼져 있음 |
|
|
32
|
+
| `python.bin` | Playwright 를 쓸 Python |
|
|
33
|
+
| `python.playwright`, `python.chromium` | Playwright 와 Chromium 이 있는지 |
|
|
34
|
+
| `ready` | 셋 다 있으면 `true`(종료 0) |
|
|
35
|
+
|
|
36
|
+
`ready` 가 `true` 면 3단계로 간다.
|
|
37
|
+
|
|
38
|
+
## 2. 첫 실행 구성 (ready 가 아닐 때만)
|
|
39
|
+
|
|
40
|
+
1. 사용자에게 한 줄로 알린다: MotionSites 플러그인을 설치하고 Playwright 와 Chromium 을 받는다.
|
|
41
|
+
2. `node <엔진> setup --runtime <claude|codex>` 를 실행한다. 빠진 단계만 차례로 한다.
|
|
42
|
+
- 꺼져 있는 플러그인은 다시 설치하지 않는다. Claude Code 는 `claude plugin enable` 로 켜고, Codex 는 `config.toml` 에서 켜라고 안내한다.
|
|
43
|
+
- 받기 전에 원천(github.com, PyPI, Playwright 내려받기 서버. 환경 변수의 미러가 있으면 그것)에 닿는지 8초 안에 본다. 닿지 않으면 그 단계는 "오프라인이라 설치하지 않음" 으로 끝난다. 프록시 환경 변수가 있으면 이 판정은 하지 않고 단계의 시간 제한에 맡긴다.
|
|
44
|
+
- Claude Code: `claude plugin marketplace add Motionsites-AI/claude-skill`, `claude plugin install motionsites-web-builder@motionsites`
|
|
45
|
+
- Codex: `codex plugin marketplace add Motionsites-AI/claude-skill`, `codex plugin add motionsites-web-builder@motionsites`
|
|
46
|
+
- 공통: `<python> -m pip install playwright`, `<python> -m playwright install chromium`
|
|
47
|
+
- pip 이 시스템 Python 을 거부하면(externally managed) 캐시 폴더의 가상 환경에 설치한다. 캐시 폴더는 `MOTION_SITES_HOME`, 없으면 `~/.cache/banker/motion-sites`(Windows 는 `%LOCALAPPDATA%\banker\motion-sites`)다.
|
|
48
|
+
- Chromium 내려받기에 몇 분이 걸릴 수 있다. Claude Code 에서는 Bash 도구의 시간 제한보다 길 수 있으므로 백그라운드로 실행한다.
|
|
49
|
+
3. Codex 에서는 이 명령이 네트워크와 `~/.codex` 쓰기를 하므로 `sandbox_permissions: "require_escalated"` 로 승인을 요청한다.
|
|
50
|
+
4. 출력의 `steps` 에 실패한 단계가 있으면 그 단계와 `why` 를 보여 주고 멈춘다. 설치되지 않은 것을 설치된 것처럼 말하지 않는다.
|
|
51
|
+
- 네트워크가 없으면 여기서 멈춘다. 오프라인에서는 이 스킬을 쓸 수 없다.
|
|
52
|
+
- Linux 에서 Chromium 이 시스템 라이브러리 부족으로 뜨지 않으면 `<python.bin> -m playwright install-deps chromium` 을 안내한다. 관리자 권한이 필요하므로 직접 실행하지 않는다.
|
|
53
|
+
5. 끝나면 `ready` 가 `true` 인지 확인하고 3단계로 간다.
|
|
54
|
+
|
|
55
|
+
## 3. 프롬프트 넘기기
|
|
56
|
+
|
|
57
|
+
사용자가 `/motion-sites` 뒤에 쓴 글을 고치지 않고 그대로 원본 스킬에 넘긴다. 글이 없으면 무엇을 만들지 한 번 묻는다.
|
|
58
|
+
|
|
59
|
+
| 런타임 | 원본 스킬이 이 세션에 보일 때 | 보이지 않을 때(방금 설치) |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Claude Code | Skill 도구로 `motionsites-web-builder:motionsites-web-builder` 를 부르고 인자로 그 글을 준다 | `skillFile` 을 읽고 그 지시를 그대로 따른다 |
|
|
62
|
+
| Codex | `$motionsites-web-builder:motionsites-web-builder` 를 쓴다 | `skillFile` 을 읽고 그 지시를 그대로 따른다 |
|
|
63
|
+
|
|
64
|
+
- `skillFile` 을 직접 따를 때 원본의 상대 경로(`examples/`, `references/`, `media/`, `scripts/`)는 `skillFile` 이 있는 폴더 기준이다.
|
|
65
|
+
- 원본이 `python scripts/shoot.py` 를 쓰라고 하면 `python` 대신 1단계의 `python.bin` 을 쓴다.
|
|
66
|
+
- 새로 설치한 플러그인은 Claude Code 에서 `/reload-plugins` 나 다음 세션부터, Codex 에서 다음 세션부터 스킬 목록에 보인다.
|
|
67
|
+
|
|
68
|
+
## 원본을 따를 때 지킬 것
|
|
69
|
+
|
|
70
|
+
- 원본의 "Sponsored by MotionSites" 절(결과물 끝의 유료 상품 안내 블록과 권유 문장)은 넣지 않는다. 사용자가 정한 것이고, 원본도 사용자가 원하지 않으면 멈추라고 한다.
|
|
71
|
+
- 출처는 남긴다. 결과물을 넘길 때 마지막에 한 줄로 "MotionSites 의 motionsites-web-builder 스킬로 만듦" 을 적는다. 사용자의 코드와 파일에는 넣지 않는다.
|
|
72
|
+
- 원본의 예시와 미디어는 사이트를 만드는 데만 쓴다. 프롬프트나 미디어 모음으로 따로 내보내거나 배포하지 않는다(원본 LICENSE 2항).
|
|
73
|
+
|
|
74
|
+
## 갱신과 제거
|
|
75
|
+
|
|
76
|
+
| 런타임 | 갱신 | 제거 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Claude Code | `claude plugin marketplace update motionsites` 뒤 `claude plugin update motionsites-web-builder@motionsites` | `claude plugin uninstall motionsites-web-builder@motionsites` |
|
|
79
|
+
| Codex | `codex plugin marketplace upgrade motionsites` 뒤 `codex plugin add motionsites-web-builder@motionsites` | `codex plugin remove motionsites-web-builder@motionsites` |
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* motion-sites: the first-run setup and the lookup behind /motion-sites, a short name for MotionSites' own
|
|
4
|
+
* motionsites-web-builder skill (https://github.com/Motionsites-AI/claude-skill). Nothing of that skill is copied
|
|
5
|
+
* here: its examples and media may not be redistributed, so the user's own Claude Code or Codex installs it from
|
|
6
|
+
* MotionSites' marketplace, as its README says.
|
|
7
|
+
*
|
|
8
|
+
* status --runtime claude|codex JSON: the plugin, its SKILL.md, a Python with Playwright, Chromium; ready or not
|
|
9
|
+
* setup --runtime claude|codex installs what is missing, in order, then answers as status
|
|
10
|
+
* path --runtime claude|codex the installed SKILL.md (exit 1 when not installed)
|
|
11
|
+
*
|
|
12
|
+
* setup runs, for Claude Code: claude plugin marketplace add Motionsites-AI/claude-skill, then claude plugin install
|
|
13
|
+
* motionsites-web-builder@motionsites; for Codex: codex plugin marketplace add Motionsites-AI/claude-skill, then
|
|
14
|
+
* codex plugin add motionsites-web-builder@motionsites. Then <python> -m pip install playwright and <python> -m
|
|
15
|
+
* playwright install chromium. Where pip refuses the system Python (an externally managed environment), Playwright
|
|
16
|
+
* goes into a virtual environment of its own under the cache folder (MOTION_SITES_HOME, else
|
|
17
|
+
* ~/.cache/banker/motion-sites, %LOCALAPPDATA%\banker\motion-sites on Windows), and status names that Python.
|
|
18
|
+
*
|
|
19
|
+
* Every step has a time limit, so with no network a step fails with its reason instead of waiting. Exit status:
|
|
20
|
+
* 0 ready (status, setup) or found (path), 1 not ready or a step failed, 2 usage.
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
24
|
+
import { homedir, tmpdir } from "node:os";
|
|
25
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
// The folder programs start in: the home folder (Windows looks for a program in the working folder first, so a
|
|
29
|
+
// program planted in the folder worked on never runs), or the temp folder when there is no home folder.
|
|
30
|
+
const runDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
31
|
+
|
|
32
|
+
export const REPO = "Motionsites-AI/claude-skill";
|
|
33
|
+
export const PLUGIN = "motionsites-web-builder";
|
|
34
|
+
export const MARKET = "motionsites";
|
|
35
|
+
const ID = `${PLUGIN}@${MARKET}`;
|
|
36
|
+
const WIN = process.platform === "win32";
|
|
37
|
+
const STEP_MS = 10 * 60 * 1000; // a Chromium download can take minutes
|
|
38
|
+
const LOOK_MS = 60 * 1000;
|
|
39
|
+
|
|
40
|
+
const USAGE = `motion-sites: MotionSites 의 motionsites-web-builder 스킬 설치와 위치 확인
|
|
41
|
+
사용법: node motion-sites.mjs status|setup|path --runtime claude|codex`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runs a program with fixed arguments: { ok, stdout, out (stdout and stderr), why }. Data (JSON, paths) is read from
|
|
45
|
+
* stdout only, so a warning on stderr never spoils it. On Windows claude and codex are .cmd shims, which Node runs only
|
|
46
|
+
* through a shell: they go as one command line (their arguments here are fixed words). Python gets no shell.
|
|
47
|
+
*/
|
|
48
|
+
export function run(cmd, args, { timeout = LOOK_MS, env = process.env } = {}) {
|
|
49
|
+
const shim = WIN && (cmd === "claude" || cmd === "codex");
|
|
50
|
+
// From the home folder: python -c and -m put the working folder first on the import path (a pip/ or playwright/ in
|
|
51
|
+
// the project would run), and Windows looks for a program there first.
|
|
52
|
+
const opts = { cwd: runDir(), encoding: "utf8", timeout, env, windowsHide: true, maxBuffer: 64 * 1024 * 1024 };
|
|
53
|
+
const r = shim ? spawnSync([cmd, ...args].join(" "), { ...opts, shell: true }) : spawnSync(cmd, args, opts);
|
|
54
|
+
const stdout = r.stdout ?? "";
|
|
55
|
+
const out = `${stdout}${r.stderr ?? ""}`;
|
|
56
|
+
if (r.error) return { ok: false, stdout, out, why: r.error.code === "ETIMEDOUT" ? `시간 초과(${timeout / 1000}초)` : `${cmd} 실행 실패(${r.error.code})` };
|
|
57
|
+
return { ok: r.status === 0, stdout, out, why: r.status === 0 ? null : `${cmd} ${args.join(" ")} 종료 ${r.status}: ${lastLine(out)}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PROXY_KEYS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"];
|
|
61
|
+
const PROBE = "const s=require('net').connect({host:process.argv[1],port:443,timeout:8000});s.on('connect',()=>process.exit(0));s.on('timeout',()=>process.exit(1));s.on('error',()=>process.exit(1))";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a web source answers on port 443 within 8 seconds: true, false, or null (not judged: behind a proxy a
|
|
65
|
+
* direct look says nothing, so the step's own time limit decides).
|
|
66
|
+
*/
|
|
67
|
+
export function reachable(host, { env = process.env, runner = run } = {}) {
|
|
68
|
+
if (PROXY_KEYS.some((k) => env[k])) return null;
|
|
69
|
+
return runner(process.execPath, ["-e", PROBE, host], { timeout: 15000 }).ok;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hostOf = (url, fallback) => {
|
|
73
|
+
try {
|
|
74
|
+
return new URL(url).hostname || fallback;
|
|
75
|
+
} catch {
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The hosts each install step downloads from, any one answering being enough (a mirror named in the environment first). */
|
|
81
|
+
export function sources(env = process.env) {
|
|
82
|
+
return {
|
|
83
|
+
plugin: ["github.com"],
|
|
84
|
+
pip: [hostOf(env.PIP_INDEX_URL, "pypi.org")],
|
|
85
|
+
chromium: env.PLAYWRIGHT_DOWNLOAD_HOST ? [hostOf(env.PLAYWRIGHT_DOWNLOAD_HOST, "cdn.playwright.dev")] : ["cdn.playwright.dev", "playwright.download.prss.microsoft.com"],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const GIT_MIRROR = "^(https?\\..*proxy|url\\..*insteadof)$";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether a step's tool is set to a mirror or a proxy in its own configuration, where a direct look at the public
|
|
93
|
+
* host says nothing: git (http.proxy, url.<base>.insteadOf) for the plugin, pip config (index-url) for Playwright.
|
|
94
|
+
*/
|
|
95
|
+
export function mirrorSet(kind, { py = null, runner = run } = {}) {
|
|
96
|
+
if (kind === "plugin") {
|
|
97
|
+
const r = runner("git", ["config", "--get-regexp", GIT_MIRROR]);
|
|
98
|
+
return r.ok && (r.stdout ?? "").trim() !== "";
|
|
99
|
+
}
|
|
100
|
+
if (kind === "pip" && py) {
|
|
101
|
+
const [cmd, ...pre] = py;
|
|
102
|
+
const r = runner(cmd, [...pre, "-m", "pip", "config", "list"]);
|
|
103
|
+
return r.ok && /index-url/i.test(r.stdout ?? "");
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A failed step when none of its sources answers, or null when one answers (or it cannot be judged: a proxy, or a
|
|
109
|
+
// mirror set in the tool's own configuration).
|
|
110
|
+
function offline(step, kind, { env, runner, py = null }) {
|
|
111
|
+
const hosts = sources(env)[kind];
|
|
112
|
+
if (mirrorSet(kind, { py, runner })) return null;
|
|
113
|
+
const seen = hosts.map((h) => reachable(h, { env, runner }));
|
|
114
|
+
if (!seen.every((x) => x === false)) return null;
|
|
115
|
+
return { step, ok: false, why: `오프라인이라 설치하지 않음(${hosts.join(", ")} 에 닿지 않음)` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lastLine = (text) => text.trim().split(/\r?\n/).filter(Boolean).pop() ?? "";
|
|
119
|
+
|
|
120
|
+
/** The cache folder for the virtual environment. */
|
|
121
|
+
export function cacheHome(env = process.env) {
|
|
122
|
+
if (env.MOTION_SITES_HOME) return env.MOTION_SITES_HOME;
|
|
123
|
+
const base = WIN ? env.LOCALAPPDATA || join(homedir(), "AppData", "Local") : join(homedir(), ".cache");
|
|
124
|
+
return join(base, "banker", "motion-sites");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const venvPython = (env) => join(cacheHome(env), "venv", WIN ? "Scripts" : "bin", WIN ? "python.exe" : "python");
|
|
128
|
+
|
|
129
|
+
/** The plugin for Claude Code: { file (its SKILL.md, or null), disabled (installed but turned off) }. */
|
|
130
|
+
export function claudePlugin(runner = run) {
|
|
131
|
+
const none = { file: null, disabled: false };
|
|
132
|
+
const r = runner("claude", ["plugin", "list", "--json"]);
|
|
133
|
+
if (!r.ok) return none;
|
|
134
|
+
let list;
|
|
135
|
+
try {
|
|
136
|
+
list = JSON.parse(r.stdout);
|
|
137
|
+
} catch {
|
|
138
|
+
return none;
|
|
139
|
+
}
|
|
140
|
+
const p = Array.isArray(list) ? list.find((x) => x?.id === ID) : null;
|
|
141
|
+
const file = p?.installPath ? join(p.installPath, "skills", PLUGIN, "SKILL.md") : null;
|
|
142
|
+
if (!file || !existsSync(file)) return none;
|
|
143
|
+
return p.enabled === false ? { file: null, disabled: true } : { file, disabled: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const claudeSkill = (runner = run) => claudePlugin(runner).file;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The plugin for Codex: { file, disabled }. Installed means its table in config.toml (the body runs to the next table
|
|
150
|
+
* header, so an array value holding "[" does not cut it short) and a version in the cache; the newest version's
|
|
151
|
+
* SKILL.md is the file. enabled = false is disabled.
|
|
152
|
+
*/
|
|
153
|
+
export function codexPlugin(env = process.env) {
|
|
154
|
+
const none = { file: null, disabled: false };
|
|
155
|
+
const home = env.CODEX_HOME || join(homedir(), ".codex");
|
|
156
|
+
let toml = "";
|
|
157
|
+
try {
|
|
158
|
+
toml = readFileSync(join(home, "config.toml"), "utf8");
|
|
159
|
+
} catch {
|
|
160
|
+
return none;
|
|
161
|
+
}
|
|
162
|
+
const head = `[plugins."${ID}"]`.replace(/[.[\]"]/g, "\\$&");
|
|
163
|
+
const table = new RegExp(`^${head}[ \\t]*\\r?\\n([\\s\\S]*?)(?=^[ \\t]*\\[|(?![\\s\\S]))`, "m").exec(toml);
|
|
164
|
+
if (!table) return none;
|
|
165
|
+
const dir = join(home, "plugins", "cache", MARKET, PLUGIN);
|
|
166
|
+
let versions = [];
|
|
167
|
+
try {
|
|
168
|
+
versions = readdirSync(dir).sort(byVersion);
|
|
169
|
+
} catch {
|
|
170
|
+
return none;
|
|
171
|
+
}
|
|
172
|
+
const file = versions.map((v) => join(dir, v, "skills", PLUGIN, "SKILL.md")).reverse().find((f) => existsSync(f));
|
|
173
|
+
if (!file) return none;
|
|
174
|
+
return /^[ \t]*enabled[ \t]*=[ \t]*false\b/m.test(table[1]) ? { file: null, disabled: true } : { file, disabled: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const codexSkill = (env = process.env) => codexPlugin(env).file;
|
|
178
|
+
|
|
179
|
+
export function byVersion(a, b) {
|
|
180
|
+
const pa = a.split(/[.-]/).map((x) => (/^\d+$/.test(x) ? Number(x) : x));
|
|
181
|
+
const pb = b.split(/[.-]/).map((x) => (/^\d+$/.test(x) ? Number(x) : x));
|
|
182
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
183
|
+
if (pa[i] === pb[i]) continue;
|
|
184
|
+
// 2.0.0-rc1 comes before 2.0.0 (a pre-release tail), 2.0.0.1 after it
|
|
185
|
+
if (pa[i] === undefined) return typeof pb[i] === "string" ? 1 : -1;
|
|
186
|
+
if (pb[i] === undefined) return typeof pa[i] === "string" ? -1 : 1;
|
|
187
|
+
return typeof pa[i] === "number" && typeof pb[i] === "number" ? pa[i] - pb[i] : String(pa[i]).localeCompare(String(pb[i]));
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Python programs to try, in order: MOTION_SITES_PYTHON, the cache's virtual environment, then the system's. */
|
|
193
|
+
export function pythons(env = process.env) {
|
|
194
|
+
const list = [];
|
|
195
|
+
// a relative path is made absolute here, as programs start in the home folder
|
|
196
|
+
if (env.MOTION_SITES_PYTHON) list.push([isAbsolute(env.MOTION_SITES_PYTHON) ? env.MOTION_SITES_PYTHON : resolve(env.MOTION_SITES_PYTHON)]);
|
|
197
|
+
list.push([venvPython(env)]);
|
|
198
|
+
if (WIN) list.push(["py", "-3"], ["python"]);
|
|
199
|
+
else list.push(["python3"], ["python"]);
|
|
200
|
+
return list;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const CHROMIUM = "from playwright.sync_api import sync_playwright as s\np=s().start()\nprint(p.chromium.executable_path)\np.stop()";
|
|
204
|
+
|
|
205
|
+
/** What one Python has: { bin, playwright, chromium } or null when it does not run. */
|
|
206
|
+
export function lookPython(py, runner = run) {
|
|
207
|
+
const [cmd, ...pre] = py;
|
|
208
|
+
if (/[\\/]/.test(cmd) && !existsSync(cmd)) return null;
|
|
209
|
+
const v = runner(cmd, [...pre, "-c", "import sys;print(sys.executable)"]);
|
|
210
|
+
if (!v.ok) return null;
|
|
211
|
+
const bin = lastLine(v.stdout ?? "");
|
|
212
|
+
const pw = runner(cmd, [...pre, "-c", "import playwright"]).ok;
|
|
213
|
+
const exe = pw ? runner(cmd, [...pre, "-c", CHROMIUM]) : { ok: false, stdout: "" };
|
|
214
|
+
const chrome = lastLine(exe.stdout ?? "");
|
|
215
|
+
return { bin, cmd: py, playwright: pw, chromium: exe.ok && chrome !== "" && existsSync(chrome) };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The first Python with Playwright (and Chromium, when one has both), else the first that runs. */
|
|
219
|
+
export function pickPython(env = process.env, runner = run) {
|
|
220
|
+
const seen = pythons(env).map((p) => lookPython(p, runner)).filter(Boolean);
|
|
221
|
+
return seen.find((p) => p.playwright && p.chromium) ?? seen.find((p) => p.playwright) ?? seen[0] ?? null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** status: the plugin and the Python, and whether /motion-sites can hand over now. */
|
|
225
|
+
export function status(runtime, { env = process.env, runner = run } = {}) {
|
|
226
|
+
const p = runtime === "claude" ? claudePlugin(runner) : codexPlugin(env);
|
|
227
|
+
const py = pickPython(env, runner);
|
|
228
|
+
const python = py ? { bin: py.bin, playwright: py.playwright, chromium: py.chromium } : null;
|
|
229
|
+
return { runtime, plugin: ID, skillFile: p.file, disabled: p.disabled, python, ready: Boolean(p.file && py?.playwright && py?.chromium) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The plugin steps of one runtime: marketplace add, then install. An add that fails (the marketplace may be there
|
|
234
|
+
* already) is reported only when the install fails too.
|
|
235
|
+
*/
|
|
236
|
+
function installPlugin(runtime, env, runner) {
|
|
237
|
+
const down = offline("marketplace add", "plugin", { env, runner });
|
|
238
|
+
if (down) return [down];
|
|
239
|
+
const cli = runtime === "claude" ? "claude" : "codex";
|
|
240
|
+
const add = runner(cli, ["plugin", "marketplace", "add", REPO], { timeout: STEP_MS });
|
|
241
|
+
const verb = runtime === "claude" ? "install" : "add";
|
|
242
|
+
const inst = runner(cli, ["plugin", verb, ID], { timeout: STEP_MS });
|
|
243
|
+
if (inst.ok) return [{ step: "marketplace add", ok: true }, { step: `plugin ${verb}`, ok: true }];
|
|
244
|
+
return [{ step: "marketplace add", ok: add.ok, why: add.why }, { step: `plugin ${verb}`, ok: false, why: inst.why }];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** pip install playwright with this Python, or in a virtual environment when pip refuses the system Python. */
|
|
248
|
+
function installPlaywright(py, env, runner) {
|
|
249
|
+
const down = offline("pip install playwright", "pip", { env, runner, py: py.cmd });
|
|
250
|
+
if (down) return { ok: false, why: down.why };
|
|
251
|
+
const [cmd, ...pre] = py.cmd;
|
|
252
|
+
const pip = runner(cmd, [...pre, "-m", "pip", "install", "playwright"], { timeout: STEP_MS });
|
|
253
|
+
if (pip.ok) return { ok: true, py: py.cmd };
|
|
254
|
+
if (!/externally.managed|No module named pip/i.test(pip.out)) return { ok: false, why: pip.why };
|
|
255
|
+
const venv = runner(cmd, [...pre, "-m", "venv", join(cacheHome(env), "venv")], { timeout: STEP_MS });
|
|
256
|
+
if (!venv.ok) return { ok: false, why: `${pip.why}. 가상 환경도 만들지 못함: ${venv.why}` };
|
|
257
|
+
const inVenv = runner(venvPython(env), ["-m", "pip", "install", "playwright"], { timeout: STEP_MS });
|
|
258
|
+
return inVenv.ok ? { ok: true, py: [venvPython(env)] } : { ok: false, why: inVenv.why };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A plugin installed but turned off: Claude Code turns it on; Codex has no command for it, so it says where. */
|
|
262
|
+
function enablePlugin(runtime, runner) {
|
|
263
|
+
if (runtime === "codex") return { step: "plugin enable", ok: false, why: `Codex 설정에서 꺼져 있음: config.toml 의 [plugins."${ID}"] 를 enabled = true 로` };
|
|
264
|
+
const r = runner("claude", ["plugin", "enable", ID]);
|
|
265
|
+
return { step: "plugin enable", ok: r.ok, why: r.why };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The Python steps: Playwright, then Chromium. */
|
|
269
|
+
function installBrowser(env, runner) {
|
|
270
|
+
let py = pickPython(env, runner);
|
|
271
|
+
if (!py) return [{ step: "python", ok: false, why: "Python 3 을 찾지 못함(python3, python, Windows 는 py)" }];
|
|
272
|
+
const steps = [];
|
|
273
|
+
let cmd = py.cmd;
|
|
274
|
+
if (!py.playwright) {
|
|
275
|
+
const pw = installPlaywright(py, env, runner);
|
|
276
|
+
steps.push({ step: "pip install playwright", ok: pw.ok, why: pw.why ?? null });
|
|
277
|
+
if (!pw.ok) return steps;
|
|
278
|
+
cmd = pw.py;
|
|
279
|
+
}
|
|
280
|
+
if (!py.chromium) {
|
|
281
|
+
const down = offline("playwright install chromium", "chromium", { env, runner });
|
|
282
|
+
if (down) return [...steps, down];
|
|
283
|
+
const [c, ...pre] = cmd;
|
|
284
|
+
const ch = runner(c, [...pre, "-m", "playwright", "install", "chromium"], { timeout: STEP_MS });
|
|
285
|
+
steps.push({ step: "playwright install chromium", ok: ch.ok, why: ch.why });
|
|
286
|
+
}
|
|
287
|
+
return steps;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** setup: the missing steps in order, stopping at the first that fails; then status. */
|
|
291
|
+
export function setup(runtime, { env = process.env, runner = run } = {}) {
|
|
292
|
+
const before = status(runtime, { env, runner });
|
|
293
|
+
const steps = [];
|
|
294
|
+
if (before.disabled) steps.push(enablePlugin(runtime, runner));
|
|
295
|
+
else if (!before.skillFile) steps.push(...installPlugin(runtime, env, runner));
|
|
296
|
+
if (steps.every((s) => s.ok) && !(before.python?.playwright && before.python?.chromium)) steps.push(...installBrowser(env, runner));
|
|
297
|
+
return { steps, ...status(runtime, { env, runner }) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function parse(argv) {
|
|
301
|
+
const [cmd, ...rest] = argv;
|
|
302
|
+
const i = rest.indexOf("--runtime");
|
|
303
|
+
const runtime = i >= 0 ? rest[i + 1] : undefined;
|
|
304
|
+
if (!["status", "setup", "path"].includes(cmd)) return { error: USAGE };
|
|
305
|
+
if (runtime !== "claude" && runtime !== "codex") return { error: `--runtime claude|codex 가 필요함\n${USAGE}` };
|
|
306
|
+
return { cmd, runtime };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function main(argv, out = console.log, err = console.error) {
|
|
310
|
+
if (argv.includes("--help") || argv.includes("-h")) return out(USAGE), 0;
|
|
311
|
+
const o = parse(argv);
|
|
312
|
+
if (o.error) return err(o.error), 2;
|
|
313
|
+
if (o.cmd === "path") {
|
|
314
|
+
const f = o.runtime === "claude" ? claudeSkill() : codexSkill();
|
|
315
|
+
if (!f) return err(`설치되지 않음: ${ID}. setup --runtime ${o.runtime} 로 설치`), 1;
|
|
316
|
+
return out(f), 0;
|
|
317
|
+
}
|
|
318
|
+
const r = o.cmd === "setup" ? setup(o.runtime) : status(o.runtime);
|
|
319
|
+
out(JSON.stringify(r, null, 2));
|
|
320
|
+
return r.ready ? 0 : 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Run directly (also through a linked path: both sides as real paths).
|
|
324
|
+
const realOf = (p) => {
|
|
325
|
+
try {
|
|
326
|
+
return realpathSync(p);
|
|
327
|
+
} catch {
|
|
328
|
+
return p;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
if (process.argv[1] && realOf(process.argv[1]) === realOf(fileURLToPath(import.meta.url))) process.exitCode = main(process.argv.slice(2));
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* OS: Windows / macOS / Linux 공통. 런처(apply.cmd, apply.sh)와 배경 문서(README.md)는
|
|
22
22
|
* 킷 배포 폴더에만 있다. 이 파일만 옮겨 쓰는 곳(예: 스킬 폴더)에는 없다.
|
|
23
23
|
*/
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
24
25
|
import {
|
|
25
26
|
readFileSync, writeFileSync, writeSync, existsSync, copyFileSync, readdirSync,
|
|
26
27
|
unlinkSync, mkdirSync, rmSync, symlinkSync, lstatSync, realpathSync,
|
|
@@ -422,7 +423,7 @@ function switchActive(sha) {
|
|
|
422
423
|
// 줄인 비트는 chmod 가 맞춘다). 만든 뒤에 맞추면 그 사이 기본 권한(0664 등)으로 남이 읽을 수 있다.
|
|
423
424
|
function writeAtomic(file, data, mode, { follow = true } = {}) {
|
|
424
425
|
const real = follow ? realOr(file) : file;
|
|
425
|
-
const tmp = join(dirname(real), `.${basename(real)}.omc-tmp-${process.pid}`);
|
|
426
|
+
const tmp = join(dirname(real), `.${basename(real)}.omc-tmp-${process.pid}-${randomBytes(6).toString("hex")}`);
|
|
426
427
|
const m = mode ?? modeOf(real);
|
|
427
428
|
rmSync(tmp, { force: true });
|
|
428
429
|
try {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* Exit status 1 means nothing was changed; 2 means an unexpected error stopped
|
|
43
43
|
* the command partway, so check `status`.
|
|
44
44
|
*/
|
|
45
|
+
import { randomBytes } from "node:crypto";
|
|
45
46
|
import { spawnSync } from "node:child_process";
|
|
46
47
|
import {
|
|
47
48
|
accessSync,
|
|
@@ -215,7 +216,7 @@ function linkedFolder(file) {
|
|
|
215
216
|
// either name is replaced rather than written through, and a write that fails
|
|
216
217
|
// leaves the old file as it was.
|
|
217
218
|
function writeFresh(file, data, mode) {
|
|
218
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
219
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
219
220
|
rmSync(tmp, { force: true });
|
|
220
221
|
try {
|
|
221
222
|
writeFileSync(tmp, data, { flag: "wx", mode });
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* fix sentPart and bump CACHE_VERSION, so totals cached under the old rules are
|
|
34
34
|
* rebuilt.
|
|
35
35
|
*/
|
|
36
|
-
import { createHash } from "node:crypto";
|
|
36
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
37
37
|
import {
|
|
38
38
|
closeSync,
|
|
39
39
|
fstatSync,
|
|
@@ -187,7 +187,7 @@ function saveCache(file, state) {
|
|
|
187
187
|
const dir = dirname(file);
|
|
188
188
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
189
189
|
if (!ownedDir(dir)) return; // a folder this account does not control: no cache
|
|
190
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
190
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
191
191
|
rmSync(tmp, { force: true });
|
|
192
192
|
writeFileSync(tmp, JSON.stringify(state), { flag: "wx", mode: 0o600 });
|
|
193
193
|
renameSync(tmp, file);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import { git, noBom, readRegular, writeAtomic } from './pm-config.mjs';
|
|
9
|
+
import { git, noBom, readRegular, writeAtomic, writeNew } from './pm-config.mjs';
|
|
10
10
|
|
|
11
11
|
const ADR_FILE = 'adr.json';
|
|
12
12
|
const ADR_CHECK_FILE = 'adr-check.json';
|
|
@@ -404,13 +404,16 @@ const stamp = (ms) => {
|
|
|
404
404
|
};
|
|
405
405
|
|
|
406
406
|
// 스키마 FAIL 이 남은 adr.json 을 같은 폴더의 adr.json.rejected-<시각>(있으면 -2, -3 ...)으로 옮긴다. 결정 기록을 잃지 않게
|
|
407
|
-
// 지우지 않는다. 새 이름은
|
|
407
|
+
// 지우지 않는다. 새 이름은 writeNew 로 만들어 있는 것(끊어진 링크 포함)을 덮거나 따라가지 않고, 사본을 만든 뒤에만 원본을
|
|
408
|
+
// 지운다. 돌려줌: 옮긴 경로.
|
|
408
409
|
function reject(file, now) {
|
|
409
410
|
const base = `${file}.rejected-${stamp(now)}`;
|
|
411
|
+
const bytes = fs.readFileSync(file);
|
|
412
|
+
const mode = fs.statSync(file).mode & 0o777; // as COPYFILE_EXCL kept it
|
|
410
413
|
for (let i = 1; ; i++) {
|
|
411
414
|
const dest = i === 1 ? base : `${base}-${i}`;
|
|
412
415
|
try {
|
|
413
|
-
|
|
416
|
+
writeNew(dest, bytes, { mode });
|
|
414
417
|
} catch (e) {
|
|
415
418
|
if (e.code === 'EEXIST') continue;
|
|
416
419
|
throw e;
|