@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,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* setup-omc-hud 의 Codex 대상. Codex TUI 의 상태표시줄은 `$CODEX_HOME/config.toml` 의 `[tui] status_line` 이다.
|
|
4
|
+
* OMX 는 `omx setup` 에서 이 줄을 `# omx:managed-status-line` 표시와 함께 관리한다.
|
|
5
|
+
*
|
|
6
|
+
* - on: status_line 이 없을 때만 OMX 의 프리셋 항목으로 넣고 `# banker:status-line` 표시를 붙인다. OMX 나 사용자가
|
|
7
|
+
* 이미 둔 줄은 건드리지 않는다(누가 둔 것인지 알린다).
|
|
8
|
+
* - off: banker 표시가 붙은 줄만 뺀다.
|
|
9
|
+
* - status: 지금 줄과 그 주인.
|
|
10
|
+
* 쓰기 전 원본을 config.toml.banker-hud.bak 에 둔다. 공통 모듈 codex-config.mjs 는 setup-bypass-permissions 의 같은 사본.
|
|
11
|
+
*
|
|
12
|
+
* 사용: node codex-status-line.mjs <on [--preset focused|minimal]|off|status>
|
|
13
|
+
* 종료: 0 완료 또는 이미 그 상태, 1 거부(파일은 그대로), 2 예기치 못한 오류, 3 이 계정이 쓸 수 없음(샌드박스 등)
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
import { codexConfigFile, definedInline, denied, dropEmptyTable, findKey, readToml, Refused, removeKey, setKey, writeToml } from './codex-config.mjs';
|
|
20
|
+
|
|
21
|
+
export const MARK = '# banker:status-line';
|
|
22
|
+
const OMX_MARK = '# omx:managed-status-line';
|
|
23
|
+
// oh-my-codex 의 상태표시줄 프리셋(config/generator.js 의 STATUS_LINE_PRESETS)과 같은 항목.
|
|
24
|
+
export const PRESETS = {
|
|
25
|
+
focused: ['model-with-reasoning', 'git-branch', 'context-remaining', 'total-input-tokens', 'total-output-tokens', 'five-hour-limit', 'weekly-limit'],
|
|
26
|
+
minimal: ['model-with-reasoning', 'git-branch'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 지금 status_line 과 그 주인: 'banker', 'omx', 'user', 없으면 null.
|
|
30
|
+
export function owner(text) {
|
|
31
|
+
const hit = findKey(text, 'tui', 'status_line');
|
|
32
|
+
if (!hit) return { owner: null, raw: '' };
|
|
33
|
+
const above = text.split(/\r?\n/)[hit.at - 1];
|
|
34
|
+
return { owner: above === MARK ? 'banker' : above === OMX_MARK ? 'omx' : 'user', raw: hit.raw };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const presetValue = (fields) => `[${fields.map((f) => `"${f}"`).join(', ')}]`;
|
|
38
|
+
const sameValue = (a, b) => a.replace(/\s+/g, '').replace(/,\]$/, ']') === b.replace(/\s+/g, '');
|
|
39
|
+
|
|
40
|
+
export function turnOn({ file = codexConfigFile(), preset = 'focused' } = {}) {
|
|
41
|
+
if (!Object.prototype.hasOwnProperty.call(PRESETS, preset)) return { code: 1, msg: `프리셋은 ${Object.keys(PRESETS).join(', ')} 중 하나` };
|
|
42
|
+
const { text, mode, exists } = readToml(file);
|
|
43
|
+
// 최상위의 `tui.x = ` 나 `tui = { }` 가 있는 파일에 [tui] 를 더하면 키가 겹쳐 Codex 가 시작하지 못한다.
|
|
44
|
+
if (definedInline(text, 'tui')) return { code: 1, msg: '거부: 최상위에서 tui 를 점 키나 인라인 표로 정한 설정이라 [tui] 를 더하지 않음. status_line 을 직접 넣으십시오' };
|
|
45
|
+
const now = owner(text);
|
|
46
|
+
if (now.owner === 'omx') return { code: 0, msg: '이미 있음: OMX 가 관리하는 상태표시줄(바꾸려면 omx setup)' };
|
|
47
|
+
if (now.owner === 'user') return { code: 0, msg: '이미 있음: 사용자가 둔 상태표시줄이라 그대로 둠' };
|
|
48
|
+
if (!fs.existsSync(path.dirname(file))) return { code: 1, msg: `Codex 설정 폴더가 없음: ${path.dirname(file)}` };
|
|
49
|
+
const next = setKey(text, 'tui', 'status_line', presetValue(PRESETS[preset]), MARK);
|
|
50
|
+
if (next === text) return { code: 0, msg: '이미 켜져 있음' };
|
|
51
|
+
writeToml(file, next, { mode, backup: exists ? `${file}.banker-hud.bak` : null });
|
|
52
|
+
return { code: 0, msg: `켬: [tui] status_line(${preset}). Codex 를 다시 열면 보임` };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function turnOff({ file = codexConfigFile() } = {}) {
|
|
56
|
+
const { text, mode } = readToml(file);
|
|
57
|
+
const now = owner(text);
|
|
58
|
+
if (now.owner !== 'banker') return { code: 0, msg: now.owner ? `banker 가 둔 줄이 아님(${now.owner}): 그대로 둠` : '이미 꺼져 있음' };
|
|
59
|
+
// 표시는 남았지만 값을 사용자가 고쳤으면 사용자의 줄로 본다.
|
|
60
|
+
if (!Object.values(PRESETS).some((f) => sameValue(now.raw, presetValue(f)))) return { code: 0, msg: '사용자가 값을 고친 줄이라 그대로 둠(banker 표시만 남음)' };
|
|
61
|
+
writeToml(file, dropEmptyTable(removeKey(text, 'tui', 'status_line', MARK), 'tui'), { mode });
|
|
62
|
+
return { code: 0, msg: '끔: banker 가 둔 status_line 을 뺌' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function status({ file = codexConfigFile() } = {}) {
|
|
66
|
+
const now = owner(readToml(file).text);
|
|
67
|
+
return { code: 0, msg: now.owner ? `status_line(${now.owner}): ${now.raw.replace(/\s+/g, ' ')}` : 'status_line 없음' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function run(argv) {
|
|
71
|
+
const [cmd] = argv;
|
|
72
|
+
const at = argv.indexOf('--preset');
|
|
73
|
+
if (cmd === 'on') return turnOn({ preset: at >= 0 ? argv[at + 1] : 'focused' });
|
|
74
|
+
if (cmd === 'off') return turnOff();
|
|
75
|
+
return status();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main(argv) {
|
|
79
|
+
let r;
|
|
80
|
+
try {
|
|
81
|
+
r = run(argv);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
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}` };
|
|
84
|
+
}
|
|
85
|
+
console.log(`${r.msg}\n설정 파일: ${codexConfigFile()}`);
|
|
86
|
+
return r.code;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const same = (a, b) => { try { return fs.realpathSync(a) === fs.realpathSync(b); } catch { return false; } };
|
|
90
|
+
if (process.argv[1] && same(fileURLToPath(import.meta.url), path.resolve(process.argv[1]))) process.exitCode = main(process.argv.slice(2));
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* segment each: 🤖1|🔧14|⚡3. A count OMC leaves out (zero) stays out.
|
|
19
19
|
* - per-model usage badge (fable:0%(1h31m)) -> Fa:0%/1h31m, after the rate windows.
|
|
20
20
|
* - PL:<MB, 1 decimal>M always shown before se. It reads payload-mon's module
|
|
21
|
-
* copy
|
|
22
|
-
*
|
|
21
|
+
* copy, payload-mon/payload-size.mjs, from the first of these folders that
|
|
22
|
+
* holds it as a regular file: this file's (the one a statusLine link points
|
|
23
|
+
* at), the link's, the config folder's hud/, then the folder import.meta.url
|
|
24
|
+
* names (the link's under --preserve-symlinks); without it PL is left out.
|
|
23
25
|
* - cwd group split on "|"; branch shown without "branch:" after the account.
|
|
24
26
|
* - OMC#X.Y.Z label no longer shown, nor OMC's update hint lines
|
|
25
27
|
* ("[!] omc X - paste: ! ...", "[!] claude X - paste: ! claude update").
|
|
@@ -68,14 +70,43 @@
|
|
|
68
70
|
* We wait for the first complete line instead.
|
|
69
71
|
*/
|
|
70
72
|
import { spawnSync } from "node:child_process";
|
|
71
|
-
import { readFileSync } from "node:fs";
|
|
73
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
72
74
|
import { homedir } from "node:os";
|
|
73
|
-
import { dirname, join } from "node:path";
|
|
75
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
74
76
|
import { Readable } from "node:stream";
|
|
75
77
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
76
78
|
|
|
77
79
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
78
|
-
|
|
80
|
+
// The statusLine command names this file. When that name is a link (dotfiles, say), node runs the
|
|
81
|
+
// file it points at: the installer puts omc-hud.mjs beside the link and payload-mon puts its copy
|
|
82
|
+
// beside the file, so both folders are looked in. The command's name counts only when it leads to
|
|
83
|
+
// this file (another script may have imported this one), and the file's own folder comes from
|
|
84
|
+
// realpath: with --preserve-symlinks-main, import.meta.url names the link.
|
|
85
|
+
function hereDirs() {
|
|
86
|
+
const dirs = { named: null, real: __dirname, self: __dirname };
|
|
87
|
+
try {
|
|
88
|
+
const real = realpathSync(fileURLToPath(import.meta.url));
|
|
89
|
+
dirs.real = dirname(real);
|
|
90
|
+
const named = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
91
|
+
if (named && realpathSync(named) === real) dirs.named = dirname(named);
|
|
92
|
+
} catch {
|
|
93
|
+
/* keep this file's own folder */
|
|
94
|
+
}
|
|
95
|
+
return dirs;
|
|
96
|
+
}
|
|
97
|
+
const HERE = hereDirs();
|
|
98
|
+
const isFile = (p) => {
|
|
99
|
+
try {
|
|
100
|
+
return statSync(p).isFile();
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// The first of these folders (each once, in this order) that holds the file as a regular file, "" when none
|
|
106
|
+
// does: a folder at that name is no module, and opening a FIFO waits for a writer for good.
|
|
107
|
+
function findSibling(dirs, ...parts) {
|
|
108
|
+
return [...new Set(dirs.filter(Boolean))].map((d) => join(d, ...parts)).find(isFile) ?? "";
|
|
109
|
+
}
|
|
79
110
|
|
|
80
111
|
const STDIN_TIMEOUT_MS = 2000; // Claude Code pipes the payload immediately
|
|
81
112
|
const RENDER_TIMEOUT_MS = 10000; // see timer discipline note above
|
|
@@ -122,10 +153,39 @@ function emit(s) {
|
|
|
122
153
|
// that file in $CLAUDE_CONFIG_DIR when set, otherwise in the home folder.
|
|
123
154
|
function claudeJsonDir() {
|
|
124
155
|
const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
|
|
125
|
-
|
|
156
|
+
return v ? expandDir(v) : homedir();
|
|
157
|
+
}
|
|
158
|
+
// Where Claude Code was started (the statusline JSON's workspace.project_dir, else the working
|
|
159
|
+
// folder). A relative CLAUDE_CONFIG_DIR is read from there: Claude Code reads it from its own working
|
|
160
|
+
// folder, which is the same one until it moves (into a worktree, say).
|
|
161
|
+
function startDir() {
|
|
162
|
+
try {
|
|
163
|
+
const d = JSON.parse(input)?.workspace?.project_dir;
|
|
164
|
+
if (typeof d === "string" && isAbsolute(d)) return d;
|
|
165
|
+
} catch {
|
|
166
|
+
/* no or bad stdin JSON */
|
|
167
|
+
}
|
|
168
|
+
return process.cwd();
|
|
169
|
+
}
|
|
170
|
+
function expandDir(v) {
|
|
126
171
|
if (v === "~") return homedir();
|
|
127
172
|
if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
|
|
128
|
-
return v;
|
|
173
|
+
return resolve(startDir(), v);
|
|
174
|
+
}
|
|
175
|
+
// Claude Code's config folder. Its hud/ is where the installer puts the loader: the third place
|
|
176
|
+
// siblings are looked for, as when another script imported this file through a link. "" when the
|
|
177
|
+
// home folder cannot be told (no HOME and no account entry) or the working folder is gone: the
|
|
178
|
+
// other places still count.
|
|
179
|
+
function configDir() {
|
|
180
|
+
const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
|
|
181
|
+
return v ? expandDir(v) : join(homedir(), ".claude");
|
|
182
|
+
}
|
|
183
|
+
function configHud() {
|
|
184
|
+
try {
|
|
185
|
+
return join(configDir(), "hud");
|
|
186
|
+
} catch {
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
129
189
|
}
|
|
130
190
|
function loginAccount() {
|
|
131
191
|
try {
|
|
@@ -175,6 +235,7 @@ function readStdinBounded(ms) {
|
|
|
175
235
|
}
|
|
176
236
|
|
|
177
237
|
const input = await readStdinBounded(STDIN_TIMEOUT_MS);
|
|
238
|
+
const HUD = findSibling([HERE.named, HERE.real, configHud(), HERE.self], "omc-hud.mjs");
|
|
178
239
|
|
|
179
240
|
// Current reasoning effort level (low/medium/high/xhigh) from the statusline
|
|
180
241
|
// payload (effort.level) — shown next to the model. Empty if absent.
|
|
@@ -278,13 +339,18 @@ for (const s of [process.stdout, process.stderr]) {
|
|
|
278
339
|
}
|
|
279
340
|
|
|
280
341
|
let raw = "";
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
342
|
+
if (!HUD) {
|
|
343
|
+
// No regular file at any place looked in: nothing to run. Named where the installer puts it.
|
|
344
|
+
emit(`[OMC HUD] omc-hud.mjs not found: ${join(HERE.named || HERE.real, "omc-hud.mjs")}\n`);
|
|
345
|
+
} else {
|
|
346
|
+
try {
|
|
347
|
+
raw = (await renderInProcess(RENDER_TIMEOUT_MS)).replace(/\r/g, "");
|
|
348
|
+
} catch {
|
|
349
|
+
raw = ""; // fall through to the child-process path
|
|
350
|
+
}
|
|
285
351
|
}
|
|
286
352
|
|
|
287
|
-
if (!raw.trim()) {
|
|
353
|
+
if (HUD && !raw.trim()) {
|
|
288
354
|
// In-process rendering produced nothing usable. Fall back to the original
|
|
289
355
|
// out-of-process path: it costs an extra node, but a statusline that renders
|
|
290
356
|
// beats one that does not — e.g. if a future OMC version stops being
|
|
@@ -481,8 +547,9 @@ try {
|
|
|
481
547
|
// without it (payload-mon off) or without a transcript, PL is left out.
|
|
482
548
|
try {
|
|
483
549
|
const transcript = JSON.parse(input)?.transcript_path;
|
|
484
|
-
|
|
485
|
-
|
|
550
|
+
const copy = transcript ? findSibling([HERE.real, HERE.named, configHud(), HERE.self], "payload-mon", "payload-size.mjs") : "";
|
|
551
|
+
if (copy) {
|
|
552
|
+
const { estimatePayloadBytes, REQUEST_LIMIT_BYTES } = await import(pathToFileURL(copy).href);
|
|
486
553
|
const bytes = estimatePayloadBytes(transcript);
|
|
487
554
|
if (Number.isFinite(bytes)) {
|
|
488
555
|
for (let i = colored.length - 1; i >= 0; i--) if (/^payload:/.test(stripAnsi(colored[i]))) colored.splice(i, 1);
|
|
@@ -35,7 +35,7 @@ const OPEN = "// >>> claude-update-last >>>";
|
|
|
35
35
|
const CLOSE = "// <<< claude-update-last <<<";
|
|
36
36
|
// A block's body never crosses another opening marker, so a stray one above a
|
|
37
37
|
// block is left over (and refused) instead of taking the code between with it.
|
|
38
|
-
const BLOCKS =
|
|
38
|
+
export const BLOCKS =
|
|
39
39
|
/^[ \t]*\/\/ >>> claude-update-last >>>(?:(?!\/\/ >>> claude-update-last >>>)[\s\S])*?\/\/ <<< claude-update-last <<<[ \t]*(?:\r?\n|(?![\s\S]))/gm;
|
|
40
40
|
// The block goes right before the line that joins the segments into the statusline.
|
|
41
41
|
const JOIN_LINE = /^ {2}let result = colored\.join\(SEP\);\r?$/gm;
|
|
@@ -150,12 +150,14 @@ function writeFresh(file, data) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// The first lines of node's complaint about `text` as an ES module, "" if it parses.
|
|
153
|
+
// The first lines of node's complaint about `text` as an ES module, "" if it parses. A leading BOM
|
|
154
|
+
// is left out: node runs a wrapper that starts with one, but `node --check` rejects a BOM before a
|
|
155
|
+
// shebang (Node 20, 24).
|
|
154
156
|
function syntaxError(text) {
|
|
155
157
|
const dir = mkdtempSync(join(tmpdir(), "claude-update-last-"));
|
|
156
158
|
try {
|
|
157
159
|
const file = join(dir, "check.mjs");
|
|
158
|
-
writeFileSync(file, text);
|
|
160
|
+
writeFileSync(file, text.replace(/^\uFEFF/, ""));
|
|
159
161
|
const r = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
160
162
|
return r.status === 0 ? "" : String(r.stderr || r.error).trim().split("\n").slice(0, 3).join(" | ");
|
|
161
163
|
} finally {
|
|
@@ -19,6 +19,49 @@ for t in conda node npx python3 Xvfb xvfb-run; do command -v $t >/dev/null && ec
|
|
|
19
19
|
```
|
|
20
20
|
**이미 설치 확인**: `ls "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}" 2>/dev/null | grep -i chromium` 가 있으면 브라우저 설치는 skip(검증만).
|
|
21
21
|
|
|
22
|
+
### 네트워크 확인 (내려받기 전)
|
|
23
|
+
|
|
24
|
+
이 스킬이 내려받는 원천에 닿는지 먼저 본다(curl 은 10초, npm 은 15초 안). 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
28
|
+
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"; }
|
|
29
|
+
in15 sh -c 'cd ~ && exec npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier' # npx playwright
|
|
30
|
+
# pip 이 읽는 설정 전부(PIP_CONFIG_FILE 포함)를 홈 폴더에서 읽는다. install 절이 global 절보다 앞선다
|
|
31
|
+
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 미만). 사내 미러를 쓰면 확인 주소를 그 미러로 바꿔 본다"
|
|
32
|
+
I="${PIP_INDEX_URL:-$(printf '%s\n' "$L" | sed -n "s/^install\.index-url='\(.*\)'$/\1/p")}"
|
|
33
|
+
I="${I:-$(printf '%s\n' "$L" | sed -n "s/^global\.index-url='\(.*\)'$/\1/p")}"
|
|
34
|
+
I=$(printf %s "${I:-https://pypi.org/simple}" | sed -E 's#^([A-Za-z][A-Za-z0-9+.-]*://)[^/]*@#\1#; s#/$##') # 주소의 사용자 정보는 뗌
|
|
35
|
+
printf 'url = "%s"\n' "$I/pip/" | curl --ssl-no-revoke -K - -sSI -o /dev/null --connect-timeout 5 --max-time 10 # Python playwright
|
|
36
|
+
H="${PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST:-${PLAYWRIGHT_DOWNLOAD_HOST:-https://cdn.playwright.dev}}" # chromium 내려받기 서버(설치본 playwright-core 1.58.2~1.64 의 기본값)
|
|
37
|
+
F="${PLAYWRIGHT_DOWNLOAD_HOST:-https://cdn.playwright.dev}" # ffmpeg 내려받기 서버(chromium 미러 변수를 따르지 않음)
|
|
38
|
+
H=$(printf %s "$H" | sed -E 's#^([A-Za-z][A-Za-z0-9+.-]*://)[^/]*@#\1#; s#/$##'); F=$(printf %s "$F" | sed -E 's#^([A-Za-z][A-Za-z0-9+.-]*://)[^/]*@#\1#; s#/$##') # 사용자 정보는 뗌
|
|
39
|
+
printf 'url = "%s/"\n' "$H" | curl --ssl-no-revoke -K - -sSI -o /dev/null --connect-timeout 5 --max-time 10 # 브라우저(chromium, headless shell)
|
|
40
|
+
[ "$F" = "$H" ] || printf 'url = "%s/"\n' "$F" | curl --ssl-no-revoke -K - -sSI -o /dev/null --connect-timeout 5 --max-time 10 # ffmpeg
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `npm ping` 은 홈 폴더에서 부른다. 지금 폴더에 있는 프로젝트의 `.npmrc` 가 정한 레지스트리와 토큰을 따르지 않게 하려는 것이다.
|
|
44
|
+
- `npm ping` 은 15초 감시(`in15`) 안에서 부른다. 패킷을 버리는 망에서 npm 은 연결에 시간 제한을 두지 않아(`--fetch-timeout` 은 받는 중의 쉼만 잰다) OS 의 연결 재시도가 끝날 때까지 기다린다(Rocky 8.10 실측 130초). 15초 안에 끝나지 않으면 `15초 안에 끝나지 않아 멈춤(패킷을 버리는 망일 수 있음)` 과 종료 143 이다. `--no-update-notifier` 는 npm 이 제 판을 묻는 요청을 하나 더 내지 않게 한다.
|
|
45
|
+
- `npm ping` 이 실패하면 ping 에 답하지 않는 사내 미러일 수 있다. `(cd ~ && T=$(mktemp -d) && { npm view playwright version --fetch-retries=0 --fetch-timeout=10000 --cache "$T" --no-update-notifier; r=$?; rm -rf "$T"; exit $r; })` 도 실패해야 오프라인으로 본다. 이 스킬이 설치할 패키지(`playwright`)를 빈 캐시로 물으므로 옛 값이 나올 수 없다. ping 이 15초 감시에 걸려 멈췄으면(종료 143) 이 확인은 하지 않는다. 같은 레지스트리라 같은 시간을 또 기다린다.
|
|
46
|
+
- `npm` 이 없으면(명령 없음) 오프라인이 아니라 확인하지 못한 것이다. Node 를 먼저 갖춘다(`setup-node`).
|
|
47
|
+
- `npm ping` 은 npm 의 프록시 설정(`proxy`, `https-proxy`)을 따른다. 사내 미러만 열린 망이면 그 미러가 쓰이는지 npm 설정을 본다.
|
|
48
|
+
- `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 실측).
|
|
49
|
+
- PyPI 는 pip 이 쓰는 색인(`PIP_INDEX_URL`, `pip config list` 의 `install.index-url` 이나 `global.index-url`, 없으면 pypi.org)의 작은 쪽(`<색인>/pip/`)을 본다.
|
|
50
|
+
- `pip config get` 은 `PIP_CONFIG_FILE` 로 준 파일을 읽지 않아(pip 25.0 실측) `pip config list` 를 읽는다.
|
|
51
|
+
- Rocky 8 의 기본 pip(9.0.3, `pip3`)은 `config` 명령이 없고 `pip` 이름은 없다(Rocky 8.10 실측). 그래서 판 붙은 python(3.13, 3.12, 3.11)의 pip 도 차례로 본다.
|
|
52
|
+
- 홈 폴더에서 읽는다. `python3 -m pip` 는 지금 폴더를 import 경로 맨 앞에 두어, 지금 폴더에 있는 남의 저장소의 `pip/` 가 실행될 수 있다.
|
|
53
|
+
- bash 블록은 주소를 명령 인자가 아니라 표준 입력의 curl 설정(`-K -`)으로 넘긴다. 명령 인자는 같은 PC 의 다른 사용자가 `ps` 로 볼 수 있어, 경로나 질의에 든 토큰도 거기 싣지 않는다. 사용자 정보(`사용자:토큰@`)는 떼고 묻는다. 닿는지만 보므로 인증은 필요 없다(401 도 닿은 것이다). Windows 에서도 뗀 주소를 쓴다.
|
|
54
|
+
- 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초로 알려져 있다. 미실측).
|
|
55
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
56
|
+
- OS 패키지 관리자(dnf, apt, brew, winget)의 단계는 자기 설정의 미러를 쓰므로 이 확인과 관계없이 시도하고, 실패하면 그 단계를 실패로 보고한다.
|
|
57
|
+
- 패킷을 버리거나 답하지 않는 망에서 dnf 는 저장소마다 약 2분(SYN 을 버리는 망 실측 121초), apt 는 원천마다 약 4분(답하지 않는 원천 실측 247초) 기다린 뒤 실패로 끝난다. 시간 제한은 관리자 설정을 따르므로 이 스킬이 바꾸지 않는다.
|
|
58
|
+
- `apt-get update` 는 받지 못해도 종료 0 이다. 출력의 `W: Failed to fetch` 가 있으면 그 단계를 실패로 보고한다.
|
|
59
|
+
- 브라우저(chromium, chromium headless shell, ffmpeg)는 Playwright 의 내려받기 서버에서 받는다(이 PC 의 설치본 playwright-core 1.58.2 에서 1.64 까지 기본값 `https://cdn.playwright.dev`). 막힌 망에서는 `PLAYWRIGHT_DOWNLOAD_HOST` 로 사내 미러를 준다. chromium 만 사내 미러(`PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST`)로 바꾸면 ffmpeg 는 기본 서버에서 받는다. 그래서 확인도 두 서버를 본다(같으면 한 번). 둘 다 바꾸려면 `PLAYWRIGHT_DOWNLOAD_HOST` 를 준다. 확인 없이 받으면 패킷을 버리는 망에서 약 11분(실측 658초) 뒤에 실패한다. Windows 에서도 같은 두 주소(사용자 정보를 뗀 것)를 본다.
|
|
60
|
+
- 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'` 로 본다.
|
|
61
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
62
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
63
|
+
- 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` 도 된다.
|
|
64
|
+
|
|
22
65
|
## 1. 공유 브라우저 경로 + 환경 (모든 OS 공통)
|
|
23
66
|
브라우저를 한 곳에 두고 MCP·스크립트가 `PLAYWRIGHT_BROWSERS_PATH` 로 공유한다.
|
|
24
67
|
- **root/sudo 가능**: 공용 경로(예 `/app/playwright-browsers`) + `/etc/profile.d/playwright.sh`.
|
|
@@ -33,7 +76,7 @@ grep -q PLAYWRIGHT_BROWSERS_PATH ~/.bashrc 2>/dev/null || echo 'export PLAYWRIGH
|
|
|
33
76
|
- **Node 경로(권장·간단)**: `npx playwright install chromium` (root면 `--with-deps` 로 OS 라이브러리까지). Node 없으면 distro 패키지/nvm 로 Node 먼저.
|
|
34
77
|
- **Python venv(conda 없을 때)**:
|
|
35
78
|
```bash
|
|
36
|
-
python3 -m venv ~/.venvs/playwright
|
|
79
|
+
(cd ~ && python3 -m venv ~/.venvs/playwright) # 홈에서: -c, -m, 표준 입력 스크립트는 지금 폴더의 모듈을 먼저 불러온다
|
|
37
80
|
~/.venvs/playwright/bin/pip install -U pip playwright
|
|
38
81
|
~/.venvs/playwright/bin/playwright install chromium
|
|
39
82
|
```
|
|
@@ -114,7 +157,7 @@ ls "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}" | grep -i chromium
|
|
|
114
157
|
[ "$(uname -s)" = Linux ] && { ps -ef | grep -q "[X]vfb.*:99" && echo "Xvfb :99 up" || echo "Xvfb down (headless면 §3)"; }
|
|
115
158
|
# launch smoke test (Node 또는 venv)
|
|
116
159
|
DISPLAY=${DISPLAY:-:99} npx playwright --version 2>/dev/null || \
|
|
117
|
-
~/.venvs/playwright/bin/python -c "from playwright.sync_api import sync_playwright as s; p=s().start(); b=p.chromium.launch(headless=True); print('launch OK'); b.close(); p.stop()"
|
|
160
|
+
(cd ~ && ~/.venvs/playwright/bin/python -c "from playwright.sync_api import sync_playwright as s; p=s().start(); b=p.chromium.launch(headless=True); print('launch OK'); b.close(); p.stop()")
|
|
118
161
|
```
|
|
119
162
|
4-field 보고: 변경(설치 항목·env 파일) / Evidence(위 출력) / 검증(launch OK) / Unknown(관리자 권한 라이브러리 미설치 등).
|
|
120
163
|
|
|
@@ -29,6 +29,25 @@ Get-Command git, pwsh -ErrorAction SilentlyContinue
|
|
|
29
29
|
에서는 `$null`(미정의)** 이다 - `$IsWindows` 로만 판정하면 진짜 Windows 를 비-Windows 로 오판해 no-op 한다. 보조 신호로만 쓴다.
|
|
30
30
|
- 비-Windows(PowerShell 없이 `uname` 만)면 **"Windows 전용 - 해당 없음"** 을 보고하고 멈춘다(no-op 도 유효한 결과).
|
|
31
31
|
|
|
32
|
+
### 네트워크 확인 (내려받기 전)
|
|
33
|
+
|
|
34
|
+
winget 의 기본 원천에 10초 안에 닿는지 먼저 본다. 닿지 않으면 winget 을 부르지 않고, 이미 설치된 pwsh 의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: PowerShell 7)" 으로 보고한다.
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
if ($env:CODEX_SANDBOX_NETWORK_DISABLED -eq '1') { 'Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)' }
|
|
38
|
+
if (Get-Command curl.exe -ErrorAction SilentlyContinue) {
|
|
39
|
+
curl.exe --ssl-no-revoke -sSI -o NUL --connect-timeout 5 --max-time 10 https://cdn.winget.microsoft.com/cache # winget 의 기본 원천(머리만)
|
|
40
|
+
"rc=$LASTEXITCODE" # 0 이면 닿음
|
|
41
|
+
} else { 'curl.exe 없음(확인하지 못함)' }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- PowerShell 5.1 의 `curl` 은 `Invoke-WebRequest` 별칭이라 `curl.exe` 로 부른다. `curl.exe` 는 Windows 시스템 프록시를 따르지 않고 `HTTPS_PROXY` 만 따른다. 시스템 프록시만 쓰는 망이면 `HTTPS_PROXY` 를 먼저 맞추고 본다.
|
|
45
|
+
- `--ssl-no-revoke` 는 폐지 목록 서버에 닿지 못하는 망에서 Schannel 의 폐지 확인 실패를 오프라인으로 읽지 않게 한다. 닿는지만 보고 받은 내용은 쓰지 않으므로 이 확인에만 준다. `curl.exe` 가 없으면 `$LASTEXITCODE` 에 앞 명령의 값이 남으므로 `Get-Command curl.exe -ErrorAction SilentlyContinue` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다(블록이 그렇게 한다).
|
|
46
|
+
- 사내 winget 원천을 쓰면(`winget source list`) 그 주소로 본다.
|
|
47
|
+
- 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'` 로 본다.
|
|
48
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
49
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
50
|
+
|
|
32
51
|
## 1. winget 설치
|
|
33
52
|
```powershell
|
|
34
53
|
winget install --id Microsoft.PowerShell -e
|
|
@@ -28,6 +28,36 @@ if (Get-Command pipx -ErrorAction SilentlyContinue) { pipx --version } else { "p
|
|
|
28
28
|
if (Get-Command uv -ErrorAction SilentlyContinue) { uv --version } else { "uv: 미설치" }
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
### 네트워크 확인 (내려받기 전)
|
|
32
|
+
|
|
33
|
+
이 스킬이 내려받는 원천에 10초 안에 닿는지 먼저 본다. 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
37
|
+
# pip 이 읽는 설정 전부(PIP_CONFIG_FILE 포함)를 홈 폴더에서 읽는다. install 절이 global 절보다 앞선다
|
|
38
|
+
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 미만). 사내 미러를 쓰면 확인 주소를 그 미러로 바꿔 본다"
|
|
39
|
+
I="${PIP_INDEX_URL:-$(printf '%s\n' "$L" | sed -n "s/^install\.index-url='\(.*\)'$/\1/p")}"
|
|
40
|
+
I="${I:-$(printf '%s\n' "$L" | sed -n "s/^global\.index-url='\(.*\)'$/\1/p")}"
|
|
41
|
+
I=$(printf %s "${I:-https://pypi.org/simple}" | sed -E 's#^([A-Za-z][A-Za-z0-9+.-]*://)[^/]*@#\1#; s#/$##') # 주소의 사용자 정보는 뗌
|
|
42
|
+
printf 'url = "%s"\n' "$I/pip/" | curl --ssl-no-revoke -K - -sSI -o /dev/null --connect-timeout 5 --max-time 10 # pipx, uv 패키지(PyPI)
|
|
43
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://astral.sh # uv 설치 스크립트
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `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 실측).
|
|
47
|
+
- PyPI 는 pip 이 쓰는 색인(`PIP_INDEX_URL`, `pip config list` 의 `install.index-url` 이나 `global.index-url`, 없으면 pypi.org)의 작은 쪽(`<색인>/pip/`)을 본다.
|
|
48
|
+
- `pip config get` 은 `PIP_CONFIG_FILE` 로 준 파일을 읽지 않아(pip 25.0 실측) `pip config list` 를 읽는다.
|
|
49
|
+
- Rocky 8 의 기본 pip(9.0.3, `pip3`)은 `config` 명령이 없고 `pip` 이름은 없다(Rocky 8.10 실측). 그래서 판 붙은 python(3.13, 3.12, 3.11)의 pip 도 차례로 본다.
|
|
50
|
+
- 홈 폴더에서 읽는다. `python3 -m pip` 는 지금 폴더를 import 경로 맨 앞에 두어, 지금 폴더에 있는 남의 저장소의 `pip/` 가 실행될 수 있다.
|
|
51
|
+
- bash 블록은 주소를 명령 인자가 아니라 표준 입력의 curl 설정(`-K -`)으로 넘긴다. 명령 인자는 같은 PC 의 다른 사용자가 `ps` 로 볼 수 있어, 경로나 질의에 든 토큰도 거기 싣지 않는다. 사용자 정보(`사용자:토큰@`)는 떼고 묻는다. 닿는지만 보므로 인증은 필요 없다(401 도 닿은 것이다). Windows 에서도 뗀 주소를 쓴다.
|
|
52
|
+
- 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` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다.
|
|
53
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
54
|
+
- OS 패키지 관리자(dnf, apt, brew, winget)의 단계는 자기 설정의 미러를 쓰므로 이 확인과 관계없이 시도하고, 실패하면 그 단계를 실패로 보고한다.
|
|
55
|
+
- 패킷을 버리거나 답하지 않는 망에서 dnf 는 저장소마다 약 2분(SYN 을 버리는 망 실측 121초), apt 는 원천마다 약 4분(답하지 않는 원천 실측 247초) 기다린 뒤 실패로 끝난다. 시간 제한은 관리자 설정을 따르므로 이 스킬이 바꾸지 않는다.
|
|
56
|
+
- `apt-get update` 는 받지 못해도 종료 0 이다. 출력의 `W: Failed to fetch` 가 있으면 그 단계를 실패로 보고한다.
|
|
57
|
+
- 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'` 로 본다.
|
|
58
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
59
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
60
|
+
|
|
31
61
|
## 1. Rocky Linux 8
|
|
32
62
|
```bash
|
|
33
63
|
sudo dnf -y module install python311/common
|
|
@@ -80,13 +110,15 @@ PY=$(command -v python3.11 || command -v python3.12 || command -v python3)
|
|
|
80
110
|
[ -f /etc/os-release ] && . /etc/os-release
|
|
81
111
|
case "${ID:-}${ID_LIKE:-}" in
|
|
82
112
|
*debian*|*ubuntu*) sudo apt-get install -y pipx ;; # python3-pip 동반, PEP668 무충돌
|
|
83
|
-
*) "$PY" -m pip install --user pipx \
|
|
84
|
-
|| "$PY" -m pip install --user pipx --break-system-packages ;; # RHEL8/macOS: 인터프리터에 pip
|
|
113
|
+
*) (cd ~ && "$PY" -m pip install --user pipx) \
|
|
114
|
+
|| (cd ~ && "$PY" -m pip install --user pipx --break-system-packages) ;; # RHEL8/macOS: 인터프리터에 pip 번들. 홈에서: -m 은 지금 폴더의 모듈을 먼저 불러온다
|
|
85
115
|
esac
|
|
86
|
-
command -v pipx >/dev/null && pipx ensurepath || "$PY" -m pipx ensurepath
|
|
116
|
+
command -v pipx >/dev/null && pipx ensurepath || (cd ~ && "$PY" -m pipx ensurepath)
|
|
87
117
|
# macOS 대안: brew install pipx && pipx ensurepath
|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
T=$(mktemp -d) # uv: macOS/Linux 공통, 항상 최신 fetch(버전 고정 불필요)
|
|
120
|
+
curl -LsSf --connect-timeout 10 --max-time 120 -o "$T/uv-install.sh" https://astral.sh/uv/install.sh && sh "$T/uv-install.sh"
|
|
121
|
+
rm -rf "$T" # 받았을 때만 실행(셸에 바로 넘기면 받기 실패도 종료 0)
|
|
90
122
|
|
|
91
123
|
RC_FILE="$HOME/.bashrc" # macOS는 $HOME/.zshrc
|
|
92
124
|
grep -q '### BANKER_PYTHON_BEGIN ###' "$RC_FILE" || cat >> "$RC_FILE" <<'EOF'
|
|
@@ -98,9 +130,15 @@ EOF
|
|
|
98
130
|
source "$RC_FILE"
|
|
99
131
|
```
|
|
100
132
|
```powershell
|
|
101
|
-
python -m pip install --user pipx
|
|
102
|
-
|
|
103
|
-
|
|
133
|
+
Push-Location ~; python -m pip install --user pipx; python -m pipx ensurepath; Pop-Location # 홈에서(지금 폴더의 pip/ 를 실행하지 않게)
|
|
134
|
+
$D = Join-Path $env:TEMP ([guid]::NewGuid()) # 예측할 수 없는 새 폴더(공용 TEMP 에서 남이 미리 둔 파일을 실행하지 않게)
|
|
135
|
+
try {
|
|
136
|
+
New-Item -ItemType Directory -ErrorAction Stop $D | Out-Null
|
|
137
|
+
$T = Join-Path $D "uv-install.ps1"
|
|
138
|
+
Invoke-RestMethod -TimeoutSec 60 -OutFile $T -ErrorAction Stop https://astral.sh/uv/install.ps1
|
|
139
|
+
powershell -ExecutionPolicy ByPass -File $T
|
|
140
|
+
if ($LASTEXITCODE -ne 0) { "uv 설치 스크립트가 실패했습니다(종료 $LASTEXITCODE)" }
|
|
141
|
+
} catch { "uv 설치 스크립트를 받지 못했습니다: $($_.Exception.Message)" } finally { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $D }
|
|
104
142
|
```
|
|
105
143
|
|
|
106
144
|
## 검증 (보고 의무)
|
|
@@ -31,6 +31,25 @@ git config --global --get-all safe.directory 2>/dev/null
|
|
|
31
31
|
[ -f ~/.claude/settings.json ] && grep -A3 '"sandbox"' ~/.claude/settings.json
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
### 네트워크 확인 (내려받기 전)
|
|
35
|
+
|
|
36
|
+
이 스킬이 내려받는 원천에 10초 안에 닿는지 먼저 본다. 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
40
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://sh.rustup.rs # rust, cargo(4단계)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `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 실측).
|
|
44
|
+
- 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` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다.
|
|
45
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
46
|
+
- OS 패키지 관리자(dnf, apt, brew, winget)의 단계는 자기 설정의 미러를 쓰므로 이 확인과 관계없이 시도하고, 실패하면 그 단계를 실패로 보고한다.
|
|
47
|
+
- 패킷을 버리거나 답하지 않는 망에서 dnf 는 저장소마다 약 2분(SYN 을 버리는 망 실측 121초), apt 는 원천마다 약 4분(답하지 않는 원천 실측 247초) 기다린 뒤 실패로 끝난다. 시간 제한은 관리자 설정을 따르므로 이 스킬이 바꾸지 않는다.
|
|
48
|
+
- `apt-get update` 는 받지 못해도 종료 0 이다. 출력의 `W: Failed to fetch` 가 있으면 그 단계를 실패로 보고한다.
|
|
49
|
+
- 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'` 로 본다.
|
|
50
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
51
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
52
|
+
|
|
34
53
|
## 1. Linux - bubblewrap + socat + userns sysctl
|
|
35
54
|
### Rocky Linux 8 / RHEL8
|
|
36
55
|
```bash
|
|
@@ -87,7 +106,12 @@ command -v sandbox-exec >/dev/null && echo "Seatbelt 내장 OK"
|
|
|
87
106
|
|
|
88
107
|
## 4. 폴드인 - rust/cargo
|
|
89
108
|
```bash
|
|
90
|
-
command -v cargo >/dev/null ||
|
|
109
|
+
command -v cargo >/dev/null || {
|
|
110
|
+
T=$(mktemp -d)
|
|
111
|
+
curl --proto '=https' --tlsv1.2 -sSf --connect-timeout 10 --max-time 120 -o "$T/rustup-init.sh" https://sh.rustup.rs \
|
|
112
|
+
&& sh "$T/rustup-init.sh" -y # 받았을 때만 실행
|
|
113
|
+
rm -rf "$T"
|
|
114
|
+
}
|
|
91
115
|
source "$HOME/.cargo/env" 2>/dev/null
|
|
92
116
|
cargo --version
|
|
93
117
|
```
|
|
@@ -123,12 +147,12 @@ Claude Code 의 Bash 도구 샌드박스는 `~/.claude/settings.json` 의 `sandb
|
|
|
123
147
|
흔하므로(예: `docs-setup` 이 다루는 것과 같은 함정) 먼저 3.11+ 인터프리터를 고른다:
|
|
124
148
|
```bash
|
|
125
149
|
PY="$(command -v python3.12 || command -v python3.11 || command -v python3)"
|
|
126
|
-
"$PY" -c "import tomllib" 2>/dev/null || echo "⚠ tomllib 없음 - Python 3.11+ 설치 후 재시도(setup-python 참고)"
|
|
150
|
+
(cd ~ && "$PY" -c "import tomllib") 2>/dev/null || echo "⚠ tomllib 없음 - Python 3.11+ 설치 후 재시도(setup-python 참고)" # 홈에서: -c, -m, 표준 입력 스크립트는 지금 폴더의 모듈을 먼저 불러온다
|
|
127
151
|
```
|
|
128
152
|
그 `$PY` 로 패치한다. `sandbox_mode` 치환은 **첫 `[` 테이블 헤더 이전(root 영역)만** 대상으로 삼아
|
|
129
153
|
`[profiles.*]` 하위의 같은 이름 override(예: 프로필별 `read-only` 강제)를 절대 건드리지 않는다(멱등):
|
|
130
154
|
```bash
|
|
131
|
-
"$PY" <<'EOF'
|
|
155
|
+
(cd ~ && "$PY" <<'EOF' # 홈에서: 표준 입력 스크립트는 지금 폴더의 모듈을 먼저 불러온다
|
|
132
156
|
import re
|
|
133
157
|
from pathlib import Path
|
|
134
158
|
import tomllib
|
|
@@ -157,6 +181,7 @@ tomllib.loads(text) # 패치 후 재검증
|
|
|
157
181
|
CFG.write_text(text, encoding="utf-8")
|
|
158
182
|
print("✓ sandbox_mode=workspace-write, network_access=true")
|
|
159
183
|
EOF
|
|
184
|
+
)
|
|
160
185
|
```
|
|
161
186
|
|
|
162
187
|
## 검증 (보고 의무)
|
|
@@ -170,10 +195,10 @@ git config --global --get-all safe.directory
|
|
|
170
195
|
|
|
171
196
|
grep -n 'sandbox_mode\|network_access' ~/.codex/config.toml
|
|
172
197
|
PY="$(command -v python3.12 || command -v python3.11 || command -v python3)" # tomllib=3.11+
|
|
173
|
-
"$PY" -c "import tomllib; tomllib.loads(open('$HOME/.codex/config.toml', encoding='utf-8').read()); print('✓ TOML 유효')"
|
|
198
|
+
(cd ~ && "$PY" -c "import tomllib; tomllib.loads(open('$HOME/.codex/config.toml', encoding='utf-8').read()); print('✓ TOML 유효')")
|
|
174
199
|
timeout 30 codex sandbox -- echo sandbox-ok # Codex 자체 샌드박스 러너로 실제 실행 확인
|
|
175
200
|
|
|
176
|
-
python3 -m json.tool ~/.claude/settings.json >/dev/null && echo "settings.json 유효"
|
|
201
|
+
(cd ~ && python3 -m json.tool ~/.claude/settings.json) >/dev/null && echo "settings.json 유효"
|
|
177
202
|
# Claude Code sandbox 설정은 settings.json 로 확인(위 json.tool). 실제 동작은 세션에서 Bash 도구 실행으로 관찰
|
|
178
203
|
```
|
|
179
204
|
4-field 보고: 변경(설치 패키지·sysctl 파일·config.toml/settings.json 변경분) / Evidence(위 출력) /
|
|
@@ -12,6 +12,28 @@ Stitch(withgoogle) 디자인 생성 MCP 를 프록시로 Claude Code 에 붙인
|
|
|
12
12
|
- Node/npx 설치 확인(`node -v`).
|
|
13
13
|
- API key 발급: https://stitch.withgoogle.com/settings (사용자가 직접).
|
|
14
14
|
|
|
15
|
+
### 네트워크 확인 (내려받기 전)
|
|
16
|
+
|
|
17
|
+
이 스킬이 내려받는 원천에 닿는지 먼저 본다(curl 은 10초, npm 은 15초 안). 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
21
|
+
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"; }
|
|
22
|
+
in15 sh -c 'cd ~ && exec npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier' # npx 프록시
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `npm ping` 은 홈 폴더에서 부른다. 지금 폴더에 있는 프로젝트의 `.npmrc` 가 정한 레지스트리와 토큰을 따르지 않게 하려는 것이다.
|
|
26
|
+
- `npm ping` 은 15초 감시(`in15`) 안에서 부른다. 패킷을 버리는 망에서 npm 은 연결에 시간 제한을 두지 않아(`--fetch-timeout` 은 받는 중의 쉼만 잰다) OS 의 연결 재시도가 끝날 때까지 기다린다(Rocky 8.10 실측 130초). 15초 안에 끝나지 않으면 `15초 안에 끝나지 않아 멈춤(패킷을 버리는 망일 수 있음)` 과 종료 143 이다. `--no-update-notifier` 는 npm 이 제 판을 묻는 요청을 하나 더 내지 않게 한다.
|
|
27
|
+
- `npm ping` 이 실패하면 ping 에 답하지 않는 사내 미러일 수 있다. `(cd ~ && T=$(mktemp -d) && { npm view @_davideast/stitch-mcp version --fetch-retries=0 --fetch-timeout=10000 --cache "$T" --no-update-notifier; r=$?; rm -rf "$T"; exit $r; })` 도 실패해야 오프라인으로 본다. 이 스킬이 설치할 패키지(`@_davideast/stitch-mcp`)를 빈 캐시로 물으므로 옛 값이 나올 수 없다. ping 이 15초 감시에 걸려 멈췄으면(종료 143) 이 확인은 하지 않는다. 같은 레지스트리라 같은 시간을 또 기다린다.
|
|
28
|
+
- `npm` 이 없으면(명령 없음) 오프라인이 아니라 확인하지 못한 것이다. Node 를 먼저 갖춘다(`setup-node`).
|
|
29
|
+
- `npm ping` 은 npm 의 프록시 설정(`proxy`, `https-proxy`)을 따른다. 사내 미러만 열린 망이면 그 미러가 쓰이는지 npm 설정을 본다.
|
|
30
|
+
- Windows PowerShell 에서는 `npm ping` 은 `Push-Location ~; npm ping --fetch-retries=0 --fetch-timeout=10000 --no-update-notifier; Pop-Location` 처럼 홈에서 부르고 돌아온다(돌아오지 않으면 뒤 단계가 프로젝트가 아니라 홈에서 돈다). PowerShell 에는 15초 감시를 두지 않는다(Windows 의 연결 시간 제한은 약 21초로 알려져 있다. 미실측).
|
|
31
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
32
|
+
- Stitch 는 원격 서비스라 네트워크가 없으면 쓸 수 없다. 확인이 실패하면 등록하지 않고 "오프라인이라 설치하지 않음(필요한 것: 네트워크, Stitch 서비스)" 으로 보고한다.
|
|
33
|
+
- 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'` 로 본다.
|
|
34
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
35
|
+
- 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` 도 된다.
|
|
36
|
+
|
|
15
37
|
## 1. mac / linux — 프록시 스크립트 + MCP 등록
|
|
16
38
|
> key 는 채팅/기록에 남기지 말고, 사용자가 **자기 터미널**에서 `YOUR_STITCH_API_KEY` 를 실제 키로 교체.
|
|
17
39
|
```bash
|
|
@@ -28,6 +28,25 @@ Windows(PowerShell):
|
|
|
28
28
|
Get-Command tmux -ErrorAction SilentlyContinue
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
### 네트워크 확인 (내려받기 전)
|
|
32
|
+
|
|
33
|
+
이 스킬이 내려받는 원천에 10초 안에 닿는지 먼저 본다. 닿지 않으면 그 원천에서 받는 단계는 하지 않고, 이미 설치된 것의 확인만 한 뒤 "오프라인이라 설치하지 않음(필요한 것: <못 깐 것>)" 으로 보고한다.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
[ "${CODEX_SANDBOX_NETWORK_DISABLED:-}" = 1 ] && echo "Codex sandbox 가 네트워크를 막음(아래 실패는 오프라인이 아님)"
|
|
37
|
+
curl --ssl-no-revoke -sSI -o /dev/null --connect-timeout 5 --max-time 10 https://github.com # tmux 소스(Rocky8 빌드)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `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 실측).
|
|
41
|
+
- 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` 로 먼저 보고, 없으면 확인하지 못한 것으로 적는다.
|
|
42
|
+
- 사내 미러만 열린 망이면 확인 주소를 그 미러로 바꿔 본다.
|
|
43
|
+
- OS 패키지 관리자(dnf, apt, brew, winget)의 단계는 자기 설정의 미러를 쓰므로 이 확인과 관계없이 시도하고, 실패하면 그 단계를 실패로 보고한다.
|
|
44
|
+
- 패킷을 버리거나 답하지 않는 망에서 dnf 는 저장소마다 약 2분(SYN 을 버리는 망 실측 121초), apt 는 원천마다 약 4분(답하지 않는 원천 실측 247초) 기다린 뒤 실패로 끝난다. 시간 제한은 관리자 설정을 따르므로 이 스킬이 바꾸지 않는다.
|
|
45
|
+
- `apt-get update` 는 받지 못해도 종료 0 이다. 출력의 `W: Failed to fetch` 가 있으면 그 단계를 실패로 보고한다.
|
|
46
|
+
- 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'` 로 본다.
|
|
47
|
+
- 확인이 실패하면 보고에 그 명령, 종료 코드, stderr 첫 줄을 붙인다(예: `npm error code ETIMEDOUT`, `curl: (28) ...`). 원인을 '오프라인' 하나로 짐작하지 않는다.
|
|
48
|
+
- curl 의 종료 60(인증서 검증 실패)도 오프라인이 아니다. 망은 열려 있고, TLS 를 가로채는 사내 프록시의 인증서를 OS 가 믿지 않는 것이다. '인증서 문제로 설치하지 않음' 으로 보고한다.
|
|
49
|
+
|
|
31
50
|
## 1. OS별 설치
|
|
32
51
|
|
|
33
52
|
### Rocky Linux 8 / RHEL8 - 소스빌드 필수
|
|
@@ -37,10 +56,11 @@ Get-Command tmux -ErrorAction SilentlyContinue
|
|
|
37
56
|
# 이미 3.6a+ 면 재빌드 불필요(멱등)
|
|
38
57
|
/usr/local/bin/tmux -V 2>/dev/null | grep -qE '3\.[6-9]|[4-9]\.' && echo "이미 충족 - skip" || {
|
|
39
58
|
sudo dnf install -y libevent-devel ncurses-devel gcc make byacc
|
|
40
|
-
cd
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
T=$(mktemp -d) && cd "$T" \
|
|
60
|
+
&& curl -fL --connect-timeout 10 --max-time 300 -o tmux-3.6a.tar.gz https://github.com/tmux/tmux/releases/download/3.6a/tmux-3.6a.tar.gz \
|
|
61
|
+
&& tar xzf tmux-3.6a.tar.gz && cd tmux-3.6a \
|
|
62
|
+
&& ./configure && make && sudo make install # /usr/local/bin/tmux 로 설치됨
|
|
63
|
+
cd ~; rm -rf "$T"
|
|
44
64
|
}
|
|
45
65
|
```
|
|
46
66
|
> **`/usr/bin/tmux`(2.7)를 지우지 않는다** - PATH 상 `/usr/local/bin` 이 먼저 오는지만 확인해
|