@kaydash9999/banker-plugins 0.20.2 → 0.21.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 +203 -1
- package/PRIVACY.md +34 -4
- package/README.md +46 -15
- package/bin/banker.js +228 -51
- package/bin/lib/codex-hooks.mjs +249 -0
- package/bin/lib/telemetry-config.mjs +33 -16
- package/codex/manifest.json +14 -11
- package/codex/skills/graceful-pause/SKILL.md +30 -0
- package/codex/skills/progress/SKILL.md +42 -0
- package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
- package/codex/skills/progress/scripts/progress.mjs +295 -0
- package/codex/transform-matrix.md +19 -14
- package/commands/setup.md +23 -0
- package/hooks/obsidize-hook.mjs +116 -4
- package/hooks/progress-plan.mjs +1 -1
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/telemetry-count-codex.mjs +222 -0
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +30 -17
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +11 -5
- package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-build/references/curate.mjs +48 -14
- package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
- package/skills/3d-intro-build/references/prompts.md +4 -4
- package/skills/3d-intro-build/references/serve.mjs +6 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
- package/skills/3d-intro-build/references/video-pool.mjs +250 -135
- package/skills/3d-intro-setup/SKILL.md +41 -10
- package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
- package/skills/anti-drm/SKILL.md +6 -3
- package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
- package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
- package/skills/append_wiki/SKILL.md +34 -8
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/compact-wiki/SKILL.md +37 -6
- package/skills/deep-research/SKILL.md +11 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lfm/SKILL.md +148 -0
- package/skills/lineage/SKILL.md +25 -15
- package/skills/lineage/lineage.py +564 -301
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-make/references/curate.mjs +48 -14
- package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
- package/skills/motion-graphic-make/references/serve.mjs +6 -1
- package/skills/motion-graphic-setup/SKILL.md +58 -13
- package/skills/obsidizer/SKILL.md +30 -11
- package/skills/obsidizer/obsidize.mjs +50 -12
- package/skills/omc-patch/SKILL.md +86 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +6 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +88 -12
- package/skills/payload-mon/scripts/payload-size.mjs +4 -1
- package/skills/pdf-vision-extract/SKILL.md +11 -6
- package/skills/ralph-qa/SKILL.md +125 -151
- package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
- package/skills/ralph-qa/references/payload-scan.mjs +22 -7
- package/skills/ralph-qa/references/seat-payload.mjs +646 -0
- package/skills/ralph-qa/references/seat-run.mjs +956 -0
- package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
- package/skills/rfp-author/SKILL.md +5 -3
- package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
- package/skills/setup-bypass-permissions/SKILL.md +27 -9
- package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
- package/skills/setup-harness-factory/SKILL.md +17 -1
- package/skills/setup-insane-search/SKILL.md +16 -0
- package/skills/setup-java/SKILL.md +20 -1
- package/skills/setup-lsp/SKILL.md +56 -13
- package/skills/setup-mcp/SKILL.md +73 -4
- package/skills/setup-node/SKILL.md +31 -6
- package/skills/setup-omc/SKILL.md +24 -0
- package/skills/setup-omc-hud/SKILL.md +42 -9
- package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
- package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
- package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
- package/skills/setup-playwright/SKILL.md +45 -2
- package/skills/setup-pwsh/SKILL.md +19 -0
- package/skills/setup-python/SKILL.md +45 -7
- package/skills/setup-sandbox/SKILL.md +30 -5
- package/skills/setup-stitch/SKILL.md +22 -0
- package/skills/setup-tmux/SKILL.md +24 -4
- package/skills/smart-compact/SKILL.md +58 -12
- package/skills/smart-compact/scripts/codex-gate.mjs +384 -0
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +12 -7
- package/skills/ultra-chain/references/preview-lib.mjs +51 -15
- package/skills/ultra-chain/references/serve.mjs +6 -1
- package/skills/ultra-interview/SKILL.md +14 -2
- package/skills/update-banker/SKILL.md +19 -3
- package/skills/vertical-pptx/SKILL.md +6 -3
- package/skills/vertical-pptx-setup/SKILL.md +76 -20
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* setup-bypass-permissions 의 Codex 대상. Claude Code 의 bypassPermissions 에 해당하는 Codex 설정을 켜고 끈다:
|
|
4
|
+
* `$CODEX_HOME/config.toml` 최상위의 approval_policy = "never", sandbox_mode = "danger-full-access"
|
|
5
|
+
* (Codex 의 --dangerously-bypass-approvals-and-sandbox 와 같은 조합).
|
|
6
|
+
*
|
|
7
|
+
* - 켜기 전 두 키의 값(없으면 없음)을 config.toml.bypass-permissions.json 에 기록하고, 원본 바이트를
|
|
8
|
+
* config.toml.bypass-permissions.bak 에 둔다. off 는 기록대로 되돌린다. 기록이 없으면 이 스크립트가 쓰는 값과
|
|
9
|
+
* 같은 키만 뺀다.
|
|
10
|
+
* - 조직 정책(/etc/codex/requirements.toml 의 allowed_approval_policies, allowed_sandbox_modes)이 이 값을 허용하지
|
|
11
|
+
* 않으면 켜지 않는다.
|
|
12
|
+
*
|
|
13
|
+
* 사용: node codex-bypass.mjs <on --yes|off|status>
|
|
14
|
+
* 종료: 0 완료 또는 이미 그 상태, 1 거부(파일은 그대로), 2 예기치 못한 오류, 3 이 계정이 쓸 수 없음(샌드박스 등)
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import { codexConfigFile, denied, eolOf, findKey, readToml, Refused, removeKey, setKey, tablesSetting, writeToml } from './codex-config.mjs';
|
|
21
|
+
|
|
22
|
+
const WANT = { approval_policy: '"never"', sandbox_mode: '"danger-full-access"' };
|
|
23
|
+
const KEYS = Object.keys(WANT);
|
|
24
|
+
const POLICY_FILES = ['/etc/codex/requirements.toml', '/etc/codex/managed_config.toml'];
|
|
25
|
+
|
|
26
|
+
const recordOf = (file) => `${file}.bypass-permissions.json`;
|
|
27
|
+
const backupOf = (file) => `${file}.bypass-permissions.bak`;
|
|
28
|
+
const valueOf = (text, key) => findKey(text, '', key)?.raw ?? null;
|
|
29
|
+
const isOn = (text) => KEYS.every((k) => valueOf(text, k) === WANT[k]);
|
|
30
|
+
|
|
31
|
+
// 조직 정책이 이 값을 막거나 정책 파일을 읽을 수 없으면 그 이유, 아니면 ''.
|
|
32
|
+
export function policyBlock(files = POLICY_FILES) {
|
|
33
|
+
for (const f of files) {
|
|
34
|
+
let text = '';
|
|
35
|
+
try {
|
|
36
|
+
text = fs.readFileSync(f, 'utf8');
|
|
37
|
+
} catch (e) {
|
|
38
|
+
if (e.code === 'ENOENT') continue;
|
|
39
|
+
return `조직 정책 파일을 읽을 수 없음(${e.code}): ${f}`;
|
|
40
|
+
}
|
|
41
|
+
for (const [key, want] of [['allowed_approval_policies', 'never'], ['allowed_sandbox_modes', 'danger-full-access']]) {
|
|
42
|
+
const hit = findKey(text, '', key);
|
|
43
|
+
if (hit && !new RegExp(`["']${want}["']`).test(hit.raw)) return `${f} 의 ${key} 가 ${want} 를 허용하지 않음`;
|
|
44
|
+
}
|
|
45
|
+
// 옛 managed_config.toml 은 값 자체를 덮어쓴다. 다른 값이면 사용자 설정이 적용되지 않는다.
|
|
46
|
+
for (const k of KEYS) {
|
|
47
|
+
const forced = findKey(text, '', k)?.raw;
|
|
48
|
+
if (forced && forced.replace(/'/g, '"') !== WANT[k]) return `${f} 가 ${k} 를 ${forced} 로 정함`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 최상위 profile 이 고른 기본 프로필이 두 키를 따로 정하면 그 값이 이긴다. 그 프로필 이름, 없으면 ''.
|
|
55
|
+
export function overridingProfile(text) {
|
|
56
|
+
const raw = findKey(text, '', 'profile')?.raw ?? '';
|
|
57
|
+
const m = /^"([^"]+)"$|^'([^']+)'$/.exec(raw);
|
|
58
|
+
if (!m) return '';
|
|
59
|
+
const name = m[1] ?? m[2];
|
|
60
|
+
const table = `profiles."${name}"`;
|
|
61
|
+
// [profiles.<이름>] 표의 키, 또는 최상위의 점 키 profiles.<이름>.<키>.
|
|
62
|
+
const sets = KEYS.some((k) => findKey(text, table, k) || findKey(text, '', `${table}.${k}`));
|
|
63
|
+
return sets ? `profiles.${name}` : '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function turnOn({ file = codexConfigFile(), policies = POLICY_FILES } = {}) {
|
|
67
|
+
const why = policyBlock(policies);
|
|
68
|
+
if (why) return { code: 1, msg: `켜지 않음: ${why}` };
|
|
69
|
+
const { text, mode, exists } = readToml(file);
|
|
70
|
+
const prof = overridingProfile(text);
|
|
71
|
+
if (prof) return { code: 1, msg: `켜지 않음: 기본 프로필 ${prof} 가 approval_policy 나 sandbox_mode 를 정해 최상위 값이 적용되지 않음. 그 프로필의 값을 직접 바꾸십시오` };
|
|
72
|
+
if (isOn(text)) return { code: 0, msg: '이미 켜져 있음' };
|
|
73
|
+
const before = Object.fromEntries(KEYS.map((k) => [k, valueOf(text, k)]));
|
|
74
|
+
let next = text;
|
|
75
|
+
for (const k of KEYS) next = setKey(next, '', k, WANT[k]);
|
|
76
|
+
if (!fs.existsSync(path.dirname(file))) return { code: 1, msg: `Codex 설정 폴더가 없음: ${path.dirname(file)}` };
|
|
77
|
+
// 켜져 있지 않은 상태에서 켜므로 지금 값이 되돌릴 값이다. 예전 기록이 남아 있어도(켬, 손으로 바꿈, 다시 켬) 지금 값으로 바꾼다.
|
|
78
|
+
// 설정을 먼저 쓰고 기록을 쓴다. 설정 쓰기가 실패하면 앞선 기록이 그대로 남는다.
|
|
79
|
+
const nl = eolOf(text);
|
|
80
|
+
writeToml(file, next.endsWith(nl) || !next ? next : `${next}${nl}`, { mode, backup: exists ? backupOf(file) : null });
|
|
81
|
+
writeToml(recordOf(file), `${JSON.stringify({ before, existed: exists })}\n`);
|
|
82
|
+
return { code: 0, msg: '켬: approval_policy = "never", sandbox_mode = "danger-full-access"' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function turnOff({ file = codexConfigFile() } = {}) {
|
|
86
|
+
const { text, mode, exists } = readToml(file);
|
|
87
|
+
let record = null;
|
|
88
|
+
try { record = JSON.parse(fs.readFileSync(recordOf(file), 'utf8')); } catch { /* 기록 없음 */ }
|
|
89
|
+
let next = text;
|
|
90
|
+
// 기록이 없으면 두 키가 모두 이 스크립트의 값일 때만 뺀다. 사용자가 스스로 둔 approval_policy = "never" 하나는 남긴다.
|
|
91
|
+
const ours = record || isOn(text);
|
|
92
|
+
for (const k of KEYS) {
|
|
93
|
+
const was = record?.before?.[k];
|
|
94
|
+
if (typeof was === 'string') next = setKey(next, '', k, was);
|
|
95
|
+
else if (ours) next = removeKey(next, '', k);
|
|
96
|
+
}
|
|
97
|
+
if (next !== text && exists) writeToml(file, next, { mode });
|
|
98
|
+
fs.rmSync(recordOf(file), { force: true });
|
|
99
|
+
return { code: 0, msg: next === text ? '이미 꺼져 있음' : record ? '끔: 켜기 전 값으로 되돌림' : '끔: 이 스크립트가 쓰는 두 값의 키를 뺌' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function status({ file = codexConfigFile(), policies = POLICY_FILES } = {}) {
|
|
103
|
+
const { text } = readToml(file);
|
|
104
|
+
const out = [`${isOn(text) ? '켜짐' : '꺼짐'}: approval_policy = ${valueOf(text, 'approval_policy') ?? '(없음)'}, sandbox_mode = ${valueOf(text, 'sandbox_mode') ?? '(없음)'}`];
|
|
105
|
+
const prof = overridingProfile(text);
|
|
106
|
+
if (prof) out.push(`기본 프로필 ${prof} 가 값을 정해 최상위 값이 적용되지 않음`);
|
|
107
|
+
const profiles = [...new Set(KEYS.flatMap((k) => tablesSetting(text, 'profiles', k)))];
|
|
108
|
+
if (profiles.length) out.push(`프로필이 따로 정함(그 프로필로 시작하면 그 값): ${profiles.join(', ')}`);
|
|
109
|
+
const why = policyBlock(policies);
|
|
110
|
+
if (why) out.push(`조직 정책: ${why}`);
|
|
111
|
+
if (fs.existsSync(recordOf(file))) out.push(`되돌리기 기록: ${recordOf(file)}`);
|
|
112
|
+
return { code: 0, msg: out.join('\n') };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function run(argv) {
|
|
116
|
+
const [cmd] = argv;
|
|
117
|
+
if (cmd === 'on' && !argv.includes('--yes')) return { code: 1, msg: '켜기는 확인을 받은 뒤 --yes 와 함께 실행' };
|
|
118
|
+
if (cmd === 'on') return turnOn();
|
|
119
|
+
if (cmd === 'off') return turnOff();
|
|
120
|
+
return status();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function main(argv) {
|
|
124
|
+
let r;
|
|
125
|
+
try {
|
|
126
|
+
r = run(argv);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
r = e instanceof Refused ? { code: 1, msg: `거부: ${e.message}` } : denied(e) ? { code: 3, msg: `쓸 수 없음(${e.code}): ${e.path ?? ''}${e.code === 'EPERM' && process.platform === 'win32' ? '. 다른 프로그램(Codex, 편집기)이 파일을 잡고 있을 수 있음' : ''}` } : { code: 2, msg: `오류: ${e.message}` };
|
|
129
|
+
}
|
|
130
|
+
console.log(`${r.msg}\n설정 파일: ${codexConfigFile()}`);
|
|
131
|
+
return r.code;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const same = (a, b) => { try { return fs.realpathSync(a) === fs.realpathSync(b); } catch { return false; } };
|
|
135
|
+
if (process.argv[1] && same(fileURLToPath(import.meta.url), path.resolve(process.argv[1]))) process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Codex 의 config.toml 을 줄 단위로 읽고 고치는 공통 모듈. setup-bypass-permissions 가 정본이고 setup-omc-hud 의
|
|
3
|
+
* assets/ 에 같은 사본을 둔다(scripts/sync-adapter.js 가 지킴).
|
|
4
|
+
*
|
|
5
|
+
* TOML 전체를 다시 쓰지 않는다. 바꾸는 키의 줄만 고쳐 주석, 순서, 다른 표는 그대로 둔다.
|
|
6
|
+
* - 줄을 훑으며 문자열(한 줄, 여러 줄)과 괄호 깊이를 따라가, 여러 줄 문자열이나 배열 안의 줄을 표 머리나 키로 보지 않는다.
|
|
7
|
+
* - 키 이름은 따옴표를 벗겨 비교한다(`"approval_policy"` 도 approval_policy). 값 끝의 주석은 값으로 보지 않는다.
|
|
8
|
+
* - 최상위가 표를 점 키나 인라인 표로 정했는지(definedInline) 알려, 그런 파일에 같은 표를 더하지 않게 한다.
|
|
9
|
+
* - 파일이 링크이거나 일반 파일이 아니면 고치지 않는다. 쓰기 전 원본을 백업(이 계정만 읽음)하고, 새 파일에 쓴 뒤
|
|
10
|
+
* 바꿔 넣으며 파일 모드를 지킨다.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
// Codex 처럼 상대 경로 CODEX_HOME 은 지금 폴더 기준으로 푼다.
|
|
17
|
+
export function codexConfigFile(env = process.env) {
|
|
18
|
+
const home = env.CODEX_HOME ? path.resolve(env.CODEX_HOME) : path.join(os.homedir(), '.codex');
|
|
19
|
+
return path.join(home, 'config.toml');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Refused extends Error {}
|
|
23
|
+
|
|
24
|
+
// { text, mode, exists }. 없으면 빈 글. 링크나 일반 파일이 아니면 Refused.
|
|
25
|
+
export function readToml(file) {
|
|
26
|
+
let st;
|
|
27
|
+
try {
|
|
28
|
+
st = fs.lstatSync(file);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e.code === 'ENOENT') return { text: '', mode: null, exists: false };
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
if (!st.isFile()) throw new Refused(`${file} 이 일반 파일이 아님(링크이거나 다른 종류)`);
|
|
34
|
+
return { text: fs.readFileSync(file, 'utf8'), mode: st.mode & 0o7777, exists: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 원본을 backup 에 복사한 뒤(원본이 있을 때) 새 파일에 써서 바꿔 넣는다.
|
|
38
|
+
export function writeToml(file, text, { mode = null, backup = null } = {}) {
|
|
39
|
+
if (backup && fs.existsSync(file)) {
|
|
40
|
+
fs.copyFileSync(file, backup);
|
|
41
|
+
fs.chmodSync(backup, 0o600); // 원본에 비밀 값이 있을 수 있다
|
|
42
|
+
}
|
|
43
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(tmp, text, { flag: 'wx', mode: mode ?? 0o600 });
|
|
46
|
+
if (mode !== null) fs.chmodSync(tmp, mode);
|
|
47
|
+
fs.renameSync(tmp, file);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
fs.rmSync(tmp, { force: true });
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = (text) => text.split(/\r?\n/);
|
|
55
|
+
const eol = (text) => (text.includes('\r\n') ? '\r\n' : '\n');
|
|
56
|
+
// 파일이 쓰는 줄바꿈(CRLF 가 하나라도 있으면 CRLF).
|
|
57
|
+
export const eolOf = eol;
|
|
58
|
+
const TRIPLE = ['"""', "'''"];
|
|
59
|
+
|
|
60
|
+
// 키나 표 이름을 낱말 배열로. 따옴표 안의 점은 나누지 않는다(`profiles."a.b"` 는 ['profiles', 'a.b']).
|
|
61
|
+
export const nameParts = (s) => (s.match(/"[^"]*"|'[^']*'|[^.\s]+/g) ?? []).map((p) => p.replace(/^"(.*)"$|^'(.*)'$/, '$1$2'));
|
|
62
|
+
// 비교용 이름. 낱말 사이를 \u0000 으로 이어, 점이 든 낱말과 점으로 나뉜 낱말을 구분한다.
|
|
63
|
+
const plainName = (s) => nameParts(s).join('\u0000');
|
|
64
|
+
|
|
65
|
+
// 한 줄을 문자열 밖에서 훑는다. { d: 괄호 깊이 변화, multi: 줄 끝에 열려 있는 여러 줄 문자열 구분자, cut: 주석 시작 위치 }.
|
|
66
|
+
function scanLine(s, multi = '') {
|
|
67
|
+
let d = 0;
|
|
68
|
+
for (let i = 0; i < s.length; i++) {
|
|
69
|
+
if (multi) {
|
|
70
|
+
if (s.startsWith(multi, i)) { i += 2; multi = ''; } else if (multi === TRIPLE[0] && s[i] === '\\') i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const c = s[i];
|
|
74
|
+
if (TRIPLE.some((t) => s.startsWith(t, i))) { multi = s.slice(i, i + 3); i += 2; }
|
|
75
|
+
else if (c === '"') { for (i++; i < s.length && s[i] !== '"'; i++) if (s[i] === '\\') i++; }
|
|
76
|
+
else if (c === "'") { const k = s.indexOf("'", i + 1); i = k < 0 ? s.length : k; }
|
|
77
|
+
else if (c === '#') return { d, multi, cut: i };
|
|
78
|
+
else if (c === '[' || c === '{') d++;
|
|
79
|
+
else if (c === ']' || c === '}') d--;
|
|
80
|
+
}
|
|
81
|
+
return { d, multi, cut: s.length };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 표 머리. 이름 안의 따옴표 친 낱말에는 ] 가 들어갈 수 있다(`[projects."/a]b"]`).
|
|
85
|
+
// 이름 부분은 탐욕 한 번으로 읽는다(겹치는 공백 패턴은 긴 줄에서 역추적이 폭증한다). 앞뒤 공백은 nameParts 가 지운다.
|
|
86
|
+
const HEADER_RE = /^\s*\[\[?((?:"[^"]*"|'[^']*'|[^\]"'])+)\]\]?\s*(#.*)?$/;
|
|
87
|
+
const KEY_RE = /^\s*((?:"[^"]*"|'[^']*'|[A-Za-z0-9_-]+)(?:\s*\.\s*(?:"[^"]*"|'[^']*'|[A-Za-z0-9_-]+))*)\s*=(.*)$/;
|
|
88
|
+
|
|
89
|
+
// 줄마다 { kind: 'header'|'key'|'inside'|'other', name, end }. header 의 name 은 표 이름, key 의 name 은 키 이름이고
|
|
90
|
+
// end 는 값이 끝나는 줄 다음. 여러 줄 문자열이나 배열 안의 줄은 inside.
|
|
91
|
+
export function scan(text) {
|
|
92
|
+
const ls = lines(text);
|
|
93
|
+
const out = ls.map(() => ({ kind: 'other' }));
|
|
94
|
+
for (let i = 0; i < ls.length; i++) {
|
|
95
|
+
const header = HEADER_RE.exec(ls[i]);
|
|
96
|
+
if (header) { out[i] = { kind: 'header', name: plainName(header[1]) }; continue; }
|
|
97
|
+
const key = KEY_RE.exec(ls[i]);
|
|
98
|
+
if (!key) continue;
|
|
99
|
+
let { d, multi } = scanLine(key[2]);
|
|
100
|
+
let j = i + 1;
|
|
101
|
+
for (; (d > 0 || multi) && j < ls.length; j++) {
|
|
102
|
+
out[j] = { kind: 'inside' };
|
|
103
|
+
const r = scanLine(ls[j], multi);
|
|
104
|
+
d += r.d;
|
|
105
|
+
multi = r.multi;
|
|
106
|
+
}
|
|
107
|
+
out[i] = { kind: 'key', name: plainName(key[1]), end: j };
|
|
108
|
+
i = j - 1;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 표의 줄 범위 [from, to). table '' 은 최상위. 표가 없으면 null.
|
|
114
|
+
function tableRange(info, table) {
|
|
115
|
+
table = table.includes('\u0000') || !table ? table : plainName(table);
|
|
116
|
+
let from = table ? -1 : 0;
|
|
117
|
+
for (let i = 0; i < info.length; i++) {
|
|
118
|
+
if (info[i].kind !== 'header') continue;
|
|
119
|
+
if (from >= 0) return [from, i];
|
|
120
|
+
if (info[i].name === table) from = i + 1;
|
|
121
|
+
}
|
|
122
|
+
return from >= 0 ? [from, info.length] : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function keysOf(text, table) {
|
|
126
|
+
const info = scan(text);
|
|
127
|
+
const range = tableRange(info, table);
|
|
128
|
+
if (!range) return [];
|
|
129
|
+
const keys = [];
|
|
130
|
+
for (let i = range[0]; i < range[1]; i++) if (info[i].kind === 'key') keys.push({ at: i, end: info[i].end, name: info[i].name });
|
|
131
|
+
return keys;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 키의 { at, end, raw }. raw 는 값 끝의 주석을 뺀 글. 없으면 null.
|
|
135
|
+
export function findKey(text, table, key) {
|
|
136
|
+
const want = nameParts(key).join('\u0000');
|
|
137
|
+
const hit = keysOf(text, plainName(table)).find((k) => k.name === want);
|
|
138
|
+
if (!hit) return null;
|
|
139
|
+
const ls = lines(text).slice(hit.at, hit.end);
|
|
140
|
+
const parts = [ls[0].slice(ls[0].indexOf('=') + 1), ...ls.slice(1)];
|
|
141
|
+
let multi = '';
|
|
142
|
+
for (const part of parts.slice(0, -1)) multi = scanLine(part, multi).multi;
|
|
143
|
+
const last = parts.length - 1;
|
|
144
|
+
parts[last] = parts[last].slice(0, scanLine(parts[last], multi).cut); // 마지막 줄의 주석을 뗀다
|
|
145
|
+
const raw = parts.join('\n');
|
|
146
|
+
return { at: hit.at, end: hit.end, raw: raw.trim() };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 최상위가 이 표를 점 키나 인라인 표로 정했는지(`tui.x = 1`, `tui = { ... }`). 그런 파일에 [표] 를 더하면 키가 겹쳐
|
|
150
|
+
// Codex 가 설정을 읽지 못한다.
|
|
151
|
+
export function definedInline(text, table) {
|
|
152
|
+
const t = plainName(table);
|
|
153
|
+
return keysOf(text, '').some((k) => k.name === t || k.name.startsWith(`${t}\u0000`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 키를 rawValue 로 둔다. 있으면 그 줄을 바꾸고, 없으면 표 끝(최상위는 첫 표 머리 앞)에 넣는다. 표가 없으면 끝에 만든다.
|
|
157
|
+
// comment 가 있으면 키 바로 위에 그 주석 줄을 둔다.
|
|
158
|
+
export function setKey(text, table, key, rawValue, comment = '') {
|
|
159
|
+
const ls = lines(text);
|
|
160
|
+
const nl = eol(text);
|
|
161
|
+
const line = [...(comment ? [comment] : []), `${key} = ${rawValue}`];
|
|
162
|
+
const hit = findKey(text, table, key);
|
|
163
|
+
if (hit) {
|
|
164
|
+
const above = comment && ls[hit.at - 1] === comment ? 1 : 0;
|
|
165
|
+
ls.splice(hit.at - above, hit.end - hit.at + above, ...line);
|
|
166
|
+
return ls.join(nl);
|
|
167
|
+
}
|
|
168
|
+
const range = tableRange(scan(text), table);
|
|
169
|
+
if (!range) {
|
|
170
|
+
while (ls.length && ls[ls.length - 1] === '') ls.pop();
|
|
171
|
+
return [...ls, ...(ls.length ? [''] : []), `[${table}]`, ...line, ''].join(nl);
|
|
172
|
+
}
|
|
173
|
+
// 다음 표 머리에 붙은 주석(`# MCP servers` 처럼 머리 바로 위의 주석 줄)과 그 앞 빈 줄은 건너뛰고 넣는다.
|
|
174
|
+
let at = range[1];
|
|
175
|
+
if (range[1] < ls.length) while (at > range[0] && ls[at - 1].trim().startsWith('#')) at--;
|
|
176
|
+
while (at > range[0] && ls[at - 1].trim() === '') at--;
|
|
177
|
+
ls.splice(at, 0, ...line);
|
|
178
|
+
return ls.join(nl);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 키를 뺀다. comment 와 같은 주석 줄이 바로 위에 있으면 그것도 뺀다.
|
|
182
|
+
export function removeKey(text, table, key, comment = '') {
|
|
183
|
+
const hit = findKey(text, table, key);
|
|
184
|
+
if (!hit) return text;
|
|
185
|
+
const ls = lines(text);
|
|
186
|
+
const above = comment && ls[hit.at - 1] === comment ? 1 : 0;
|
|
187
|
+
ls.splice(hit.at - above, hit.end - hit.at + above);
|
|
188
|
+
return ls.join(eol(text));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 표에 키도 주석도 없으면 그 머리와 뒤의 빈 줄을 뺀다.
|
|
192
|
+
export function dropEmptyTable(text, table) {
|
|
193
|
+
const info = scan(text);
|
|
194
|
+
const range = tableRange(info, table);
|
|
195
|
+
if (!range) return text;
|
|
196
|
+
const ls = lines(text);
|
|
197
|
+
if (ls.slice(range[0], range[1]).some((l) => l.trim())) return text;
|
|
198
|
+
ls.splice(range[0] - 1, range[1] - range[0] + 1);
|
|
199
|
+
return ls.join(eol(text));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 이름이 prefix 로 시작하는 표들 중 key 를 정한 표 이름(예: profiles.* 의 approval_policy).
|
|
203
|
+
export function tablesSetting(text, prefix, key) {
|
|
204
|
+
const p = plainName(prefix);
|
|
205
|
+
const names = scan(text).filter((x) => x.kind === 'header' && x.name.startsWith(`${p}\u0000`)).map((x) => x.name);
|
|
206
|
+
return [...new Set(names)].filter((n) => findKey(text, n, key)).map((n) => n.split('\u0000').join('.'));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 쓰기 실패가 이 계정의 권한(샌드박스, 읽기 전용, 다른 소유자) 때문인지.
|
|
210
|
+
export const denied = (e) => ['EACCES', 'EPERM', 'EROFS'].includes(e?.code);
|
|
@@ -26,6 +26,22 @@ ls -la .claude/agents .claude/skills 2>/dev/null # 기존 harness 산출물
|
|
|
26
26
|
- 기존 `.claude/agents/`·`.claude/skills/`·`CLAUDE.md`에 harness 흔적이 있으면 신규 생성이 아니라
|
|
27
27
|
확장·유지보수로 다룬다(harness 자신의 Phase 0 감사 원칙).
|
|
28
28
|
|
|
29
|
+
### 네트워크 확인 (내려받기 전)
|
|
30
|
+
|
|
31
|
+
이 스킬이 내려받는 원천에 10초 안에 닿는지 먼저 본다. 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
35
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://github.com # 마켓플레이스, git clone
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `curl -sSI` 는 머리만 받는다(본문을 받지 않음). 닿지 못하면 종료 코드가 0 이 아니다(HTTP 상태와 관계없음). `curl` 은 `HTTPS_PROXY` 를 따른다. 확인 줄은 `--ssl-no-revoke` 를 준다. Windows 의 Git Bash curl 도 Schannel 이라, 폐지 목록 서버에 닿지 못하는 망에서 폐지 확인이 실패하면 종료 60 이다(Windows 박스 실측). 다른 curl 은 이 깃발을 받고 무시한다(macOS curl 8.7.1, Rocky 8.10 curl 7.61.1 실측).
|
|
39
|
+
- Windows PowerShell 에서는 같은 주소를 `curl.exe --ssl-no-revoke -sSI -o NUL --connect-timeout 5 --max-time 10 <주소>` 로 보고 `$LASTEXITCODE` 가 0 인지 본다(PowerShell 5.1 의 `curl` 은 `Invoke-WebRequest` 별칭이다). `curl.exe` 는 Windows 시스템 프록시를 따르지 않고 `HTTPS_PROXY` 만 따른다. `--ssl-no-revoke` 는 폐지 목록 서버에 닿지 못하는 망에서 Schannel 의 폐지 확인 실패를 오프라인으로 읽지 않게 한다. 닿는지만 보고 받은 내용은 쓰지 않으므로 이 확인에만 준다. `curl.exe` 가 없으면 `$LASTEXITCODE` 에 앞 명령의 값이 남으므로 `Get-Command curl.exe -ErrorAction SilentlyContinue` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다.
|
|
40
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
41
|
+
- Codex sandbox 가 네트워크를 막는 모드(기본 `workspace-write`, `read-only`)에서는 `CODEX_SANDBOX_NETWORK_DISABLED=1` 이 서고 확인이 모두 실패한다(curl 종료 7, npm `EPERM`). 블록의 첫 줄이 그렇다고 알린다. 그때는 '오프라인' 이 아니라 'sandbox 가 네트워크를 막음' 으로 보고하고, 승인을 받아 sandbox 밖에서 확인부터 다시 한다. Claude Code sandbox 안(`SANDBOX_RUNTIME=1`)에서는 허용 목록 밖 주소를 sandbox 프록시가 막는다. Windows PowerShell 에서는 `$env:CODEX_SANDBOX_NETWORK_DISABLED -eq '1'` 로 본다.
|
|
42
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
43
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
44
|
+
|
|
29
45
|
## 1. 설치
|
|
30
46
|
### Claude Code
|
|
31
47
|
세션 안(슬래시 커맨드):
|
|
@@ -43,7 +59,7 @@ ls -la .claude/agents .claude/skills 2>/dev/null # 기존 harness 산출물
|
|
|
43
59
|
### Codex CLI
|
|
44
60
|
마켓플레이스 개념이 없고 Python 인스톨러를 쓴다:
|
|
45
61
|
```bash
|
|
46
|
-
git clone https://github.com/SaehwanPark/meta-harness
|
|
62
|
+
git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30 clone https://github.com/SaehwanPark/meta-harness
|
|
47
63
|
cd meta-harness
|
|
48
64
|
python3 scripts/install_harness.py --scope user --layout codex
|
|
49
65
|
# 프로젝트 범위: python3 scripts/install_harness.py --scope project --target <repo> --layout codex
|
|
@@ -8,6 +8,22 @@ description: "차단이나 봇 방어가 있는 사이트도 우회해 검색하
|
|
|
8
8
|
차단 사이트 우회(WAF·봇방어; X·Reddit·YouTube·GitHub 등) 검색·페치를 제공하는 `insane-search` 플러그인을
|
|
9
9
|
설치한다. 현재 도구(Claude Code / Codex)를 감지해 알맞은 경로로 진행하고, 이미 설치돼 있으면 skip(멱등). 답변은 한글.
|
|
10
10
|
|
|
11
|
+
## 0. 네트워크 확인 (내려받기 전)
|
|
12
|
+
|
|
13
|
+
이 스킬이 내려받는 원천에 10초 안에 닿는지 먼저 본다. 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
17
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://github.com # 마켓플레이스(GitHub)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `curl -sSI` 는 머리만 받는다(본문을 받지 않음). 닿지 못하면 종료 코드가 0 이 아니다(HTTP 상태와 관계없음). `curl` 은 `HTTPS_PROXY` 를 따른다. 확인 줄은 `--ssl-no-revoke` 를 준다. Windows 의 Git Bash curl 도 Schannel 이라, 폐지 목록 서버에 닿지 못하는 망에서 폐지 확인이 실패하면 종료 60 이다(Windows 박스 실측). 다른 curl 은 이 깃발을 받고 무시한다(macOS curl 8.7.1, Rocky 8.10 curl 7.61.1 실측).
|
|
21
|
+
- Windows PowerShell 에서는 같은 주소를 `curl.exe --ssl-no-revoke -sSI -o NUL --connect-timeout 5 --max-time 10 <주소>` 로 보고 `$LASTEXITCODE` 가 0 인지 본다(PowerShell 5.1 의 `curl` 은 `Invoke-WebRequest` 별칭이다). `curl.exe` 는 Windows 시스템 프록시를 따르지 않고 `HTTPS_PROXY` 만 따른다. `--ssl-no-revoke` 는 폐지 목록 서버에 닿지 못하는 망에서 Schannel 의 폐지 확인 실패를 오프라인으로 읽지 않게 한다. 닿는지만 보고 받은 내용은 쓰지 않으므로 이 확인에만 준다. `curl.exe` 가 없으면 `$LASTEXITCODE` 에 앞 명령의 값이 남으므로 `Get-Command curl.exe -ErrorAction SilentlyContinue` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다.
|
|
22
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
23
|
+
- Codex sandbox 가 네트워크를 막는 모드(기본 `workspace-write`, `read-only`)에서는 `CODEX_SANDBOX_NETWORK_DISABLED=1` 이 서고 확인이 모두 실패한다(curl 종료 7, npm `EPERM`). 블록의 첫 줄이 그렇다고 알린다. 그때는 '오프라인' 이 아니라 'sandbox 가 네트워크를 막음' 으로 보고하고, 승인을 받아 sandbox 밖에서 확인부터 다시 한다. Claude Code sandbox 안(`SANDBOX_RUNTIME=1`)에서는 허용 목록 밖 주소를 sandbox 프록시가 막는다. Windows PowerShell 에서는 `$env:CODEX_SANDBOX_NETWORK_DISABLED -eq '1'` 로 본다.
|
|
24
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
25
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
26
|
+
|
|
11
27
|
## Claude Code · 세션 안 (슬래시 커맨드)
|
|
12
28
|
```
|
|
13
29
|
/plugin marketplace add https://github.com/fivetaku/gptaku_plugins.git
|
|
@@ -26,6 +26,25 @@ if (Get-Command java -ErrorAction SilentlyContinue) { java -version } else { "ja
|
|
|
26
26
|
[Environment]::GetEnvironmentVariable('JAVA_HOME','User')
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
### 네트워크 확인 (내려받기 전)
|
|
30
|
+
|
|
31
|
+
이 스킬이 내려받는 원천에 10초 안에 닿는지 먼저 본다. 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
35
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://packages.adoptium.net # Debian 12 의 Adoptium 저장소
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `curl -sSI` 는 머리만 받는다(본문을 받지 않음). 닿지 못하면 종료 코드가 0 이 아니다(HTTP 상태와 관계없음). `curl` 은 `HTTPS_PROXY` 를 따른다. 확인 줄은 `--ssl-no-revoke` 를 준다. Windows 의 Git Bash curl 도 Schannel 이라, 폐지 목록 서버에 닿지 못하는 망에서 폐지 확인이 실패하면 종료 60 이다(Windows 박스 실측). 다른 curl 은 이 깃발을 받고 무시한다(macOS curl 8.7.1, Rocky 8.10 curl 7.61.1 실측).
|
|
39
|
+
- Windows PowerShell 에서는 같은 주소를 `curl.exe --ssl-no-revoke -sSI -o NUL --connect-timeout 5 --max-time 10 <주소>` 로 보고 `$LASTEXITCODE` 가 0 인지 본다(PowerShell 5.1 의 `curl` 은 `Invoke-WebRequest` 별칭이다). `curl.exe` 는 Windows 시스템 프록시를 따르지 않고 `HTTPS_PROXY` 만 따른다. `--ssl-no-revoke` 는 폐지 목록 서버에 닿지 못하는 망에서 Schannel 의 폐지 확인 실패를 오프라인으로 읽지 않게 한다. 닿는지만 보고 받은 내용은 쓰지 않으므로 이 확인에만 준다. `curl.exe` 가 없으면 `$LASTEXITCODE` 에 앞 명령의 값이 남으므로 `Get-Command curl.exe -ErrorAction SilentlyContinue` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다.
|
|
40
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
41
|
+
- OS 패키지 관리자(dnf, apt, brew, winget)의 단계는 자기 설정의 미러를 쓰므로 이 확인과 관계없이 시도하고, 실패하면 그 단계를 실패로 보고한다.
|
|
42
|
+
- 패킷을 버리거나 답하지 않는 망에서 dnf 는 저장소마다 약 2분(SYN 을 버리는 망 실측 121초), apt 는 원천마다 약 4분(답하지 않는 원천 실측 247초) 기다린 뒤 실패로 끝난다. 시간 제한은 관리자 설정을 따르므로 이 스킬이 바꾸지 않는다.
|
|
43
|
+
- `apt-get update` 는 받지 못해도 종료 0 이다. 출력의 `W: Failed to fetch` 가 있으면 그 단계를 실패로 보고한다.
|
|
44
|
+
- Codex sandbox 가 네트워크를 막는 모드(기본 `workspace-write`, `read-only`)에서는 `CODEX_SANDBOX_NETWORK_DISABLED=1` 이 서고 확인이 모두 실패한다(curl 종료 7, npm `EPERM`). 블록의 첫 줄이 그렇다고 알린다. 그때는 '오프라인' 이 아니라 'sandbox 가 네트워크를 막음' 으로 보고하고, 승인을 받아 sandbox 밖에서 확인부터 다시 한다. Claude Code sandbox 안(`SANDBOX_RUNTIME=1`)에서는 허용 목록 밖 주소를 sandbox 프록시가 막는다. Windows PowerShell 에서는 `$env:CODEX_SANDBOX_NETWORK_DISABLED -eq '1'` 로 본다.
|
|
45
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
46
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
47
|
+
|
|
29
48
|
## 1. Rocky Linux 8
|
|
30
49
|
```bash
|
|
31
50
|
sudo dnf install -y java-21-openjdk-devel
|
|
@@ -51,7 +70,7 @@ export JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")
|
|
|
51
70
|
⚠️ **default·backports 저장소 모두 JDK 21을 제공하지 않는다.** Adoptium 레포 추가가 유일한 경로(재확인: 배포판이 이후 21을 backport하면 이 단계는 생략 가능).
|
|
52
71
|
```bash
|
|
53
72
|
sudo apt-get install -y wget gnupg
|
|
54
|
-
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | \
|
|
73
|
+
wget -qO - --timeout=10 --tries=2 https://packages.adoptium.net/artifactory/api/gpg/key/public | \
|
|
55
74
|
sudo gpg --dearmor -o /usr/share/keyrings/adoptium.gpg
|
|
56
75
|
echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb bookworm main" | \
|
|
57
76
|
sudo tee /etc/apt/sources.list.d/adoptium.list
|
|
@@ -40,6 +40,44 @@ ls src/main/resources/application.* >/dev/null 2>&1 && echo "lang: spring"
|
|
|
40
40
|
```
|
|
41
41
|
> Windows(PowerShell): `Get-Command claude,codex,npm,pipx,java`, `Test-Path tsconfig.json,pom.xml` 등으로 동일 감지. jdtls 는 `jdtls.bat`.
|
|
42
42
|
|
|
43
|
+
### 네트워크 확인 (내려받기 전)
|
|
44
|
+
|
|
45
|
+
이 스킬이 내려받는 원천에 닿는지 먼저 본다(curl 은 10초, npm 은 15초 안). 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
49
|
+
in15() { "$@" & p=$!; ( sleep 15 && kill "$p" ) > /dev/null 2>&1 & k=$!; wait "$p" 2>/dev/null; r=$?; kill "$k" 2>/dev/null; wait "$k" 2>/dev/null; [ "$r" = 143 ] && echo "15초 안에 끝나지 않아 멈춤(패킷을 버리는 망일 수 있음)"; return "$r"; }
|
|
50
|
+
in15 sh -c 'cd ~ && exec npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier' # ts, bash 서버(npm)
|
|
51
|
+
# pip 이 읽는 설정 전부(PIP_CONFIG_FILE 포함)를 홈 폴더에서 읽는다. install 절이 global 절보다 앞선다
|
|
52
|
+
L=$( (cd ~ && { pip config list || pip3 config list || python3.13 -m pip config list || python3.12 -m pip config list || python3.11 -m pip config list || python3 -m pip config list; }) 2>/dev/null) || echo "pip 설정을 읽지 못함(pip 이 없거나 10 미만). 사내 미러를 쓰면 확인 주소를 그 미러로 바꿔 본다"
|
|
53
|
+
I="${PIP_INDEX_URL:-$(printf '%s\n' "$L" | sed -n "s/^install\.index-url='\(.*\)'$/\1/p")}"
|
|
54
|
+
I="${I:-$(printf '%s\n' "$L" | sed -n "s/^global\.index-url='\(.*\)'$/\1/p")}"
|
|
55
|
+
I=$(printf %s "${I:-https://pypi.org/simple}" | sed -E 's#^([A-Za-z][A-Za-z0-9+.-]*://)[^/]*@#\1#; s#/$##') # 주소의 사용자 정보는 뗌
|
|
56
|
+
printf 'url = "%s"\n' "$I/pip/" | curl --ssl-no-revoke -K - -sSI -o /dev/null --connect-timeout 5 --max-time 10 # py 서버(pipx)
|
|
57
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://download.eclipse.org # jdtls
|
|
58
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://github.com # lsp-mcp
|
|
59
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://vmware.gallery.vsassets.io # Spring Boot LS(VSIX)
|
|
60
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://marketplace.visualstudio.com # Spring Boot LS(VSIX 대체 원천. 둘 중 하나만 닿아도 받을 수 있다)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `npm ping` 은 홈 폴더에서 부른다. 지금 폴더에 있는 프로젝트의 `.npmrc` 가 정한 레지스트리와 토큰을 따르지 않게 하려는 것이다.
|
|
64
|
+
- `npm ping` 은 15초 감시(`in15`) 안에서 부른다. 패킷을 버리는 망에서 npm 은 연결에 시간 제한을 두지 않아(`--fetch-timeout` 은 받는 중의 쉼만 잰다) OS 의 연결 재시도가 끝날 때까지 기다린다(Rocky 8.10 실측 130초). 15초 안에 끝나지 않으면 `15초 안에 끝나지 않아 멈춤(패킷을 버리는 망일 수 있음)` 과 종료 143 이다. `--no-update-notifier` 는 npm 이 제 판을 묻는 요청을 하나 더 내지 않게 한다.
|
|
65
|
+
- `npm ping` 이 실패하면 ping 에 답하지 않는 사내 미러일 수 있다. `(cd ~ && T=$(mktemp -d) && { npm view bash-language-server version --fetch-retries=0 --fetch-timeout=10000 --cache "$T" --no-update-notifier; r=$?; rm -rf "$T"; exit $r; })` 도 실패해야 오프라인으로 본다. 이 스킬이 설치할 패키지(`bash-language-server`)를 빈 캐시로 물으므로 옛 값이 나올 수 없다. ping 이 15초 감시에 걸려 멈췄으면(종료 143) 이 확인은 하지 않는다. 같은 레지스트리라 같은 시간을 또 기다린다.
|
|
66
|
+
- `npm` 이 없으면(명령 없음) 오프라인이 아니라 확인하지 못한 것이다. Node 를 먼저 갖춘다(`setup-node`).
|
|
67
|
+
- `npm ping` 은 npm 의 프록시 설정(`proxy`, `https-proxy`)을 따른다. 사내 미러만 열린 망이면 그 미러가 쓰이는지 npm 설정을 본다.
|
|
68
|
+
- `curl -sSI` 는 머리만 받는다(본문을 받지 않음). 닿지 못하면 종료 코드가 0 이 아니다(HTTP 상태와 관계없음). `curl` 은 `HTTPS_PROXY` 를 따른다. 확인 줄은 `--ssl-no-revoke` 를 준다. Windows 의 Git Bash curl 도 Schannel 이라, 폐지 목록 서버에 닿지 못하는 망에서 폐지 확인이 실패하면 종료 60 이다(Windows 박스 실측). 다른 curl 은 이 깃발을 받고 무시한다(macOS curl 8.7.1, Rocky 8.10 curl 7.61.1 실측).
|
|
69
|
+
- PyPI 는 pip 이 쓰는 색인(`PIP_INDEX_URL`, `pip config list` 의 `install.index-url` 이나 `global.index-url`, 없으면 pypi.org)의 작은 쪽(`<색인>/pip/`)을 본다.
|
|
70
|
+
- `pip config get` 은 `PIP_CONFIG_FILE` 로 준 파일을 읽지 않아(pip 25.0 실측) `pip config list` 를 읽는다.
|
|
71
|
+
- Rocky 8 의 기본 pip(9.0.3, `pip3`)은 `config` 명령이 없고 `pip` 이름은 없다(Rocky 8.10 실측). 그래서 판 붙은 python(3.13, 3.12, 3.11)의 pip 도 차례로 본다.
|
|
72
|
+
- 홈 폴더에서 읽는다. `python3 -m pip` 는 지금 폴더를 import 경로 맨 앞에 두어, 지금 폴더에 있는 남의 저장소의 `pip/` 가 실행될 수 있다.
|
|
73
|
+
- bash 블록은 주소를 명령 인자가 아니라 표준 입력의 curl 설정(`-K -`)으로 넘긴다. 명령 인자는 같은 PC 의 다른 사용자가 `ps` 로 볼 수 있어, 경로나 질의에 든 토큰도 거기 싣지 않는다. 사용자 정보(`사용자:토큰@`)는 떼고 묻는다. 닿는지만 보므로 인증은 필요 없다(401 도 닿은 것이다). Windows 에서도 뗀 주소를 쓴다.
|
|
74
|
+
- Windows PowerShell 에서는 같은 주소를 `curl.exe --ssl-no-revoke -sSI -o NUL --connect-timeout 5 --max-time 10 <주소>` 로 보고 `$LASTEXITCODE` 가 0 인지 본다(PowerShell 5.1 의 `curl` 은 `Invoke-WebRequest` 별칭이다). `curl.exe` 는 Windows 시스템 프록시를 따르지 않고 `HTTPS_PROXY` 만 따른다. `--ssl-no-revoke` 는 폐지 목록 서버에 닿지 못하는 망에서 Schannel 의 폐지 확인 실패를 오프라인으로 읽지 않게 한다. 닿는지만 보고 받은 내용은 쓰지 않으므로 이 확인에만 준다. `curl.exe` 가 없으면 `$LASTEXITCODE` 에 앞 명령의 값이 남으므로 `Get-Command curl.exe -ErrorAction SilentlyContinue` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다. `npm ping` 은 `Push-Location ~; npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier; Pop-Location` 처럼 홈에서 부르고 돌아온다(돌아오지 않으면 뒤 단계가 프로젝트가 아니라 홈에서 돈다). PowerShell 에는 15초 감시를 두지 않는다(Windows 의 연결 시간 제한은 약 21초로 알려져 있다. 미실측).
|
|
75
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
76
|
+
- Codex sandbox 가 네트워크를 막는 모드(기본 `workspace-write`, `read-only`)에서는 `CODEX_SANDBOX_NETWORK_DISABLED=1` 이 서고 확인이 모두 실패한다(curl 종료 7, npm `EPERM`). 블록의 첫 줄이 그렇다고 알린다. 그때는 '오프라인' 이 아니라 'sandbox 가 네트워크를 막음' 으로 보고하고, 승인을 받아 sandbox 밖에서 확인부터 다시 한다. Claude Code sandbox 안(`SANDBOX_RUNTIME=1`)에서는 허용 목록 밖 주소를 sandbox 프록시가 막는다. Windows PowerShell 에서는 `$env:CODEX_SANDBOX_NETWORK_DISABLED -eq '1'` 로 본다.
|
|
77
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
78
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
79
|
+
- curl 은 닿는데 npm 만 인증서 오류(`UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `SELF_SIGNED_CERT_IN_CHAIN`, `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`)로 실패하면 망은 열려 있고 npm 이 사내 인증서를 믿지 않는 것이다(npm 은 OS 인증서 저장소를 쓰지 않는다). '인증서 문제로 설치하지 않음' 으로 보고하고 `NODE_EXTRA_CA_CERTS=<사내 CA 파일>` 을 안내한다. Node 24 는 `NODE_OPTIONS=--use-system-ca` 도 된다.
|
|
80
|
+
|
|
43
81
|
## 1. 사전요구(preflight)
|
|
44
82
|
|
|
45
83
|
각 서버는 선행 런타임이 필요하다 - **없으면 먼저 해당 setup 스킬을 실행**(추정·강행 금지):
|
|
@@ -86,10 +124,11 @@ for c in basedpyright-langserver ruff; do command -v $c >/dev/null && echo "✓
|
|
|
86
124
|
**jdtls (Java LSP)** - Eclipse 최신 스냅샷:
|
|
87
125
|
```bash
|
|
88
126
|
mkdir -p ~/.local/jdtls
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
T=$(mktemp -d) \
|
|
128
|
+
&& curl -fL --connect-timeout 10 --speed-limit 1000 --speed-time 30 -o "$T/jdtls.tar.gz" \
|
|
129
|
+
"https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz" \
|
|
130
|
+
&& tar -xzf "$T/jdtls.tar.gz" -C ~/.local/jdtls
|
|
131
|
+
rm -rf "$T"
|
|
93
132
|
chmod +x ~/.local/jdtls/bin/jdtls
|
|
94
133
|
which jdtls # PATH 에 ~/.local/jdtls/bin 필요
|
|
95
134
|
```
|
|
@@ -97,28 +136,31 @@ which jdtls # PATH 에 ~/.local/jdtls/bin 필요
|
|
|
97
136
|
|
|
98
137
|
**Spring Boot Language Server** - VSIX(=ZIP) 다운로드. vsassets.io/marketplace 가 **HTML 에러페이지**를 반환할 수 있어 **ZIP 매직바이트(`504b0304`) 검증** 후 `find` 로 `*-exec.jar` 탐색 + 심볼릭 링크:
|
|
99
138
|
```bash
|
|
139
|
+
(
|
|
100
140
|
set -euo pipefail
|
|
101
|
-
TARGET="$HOME/.local/spring-boot-ls"; VSIX
|
|
141
|
+
TARGET="$HOME/.local/spring-boot-ls"; T=$(mktemp -d); trap 'rm -rf "$T"' EXIT; VSIX="$T/sbls.vsix"; LINK=spring-boot-language-server.jar
|
|
102
142
|
mkdir -p "$TARGET"
|
|
103
143
|
VSIX_URL="https://vmware.gallery.vsassets.io/_apis/public/gallery/publisher/vmware/extension/vscode-spring-boot/latest/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage"
|
|
104
144
|
FALLBACK="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/vmware/vsextensions/vscode-spring-boot/latest/vspackage"
|
|
105
|
-
curl -fL --connect-timeout 30 --retry 2 -H "User-Agent: Mozilla/5.0" -o "$VSIX" "$VSIX_URL" 2>/dev/null \
|
|
106
|
-
|| curl -fL --connect-timeout 30 --retry 2 -H "User-Agent: Mozilla/5.0" -o "$VSIX" "$FALLBACK"
|
|
145
|
+
curl -fL --connect-timeout 30 --speed-limit 1000 --speed-time 30 --retry 2 -H "User-Agent: Mozilla/5.0" -o "$VSIX" "$VSIX_URL" 2>/dev/null \
|
|
146
|
+
|| curl -fL --connect-timeout 30 --speed-limit 1000 --speed-time 30 --retry 2 -H "User-Agent: Mozilla/5.0" -o "$VSIX" "$FALLBACK"
|
|
107
147
|
# ZIP 매직 검증 (HTML 에러페이지면 즉시 중단)
|
|
108
148
|
[ "$(xxd -l 4 "$VSIX" | awk 'NR==1{print $2$3}')" = "504b0304" ] \
|
|
109
149
|
|| { echo "✗ ZIP 아님(HTML 에러페이지?)"; rm -f "$VSIX"; exit 1; }
|
|
110
|
-
|
|
150
|
+
find "${TARGET:?}" -mindepth 1 -delete; unzip -o -q "$VSIX" -d "$TARGET" # 글롭 없이 비움(zsh 는 빈 폴더의 * 에서 멈춤)
|
|
111
151
|
JAR=$(find "$TARGET" -name "spring-boot-language-server-*-exec.jar" | sort -V | tail -1)
|
|
112
152
|
[ -n "$JAR" ] || { echo "✗ exec.jar 미발견"; rm -f "$VSIX"; exit 1; }
|
|
113
153
|
ln -sf "$(basename "$JAR")" "$(dirname "$JAR")/$LINK" # 디렉토리 이동에도 유효한 상대 링크
|
|
114
154
|
rm -f "$VSIX"; find ~/.local/spring-boot-ls -name "$LINK" -ls
|
|
155
|
+
)
|
|
115
156
|
```
|
|
157
|
+
블록 전체가 괄호 안의 서브셸에서 돈다. `set -e`, EXIT trap, `exit 1` 이 이 블록 안에만 머물러, 같은 셸의 다음 명령을 끝내거나 그 셸이 끝날 때 다른 `T` 를 지우지 않는다.
|
|
116
158
|
> Windows: 심볼릭 링크는 **개발자 모드/관리자 권한** 필요 → 없으면 `Copy-Item` 복사 fallback(둘 다 동작). 매직검증·find 는 PowerShell 로 동일 수행, JAR 경로는 `.Replace('\','/')`. RHEL8 에 `xxd` 없으면 `dnf install -y vim-common` 또는 `od -An -tx1 -N4`.
|
|
117
159
|
|
|
118
160
|
## 6. lsp-mcp 브리지 설치 (Codex 경로 - Claude Code 는 건너뜀)
|
|
119
161
|
```bash
|
|
120
162
|
mkdir -p ~/.local/src
|
|
121
|
-
git clone https://github.com/CesarPetrescu/lsp-mcp.git ~/.local/src/lsp-mcp
|
|
163
|
+
git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=30 clone https://github.com/CesarPetrescu/lsp-mcp.git ~/.local/src/lsp-mcp
|
|
122
164
|
pipx install --editable ~/.local/src/lsp-mcp # → ~/.local/bin/codex-lsp-bridge (Windows: .exe)
|
|
123
165
|
which codex-lsp-bridge && codex-lsp-bridge --help | head -5
|
|
124
166
|
```
|
|
@@ -144,7 +186,7 @@ language_id = "spring-boot-properties"
|
|
|
144
186
|
EOF
|
|
145
187
|
# TOML 유효성 (중복/문법오류 즉시 감지). tomllib 는 Python 3.11+ - RHEL8/Rocky8 기본 python3(3.6)면 setup-python 의 3.11 을 쓴다:
|
|
146
188
|
PY="$(command -v python3.12 || command -v python3.11 || command -v python3)"
|
|
147
|
-
"$PY" -c "import tomllib; tomllib.loads(open('config/default.toml',encoding='utf-8').read()); print('OK lsp-mcp TOML')" 2>/dev/null \
|
|
189
|
+
(cd ~ && "$PY" -c "import tomllib; tomllib.loads(open('$HOME/.local/src/lsp-mcp/config/default.toml',encoding='utf-8').read()); print('OK lsp-mcp TOML')") 2>/dev/null \
|
|
148
190
|
|| echo "⚠ tomllib(3.11+) 미가용 - append 는 위 grep guard 로 이미 적용됨, setup-python 설치 후 재검증"
|
|
149
191
|
cd - >/dev/null
|
|
150
192
|
```
|
|
@@ -156,7 +198,7 @@ cd - >/dev/null
|
|
|
156
198
|
### 7-Codex - `[mcp_servers.lsp_bridge]` 등록
|
|
157
199
|
OMX 가 배포한 기존 엔트리를 보존하려 **Python replace-or-append** 로 patch(전체 덮어쓰기 금지):
|
|
158
200
|
```bash
|
|
159
|
-
python3 - << 'PY'
|
|
201
|
+
(cd ~ && python3 - << 'PY' # 홈에서: -c, -m, 표준 입력 스크립트는 지금 폴더의 모듈을 먼저 불러온다
|
|
160
202
|
import re
|
|
161
203
|
from pathlib import Path
|
|
162
204
|
home = str(Path.home()) # Windows: str(Path.home()).replace("\\","/")
|
|
@@ -178,8 +220,9 @@ txt = rep(txt, "mcp_servers.lsp_bridge", block + "\n")
|
|
|
178
220
|
p.write_text(txt, encoding="utf-8") # Windows: encoding 필수(CP949 회피)
|
|
179
221
|
print("patched")
|
|
180
222
|
PY
|
|
223
|
+
)
|
|
181
224
|
PY="$(command -v python3.12 || command -v python3.11 || command -v python3)" # tomllib=3.11+
|
|
182
|
-
"$PY" -c "import tomllib; tomllib.loads(open('$HOME/.codex/config.toml',encoding='utf-8').read()); print('OK config.toml valid')" 2>/dev/null \
|
|
225
|
+
(cd ~ && "$PY" -c "import tomllib; tomllib.loads(open('$HOME/.codex/config.toml',encoding='utf-8').read()); print('OK config.toml valid')") 2>/dev/null \
|
|
183
226
|
|| echo "⚠ tomllib(3.11+) 미가용 - patch 는 적용됨, setup-python 후 재검증"
|
|
184
227
|
```
|
|
185
228
|
|
|
@@ -213,7 +256,7 @@ jdtls --version 2>/dev/null || which jdtls # Windows: jdtls.bat
|
|
|
213
256
|
codex-lsp-bridge --help | head -3
|
|
214
257
|
grep -q 'lsp_bridge' ~/.codex/config.toml && echo "✓ config.toml lsp_bridge" || echo "✗ 미등록"
|
|
215
258
|
PY="$(command -v python3.12 || command -v python3.11 || command -v python3)" # tomllib=3.11+
|
|
216
|
-
"$PY" -c "import tomllib; tomllib.loads(open('$HOME/.codex/config.toml',encoding='utf-8').read()); print('✓ config.toml valid')" 2>/dev/null || echo "⚠ tomllib(3.11+) 미가용 - setup-python 후 재검증"
|
|
259
|
+
(cd ~ && "$PY" -c "import tomllib; tomllib.loads(open('$HOME/.codex/config.toml',encoding='utf-8').read()); print('✓ config.toml valid')") 2>/dev/null || echo "⚠ tomllib(3.11+) 미가용 - setup-python 후 재검증"
|
|
217
260
|
# Claude 만: lsp_servers 도구로 인식 서버 나열
|
|
218
261
|
```
|
|
219
262
|
4-field 보고: **변경**(설치 서버·브리지·config 배선) / **Evidence**(위 --version·valid 출력) / **검증**(각 서버 응답 + 브리지·config TOML valid + 런타임 인식) / **Unknown**(네트워크 차단으로 미설치·JDK 21 부재로 java/spring 스킵 등).
|