@kaydash9999/banker-plugins 0.21.0 → 0.23.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 +69 -0
- package/PRIVACY.md +8 -3
- package/README.md +78 -15
- package/bin/banker.js +154 -32
- package/bin/lib/codex-hooks.mjs +123 -20
- package/bin/lib/codex-names.mjs +4 -0
- package/bin/lib/codex-routes.mjs +204 -0
- package/bin/lib/command-skill.cjs +50 -0
- package/bin/lib/telemetry-config.mjs +2 -1
- package/codex/command-skills/front-qa/SKILL.md +56 -0
- package/codex/command-skills/setup/SKILL.md +87 -0
- package/codex/hooks.json +29 -0
- package/codex/manifest.json +18 -15
- package/codex/skills/graceful-pause/SKILL.md +2 -2
- package/codex/skills/progress/scripts/progress.mjs +44 -14
- package/codex/skills/teamwork/SKILL.md +137 -0
- package/codex/skills/teamwork/scripts/teamwork.mjs +957 -0
- package/codex/transform-matrix.md +5 -3
- package/commands/setup.md +2 -2
- package/hooks/obsidize-hook.mjs +75 -32
- package/hooks/telemetry-count-codex.mjs +303 -35
- package/hooks/update-fetch.mjs +2 -1
- package/hooks/update-notify.mjs +146 -13
- package/package.json +3 -1
- package/skills/3d-intro-build/SKILL.md +101 -20
- package/skills/3d-intro-build/references/NOTICE.md +9 -2
- package/skills/3d-intro-build/references/assemble.mjs +113 -22
- package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-build/references/clip-post.mjs +443 -0
- package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
- package/skills/3d-intro-build/references/intro-fixes.css +11 -0
- package/skills/3d-intro-build/references/prompts.md +51 -4
- package/skills/3d-intro-build/references/scrub-engine.js +1 -0
- package/skills/3d-intro-build/references/step-nav.css +29 -0
- package/skills/3d-intro-build/references/step-nav.js +277 -106
- package/skills/3d-intro-build/references/verify-intro.mjs +387 -32
- package/skills/3d-intro-build/references/verify.md +6 -0
- package/skills/3d-intro-build/references/video-pool.mjs +142 -12
- package/skills/3d-intro-setup/SKILL.md +20 -7
- package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
- package/skills/all-in-one/SKILL.md +125 -86
- package/skills/anti-drm/SKILL.md +1 -1
- package/skills/anti-drm/scripts/instruction-toggle.mjs +57 -4
- package/skills/arch-diagram/build_pptx_template.py +47 -28
- package/skills/audit-web-page/SKILL.md +1 -1
- package/skills/audit-web-page/harness.py +0 -1
- package/skills/lineage/lineage.py +1435 -533
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/obsidizer/SKILL.md +20 -5
- package/skills/omc-patch/SKILL.md +1 -1
- package/skills/payload-mon/SKILL.md +2 -2
- package/skills/play-qa/SKILL.md +7 -5
- package/skills/progress-mon/README.md +162 -0
- package/skills/progress-mon/SKILL.md +165 -0
- package/skills/progress-mon/assets/fixture/README.md +1 -0
- package/skills/progress-mon/assets/fixture/arch.json +33 -0
- package/skills/progress-mon/assets/fixture/commits.json +7 -0
- package/skills/progress-mon/assets/fixture/config.json +30 -0
- package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
- package/skills/progress-mon/assets/fixture/flow.json +44 -0
- package/skills/progress-mon/assets/fixture/items.json +14 -0
- package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
- package/skills/progress-mon/assets/fixture/reqs.json +11 -0
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
- package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
- package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
- package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
- package/skills/progress-mon/references/arch.md +280 -0
- package/skills/progress-mon/references/authoring.md +199 -0
- package/skills/progress-mon/references/config.md +115 -0
- package/skills/progress-mon/references/flow.md +75 -0
- package/skills/progress-mon/references/inventory.md +58 -0
- package/skills/progress-mon/references/items.md +74 -0
- package/skills/progress-mon/references/req-spec.md +74 -0
- package/skills/progress-mon/references/reqs.md +30 -0
- package/skills/progress-mon/scripts/index.js +31 -0
- package/skills/progress-mon/scripts/pm-check.mjs +526 -0
- package/skills/progress-mon/scripts/pm-config.mjs +341 -0
- package/skills/progress-mon/scripts/pm-data.mjs +394 -0
- package/skills/progress-mon/scripts/pm-html.mjs +77 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +308 -0
- package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
- package/skills/progress-mon/scripts/pm-server.mjs +735 -0
- package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
- package/skills/ralph-qa/SKILL.md +105 -16
- package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
- package/skills/ralph-qa/references/payload-scan.mjs +19 -2
- package/skills/ralph-qa/references/run-folder.mjs +299 -0
- package/skills/ralph-qa/references/seat-payload.mjs +60 -13
- package/skills/ralph-qa/references/seat-run.mjs +159 -155
- package/skills/remains/SKILL.md +2 -1
- package/skills/remains/scripts/boxes.mjs +6 -2
- package/skills/rfp-author/scripts/md2docx.py +63 -34
- package/skills/setup-bypass-permissions/SKILL.md +2 -2
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +66 -15
- package/skills/setup-mcp/SKILL.md +1 -1
- package/skills/setup-omc-hud/SKILL.md +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
- package/skills/smart-compact/SKILL.md +1 -1
- package/skills/smart-compact/scripts/codex-gate.mjs +2 -1
- package/skills/theme-selection/SKILL.md +1 -1
- package/skills/tone-compact/SKILL.md +1 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +57 -4
- package/skills/ultra-chain/SKILL.md +34 -16
- package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
- package/skills/ultra-init/SKILL.md +30 -22
- package/skills/ultra-ui-qa/SKILL.md +1 -1
- package/skills/ultraqa/NOTICE.md +34 -0
- package/skills/ultraqa/SKILL.md +196 -0
- package/skills/ultraqa/references/gate-evidence.mjs +457 -0
- package/skills/ultraqa/references/run-folder.mjs +299 -0
- package/skills/ultraqa/references/tmux-drive.md +52 -0
- package/skills/ultraqa/references/uqa-run.mjs +339 -0
- package/skills/update-banker/SKILL.md +32 -7
- package/skills/vertical-pptx/SKILL.md +1 -1
- package/skills/vertical-pptx/build_a4p.js +20 -2
- package/skills/vertical-pptx/build_a4p.py +560 -359
package/bin/banker.js
CHANGED
|
@@ -12,6 +12,7 @@ const os = require('os');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const cp = require('child_process');
|
|
14
14
|
const { pathToFileURL } = require('url');
|
|
15
|
+
const { codexFrontmatter, commandSkillText } = require('./lib/command-skill.cjs');
|
|
15
16
|
|
|
16
17
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
17
18
|
const MANIFEST = path.join(PKG_ROOT, 'codex', 'manifest.json');
|
|
@@ -33,15 +34,22 @@ function isoTime(t) {
|
|
|
33
34
|
return Number.isNaN(d.getTime()) ? '' : d.toISOString();
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// What banker starts by bare name (gh, claude) runs from the home folder with Windows' search of the current folder off:
|
|
38
|
+
// on Windows the program search (cmd, and CreateProcess through libuv) looks in the current folder before PATH, and the
|
|
39
|
+
// current folder may be a project with a planted gh.exe or claude.cmd.
|
|
40
|
+
function bareSpawnOpts() {
|
|
41
|
+
return { cwd: os.homedir(), env: { ...process.env, NoDefaultCurrentDirectoryInExePath: '1' } };
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
// GitHub 저장소에 별을 단다 (gh CLI 인증 사용). star 는 repo 쓰기 권한이 필요 없어 인증된 어느 계정으로도
|
|
37
45
|
// 가능하다. gh 미설치/미인증/실패는 {ok:false, reason} 로 반환해 호출부가 URL 안내로 폴백하게 한다.
|
|
38
46
|
// spawn 은 테스트 주입용(기본 cp.spawnSync). 자체 timeout·오류삼킴으로 setup 을 절대 막지 않는다.
|
|
39
47
|
function starRepo(spawn = cp.spawnSync) {
|
|
40
48
|
try {
|
|
41
|
-
const v = spawn('gh', ['--version'], { stdio: 'ignore', timeout: 3000, windowsHide: true });
|
|
49
|
+
const v = spawn('gh', ['--version'], { stdio: 'ignore', timeout: 3000, windowsHide: true, ...bareSpawnOpts() });
|
|
42
50
|
if (v.error || v.status !== 0) return { ok: false, reason: 'gh CLI 없음' };
|
|
43
51
|
const r = spawn('gh', ['api', '-X', 'PUT', `/user/starred/${REPO_SLUG}`],
|
|
44
|
-
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10000, windowsHide: true });
|
|
52
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10000, windowsHide: true, ...bareSpawnOpts() });
|
|
45
53
|
if (r.error) return { ok: false, reason: r.error.message };
|
|
46
54
|
if (r.status !== 0) {
|
|
47
55
|
const msg = (`${r.stderr || r.stdout || ''}`).trim().split('\n')[0] || `gh exited ${r.status}`;
|
|
@@ -150,29 +158,31 @@ function readSkillName(skillMd) {
|
|
|
150
158
|
|
|
151
159
|
// Codex discovers skills by directory and expects the SKILL.md frontmatter `name:` to equal the
|
|
152
160
|
// directory name. Our Codex dir is `banker-<name>` (prefixed to avoid colliding with omx/system
|
|
153
|
-
// skills), so rewrite ONLY the first `name:` line inside the YAML frontmatter to match
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
// skills), so rewrite ONLY the first `name:` line inside the YAML frontmatter to match, and the
|
|
162
|
+
// `description:` line when the manifest gives the skill a Codex one (codexDescription).
|
|
163
|
+
function setCodexSkillName(skillMd, newName, dryRun, description) {
|
|
164
|
+
if (dryRun) { log(` [dry-run] set frontmatter name: ${newName}${description ? ' and the Codex description' : ''}`); return; }
|
|
156
165
|
let src;
|
|
157
166
|
try { src = fs.readFileSync(skillMd, 'utf8'); } catch { return; }
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
let block = fm[1];
|
|
161
|
-
block = /^name:.*$/m.test(block) ? block.replace(/^name:.*$/m, `name: ${newName}`) : `name: ${newName}\n${block}`;
|
|
162
|
-
fs.writeFileSync(skillMd, src.slice(0, fm.index) + `---\n${block}\n---` + src.slice(fm.index + fm[0].length));
|
|
167
|
+
const out = codexFrontmatter(src, newName, description); // no frontmatter: left untouched
|
|
168
|
+
if (out !== src) fs.writeFileSync(skillMd, out);
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
function
|
|
171
|
+
// The name Codex gives a skill (manifest codexName: setup-omc is setup-omx there), and Claude name to Codex name.
|
|
172
|
+
function codexNameOf(s) { return s.codexName || s.name; }
|
|
173
|
+
function codexRenames(m) { return Object.fromEntries(eligible(m).filter(s => s.codexName).map(s => [s.name, s.codexName])); }
|
|
174
|
+
|
|
175
|
+
// Installed banker-<old> skill folders whose skill Codex now names apart, as [old, new] folder names (found before the sweep).
|
|
176
|
+
function renamedInstalls(skillsDir, m) {
|
|
177
|
+
return eligible(m).filter(s => s.codexName && fs.existsSync(path.join(skillsDir, `banker-${s.name}`, 'SKILL.md')))
|
|
178
|
+
.map(s => [`banker-${s.name}`, `banker-${s.codexName}`]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A command as a Codex skill (bin/lib/command-skill.cjs, shared with the Codex plugin's codex/command-skills/).
|
|
182
|
+
function writeCommandSkill(src, destDir, name, dryRun, codex = {}) {
|
|
168
183
|
if (dryRun) { log(` [dry-run] write command skill ${path.relative(PKG_ROOT, src)} -> ${destDir}`); return; }
|
|
169
|
-
const text = fs.readFileSync(src, 'utf8');
|
|
170
|
-
const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
171
|
-
const desc = fm && fm[1].match(/^description:\s*(.*)$/m);
|
|
172
|
-
const body = fm ? text.slice(fm[0].length) : text;
|
|
173
|
-
const note = '> Codex: 이 명령은 스킬로 설치됐다. 본문의 `$ARGUMENTS`, `$1` 은 사용자가 `$' + name + '` 뒤에 쓴 글이다.\n\n';
|
|
174
184
|
fs.mkdirSync(destDir, { recursive: true });
|
|
175
|
-
fs.writeFileSync(path.join(destDir, 'SKILL.md'),
|
|
185
|
+
fs.writeFileSync(path.join(destDir, 'SKILL.md'), commandSkillText(fs.readFileSync(src, 'utf8'), name, `$${name}`, codex));
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
/* ---------- Claude Code target ---------- */
|
|
@@ -185,7 +195,7 @@ function setupClaude(dryRun) {
|
|
|
185
195
|
];
|
|
186
196
|
for (const [bin, args] of steps) {
|
|
187
197
|
if (dryRun) { log(` [dry-run] ${bin} ${args.join(' ')}`); continue; }
|
|
188
|
-
try { cp.execFileSync(bin, args, { stdio: 'inherit' }); }
|
|
198
|
+
try { cp.execFileSync(bin, args, { stdio: 'inherit', ...bareSpawnOpts() }); }
|
|
189
199
|
catch (e) { warn(` step failed (may already be applied): ${bin} ${args.join(' ')}`); }
|
|
190
200
|
}
|
|
191
201
|
log(' → skills/commands available as /banker:* (reload-plugins or restart to apply).');
|
|
@@ -200,30 +210,38 @@ function setupCodex(dryRun, scope) {
|
|
|
200
210
|
const base = codexBase(scope);
|
|
201
211
|
const skillsDir = path.join(base, 'skills');
|
|
202
212
|
const promptsDir = path.join(base, 'prompts');
|
|
213
|
+
const renames = renamedInstalls(skillsDir, m);
|
|
203
214
|
// Clean reinstall: remove any prior banker-* first so an update never leaves a stale duplicate.
|
|
204
215
|
const swept = sweepCodexArtifacts(base, dryRun);
|
|
205
216
|
if (swept) log(` swept ${swept} prior banker-* artifact(s) before reinstall.`);
|
|
206
217
|
let nSkill = 0, nCmd = 0;
|
|
207
218
|
for (const s of eligible(m)) {
|
|
208
219
|
if (s.type === 'skill') {
|
|
209
|
-
const dest = path.join(skillsDir, `banker-${s
|
|
220
|
+
const dest = path.join(skillsDir, `banker-${codexNameOf(s)}`);
|
|
210
221
|
copyDir(skillSource(s), dest, dryRun);
|
|
211
|
-
setCodexSkillName(path.join(dest, 'SKILL.md'), `banker-${s
|
|
222
|
+
setCodexSkillName(path.join(dest, 'SKILL.md'), `banker-${codexNameOf(s)}`, dryRun, s.codexDescription); // dir==name (Codex discovery)
|
|
212
223
|
nSkill++;
|
|
213
224
|
} else if (s.type === 'command') {
|
|
214
225
|
const src = path.join(PKG_ROOT, 'commands', `${s.name}.md`);
|
|
215
226
|
copyFile(src, path.join(promptsDir, `banker-${s.name}.md`), dryRun);
|
|
216
|
-
writeCommandSkill(src, path.join(skillsDir, `banker-${s.name}`), `banker-${s.name}`, dryRun);
|
|
227
|
+
writeCommandSkill(src, path.join(skillsDir, `banker-${s.name}`), `banker-${s.name}`, dryRun, { description: s.codexDescription, rename: codexRenames(m) });
|
|
217
228
|
nCmd++;
|
|
218
229
|
}
|
|
219
230
|
}
|
|
220
231
|
log(` → ${nSkill} skills -> ${path.join(base, 'skills', 'banker-*')}, ${nCmd} commands -> skills banker-* and prompts ${path.join(base, 'prompts', 'banker-*.md')}`);
|
|
232
|
+
reportRenames(renames);
|
|
221
233
|
log(' → Codex 0.144+ has no custom prompts, so each command is also a skill: invoke it as $banker-<name>.');
|
|
222
234
|
log(' → skills are invoked as `banker-<name>` (frontmatter name rewritten to match the dir).');
|
|
223
235
|
log(' → ~/.codex/AGENTS.md is NOT modified (omx regenerates it); skills auto-discovered from ~/.codex/skills/.');
|
|
224
236
|
return true;
|
|
225
237
|
}
|
|
226
238
|
|
|
239
|
+
// The skills an update installed under their new Codex name, and what a user who turned an old name off must do.
|
|
240
|
+
function reportRenames(renames) {
|
|
241
|
+
for (const [from, to] of renames) log(` 이름 바뀜: ${from} 에서 ${to} (부를 때는 $${to})`);
|
|
242
|
+
if (renames.length) log(' 옛 이름으로 끈 설정(config.toml 의 [[skills.config]])은 새 이름에 닿지 않으니 새 이름으로 다시 적으십시오.');
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
/* ---------- doctor ---------- */
|
|
228
246
|
// The newest Codex session record (rollout) under <base>/sessions/YYYY/MM/DD/, or null.
|
|
229
247
|
function newestRollout(base) {
|
|
@@ -269,16 +287,72 @@ function doctorCodexShape(base) {
|
|
|
269
287
|
const file = newestRollout(base);
|
|
270
288
|
if (!file) { log(' Codex record shape: no session record yet (run one Codex session, then doctor again)'); return; }
|
|
271
289
|
const r = codexShapeReport(file);
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
if (!r || !r.version) { log(` ⚠ Codex record shape: ${path.basename(file)} does not open with session_meta - lineage, progress and usage counting cannot read it`); return; }
|
|
290
|
+
const early = shapeEarlyNote(r, path.basename(file));
|
|
291
|
+
if (early) { log(early); return; }
|
|
275
292
|
const mark = (ok) => (ok ? 'ok' : 'MISSING');
|
|
276
293
|
log(` Codex record shape (Codex ${r.version}, ${path.basename(file)}): turn start ${mark(r.turnStart)}, user text ${mark(r.userText)}, tool calls ${mark(r.toolCall)}`);
|
|
294
|
+
doctorShapeVerdict(r);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// What doctor says before the three checks: the first line is too long, no turn yet, or not a Codex record. Lines after
|
|
298
|
+
// session_meta of which banker reads none (no turn start, user text or tool call) are what a Codex update that changed
|
|
299
|
+
// the whole record shape leaves, so they are flagged, not taken for a session with no turn yet.
|
|
300
|
+
function shapeEarlyNote(r, name) {
|
|
301
|
+
const unread = ` ⚠ Codex record shape: ${name} does not open with session_meta - lineage, progress and usage counting cannot read it`;
|
|
302
|
+
if (!r) return unread;
|
|
303
|
+
if (r.tooLong) return ` Codex record shape: ${name} opens with a line over 8 MB, not checked`;
|
|
304
|
+
if (r.fresh) return ` Codex record shape: ${name} holds no turn yet (run one Codex turn, then doctor again)`;
|
|
305
|
+
if (r.version && !(r.turnStart || r.userText || r.toolCall)) {
|
|
306
|
+
return ` ⚠ Codex record shape (Codex ${r.version}, ${name}): it has lines after session_meta but no turn start, user text or tool call banker reads - `
|
|
307
|
+
+ 'the record shape may have changed (lineage stops, progress warns, usage counting misses); update banker. If this session has run no turn yet, run one and doctor again';
|
|
308
|
+
}
|
|
309
|
+
return r.version ? '' : unread;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// A turn opens with task_started, then the user's text. Text or tool calls with no turn start, or a turn start with no
|
|
313
|
+
// text, is a record shape banker does not know. Neither one is a session whose first turn has not begun.
|
|
314
|
+
function doctorShapeVerdict(r) {
|
|
277
315
|
if (!r.turnStart || !r.userText) log(' ⚠ banker may not read this Codex version\'s records (lineage stops, progress warns, usage counting misses); update banker');
|
|
278
316
|
else if (!r.toolCall) log(' (no tool calls: fine for a session that used no tools)');
|
|
279
317
|
}
|
|
280
318
|
|
|
281
|
-
|
|
319
|
+
// bin/lib/codex-routes.mjs is ESM: the two ways banker gets into Codex (this npm install, and the Codex plugin).
|
|
320
|
+
async function loadCodexRoutes() {
|
|
321
|
+
return import(pathToFileURL(path.join(__dirname, 'lib', 'codex-routes.mjs')).href);
|
|
322
|
+
}
|
|
323
|
+
// Codex plugin installs of banker in <base> as 'banker@<marketplace> (<versions in the plugin cache>)'.
|
|
324
|
+
async function codexPluginInstalls(base) {
|
|
325
|
+
const r = await loadCodexRoutes();
|
|
326
|
+
return r.pluginRoutes(base).map((id) => {
|
|
327
|
+
let vers = [];
|
|
328
|
+
try { vers = fs.readdirSync(path.join(base, 'plugins', 'cache', id.slice(id.indexOf('@') + 1), PLUGIN_NAME)); } catch {}
|
|
329
|
+
return `${id}${vers.length ? ` (${vers.join(', ')})` : ''}`;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// Beside the Codex plugin, this npm install shows every skill twice and runs every hook twice. Say so; remove nothing.
|
|
333
|
+
async function warnCodexPlugin(base) {
|
|
334
|
+
const ids = await codexPluginInstalls(base);
|
|
335
|
+
if (!ids.length) return;
|
|
336
|
+
warn(` ⚠ banker is also installed as a Codex plugin here: ${ids.join(', ')}. Skills show twice ($banker-<name> and $banker:<name>) and the hooks run twice.`);
|
|
337
|
+
warn(` Keep one: codex plugin remove ${ids[0].split(' ')[0]}, or banker uninstall --codex.`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// The plugin's per-skill list was measured on Codex 0.160.1 and later; below that the banker skills may not show.
|
|
341
|
+
async function warnOldCodex(plugins) {
|
|
342
|
+
if (!plugins.length) return;
|
|
343
|
+
const r = await loadCodexRoutes();
|
|
344
|
+
const note = r.pluginVersionNote(r.codexCliVersion(cp.spawnSync));
|
|
345
|
+
if (note) log(` ⚠ ${note}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// A Codex home inside the folder doctor runs in (not its top .codex, which Codex's sandbox keeps read-only) is one a
|
|
349
|
+
// Codex session opened here can change from the sandbox: its config, the hook copies, the plugins.
|
|
350
|
+
async function warnCodexHomeHere(base) {
|
|
351
|
+
if (!(await loadCodexRoutes()).homeInSession(base, process.cwd())) return;
|
|
352
|
+
log(` ⚠ the Codex home ${base} is inside this folder (not its top .codex): a Codex session opened here can change its config and hooks from the sandbox; use a Codex home outside the folders you open Codex in`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function doctor() {
|
|
282
356
|
log(`banker v${version()} (pkg: ${PKG_ROOT})`);
|
|
283
357
|
log(`plugin.json version: ${(() => { try { return require(PLUGIN_JSON).version; } catch { return '?'; } })()}`);
|
|
284
358
|
log('Claude Code:');
|
|
@@ -290,6 +364,7 @@ function doctor() {
|
|
|
290
364
|
try { installed = fs.readdirSync(sdir).filter(d => d.startsWith('banker-')); } catch {}
|
|
291
365
|
log(` codex CLI: ${have('codex') ? 'found' : 'NOT found'}`);
|
|
292
366
|
log(` installed banker skills in ${sdir}: ${installed.length}`);
|
|
367
|
+
await warnCodexHomeHere(base);
|
|
293
368
|
doctorCodexShape(base);
|
|
294
369
|
// Codex only discovers a skill when its dir name equals the SKILL.md `name:` — flag any mismatch.
|
|
295
370
|
const mismatched = installed
|
|
@@ -299,7 +374,14 @@ function doctor() {
|
|
|
299
374
|
log(` ⚠ dir/name mismatch (Codex will NOT list these) — reinstall with this version:`);
|
|
300
375
|
for (const x of mismatched) log(` ${x.d}/ has name: ${x.name}`);
|
|
301
376
|
}
|
|
302
|
-
|
|
377
|
+
const plugins = await codexPluginInstalls(base);
|
|
378
|
+
log(` Codex plugin (codex plugin add): ${plugins.length ? plugins.join(', ') : 'not installed'}`);
|
|
379
|
+
await warnOldCodex(plugins);
|
|
380
|
+
doctorTeamwork(base);
|
|
381
|
+
// A project-scope npm install (banker setup --codex --scope project) in this folder doubles the skills here too.
|
|
382
|
+
const projectNpm = (await loadCodexRoutes()).projectNpmRoot(process.cwd());
|
|
383
|
+
if (plugins.length && (installed.length || projectNpm)) log(` ⚠ banker is installed both ways${projectNpm && !installed.length ? ` (npm in ${path.join(projectNpm, '.codex')})` : ''}: skills show twice ($banker-<name> and $banker:<name>). Keep one: banker uninstall --codex, or codex plugin remove ${plugins[0].split(' ')[0]}`);
|
|
384
|
+
if (have('codex') && installed.length === 0 && !plugins.length) log(' ⚠ codex found but 0 banker skills installed — run: banker setup --codex, or codex plugin add banker@banker-plugins');
|
|
303
385
|
const m = readManifest();
|
|
304
386
|
const nSkills = eligible(m).filter(s => s.type === 'skill').length;
|
|
305
387
|
log(` manifest: ${eligible(m).length} codex-eligible surfaces (${nSkills} skills) of ${m.surfaces.length}.`);
|
|
@@ -307,13 +389,27 @@ function doctor() {
|
|
|
307
389
|
log(' → install via /banker:setup (Claude) or the setup-* skills; skills also guide you if a dependency is missing.');
|
|
308
390
|
}
|
|
309
391
|
|
|
392
|
+
// The Codex-only teamwork skill keeps its engine, hooks and approval rule in the Codex home: say whether the programs it
|
|
393
|
+
// recorded still exist, and whether a rules/teamwork.rules it did not write sits where its rule goes.
|
|
394
|
+
function doctorTeamwork(base) {
|
|
395
|
+
let st = null;
|
|
396
|
+
try { st = JSON.parse(fs.readFileSync(path.join(base, 'teamwork', 'state.json'), 'utf8')); } catch { return; }
|
|
397
|
+
const missing = ['node', 'codex'].filter((k) => !(st.tools && st.tools[k] && fs.existsSync(st.tools[k])));
|
|
398
|
+
// The hooks' wrapper needs the engine and node only; send and reply start the recorded codex.
|
|
399
|
+
const fails = [...(missing.includes('node') ? ['its hooks stay silent'] : []), ...(missing.includes('codex') ? ['send and reply fail'] : [])];
|
|
400
|
+
log(` teamwork: ${st.enabled ? 'on' : 'off'} (engine ${st.version || '?'})${missing.length ? ` ⚠ recorded ${missing.join(', ')} missing: ${fails.join(' and ')} until $banker-teamwork on runs again` : ''}`);
|
|
401
|
+
let rules = '';
|
|
402
|
+
try { rules = fs.readFileSync(path.join(base, 'rules', 'teamwork.rules'), 'utf8'); } catch { return; }
|
|
403
|
+
if (!rules.startsWith('# teamwork ')) log(` ⚠ ${path.join(base, 'rules', 'teamwork.rules')} was not written by teamwork; teamwork leaves it as it is and asks for approval on every call`);
|
|
404
|
+
}
|
|
405
|
+
|
|
310
406
|
/* ---------- uninstall ---------- */
|
|
311
407
|
function uninstallClaude(dryRun) {
|
|
312
408
|
log('• Claude Code uninstall:');
|
|
313
409
|
if (!have('claude')) { warn(' claude CLI not found; skipping.'); return; }
|
|
314
410
|
for (const args of [['plugin', 'uninstall', PLUGIN_NAME], ['plugin', 'marketplace', 'remove', MARKETPLACE]]) {
|
|
315
411
|
if (dryRun) { log(` [dry-run] claude ${args.join(' ')}`); continue; }
|
|
316
|
-
try { cp.execFileSync('claude', args, { stdio: 'inherit' }); } catch { warn(` (not present) claude ${args.join(' ')}`); }
|
|
412
|
+
try { cp.execFileSync('claude', args, { stdio: 'inherit', ...bareSpawnOpts() }); } catch { warn(` (not present) claude ${args.join(' ')}`); }
|
|
317
413
|
}
|
|
318
414
|
}
|
|
319
415
|
function uninstallCodex(dryRun, scopes) {
|
|
@@ -337,6 +433,21 @@ function uninstallCodex(dryRun, scopes) {
|
|
|
337
433
|
}
|
|
338
434
|
}
|
|
339
435
|
|
|
436
|
+
// teamwork (codex-only) goes only when no other way of banker into this Codex home is left: the user-scope npm skills
|
|
437
|
+
// (kept by --scope project) and the Codex plugin. Its own uninstall removes its two hooks, its rule and its state.
|
|
438
|
+
// It runs only with the user scope in the sweep, so a dry run counts the npm skills as gone, as the real run finds them.
|
|
439
|
+
async function uninstallTeamwork(dryRun) {
|
|
440
|
+
const base = codexBase('user');
|
|
441
|
+
if (!fs.existsSync(path.join(base, 'teamwork'))) return;
|
|
442
|
+
const npmLeft = !dryRun && fs.existsSync(path.join(base, 'skills', 'banker-teamwork'));
|
|
443
|
+
const left = [...(npmLeft ? ['npm skills'] : []), ...(await codexPluginInstalls(base))];
|
|
444
|
+
if (left.length) { log(` teamwork kept: banker is still in this Codex home (${left.join(', ')})`); return; }
|
|
445
|
+
if (dryRun) { log(` [dry-run] teamwork uninstall in ${base}`); return; }
|
|
446
|
+
const r = cp.spawnSync(process.execPath, [path.join(PKG_ROOT, 'codex', 'skills', 'teamwork', 'scripts', 'teamwork.mjs'), 'uninstall'], { env: { ...process.env, CODEX_HOME: base }, encoding: 'utf8' });
|
|
447
|
+
if (r.status === 0) log(' removed teamwork (its hooks, approval rule and state)');
|
|
448
|
+
else warn(` teamwork not removed (exit ${r.status}): ${(r.stderr || '').trim()}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
340
451
|
/* ---------- Codex hooks ---------- */
|
|
341
452
|
// bin/lib/codex-hooks.mjs is ESM; loaded lazily like telemetry-config. Hooks go into the same Codex home as the skills.
|
|
342
453
|
async function loadCodexHooks() {
|
|
@@ -353,6 +464,7 @@ async function setupCodexHooks(dryRun, scope) {
|
|
|
353
464
|
const r = ch.enableHooks(codexHookPaths(ch), ch.DEFAULT_HOOKS, PKG_ROOT);
|
|
354
465
|
if (r.code) { warn(` Codex hooks not changed: ${r.msg}`); return; }
|
|
355
466
|
log(` → Codex hooks ${ch.DEFAULT_HOOKS.join(', ')} in ${codexHookPaths(ch).hooks}${r.changed.length ? ` (changed: ${r.changed.join(', ')})` : ' (already in place)'}.`);
|
|
467
|
+
if (r.note) log(` → ${r.note}`);
|
|
356
468
|
if (r.changed.length) log(` → ${ch.TRUST_NOTE}`);
|
|
357
469
|
}
|
|
358
470
|
|
|
@@ -380,7 +492,12 @@ async function hooksCmd(sub, ids) {
|
|
|
380
492
|
}
|
|
381
493
|
const r = sub === 'enable' ? ch.enableHooks(p, want, PKG_ROOT) : ch.disableHooks(p, want);
|
|
382
494
|
if (r.code) { warn(`banker hooks: not changed: ${r.msg}`); process.exitCode = 1; return; }
|
|
495
|
+
reportHooks(sub, r, ch);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function reportHooks(sub, r, ch) {
|
|
383
499
|
log(`banker hooks ${sub}: ${r.changed.length ? r.changed.join(', ') : 'nothing to change'}`);
|
|
500
|
+
if (r.note) log(r.note);
|
|
384
501
|
if (sub === 'enable' && r.changed.length) log(ch.TRUST_NOTE);
|
|
385
502
|
if (r.shifted && r.shifted.length) log(`later ${r.shifted.join(', ')} hooks may need trusting again in /hooks.`);
|
|
386
503
|
}
|
|
@@ -543,6 +660,8 @@ Notes:
|
|
|
543
660
|
- --claude registers the marketplace + installs banker@banker-plugins (skills as /banker:*).
|
|
544
661
|
- --codex copies codex-eligible skills -> ~/.codex/skills/banker-* and commands -> ~/.codex/prompts/banker-*.md
|
|
545
662
|
($CODEX_HOME instead of ~/.codex when it is set).
|
|
663
|
+
- Codex can also take banker as a plugin (codex plugin marketplace add smallOpenSource/banker_plugins, then
|
|
664
|
+
codex plugin add banker@banker-plugins; skills as $banker:<name>). Keep one of the two ways in.
|
|
546
665
|
- telemetry on|off toggles anonymous usage counting opt-out (default-on; config only); status prints counting + update-check state. See PRIVACY.md.
|
|
547
666
|
- Never runs as root. --dry-run prints planned actions without writing.`;
|
|
548
667
|
|
|
@@ -550,14 +669,17 @@ async function main() {
|
|
|
550
669
|
const a = parseArgs(process.argv.slice(2));
|
|
551
670
|
if (a.version) { log(version()); return; }
|
|
552
671
|
if (a.help || a.cmd === 'help' || !a.cmd) { log(HELP); return; }
|
|
553
|
-
if (a.cmd === 'doctor') { doctor(); return; }
|
|
672
|
+
if (a.cmd === 'doctor') { await doctor(); return; }
|
|
554
673
|
if (a.cmd === 'telemetry') { await telemetryCmd(a.sub); return; }
|
|
555
674
|
if (a.cmd === 'hooks') { assertNotRoot(); await hooksCmd(a.sub, a.hookIds); return; }
|
|
556
675
|
assertNotRoot();
|
|
557
676
|
if (a.cmd === 'setup') {
|
|
558
677
|
log(`banker setup${a.dryRun ? ' (dry-run)' : ''}: claude=${a.claude} codex=${a.codex}`);
|
|
559
678
|
if (a.claude) setupClaude(a.dryRun);
|
|
560
|
-
if (a.codex && setupCodex(a.dryRun, a.scope))
|
|
679
|
+
if (a.codex && setupCodex(a.dryRun, a.scope)) {
|
|
680
|
+
await setupCodexHooks(a.dryRun, a.scope);
|
|
681
|
+
if (a.scope === 'user') await warnCodexPlugin(codexBase('user'));
|
|
682
|
+
}
|
|
561
683
|
// Best-effort first-run prompts (interactive TTY only). A prompt failure must never fail setup.
|
|
562
684
|
try { await maybeFirstRunPrompts(a); } catch { /* non-fatal */ }
|
|
563
685
|
log('done.');
|
|
@@ -567,7 +689,7 @@ async function main() {
|
|
|
567
689
|
if (a.claude) uninstallClaude(a.dryRun);
|
|
568
690
|
if (a.codex) {
|
|
569
691
|
uninstallCodex(a.dryRun, a.scopeExplicit ? [a.scope] : ['user', 'project']);
|
|
570
|
-
if (!a.scopeExplicit || a.scope === 'user') await uninstallCodexHooks(a.dryRun);
|
|
692
|
+
if (!a.scopeExplicit || a.scope === 'user') { await uninstallCodexHooks(a.dryRun); await uninstallTeamwork(a.dryRun); }
|
|
571
693
|
}
|
|
572
694
|
log('done.');
|
|
573
695
|
return;
|
package/bin/lib/codex-hooks.mjs
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* 돌린다. Codex 는 신뢰한 훅을 샌드박스 밖에서 돌리므로, 작업 폴더 안의 파일을 가리키지 않게 하기 위해서다.
|
|
7
7
|
* - 묶음은 각 이벤트의 맨 뒤에 더한다. Codex 는 신뢰를 <hooks.json>:<이벤트>:<묶음 번호>:<항목 번호> 로 기록해, 앞에
|
|
8
8
|
* 끼우면 OMX 훅의 번호가 밀려 다시 신뢰해야 한다. 빼기는 우리 항목만, 낡은 명령은 같은 자리에서 고친다.
|
|
9
|
-
* - Windows 의 Codex 는 훅 명령을 PowerShell 로 돌린다(Codex 0.160.1 실측): `& '<node>' '<스크립트>' 인자`.
|
|
9
|
+
* - Windows 의 Codex 는 훅 명령을 PowerShell 로 돌린다(Codex 0.160.1 실측): `& '<node>' -e '<검사>' '<스크립트>' 인자`.
|
|
10
|
+
* 검사(hookGuard)는 스크립트의 실제 경로가 임시 폴더나 세션 폴더 아래면 돌리지 않는다.
|
|
10
11
|
* - hooks.json 이 링크이거나 JSON 객체가 아니거나 "없음" 말고 다른 이유로 확인할 수 없으면 바꾸지 않는다. 바꿀 때마다
|
|
11
12
|
* 직전 내용을 hooks.json.banker.bak 에 둔다. smart-compact 의 codex-gate.mjs 와 같은 규칙이다.
|
|
12
13
|
*/
|
|
13
|
-
import
|
|
14
|
+
import crypto from 'node:crypto';
|
|
14
15
|
import fs from 'node:fs';
|
|
15
16
|
import os from 'node:os';
|
|
16
17
|
import path from 'node:path';
|
|
@@ -25,6 +26,8 @@ export const HOOKS = {
|
|
|
25
26
|
export const PAYLOAD = [
|
|
26
27
|
'.claude-plugin/plugin.json',
|
|
27
28
|
'bin/lib/telemetry-config.mjs',
|
|
29
|
+
'bin/lib/codex-routes.mjs',
|
|
30
|
+
'bin/lib/codex-names.mjs',
|
|
28
31
|
'hooks/update-notify.mjs',
|
|
29
32
|
'hooks/update-fetch.mjs',
|
|
30
33
|
'hooks/update-checkin.mjs',
|
|
@@ -42,8 +45,9 @@ export function codexPaths(env = process.env) {
|
|
|
42
45
|
|
|
43
46
|
const isObject = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
44
47
|
|
|
48
|
+
// 임시 파일은 난수 이름과 wx: 남이 미리 심은 같은 이름의 파일이나 링크를 따라가지 않는다.
|
|
45
49
|
function writeAtomic(file, data) {
|
|
46
|
-
const tmp = `${file}.${
|
|
50
|
+
const tmp = `${file}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
47
51
|
try {
|
|
48
52
|
fs.writeFileSync(tmp, data, { flag: 'wx' });
|
|
49
53
|
const mode = modeOf(file);
|
|
@@ -65,19 +69,45 @@ function modeOf(file) {
|
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
// 명령 글. POSIX 는 큰따옴표, 셸이 그 안에서 읽는 글자는 거부. Windows 는 PowerShell 호출 연산자와 작은따옴표.
|
|
68
|
-
|
|
72
|
+
// guard 가 있으면 node -e <guard> <스크립트> 인자 꼴(guard 는 큰따옴표, $, 백틱, 백슬래시가 없는 고정 글).
|
|
73
|
+
export function formatCommand(node, script, args, win = process.platform === 'win32', guard = '') {
|
|
69
74
|
const all = [node, script, ...args].join('');
|
|
70
75
|
if (/[\r\n\0]/.test(all)) return null;
|
|
76
|
+
const pre = guard ? ['-e'] : [];
|
|
71
77
|
if (win) {
|
|
72
78
|
const q = (s) => `'${s.replace(/['‘-‛]/g, '$&$&')}'`;
|
|
73
|
-
return ['&', q(node), q(script), ...args].join(' ');
|
|
79
|
+
return ['&', q(node), ...pre, ...(guard ? [q(guard)] : []), q(script), ...args].join(' ');
|
|
74
80
|
}
|
|
75
81
|
if (/["$`\\]/.test(all)) return null;
|
|
76
|
-
return [`"${node}"`, `"${script}"`, ...args].join(' ');
|
|
82
|
+
return [`"${node}"`, ...pre, ...(guard ? [`"${guard}"`] : []), `"${script}"`, ...args].join(' ');
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
// npm 길 훅 명령이 스크립트를 부르기 전에 하는 검사(Codex 플러그인 훅의 래퍼와 같은 규칙): 스크립트의 실제 경로가 임시
|
|
86
|
+
// 폴더나 세션 폴더(Codex 가 훅을 도는 폴더) 아래면 돌리지 않고 까닭을 알린다. 세션 폴더가 홈 폴더 자체면 세션 폴더
|
|
87
|
+
// 규칙은 쓰지 않는다. Codex 홈이 세션 폴더 깊은 곳(CODEX_HOME=<세션>/deep/.codex)이면 샌드박스가 그 사본을 고칠 수
|
|
88
|
+
// 있기 때문이다. 검사는 신뢰 해시에 묶인 명령 글에 있다. 세션 시작 훅은 systemMessage 로, 다른 훅은 stderr 로 알린다.
|
|
89
|
+
// 실제 경로는 realpathSync.native 로 푼다: Windows 의 JS 판은 8.3 짧은 이름의 임시 폴더(TEMP=C:\Users\LONGNA~1\...)를
|
|
90
|
+
// 긴 이름으로 풀지 않아 임시 폴더 규칙이 빗나간다(Windows 박스 실측).
|
|
91
|
+
export function hookGuard(notice) {
|
|
92
|
+
const say = notice
|
|
93
|
+
? 'process.stdout.write(JSON.stringify({systemMessage:m}))'
|
|
94
|
+
: 'process.stderr.write(m+String.fromCharCode(10))';
|
|
95
|
+
return [
|
|
96
|
+
"const f=require('fs'),p=require('path'),o=require('os');",
|
|
97
|
+
'const r=(x)=>{try{return f.realpathSync.native(x)}catch{try{return f.realpathSync(x)}catch{return x}}};',
|
|
98
|
+
'const s=r(process.argv[1]),w=r(process.cwd()),h=r(o.homedir());',
|
|
99
|
+
"const u=(d)=>{const q=p.relative(d,s);return q===''||!(q.startsWith('..')||p.isAbsolute(q))};",
|
|
100
|
+
"if([o.tmpdir()].concat(process.platform==='win32'?[]:['/tmp']).map(r).some(u)||(w!==h&&u(w))){",
|
|
101
|
+
"const m='banker hooks skipped: the hook script is in a temp folder or the session folder ('+s+')';",
|
|
102
|
+
`${say};process.exit(0)}`,
|
|
103
|
+
"import(require('url').pathToFileURL(s).href)",
|
|
104
|
+
].join('');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const hookCommand = (p, id, node, win = process.platform === 'win32') => formatCommand(node, scriptOf(p, id), HOOKS[id].args, win, hookGuard(HOOKS[id].event === 'SessionStart'));
|
|
108
|
+
|
|
79
109
|
// 임시 폴더의 node 는 거부한다. Codex 안에서 돌 때는 작업 폴더 안의 node 도(샌드박스 안 세션이 고칠 수 있다).
|
|
80
|
-
export function safeNode(node, { win = process.platform === 'win32', cwd = process.cwd(), codex = Boolean(process.env.CODEX_THREAD_ID) } = {}) {
|
|
110
|
+
export function safeNode(node, { win = process.platform === 'win32', cwd = process.cwd(), codex = Boolean(process.env.CODEX_THREAD_ID), home = os.homedir() } = {}) {
|
|
81
111
|
const P = win ? path.win32 : path.posix;
|
|
82
112
|
const under = (f, dir) => {
|
|
83
113
|
const r = P.relative(dir, f);
|
|
@@ -85,14 +115,25 @@ export function safeNode(node, { win = process.platform === 'win32', cwd = proce
|
|
|
85
115
|
};
|
|
86
116
|
// A link elsewhere is judged where it leads too, so a link in a safe place cannot pin a node from the temp folder.
|
|
87
117
|
const places = [node, real(node)];
|
|
88
|
-
|
|
118
|
+
// A session in the home folder is no workspace to judge by (as homeInWorkspace): nvm keeps node under ~/.nvm. Only an
|
|
119
|
+
// absolute home names one (HOME=. would make every folder the home); path.relative compares as the OS does (case on Windows).
|
|
120
|
+
const isHome = P.isAbsolute(home) && P.relative(real(home), real(cwd)) === '';
|
|
121
|
+
const inWork = codex && !isHome;
|
|
122
|
+
return !places.some((f) => under(f, os.tmpdir()) || (inWork && under(f, cwd)));
|
|
89
123
|
}
|
|
90
124
|
|
|
125
|
+
// 실제 경로. 아직 없는 절대 경로는 있는 조상까지만 풀고 나머지를 붙인다(macOS 의 /var 는 /private/var 로 가는 링크라,
|
|
126
|
+
// 풀지 않으면 링크 아래의 없는 Codex 홈이 풀린 작업 폴더 밖으로 보인다). native 먼저: Windows 의 8.3 짧은 이름을 푼다.
|
|
91
127
|
function real(file) {
|
|
92
128
|
try {
|
|
93
|
-
return fs.realpathSync(file);
|
|
129
|
+
return fs.realpathSync.native(file);
|
|
94
130
|
} catch {
|
|
95
|
-
|
|
131
|
+
try {
|
|
132
|
+
return fs.realpathSync(file);
|
|
133
|
+
} catch {
|
|
134
|
+
const up = path.dirname(file);
|
|
135
|
+
return path.isAbsolute(file) && up !== file ? path.join(real(up), path.basename(file)) : file;
|
|
136
|
+
}
|
|
96
137
|
}
|
|
97
138
|
}
|
|
98
139
|
|
|
@@ -141,6 +182,49 @@ function saveHooks(p, doc) {
|
|
|
141
182
|
writeAtomic(p.hooks, `${JSON.stringify(doc, null, 2)}\n`);
|
|
142
183
|
}
|
|
143
184
|
|
|
185
|
+
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
186
|
+
|
|
187
|
+
// hooks.json 을 고치는 쪽(banker 와 Codex 전용 스킬 teamwork 의 엔진)이 함께 쓰는 잠금: hooks.json 옆의 폴더
|
|
188
|
+
// hooks.json.lock. 10초 넘은 잠금은 죽은 쓰기로 보고 치우고, 5초 안에 못 얻으면 바꾸지 않는다(까닭을 돌려줌).
|
|
189
|
+
// 치우지 못한 낡은 잠금(일반 파일, 비지 않은 폴더, 권한)도 기다림과 횟수에 들어, 끝없이 돌지 않고 그 까닭으로 끝난다.
|
|
190
|
+
export function withHooksLock(file, fn, onBusy) {
|
|
191
|
+
const lock = `${file}.lock`;
|
|
192
|
+
let stuck = '';
|
|
193
|
+
for (let i = 0; ; i++) {
|
|
194
|
+
try {
|
|
195
|
+
fs.mkdirSync(lock);
|
|
196
|
+
break;
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// Codex 홈이 아직 없으면 다툴 파일도 없다. 그 밖에 잠금을 만들 수 없으면 바꾸지 않는다.
|
|
199
|
+
if (e.code === 'ENOENT') return fn();
|
|
200
|
+
if (e.code !== 'EEXIST') return onBusy(`잠금 ${lock} 을 확인하지 못함(${e.code})`);
|
|
201
|
+
if (i > 200) return onBusy(stuck ? `${file} 의 낡은 잠금 ${lock} 을 지우지 못함(${stuck}). 다른 banker 나 teamwork 가 돌고 있지 않으면 직접 지우십시오` : `${file} 의 잠금(${lock})을 얻지 못함`);
|
|
202
|
+
stuck = clearStale(lock);
|
|
203
|
+
if (stuck !== '') pause(25);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return fn();
|
|
208
|
+
} finally {
|
|
209
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 낡은 잠금 치우기: 치웠거나 그사이 풀렸으면 ''(바로 다시 잡음), 10초가 안 된 잠금이면 null(기다림), 10초 넘었는데
|
|
214
|
+
// 지우지 못했으면 그 오류 코드(기다림, 마지막에 까닭으로 알림).
|
|
215
|
+
function clearStale(lock) {
|
|
216
|
+
try {
|
|
217
|
+
if (Date.now() - fs.statSync(lock).mtimeMs <= 10000) return null;
|
|
218
|
+
fs.rmdirSync(lock);
|
|
219
|
+
return '';
|
|
220
|
+
} catch (e) {
|
|
221
|
+
// Windows 의 rmdir 은 파일인 잠금에 ENOENT 를 내므로, 정말 없어졌는지는 다시 보고 정한다.
|
|
222
|
+
let st;
|
|
223
|
+
try { st = fs.lstatSync(lock); } catch { return ''; }
|
|
224
|
+
return st.isDirectory() ? String((e && e.code) || e) : 'ENOTDIR';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
144
228
|
// 사본 폴더. 링크인 폴더는 거부하고, 파일은 내용이 다를 때만 새로 쓴다.
|
|
145
229
|
export function installPayload(p, pkgRoot) {
|
|
146
230
|
fs.mkdirSync(p.dir, { recursive: true });
|
|
@@ -167,33 +251,52 @@ function place(doc, id, command) {
|
|
|
167
251
|
return true;
|
|
168
252
|
}
|
|
169
253
|
|
|
170
|
-
// Codex 홈이 지금
|
|
171
|
-
//
|
|
254
|
+
// Codex 홈이 지금 폴더 안이면(예: 폴더의 .envrc 가 CODEX_HOME=./.codex) 훅 사본도 작업 폴더 안에 놓여, 샌드박스 안의
|
|
255
|
+
// 세션이 샌드박스 밖에서 도는 훅을 고칠 수 있다. 홈 폴더와 파일 시스템 루트는 작업 폴더로 보지 않는다.
|
|
172
256
|
export function homeInWorkspace(home, cwd = process.cwd()) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
const skip = new Set([real(os.homedir()), path.parse(real(cwd)).root]);
|
|
257
|
+
const r = real(cwd);
|
|
258
|
+
if (r === real(os.homedir()) || r === path.parse(r).root) return false;
|
|
176
259
|
const h = real(home);
|
|
177
|
-
return
|
|
260
|
+
return h === r || h.startsWith(r.endsWith(path.sep) ? r : r + path.sep);
|
|
178
261
|
}
|
|
179
262
|
|
|
180
|
-
//
|
|
263
|
+
// Codex 홈에서 위로 올라가며 .git(폴더나 파일)이 있는 첫 폴더, 없으면 ''. git 을 실행하지 않는다. 사용자 홈 폴더 자체의
|
|
264
|
+
// .git(dotfiles 저장소)은 보지 않는다.
|
|
265
|
+
export function repoAbove(home, userHome = os.homedir()) {
|
|
266
|
+
const skip = real(userHome);
|
|
267
|
+
for (let d = real(home); ; d = path.dirname(d)) {
|
|
268
|
+
if (d !== skip && fs.existsSync(path.join(d, '.git'))) return d;
|
|
269
|
+
if (path.dirname(d) === d) return '';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ids 를 켠다. { code, changed:[id], msg, note }. code 1 이면 아무것도 바꾸지 않았다. note 는 알릴 것(Codex 홈 위의 저장소).
|
|
181
274
|
export function enableHooks(p, ids, pkgRoot, node = process.execPath, cwd = process.cwd()) {
|
|
275
|
+
return withHooksLock(p.hooks, () => enableLocked(p, ids, pkgRoot, node, cwd), (msg) => ({ code: 1, changed: [], msg }));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function enableLocked(p, ids, pkgRoot, node, cwd) {
|
|
182
279
|
if (!safeNode(node)) return { code: 1, changed: [], msg: `node 가 안전하지 않은 곳에 있음(${node})` };
|
|
183
280
|
if (homeInWorkspace(p.home, cwd)) return { code: 1, changed: [], msg: `Codex 홈(${p.home})이 작업 폴더 안이라 훅을 설치하지 않음. 샌드박스가 고칠 수 있는 곳임` };
|
|
184
281
|
const { doc, why } = loadHooks(p.hooks, ids.map((id) => HOOKS[id].event));
|
|
185
282
|
if (why) return { code: 1, changed: [], msg: why };
|
|
186
|
-
const cmds = ids.map((id) => [id,
|
|
283
|
+
const cmds = ids.map((id) => [id, hookCommand(p, id, node)]);
|
|
187
284
|
if (cmds.some(([, c]) => !c)) return { code: 1, changed: [], msg: '경로에 셸이 읽는 글자가 있음' };
|
|
188
285
|
const bad = installPayload(p, pkgRoot);
|
|
189
286
|
if (bad) return { code: 1, changed: [], msg: bad };
|
|
190
287
|
const changed = cmds.filter(([id, c]) => place(doc, id, c)).map(([id]) => id);
|
|
191
288
|
if (changed.length) saveHooks(p, doc);
|
|
192
|
-
|
|
289
|
+
const repo = repoAbove(p.home);
|
|
290
|
+
const note = repo ? `Codex 홈(${p.home})이 git 저장소(${repo}) 안에 있음. Codex 홈을 품은 폴더에서 Codex 를 열면 banker 훅은 돌지 않고 세션 시작 때 까닭을 알림` : '';
|
|
291
|
+
return { code: 0, changed, msg: '', note };
|
|
193
292
|
}
|
|
194
293
|
|
|
195
294
|
// ids 를 끈다: 우리 항목만 빼고, 비는 묶음과 이벤트를 지운다. 뒤 묶음의 번호가 밀리면 shifted 에 이벤트를 적는다.
|
|
196
295
|
export function disableHooks(p, ids) {
|
|
296
|
+
return withHooksLock(p.hooks, () => disableLocked(p, ids), (msg) => ({ code: 1, changed: [], shifted: [], msg }));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function disableLocked(p, ids) {
|
|
197
300
|
const { doc, why } = loadHooks(p.hooks, ids.map((id) => HOOKS[id].event));
|
|
198
301
|
if (why) return { code: 1, changed: [], shifted: [], msg: why };
|
|
199
302
|
const changed = [];
|
|
@@ -226,7 +329,7 @@ export function hookStatus(p, ids, pkgRoot, node = process.execPath) {
|
|
|
226
329
|
const groups = doc.hooks[HOOKS[id].event] || [];
|
|
227
330
|
const at = findOurs(groups, id);
|
|
228
331
|
if (!at) out[id] = 'off';
|
|
229
|
-
else out[id] = groups[at[0]].hooks[at[1]].command ===
|
|
332
|
+
else out[id] = groups[at[0]].hooks[at[1]].command === hookCommand(p, id, node) && copyCurrent(p, pkgRoot) ? 'on' : 'stale';
|
|
230
333
|
}
|
|
231
334
|
return { status: out };
|
|
232
335
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Written by scripts/sync-codex-plugin.js from codex/manifest.json (codexName). Do not edit.
|
|
2
|
+
// CODEX_NAMES: Claude name to Codex name; CLAUDE_NAMES: the other way.
|
|
3
|
+
export const CODEX_NAMES = Object.freeze({"omc-reference":"omx-reference","setup-omc":"setup-omx","setup-omc-hud":"setup-omx-hud"});
|
|
4
|
+
export const CLAUDE_NAMES = Object.freeze(Object.fromEntries(Object.entries(CODEX_NAMES).map(([a, b]) => [b, a])));
|